Skip to content

Commit

Permalink
Feat/admin tabs layout improvements (#125)
Browse files Browse the repository at this point in the history
* feat: add skeleton, table layout, fix bucket creation bug

* Fix: manual alert dialog control

* feat: improve selector, general layout fixes
  • Loading branch information
DeutscherDude authored Oct 18, 2024
1 parent effb140 commit 76980fc
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ export const createBucketBodyDTOSchema = Type.Object({
pattern: '^(?!.*\\..*)([a-z0-9])(?:[a-z0-9.-]*[a-z0-9])?$',

minLength: 3,
maxLength: 63,
/**
* JB
* Has to be 54, as we are adding `-previews` to a newly created bucket
* And we are causing an error down the drain
*/
maxLength: 54,
}),
});

Expand Down
43 changes: 43 additions & 0 deletions apps/frontend/src/modules/admin/components/AdminTabSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useNavigate } from "@tanstack/react-router";
import { TrashIcon, UserIcon } from '@heroicons/react/24/solid';
import { cn } from "../../../../@/lib/utils";
import { FC, useMemo } from "react";

interface AdminTabSelectorProps {
currentlySelected: 'bucket' | 'user' | 'none';
}

export const AdminTabSelector: FC<AdminTabSelectorProps> = ({ currentlySelected }) => {
const navigate = useNavigate();

const bucketSelected = useMemo(() => currentlySelected === 'bucket', [currentlySelected]);

const userSelected = useMemo(() => currentlySelected === 'user', [currentlySelected]);

return (
<div className="flex justify-center items-center gap-4">
<div
onClick={() =>
navigate({
to: '/admin/user',
})
}
className={cn("flex flex-col items-center gap-2 p-4 bg-primary-foreground rounded-md border border-primary cursor-pointer", userSelected && 'bg-muted border-accent pointer-events-none')}
>
<UserIcon className={cn("w-7 h-7 pointer-events-none", userSelected && "text-secondary-foreground")}></UserIcon>
<p className="text-primary pointer-events-none">Users</p>
</div>
<div
onClick={() =>
navigate({
to: '/admin/bucket',
})
}
className={cn("flex flex-col items-center gap-2 p-4 bg-primary-foreground rounded-md border border-primary cursor-pointer", bucketSelected && 'bg-muted border-accent pointer-events-none')}
>
<TrashIcon className={cn("w-7 h-7 pointer-events-none", bucketSelected && "text-secondary-foreground")}></TrashIcon>
<p className="text-primary pointer-events-none">Buckets</p>
</div>
</div>
)
}
66 changes: 66 additions & 0 deletions apps/frontend/src/modules/admin/components/BucketTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useQuery } from "@tanstack/react-query";
import { bucketTableColumns } from "../../bucket/components/bucketTableColumns/bucketTableColumns"
import { DataTable } from "../../common/components/dataTable/dataTable"
import { adminFindBucketsQueryOptions } from "../../bucket/api/admin/queries/adminFindBuckets/adminFindBucketsQueryOptions";
import { useMemo, useState } from "react";
import { userAccessTokenSelector, useUserTokensStore } from "../../core/stores/userTokens/userTokens";
import { DataSkeletonTable } from "../../common/components/dataTable/dataTableSkeleton";



export const BucketTable = () => {
const accessToken = useUserTokensStore(userAccessTokenSelector);
const [page, setPage] = useState(0);
const [pageSize] = useState(10);

const { data: buckets, isFetching } = useQuery({
...adminFindBucketsQueryOptions({
accessToken,
page,
pageSize,
}),
});

const pageCount = useMemo(() => {
return buckets?.metadata.totalPages;
}, [buckets?.metadata.totalPages]);

const skeletonSizes = [{
width: '8',
height: '8'
}, {
width: 'full',
height: '10',
}, {
width: 'full',
height: '10'
}];

return (
<>
{ isFetching &&
<DataSkeletonTable
columns={bucketTableColumns}
skeletonSizes={skeletonSizes}
pageIndex={page}
pageCount={pageCount}
pageSize={pageSize}
onPreviousPage={() => setPage(page - 1)}
onNextPage={() => setPage(page + 1)}
/>
}
{ !isFetching &&
<DataTable
columns={bucketTableColumns}
data={buckets?.data ?? []}
pageIndex={page}
pageCount={pageCount}
pageSize={pageSize}
filterLabel="Filter bucket name..."
onPreviousPage={() => setPage(page - 1)}
onNextPage={() => setPage(page + 1)}
/>
}
</>
)
}
19 changes: 19 additions & 0 deletions apps/frontend/src/modules/admin/components/BucketTopBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useState } from "react";
import { CreateBucketDialog } from "../../bucket/components/createBucketDialog/createBucketDialog"
import { AdminTabSelector } from "./AdminTabSelector"

