diff --git a/src/Components/Header/Components/AdminMenu.tsx b/src/Components/Header/Components/AdminMenu.tsx
index e16cc858..c33acecf 100644
--- a/src/Components/Header/Components/AdminMenu.tsx
+++ b/src/Components/Header/Components/AdminMenu.tsx
@@ -35,6 +35,7 @@ export const AdminMenu: FC = () => {
>
}
>
+ All Teams
Patterns
Contacts
Notifier
diff --git a/src/Components/ModalError/ModalError.tsx b/src/Components/ModalError/ModalError.tsx
index 35a79c4b..670b8b8e 100644
--- a/src/Components/ModalError/ModalError.tsx
+++ b/src/Components/ModalError/ModalError.tsx
@@ -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 ? (
-
+
{message}
diff --git a/src/Components/Teams/Confirm.tsx b/src/Components/Teams/Confirm.tsx
index e97b07ca..8b3894a7 100644
--- a/src/Components/Teams/Confirm.tsx
+++ b/src/Components/Teams/Confirm.tsx
@@ -23,7 +23,7 @@ export function Confirm({
active = true,
}: ConfirmProps): ReactElement {
const [opened, setOpened] = useState(false);
- const [error, setError] = useState
("");
+ const [error, setError] = useState("");
const handleConfirm = async () => {
try {
diff --git a/src/Components/Teams/ConfirmFullTeamDeletion/ConfirmFullTeamDeletion.tsx b/src/Components/Teams/ConfirmFullTeamDeletion/ConfirmFullTeamDeletion.tsx
index fe486faf..1be0cbe0 100644
--- a/src/Components/Teams/ConfirmFullTeamDeletion/ConfirmFullTeamDeletion.tsx
+++ b/src/Components/Teams/ConfirmFullTeamDeletion/ConfirmFullTeamDeletion.tsx
@@ -5,7 +5,7 @@ import { Hovered, HoveredShow } from "../Hovered/Hovered";
import { Team } from "../../../Domain/Team";
import { Confirm } from "../Confirm";
import DeleteIcon from "@skbkontur/react-icons/Delete";
-import { useFyllyDeleteTeam } from "../../../hooks/useFullyDeleteTeam";
+import { useFullyDeleteTeam } from "../../../hooks/useFullyDeleteTeam";
import { fullyDeleteTeamConfirmText } from "../../../helpers/teamOperationsConfirmMessages";
interface IConfirmFullTeamDeleteionProps {
@@ -25,7 +25,7 @@ export const ConfirmFullTeamDeleteion: FC = ({
isDeletingSubscriptions,
isDeletingUsers,
isDeletingTeam,
- } = useFyllyDeleteTeam(teamId, !isConfirmOpened);
+ } = useFullyDeleteTeam(teamId, !isConfirmOpened);
const confirmMessage = fullyDeleteTeamConfirmText(
isFetchingData,
diff --git a/src/Components/Teams/TeamCard/TeamCard.less b/src/Components/Teams/TeamCard/TeamCard.less
new file mode 100644
index 00000000..aa6a7d8b
--- /dev/null
+++ b/src/Components/Teams/TeamCard/TeamCard.less
@@ -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;
+ }
+}
diff --git a/src/Components/Teams/TeamCard/TeamCard.tsx b/src/Components/Teams/TeamCard/TeamCard.tsx
new file mode 100644
index 00000000..96beed93
--- /dev/null
+++ b/src/Components/Teams/TeamCard/TeamCard.tsx
@@ -0,0 +1,126 @@
+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 { useFullyDeleteTeam } 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 = ({ team, isDeleting, onOpenDelete, onCloseDelete }) => {
+ const { name, description, id } = team;
+ const [error, setError] = useState("");
+ const { isModalOpen, openModal, closeModal } = useModal();
+
+ const handleConfirm = async () => {
+ try {
+ await handleFullyDeleteTeam();
+ onCloseDelete();
+ } catch (error) {
+ setError(error);
+ }
+ };
+
+ const {
+ handleFullyDeleteTeam,
+ isFetchingData,
+ isDeletingContacts,
+ isDeletingSubscriptions,
+ isDeletingUsers,
+ isDeletingTeam,
+ } = useFullyDeleteTeam(id, !isDeleting);
+
+ const confirmMessage = fullyDeleteTeamConfirmText(
+ isFetchingData,
+ isDeletingContacts,
+ isDeletingSubscriptions,
+ isDeletingUsers,
+ isDeletingTeam,
+ name
+ );
+
+ const isLoading =
+ isFetchingData ||
+ isDeletingContacts ||
+ isDeletingSubscriptions ||
+ isDeletingUsers ||
+ isDeletingTeam;
+
+ return (
+ <>
+
+
+
+ } onClick={openModal}>
+ Edit
+
+ } onClick={onOpenDelete}>
+ Delete
+
+
+ {isDeleting ? (
+ <>
+
+
+ {confirmMessage +
+ " If you are not a member, add yourself before deleting."}
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+ {name}
+ {`id: ${id}`}
+
+ {description && }
+
+
+
+
+
+
+ Team settings
+
+
+ >
+ )}
+
+
+ {isModalOpen && }
+ >
+ );
+};
diff --git a/src/Components/Teams/TeamEditor/TeamEditor.tsx b/src/Components/Teams/TeamEditor/TeamEditor.tsx
index 6139f81e..d9d49c65 100644
--- a/src/Components/Teams/TeamEditor/TeamEditor.tsx
+++ b/src/Components/Teams/TeamEditor/TeamEditor.tsx
@@ -18,7 +18,7 @@ import styles from "./TeamEditor.less";
const cn = classNames.bind(styles);
interface ITeamEditorProps {
- team?: Team;
+ team?: Team | null;
onClose: () => void;
}
@@ -30,11 +30,11 @@ export const TeamEditor: FC = ({ team, onClose }: ITeamEditorP
const [error, setError] = useState(null);
const { handleUpdateTeam, isUpdatingTeam } = useUpdateTeam(
validationRef,
- team,
name,
description,
setError,
- onClose
+ onClose,
+ team
);
const { handleAddTeam, isAddingTeam } = useAddTeam(
validationRef,
diff --git a/src/Components/Teams/Users.tsx b/src/Components/Teams/Users.tsx
index 00cc8b80..2d1f8c52 100644
--- a/src/Components/Teams/Users.tsx
+++ b/src/Components/Teams/Users.tsx
@@ -51,7 +51,7 @@ export function Users({ team }: UsersProps): ReactElement {
onCollapse={handleCollapse}
>
{users?.length ? (
-
+
{users.map((userName) => (
= [
+ ["asc", "Name: A-Z"],
+ ["desc", "Name: Z-A"],
+];
+
+const AllTeamsContainer: FC = () => {
+ const { error, isLoading } = useAppSelector(UIState);
+ const [deletingTeam, setDeletingTeam] = useState(null);
+ const [searchValue, setSearchValue] = useState("");
+ const [activePage, setActivePage] = useState(1);
+ const [sortDirection, setSortDirection] = useState("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 (
+
+
+ Teams
+
+
+ setSearchValue("")}
+ />
+
+ {teams?.list.length ? (
+
+ {teams?.list.map((team) => (
+ setDeletingTeam(team)}
+ onCloseDelete={() => setDeletingTeam(null)}
+ />
+ ))}
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default AllTeamsContainer;
diff --git a/src/Domain/Global.ts b/src/Domain/Global.ts
index 06564e99..3c274973 100644
--- a/src/Domain/Global.ts
+++ b/src/Domain/Global.ts
@@ -12,6 +12,7 @@ export const PagesPaths = {
teams: "/teams",
team: "/teams/:teamId?",
contacts: "/contacts",
+ allTeams: "/teams/all",
};
export const PagesLinks = {
@@ -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;
diff --git a/src/Domain/Team.ts b/src/Domain/Team.ts
index eb39d0ee..e54ad53e 100644
--- a/src/Domain/Team.ts
+++ b/src/Domain/Team.ts
@@ -3,3 +3,10 @@ export interface Team {
name: string;
description?: string;
}
+
+export interface ITeamList {
+ list: Team[];
+ page: number;
+ size: number;
+ total: number;
+}
diff --git a/src/desktop.bundle.tsx b/src/desktop.bundle.tsx
index 8155ab39..8c603df3 100644
--- a/src/desktop.bundle.tsx
+++ b/src/desktop.bundle.tsx
@@ -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";
@@ -74,6 +75,7 @@ function Desktop() {
/>
+
{
+export const useFullyDeleteTeam = (teamId: string, skip?: boolean) => {
const { data: teamSettings, isLoading: isGettingSettings } = useGetTeamSettingsQuery(
{ teamId, handleLoadingLocally: true },
{ skip }
@@ -61,7 +61,7 @@ export const useFyllyDeleteTeam = (teamId: string, skip?: boolean) => {
teamId,
handleLoadingLocally: true,
handleErrorLocally: true,
- });
+ }).unwrap();
};
const isFetchingData = isGettingTeamUsers || isGettingSettings;
diff --git a/src/hooks/useUpdateTeam.tsx b/src/hooks/useUpdateTeam.tsx
index 90d1a16d..05a6b276 100644
--- a/src/hooks/useUpdateTeam.tsx
+++ b/src/hooks/useUpdateTeam.tsx
@@ -5,11 +5,11 @@ import { validateForm } from "../helpers/validations";
export const useUpdateTeam = (
validationRef: React.RefObject,
- team: Team | undefined,
name: string,
description: string,
setError: (error: string) => void,
- onClose: () => void
+ onClose: () => void,
+ team?: Team | null
) => {
const [updateTeam, { isLoading: isUpdatingTeam }] = useUpdateTeamMutation();
diff --git a/src/services/BaseApi.ts b/src/services/BaseApi.ts
index 27984ad2..56bdf69c 100644
--- a/src/services/BaseApi.ts
+++ b/src/services/BaseApi.ts
@@ -70,7 +70,8 @@ export type TApiInvalidateTags =
| "UserTeams"
| "Team"
| "TriggerState"
- | "Trigger";
+ | "Trigger"
+ | "AllTeams";
export const BaseApi = createApi({
reducerPath: "BaseApi",
@@ -85,6 +86,7 @@ export const BaseApi = createApi({
"Team",
"TriggerState",
"Trigger",
+ "AllTeams",
],
baseQuery: customFetchBaseQuery,
endpoints: (builder) => ({
diff --git a/src/services/TeamsApi.ts b/src/services/TeamsApi.ts
index 254b579c..21fbccb1 100644
--- a/src/services/TeamsApi.ts
+++ b/src/services/TeamsApi.ts
@@ -2,8 +2,11 @@ import { SubscriptionCreateInfo } from "../Api/MoiraApi";
import { Contact, TeamContactCreateInfo } from "../Domain/Contact";
import { Settings } from "../Domain/Settings";
import { Subscription } from "../Domain/Subscription";
-import { Team } from "../Domain/Team";
+import { ITeamList, Team } from "../Domain/Team";
import { BaseApi, CustomBaseQueryArgs, TApiInvalidateTags } from "./BaseApi";
+import qs from "qs";
+
+const ALL_TEAMS_LIST_SIZE = 30;
export const TeamsApi = BaseApi.injectEndpoints({
endpoints: (builder) => ({
@@ -81,7 +84,7 @@ export const TeamsApi = BaseApi.injectEndpoints({
if (error) {
return [];
}
- return ["UserTeams", { type: "Team", id }];
+ return ["UserTeams", { type: "Team", id }, "AllTeams"];
},
}),
deleteTeam: builder.mutation>({
@@ -94,7 +97,7 @@ export const TeamsApi = BaseApi.injectEndpoints({
if (error) {
return [];
}
- return ["UserTeams"];
+ return ["UserTeams", "AllTeams"];
},
}),
getTeamUsers: builder.query>({
@@ -143,6 +146,37 @@ export const TeamsApi = BaseApi.injectEndpoints({
return tagsToInvalidate;
},
}),
+ getAllTeams: builder.query<
+ ITeamList,
+ CustomBaseQueryArgs<{
+ page: number;
+ searchText?: string | null;
+ sort?: "asc" | "desc" | null;
+ }>
+ >({
+ query: ({ page, searchText, sort }) => {
+ const params = qs.stringify(
+ {
+ p: page,
+ size: ALL_TEAMS_LIST_SIZE,
+ searchText,
+ sort,
+ },
+ { arrayFormat: "comma", skipNulls: true }
+ );
+ return {
+ url: `teams/all?${params}`,
+ method: "GET",
+ credentials: "same-origin",
+ };
+ },
+ providesTags: (_result, error) => {
+ if (error) {
+ return [];
+ }
+ return ["AllTeams"];
+ },
+ }),
}),
});
@@ -159,4 +193,5 @@ export const {
useGetTeamUsersQuery,
useAddUserToTeamMutation,
useDeleteUserFromTeamMutation,
+ useGetAllTeamsQuery,
} = TeamsApi;