Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added all teams page #561

Merged
merged 4 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Components/Header/Components/AdminMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const AdminMenu: FC = () => {
</>
}
>
<LinkMenuItem link={getPageLink("allTeams")}>All Teams</LinkMenuItem>
<LinkMenuItem link={getPageLink("patterns")}>Patterns</LinkMenuItem>
<LinkMenuItem link={getPageLink("contacts")}>Contacts</LinkMenuItem>
<LinkMenuItem link={getPageLink("notifications")}>Notifier</LinkMenuItem>
Expand Down
4 changes: 3 additions & 1 deletion src/Components/ModalError/ModalError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ type FooterErrorProps = {
message?: string | null;
maxWidth?: string;
margin?: string | number;
padding?: string | number;
};

export default function ModalError({
message,
maxWidth,
margin,
padding,
}: FooterErrorProps): React.ReactElement | null {
return message ? (
<div className={cn("root")} style={{ margin }}>
<div className={cn("root")} style={{ margin, padding }}>
<div style={{ maxWidth }}>
<ErrorIcon /> {message}
</div>
Expand Down
42 changes: 42 additions & 0 deletions src/Components/Teams/TeamCard/TeamCard.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@import "~styles/variables.less";
@import "~styles/mixins.less";

.team-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: start;
color: #333;
position: relative;

.team-name {
overflow: hidden;
max-width: 100%;
font-size: 18px;
line-height: 24px;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100% - 12px);
}

.team-description {
overflow: hidden;
text-overflow: ellipsis;
max-height: 120px;
}

.team-id {
font-size: 14px;
color: #999;
}

.team-card-kebab {
position: absolute;
right: 4px;
}

.team-users {
margin-left: -4px;
}
}
131 changes: 131 additions & 0 deletions src/Components/Teams/TeamCard/TeamCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React, { FC, useState } from "react";
import TrashIcon from "@skbkontur/react-icons/Trash";
import EditIcon from "@skbkontur/react-icons/Edit";
import { Button, Kebab } from "@skbkontur/react-ui";
import { MenuItem } from "@skbkontur/react-ui/components/MenuItem";
import { useFyllyDeleteTeam } from "../../../hooks/useFullyDeleteTeam";
import WarningIcon from "@skbkontur/react-icons/Warning";
import { fullyDeleteTeamConfirmText } from "../../../helpers/teamOperationsConfirmMessages";
import ModalError from "../../../Components/ModalError/ModalError";
import { Users } from "../../../Components/Teams/Users";
import { useModal } from "../../../hooks/useModal";
import { TeamEditor } from "../../../Components/Teams/TeamEditor/TeamEditor";
import RouterLink from "../../../Components/RouterLink/RouterLink";
import { getPageLink } from "../../../Domain/Global";
import { Markdown } from "../../../Components/Markdown/Markdown";
import { Team } from "../../../Domain/Team";
import { Flexbox } from "../../Flexbox/FlexBox";
import classNames from "classnames/bind";

import styles from "./TeamCard.less";

const cn = classNames.bind(styles);

interface ITeamCardProps {
team: Team;
isDeleting: boolean;
onOpenDelete: () => void;
onCloseDelete: () => void;
}

