feat(hosts): generate unique 8-digit asset ID
CI / Lint · Typecheck · Test · Build (push) Successful in 1m2s
CI / Playwright (smoke) (push) Has been skipped
CI / Build & push images (push) Successful in 1m21s

Add a Generate button to the host create dialog that fetches a random
8-digit asset ID from the new GET /hosts/generate-asset-id endpoint.
The service retries against the unique index so the returned ID is
guaranteed unused. Edit mode hides the button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 14:30:24 -04:00
parent 95e501a9c8
commit 0b29e706b0
5 changed files with 59 additions and 5 deletions
@@ -3,7 +3,7 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';
import { Loader2 } from 'lucide-react';
import { Loader2, Sparkles } from 'lucide-react';
import { toast } from 'sonner';
import { HostStack, HostState } from '@vector/shared';
import {
@@ -28,7 +28,7 @@ import {
SelectValue,
Textarea,
} from '@vector/ui';
import { createHost, updateHost } from '../../lib/api/hosts.js';
import { createHost, generateHostAssetId, updateHost } from '../../lib/api/hosts.js';
import { ApiRequestError } from '../../lib/api/client.js';
import { queryKeys } from '../../lib/queryKeys.js';
import type { Host } from '../../lib/api/types.js';
@@ -88,6 +88,15 @@ export function HostFormDialog({ open, onOpenChange, host }: HostFormDialogProps
});
}, [open, host, form]);
const generateMutation = useMutation({
mutationFn: () => generateHostAssetId(),
onSuccess: ({ assetId }) => {
form.setValue('assetId', assetId, { shouldDirty: true, shouldValidate: true });
},
onError: (err) =>
toast.error(err instanceof ApiRequestError ? err.body.message : 'Generate failed'),
});
const mutation = useMutation({
mutationFn: async (values: Values) => {
if (editing && host) {
@@ -136,9 +145,27 @@ export function HostFormDialog({ open, onOpenChange, host }: HostFormDialogProps
render={({ field }) => (
<FormItem>
<FormLabel>Asset ID</FormLabel>
<FormControl>
<Input autoFocus placeholder="e.g. ASSET-001" {...field} />
</FormControl>
<div className="flex gap-2">
<FormControl>
<Input autoFocus placeholder="e.g. ASSET-001" {...field} />
</FormControl>
{!editing && (
<Button
type="button"
variant="outline"
onClick={() => generateMutation.mutate()}
disabled={generateMutation.isPending}
title="Generate an unused 8-digit asset ID"
>
{generateMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Sparkles className="h-4 w-4" />
)}
Generate
</Button>
)}
</div>
<FormMessage />
</FormItem>
)}
+5
View File
@@ -42,3 +42,8 @@ export async function updateHost(id: string, input: UpdateHostRequest): Promise<
export async function deleteHost(id: string): Promise<void> {
await api.delete(`/hosts/${id}`);
}
export async function generateHostAssetId(): Promise<{ assetId: string }> {
const res = await api.get<{ assetId: string }>('/hosts/generate-asset-id');
return res.data;
}