mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat(workspace): real database + template entry points in Add Service
type=database now provisions a managed database via POST /databases (engine + plan pickers) instead of a hollow service row; template source navigates to the existing catalog with the project preselected (/templates?project= now seeds the deploy target). api-client gains createManagedDatabase + DatabaseEntity from generated schemas. Authored By: TDvorak <[email protected]>
This commit is contained in:
1 parent
25703592c6
commit
e6e76c1084
4 files changed
+113
-15
No files matched your search
@@ -377,7 +377,7 @@ export function TemplatesPage() {
|
||||
const [categoryFilter, setCategoryFilter] = useState('');
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const [selectedTemplateIdState, setSelectedTemplateId] = useState<string | null>(null);
|
||||
const [deployProjectIdState, setDeployProjectId] = useState('');
|
||||
const [deployProjectIdState, setDeployProjectId] = useState(searchParams.get('project') ?? '');
|
||||
const [deployNameByTemplate, setDeployNameByTemplate] = useState<Record<string, string>>({});
|
||||
const [variableValuesByTemplate, setVariableValuesByTemplate] = useState<
|
||||
Record<string, Record<string, string>>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ApiError,
|
||||
connectGitRepository,
|
||||
createGitWebhook,
|
||||
createManagedDatabase,
|
||||
createService,
|
||||
getProjectById,
|
||||
listConnectedGitRepositories,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
listServiceLogs,
|
||||
listServiceVariables,
|
||||
listServicesByProject,
|
||||
type CreateDatabaseInput,
|
||||
type CreateServiceInput,
|
||||
} from '@/lib/api-client';
|
||||
import { getDemoProjectById, getDemoServicesByProject, getDemoVariablesByProject } from '@/lib/demo-data';
|
||||
@@ -57,12 +59,26 @@ const serviceEnvironments = ['production', 'preview', 'development'] as const;
|
||||
type CreateServiceSubmit = CreateServiceInput & {
|
||||
gitProviderId?: string;
|
||||
gitRepoFullName?: string;
|
||||
database?: { engine: CreateDatabaseInput['type']; plan: CreateDatabaseInput['plan'] };
|
||||
};
|
||||
|
||||
const databaseEngines = [
|
||||
{ value: 'postgresql', label: 'PostgreSQL' },
|
||||
{ value: 'mysql', label: 'MySQL' },
|
||||
{ value: 'mariadb', label: 'MariaDB' },
|
||||
{ value: 'redis', label: 'Redis' },
|
||||
{ value: 'dragonfly', label: 'Dragonfly' },
|
||||
{ value: 'mongodb', label: 'MongoDB' },
|
||||
{ value: 'clickhouse', label: 'ClickHouse' },
|
||||
] as const;
|
||||
|
||||
const databasePlans = ['hobby', 'starter', 'standard', 'business'] as const;
|
||||
|
||||
function ServiceCreateDialog(props: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (payload: CreateServiceSubmit) => void;
|
||||
onBrowseTemplates: () => void;
|
||||
loading: boolean;
|
||||
errorMessage?: string;
|
||||
}) {
|
||||
@@ -77,6 +93,10 @@ function ServiceCreateDialog(props: {
|
||||
const [providerId, setProviderId] = useState('');
|
||||
const [repoFullName, setRepoFullName] = useState('');
|
||||
const [repoSearch, setRepoSearch] = useState('');
|
||||
const [dbEngine, setDbEngine] = useState<(typeof databaseEngines)[number]['value']>('postgresql');
|
||||
const [dbPlan, setDbPlan] = useState<(typeof databasePlans)[number]>('hobby');
|
||||
|
||||
const isDatabase = form.type === 'database';
|
||||
|
||||
const providersQuery = useQuery({
|
||||
queryKey: ['git-providers'],
|
||||
@@ -110,7 +130,7 @@ function ServiceCreateDialog(props: {
|
||||
|
||||
const canSubmit =
|
||||
Boolean(form.name.trim()) &&
|
||||
(source === 'image' || Boolean(repoFullName));
|
||||
(isDatabase ? Boolean(dbEngine) : source === 'image' || Boolean(repoFullName));
|
||||
|
||||
if (!props.open) return null;
|
||||
|
||||
@@ -172,11 +192,48 @@ function ServiceCreateDialog(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDatabase ? (
|
||||
<div className="space-y-3 p-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]/50">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium uppercase tracking-wider text-[var(--text-tertiary)] mb-2">
|
||||
Engine
|
||||
</label>
|
||||
<select
|
||||
value={dbEngine}
|
||||
onChange={(e) => setDbEngine(e.target.value as typeof dbEngine)}
|
||||
className="w-full h-11 px-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-[var(--text-primary)] focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)] transition-all"
|
||||
>
|
||||
{databaseEngines.map((engine) => (
|
||||
<option key={engine.value} value={engine.value}>{engine.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium uppercase tracking-wider text-[var(--text-tertiary)] mb-2">
|
||||
Plan
|
||||
</label>
|
||||
<select
|
||||
value={dbPlan}
|
||||
onChange={(e) => setDbPlan(e.target.value as typeof dbPlan)}
|
||||
className="w-full h-11 px-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-[var(--text-primary)] focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)] transition-all"
|
||||
>
|
||||
{databasePlans.map((plan) => (
|
||||
<option key={plan} value={plan}>{plan.charAt(0).toUpperCase() + plan.slice(1)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-muted)]">
|
||||
Provisions a managed database container. Connection details appear under Databases once running.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label className="block text-xs font-medium uppercase tracking-wider text-[var(--text-tertiary)] mb-2">
|
||||
Source
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSource('image')}
|
||||
@@ -201,10 +258,19 @@ function ServiceCreateDialog(props: {
|
||||
<GitBranch size={14} />
|
||||
Git Repository
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onBrowseTemplates}
|
||||
className="flex items-center justify-center gap-2 h-10 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm font-medium text-[var(--text-tertiary)] hover:border-[var(--border-default)] transition-colors"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{source === 'image' ? (
|
||||
{isDatabase ? null : source === 'image' ? (
|
||||
<div>
|
||||
<label className="block text-xs font-medium uppercase tracking-wider text-[var(--text-tertiary)] mb-2">
|
||||
Image <span className="normal-case text-[var(--text-muted)]">(optional)</span>
|
||||
@@ -369,19 +435,20 @@ function ServiceCreateDialog(props: {
|
||||
props.onSubmit({
|
||||
...form,
|
||||
name: form.name.trim(),
|
||||
image: source === 'image' ? form.image?.trim() : '',
|
||||
image: !isDatabase && source === 'image' ? form.image?.trim() : '',
|
||||
command: form.command?.trim(),
|
||||
git_repo: source === 'git' ? form.git_repo?.trim() : '',
|
||||
git_branch: source === 'git' ? form.git_branch?.trim() : '',
|
||||
build_path: source === 'git' ? form.build_path?.trim() : '',
|
||||
gitProviderId: source === 'git' && repoFullName ? providerId : undefined,
|
||||
gitRepoFullName: source === 'git' ? repoFullName || undefined : undefined,
|
||||
git_repo: !isDatabase && source === 'git' ? form.git_repo?.trim() : '',
|
||||
git_branch: !isDatabase && source === 'git' ? form.git_branch?.trim() : '',
|
||||
build_path: !isDatabase && source === 'git' ? form.build_path?.trim() : '',
|
||||
gitProviderId: !isDatabase && source === 'git' && repoFullName ? providerId : undefined,
|
||||
gitRepoFullName: !isDatabase && source === 'git' ? repoFullName || undefined : undefined,
|
||||
database: isDatabase ? { engine: dbEngine, plan: dbPlan } : undefined,
|
||||
})
|
||||
}
|
||||
className="px-5 py-2 rounded-[var(--radius-md)] text-[var(--accent-on)] text-sm font-medium shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
style={{ background: 'var(--accent-primary)' }}
|
||||
>
|
||||
{props.loading ? 'Creating...' : 'Create Service'}
|
||||
{props.loading ? 'Creating...' : isDatabase ? 'Create Database' : 'Create Service'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -428,7 +495,16 @@ export function ProjectWorkspacePage() {
|
||||
|
||||
const createServiceMutation = useMutation({
|
||||
mutationFn: async (payload: CreateServiceSubmit) => {
|
||||
const { gitProviderId, gitRepoFullName, ...serviceInput } = payload;
|
||||
const { gitProviderId, gitRepoFullName, database, ...serviceInput } = payload;
|
||||
if (database) {
|
||||
await createManagedDatabase({
|
||||
name: serviceInput.name,
|
||||
type: database.engine,
|
||||
plan: database.plan,
|
||||
region: 'local',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const service = await createService(projectId, serviceInput);
|
||||
if (gitProviderId && gitRepoFullName) {
|
||||
let repoId: string | undefined;
|
||||
@@ -881,6 +957,10 @@ export function ProjectWorkspacePage() {
|
||||
loading={createServiceMutation.isPending}
|
||||
errorMessage={createServiceMutation.error ? (createServiceMutation.error as Error).message : undefined}
|
||||
onSubmit={(payload) => createServiceMutation.mutate(payload)}
|
||||
onBrowseTemplates={() => {
|
||||
setCreateOpen(false);
|
||||
navigate(`/templates?project=${projectId}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1231,6 +1231,22 @@ export async function listGitBranches(
|
||||
return payload.branches ?? [];
|
||||
}
|
||||
|
||||
export type CreateDatabaseInput = components['schemas']['CreateDatabaseRequest'];
|
||||
export type DatabaseEntity = components['schemas']['Database'];
|
||||
|
||||
export async function createManagedDatabase(
|
||||
input: CreateDatabaseInput,
|
||||
): Promise<{ id: string; status: string }> {
|
||||
const payload = await requestJson<{ id?: string; status?: string }>('/databases', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!payload.id) {
|
||||
throw new ApiError('Database response is invalid', 500);
|
||||
}
|
||||
return { id: payload.id, status: payload.status ?? 'building' };
|
||||
}
|
||||
|
||||
export type ConnectGitRepositoryInput = components['schemas']['ConnectGitRepoRequest'];
|
||||
export type CreateGitWebhookInput = components['schemas']['CreateWebhookRequest'];
|
||||
export type GitWebhookEntity = components['schemas']['GitWebhook'];
|
||||
|
||||
Reference in new issue
Block a user