feat: vendor console — tenant list, create wizard, detail page
Implements Task 9: shared components (ServerStatusBadge, UsageIndicator, platform.module.css, tierColor utility) and full vendor console pages (VendorTenantsPage, CreateTenantPage, TenantDetailPage). Build passes cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
110
ui/src/pages/vendor/CreateTenantPage.tsx
vendored
110
ui/src/pages/vendor/CreateTenantPage.tsx
vendored
@@ -1 +1,109 @@
|
||||
export function CreateTenantPage() { return <div>CreateTenantPage (TODO)</div>; }
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Button, Card, FormField, Input, useToast } from '@cameleer/design-system';
|
||||
import { useCreateTenant } from '../../api/vendor-hooks';
|
||||
import { toSlug } from '../../utils/slug';
|
||||
|
||||
const TIERS = ['STARTER', 'PROFESSIONAL', 'ENTERPRISE'];
|
||||
|
||||
export function CreateTenantPage() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const createTenant = useCreateTenant();
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [slug, setSlug] = useState('');
|
||||
const [slugTouched, setSlugTouched] = useState(false);
|
||||
const [tier, setTier] = useState('STARTER');
|
||||
|
||||
useEffect(() => {
|
||||
if (!slugTouched) {
|
||||
setSlug(toSlug(name));
|
||||
}
|
||||
}, [name, slugTouched]);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const result = await createTenant.mutateAsync({ name, slug, tier });
|
||||
toast({ title: 'Tenant created', variant: 'success' });
|
||||
navigate(`/vendor/tenants/${result.id}`);
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to create tenant', description: String(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', maxWidth: 560 }}>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<h1 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 600 }}>Create Tenant</h1>
|
||||
</div>
|
||||
|
||||
<Card title="Tenant details">
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16, padding: '8px 0' }}>
|
||||
<FormField label="Name" htmlFor="tenant-name" required>
|
||||
<Input
|
||||
id="tenant-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Acme Corp"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Slug" htmlFor="tenant-slug" required hint="URL-safe identifier, auto-generated from name">
|
||||
<Input
|
||||
id="tenant-slug"
|
||||
value={slug}
|
||||
onChange={(e) => {
|
||||
setSlug(e.target.value);
|
||||
setSlugTouched(true);
|
||||
}}
|
||||
placeholder="acme-corp"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Tier" htmlFor="tenant-tier" required>
|
||||
<select
|
||||
id="tenant-tier"
|
||||
value={tier}
|
||||
onChange={(e) => setTier(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 6,
|
||||
background: 'var(--bg-surface)',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{TIERS.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', paddingTop: 8 }}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => navigate('/vendor/tenants')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
loading={createTenant.isPending}
|
||||
disabled={!name || !slug}
|
||||
>
|
||||
Create Tenant
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user