diff --git a/frontend/package.json b/frontend/package.json index 12a7bf4..67fb8bb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,12 +17,14 @@ }, "dependencies": { "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.7", + "@tanstack/react-table": "^8.21.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.471.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 05856f8..79c7da4 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@hookform/resolvers': specifier: ^3.10.0 version: 3.10.0(react-hook-form@7.54.2(react@19.0.0)) + '@radix-ui/react-checkbox': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dialog': specifier: ^1.1.5 version: 1.1.5(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -29,6 +32,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.7 version: 1.1.7(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/react-table': + specifier: ^8.21.2 + version: 8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -409,6 +415,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.1.4': + resolution: {integrity: sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.1': resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} peerDependencies: @@ -597,6 +616,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.2': + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.1': resolution: {integrity: sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==} peerDependencies: @@ -632,6 +664,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.2': + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tooltip@1.1.7': resolution: {integrity: sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==} peerDependencies: @@ -681,6 +722,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.1.0': resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: @@ -727,6 +777,17 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tanstack/react-table@8.21.2': + resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.2': + resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==} + engines: {node: '>=12'} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -2625,6 +2686,22 @@ snapshots: '@types/react': 19.0.4 '@types/react-dom': 19.0.2(@types/react@19.0.4) + '@radix-ui/react-checkbox@1.1.4(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.4)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.4)(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.4)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.4)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.4)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.4 + '@types/react-dom': 19.0.2(@types/react@19.0.4) + '@radix-ui/react-collection@1.1.1(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.4)(react@19.0.0) @@ -2811,6 +2888,15 @@ snapshots: '@types/react': 19.0.4 '@types/react-dom': 19.0.2(@types/react@19.0.4) + '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.4)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.4 + '@types/react-dom': 19.0.2(@types/react@19.0.4) + '@radix-ui/react-roving-focus@1.1.1(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -2844,6 +2930,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.4 + '@radix-ui/react-slot@1.1.2(@types/react@19.0.4)(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.4)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.4 + '@radix-ui/react-tooltip@1.1.7(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -2890,6 +2983,12 @@ snapshots: optionalDependencies: '@types/react': 19.0.4 + '@radix-ui/react-use-previous@1.1.0(@types/react@19.0.4)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.4 + '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.4)(react@19.0.0)': dependencies: '@radix-ui/rect': 1.1.0 @@ -2925,6 +3024,14 @@ snapshots: dependencies: tslib: 2.8.1 + '@tanstack/react-table@8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@tanstack/table-core': 8.21.2 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@tanstack/table-core@8.21.2': {} + '@types/estree@1.0.6': {} '@types/json-schema@7.0.15': {} diff --git a/frontend/src/app/LayoutContent.tsx b/frontend/src/app/LayoutContent.tsx index 3d78eb8..f5d27be 100644 --- a/frontend/src/app/LayoutContent.tsx +++ b/frontend/src/app/LayoutContent.tsx @@ -53,7 +53,7 @@ export function LayoutContent({ children }: { children: React.ReactNode }) { return ( -
+
-

Light Switch

+
+
+ + Feature Flag Management +
+ +
); } diff --git a/frontend/src/components/TanstackTable.tsx b/frontend/src/components/TanstackTable.tsx new file mode 100644 index 0000000..2c2819f --- /dev/null +++ b/frontend/src/components/TanstackTable.tsx @@ -0,0 +1,130 @@ +'use client'; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getPaginationRowModel, + useReactTable, + getSortedRowModel, + PaginationState, +} from '@tanstack/react-table'; +import { useState } from 'react'; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { FeatureFlagTableType } from '@/types/types'; + +export interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function TanstackTable({ + columns, + data, +}: DataTableProps) { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 9, + }); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { + pagination, + }, + onPaginationChange: setPagination, + }); + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+ {`${table.getFilteredSelectedRowModel().rows.length} of ${table.getFilteredRowModel().rows.length} row(s) selected`} +
+ + {`Page ${pagination.pageIndex + 1} of ${table.getPageCount()}`} + +
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..baaf548 --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +'use client'; + +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { Check } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..d0c6422 --- /dev/null +++ b/frontend/src/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); +Table.displayName = 'Table'; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = 'TableHeader'; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = 'TableBody'; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', + className, + )} + {...props} + /> +)); +TableFooter.displayName = 'TableFooter'; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = 'TableRow'; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]', + className, + )} + {...props} + /> +)); +TableHead.displayName = 'TableHead'; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]', + className, + )} + {...props} + /> +)); +TableCell.displayName = 'TableCell'; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = 'TableCaption'; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/frontend/src/constants/featureFlagColumns.tsx b/frontend/src/constants/featureFlagColumns.tsx new file mode 100644 index 0000000..1e1956e --- /dev/null +++ b/frontend/src/constants/featureFlagColumns.tsx @@ -0,0 +1,26 @@ +import { ColumnDef } from '@tanstack/react-table'; + +import { FeatureFlagTableType } from '@/types/types'; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'Key', + header: 'Key', + }, + { + accessorKey: 'type', + header: 'type', + }, + { + accessorKey: 'createdAt', + header: 'createdAt', + }, + { + accessorKey: 'createdBy', + header: 'createdBy', + }, + { + accessorKey: 'status', + header: 'status ', + }, +]; diff --git a/frontend/src/constants/tableMockDatat.ts b/frontend/src/constants/tableMockDatat.ts new file mode 100644 index 0000000..8875df0 --- /dev/null +++ b/frontend/src/constants/tableMockDatat.ts @@ -0,0 +1,102 @@ +import { FeatureFlagTableType, FeatureFlagType } from '@/types/types'; + +export const ExampleData: FeatureFlagTableType[] = [ + { + Key: 'New Dashboard', + type: FeatureFlagType.Boolean, + createdAt: '2024-01-15', + createdBy: 'Alice', + status: false, + }, + { + Key: 'Feature Flags', + type: FeatureFlagType.String, + createdAt: '2024-02-01', + createdBy: 'KeunJae', + status: true, + }, + { + Key: 'Example 1', + type: FeatureFlagType.Number, + createdAt: '2024-01-28', + createdBy: 'Elon Musk', + status: true, + }, + { + Key: 'Example 2', + type: FeatureFlagType.String, + createdAt: '2024-02-01', + createdBy: 'KeunJae', + status: true, + }, + { + Key: 'User Preferences', + type: FeatureFlagType.Boolean, + createdAt: '2024-03-05', + createdBy: 'Sarah', + status: false, + }, + { + Key: 'Application Mode', + type: FeatureFlagType.String, + createdAt: '2024-03-10', + createdBy: 'Michael', + status: true, + }, + { + Key: 'Retry Count', + type: FeatureFlagType.Number, + createdAt: '2024-03-15', + createdBy: 'Nina', + status: false, + }, + { + Key: 'Notifications', + type: FeatureFlagType.Boolean, + createdAt: '2024-03-20', + createdBy: 'Tom', + status: true, + }, + { + Key: 'Dark Mode', + type: FeatureFlagType.Boolean, + createdAt: '2024-03-25', + createdBy: 'Emma', + status: false, + }, + { + Key: 'Max Items', + type: FeatureFlagType.Number, + createdAt: '2024-03-30', + createdBy: 'John', + status: true, + }, + { + Key: 'User Role', + type: FeatureFlagType.String, + createdAt: '2024-04-05', + createdBy: 'Sophia', + status: true, + }, + { + Key: 'Enable Logging', + type: FeatureFlagType.Boolean, + createdAt: '2024-04-10', + createdBy: 'Alice', + status: false, + }, + { + Key: 'API Key', + type: FeatureFlagType.String, + createdAt: '2024-04-15', + createdBy: 'KeunJae', + status: true, + }, + { + Key: 'Connection Timeout', + type: FeatureFlagType.Number, + createdAt: '2024-04-20', + createdBy: 'Elon Musk', + status: false, + }, +]; diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts new file mode 100644 index 0000000..7668c65 --- /dev/null +++ b/frontend/src/types/types.ts @@ -0,0 +1,13 @@ +export enum FeatureFlagType { + Boolean = 'Boolean', + String = 'String', + Number = 'Number', +} + +export interface FeatureFlagTableType { + Key: string; + type: FeatureFlagType; + createdAt: string; + createdBy: string; + status: boolean; +}