From f7be186b39957f07400cf8fa025841b360f0ed2a Mon Sep 17 00:00:00 2001 From: Jyrki Keisala Date: Thu, 24 Oct 2024 10:11:03 +0300 Subject: [PATCH] feat(ui): Add support for basic CR(U)D for users to admin section Add support for creating, listing and deleting users. Updating user data wasn't considered to be necessary at this point, so there's currently no back-end support for it, thus it is also omitted from the front-end. List users (username, first name, last name, email) in a data table which is paginated on client-side. Users can be added and deleted from this view, and because deleting a user might be considered a dangerous operation, deleting needs manual text input in the delete dialog. Signed-off-by: Jyrki Keisala --- ui/src/routes/_layout/admin/route.tsx | 8 +- ui/src/routes/_layout/admin/users/index.tsx | 205 ++++++++++++++++++++ 2 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 ui/src/routes/_layout/admin/users/index.tsx diff --git a/ui/src/routes/_layout/admin/route.tsx b/ui/src/routes/_layout/admin/route.tsx index 68d290a72..46b3ce6cf 100644 --- a/ui/src/routes/_layout/admin/route.tsx +++ b/ui/src/routes/_layout/admin/route.tsx @@ -18,7 +18,7 @@ */ import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; -import { Eye, KeyRound, ListVideo, UserPlus } from 'lucide-react'; +import { Eye, KeyRound, ListVideo, User } from 'lucide-react'; import { PageLayout } from '@/components/page-layout'; @@ -37,9 +37,9 @@ const Layout = () => { label: 'User Management', items: [ { - title: 'Create User', - to: '/admin/users/create-user', - icon: () => , + title: 'Users', + to: '/admin/users', + icon: () => , }, { title: 'Authorization', diff --git a/ui/src/routes/_layout/admin/users/index.tsx b/ui/src/routes/_layout/admin/users/index.tsx new file mode 100644 index 000000000..b37643045 --- /dev/null +++ b/ui/src/routes/_layout/admin/users/index.tsx @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2024 The ORT Server Authors (See ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import { useQueryClient } from '@tanstack/react-query'; +import { createFileRoute, Link } from '@tanstack/react-router'; +import { + createColumnHelper, + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { UserPlus } from 'lucide-react'; +import { useState } from 'react'; + +import { + useAdminServiceDeleteUserByUsername, + useAdminServiceGetUsersKey, +} from '@/api/queries'; +import { prefetchUseAdminServiceGetUsers } from '@/api/queries/prefetch'; +import { useAdminServiceGetUsersSuspense } from '@/api/queries/suspense'; +import { ApiError, User } from '@/api/requests'; +import { DataTable } from '@/components/data-table/data-table'; +import { DeleteDialog } from '@/components/delete-dialog'; +import { LoadingIndicator } from '@/components/loading-indicator'; +import { ToastError } from '@/components/toast-error'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { toast } from '@/lib/toast'; +import { paginationSchema } from '@/schemas'; + +const defaultPageSize = 10; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor('username', { + header: 'Username', + cell: ({ row }) => <>{row.original.username}, + }), + columnHelper.accessor('firstName', { + header: 'First name', + cell: ({ row }) => <>{row.original.firstName}, + }), + columnHelper.accessor('lastName', { + header: 'Last name', + cell: ({ row }) => <>{row.original.lastName}, + }), + columnHelper.accessor('email', { + header: 'Email address', + cell: ({ row }) => <>{row.original.email}, + }), + columnHelper.display({ + id: 'actions', + header: () =>
Actions
, + cell: function CellComponent({ row }) { + const queryClient = useQueryClient(); + const [openDelDialog, setOpenDelDialog] = useState(false); + + const { mutateAsync: delUser, isPending: isDelPending } = + useAdminServiceDeleteUserByUsername({ + onSuccess() { + setOpenDelDialog(false); + toast.info('Delete User', { + description: `User "${row.original.username}" deleted successfully.`, + }); + queryClient.invalidateQueries({ + queryKey: [useAdminServiceGetUsersKey], + }); + }, + onError(error: ApiError) { + toast.error(error.message, { + description: , + duration: Infinity, + cancel: { + label: 'Dismiss', + onClick: () => {}, + }, + }); + }, + }); + + return ( +
+ delUser({ username: row.original.username })} + isPending={isDelPending} + textConfirmation={true} + /> +
+ ); + }, + }), +]; + +const Users = () => { + const search = Route.useSearch(); + const pageIndex = search.page ? search.page - 1 : 0; + const pageSize = search.pageSize ? search.pageSize : defaultPageSize; + + const { data: users } = useAdminServiceGetUsersSuspense(); + + const table = useReactTable({ + data: users || [], + columns, + + state: { + pagination: { + pageIndex, + pageSize, + }, + }, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + return ( + + + Users + + These are all current users of the server. By clicking the delete + button in the action column you can delete users, and a written + confirmation is required to prevent accidental deletions. + +
+ + + + + + Add a new user to the server. Note that the username has to be + unique. + + +
+
+ + + { + return { + to: Route.to, + search: { ...search, page: currentPage }, + }; + }} + setPageSizeOptions={(size) => { + return { + to: Route.to, + search: { ...search, page: 1, pageSize: size }, + }; + }} + /> + + +
+ ); +}; + +export const Route = createFileRoute('/_layout/admin/users/')({ + validateSearch: paginationSchema, + loader: async ({ context }) => { + prefetchUseAdminServiceGetUsers(context.queryClient); + }, + component: Users, + pendingComponent: LoadingIndicator, +});