mirror of https://github.com/buster-so/buster.git
Add attributes and teams
This commit is contained in:
parent
78ffb7f8c5
commit
395b1773e0
|
@ -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 {
|
||||
|
|
|
@ -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';
|
|
@ -5,3 +5,4 @@ export * from './PermissionSearchAndListWrapper';
|
|||
export * from './NewPermissionGroupModal';
|
||||
export * from './PermissionAssignedCell';
|
||||
export * from './PermissionAssignedButton';
|
||||
export * from './PermissionAssignTeamRole';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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';
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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';
|
Loading…
Reference in New Issue