feat: add governed configuration reference selectors

This commit is contained in:
2026-07-29 14:16:28 +02:00
parent 36291a57c1
commit f3e69b97ee
8 changed files with 503 additions and 47 deletions
@@ -0,0 +1,191 @@
import {
filterSearchableSelectOptions,
unavailableReferenceOption,
type ApiSettings,
type ConfigurationReferenceSelectorsUiCapability,
type ReferenceOption,
type ReferenceOptionProvider
} from "@govoplan/core-webui";
import {
fetchConfigurationChanges,
fetchTenants,
type ConfigurationChangeRequest
} from "../../api/admin";
export const configurationReferenceSelectors: ConfigurationReferenceSelectorsUiCapability = {
tenantProvider: createTenantReferenceProvider,
changeRequestProvider: createChangeRequestReferenceProvider
};
export function createTenantReferenceProvider(
settings: ApiSettings
): ReferenceOptionProvider {
async function catalogue(signal: AbortSignal): Promise<ReferenceOption[]> {
const tenants = await fetchTenants(settings);
if (signal.aborted) throw abortError();
return tenants.map((tenant) => ({
value: tenant.id,
label: tenant.name || tenant.slug || tenant.id,
description: [
tenant.slug,
tenant.is_active ? null : "Inactive",
tenant.id
].filter(Boolean).join(" · "),
kind: "tenant",
availability: tenant.is_active ? "available" : "inactive",
disabled: !tenant.is_active,
sourceModule: "tenancy",
provenance: {
tenantId: tenant.id,
active: tenant.is_active
}
}));
}
return {
async search(query, context) {
const options = await catalogue(context.signal);
return retainSelected(
filterSearchableSelectOptions(options, query, context.limit),
options,
context.selectedValues
);
},
async resolve(values, context) {
const options = await catalogue(context.signal);
const byValue = new Map(options.map((option) => [option.value, option]));
return values.map(
(value) =>
byValue.get(value)
?? unavailableReferenceOption(value, "Unavailable tenant")
);
}
};
}
export function createChangeRequestReferenceProvider(
settings: ApiSettings,
{
purpose,
tenantId
}: {
purpose: string;
tenantId?: string | null;
}
): ReferenceOptionProvider {
async function catalogue(signal: AbortSignal): Promise<{
eligible: ReferenceOption[];
all: ReferenceOption[];
}> {
const response = await fetchConfigurationChanges(settings);
if (signal.aborted) throw abortError();
const matching = response.requests.filter(
(request) =>
request.key === purpose
&& requestTargetsTenant(request, tenantId)
);
const all = matching.map(changeRequestOption);
return {
eligible: all.filter((option) => !option.disabled),
all
};
}
return {
async search(query, context) {
const options = await catalogue(context.signal);
return retainSelected(
filterSearchableSelectOptions(
options.eligible,
query,
context.limit
),
options.all,
context.selectedValues
);
},
async resolve(values, context) {
const options = await catalogue(context.signal);
const byValue = new Map(
options.all.map((option) => [option.value, option])
);
return values.map(
(value) =>
byValue.get(value)
?? unavailableReferenceOption(
value,
"Unavailable configuration request"
)
);
}
};
}
function changeRequestOption(
request: ConfigurationChangeRequest
): ReferenceOption {
const closed = request.status === "applied" || request.status === "rejected";
const eligible = request.status === "approved" && request.dry_run;
return {
value: request.id,
label: request.label || request.key,
description: [
request.status.split("_").join(" "),
request.dry_run ? null : "dry run not recorded",
formatTimestamp(request.requested_at),
`requested by ${request.requested_by}`,
request.id
].filter(Boolean).join(" · "),
searchText: `${request.id} ${request.requested_by} ${request.status}`,
kind: "configuration_change_request",
availability: closed
? "unavailable"
: eligible
? "available"
: "inactive",
disabled: !eligible,
sourceModule: "admin",
provenance: {
purpose: request.key,
target: request.target ?? {},
requestedBy: request.requested_by,
requestedAt: request.requested_at,
status: request.status,
dryRunRecorded: request.dry_run
}
};
}
function requestTargetsTenant(
request: ConfigurationChangeRequest,
tenantId?: string | null
): boolean {
if (!tenantId) return true;
const targetTenant = request.target?.tenant_id;
return !targetTenant || String(targetTenant) === tenantId;
}
function retainSelected(
matches: readonly ReferenceOption[],
catalogue: readonly ReferenceOption[],
selectedValues: readonly string[]
): ReferenceOption[] {
const result = [...matches];
const returned = new Set(result.map((option) => option.value));
const byValue = new Map(catalogue.map((option) => [option.value, option]));
for (const value of selectedValues) {
if (returned.has(value)) continue;
result.push(
byValue.get(value)
?? unavailableReferenceOption(value)
);
}
return result;
}
function formatTimestamp(value: string): string {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
}
function abortError(): DOMException {
return new DOMException("The operation was aborted.", "AbortError");
}