diff --git a/web/src/api/buster-rest/permission_groups/queryRequests.ts b/web/src/api/buster-rest/permission_groups/queryRequests.ts index a5b9a7c34..084f0edb4 100644 --- a/web/src/api/buster-rest/permission_groups/queryRequests.ts +++ b/web/src/api/buster-rest/permission_groups/queryRequests.ts @@ -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 }[]) => { diff --git a/web/src/api/buster-rest/permission_groups/requests.ts b/web/src/api/buster-rest/permission_groups/requests.ts index 00cf63c88..2b4480dbc 100644 --- a/web/src/api/buster-rest/permission_groups/requests.ts +++ b/web/src/api/buster-rest/permission_groups/requests.ts @@ -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 => { - return await mainApi.put(`/permission_groups/${id}`, data).then((res) => res.data); +}): Promise => { + return await serverFetch(`/permission_groups/${id}`); +}; + +export const updatePermissionGroups = async ( + data: { id: string; name: string }[] +): Promise => { + return await mainApi.put(`/permission_groups`, data).then((res) => res.data); }; export const deletePermissionGroup = async ({ id }: { id: string }): Promise => { @@ -49,8 +54,18 @@ export const getPermissionGroupUsers = async ({ id }: { id: string; -}): Promise => { - return await mainApi.get(`/permission_groups/${id}/users`).then((res) => res.data); +}): Promise => { + return await mainApi + .get(`/permission_groups/${id}/users`) + .then((res) => res.data); +}; + +export const getPermissionGroupUsers_server = async ({ + id +}: { + id: string; +}): Promise => { + return await serverFetch(`/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 => { + 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 => { + return await serverFetch(`/permission_groups/${id}/dataset_groups`); +}; + export const updatePermissionGroupUsers = async ({ id, data diff --git a/web/src/api/buster-rest/permission_groups/responseInterfaces.ts b/web/src/api/buster-rest/permission_groups/responseInterfaces.ts index 80462d658..0b354df82 100644 --- a/web/src/api/buster-rest/permission_groups/responseInterfaces.ts +++ b/web/src/api/buster-rest/permission_groups/responseInterfaces.ts @@ -13,6 +13,8 @@ export interface CreatePermissionGroupResponse extends GetPermissionGroupRespons export interface GetPermissionGroupUsersResponse { id: string; assigned: boolean; + email: string; + name: string; } export interface GetPermissionGroupDatasetsResponse { diff --git a/web/src/app/app/settings/permission-groups/ListPermissionGroupsComponent.tsx b/web/src/app/app/settings/permission-groups/ListPermissionGroupsComponent.tsx index 90bf5ba6f..d8da876b6 100644 --- a/web/src/app/app/settings/permission-groups/ListPermissionGroupsComponent.tsx +++ b/web/src/app/app/settings/permission-groups/ListPermissionGroupsComponent.tsx @@ -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' } ], diff --git a/web/src/app/app/settings/permission-groups/[permissionGroupId]/PermissionAppSegments.tsx b/web/src/app/app/settings/permission-groups/[permissionGroupId]/PermissionAppSegments.tsx new file mode 100644 index 000000000..09fcdc16b --- /dev/null +++ b/web/src/app/app/settings/permission-groups/[permissionGroupId]/PermissionAppSegments.tsx @@ -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 = { + [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 ( +
+ + +
+ ); +}; diff --git a/web/src/app/app/settings/permission-groups/[permissionGroupId]/PermissionBackButton.tsx b/web/src/app/app/settings/permission-groups/[permissionGroupId]/PermissionBackButton.tsx new file mode 100644 index 000000000..7f3184306 --- /dev/null +++ b/web/src/app/app/settings/permission-groups/[permissionGroupId]/PermissionBackButton.tsx @@ -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 ; +}; diff --git a/web/src/app/app/settings/permission-groups/[permissionGroupId]/PermissionGroupTitleAndDescription.tsx b/web/src/app/app/settings/permission-groups/[permissionGroupId]/PermissionGroupTitleAndDescription.tsx new file mode 100644 index 000000000..c6849a6a1 --- /dev/null +++ b/web/src/app/app/settings/permission-groups/[permissionGroupId]/PermissionGroupTitleAndDescription.tsx @@ -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 ( +
+ +
+ ); +}); + +PermissionGroupTitleAndDescription.displayName = 'PermissionGroupTitleAndDescription'; diff --git a/web/src/app/app/settings/permission-groups/[permissionGroupId]/layout.tsx b/web/src/app/app/settings/permission-groups/[permissionGroupId]/layout.tsx new file mode 100644 index 000000000..e02bfe36d --- /dev/null +++ b/web/src/app/app/settings/permission-groups/[permissionGroupId]/layout.tsx @@ -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 ( + +
+ + + + {children} +
+
+ ); +}; + +export default PermissionGroupLayout; diff --git a/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/PermissionGroupUsersController.tsx b/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/PermissionGroupUsersController.tsx new file mode 100644 index 000000000..0e7f509ab --- /dev/null +++ b/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/PermissionGroupUsersController.tsx @@ -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 ( + + ); + }, []); + + return ( + <> + + + + + ); +}; diff --git a/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/PermissionGroupUsersListContainer.tsx b/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/PermissionGroupUsersListContainer.tsx new file mode 100644 index 000000000..4ae031e1c --- /dev/null +++ b/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/PermissionGroupUsersListContainer.tsx @@ -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([]); + 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 ; + } + }, + { + title: 'Assigned', + dataIndex: 'assigned', + width: 130 + 85, + render: (assigned: boolean, permissionGroup: GetPermissionGroupUsersResponse) => { + return ( +
+ +
+ ); + } + } + ], + [] + ); + + 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 ( + + }> + } + /> + + ); +}); + +PermissionGroupUsersListContainer.displayName = 'PermissionGroupUsersListContainer'; diff --git a/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/PermissionGroupUsersSelectedPopup.tsx b/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/PermissionGroupUsersSelectedPopup.tsx new file mode 100644 index 000000000..2f3c1bba3 --- /dev/null +++ b/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/PermissionGroupUsersSelectedPopup.tsx @@ -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 ( + + ]} + /> + ); +}); + +PermissionGroupUsersSelectedPopup.displayName = 'PermissionGroupUsersSelectedPopup'; diff --git a/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/page.tsx b/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/page.tsx index 6f68f3aea..e1d1d9da3 100644 --- a/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/page.tsx +++ b/web/src/app/app/settings/permission-groups/[permissionGroupId]/users/page.tsx @@ -1,3 +1,17 @@ -export default function Page() { - return
Users
; +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 ( + + + + ); } diff --git a/web/src/app/app/settings/users/[userId]/_LayoutHeaderAndSegment/UsersBackButton.tsx b/web/src/app/app/settings/users/[userId]/_LayoutHeaderAndSegment/UsersBackButton.tsx index 940f4a228..79c063b81 100644 --- a/web/src/app/app/settings/users/[userId]/_LayoutHeaderAndSegment/UsersBackButton.tsx +++ b/web/src/app/app/settings/users/[userId]/_LayoutHeaderAndSegment/UsersBackButton.tsx @@ -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 ; };