Skip to content

Commit

Permalink
Merge pull request #1769 from gettakaro/feature/roles-table-view
Browse files Browse the repository at this point in the history
Feat: add table view for roles
  • Loading branch information
emielvanseveren authored Nov 3, 2024
2 parents c67fd7e + 9f930d9 commit b7bc839
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 174 deletions.
27 changes: 27 additions & 0 deletions packages/web-main/src/components/TableListToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ToggleButtonGroup } from '@takaro/lib-components';
import { FC } from 'react';
import { AiOutlineTable as TableViewIcon, AiOutlineUnorderedList as ListViewIcon } from 'react-icons/ai';

export type ViewType = 'list' | 'table';
interface TableListToggleButtonProps {
onChange: (view: ViewType) => void;
value: ViewType;
}

export const TableListToggleButton: FC<TableListToggleButtonProps> = ({ value, onChange }) => {
return (
<ToggleButtonGroup
onChange={(val) => onChange(val as ViewType)}
exclusive={true}
orientation="horizontal"
defaultValue={value}
>
<ToggleButtonGroup.Button value="list" tooltip="List view">
<ListViewIcon size={20} />
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value="table" tooltip="Table view">
<TableViewIcon size={20} />
</ToggleButtonGroup.Button>
</ToggleButtonGroup>
);
};
110 changes: 4 additions & 106 deletions packages/web-main/src/components/cards/RoleCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,11 @@
import { FC, MouseEvent, useEffect, useState } from 'react';
import {
Button,
Card,
Chip,
Dialog,
Dropdown,
IconButton,
Tooltip,
ValueConfirmationField,
useTheme,
} from '@takaro/lib-components';
import { FC } from 'react';
import { Card, Chip, Tooltip } from '@takaro/lib-components';
import { Header, TitleContainer } from './style';
import { useNavigate } from '@tanstack/react-router';
import { useRoleRemove } from 'queries/role';
import { RoleOutputDTO } from '@takaro/apiclient';
import { InnerBody } from '../style';

import {
AiOutlineMenu as MenuIcon,
AiOutlineEdit as EditIcon,
AiOutlineDelete as DeleteIcon,
AiOutlineEye as ViewIcon,
} from 'react-icons/ai';
import { RoleActions } from 'routes/_auth/_global/roles';

export const RoleCard: FC<RoleOutputDTO> = ({ id, name, system }) => {
const [openDialog, setOpenDialog] = useState<boolean>(false);
const [valid, setValid] = useState<boolean>(false);
const theme = useTheme();
const navigate = useNavigate();
const { mutate, isPending: isDeleting, isSuccess } = useRoleRemove();

const handleOnEditClick = (e: MouseEvent): void => {
e.stopPropagation();
navigate({ to: '/roles/update/$roleId', params: { roleId: id } });
};
const handleOnDeleteClick = (e: MouseEvent) => {
e.stopPropagation();

if (e.shiftKey) {
handleOnDelete(e);
} else {
setOpenDialog(true);
}
};

const handleOnViewClick = (e: MouseEvent) => {
e.stopPropagation();
navigate({ to: '/roles/view/$roleId', params: { roleId: id } });
};

const handleOnDelete = (e: MouseEvent) => {
e.stopPropagation();
mutate({ roleId: id });
};

useEffect(() => {
if (isSuccess) {
setOpenDialog(false);
}
}, []);

return (
<>
<Card data-testid={`role-${name}`}>
Expand All @@ -76,62 +22,14 @@ export const RoleCard: FC<RoleOutputDTO> = ({ id, name, system }) => {
) : (
<span />
)}

<Dropdown>
<Dropdown.Trigger asChild>
<IconButton icon={<MenuIcon />} ariaLabel="Settings" />
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Menu.Group label="Actions">
<Dropdown.Menu.Item onClick={handleOnViewClick} icon={<ViewIcon />} label="View role" />
{name !== 'root' && (
<Dropdown.Menu.Item onClick={handleOnEditClick} icon={<EditIcon />} label="Edit role" />
)}
{!system && (
<Dropdown.Menu.Item
onClick={handleOnDeleteClick}
icon={<DeleteIcon fill={theme.colors.error} />}
label="Delete role"
/>
)}
</Dropdown.Menu.Group>

<Dropdown.Menu.Item onClick={() => {}} label="Manage users (coming soon)" disabled />
<Dropdown.Menu.Item onClick={() => {}} label="Manage players (coming soon)" disabled />
</Dropdown.Menu>
</Dropdown>
<RoleActions roleId={id} roleName={name} isSystem={system} />
</Header>
<TitleContainer>
<h3>Role: {name}</h3>
</TitleContainer>
</InnerBody>
</Card.Body>
</Card>

<Dialog open={openDialog} onOpenChange={setOpenDialog}>
<Dialog.Content>
<Dialog.Heading>Delete role</Dialog.Heading>
<Dialog.Body size="medium">
<p>
Are you sure you want to delete the role? To confirm, type <strong>{name}</strong>.
</p>
<ValueConfirmationField
value={name}
onValidChange={(v) => setValid(v)}
label="Role name"
id="deleteRoleConfirmation"
/>
<Button
isLoading={isDeleting}
onClick={handleOnDelete}
disabled={!valid}
fullWidth
text="Delete role"
color="error"
/>
</Dialog.Body>
</Dialog.Content>
</Dialog>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { AddCard, CardList, RoleCard } from 'components/cards';
import { useNavigate } from '@tanstack/react-router';
import { InfiniteScroll, Skeleton } from '@takaro/lib-components';
import { rolesInfiniteQueryOptions } from 'queries/role';
import { useInfiniteQuery } from '@tanstack/react-query';

export const RolesCardView = () => {
const {
data: roles,
isLoading,
isFetching,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
...rolesInfiniteQueryOptions({ sortBy: 'system', sortDirection: 'desc' }),
});
const navigate = useNavigate();

if (isLoading) {
return (
<CardList>
<Skeleton variant="rectangular" height="100%" width="100%" />
<Skeleton variant="rectangular" height="100%" width="100%" />
<Skeleton variant="rectangular" height="100%" width="100%" />
<Skeleton variant="rectangular" height="100%" width="100%" />
</CardList>
);
}

if (!roles) {
return 'Failed to load roles?';
}

return (
<>
<CardList>
{roles.pages
.flatMap((page) => page.data)
.map((role) => (
<RoleCard key={role.id} {...role} />
))}
<AddCard title="Role" onClick={() => navigate({ to: '/roles/create' })} />
</CardList>
<InfiniteScroll
isFetching={isFetching}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
isFetchingNextPage={isFetchingNextPage}
/>
</>
);
};
168 changes: 168 additions & 0 deletions packages/web-main/src/routes/_auth/_global/-roles/RolesTableView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { RoleOutputDTO, RoleSearchInputDTOSortDirectionEnum } from '@takaro/apiclient';
import { Chip, Skeleton, Table, Tooltip, useTableActions } from '@takaro/lib-components';
import { useQuery } from '@tanstack/react-query';
import { rolesQueryOptions } from 'queries/role';
import { FC, useState } from 'react';
import { createColumnHelper } from '@tanstack/react-table';
import { RoleActions } from '../roles';
import { Link } from '@tanstack/react-router';
import { playersQueryOptions } from 'queries/player';
import { usersQueryOptions } from 'queries/user';

