Add attributes and teams

This commit is contained in:
Nate Kelley 2025-01-21 11:48:59 -07:00
parent 78ffb7f8c5
commit 395b1773e0
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
8 changed files with 369 additions and 41 deletions

View File

@ -15,9 +15,9 @@ export interface BusterUserDataset {
}
export interface BusterUserAttribute {
id: string;
name: string;
user_id: string;
value: string | number | boolean;
read_only: boolean;
}
export interface BusterUserTeamListItem {

View File

@ -0,0 +1,40 @@
import { TeamRole } from '@/api';
import { Select } from 'antd';
import React from 'react';
const options: { label: string; value: TeamRole }[] = [
{
label: 'Manager',
value: TeamRole.MANAGER
},
{
label: 'Member',
value: TeamRole.MEMBER
},
{
label: 'Not a Member',
value: TeamRole.NONE
}
];
export const PermissionAssignTeamRole: React.FC<{
role: TeamRole;
id: string;
onRoleChange: (data: { id: string; role: TeamRole }) => void;
}> = React.memo(({ role, id, onRoleChange }) => {
return (
<Select
options={options}
value={role}
onChange={(v) => {
onRoleChange({ id, role: v });
}}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
/>
);
});
PermissionAssignTeamRole.displayName = 'PermissionAssignTeamRole';

View File

@ -5,3 +5,4 @@ export * from './PermissionSearchAndListWrapper';
export * from './NewPermissionGroupModal';
export * from './PermissionAssignedCell';
export * from './PermissionAssignedButton';
export * from './PermissionAssignTeamRole';

View File

@ -66,7 +66,8 @@ export const UserSegments: React.FC<{
{
label: 'Attributes',
value: UserSegmentsApps.ATTRIBUTES,
link: createBusterRoute({ route: BusterRoutes.APP_SETTINGS_USERS_ID_ATTRIBUTES, userId })
link: createBusterRoute({ route: BusterRoutes.APP_SETTINGS_USERS_ID_ATTRIBUTES, userId }),
hide: true
},
{
label: 'Teams',

View File

@ -1,27 +1,66 @@
'use client';
import { Card } from 'antd';
import { createStyles } from 'antd-style';
import {
useGetDatasetGroup,
useGetUserAttributes,
useGetUserDatasetGroups,
useGetUserDatasets,
useGetUserPermissionGroups
} from '@/api/buster-rest';
import { useDebounceSearch } from '@/hooks';
import {
NewPermissionGroupModal,
PermissionSearchAndListWrapper
} from '@appComponents/PermissionComponents';
import React, { useMemo, useState } from 'react';
import { UserAttributesListContainer } from './UserAttributesListContainer';
import { Button } from 'antd';
import { useMemoizedFn } from 'ahooks';
import { AppMaterialIcons } from '@/components/icons';
const useStyles = createStyles(({ token }) => ({
container: {
border: `0.5px solid ${token.colorBorder}`,
borderRadius: token.borderRadius,
padding: token.padding
}
}));
export const UserAttributesController: React.FC<{ userId: string }> = ({ userId }) => {
const { data: attributes } = useGetUserAttributes({ userId });
const [isNewAttributeModalOpen, setIsNewAttributeModalOpen] = useState(false);
const { filteredItems, searchText, handleSearchChange } = useDebounceSearch({
items: attributes || [],
searchPredicate: (item, searchText) => item.name.toLowerCase().includes(searchText)
});
interface UserAttributesControllerProps {
userId: string;
}
const onCloseNewAttributeModal = useMemoizedFn(() => {
setIsNewAttributeModalOpen(false);
});
export function UserAttributesController({ userId }: UserAttributesControllerProps) {
const { styles } = useStyles();
const onOpenNewAttributeModal = useMemoizedFn(() => {
setIsNewAttributeModalOpen(true);
});
const NewAttributeButton: React.ReactNode = useMemo(() => {
return (
<Button
type="default"
icon={<AppMaterialIcons icon="add" />}
onClick={onOpenNewAttributeModal}>
New attribute
</Button>
);
}, []);
return (
<Card className={styles.container}>
<h1>Attributes</h1>
{/* TODO: Add attributes list component */}
</Card>
<>
<PermissionSearchAndListWrapper
searchText={searchText}
handleSearchChange={handleSearchChange}
searchPlaceholder="Search by attribute"
// searchChildren={NewAttributeButton}
>
<UserAttributesListContainer filteredAttributes={filteredItems} userId={userId} />
</PermissionSearchAndListWrapper>
<NewPermissionGroupModal
isOpen={isNewAttributeModalOpen}
onClose={onCloseNewAttributeModal}
datasetId={null}
/>
</>
);
}
};

View File

@ -0,0 +1,61 @@
import { BusterUserAttribute } from '@/api/buster-rest';
import {
BusterInfiniteList,
BusterListColumn,
BusterListRowItem,
EmptyStateList,
InfiniteListContainer
} from '@/components/list';
import React, { useMemo } from 'react';
export const UserAttributesListContainer: React.FC<{
filteredAttributes: BusterUserAttribute[];
userId: string;
}> = React.memo(({ filteredAttributes, userId }) => {
const columns: BusterListColumn[] = useMemo(
() => [
{
title: 'Name',
dataIndex: 'name',
width: 320
},
{
title: 'Value',
dataIndex: 'value'
}
],
[]
);
const rows: BusterListRowItem[] = useMemo(
() =>
filteredAttributes.map((attribute) => ({
id: attribute.name,
data: attribute
})),
[filteredAttributes]
);
return (
<InfiniteListContainer
// popupNode={
// <PermissionDatasetGroupSelectedPopup
// selectedRowKeys={selectedRowKeys}
// onSelectChange={setSelectedRowKeys}
// datasetId={datasetId}
// />
// }
>
<BusterInfiniteList
columns={columns}
rows={rows}
showHeader={true}
showSelectAll={false}
useRowClickSelectChange={true}
emptyState={<EmptyStateList text="No datasets found" />}
/>
</InfiniteListContainer>
);
});
UserAttributesListContainer.displayName = 'UserDatasetsListContainer';

View File

@ -1,27 +1,56 @@
'use client';
import { Card } from 'antd';
import { createStyles } from 'antd-style';
import { useGetUserTeams } from '@/api/buster-rest';
import { useDebounceSearch } from '@/hooks';
import {
NewPermissionGroupModal,
PermissionSearchAndListWrapper
} from '@appComponents/PermissionComponents';
import React, { useMemo, useState } from 'react';
import { Button } from 'antd';
import { useMemoizedFn } from 'ahooks';
import { AppMaterialIcons } from '@/components/icons';
import { UserTeamsListContainer } from './UserTeamsListContainer';
const useStyles = createStyles(({ token }) => ({
container: {
border: `0.5px solid ${token.colorBorder}`,
borderRadius: token.borderRadius,
padding: token.padding
}
}));
export const UserTeamsController: React.FC<{ userId: string }> = ({ userId }) => {
const { data: teams } = useGetUserTeams({ userId });
const [isNewTeamModalOpen, setIsNewTeamModalOpen] = useState(false);
const { filteredItems, searchText, handleSearchChange } = useDebounceSearch({
items: teams || [],
searchPredicate: (item, searchText) => item.name.toLowerCase().includes(searchText)
});
interface UserTeamsControllerProps {
userId: string;
}
const onCloseNewTeamModal = useMemoizedFn(() => {
setIsNewTeamModalOpen(false);
});
export function UserTeamsController({ userId }: UserTeamsControllerProps) {
const { styles } = useStyles();
const onOpenNewTeamModal = useMemoizedFn(() => {
setIsNewTeamModalOpen(true);
});
const NewTeamButton: React.ReactNode = useMemo(() => {
return (
<Button type="default" icon={<AppMaterialIcons icon="add" />} onClick={onOpenNewTeamModal}>
New team
</Button>
);
}, []);
return (
<Card className={styles.container}>
<h1>Teams</h1>
{/* TODO: Add teams list component */}
</Card>
<>
<PermissionSearchAndListWrapper
searchText={searchText}
handleSearchChange={handleSearchChange}
searchPlaceholder="Search by team name"
searchChildren={NewTeamButton}>
<UserTeamsListContainer filteredTeams={filteredItems} userId={userId} />
</PermissionSearchAndListWrapper>
<NewPermissionGroupModal
isOpen={isNewTeamModalOpen}
onClose={onCloseNewTeamModal}
datasetId={null}
/>
</>
);
}
};

View File

@ -0,0 +1,157 @@
import {
BusterUserTeamListItem,
TeamRole,
useUpdateUserDatasets,
useUpdateUserTeams,
type BusterUserPermissionGroup
} from '@/api/buster-rest';
import { PermissionAssignTeamRole } from '@appComponents/PermissionComponents';
import {
BusterInfiniteList,
BusterListColumn,
BusterListRowItem,
EmptyStateList,
InfiniteListContainer
} from '@/components/list';
import { BusterRoutes, createBusterRoute } from '@/routes';
import { useMemoizedFn, useWhyDidYouUpdate } from 'ahooks';
import React, { useMemo, useState } from 'react';
export const UserTeamsListContainer: React.FC<{
filteredTeams: BusterUserTeamListItem[];
userId: string;
}> = React.memo(({ filteredTeams, userId }) => {
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const { mutateAsync: updateUserTeams } = useUpdateUserTeams({
userId: userId
});
const onRoleChange = useMemoizedFn(async (params: { id: string; role: TeamRole }) => {
await updateUserTeams([params]);
});
const columns: BusterListColumn[] = useMemo(
() => [
{
title: 'Name',
dataIndex: 'name',
width: 270
},
{
title: 'Role',
dataIndex: 'assigned',
render: (assigned: boolean, permissionGroup: BusterUserTeamListItem) => {
return (
<div className="flex justify-end">
<PermissionAssignTeamRole
role={permissionGroup.role}
id={permissionGroup.id}
onRoleChange={onRoleChange}
/>
</div>
);
}
}
],
[]
);
const { managerTeams, memberTeams, notAMemberTeams } = useMemo(() => {
const result: {
managerTeams: BusterListRowItem[];
memberTeams: BusterListRowItem[];
notAMemberTeams: BusterListRowItem[];
} = filteredTeams.reduce<{
managerTeams: BusterListRowItem[];
memberTeams: BusterListRowItem[];
notAMemberTeams: BusterListRowItem[];
}>(
(acc, team) => {
const teamItem: BusterListRowItem = {
id: team.id,
data: team,
link: createBusterRoute({
route: BusterRoutes.APP_SETTINGS_USERS_ID,
userId: team.id
})
};
if (team.role === 'manager') {
acc.managerTeams.push(teamItem);
} else if (team.role === 'member') {
acc.memberTeams.push(teamItem);
} else {
acc.notAMemberTeams.push(teamItem);
}
return acc;
},
{
managerTeams: [] as BusterListRowItem[],
memberTeams: [] as BusterListRowItem[],
notAMemberTeams: [] as BusterListRowItem[]
}
);
return result;
}, [filteredTeams]);
const rows = useMemo(
() =>
[
{
id: 'header-manager',
data: {},
hidden: managerTeams.length === 0,
rowSection: {
title: 'Manager',
secondaryTitle: managerTeams.length.toString()
}
},
...managerTeams,
{
id: 'header-member',
data: {},
hidden: memberTeams.length === 0,
rowSection: {
title: 'Member',
secondaryTitle: memberTeams.length.toString()
}
},
...memberTeams,
{
id: 'header-not-assigned',
data: {},
hidden: notAMemberTeams.length === 0,
rowSection: {
title: 'Not a member',
secondaryTitle: notAMemberTeams.length.toString()
}
},
...notAMemberTeams
].filter((row) => !(row as any).hidden),
[managerTeams, memberTeams, notAMemberTeams]
);
return (
<InfiniteListContainer
// popupNode={
// <PermissionDatasetGroupSelectedPopup
// selectedRowKeys={selectedRowKeys}
// onSelectChange={setSelectedRowKeys}
// datasetId={datasetId}
// />
// }
>
<BusterInfiniteList
columns={columns}
rows={rows}
showHeader={false}
showSelectAll={false}
useRowClickSelectChange={true}
selectedRowKeys={selectedRowKeys}
onSelectChange={setSelectedRowKeys}
emptyState={<EmptyStateList text="No datasets found" />}
/>
</InfiniteListContainer>
);
});
UserTeamsListContainer.displayName = 'UserTeamsListContainer';