mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add filtering and pagination to group members page (#23392)
Makes use of the new group members endpoint added in https://github.com/coder/coder/pull/23067
This commit is contained in:
+20
-1
@@ -29,6 +29,7 @@ import type {
|
||||
DeleteExternalAuthByIDResponse,
|
||||
DynamicParametersRequest,
|
||||
PostWorkspaceUsageRequest,
|
||||
UsersRequest,
|
||||
} from "./typesGenerated";
|
||||
import * as TypesGen from "./typesGenerated";
|
||||
|
||||
@@ -2134,10 +2135,28 @@ class ApiMethods {
|
||||
getGroup = async (
|
||||
organization: string,
|
||||
groupName: string,
|
||||
req: TypesGen.GroupRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TypesGen.Group> => {
|
||||
const response = await this.axios.get(
|
||||
const url = getURLWithSearchParams(
|
||||
`/api/v2/organizations/${organization}/groups/${groupName}`,
|
||||
req,
|
||||
);
|
||||
const response = await this.axios.get(url, { signal });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getGroupMembers = async (
|
||||
organization: string,
|
||||
groupName: string,
|
||||
filter?: UsersRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TypesGen.GroupMembersResponse> => {
|
||||
const url = getURLWithSearchParams(
|
||||
`/api/v2/organizations/${organization}/groups/${groupName}/members`,
|
||||
filter,
|
||||
);
|
||||
const response = await this.axios.get(url.toString(), { signal });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,14 @@ import { API } from "api/api";
|
||||
import type {
|
||||
CreateGroupRequest,
|
||||
Group,
|
||||
GroupMembersResponse,
|
||||
GroupRequest,
|
||||
PatchGroupRequest,
|
||||
UsersRequest,
|
||||
} from "api/typesGenerated";
|
||||
import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery";
|
||||
import type { QueryClient, UseQueryOptions } from "react-query";
|
||||
import { prepareQuery } from "utils/filters";
|
||||
|
||||
type GroupSortOrder = "asc" | "desc";
|
||||
|
||||
@@ -31,20 +36,64 @@ export const groupsByOrganization = (organization: string) => {
|
||||
} satisfies UseQueryOptions<Group[]>;
|
||||
};
|
||||
|
||||
export const getGroupQueryKey = (organization: string, groupName: string) => [
|
||||
const getRootGroupQueryKey = (organization: string, groupName: string) => [
|
||||
"organization",
|
||||
organization,
|
||||
"group",
|
||||
groupName,
|
||||
];
|
||||
|
||||
export const group = (organization: string, groupName: string) => {
|
||||
export const getGroupQueryKey = (
|
||||
organization: string,
|
||||
groupName: string,
|
||||
req: GroupRequest,
|
||||
) => {
|
||||
const base = getRootGroupQueryKey(organization, groupName);
|
||||
return [...base, req];
|
||||
};
|
||||
|
||||
export const group = (
|
||||
organization: string,
|
||||
groupName: string,
|
||||
req: GroupRequest,
|
||||
): UseQueryOptions<Group> => {
|
||||
return {
|
||||
queryKey: getGroupQueryKey(organization, groupName),
|
||||
queryFn: () => API.getGroup(organization, groupName),
|
||||
queryKey: getGroupQueryKey(organization, groupName, req),
|
||||
queryFn: ({ signal }) => API.getGroup(organization, groupName, req, signal),
|
||||
};
|
||||
};
|
||||
|
||||
export const getGroupMembersQueryKey = (
|
||||
organization: string,
|
||||
groupName: string,
|
||||
req?: UsersRequest,
|
||||
) => {
|
||||
const base = [...getRootGroupQueryKey(organization, groupName), "members"];
|
||||
return req ? [...base, req] : base;
|
||||
};
|
||||
|
||||
export function groupMembers(
|
||||
organization: string,
|
||||
groupName: string,
|
||||
searchParams: URLSearchParams,
|
||||
): UsePaginatedQueryOptions<GroupMembersResponse, UsersRequest> {
|
||||
return {
|
||||
searchParams,
|
||||
queryPayload: ({ limit, offset }) => {
|
||||
return {
|
||||
limit,
|
||||
offset,
|
||||
q: prepareQuery(searchParams.get("filter") ?? ""),
|
||||
};
|
||||
},
|
||||
|
||||
queryKey: ({ payload }) =>
|
||||
getGroupMembersQueryKey(organization, groupName, payload),
|
||||
queryFn: ({ payload, signal }) =>
|
||||
API.getGroupMembers(organization, groupName, payload, signal),
|
||||
};
|
||||
}
|
||||
|
||||
export type GroupsByUserId = Readonly<Map<string, readonly Group[]>>;
|
||||
|
||||
export function groupsByUserId() {
|
||||
@@ -183,7 +232,7 @@ const invalidateGroup = (
|
||||
queryKey: getGroupsByOrganizationQueryKey(organization),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGroupQueryKey(organization, groupName),
|
||||
queryKey: getRootGroupQueryKey(organization, groupName),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
+5
-5
@@ -57,8 +57,8 @@ const PRESET_FILTERS = [
|
||||
interface UsersFilterProps {
|
||||
filter: ReturnType<typeof useFilter>;
|
||||
error?: unknown;
|
||||
menus: {
|
||||
status: StatusFilterMenu;
|
||||
menus?: {
|
||||
status?: StatusFilterMenu;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,11 +69,11 @@ export const UsersFilter: FC<UsersFilterProps> = ({ filter, error, menus }) => {
|
||||
learnMoreLink={docs("/admin/users#user-filtering")}
|
||||
learnMoreLabel2="User status"
|
||||
learnMoreLink2={docs("/admin/users#user-status")}
|
||||
isLoading={menus.status.isInitializing}
|
||||
isLoading={menus?.status?.isInitializing ?? false}
|
||||
filter={filter}
|
||||
error={error}
|
||||
options={<StatusMenu {...menus.status} />}
|
||||
optionsSkeleton={<MenuSkeleton />}
|
||||
options={menus?.status && <StatusMenu {...menus.status} />}
|
||||
optionsSkeleton={menus?.status && <MenuSkeleton />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "components/DropdownMenu/DropdownMenu";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { UsersFilter } from "components/Filter/UsersFilter";
|
||||
import { LastSeen } from "components/LastSeen/LastSeen";
|
||||
import { PaginationContainer } from "components/PaginationWidget/PaginationContainer";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
@@ -27,10 +29,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "components/Table/Table";
|
||||
import {
|
||||
PaginationStatus,
|
||||
TableToolbar,
|
||||
} from "components/TableToolbar/TableToolbar";
|
||||
import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import { EllipsisVertical, UserPlusIcon } from "lucide-react";
|
||||
import { isEveryoneGroup } from "modules/groups";
|
||||
@@ -43,9 +41,11 @@ import type { GroupPageOutletContext } from "./GroupPage";
|
||||
const GroupMembersPage: FC = () => {
|
||||
const {
|
||||
group: groupData,
|
||||
members,
|
||||
organization,
|
||||
permissions,
|
||||
groupQuery,
|
||||
membersQuery,
|
||||
filterProps,
|
||||
} = useOutletContext<GroupPageOutletContext>();
|
||||
const queryClient = useQueryClient();
|
||||
const addMemberMutation = useMutation(addMember(queryClient, organization));
|
||||
@@ -57,87 +57,74 @@ const GroupMembersPage: FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1 pb-8">
|
||||
{canUpdateGroup && groupData && !isEveryoneGroup(groupData) && (
|
||||
<AddGroupMember
|
||||
isLoading={addMemberMutation.isPending}
|
||||
organizationId={groupData.organization_id}
|
||||
onSubmit={async (member, reset) => {
|
||||
try {
|
||||
await addMemberMutation.mutateAsync({
|
||||
groupId,
|
||||
userId: member.user_id,
|
||||
});
|
||||
reset();
|
||||
await groupQuery.refetch();
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "Failed to add member."), {
|
||||
description: getErrorDetail(error),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<TableToolbar>
|
||||
<PaginationStatus
|
||||
isLoading={false}
|
||||
showing={groupData?.members.length ?? 0}
|
||||
total={groupData?.members.length ?? 0}
|
||||
label="members"
|
||||
/>
|
||||
</TableToolbar>
|
||||
<div className="flex flex-row justify-between">
|
||||
<UsersFilter {...filterProps} />
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-2/5">User</TableHead>
|
||||
<TableHead className="w-3/5">Status</TableHead>
|
||||
<TableHead className="w-auto" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{canUpdateGroup && groupData && !isEveryoneGroup(groupData) && (
|
||||
<AddGroupMember
|
||||
isLoading={addMemberMutation.isPending}
|
||||
organizationId={groupData.organization_id}
|
||||
onSubmit={async (member, reset) => {
|
||||
try {
|
||||
await addMemberMutation.mutateAsync({
|
||||
groupId,
|
||||
userId: member.user_id,
|
||||
});
|
||||
reset();
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "Failed to add member."), {
|
||||
description: getErrorDetail(error),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TableBody>
|
||||
{groupData?.members.length === 0 ? (
|
||||
<PaginationContainer query={membersQuery} paginationUnitLabel="members">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message="No members yet"
|
||||
description="Add a member using the controls above"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead className="w-2/5">User</TableHead>
|
||||
<TableHead className="w-3/5">Status</TableHead>
|
||||
<TableHead className="w-auto" />
|
||||
</TableRow>
|
||||
) : (
|
||||
groupData?.members.map((member) => (
|
||||
<GroupMemberRow
|
||||
member={member}
|
||||
group={groupData}
|
||||
key={member.id}
|
||||
canUpdate={canUpdateGroup}
|
||||
onRemove={async () => {
|
||||
const mutation = removeMemberMutation.mutateAsync(
|
||||
{
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{members.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState message="No members found" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
members.map((member) => (
|
||||
<GroupMemberRow
|
||||
member={member}
|
||||
group={groupData}
|
||||
key={member.id}
|
||||
canUpdate={canUpdateGroup}
|
||||
onRemove={async () => {
|
||||
const mutation = removeMemberMutation.mutateAsync({
|
||||
groupId: groupData.id,
|
||||
userId: member.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
groupQuery.refetch();
|
||||
},
|
||||
},
|
||||
);
|
||||
toast.promise(mutation, {
|
||||
loading: `Removing member "${member.username}" from "${groupData.name}"...`,
|
||||
success: `Member "${member.username}" has been removed from "${groupData.name}" successfully.`,
|
||||
error: (error) => ({
|
||||
message: `Failed to remove member "${member.username}" from "${groupData.name}".`,
|
||||
description: getErrorDetail(error),
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
});
|
||||
toast.promise(mutation, {
|
||||
loading: `Removing member "${member.username}" from "${groupData.name}"...`,
|
||||
success: `Member "${member.username}" has been removed from "${groupData.name}" successfully.`,
|
||||
error: (error) => ({
|
||||
message: `Failed to remove member "${member.username}" from "${groupData.name}".`,
|
||||
description: getErrorDetail(error),
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PaginationContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import {
|
||||
MockDefaultOrganization,
|
||||
MockGroup,
|
||||
MockGroupWithoutMembers,
|
||||
MockOrganizationMember,
|
||||
MockOrganizationMember2,
|
||||
} from "testHelpers/entities";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { API } from "api/api";
|
||||
import { getGroupQueryKey, groupPermissionsKey } from "api/queries/groups";
|
||||
import {
|
||||
getGroupMembersQueryKey,
|
||||
getGroupQueryKey,
|
||||
groupPermissionsKey,
|
||||
} from "api/queries/groups";
|
||||
import { organizationMembersKey } from "api/queries/organizations";
|
||||
import { spyOn, userEvent, within } from "storybook/test";
|
||||
import {
|
||||
@@ -24,7 +29,7 @@ const meta: Meta<typeof GroupPage> = {
|
||||
location: {
|
||||
pathParams: {
|
||||
organization: MockDefaultOrganization.name,
|
||||
groupName: MockGroup.name,
|
||||
groupName: MockGroupWithoutMembers.name,
|
||||
},
|
||||
},
|
||||
routing: reactRouterOutlet(
|
||||
@@ -36,12 +41,31 @@ const meta: Meta<typeof GroupPage> = {
|
||||
};
|
||||
|
||||
const groupQuery = (data: unknown) => ({
|
||||
key: getGroupQueryKey(MockDefaultOrganization.name, MockGroup.name),
|
||||
key: getGroupQueryKey(
|
||||
MockDefaultOrganization.name,
|
||||
MockGroupWithoutMembers.name,
|
||||
{
|
||||
exclude_members: true,
|
||||
},
|
||||
),
|
||||
data,
|
||||
});
|
||||
|
||||
const groupMembersQuery = (data: unknown) => ({
|
||||
key: getGroupMembersQueryKey(
|
||||
MockDefaultOrganization.name,
|
||||
MockGroupWithoutMembers.name,
|
||||
{
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
q: "",
|
||||
},
|
||||
),
|
||||
data,
|
||||
});
|
||||
|
||||
const permissionsQuery = (data: unknown, id?: string) => ({
|
||||
key: groupPermissionsKey(id ?? MockGroup.id),
|
||||
key: groupPermissionsKey(id ?? MockGroupWithoutMembers.id),
|
||||
data,
|
||||
});
|
||||
|
||||
@@ -55,27 +79,62 @@ type Story = StoryObj<typeof GroupPage>;
|
||||
|
||||
export const LoadingGroup: Story = {
|
||||
parameters: {
|
||||
queries: [groupQuery(null), permissionsQuery({})],
|
||||
queries: [groupQuery(null), groupMembersQuery(null), permissionsQuery({})],
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadingGroupMembers: Story = {
|
||||
parameters: {
|
||||
queries: [
|
||||
groupQuery(MockGroupWithoutMembers),
|
||||
groupMembersQuery(null),
|
||||
permissionsQuery({}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const GroupError: Story = {
|
||||
beforeEach: () => {
|
||||
spyOn(API, "getGroup").mockRejectedValue(new Error("test group error"));
|
||||
spyOn(API, "getGroupMembers").mockResolvedValue({
|
||||
users: [],
|
||||
count: 0,
|
||||
});
|
||||
spyOn(API, "checkAuthorization").mockResolvedValue({});
|
||||
},
|
||||
};
|
||||
|
||||
export const GroupMembersError: Story = {
|
||||
beforeEach: () => {
|
||||
spyOn(API, "getGroup").mockResolvedValue(MockGroupWithoutMembers);
|
||||
spyOn(API, "getGroupMembers").mockRejectedValue(
|
||||
new Error("test group members error"),
|
||||
);
|
||||
spyOn(API, "checkAuthorization").mockResolvedValue({});
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadingPermissions: Story = {
|
||||
parameters: {
|
||||
queries: [groupQuery(MockGroup), permissionsQuery(null)],
|
||||
queries: [
|
||||
groupQuery(MockGroupWithoutMembers),
|
||||
groupMembersQuery({
|
||||
users: MockGroup.members,
|
||||
count: MockGroup.members.length,
|
||||
}),
|
||||
permissionsQuery(null),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const NoUpdatePermission: Story = {
|
||||
parameters: {
|
||||
queries: [
|
||||
groupQuery(MockGroup),
|
||||
groupQuery(MockGroupWithoutMembers),
|
||||
groupMembersQuery({
|
||||
users: MockGroup.members,
|
||||
count: MockGroup.members.length,
|
||||
}),
|
||||
permissionsQuery({ canUpdateGroup: false }),
|
||||
],
|
||||
},
|
||||
@@ -85,10 +144,14 @@ export const EveryoneGroup: Story = {
|
||||
parameters: {
|
||||
queries: [
|
||||
groupQuery({
|
||||
...MockGroup,
|
||||
...MockGroupWithoutMembers,
|
||||
// The everyone group has the same ID as the organization.
|
||||
id: MockDefaultOrganization.id,
|
||||
}),
|
||||
groupMembersQuery({
|
||||
users: MockGroup.members,
|
||||
count: MockGroup.members.length,
|
||||
}),
|
||||
permissionsQuery({ canUpdateGroup: true }, MockDefaultOrganization.id),
|
||||
],
|
||||
},
|
||||
@@ -96,7 +159,7 @@ export const EveryoneGroup: Story = {
|
||||
|
||||
export const MembersError: Story = {
|
||||
beforeEach() {
|
||||
spyOn(API, "getGroup").mockResolvedValue(MockGroup);
|
||||
spyOn(API, "getGroup").mockResolvedValue(MockGroupWithoutMembers);
|
||||
spyOn(API, "checkAuthorization").mockResolvedValue({
|
||||
canUpdateGroup: true,
|
||||
});
|
||||
@@ -104,6 +167,14 @@ export const MembersError: Story = {
|
||||
new Error("test members error"),
|
||||
);
|
||||
},
|
||||
parameters: {
|
||||
queries: [
|
||||
groupMembersQuery({
|
||||
users: MockGroup.members,
|
||||
count: MockGroup.members.length,
|
||||
}),
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
@@ -115,10 +186,8 @@ export const MembersError: Story = {
|
||||
export const NoMembers: Story = {
|
||||
parameters: {
|
||||
queries: [
|
||||
groupQuery({
|
||||
...MockGroup,
|
||||
members: [],
|
||||
}),
|
||||
groupQuery(MockGroupWithoutMembers),
|
||||
groupMembersQuery({ users: [], count: 0 }),
|
||||
permissionsQuery({ canUpdateGroup: true }),
|
||||
membersQuery({ members: [] }),
|
||||
],
|
||||
@@ -134,7 +203,11 @@ export const NoMembers: Story = {
|
||||
export const FiltersByMembers: Story = {
|
||||
parameters: {
|
||||
queries: [
|
||||
groupQuery(MockGroup),
|
||||
groupQuery(MockGroupWithoutMembers),
|
||||
groupMembersQuery({
|
||||
users: MockGroup.members,
|
||||
count: MockGroup.members.length,
|
||||
}),
|
||||
permissionsQuery({ canUpdateGroup: true }),
|
||||
membersQuery({
|
||||
members: [MockOrganizationMember, MockOrganizationMember2],
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
import { getErrorDetail, getErrorMessage } from "api/errors";
|
||||
import { deleteGroup, group, groupPermissions } from "api/queries/groups";
|
||||
import type { Group } from "api/typesGenerated";
|
||||
import {
|
||||
deleteGroup,
|
||||
group,
|
||||
groupMembers,
|
||||
groupPermissions,
|
||||
} from "api/queries/groups";
|
||||
import type { Group, ReducedUser } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { useFilter } from "components/Filter/Filter";
|
||||
import type { UsersFilter } from "components/Filter/UsersFilter";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import type { PaginationResult } from "components/PaginationWidget/PaginationContainer";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "components/SettingsHeader/SettingsHeader";
|
||||
import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs";
|
||||
import { usePaginatedQuery } from "hooks/usePaginatedQuery";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { type ComponentProps, type FC, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { Outlet, useLocation, useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
Outlet,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
} from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { pageTitle } from "utils/page";
|
||||
|
||||
export type GroupPageOutletContext = {
|
||||
group: Group;
|
||||
members: readonly ReducedUser[];
|
||||
permissions: { canUpdateGroup: boolean };
|
||||
organization: string;
|
||||
groupQuery: ReturnType<typeof useQuery>;
|
||||
membersQuery: PaginationResult;
|
||||
filterProps: ComponentProps<typeof UsersFilter>;
|
||||
};
|
||||
|
||||
const GroupPage: FC = () => {
|
||||
@@ -33,7 +51,19 @@ const GroupPage: FC = () => {
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const groupQuery = useQuery(group(organization, groupName));
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const groupQuery = useQuery(
|
||||
group(organization, groupName, { exclude_members: true }),
|
||||
);
|
||||
const membersQuery = usePaginatedQuery(
|
||||
groupMembers(organization, groupName, searchParams),
|
||||
);
|
||||
const useFilterResult = useFilter({
|
||||
searchParams,
|
||||
onSearchParamsChange: setSearchParams,
|
||||
onUpdate: membersQuery.goToFirstPage,
|
||||
});
|
||||
|
||||
const groupData = groupQuery.data;
|
||||
const { data: permissions } = useQuery({
|
||||
...groupPermissions(groupData?.id ?? ""),
|
||||
@@ -43,7 +73,12 @@ const GroupPage: FC = () => {
|
||||
deleteGroup(queryClient, organization),
|
||||
);
|
||||
const [isDeletingGroup, setIsDeletingGroup] = useState(false);
|
||||
const isLoading = groupQuery.isLoading || !groupData || !permissions;
|
||||
const isLoading =
|
||||
groupQuery.isLoading ||
|
||||
!groupData ||
|
||||
!permissions ||
|
||||
membersQuery.isLoading ||
|
||||
!membersQuery.data;
|
||||
const canUpdateGroup = permissions ? permissions.canUpdateGroup : false;
|
||||
|
||||
const title = (
|
||||
@@ -52,8 +87,9 @@ const GroupPage: FC = () => {
|
||||
</title>
|
||||
);
|
||||
|
||||
if (groupQuery.error) {
|
||||
return <ErrorAlert error={groupQuery.error} />;
|
||||
const error = groupQuery.error || membersQuery.error;
|
||||
if (error) {
|
||||
return <ErrorAlert error={error} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
@@ -115,9 +151,14 @@ const GroupPage: FC = () => {
|
||||
context={
|
||||
{
|
||||
group: groupData,
|
||||
members: membersQuery.data?.users || [],
|
||||
permissions: { canUpdateGroup },
|
||||
organization,
|
||||
groupQuery,
|
||||
membersQuery,
|
||||
filterProps: {
|
||||
filter: useFilterResult,
|
||||
},
|
||||
} satisfies GroupPageOutletContext
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { User } from "api/typesGenerated";
|
||||
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { useFilter } from "components/Filter/Filter";
|
||||
import { useStatusFilterMenu } from "components/Filter/UsersFilter";
|
||||
import { isNonInitialPage } from "components/PaginationWidget/utils";
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { usePaginatedQuery } from "hooks/usePaginatedQuery";
|
||||
@@ -26,7 +27,6 @@ import { toast } from "sonner";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { generateRandomString } from "utils/random";
|
||||
import { ResetPasswordDialog } from "./ResetPasswordDialog";
|
||||
import { useStatusFilterMenu } from "./UsersFilter";
|
||||
import { UsersPageView } from "./UsersPageView";
|
||||
|
||||
type UserPageProps = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GroupsByUserId } from "api/queries/groups";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { UsersFilter } from "components/Filter/UsersFilter";
|
||||
import {
|
||||
PaginationContainer,
|
||||
type PaginationResult,
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
import { UserPlusIcon } from "lucide-react";
|
||||
import type { ComponentProps, FC } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import { UsersFilter } from "./UsersFilter";
|
||||
import { UsersTable } from "./UsersTable/UsersTable";
|
||||
|
||||
interface UsersPageViewProps {
|
||||
|
||||
@@ -2980,6 +2980,20 @@ export const MockGroup: TypesGen.Group = {
|
||||
total_member_count: 2,
|
||||
};
|
||||
|
||||
export const MockGroupWithoutMembers: TypesGen.Group = {
|
||||
id: "fbd2116a-8961-4954-87ae-e4575bd29ce0",
|
||||
name: "Front-End",
|
||||
display_name: "Front-End",
|
||||
avatar_url: "https://example.com",
|
||||
organization_id: MockOrganization.id,
|
||||
organization_name: MockOrganization.name,
|
||||
organization_display_name: MockOrganization.display_name,
|
||||
members: [],
|
||||
quota_allowance: 5,
|
||||
source: "user",
|
||||
total_member_count: 2,
|
||||
};
|
||||
|
||||
export const MockGroup2: TypesGen.Group = {
|
||||
id: "13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e2",
|
||||
name: "developer",
|
||||
|
||||
Reference in New Issue
Block a user