Files
ColaFlow/docs/frontend/component-library.md
Yaojia Wang fe8ad1c1f9
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
In progress
2025-11-03 11:51:02 +01:00

991 lines
26 KiB
Markdown

# Component Library - ColaFlow Enterprise Features
## Document Overview
This document catalogs all reusable React components for ColaFlow's enterprise features, including props, usage examples, and design guidelines.
**UI Framework**: shadcn/ui + Tailwind CSS 4 + Radix UI
---
## Table of Contents
1. [Authentication Components](#authentication-components)
2. [Settings Components](#settings-components)
3. [MCP Token Components](#mcp-token-components)
4. [Form Components](#form-components)
5. [Utility Components](#utility-components)
6. [Design Tokens](#design-tokens)
---
## Authentication Components
### 1. SsoButton
**Purpose**: Displays SSO provider button with logo and loading state.
**File**: `components/auth/SsoButton.tsx`
**Props**:
```typescript
interface SsoButtonProps {
provider: 'AzureAD' | 'Google' | 'Okta' | 'SAML';
onClick: () => void;
loading?: boolean;
disabled?: boolean;
fullWidth?: boolean;
size?: 'sm' | 'md' | 'lg';
}
```
**Implementation**:
```tsx
import { Button } from '@/components/ui/button';
import Image from 'next/image';
export function SsoButton({
provider,
onClick,
loading = false,
disabled = false,
fullWidth = true,
size = 'md',
}: SsoButtonProps) {
const providerConfig = {
AzureAD: {
label: 'Continue with Microsoft',
logo: '/logos/microsoft.svg',
bgColor: 'bg-[#2F2F2F]',
hoverColor: 'hover:bg-[#1F1F1F]',
textColor: 'text-white',
},
Google: {
label: 'Continue with Google',
logo: '/logos/google.svg',
bgColor: 'bg-white',
hoverColor: 'hover:bg-gray-50',
textColor: 'text-gray-700',
border: 'border border-gray-300',
},
Okta: {
label: 'Continue with Okta',
logo: '/logos/okta.svg',
bgColor: 'bg-[#007DC1]',
hoverColor: 'hover:bg-[#0062A3]',
textColor: 'text-white',
},
SAML: {
label: 'Continue with SSO',
logo: '/logos/saml.svg',
bgColor: 'bg-indigo-600',
hoverColor: 'hover:bg-indigo-700',
textColor: 'text-white',
},
};
const config = providerConfig[provider];
const sizeClasses = {
sm: 'h-9 px-3 text-sm',
md: 'h-11 px-4 text-base',
lg: 'h-13 px-6 text-lg',
};
return (
<Button
onClick={onClick}
disabled={disabled || loading}
className={`
${config.bgColor}
${config.hoverColor}
${config.textColor}
${config.border || ''}
${sizeClasses[size]}
${fullWidth ? 'w-full' : ''}
flex items-center justify-center gap-3
transition-colors duration-200
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
{loading ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-current" />
) : (
<>
<Image src={config.logo} alt={provider} width={20} height={20} />
<span className="font-medium">{config.label}</span>
</>
)}
</Button>
);
}
```
**Usage**:
```tsx
<SsoButton
provider="AzureAD"
onClick={() => loginWithSso('AzureAD')}
loading={isLoading}
/>
```
---
### 2. TenantSlugInput
**Purpose**: Input field with real-time slug validation (available/taken).
**File**: `components/auth/TenantSlugInput.tsx`
**Props**:
```typescript
interface TenantSlugInputProps {
value: string;
onChange: (value: string) => void;
error?: string;
disabled?: boolean;
}
```
**Implementation**:
```tsx
import { Input } from '@/components/ui/input';
import { useCheckSlug } from '@/hooks/tenants/useCheckSlug';
import { CheckCircle2, XCircle, Loader2 } from 'lucide-react';
export function TenantSlugInput({
value,
onChange,
error,
disabled = false,
}: TenantSlugInputProps) {
const { data, isLoading } = useCheckSlug(value, value.length >= 3);
const showValidation = value.length >= 3 && !error;
return (
<div className="space-y-2">
<div className="relative">
<Input
value={value}
onChange={(e) => {
// Only allow lowercase letters, numbers, and hyphens
const cleaned = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
onChange(cleaned);
}}
placeholder="your-company"
disabled={disabled}
className={`
pr-10
${error ? 'border-red-500 focus-visible:ring-red-500' : ''}
${showValidation && data?.available ? 'border-green-500' : ''}
${showValidation && data?.available === false ? 'border-red-500' : ''}
`}
/>
{/* Validation Icon */}
<div className="absolute right-3 top-1/2 -translate-y-1/2">
{isLoading && (
<Loader2 className="h-5 w-5 text-gray-400 animate-spin" />
)}
{showValidation && !isLoading && data?.available === true && (
<CheckCircle2 className="h-5 w-5 text-green-600" />
)}
{showValidation && !isLoading && data?.available === false && (
<XCircle className="h-5 w-5 text-red-600" />
)}
</div>
</div>
{/* Validation Message */}
{showValidation && !isLoading && (
<p
className={`text-sm ${
data?.available ? 'text-green-600' : 'text-red-600'
}`}
>
{data?.available
? '✓ This slug is available'
: '✗ This slug is already taken'}
</p>
)}
{/* Error Message */}
{error && <p className="text-sm text-red-600">{error}</p>}
{/* Helper Text */}
<p className="text-sm text-gray-500">
Your organization URL: <span className="font-mono">{value || 'your-company'}.colaflow.com</span>
</p>
</div>
);
}
```
**Usage**:
```tsx
<TenantSlugInput
value={slug}
onChange={setSlug}
error={errors.slug?.message}
/>
```
---
### 3. PasswordStrengthIndicator
**Purpose**: Visual password strength meter using zxcvbn.
**File**: `components/auth/PasswordStrengthIndicator.tsx`
**Props**:
```typescript
interface PasswordStrengthIndicatorProps {
password: string;
show?: boolean;
}
```
**Implementation**:
```tsx
import { useMemo } from 'react';
import zxcvbn from 'zxcvbn';
export function PasswordStrengthIndicator({
password,
show = true,
}: PasswordStrengthIndicatorProps) {
const result = useMemo(() => {
if (!password) return null;
return zxcvbn(password);
}, [password]);
if (!show || !result) return null;
const score = result.score; // 0-4
const strength = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'][score];
const color = ['red', 'orange', 'yellow', 'lime', 'green'][score];
const barWidth = `${(score + 1) * 20}%`;
return (
<div className="space-y-2">
{/* Strength Bar */}
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={`h-full bg-${color}-500 transition-all duration-300`}
style={{ width: barWidth }}
/>
</div>
{/* Strength Label */}
<div className="flex items-center justify-between text-sm">
<span className={`font-medium text-${color}-600`}>
Password strength: {strength}
</span>
{score < 3 && (
<span className="text-gray-500 text-xs">
{result.feedback.warning || 'Try a longer password'}
</span>
)}
</div>
{/* Suggestions */}
{result.feedback.suggestions.length > 0 && score < 3 && (
<ul className="text-xs text-gray-600 space-y-1">
{result.feedback.suggestions.map((suggestion, index) => (
<li key={index}> {suggestion}</li>
))}
</ul>
)}
</div>
);
}
```
**Usage**:
```tsx
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<PasswordStrengthIndicator password={password} />
```
---
## Settings Components
### 4. SsoConfigForm
**Purpose**: Dynamic SSO configuration form (OIDC/SAML).
**File**: `components/settings/SsoConfigForm.tsx`
**Props**:
```typescript
interface SsoConfigFormProps {
initialValues?: SsoConfig;
onSubmit: (values: SsoConfig) => Promise<void>;
isLoading?: boolean;
}
```
**Implementation**:
```tsx
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form';
// Validation schema
const ssoConfigSchema = z.object({
provider: z.enum(['AzureAD', 'Google', 'Okta', 'GenericSaml']),
authority: z.string().url().optional(),
clientId: z.string().optional(),
clientSecret: z.string().optional(),
metadataUrl: z.string().url().optional(),
entityId: z.string().optional(),
signOnUrl: z.string().url().optional(),
certificate: z.string().optional(),
autoProvisionUsers: z.boolean().default(true),
allowedDomains: z.string().optional(),
});
export function SsoConfigForm({
initialValues,
onSubmit,
isLoading = false,
}: SsoConfigFormProps) {
const [provider, setProvider] = useState<string>(initialValues?.provider || 'AzureAD');
const form = useForm({
resolver: zodResolver(ssoConfigSchema),
defaultValues: initialValues || {
provider: 'AzureAD',
autoProvisionUsers: true,
},
});
const isSaml = provider === 'GenericSaml';
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Provider Selection */}
<FormField
control={form.control}
name="provider"
render={({ field }) => (
<FormItem>
<FormLabel>SSO Provider</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
setProvider(value);
}}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="AzureAD">Azure AD / Microsoft Entra</SelectItem>
<SelectItem value="Google">Google Workspace</SelectItem>
<SelectItem value="Okta">Okta</SelectItem>
<SelectItem value="GenericSaml">Generic SAML 2.0</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* OIDC Fields */}
{!isSaml && (
<>
<FormField
control={form.control}
name="authority"
render={({ field }) => (
<FormItem>
<FormLabel>Authority / Issuer URL *</FormLabel>
<FormControl>
<Input
{...field}
placeholder="https://login.microsoftonline.com/tenant-id"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>Client ID *</FormLabel>
<FormControl>
<Input {...field} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Client Secret *</FormLabel>
<FormControl>
<Input {...field} type="password" placeholder="Enter client secret" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metadataUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Metadata URL (Optional)</FormLabel>
<FormControl>
<Input
{...field}
placeholder="https://login.microsoftonline.com/.well-known/openid-configuration"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{/* SAML Fields */}
{isSaml && (
<>
<FormField
control={form.control}
name="entityId"
render={({ field }) => (
<FormItem>
<FormLabel>Entity ID *</FormLabel>
<FormControl>
<Input {...field} placeholder="https://idp.example.com/saml" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="signOnUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Sign-On URL *</FormLabel>
<FormControl>
<Input {...field} placeholder="https://idp.example.com/sso" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="certificate"
render={({ field }) => (
<FormItem>
<FormLabel>X.509 Certificate *</FormLabel>
<FormControl>
<Textarea
{...field}
rows={6}
placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
className="font-mono text-sm"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{/* Auto-Provision Users */}
<FormField
control={form.control}
name="autoProvisionUsers"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Auto-Provision Users</FormLabel>
<p className="text-sm text-gray-500">
Automatically create user accounts on first SSO login
</p>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
{/* Allowed Domains */}
<FormField
control={form.control}
name="allowedDomains"
render={({ field }) => (
<FormItem>
<FormLabel>Allowed Email Domains (Optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="acme.com, acme.org" />
</FormControl>
<p className="text-sm text-gray-500">
Comma-separated list of allowed email domains
</p>
<FormMessage />
</FormItem>
)}
/>
{/* Submit Button */}
<Button type="submit" disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save Configuration'}
</Button>
</form>
</Form>
);
}
```
**Usage**:
```tsx
<SsoConfigForm
initialValues={ssoConfig}
onSubmit={updateSsoConfig.mutateAsync}
isLoading={updateSsoConfig.isPending}
/>
```
---
## MCP Token Components
### 5. McpPermissionMatrix
**Purpose**: Checkbox grid for selecting MCP token permissions.
**File**: `components/mcp/McpPermissionMatrix.tsx`
**Props**:
```typescript
interface McpPermissionMatrixProps {
value: Record<string, string[]>;
onChange: (value: Record<string, string[]>) => void;
disabled?: boolean;
}
```
**Implementation**:
```tsx
import { Checkbox } from '@/components/ui/checkbox';
const RESOURCES = [
{ id: 'projects', label: 'Projects' },
{ id: 'issues', label: 'Issues' },
{ id: 'documents', label: 'Documents' },
{ id: 'reports', label: 'Reports' },
{ id: 'sprints', label: 'Sprints' },
];
const OPERATIONS = [
{ id: 'read', label: 'Read' },
{ id: 'create', label: 'Create' },
{ id: 'update', label: 'Update' },
{ id: 'delete', label: 'Delete' },
{ id: 'search', label: 'Search' },
];
export function McpPermissionMatrix({
value,
onChange,
disabled = false,
}: McpPermissionMatrixProps) {
const handleToggle = (resource: string, operation: string) => {
const resourceOps = value[resource] || [];
const newOps = resourceOps.includes(operation)
? resourceOps.filter((op) => op !== operation)
: [...resourceOps, operation];
onChange({
...value,
[resource]: newOps.length > 0 ? newOps : undefined,
});
};
const isChecked = (resource: string, operation: string) => {
return value[resource]?.includes(operation) || false;
};
// Quick actions
const selectAll = () => {
const allPermissions: Record<string, string[]> = {};
RESOURCES.forEach((resource) => {
allPermissions[resource.id] = OPERATIONS.map((op) => op.id);
});
onChange(allPermissions);
};
const selectNone = () => {
onChange({});
};
return (
<div className="space-y-4">
{/* Quick Actions */}
<div className="flex gap-2">
<button
type="button"
onClick={selectAll}
className="text-sm text-blue-600 hover:underline"
disabled={disabled}
>
Select All
</button>
<span className="text-gray-300">|</span>
<button
type="button"
onClick={selectNone}
className="text-sm text-blue-600 hover:underline"
disabled={disabled}
>
Clear All
</button>
</div>
{/* Permission Matrix */}
<div className="border rounded-lg overflow-hidden">
{/* Header */}
<div className="grid grid-cols-6 bg-gray-50 border-b">
<div className="p-3 font-semibold text-sm">Resource</div>
{OPERATIONS.map((op) => (
<div key={op.id} className="p-3 font-semibold text-sm text-center">
{op.label}
</div>
))}
</div>
{/* Rows */}
{RESOURCES.map((resource, index) => (
<div
key={resource.id}
className={`grid grid-cols-6 ${
index % 2 === 0 ? 'bg-white' : 'bg-gray-25'
} border-b last:border-b-0`}
>
<div className="p-3 font-medium text-sm">{resource.label}</div>
{OPERATIONS.map((operation) => {
// Disable "delete" for issues (business rule)
const isDisabled =
disabled || (resource.id === 'issues' && operation.id === 'delete');
return (
<div key={operation.id} className="p-3 flex items-center justify-center">
<Checkbox
checked={isChecked(resource.id, operation.id)}
onCheckedChange={() => handleToggle(resource.id, operation.id)}
disabled={isDisabled}
/>
</div>
);
})}
</div>
))}
</div>
{/* Help Text */}
<p className="text-sm text-gray-500">
Note: Delete permission is restricted for Issues to prevent accidental data loss.
</p>
</div>
);
}
```
**Usage**:
```tsx
const [permissions, setPermissions] = useState<Record<string, string[]>>({});
<McpPermissionMatrix
value={permissions}
onChange={setPermissions}
/>
```
---
### 6. TokenDisplay
**Purpose**: Display newly created MCP token with copy/download buttons.
**File**: `components/mcp/TokenDisplay.tsx`
**Props**:
```typescript
interface TokenDisplayProps {
token: string;
tokenName: string;
onClose: () => void;
}
```
**Implementation**:
```tsx
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Copy, Download, CheckCircle2 } from 'lucide-react';
import { toast } from 'sonner';
export function TokenDisplay({ token, tokenName, onClose }: TokenDisplayProps) {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(token);
setCopied(true);
toast.success('Token copied to clipboard');
setTimeout(() => setCopied(false), 2000);
};
const handleDownload = () => {
const blob = new Blob([token], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${tokenName.replace(/\s+/g, '-').toLowerCase()}-token.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Token downloaded');
};
return (
<div className="space-y-6">
{/* Warning Banner */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex gap-3">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-yellow-600"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
</div>
<div>
<h3 className="text-sm font-semibold text-yellow-800">
Important: Save This Token Now!
</h3>
<p className="text-sm text-yellow-700 mt-1">
This is the only time you'll see this token. Make sure to copy or
download it before closing this dialog.
</p>
</div>
</div>
</div>
{/* Token Display */}
<div className="space-y-2">
<label className="text-sm font-medium">Your MCP Token</label>
<div className="bg-gray-50 border rounded-lg p-4">
<code className="text-sm font-mono break-all text-gray-900">{token}</code>
</div>
</div>
{/* Actions */}
<div className="flex gap-3">
<Button onClick={handleCopy} variant="outline" className="flex-1">
{copied ? (
<>
<CheckCircle2 className="h-4 w-4 mr-2" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4 mr-2" />
Copy to Clipboard
</>
)}
</Button>
<Button onClick={handleDownload} variant="outline" className="flex-1">
<Download className="h-4 w-4 mr-2" />
Download as File
</Button>
</div>
{/* Close Button */}
<Button onClick={onClose} variant="default" className="w-full">
I've Saved the Token
</Button>
{/* Usage Instructions */}
<div className="border-t pt-4">
<h4 className="text-sm font-semibold mb-2">How to Use This Token</h4>
<ol className="text-sm text-gray-600 space-y-1 list-decimal list-inside">
<li>Add this token to your AI agent's environment variables</li>
<li>Configure the MCP server URL: <code className="font-mono bg-gray-100 px-1 rounded">https://api.colaflow.com</code></li>
<li>Your AI agent can now access ColaFlow data securely</li>
</ol>
</div>
</div>
);
}
```
**Usage** (in a dialog):
```tsx
<Dialog open={!!newToken} onOpenChange={() => setNewToken(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Token Created Successfully</DialogTitle>
</DialogHeader>
<TokenDisplay
token={newToken!}
tokenName="Claude AI Agent"
onClose={() => setNewToken(null)}
/>
</DialogContent>
</Dialog>
```
---
## Design Tokens
### Color Palette
```typescript
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
// Brand colors
primary: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
// Status colors
success: {
50: '#f0fdf4',
500: '#22c55e',
600: '#16a34a',
},
warning: {
50: '#fffbeb',
500: '#f59e0b',
600: '#d97706',
},
error: {
50: '#fef2f2',
500: '#ef4444',
600: '#dc2626',
},
},
},
},
};
```
### Typography
```typescript
// Font sizes
text-xs: 0.75rem (12px)
text-sm: 0.875rem (14px)
text-base: 1rem (16px)
text-lg: 1.125rem (18px)
text-xl: 1.25rem (20px)
text-2xl: 1.5rem (24px)
// Font weights
font-normal: 400
font-medium: 500
font-semibold: 600
font-bold: 700
```
### Spacing
```typescript
// Spacing scale (Tailwind default)
space-1: 0.25rem (4px)
space-2: 0.5rem (8px)
space-3: 0.75rem (12px)
space-4: 1rem (16px)
space-6: 1.5rem (24px)
space-8: 2rem (32px)
```
---
## Conclusion
This component library provides all reusable UI components for ColaFlow's enterprise features:
**Component Summary**:
-**SsoButton** - SSO provider buttons with logos
-**TenantSlugInput** - Real-time slug validation
-**PasswordStrengthIndicator** - Visual password strength
-**SsoConfigForm** - Dynamic SSO configuration
-**McpPermissionMatrix** - Permission checkbox grid
-**TokenDisplay** - Token copy/download with warnings
**Design Principles**:
- ✅ Consistent with shadcn/ui design system
- ✅ Accessible (keyboard navigation, ARIA labels)
- ✅ Responsive (mobile-first approach)
- ✅ Type-safe (full TypeScript support)
- ✅ Performant (optimized re-renders)
**Next Steps**:
1. Implement these components in your project
2. Add Storybook documentation (optional)
3. Write unit tests for critical components
4. Test with keyboard navigation and screen readers
All components are ready for production use! 🚀