Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# Identity & SSO Configuration
# If Zitadel auto-generated a different Client ID for your provider, paste it here.
IDENTITY_OAUTH_CLIENT_ID=api-gateway

# Database Encryption Key
# Note: You must generate your own 32-byte url-safe base64-encoded Fernet key.
# You can generate one using: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
DB_ENCRYPTION_KEY=your_generated_fernet_key_here_must_be_32_url_safe_base64
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ check-all: format lint typecheck test
# --- Local Development Server ---

dev:
@echo "Starting both Frontend and Backend concurrently..."
pnpm dlx concurrently --kill-others -c "blue,magenta" -n "api,web" "make dev-api" "make dev-web"
@echo "Starting Frontend, API, and Worker concurrently..."
pnpm dlx concurrently --kill-others -c "blue,magenta,cyan" -n "api,web,worker" "make dev-api" "make dev-web" "make dev-worker"

dev-as2:
@echo "Starting AS2 Server with hot-reload for local development..."
Expand All @@ -55,6 +55,10 @@ dev-web:
@echo "Starting React Frontend with Vite..."
cd frontend/web && pnpm dev

dev-worker:
@echo "Starting Provision Worker for local development..."
ENVIRONMENT=development PYTHONPATH=services/worker/src:libs/database/src:libs/config/src:libs/pipeline/src uv run python services/worker/src/worker/provision/main.py

db-init:
@echo "Waiting for databases to be ready..."
@sleep 5
Expand Down
1 change: 1 addition & 0 deletions frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.18",
"@radix-ui/react-label": "^2.1.10",
"@radix-ui/react-popover": "^1.1.18",
"@radix-ui/react-radio-group": "^1.4.2",
"@radix-ui/react-select": "^2.3.1",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-toast": "^1.2.18",
Expand Down
64 changes: 64 additions & 0 deletions frontend/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions frontend/web/src/components/ui/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ export interface ComboboxProps {
placeholder?: string
emptyText?: string
disabled?: boolean
side?: "top" | "right" | "bottom" | "left"
}

