2026-04-25 00:21:07 +02:00
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { Card, Input, Button, FormField, Alert } from '@cameleer/design-system';
|
|
|
|
|
import cameleerLogo from '@cameleer/design-system/assets/cameleer-logo.svg';
|
|
|
|
|
import { api } from '../api/client';
|
|
|
|
|
import { toSlug } from '../utils/slug';
|
|
|
|
|
import styles from './OnboardingPage.module.css';
|
|
|
|
|
|
|
|
|
|
interface TenantResponse {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
slug: string;
|
|
|
|
|
status: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function OnboardingPage() {
|
|
|
|
|
const [name, setName] = useState('');
|
|
|
|
|
const [slug, setSlug] = useState('');
|
|
|
|
|
const [slugTouched, setSlugTouched] = useState(false);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!slugTouched) {
|
|
|
|
|
setSlug(toSlug(name));
|
|
|
|
|
}
|
|
|
|
|
}, [name, slugTouched]);
|
|
|
|
|
|
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setError(null);
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
await api.post<TenantResponse>('/onboarding/tenant', { name, slug });
|
|
|
|
|
// Tenant created — force a full page reload so the Logto SDK
|
|
|
|
|
// picks up the new org membership and scopes on the next token refresh.
|
|
|
|
|
window.location.href = '/platform/';
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Failed to create tenant');
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={styles.page}>
|
|
|
|
|
<div className={styles.wrapper}>
|
2026-04-25 12:43:11 +02:00
|
|
|
<Card className={styles.card}>
|
2026-04-25 00:21:07 +02:00
|
|
|
<div className={styles.inner}>
|
|
|
|
|
<div className={styles.logo}>
|
|
|
|
|
<img src={cameleerLogo} alt="" className={styles.logoImg} />
|
|
|
|
|
Welcome to Cameleer
|
|
|
|
|
</div>
|
|
|
|
|
<p className={styles.subtitle}>
|
|
|
|
|
Set up your workspace to start monitoring your Camel routes.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<div className={styles.error}>
|
|
|
|
|
<Alert variant="error">{error}</Alert>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-25 12:43:11 +02:00
|
|
|
<form onSubmit={handleSubmit} className={styles.form} noValidate>
|
2026-04-25 00:21:07 +02:00
|
|
|
<FormField label="Organization name" htmlFor="onboard-name" required>
|
|
|
|
|
<Input
|
|
|
|
|
id="onboard-name"
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
placeholder="Acme Corp"
|
|
|
|
|
autoFocus
|
|
|
|
|
disabled={loading}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
<FormField label="URL slug" htmlFor="onboard-slug" required
|
|
|
|
|
hint="Auto-generated from name. Appears in your dashboard URL."
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
id="onboard-slug"
|
|
|
|
|
value={slug}
|
|
|
|
|
onChange={(e) => { setSlugTouched(true); setSlug(e.target.value); }}
|
|
|
|
|
placeholder="acme-corp"
|
|
|
|
|
disabled={loading}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
type="submit"
|
|
|
|
|
loading={loading}
|
|
|
|
|
disabled={loading || !name || !slug}
|
|
|
|
|
className={styles.submit}
|
|
|
|
|
>
|
|
|
|
|
{loading ? 'Creating...' : 'Create workspace'}
|
|
|
|
|
</Button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|