export const BucketTopBar = () => {
const [createBucketDialogOpen, setCreateBucketDialogOpen] = useState(false);

return (
<div className="max-w-[40rem] md:max-w-screen-xl md:min-w-[50rem] flex flex-col w-full justify-center items-center px-4">
<AdminTabSelector currentlySelected="bucket" />
<div className="pt-4 w-full">
<CreateBucketDialog
dialogOpen={createBucketDialogOpen}
onOpenChange={setCreateBucketDialogOpen}
/>
</div>
</div>
);
}
52 changes: 52 additions & 0 deletions apps/frontend/src/modules/admin/components/UserTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useQuery } from "@tanstack/react-query";
import { DataTable } from "../../common/components/dataTable/dataTable"
import { userTableColumns } from "../../user/components/userTableColumns/userTableColumns"
import { adminFindUsersQueryOptions } from "../../user/api/admin/queries/findUsersQuery/findUsersQueryOptions";
import { userAccessTokenSelector, useUserTokensStore } from "../../core/stores/userTokens/userTokens";
import { useState } from "react";
import { DataSkeletonTable } from "../../common/components/dataTable/dataTableSkeleton";

export const UserTable = () => {
const [page, setPage] = useState(0);
const [pageSize] = useState(10);

const accessToken = useUserTokensStore(userAccessTokenSelector);
const { data: users, isFetching } = useQuery({
...adminFindUsersQueryOptions({
accessToken: accessToken as string,
}),
});

const skeletonSizes = [{
width: '8',
height: '8'
}, {
width: 'full',
height: '10',
}, {
width: 'full',
height: '10'
}, {
width: 'full',
height: '10'
}];

return (
<>
{!isFetching && <DataTable
columns={userTableColumns}
data={users?.data ?? []}
pageCount={1}
pageIndex={page}
pageSize={pageSize}
onPreviousPage={() => setPage(page - 1)}
onNextPage={() => setPage(page + 1)}
/>}
{isFetching && <DataSkeletonTable
columns={userTableColumns}
pageSize={pageSize}
skeletonSizes={skeletonSizes}
/>}
</>
)
}
20 changes: 20 additions & 0 deletions apps/frontend/src/modules/admin/components/UserTopBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useState } from "react";
import { CreateUserDialog } from "../../user/components/createUserDialog/createUserDialog"
import { AdminTabSelector } from "./AdminTabSelector"