export function Combobox({ options, value, onChange, placeholder = "Select option...", emptyText = "No results found.", disabled = false }: ComboboxProps) {
export function Combobox({ options, value, onChange, placeholder = "Select option...", emptyText = "No results found.", disabled = false, side = "bottom" }: ComboboxProps) {
const [open, setOpen] = React.useState(false)
const [inputValue, setInputValue] = React.useState(value || "")

Expand Down Expand Up @@ -69,7 +70,7 @@ export function Combobox({ options, value, onChange, placeholder = "Select optio
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
<PopoverContent side={side} portaled={false} className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
<Command>
<CommandInput
placeholder={placeholder}
Expand Down
103 changes: 103 additions & 0 deletions frontend/web/src/components/ui/data-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { Fragment } from 'react';
import { flexRender, type Table as ReactTable } from '@tanstack/react-table';
import { Skeleton } from '@/components/ui/skeleton';

interface DataTableProps<TData> {
table: ReactTable<TData>;
columnsLength: number;
isLoading?: boolean;
dataLength: number;
emptyIcon?: React.ReactNode;
emptyTitle?: string;
emptyDescription?: string;
renderExpandedRow?: (row: import('@tanstack/react-table').Row<TData>) => React.ReactNode;
}

export function DataTable<TData>({
table,
columnsLength,
isLoading,
dataLength,
emptyIcon,
emptyTitle = "No Data",
emptyDescription,
renderExpandedRow,
}: DataTableProps<TData>) {
if (isLoading) {
return (
<div className="bg-white border border-slate-200/60 rounded-2xl shadow-sm overflow-hidden flex flex-col">
<div className="p-8 space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-12 w-full rounded-xl bg-slate-50" />
))}
</div>
</div>
);
}

if (dataLength === 0) {
return (
<div className="bg-white rounded-2xl border border-slate-200/60 shadow-sm p-12 text-center">
<div className="w-16 h-16 mx-auto bg-slate-50 rounded-2xl border border-slate-100 flex items-center justify-center mb-4 text-slate-400 shadow-sm">
{emptyIcon}
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-1">{emptyTitle}</h3>
{emptyDescription && <p className="text-sm text-slate-500 mt-1">{emptyDescription}</p>}
</div>
);
}

return (
<div className="bg-white border border-slate-200/60 rounded-2xl shadow-sm overflow-hidden flex flex-col">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse text-sm">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="border-b border-slate-200/60 bg-slate-50/50">
{headerGroup.headers.map((header) => (
<th key={header.id} className="px-6 py-4 text-xs font-semibold text-slate-500 uppercase tracking-wider text-left align-middle">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody className="divide-y divide-slate-100">
{table.getRowModel().rows.map((row) => (
<Fragment key={row.id}>
<tr
className={`hover:bg-slate-50/50 transition-colors group ${renderExpandedRow ? 'cursor-pointer' : ''} ${row.getIsExpanded() ? 'bg-slate-50/50' : ''}`}
onClick={renderExpandedRow ? () => row.toggleExpanded() : undefined}
onKeyDown={renderExpandedRow ? (e) => {
const target = e.target as HTMLElement;
if (target.tagName === 'BUTTON' || target.tagName === 'A' || target.tagName === 'INPUT') return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
row.toggleExpanded();
}
} : undefined}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
tabIndex={renderExpandedRow ? 0 : undefined}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-6 py-4 align-middle">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
Comment on lines +70 to +88

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Expandable rows are not keyboard-accessible.

The row toggle is only wired to onClick; there's no role, tabIndex, or onKeyDown handler, so keyboard users cannot expand/collapse rows. Since this is a shared primitive used across partners, routes, and endpoints tables, this a11y gap propagates broadly.

♿ Proposed fix
                 <tr
                   className={`hover:bg-slate-50/50 transition-colors group ${renderExpandedRow ? 'cursor-pointer' : ''} ${row.getIsExpanded() ? 'bg-slate-50/50' : ''}`}
                   onClick={renderExpandedRow ? () => row.toggleExpanded() : undefined}
+                  role={renderExpandedRow ? 'button' : undefined}
+                  tabIndex={renderExpandedRow ? 0 : undefined}
+                  onKeyDown={renderExpandedRow ? (e) => {
+                    if (e.key === 'Enter' || e.key === ' ') {
+                      e.preventDefault();
+                      row.toggleExpanded();
+                    }
+                  } : undefined}
                 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<tr
className={`hover:bg-slate-50/50 transition-colors group ${renderExpandedRow ? 'cursor-pointer' : ''} ${row.getIsExpanded() ? 'bg-slate-50/50' : ''}`}
onClick={renderExpandedRow ? () => row.toggleExpanded() : undefined}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-6 py-4 align-middle">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
<tr
className={`hover:bg-slate-50/50 transition-colors group ${renderExpandedRow ? 'cursor-pointer' : ''} ${row.getIsExpanded() ? 'bg-slate-50/50' : ''}`}
onClick={renderExpandedRow ? () => row.toggleExpanded() : undefined}
role={renderExpandedRow ? 'button' : undefined}
tabIndex={renderExpandedRow ? 0 : undefined}
onKeyDown={renderExpandedRow ? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
row.toggleExpanded();
}
} : undefined}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-6 py-4 align-middle">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/web/src/components/ui/data-table.tsx` around lines 70 - 79, The
expandable row behavior in data-table is mouse-only; update the table row in the
data-table component to be keyboard accessible when renderExpandedRow is
enabled. Add appropriate accessibility semantics on the <tr> used for row
toggling, including a focusable tabIndex, a suitable role, and an onKeyDown
handler that triggers row.toggleExpanded() for Enter/Space, while keeping the
existing onClick behavior and preserving the expanded state styling.

{renderExpandedRow && row.getIsExpanded() && (
<tr>
<td colSpan={columnsLength} className="p-0">
{renderExpandedRow(row)}
</td>
</tr>
)}
</Fragment>
))}
</tbody>
</table>
</div>
</div>
);
}
77 changes: 77 additions & 0 deletions frontend/web/src/components/ui/form-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { ReactNode } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';

interface FormModalProps {
title: string;
triggerText: string;
triggerIcon?: ReactNode;
icon?: ReactNode;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
isPending: boolean;
submitText?: string;
children: ReactNode;
submitDisabled?: boolean;
maxWidth?: string;
footerContent?: ReactNode;
}

export function FormModal({
title,
triggerText,
triggerIcon = <Plus className="h-4 w-4" />,
icon,
isOpen,
onOpenChange,
onSubmit,
isPending,
submitText = 'Save',
children,
submitDisabled = false,
maxWidth = 'sm:max-w-[600px]',
footerContent
}: FormModalProps) {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl shadow-sm">
{triggerIcon}
{triggerText}
</Button>
</DialogTrigger>

<DialogContent className={`${maxWidth} rounded-2xl`} onPointerDownOutside={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle className="text-xl flex items-center gap-2">
{icon && (
<div className="w-8 h-8 rounded-lg bg-indigo-50 flex items-center justify-center text-indigo-600">
{icon}
</div>
)}
{title}
</DialogTitle>
</DialogHeader>

<form onSubmit={onSubmit} className="grid gap-6 py-4">
{children}

<div className="flex justify-between items-center mt-2">
<div>
{footerContent}
</div>
<Button
type="submit"
disabled={isPending || submitDisabled}
className="h-11 px-8 text-base font-semibold shadow-sm rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white disabled:opacity-50"
>
{isPending ? 'Saving...' : submitText}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}
16 changes: 11 additions & 5 deletions frontend/web/src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const PopoverAnchor = PopoverPrimitive.Anchor

const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & { portaled?: boolean }
>(({ className, align = "center", sideOffset = 4, portaled = true, ...props }, ref) => {
const content = (
<PopoverPrimitive.Content
ref={ref}
align={align}
Expand All @@ -24,8 +24,14 @@ const PopoverContent = React.forwardRef<
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
)

if (!portaled) {
return content
}

return <PopoverPrimitive.Portal>{content}</PopoverPrimitive.Portal>
})
PopoverContent.displayName = PopoverPrimitive.Content.displayName

export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
Loading
Loading