add user permission list

This commit is contained in:
Nate Kelley 2025-01-22 10:08:43 -07:00
parent 30cb2a0ff5
commit 2f47b45b41
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
13 changed files with 483 additions and 34 deletions

View File

@ -1,4 +1,8 @@
import { useCreateReactQuery, useCreateReactMutation } from '@/api/createReactQuery';
import {
useCreateReactQuery,
useCreateReactMutation,
PREFETCH_STALE_TIME
} from '@/api/createReactQuery';
import {
getPermissionGroup,
createPermissionGroup,
@ -10,10 +14,14 @@ import {
getPermissionGroupDatasetGroups,
updatePermissionGroupUsers,
updatePermissionGroupDatasets,
updatePermissionGroupDatasetGroups
updatePermissionGroupDatasetGroups,
getPermissionGroupUsers_server,
getPermissionGroupDatasets_server,
getPermissionGroupDatasetGroups_server,
getPermissionGroup_server
} from './requests';
import { useMemoizedFn } from 'ahooks';
import { useQueryClient } from '@tanstack/react-query';
import { QueryClient, useQueryClient } from '@tanstack/react-query';
import {
GetPermissionGroupDatasetGroupsResponse,
GetPermissionGroupDatasetsResponse,
@ -100,10 +108,23 @@ export const useGetPermissionGroup = (permissionGroupId: string) => {
const queryFn = useMemoizedFn(() => getPermissionGroup({ id: permissionGroupId }));
return useCreateReactQuery({
queryKey: ['permission_group', permissionGroupId],
queryFn
queryFn,
staleTime: PREFETCH_STALE_TIME
});
};
export const prefetchPermissionGroup = async (
permissionGroupId: string,
queryClientProp?: QueryClient
) => {
const queryClient = queryClientProp || new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['permission_group', permissionGroupId],
queryFn: () => getPermissionGroup_server({ id: permissionGroupId })
});
return queryClient;
};
export const useDeletePermissionGroup = () => {
const queryClient = useQueryClient();
return useCreateReactMutation({
@ -133,26 +154,65 @@ export const useGetPermissionGroupUsers = (permissionGroupId: string) => {
const queryFn = useMemoizedFn(() => getPermissionGroupUsers({ id: permissionGroupId }));
return useCreateReactQuery({
queryKey: ['permission_group', permissionGroupId, 'users'],
queryFn
queryFn,
staleTime: PREFETCH_STALE_TIME
});
};
export const prefetchPermissionGroupUsers = async (
permissionGroupId: string,
queryClientProp?: QueryClient
) => {
const queryClient = queryClientProp || new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['permission_group', permissionGroupId, 'users'],
queryFn: () => getPermissionGroupUsers_server({ id: permissionGroupId })
});
return queryClient;
};
export const useGetPermissionGroupDatasets = (permissionGroupId: string) => {
const queryFn = useMemoizedFn(() => getPermissionGroupDatasets({ id: permissionGroupId }));
return useCreateReactQuery({
queryKey: ['permission_group', permissionGroupId, 'datasets'],
queryFn
queryFn,
staleTime: PREFETCH_STALE_TIME
});
};
export const prefetchPermissionGroupDatasets = async (
permissionGroupId: string,
queryClientProp?: QueryClient
) => {
const queryClient = queryClientProp || new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['permission_group', permissionGroupId, 'datasets'],
queryFn: () => getPermissionGroupDatasets_server({ id: permissionGroupId })
});
return queryClient;
};
export const useGetPermissionGroupDatasetGroups = (permissionGroupId: string) => {
const queryFn = useMemoizedFn(() => getPermissionGroupDatasetGroups({ id: permissionGroupId }));
return useCreateReactQuery({
queryKey: ['permission_group', permissionGroupId, 'dataset_groups'],
queryFn
queryFn,
staleTime: PREFETCH_STALE_TIME
});
};
export const prefetchPermissionGroupDatasetGroups = async (
permissionGroupId: string,
queryClientProp?: QueryClient
) => {
const queryClient = queryClientProp || new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['permission_group', permissionGroupId, 'dataset_groups'],
queryFn: () => getPermissionGroupDatasetGroups_server({ id: permissionGroupId })
});
return queryClient;
};
export const useUpdatePermissionGroupUsers = (permissionGroupId: string) => {
const queryClient = useQueryClient();
const mutationFn = useMemoizedFn((data: { id: string; assigned: boolean }[]) => {

View File

@ -1,3 +1,4 @@
import { serverFetch } from '@/api/createServerInstance';
import { mainApi } from '../instances';
import {
CreatePermissionGroupResponse,
@ -21,14 +22,18 @@ export const getPermissionGroup = async ({
return await mainApi.get(`/permission_groups/${id}`).then((res) => res.data);
};
export const updatePermissionGroups = async ({
id,
data
export const getPermissionGroup_server = async ({
id
}: {
id: string;
data: { id: string; name: string }[];
}): Promise<void> => {
return await mainApi.put(`/permission_groups/${id}`, data).then((res) => res.data);
}): Promise<GetPermissionGroupResponse> => {
return await serverFetch<GetPermissionGroupResponse>(`/permission_groups/${id}`);
};
export const updatePermissionGroups = async (
data: { id: string; name: string }[]
): Promise<void> => {
return await mainApi.put(`/permission_groups`, data).then((res) => res.data);
};
export const deletePermissionGroup = async ({ id }: { id: string }): Promise<void> => {
@ -49,8 +54,18 @@ export const getPermissionGroupUsers = async ({
id
}: {
id: string;
}): Promise<GetPermissionGroupUsersResponse> => {
return await mainApi.get(`/permission_groups/${id}/users`).then((res) => res.data);
}): Promise<GetPermissionGroupUsersResponse[]> => {
return await mainApi
.get<GetPermissionGroupUsersResponse[]>(`/permission_groups/${id}/users`)
.then((res) => res.data);
};
export const getPermissionGroupUsers_server = async ({
id
}: {
id: string;
}): Promise<GetPermissionGroupUsersResponse[]> => {
return await serverFetch<GetPermissionGroupUsersResponse[]>(`/permission_groups/${id}/users`);
};
export const getPermissionGroupDatasets = async ({
@ -61,6 +76,14 @@ export const getPermissionGroupDatasets = async ({
return await mainApi.get(`/permission_groups/${id}/datasets`).then((res) => res.data);
};
export const getPermissionGroupDatasets_server = async ({
id
}: {
id: string;
}): Promise<GetPermissionGroupDatasetsResponse> => {
return await serverFetch(`/permission_groups/${id}/datasets`);
};
export const getPermissionGroupDatasetGroups = async ({
id
}: {
@ -69,6 +92,14 @@ export const getPermissionGroupDatasetGroups = async ({
return await mainApi.get(`/permission_groups/${id}/dataset_groups`).then((res) => res.data);
};
export const getPermissionGroupDatasetGroups_server = async ({
id
}: {
id: string;
}): Promise<GetPermissionGroupDatasetGroupsResponse> => {
return await serverFetch(`/permission_groups/${id}/dataset_groups`);
};
export const updatePermissionGroupUsers = async ({
id,
data

View File

@ -13,6 +13,8 @@ export interface CreatePermissionGroupResponse extends GetPermissionGroupRespons
export interface GetPermissionGroupUsersResponse {
id: string;
assigned: boolean;
email: string;
name: string;
}
export interface GetPermissionGroupDatasetsResponse {

View File

@ -5,13 +5,9 @@ import {
EmptyStateList,
InfiniteListContainer
} from '@/components/list';
import { Card } from 'antd';
import React, { useMemo, useState } from 'react';
import { Text } from '@/components/text';
import React, { useMemo } from 'react';
import { BusterRoutes, createBusterRoute } from '@/routes';
import { ListUserItem } from '../../_components/ListContent';
import { GetPermissionGroupResponse } from '@/api/buster-rest';
import { ListEmptyState } from '../../_components/Lists/ListEmptyState';
export const ListPermissionGroupsComponent: React.FC<{
permissionGroups: GetPermissionGroupResponse[];
@ -20,7 +16,7 @@ export const ListPermissionGroupsComponent: React.FC<{
const columns: BusterListColumn[] = useMemo(
() => [
{
title: 'Name',
title: 'Title',
dataIndex: 'name'
}
],

View File

@ -0,0 +1,66 @@
'use client';
import { AppSegmented } from '@/components/segmented';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { createBusterRoute, BusterRoutes } from '@/routes/busterRoutes';
import { useDebounce } from 'ahooks';
import { Divider } from 'antd';
import React from 'react';
export enum PermissionSegmentsApps {
USERS = 'users',
DATASET_GROUPS = 'dataset-groups',
DATASETS = 'datasets'
}
const RouteToAppSegment: Record<string, PermissionSegmentsApps> = {
[BusterRoutes.SETTINGS_PERMISSION_GROUPS_ID_USERS]: PermissionSegmentsApps.USERS,
[BusterRoutes.SETTINGS_PERMISSION_GROUPS_ID_DATASET_GROUPS]:
PermissionSegmentsApps.DATASET_GROUPS,
[BusterRoutes.SETTINGS_PERMISSION_GROUPS_ID_DATASETS]: PermissionSegmentsApps.DATASETS
};
export const PermissionAppSegments: React.FC<{
permissionGroupId: string;
}> = ({ permissionGroupId }) => {
const route = useAppLayoutContextSelector((state) => state.currentRoute);
const debouncedRoute = useDebounce(route, { wait: 10 });
const value = RouteToAppSegment[debouncedRoute] || PermissionSegmentsApps.USERS;
const options = React.useMemo(
() => [
{
label: 'Users',
value: PermissionSegmentsApps.USERS,
link: createBusterRoute({
route: BusterRoutes.SETTINGS_PERMISSION_GROUPS_ID_USERS,
permissionGroupId
})
},
{
label: 'Dataset groups',
value: PermissionSegmentsApps.DATASET_GROUPS,
link: createBusterRoute({
route: BusterRoutes.SETTINGS_PERMISSION_GROUPS_ID_DATASET_GROUPS,
permissionGroupId
})
},
{
label: 'Datasets',
value: PermissionSegmentsApps.DATASETS,
link: createBusterRoute({
route: BusterRoutes.SETTINGS_PERMISSION_GROUPS_ID_DATASETS,
permissionGroupId
})
}
],
[permissionGroupId]
);
return (
<div className="flex flex-col space-y-2">
<AppSegmented value={value} options={options} />
<Divider />
</div>
);
};

View File

@ -0,0 +1,9 @@
import { BackButton } from '@/components/buttons/BackButton';
import { createBusterRoute, BusterRoutes } from '@/routes/busterRoutes';
export const UsersBackButton = ({}: {}) => {
const route = createBusterRoute({ route: BusterRoutes.SETTINGS_PERMISSION_GROUPS });
const text = 'Permission groups';
return <BackButton text={text} linkUrl={route} />;
};

View File

@ -0,0 +1,26 @@
'use client';
import { useGetPermissionGroup, useUpdatePermissionGroup } from '@/api/buster-rest';
import React from 'react';
import { EditableTitle } from '@/components/text';
import { useMemoizedFn } from 'ahooks';
export const PermissionGroupTitleAndDescription: React.FC<{
permissionGroupId: string;
}> = React.memo(({ permissionGroupId }) => {
const { data } = useGetPermissionGroup(permissionGroupId);
const { mutate: updatePermissionGroup } = useUpdatePermissionGroup();
const onChangeTitle = useMemoizedFn(async (name: string) => {
if (!name) return;
updatePermissionGroup([{ id: permissionGroupId, name }]);
});
return (
<div className="flex flex-col space-y-0.5">
<EditableTitle children={data?.name || ''} onChange={onChangeTitle} />
</div>
);
});
PermissionGroupTitleAndDescription.displayName = 'PermissionGroupTitleAndDescription';

View File

@ -0,0 +1,26 @@
import React from 'react';
import { PermissionGroupTitleAndDescription } from './PermissionGroupTitleAndDescription';
import { prefetchPermissionGroup } from '@/api/buster-rest';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { UsersBackButton } from './PermissionBackButton';
import { PermissionAppSegments } from './PermissionAppSegments';
export const PermissionGroupLayout: React.FC<{
children: React.ReactNode;
params: { permissionGroupId: string };
}> = async ({ children, params: { permissionGroupId } }) => {
const queryClient = await prefetchPermissionGroup(permissionGroupId);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="flex h-full flex-col space-y-5 overflow-y-auto px-12 py-12">
<UsersBackButton />
<PermissionGroupTitleAndDescription permissionGroupId={permissionGroupId} />
<PermissionAppSegments permissionGroupId={permissionGroupId} />
{children}
</div>
</HydrationBoundary>
);
};
export default PermissionGroupLayout;

View File

@ -0,0 +1,49 @@
'use client';
import { useGetPermissionGroupUsers } from '@/api';
import { AppMaterialIcons } from '@/components';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { useDebounceSearch } from '@/hooks/useDebounceSearch';
import { PermissionSearchAndListWrapper } from '@appComponents/PermissionComponents';
import { Button } from 'antd';
import React, { useMemo } from 'react';
import { PermissionGroupUsersListContainer } from './PermissionGroupUsersListContainer';
export const PermissionGroupUsersController: React.FC<{
permissionGroupId: string;
}> = ({ permissionGroupId }) => {
const { data } = useGetPermissionGroupUsers(permissionGroupId);
const onToggleInviteModal = useAppLayoutContextSelector((x) => x.onToggleInviteModal);
const { filteredItems, handleSearchChange, searchText } = useDebounceSearch({
items: data || [],
searchPredicate: (item, searchText) =>
item.email.includes(searchText) || item.name.includes(searchText)
});
const NewUserButton: React.ReactNode = useMemo(() => {
return (
<Button
type="default"
icon={<AppMaterialIcons icon="add" />}
onClick={() => onToggleInviteModal(true)}>
Invite user
</Button>
);
}, []);
return (
<>
<PermissionSearchAndListWrapper
searchText={searchText}
handleSearchChange={handleSearchChange}
searchPlaceholder="Search by user name or email..."
searchChildren={NewUserButton}>
<PermissionGroupUsersListContainer
filteredUsers={filteredItems}
permissionGroupId={permissionGroupId}
/>
</PermissionSearchAndListWrapper>
</>
);
};

View File

@ -0,0 +1,147 @@
import {
BusterUserDatasetGroup,
GetPermissionGroupUsersResponse,
useUpdatePermissionGroupUsers,
useUpdateUserDatasetGroups
} from '@/api/buster-rest';
import { PermissionAssignedCell } from '@/app/app/_components/PermissionComponents';
import {
BusterInfiniteList,
BusterListColumn,
BusterListRowItem,
EmptyStateList,
InfiniteListContainer
} from '@/components/list';
import { BusterRoutes, createBusterRoute } from '@/routes';
import { useMemoizedFn } from 'ahooks';
import React, { useMemo, useState } from 'react';
import pluralize from 'pluralize';
import { Text } from '@/components/text';
import { ListUserItem } from '@/app/app/_components/ListContent';
import { PermissionGroupUsersSelectedPopup } from './PermissionGroupUsersSelectedPopup';
export const PermissionGroupUsersListContainer: React.FC<{
filteredUsers: GetPermissionGroupUsersResponse[];
permissionGroupId: string;
}> = React.memo(({ filteredUsers, permissionGroupId }) => {
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const { mutateAsync: updatePermissionGroupUsers } =
useUpdatePermissionGroupUsers(permissionGroupId);
const onSelectAssigned = useMemoizedFn(async (params: { id: string; assigned: boolean }) => {
await updatePermissionGroupUsers([params]);
});
const columns: BusterListColumn[] = useMemo(
() => [
{
title: 'Name',
dataIndex: 'name',
render: (name: string, user: GetPermissionGroupUsersResponse) => {
return <ListUserItem name={name} email={user.email} />;
}
},
{
title: 'Assigned',
dataIndex: 'assigned',
width: 130 + 85,
render: (assigned: boolean, permissionGroup: GetPermissionGroupUsersResponse) => {
return (
<div className="flex justify-end">
<PermissionAssignedCell
id={permissionGroup.id}
assigned={assigned}
text="assigned"
onSelect={onSelectAssigned}
/>
</div>
);
}
}
],
[]
);
const { cannotQueryPermissionUsers, canQueryPermissionUsers } = useMemo(() => {
const result: {
cannotQueryPermissionUsers: BusterListRowItem[];
canQueryPermissionUsers: BusterListRowItem[];
} = filteredUsers.reduce<{
cannotQueryPermissionUsers: BusterListRowItem[];
canQueryPermissionUsers: BusterListRowItem[];
}>(
(acc, user) => {
const userItem: BusterListRowItem = {
id: user.id,
data: user,
link: createBusterRoute({
route: BusterRoutes.APP_SETTINGS_USERS_ID,
userId: user.id
})
};
if (user.assigned) {
acc.canQueryPermissionUsers.push(userItem);
} else {
acc.cannotQueryPermissionUsers.push(userItem);
}
return acc;
},
{
cannotQueryPermissionUsers: [] as BusterListRowItem[],
canQueryPermissionUsers: [] as BusterListRowItem[]
}
);
return result;
}, [filteredUsers]);
const rows = useMemo(
() =>
[
{
id: 'header-assigned',
data: {},
hidden: canQueryPermissionUsers.length === 0,
rowSection: {
title: 'Assigned',
secondaryTitle: canQueryPermissionUsers.length.toString()
}
},
...canQueryPermissionUsers,
{
id: 'header-not-assigned',
data: {},
hidden: cannotQueryPermissionUsers.length === 0,
rowSection: {
title: 'Not Assigned',
secondaryTitle: cannotQueryPermissionUsers.length.toString()
}
},
...cannotQueryPermissionUsers
].filter((row) => !(row as any).hidden),
[canQueryPermissionUsers, cannotQueryPermissionUsers]
);
return (
<InfiniteListContainer
popupNode={
<PermissionGroupUsersSelectedPopup
selectedRowKeys={selectedRowKeys}
onSelectChange={setSelectedRowKeys}
permissionGroupId={permissionGroupId}
/>
}>
<BusterInfiniteList
columns={columns}
rows={rows}
showHeader={false}
showSelectAll={false}
useRowClickSelectChange={false}
selectedRowKeys={selectedRowKeys}
onSelectChange={setSelectedRowKeys}
emptyState={<EmptyStateList text="No dataset groups found" />}
/>
</InfiniteListContainer>
);
});
PermissionGroupUsersListContainer.displayName = 'PermissionGroupUsersListContainer';

View File

@ -0,0 +1,31 @@
import { useUpdatePermissionGroupUsers, useUpdateUserDatasets } from '@/api/buster-rest';
import { PermissionAssignedButton } from '@/app/app/_components/PermissionComponents';
import { BusterListSelectedOptionPopupContainer } from '@/components/list';
import React from 'react';
export const PermissionGroupUsersSelectedPopup: React.FC<{
selectedRowKeys: string[];
onSelectChange: (selectedRowKeys: string[]) => void;
permissionGroupId: string;
}> = React.memo(({ selectedRowKeys, onSelectChange, permissionGroupId }) => {
const { mutateAsync: updatePermissionGroupUsers } =
useUpdatePermissionGroupUsers(permissionGroupId);
return (
<BusterListSelectedOptionPopupContainer
selectedRowKeys={selectedRowKeys}
onSelectChange={onSelectChange}
buttons={[
<PermissionAssignedButton
key="assign"
text="assigned"
selectedRowKeys={selectedRowKeys}
onSelectChange={onSelectChange}
onUpdate={updatePermissionGroupUsers}
/>
]}
/>
);
});
PermissionGroupUsersSelectedPopup.displayName = 'PermissionGroupUsersSelectedPopup';

View File

@ -1,3 +1,17 @@
export default function Page() {
return <div>Users</div>;
import { prefetchPermissionGroupUsers } from '@/api/buster-rest';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { PermissionGroupUsersController } from './PermissionGroupUsersController';
export default async function Page({
params: { permissionGroupId }
}: {
params: { permissionGroupId: string };
}) {
const queryClient = await prefetchPermissionGroupUsers(permissionGroupId);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PermissionGroupUsersController permissionGroupId={permissionGroupId} />
</HydrationBoundary>
);
}

View File

@ -1,13 +1,11 @@
'use client';
import { BackButton } from '@/components';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { createBusterRoute, BusterRoutes } from '@/routes';
import { createBusterRoute, BusterRoutes } from '@/routes/busterRoutes';
import { useMemo } from 'react';
export const UsersBackButton = ({}: {}) => {
const previousPath = useAppLayoutContextSelector((state) => state.previousPath);
const previousRoute = useAppLayoutContextSelector((state) => state.previousRoute);
//const previousRoute = useAppLayoutContextSelector((state) => state.previousRoute);
const {
route,
@ -16,17 +14,11 @@ export const UsersBackButton = ({}: {}) => {
route: string;
text: string;
} = useMemo(() => {
// if (previousPath) {
// return {
// route: previousPath,
// text: 'Users'
// };
// }
return {
route: createBusterRoute({ route: BusterRoutes.APP_SETTINGS_USERS }),
text: 'Users'
};
}, [previousRoute]);
}, []);
return <BackButton text={text} linkUrl={route} />;
};