export const RolesTableView = () => {
const { pagination, columnFilters, sorting, columnSearch } = useTableActions<RoleOutputDTO>();
const [quickSearchInput, setQuickSearchInput] = useState<string>('');

const { data, isLoading } = useQuery({
...rolesQueryOptions({
page: pagination.paginationState.pageIndex,
limit: pagination.paginationState.pageSize,
sortBy: sorting.sortingState[0]?.id,
sortDirection: sorting.sortingState[0]
? sorting.sortingState[0]?.desc
? RoleSearchInputDTOSortDirectionEnum.Desc
: RoleSearchInputDTOSortDirectionEnum.Asc
: undefined,
filters: {
id: columnFilters.columnFiltersState.find((filter) => filter.id === 'id')?.value,
name: columnFilters.columnFiltersState.find((filter) => filter.id === 'name')?.value,
},
search: {
id: columnSearch.columnSearchState.find((search) => search.id === 'id')?.value,
name: [
...(columnSearch.columnSearchState.find((search) => search.id === 'name')?.value ?? []),
quickSearchInput,
],
},
}),
});

const columnHelper = createColumnHelper<RoleOutputDTO>();
const columnDefs = [
columnHelper.accessor('id', {
id: 'id',
header: 'Id',
cell: (info) => info.getValue(),
enableColumnFilter: true,
meta: {
hideColumn: true,
},
}),
columnHelper.accessor('name', {
id: 'name',
header: 'Name',
cell: (info) => (
<Tooltip placement="right">
<Tooltip.Trigger asChild>
<Link className="underline" to="/roles/view/$roleId" params={{ roleId: info.row.original.id }}>
{info.getValue()}
</Link>
</Tooltip.Trigger>
<Tooltip.Content>View role</Tooltip.Content>
</Tooltip>
),
}),

columnHelper.accessor('system', {
id: 'system',
header: 'System',
cell: (info) =>
info.getValue() ? (
<Tooltip>
<Tooltip.Trigger>
<Chip variant="outline" color="primary" label="System" />
</Tooltip.Trigger>
<Tooltip.Content>System roles are managed by Takaro and cannot be deleted.</Tooltip.Content>
</Tooltip>
) : (
<Chip variant="outline" color="backgroundAccent" label="Custom" />
),
enableColumnFilter: false,
enableGlobalFilter: false,
}),
columnHelper.accessor('id', {
id: 'playerCount',
header: 'Players with this role',
cell: (info) => {
return <PlayerCount roleId={info.getValue()} />;
},
}),
columnHelper.accessor('id', {
id: 'userCount',
header: 'Users with this role',
cell: (info) => {
return <UserCount roleId={info.getValue()} />;
},
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
maxSize: 25,
cell: (info) => {
return (
<RoleActions
roleId={info.row.original.id}
roleName={info.row.original.name}
isSystem={info.row.original.system}
/>
);
},
}),
];

const p =
!isLoading && data
? {
paginationState: pagination.paginationState,
setPaginationState: pagination.setPaginationState,
pageOptions: pagination.getPageOptions(data),
}
: undefined;

return (
<Table
title="List of roles"
searchInputPlaceholder="Search by role name..."
onSearchInputChanged={setQuickSearchInput}
id="roles"
columns={columnDefs}
data={data?.data ?? []}
columnFiltering={columnFilters}
columnSearch={columnSearch}
sorting={sorting}
pagination={p}
/>
);
};

const PlayerCount: FC<{ roleId: string }> = ({ roleId }) => {
const { data, isPending } = useQuery({
...playersQueryOptions({ filters: { roleId: [roleId] } }),
});

if (isPending) {
return <Skeleton variant="text" width="50px" height="15px" />;
}

if (!data) {
return 'unknown';
}

return data.data.length;
};

const UserCount: FC<{ roleId: string }> = ({ roleId }) => {
const { data, isPending } = useQuery({
...usersQueryOptions({ filters: { roleId: [roleId] } }),
});

if (isPending) {
return <Skeleton variant="text" width="50px" height="15px" />;
}

if (!data) {
return 'unknown';
}

return data.data.length;
};
Loading

0 comments on commit b7bc839

Please sign in to comment.