From 3c23f956928d255ca4b2e48d40181fff07b33d2e Mon Sep 17 00:00:00 2001 From: maikb Date: Sun, 21 Jan 2024 23:01:52 +0100 Subject: [PATCH] feat(profiles): Add ProfileDataTable component and update tests Added a new 'ProfileDataTable' component to display user profiles. This component flattens the returned profile set for easy processing and rendering. Updates have also been made to the profiles and dashboard test files to accommodate this addition. Implemented breadcrumb navigation for the profiles page for improved user experience. Refs: #1 --- src/app/profiles/page.tsx | 22 ++- .../components/header/profile-nav.test.tsx | 6 +- .../components/profile-data-table.test.tsx | 33 ++++ .../components/profile-data-table.tsx | 185 ++++++++++++++++++ 4 files changed, 240 insertions(+), 6 deletions(-) create mode 100644 src/sections/profiles/components/profile-data-table.test.tsx create mode 100644 src/sections/profiles/components/profile-data-table.tsx diff --git a/src/app/profiles/page.tsx b/src/app/profiles/page.tsx index f347e73..9f85d2f 100644 --- a/src/app/profiles/page.tsx +++ b/src/app/profiles/page.tsx @@ -1,11 +1,29 @@ 'use client'; import React from 'react'; +import { useProfileContext } from '@/sections/dashboard/context/profile-context'; +import { ProfileDataTable } from '@/sections/profiles/components/profile-data-table'; +import { ProfileSet, profileSetSchema } from '@/modules/profiles/domain'; +import BreadCrumb from '@/components/breadcrumb'; -export default function Configuration() { +export default function Profiles() { + const { data, error, isLoading } = useProfileContext(); + + if (isLoading) { + return
Loading...
; // FIXME: Make more visually appealing + } + + if (error) { + throw new Error(error.message); // FIXME: Handle error + } + + const parsed: ProfileSet = profileSetSchema.parse(data); + + const breadcrumbItems = [{ title: 'Profiles', link: '/profile' }]; return (
- Profiles Page + +
); } diff --git a/src/sections/dashboard/components/header/profile-nav.test.tsx b/src/sections/dashboard/components/header/profile-nav.test.tsx index 71fcad3..73331b9 100644 --- a/src/sections/dashboard/components/header/profile-nav.test.tsx +++ b/src/sections/dashboard/components/header/profile-nav.test.tsx @@ -2,13 +2,11 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { randomFillSync } from 'crypto'; -import { - ProfileNav, - ProfileSet, -} from '@/sections/dashboard/components/header/profile-nav'; +import { ProfileNav } from '@/sections/dashboard/components/header/profile-nav'; import { clearMocks, mockIPC } from '@tauri-apps/api/mocks'; import { ProfileProvider } from '@/sections/dashboard/context/profile-context'; import { SWRConfig } from 'swr'; +import { ProfileSet } from '@/modules/profiles/domain'; describe('', () => { const profileSet: ProfileSet = { diff --git a/src/sections/profiles/components/profile-data-table.test.tsx b/src/sections/profiles/components/profile-data-table.test.tsx new file mode 100644 index 0000000..e769cb3 --- /dev/null +++ b/src/sections/profiles/components/profile-data-table.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { ProfileSet } from '@/modules/profiles/domain'; +import { ProfileDataTable } from '@/sections/profiles/components/profile-data-table'; + +describe('', () => { + it('should render profile data table with profile set data', () => { + const profileSet: ProfileSet = { + profiles: { + prof1: { + credentials: { + access_key_id: 'key1', + secret_access_key: 'secret1', + }, + config: { + region: 'eu-west-1', + output_format: 'json', + }, + }, + }, + errors: {}, + }; + + render(); + + const row = screen.getByText(/prof1/i).closest('tr'); + + expect(row).toHaveTextContent('key1'); + expect(row).toHaveTextContent('secret1'); + expect(row).toHaveTextContent('eu-west-1'); + expect(row).toHaveTextContent('json'); + }); +}); diff --git a/src/sections/profiles/components/profile-data-table.tsx b/src/sections/profiles/components/profile-data-table.tsx new file mode 100644 index 0000000..b0bdd59 --- /dev/null +++ b/src/sections/profiles/components/profile-data-table.tsx @@ -0,0 +1,185 @@ +'use client'; + +import React from 'react'; +import { ColumnDef } from '@tanstack/table-core'; +import { ProfileSet } from '@/modules/profiles/domain'; +import { DataTable } from '@/components/ui/data-table'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { FileType, Globe2Icon, LucideIcon, MoreHorizontal } from 'lucide-react'; +import { DataTableColumnHeader } from '@/components/ui/data-table-column-header'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + FilterableColumn, + SearchInputFilter, +} from '@/components/ui/data-table-toolbar'; + +type Profile = { + name: string; + access_key_id?: string; + secret_access_key?: string; + region?: string; + output_format?: string; +}; + +const profileColumns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label='Select all' + className='translate-y-[2px]' + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label='Select row' + className='translate-y-[2px]' + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'name', + header: ({ column }) => ( + + ), + }, + { + accessorKey: 'access_key_id', + header: ({ column }) => ( + + ), + }, + { + accessorKey: 'secret_access_key', + header: ({ column }) => ( + + ), + }, + { + accessorKey: 'region', + header: ({ column }) => ( + + ), + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + }, + }, + { + accessorKey: 'output_format', + header: ({ column }) => ( + + ), + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + }, + }, + { + id: 'actions', + cell: ({ row }) => { + const profile = row.original; + + return ( + + + + + + Actions + + Update {profile.name} profile + Delete {profile.name} profile + + + ); + }, + }, +]; + +function flattenProfileSet(profileSet: ProfileSet): Profile[] { + const flattenedArr = []; + + for (const [key, value] of Object.entries(profileSet.profiles)) { + const flattenedObj = { + name: key, + ...value.credentials, + ...value.config, + }; + flattenedArr.push(flattenedObj); + } + + return flattenedArr; +} + +type ProfileDataTableProps = { + data: ProfileSet; +}; + +export function ProfileDataTable({ data }: Readonly) { + const profiles: Profile[] = flattenProfileSet(data); + + const getFilterOptions = (property: keyof Profile, icon: LucideIcon) => + profiles + .filter((profile) => profile[property] !== undefined) + .filter( + (profile, index, array) => + array.findIndex((entry) => entry[property] === profile[property]) === + index + ) + .map((profile) => { + return { + label: profile[property] as string, + value: profile[property] as string, + icon: icon, + }; + }); + + const regionFilterOptions = getFilterOptions('region', Globe2Icon); + const outputFormatFilterOptions = getFilterOptions('output_format', FileType); + + const filterableColumns: FilterableColumn[] = [ + { + title: 'Region', + columnName: 'region', + options: regionFilterOptions, + }, + { + title: 'Output Format', + columnName: 'output_format', + options: outputFormatFilterOptions, + }, + ]; + + const searchInputFilter: SearchInputFilter = { + columnName: 'name', + placeholder: 'Filter profiles', + }; + + return ( + + ); +}