Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat control list #162

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import React from 'react'
import { PageHeading } from '@repo/ui/page-heading'
import ControlsTable from '@/components/pages/protected/program/controls/controls-table'

const Page: React.FC = () => {
return <PageHeading eyebrow="Programs" heading="Controls" />
return (
<>
<PageHeading heading="Control List"></PageHeading>
<ControlsTable />
</>
)
}

export default Page
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
'use client'

import { Avatar, AvatarFallback, AvatarImage } from '@repo/ui/avatar'
import { Badge } from '@repo/ui/badge'
import { Button } from '@repo/ui/button'
import { DataTable } from '@repo/ui/data-table'
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@repo/ui/sheet'
import { ColumnDef } from '@tanstack/table-core'
import { DownloadIcon, PencilIcon } from 'lucide-react'
import React, { useState } from 'react'

// Sample data
const data = [
{
name: 'CC1.2',
ref: 'CC1.2',
description: 'The board of directors demonstrates independence from management and exercises oversight of the development and performance of internal control. (COSO Principle 2)',
tags: ['Security', 'CC1.2', 'Control Environment'],
status: 'Overdue',
updatedBy: 'Sarah Funkhouser',
updatedAt: 'less than a day',
createdBy: 'Kelsey Waters',
createdAt: 'January 7, 2024 1:22 PM',
owners: [{ avatar: '/path/to/avatar1.png', fallback: 'K' }],
},
{
name: 'CC1.3',
ref: 'CC1.3',
description: 'Management establishes, with board oversight, structures, reporting lines, and appropriate authorities and responsibilities. (COSO Principle 3)',
tags: ['Governance', 'CC1.3'],
status: 'In Progress',
updatedBy: 'John Doe',
updatedAt: '2 days ago',
createdBy: 'Kelsey Waters',
createdAt: 'January 5, 2024 10:15 AM',
owners: [{ avatar: '/path/to/avatar2.png', fallback: 'S' }],
},
]

// Columns definition
const columns: ColumnDef<any>[] = [
{
header: 'Name',
accessorKey: 'name',
cell: ({ row }) => <div>{row.getValue('name')}</div>,
},
{
header: 'Ref',
accessorKey: 'ref',
cell: ({ row }) => <div>{row.getValue('ref')}</div>,
},
{
header: 'Description',
accessorKey: 'description',
cell: ({ row }) => (
<div>
<p>{row.getValue('description')}</p>
<div className="mt-2 border-t border-dotted pt-2 flex flex-wrap gap-2">
{row.original.tags.map((tag: string, index: number) => (
<Badge key={index} variant="outline">
{tag}
</Badge>
))}
</div>
</div>
),
},
{
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => <span className="flex items-center gap-2">{row.getValue('status')}</span>,
},
{
header: 'Owners',
accessorKey: 'owners',
cell: ({ row }) => (
<div className="flex items-center gap-2">
{row.getValue('owners').map((owner: any, index: number) => (
<Avatar key={index}>
<AvatarImage src={owner.avatar} alt={owner.fallback} />
<AvatarFallback>{owner.fallback}</AvatarFallback>
</Avatar>
))}
</div>
),
},
]

