192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
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");
|
|
}
|