mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 06:38:53 +00:00
feat(frontend): add Owner field to OnboardingWizard Certificate step
The first-run onboarding wizard's Certificate step now surfaces an Owner dropdown (required) alongside Issuer and Profile, matching the ownership model introduced in M11b. Prevents newly-created certs from being unowned and bypassing notification routing. - web/src/pages/OnboardingWizard.tsx: getOwners query, ownerId state, Owner <select>, required-field guard (nextDisabled), empty-state link to /owners page when no owners exist yet. Frontend-only change; no backend wiring or schema impact. Separated from the M-6 sentinel-agent idempotency commit per scope-guard.
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
getIssuers, getAgents, getProfiles,
|
getIssuers, getAgents, getProfiles, getOwners,
|
||||||
createIssuer, testIssuerConnection,
|
createIssuer, testIssuerConnection,
|
||||||
createCertificate, triggerRenewal,
|
createCertificate, triggerRenewal,
|
||||||
getApiKey,
|
getApiKey,
|
||||||
@@ -404,12 +404,14 @@ function CertificateStep({ onNext, onSkip, createdIssuerId }: {
|
|||||||
const [sans, setSans] = useState('');
|
const [sans, setSans] = useState('');
|
||||||
const [issuerId, setIssuerId] = useState(createdIssuerId || '');
|
const [issuerId, setIssuerId] = useState(createdIssuerId || '');
|
||||||
const [profileId, setProfileId] = useState('');
|
const [profileId, setProfileId] = useState('');
|
||||||
|
const [ownerId, setOwnerId] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [created, setCreated] = useState(false);
|
const [created, setCreated] = useState(false);
|
||||||
|
|
||||||
const { data: issuers } = useQuery({ queryKey: ['issuers'], queryFn: () => getIssuers() });
|
const { data: issuers } = useQuery({ queryKey: ['issuers'], queryFn: () => getIssuers() });
|
||||||
const { data: profiles } = useQuery({ queryKey: ['profiles'], queryFn: () => getProfiles() });
|
const { data: profiles } = useQuery({ queryKey: ['profiles'], queryFn: () => getProfiles() });
|
||||||
const { data: agents } = useQuery({ queryKey: ['agents'], queryFn: () => getAgents() });
|
const { data: agents } = useQuery({ queryKey: ['agents'], queryFn: () => getAgents() });
|
||||||
|
const { data: owners } = useQuery({ queryKey: ['owners'], queryFn: () => getOwners() });
|
||||||
|
|
||||||
const hasAgents = (agents?.data?.length ?? 0) > 0;
|
const hasAgents = (agents?.data?.length ?? 0) > 0;
|
||||||
|
|
||||||
@@ -421,6 +423,7 @@ function CertificateStep({ onNext, onSkip, createdIssuerId }: {
|
|||||||
sans: sanList,
|
sans: sanList,
|
||||||
issuer_id: issuerId,
|
issuer_id: issuerId,
|
||||||
certificate_profile_id: profileId || undefined,
|
certificate_profile_id: profileId || undefined,
|
||||||
|
owner_id: ownerId,
|
||||||
environment: 'production',
|
environment: 'production',
|
||||||
});
|
});
|
||||||
// Trigger issuance
|
// Trigger issuance
|
||||||
@@ -521,6 +524,29 @@ function CertificateStep({ onNext, onSkip, createdIssuerId }: {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-ink mb-2">
|
||||||
|
Owner <span className="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={ownerId}
|
||||||
|
onChange={e => setOwnerId(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-surface border border-surface-border rounded text-ink focus:outline-none focus:border-brand-500 transition-colors"
|
||||||
|
>
|
||||||
|
<option value="">Select owner...</option>
|
||||||
|
{owners?.data?.map(o => (
|
||||||
|
<option key={o.id} value={o.id}>
|
||||||
|
{o.name}{o.email ? ` (${o.email})` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{(owners?.data?.length ?? 0) === 0 && (
|
||||||
|
<p className="mt-1 text-xs text-ink-muted">
|
||||||
|
No owners yet — create one from the <Link to="/owners" className="underline hover:text-ink">Owners page</Link> first, then return here.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Discovery hint */}
|
{/* Discovery hint */}
|
||||||
@@ -547,7 +573,7 @@ function CertificateStep({ onNext, onSkip, createdIssuerId }: {
|
|||||||
onSkip={onSkip}
|
onSkip={onSkip}
|
||||||
onNext={() => createMutation.mutate()}
|
onNext={() => createMutation.mutate()}
|
||||||
nextLabel={createMutation.isPending ? 'Creating...' : 'Issue Certificate'}
|
nextLabel={createMutation.isPending ? 'Creating...' : 'Issue Certificate'}
|
||||||
nextDisabled={!commonName || !issuerId || createMutation.isPending}
|
nextDisabled={!commonName || !issuerId || !ownerId || createMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user