// CSV export utility
const exportToCSV = (data: any[], fileName: string) => {
const csvRows = []

csvRows.push(['Name', 'Ref', 'Description', 'Tags', 'Status', 'Owners'].join(','))

data.forEach((row) => {
const owners = row.owners.map((o: any) => o.fallback).join(' | ')
csvRows.push([row.name, row.ref, row.description, row.tags.join('; '), row.status, owners].join(','))
})

const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${fileName}.csv`
a.click()
URL.revokeObjectURL(url)
}

const ControlsTable: React.FC = () => {
const [isSheetOpen, setSheetOpen] = useState(false)
const [currentRow, setCurrentRow] = useState<any>(null)

const handleRowClick = (row: any) => {
setCurrentRow(row)
setSheetOpen(true)
}

return (
<div>
<div className="flex justify-between items-center mb-4">
<h1 className="text-xl font-bold">Controls Table</h1>
<Button onClick={() => exportToCSV(data, 'control_list')} icon={<DownloadIcon />} iconPosition="left">
Export
</Button>
</div>
<DataTable columns={columns} data={data} onRowClick={(row: any) => handleRowClick(row)} />
<Sheet open={isSheetOpen} onOpenChange={setSheetOpen}>
<SheetContent>
{currentRow && (
<div>
{/* Header Section */}
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-2">
<h1 className="text-2xl font-semibold text-oxford-blue-900">{currentRow.name}</h1>
<Button variant="outline" icon={<PencilIcon />} iconPosition="left" className="ml-2 py-2 px-2" size="sm">
Edit
</Button>
</div>
</div>
<div className="flex justify-between items-center border p-4 rounded-md mb-4">
{/* Updated Info */}
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Updated:</span>
<span className="text-sm">{currentRow.updatedAt}</span>
<Avatar variant="small">
<AvatarImage src="/path/to/updated-by-avatar.png" alt="Updated By" />
<AvatarFallback>{currentRow.updatedBy[0]}</AvatarFallback>
</Avatar>
<span className="text-sm">{currentRow.updatedBy}</span>
</div>

<div className="border h-6" />

{/* Created Info */}
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Created:</span>
<span className="text-sm">{currentRow.createdAt}</span>
<Avatar variant="small">
<AvatarImage src="/path/to/created-by-avatar.png" alt="Created By" />
<AvatarFallback>{currentRow.createdBy[0]}</AvatarFallback>
</Avatar>
<span className="text-sm">{currentRow.createdBy}</span>
</div>
</div>

{/* Description Section */}
<h2 className="text-xl mt-4 font-medium">Point of Focus</h2>
<p className="text-sm ">{currentRow.description}</p>

{/* Tags Section */}
<div className="mt-4">
<h3 className="text-xl font-medium font-bold">Tags</h3>
<div className="flex gap-2 mt-2">
{currentRow.tags.map((tag: string, index: number) => (
<Badge key={index} variant="outline">
{tag}
</Badge>
))}
</div>
</div>
</div>
)}
</SheetContent>
</Sheet>
</div>
)
}

export default ControlsTable
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"./simple-form": "./src/simple-form.tsx",
"./select": "./src/select/select.tsx",
"./separator": "./src/separator/separator.tsx",
"./sheet": "./src/sheet/sheet.tsx",
"./slider": "./src/slider/slider.tsx",
"./switch": "./src/switch/switch.tsx",
"./tabs": "./src/tabs/tabs.tsx",
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,19 @@ interface DataTableProps<TData, TValue> {
showVisibility?: boolean
noResultsText?: string
noDataMarkup?: ReactElement
onRowClick?: (rowData: TData) => void
}

export function DataTable<TData, TValue>({ columns, loading = false, data, showFilter = false, showVisibility = false, noResultsText = 'No results', noDataMarkup }: DataTableProps<TData, TValue>) {
export function DataTable<TData, TValue>({
columns,
loading = false,
data,
showFilter = false,
showVisibility = false,
noResultsText = 'No results',
noDataMarkup,
onRowClick,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
Expand Down Expand Up @@ -112,7 +122,7 @@ export function DataTable<TData, TValue>({ columns, loading = false, data, showF
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
<TableRow onClick={() => onRowClick?.(row.original)} key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
Expand Down
71 changes: 71 additions & 0 deletions packages/ui/src/sheet/sheet.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from './sheet'

const meta: Meta<typeof Sheet> = {
title: 'UI/Sheet',
component: Sheet,
parameters: {
docs: {
description: {
component: 'A versatile sheet component for displaying side panels or modal-like content. Built with Radix UI Dialog and supports configurable sides.',
},
},
},
}

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
render: () => (
<Sheet>
<SheetTrigger className="btn">Open Sheet</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Default Sheet</SheetTitle>
<SheetDescription>This is a default sheet. Customize its content and behavior as needed.</SheetDescription>
</SheetHeader>
<div className="content">Your content goes here.</div>
<SheetFooter>
<button className="btn">Action</button>
</SheetFooter>
</SheetContent>
</Sheet>
),
}

export const WithCustomSide: Story = {
render: () => (
<Sheet>
<SheetTrigger className="btn">Open from Bottom</SheetTrigger>
<SheetContent side="bottom">
<SheetHeader>
<SheetTitle>Bottom Sheet</SheetTitle>
<SheetDescription>This sheet slides in from the bottom of the screen.</SheetDescription>
</SheetHeader>
<div className="content">Custom side content here.</div>
<SheetFooter>
<button className="btn">Close</button>
</SheetFooter>
</SheetContent>
</Sheet>
),
}

export const WithDisabledClose: Story = {
render: () => (
<Sheet>
<SheetTrigger className="btn">Open Sheet</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Sheet Without Close</SheetTitle>
<SheetDescription>The close button is disabled in this example.</SheetDescription>
</SheetHeader>
<div className="content">Your content goes here.</div>
<SheetFooter>
<button className="btn">Action</button>
</SheetFooter>
</SheetContent>
</Sheet>
),
}
73 changes: 73 additions & 0 deletions packages/ui/src/sheet/sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from '../../lib/utils'

const Sheet = SheetPrimitive.Root

const SheetTrigger = SheetPrimitive.Trigger

const SheetClose = SheetPrimitive.Close

const SheetPortal = SheetPrimitive.Portal

const SheetOverlay = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Overlay>, React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn('fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', className)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName

const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-[825px]',
right: 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-[825px]',
},
},
defaultVariants: {
side: 'right',
},
},
)

interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, VariantProps<typeof sheetVariants> {}

const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName

const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => <div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
SheetHeader.displayName = 'SheetHeader'

const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
SheetFooter.displayName = 'SheetFooter'

const SheetTitle = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Title>, React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>>(({ className, ...props }, ref) => (
<SheetPrimitive.Title ref={ref} className={cn('text-lg font-semibold text-foreground', className)} {...props} />
))
SheetTitle.displayName = SheetPrimitive.Title.displayName

const SheetDescription = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Description>, React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>>(({ className, ...props }, ref) => (
<SheetPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
))
SheetDescription.displayName = SheetPrimitive.Description.displayName

export { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }
Loading