export const TeamCard: FC<ITeamCardProps> = ({ team, isDeleting, onOpenDelete, onCloseDelete }) => {
const { name, description, id } = team;
const [error, setError] = useState<string>("");
flizzzzzard marked this conversation as resolved.
Show resolved Hide resolved
const { isModalOpen, openModal, closeModal } = useModal();

const handleConfirm = async () => {
try {
await handleFullyDeleteTeam();
onCloseDelete();
} catch (error) {
setError(error);
}
};

const {
handleFullyDeleteTeam,
isFetchingData,
isDeletingContacts,
isDeletingSubscriptions,
isDeletingUsers,
isDeletingTeam,
} = useFyllyDeleteTeam(id, !isDeleting);
flizzzzzard marked this conversation as resolved.
Show resolved Hide resolved

const confirmMessage = fullyDeleteTeamConfirmText(
isFetchingData,
isDeletingContacts,
isDeletingSubscriptions,
isDeletingUsers,
isDeletingTeam,
name
);

const isLoading =
isFetchingData ||
isDeletingContacts ||
isDeletingSubscriptions ||
isDeletingUsers ||
isDeletingTeam;

return (
<>
<div className={cn("team-card")}>
<Flexbox gap={5}>
<Kebab className={cn("team-card-kebab")} size="large">
<MenuItem
icon={<EditIcon />}
onClick={() => {
openModal();
flizzzzzard marked this conversation as resolved.
Show resolved Hide resolved
}}
>
Edit
</MenuItem>
<MenuItem icon={<TrashIcon />} onClick={onOpenDelete}>
Delete
</MenuItem>
</Kebab>
{isDeleting ? (
<>
flizzzzzard marked this conversation as resolved.
Show resolved Hide resolved
<Flexbox gap={8} align="center">
<WarningIcon size={40} />
{confirmMessage +
" If you are not a member, add yourself before deleting."}
<ModalError padding={"10px 16px"} margin={0} message={error} />
<Flexbox direction="row" gap={8}>
<Button
loading={isLoading}
onClick={handleConfirm}
use={"primary"}
width={100}
>
Confirm
</Button>
<Button disabled={isLoading} onClick={onCloseDelete}>
Cancel
</Button>
</Flexbox>
</Flexbox>
</>
) : (
<>
<div className={cn("team-name")}>{name}</div>
<div className={cn("team-id")}>{`id: ${id}`}</div>
<div className={cn("team-description")}>
{description && <Markdown markdown={description} />}
</div>
<Flexbox justify="space-between" direction="row">
<div className={cn("team-users")}>
<Users team={team} />
</div>
<RouterLink to={getPageLink("teamSettings", team.id)}>
Team settings
</RouterLink>
</Flexbox>
</>
)}
</Flexbox>
</div>
{isModalOpen && <TeamEditor team={team} onClose={closeModal} />}
</>
);
};
6 changes: 3 additions & 3 deletions src/Components/Teams/TeamEditor/TeamEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import styles from "./TeamEditor.less";
const cn = classNames.bind(styles);

interface ITeamEditorProps {
team?: Team;
team?: Team | null;
onClose: () => void;
}