export const UserTopBar = () => {
const [createUserDialogOpen, setCreateUserDialogOpen] = useState(false);

return (
<div className="max-w-[40rem] md:max-w-screen-xl md:min-w-[50rem] flex flex-col w-full justify-center items-center px-4">
<AdminTabSelector currentlySelected="user" />
<div className="pt-4 w-full">
<CreateUserDialog
onOpenChange={setCreateUserDialogOpen}
open={createUserDialogOpen}
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../../../../../@/components/ui/form';
import { Input } from '../../../../../@/components/ui/input';
import { useToast } from '../../../../../@/components/ui/use-toast';
import { useUserTokensStore } from '../../../core/stores/userTokens/userTokens';
import { userAccessTokenSelector, useUserTokensStore } from '../../../core/stores/userTokens/userTokens';
import { useCreateBucketMutation } from '../../api/admin/mutations/createBucketMutation/createBucketMutation';
import { adminFindBucketsQueryOptions } from '../../api/admin/queries/adminFindBuckets/adminFindBucketsQueryOptions';
import { BucketApiQueryKeys } from '../../api/bucketApiQueryKeys';
import { LoadingSpinner } from '../../../../../@/components/ui/loadingSpinner';

const bucketSchema = z
.string()
.min(3)
.max(63)
.max(54)
.regex(new RegExp(/^(?!.*\.\.)([a-z0-9])(?:[a-z0-9.-]*[a-z0-9])?$/, 'g'));

const createBucketSchema = z.object({
Expand All @@ -29,10 +30,9 @@ interface CreateBucketDialogProps {
}

export const CreateBucketDialog = ({ dialogOpen, onOpenChange }: CreateBucketDialogProps): JSX.Element => {
const accessToken = useUserTokensStore.getState().accessToken;
const accessToken = useUserTokensStore(userAccessTokenSelector);

const { toast } = useToast();

const queryClient = useQueryClient();

const createBucketForm = useForm({
Expand All @@ -48,8 +48,7 @@ export const CreateBucketDialog = ({ dialogOpen, onOpenChange }: CreateBucketDia
accessToken: accessToken as string,
}),
});

const { mutateAsync: createBucketMutation } = useCreateBucketMutation({});
const { mutateAsync: createBucketMutation, isPending: isCreating } = useCreateBucketMutation({});

const onCreateBucket = async (payload: z.infer<typeof createBucketSchema>): Promise<void> => {
try {
Expand All @@ -58,10 +57,7 @@ export const CreateBucketDialog = ({ dialogOpen, onOpenChange }: CreateBucketDia
bucketName: payload.bucketName.toLowerCase(),
});

// todo: add buckets invalidation

onOpenChange(false);

createBucketForm.reset();

await refetchBuckets();
Expand Down Expand Up @@ -120,10 +116,11 @@ export const CreateBucketDialog = ({ dialogOpen, onOpenChange }: CreateBucketDia
)}
/>
<Button
disabled={!createBucketForm.formState.isValid}
disabled={!createBucketForm.formState.isValid || isCreating }
type="submit"
>
Create
{ !isCreating && <>Create</> }
{ isCreating && <LoadingSpinner />}
</Button>
</form>
</Form>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';

import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
Expand All @@ -14,34 +12,24 @@ import {
} from '../../../../../@/components/ui/alert-dialog';
import { Button } from '../../../../../@/components/ui/button';
import { useToast } from '../../../../../@/components/ui/use-toast';
import { useUserTokensStore } from '../../../core/stores/userTokens/userTokens';
import { userAccessTokenSelector, useUserTokensStore } from '../../../core/stores/userTokens/userTokens';
import { useDeleteBucketMutation } from '../../api/admin/mutations/deleteBucketMutation/deleteBucketMutation';
import { adminFindBucketsQueryOptions } from '../../api/admin/queries/adminFindBuckets/adminFindBucketsQueryOptions';
import { BucketApiQueryKeys } from '../../api/bucketApiQueryKeys';
import { LoadingSpinner } from '../../../../../@/components/ui/loadingSpinner';

interface Props {
bucketName: string;
}

export const DeleteBucketDialog = ({ bucketName }: Props): JSX.Element => {
const { toast } = useToast();

const queryClient = useQueryClient();
const { mutateAsync: deleteBucketMutation, isPending: isDeleting } = useDeleteBucketMutation({});

const accessToken = useUserTokensStore.getState().accessToken;

const accessToken = useUserTokensStore(userAccessTokenSelector);
const [open, onOpenChange] = useState(false);

const [deleteBucketName, setDeleteBucketName] = useState('');

const { mutateAsync: deleteBucketMutation } = useDeleteBucketMutation({});

const { refetch: refetchBuckets } = useQuery({
...adminFindBucketsQueryOptions({
accessToken: accessToken as string,
}),
});

const onDeleteBucket = async (bucketName: string): Promise<void> => {
try {
await deleteBucketMutation({
Expand All @@ -59,11 +47,6 @@ export const DeleteBucketDialog = ({ bucketName }: Props): JSX.Element => {
return;
}
}

onOpenChange(false);

refetchBuckets();

await queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === BucketApiQueryKeys.adminFindBuckets,
});
Expand Down Expand Up @@ -92,16 +75,23 @@ export const DeleteBucketDialog = ({ bucketName }: Props): JSX.Element => {
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className='w-40'
>
Cancel
</Button>
<Button
className="bg-red-800 hover:bg-red-700"
type='button'
className="bg-red-800 hover:bg-red-700 w-40"
onClick={() => onDeleteBucket(deleteBucketName)}
disabled={isDeleting}
>
Delete
{!isDeleting && 'Delete'}
{isDeleting && <LoadingSpinner />}
</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
Expand Down
Loading

0 comments on commit 76980fc

Please sign in to comment.