Expand All @@ -30,11 +30,11 @@ export const TeamEditor: FC<ITeamEditorProps> = ({ team, onClose }: ITeamEditorP
const [error, setError] = useState<string | null>(null);
const { handleUpdateTeam, isUpdatingTeam } = useUpdateTeam(
validationRef,
team,
name,
description,
setError,
onClose
onClose,
team
);
const { handleAddTeam, isAddingTeam } = useAddTeam(
validationRef,
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Teams/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function Users({ team }: UsersProps): ReactElement {
onCollapse={handleCollapse}
>
{users?.length ? (
<Grid columns="20px 240px" gap="8px" margin="8px 0 0 8px">
<Grid columns="20px 110px" gap="8px" margin="8px 0 0 8px">
{users.map((userName) => (
<Fragment key={userName}>
<Confirm
Expand Down
8 changes: 8 additions & 0 deletions src/Containers/AllTeamsContainer/AllTeamsContainer.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@import "~styles/variables.less";
@import "~styles/mixins.less";

.teams-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
108 changes: 108 additions & 0 deletions src/Containers/AllTeamsContainer/AllTeamsContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useEffect, FC, useState } from "react";
import { Layout, LayoutContent, LayoutTitle } from "../../Components/Layout/Layout";
import { setDocumentTitle } from "../../helpers/setDocumentTitle";
import { useAppDispatch, useAppSelector } from "../../store/hooks";
import { UIState } from "../../store/selectors";
import { useGetAllTeamsQuery } from "../../services/TeamsApi";
import classNames from "classnames/bind";
import { Team } from "../../Domain/Team";
import { Flexbox } from "../../Components/Flexbox/FlexBox";
import { SearchInput } from "../../Components/TriggerInfo/Components/SearchInput/SearchInput";
import { useDebounce } from "../../hooks/useDebounce";
import { Paging } from "@skbkontur/react-ui/components/Paging";
import transformPageFromHumanToProgrammer from "../../logic/transformPageFromHumanToProgrammer";
import { Select } from "@skbkontur/react-ui/components/Select";
import { EmptyListText } from "../../Components/TriggerInfo/Components/EmptyListMessage/EmptyListText";
import { TeamCard } from "../../Components/Teams/TeamCard/TeamCard";

import styles from "./AllTeamsContainer.less";
import { setError } from "../../store/Reducers/UIReducer.slice";

const cn = classNames.bind(styles);

type SortDirection = "asc" | "desc";

const SORT_OPTIONS: Array<[SortDirection, string]> = [
["asc", "Name: A-Z"],
["desc", "Name: Z-A"],
];

const AllTeamsContainer: FC = () => {
const { error, isLoading } = useAppSelector(UIState);
const [deletingTeam, setDeletingTeam] = useState<Team | null>(null);
const [searchValue, setSearchValue] = useState<string>("");
const [activePage, setActivePage] = useState<number>(1);
sol-un marked this conversation as resolved.
Show resolved Hide resolved
const [sortDirection, setSortDirection] = useState<SortDirection>("asc");
const debouncedSearchMetric = useDebounce(searchValue, 500);
const dispatch = useAppDispatch();

const { data: teams } = useGetAllTeamsQuery({
page: transformPageFromHumanToProgrammer(activePage),
searchText: debouncedSearchMetric,
sort: sortDirection,
});

const pageCount = Math.ceil((teams?.total ?? 0) / (teams?.size ?? 1));

const handleSetSearchValue = (value: string) => {
dispatch(setError(null));
setSearchValue(value);
};

useEffect(() => {
setDocumentTitle("All Teams");
}, []);

return (
<Layout loading={isLoading} error={error}>
<LayoutContent>
<LayoutTitle>Teams</LayoutTitle>
<Flexbox gap={24}>
<Flexbox gap={16} direction="row">
<SearchInput
value={searchValue}
width={"100%"}
placeholder="Filter by team name or team id, regExp is supported"
onValueChange={handleSetSearchValue}
onClear={() => setSearchValue("")}
/>
<Select<SortDirection, string>
placeholder="Sort"
value={sortDirection}
renderItem={(_v, item) => item}
renderValue={(_v, item) => item}
onValueChange={setSortDirection}
items={SORT_OPTIONS}
/>
</Flexbox>
{teams?.list.length !== 0 ? (
flizzzzzard marked this conversation as resolved.
Show resolved Hide resolved
<div className={cn("teams-container")}>
{teams?.list.map((team) => (
<TeamCard
key={team.id}
team={team}
isDeleting={deletingTeam?.id === team.id}
onOpenDelete={() => setDeletingTeam(team)}
onCloseDelete={() => setDeletingTeam(null)}
/>
))}
</div>
) : (
<EmptyListText text={"There are no teams"} />
)}
{teams?.list.length !== 0 && (
sol-un marked this conversation as resolved.
Show resolved Hide resolved
<Paging
caption="Next page"
activePage={activePage}
pagesCount={pageCount}
onPageChange={setActivePage}
withoutNavigationHint
/>
)}
</Flexbox>
</LayoutContent>
</Layout>
);
};

export default AllTeamsContainer;
2 changes: 2 additions & 0 deletions src/Domain/Global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const PagesPaths = {
teams: "/teams",
team: "/teams/:teamId?",
contacts: "/contacts",
allTeams: "/teams/all",
};

export const PagesLinks = {
Expand All @@ -29,6 +30,7 @@ export const PagesLinks = {
team: "/teams/%id%",
docs: "//moira.readthedocs.org/",
contacts: "/contacts",
allTeams: "/teams/all",
};

export type PagePath = keyof typeof PagesPaths;
Expand Down
7 changes: 7 additions & 0 deletions src/Domain/Team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ export interface Team {
name: string;
description?: string;
}

export interface ITeamList {
list: Team[];
page: number;
size: number;
total: number;
}
2 changes: 2 additions & 0 deletions src/desktop.bundle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { AdminRoute } from "./PrivateRoutes/AdminRoute";
import TeamsContainer from "./Containers/TeamsContainer";
import { TeamContainer } from "./Containers/TeamContainer";
import { TeamSettingsPrivateRoute } from "./PrivateRoutes/TeamSettingsPrivateRoute";
import AllTeamsContainer from "./Containers/AllTeamsContainer/AllTeamsContainer";

import styles from "./desktop.less";

Expand Down Expand Up @@ -74,6 +75,7 @@ function Desktop() {
/>
<Route exact path={getPagePath("settings")} component={SettingsContainer} />
<Route exact path={getPagePath("teams")} component={TeamsContainer} />
<AdminRoute exact path={getPagePath("allTeams")} component={AllTeamsContainer} />
<AdminRoute exact path={getPagePath("team")} component={TeamContainer} />
<AdminRoute
exact
Expand Down
Loading
Loading