Sync GovOPlaN module state
This commit is contained in:
31
webui/package.json
Normal file
31
webui/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@govoplan/idm-webui",
|
||||
"version": "0.1.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./styles/idm.css": "./src/styles/idm.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.6",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.1",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
147
webui/src/api/idm.ts
Normal file
147
webui/src/api/idm.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
|
||||
|
||||
export type OrganizationUnitItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
unit_type_id?: string | null;
|
||||
parent_id?: string | null;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationFunctionItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
function_type_id?: string | null;
|
||||
organization_unit_id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
delegable: boolean;
|
||||
act_in_place_allowed: boolean;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationModel = {
|
||||
units: OrganizationUnitItem[];
|
||||
functions: OrganizationFunctionItem[];
|
||||
};
|
||||
|
||||
export type IdentityOption = {
|
||||
id: string;
|
||||
display_name?: string | null;
|
||||
external_subject?: string | null;
|
||||
source: string;
|
||||
primary_account_id?: string | null;
|
||||
account_ids: string[];
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type IdentityListResponse = {
|
||||
identities: IdentityOption[];
|
||||
};
|
||||
|
||||
export type OrganizationFunctionAssignmentItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
identity_id: string;
|
||||
account_id?: string | null;
|
||||
function_id: string;
|
||||
organization_unit_id: string;
|
||||
applies_to_subunits: boolean;
|
||||
source: string;
|
||||
delegated_from_assignment_id?: string | null;
|
||||
acting_for_account_id?: string | null;
|
||||
valid_from?: string | null;
|
||||
valid_until?: string | null;
|
||||
is_active: boolean;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type OrganizationFunctionAssignmentList = {
|
||||
assignments: OrganizationFunctionAssignmentItem[];
|
||||
};
|
||||
|
||||
export type IdmSettings = {
|
||||
tenant_id: string;
|
||||
require_assignment_change_requests: boolean;
|
||||
audit_detail_level: "summary" | "standard" | "full";
|
||||
change_retention_days?: number | null;
|
||||
settings: Record<string, unknown>;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
};
|
||||
|
||||
export type OrganizationFunctionAssignmentPayload = {
|
||||
identity_id: string;
|
||||
function_id: string;
|
||||
account_id?: string | null;
|
||||
applies_to_subunits?: boolean;
|
||||
source?: string;
|
||||
delegated_from_assignment_id?: string | null;
|
||||
acting_for_account_id?: string | null;
|
||||
valid_from?: string | null;
|
||||
valid_until?: string | null;
|
||||
is_active?: boolean;
|
||||
settings?: Record<string, unknown>;
|
||||
change_request_id?: string | null;
|
||||
};
|
||||
|
||||
export type IdmSettingsPayload = Partial<Pick<IdmSettings, "require_assignment_change_requests" | "audit_detail_level" | "change_retention_days" | "settings">>;
|
||||
|
||||
function post<T, P extends Record<string, unknown>>(settings: ApiSettings, path: string, payload: P): Promise<T> {
|
||||
return apiFetch<T>(settings, path, { method: "POST", body: JSON.stringify(payload) });
|
||||
}
|
||||
|
||||
function patch<T, P extends Record<string, unknown>>(settings: ApiSettings, path: string, payload: P): Promise<T> {
|
||||
return apiFetch<T>(settings, path, { method: "PATCH", body: JSON.stringify(payload) });
|
||||
}
|
||||
|
||||
export function getOrganizationModel(settings: ApiSettings): Promise<OrganizationModel> {
|
||||
return apiFetch<OrganizationModel>(settings, "/api/v1/organizations/model");
|
||||
}
|
||||
|
||||
export function getOrganizationFunctionAssignments(settings: ApiSettings): Promise<OrganizationFunctionAssignmentList> {
|
||||
return apiFetch<OrganizationFunctionAssignmentList>(settings, "/api/v1/idm/organization-function-assignments");
|
||||
}
|
||||
|
||||
export function getIdmSettings(settings: ApiSettings): Promise<IdmSettings> {
|
||||
return apiFetch<IdmSettings>(settings, "/api/v1/idm/settings");
|
||||
}
|
||||
|
||||
export function patchIdmSettings(settings: ApiSettings, payload: IdmSettingsPayload): Promise<IdmSettings> {
|
||||
return patch(settings, "/api/v1/idm/settings", payload);
|
||||
}
|
||||
|
||||
export function searchOrganizationIdentityOptions(settings: ApiSettings, query = "", limit = 50): Promise<IdentityListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
const trimmed = query.trim();
|
||||
if (trimmed) params.set("query", trimmed);
|
||||
params.set("limit", String(limit));
|
||||
return apiFetch<IdentityListResponse>(settings, `/api/v1/idm/organization-identities?${params.toString()}`);
|
||||
}
|
||||
|
||||
export function createOrganizationFunctionAssignment(
|
||||
settings: ApiSettings,
|
||||
payload: OrganizationFunctionAssignmentPayload
|
||||
): Promise<OrganizationFunctionAssignmentItem> {
|
||||
return post(settings, "/api/v1/idm/organization-function-assignments", payload);
|
||||
}
|
||||
|
||||
export function patchOrganizationFunctionAssignment(
|
||||
settings: ApiSettings,
|
||||
id: string,
|
||||
payload: Partial<OrganizationFunctionAssignmentPayload>
|
||||
): Promise<OrganizationFunctionAssignmentItem> {
|
||||
return patch(settings, `/api/v1/idm/organization-function-assignments/${encodeURIComponent(id)}`, payload);
|
||||
}
|
||||
730
webui/src/features/IdmPage.tsx
Normal file
730
webui/src/features/IdmPage.tsx
Normal file
@@ -0,0 +1,730 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react";
|
||||
import { Edit3, RefreshCw, Save, XCircle } from "lucide-react";
|
||||
import {
|
||||
ApiError,
|
||||
Button,
|
||||
Card,
|
||||
DataGrid,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
LoadingFrame,
|
||||
PageTitle,
|
||||
StatusBadge,
|
||||
hasScope,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings,
|
||||
type AuthInfo,
|
||||
type DataGridColumn
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
createOrganizationFunctionAssignment,
|
||||
getIdmSettings,
|
||||
getOrganizationFunctionAssignments,
|
||||
getOrganizationModel,
|
||||
patchIdmSettings,
|
||||
patchOrganizationFunctionAssignment,
|
||||
searchOrganizationIdentityOptions,
|
||||
type IdmSettings,
|
||||
type IdentityOption,
|
||||
type OrganizationFunctionAssignmentItem,
|
||||
type OrganizationFunctionAssignmentPayload,
|
||||
type OrganizationFunctionItem,
|
||||
type OrganizationModel,
|
||||
type OrganizationUnitItem
|
||||
} from "../api/idm";
|
||||
|
||||
type IdmPageProps = {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
};
|
||||
|
||||
type AssignmentDraft = {
|
||||
identity_id: string;
|
||||
account_id: string;
|
||||
function_id: string;
|
||||
applies_to_subunits: boolean;
|
||||
source: string;
|
||||
delegated_from_assignment_id: string;
|
||||
acting_for_account_id: string;
|
||||
is_active: boolean;
|
||||
};
|
||||
|
||||
type SettingsDraft = {
|
||||
require_assignment_change_requests: boolean;
|
||||
audit_detail_level: "summary" | "standard" | "full";
|
||||
change_retention_days: string;
|
||||
};
|
||||
|
||||
const EMPTY_MODEL: OrganizationModel = {
|
||||
units: [],
|
||||
functions: []
|
||||
};
|
||||
|
||||
const DEFAULT_IDM_SETTINGS: IdmSettings = {
|
||||
tenant_id: "",
|
||||
require_assignment_change_requests: false,
|
||||
audit_detail_level: "standard",
|
||||
change_retention_days: null,
|
||||
settings: {}
|
||||
};
|
||||
|
||||
const SOURCE_OPTIONS = [
|
||||
{ value: "direct", label: "i18n:govoplan-idm.direct.7b2b1f51" },
|
||||
{ value: "delegated", label: "i18n:govoplan-idm.delegated.b189f4b6" },
|
||||
{ value: "acting_for", label: "i18n:govoplan-idm.acting_for.8650e6a6" },
|
||||
{ value: "directory", label: "i18n:govoplan-idm.directory.12e2e859" },
|
||||
{ value: "governance", label: "i18n:govoplan-idm.governance.b989a277" },
|
||||
{ value: "system", label: "i18n:govoplan-idm.system.70c07e3f" }
|
||||
];
|
||||
|
||||
function emptyAssignmentDraft(): AssignmentDraft {
|
||||
return {
|
||||
identity_id: "",
|
||||
account_id: "",
|
||||
function_id: "",
|
||||
applies_to_subunits: false,
|
||||
source: "direct",
|
||||
delegated_from_assignment_id: "",
|
||||
acting_for_account_id: "",
|
||||
is_active: true
|
||||
};
|
||||
}
|
||||
|
||||
function textOrNull(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function assignmentPayload(draft: AssignmentDraft): OrganizationFunctionAssignmentPayload {
|
||||
return {
|
||||
identity_id: draft.identity_id,
|
||||
account_id: textOrNull(draft.account_id),
|
||||
function_id: draft.function_id,
|
||||
applies_to_subunits: draft.applies_to_subunits,
|
||||
source: draft.source,
|
||||
delegated_from_assignment_id: textOrNull(draft.delegated_from_assignment_id),
|
||||
acting_for_account_id: textOrNull(draft.acting_for_account_id),
|
||||
is_active: draft.is_active,
|
||||
settings: {}
|
||||
};
|
||||
}
|
||||
|
||||
function assignmentDraftFrom(item: OrganizationFunctionAssignmentItem): AssignmentDraft {
|
||||
return {
|
||||
identity_id: item.identity_id,
|
||||
account_id: item.account_id ?? "",
|
||||
function_id: item.function_id,
|
||||
applies_to_subunits: item.applies_to_subunits,
|
||||
source: item.source || "direct",
|
||||
delegated_from_assignment_id: item.delegated_from_assignment_id ?? "",
|
||||
acting_for_account_id: item.acting_for_account_id ?? "",
|
||||
is_active: item.is_active
|
||||
};
|
||||
}
|
||||
|
||||
function isAssignmentDirty(draft: AssignmentDraft): boolean {
|
||||
return Boolean(
|
||||
draft.identity_id ||
|
||||
draft.account_id ||
|
||||
draft.function_id ||
|
||||
draft.applies_to_subunits ||
|
||||
draft.source !== "direct" ||
|
||||
draft.delegated_from_assignment_id ||
|
||||
draft.acting_for_account_id ||
|
||||
!draft.is_active
|
||||
);
|
||||
}
|
||||
|
||||
function settingsDraftFrom(item: IdmSettings | null): SettingsDraft {
|
||||
const source = item ?? DEFAULT_IDM_SETTINGS;
|
||||
return {
|
||||
require_assignment_change_requests: source.require_assignment_change_requests,
|
||||
audit_detail_level: source.audit_detail_level,
|
||||
change_retention_days: source.change_retention_days == null ? "" : String(source.change_retention_days)
|
||||
};
|
||||
}
|
||||
|
||||
function settingsPayload(draft: SettingsDraft, item: IdmSettings | null): Pick<IdmSettings, "require_assignment_change_requests" | "audit_detail_level" | "change_retention_days" | "settings"> {
|
||||
const trimmedDays = draft.change_retention_days.trim();
|
||||
return {
|
||||
require_assignment_change_requests: draft.require_assignment_change_requests,
|
||||
audit_detail_level: draft.audit_detail_level,
|
||||
change_retention_days: trimmedDays ? Number(trimmedDays) : null,
|
||||
settings: item?.settings ?? {}
|
||||
};
|
||||
}
|
||||
|
||||
function isSettingsDirty(draft: SettingsDraft, item: IdmSettings | null): boolean {
|
||||
const baseline = settingsDraftFrom(item);
|
||||
return (
|
||||
draft.require_assignment_change_requests !== baseline.require_assignment_change_requests ||
|
||||
draft.audit_detail_level !== baseline.audit_detail_level ||
|
||||
draft.change_retention_days.trim() !== baseline.change_retention_days.trim()
|
||||
);
|
||||
}
|
||||
|
||||
function mapById<T extends { id: string }>(items: T[]): Map<string, T> {
|
||||
return new Map(items.map((item) => [item.id, item]));
|
||||
}
|
||||
|
||||
function apiErrorMessage(error: unknown): string {
|
||||
if (error instanceof ApiError) {
|
||||
try {
|
||||
const parsed = JSON.parse(error.body) as { detail?: string | { message?: string } };
|
||||
if (typeof parsed.detail === "string") return parsed.detail;
|
||||
if (parsed.detail && typeof parsed.detail.message === "string") return parsed.detail.message;
|
||||
} catch {
|
||||
// Fall back to the transport message below.
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function identityDisplay(identity: IdentityOption | undefined, fallbackId: string): JSX.Element {
|
||||
if (!identity) return <span className="idm-id">{fallbackId}</span>;
|
||||
const label = identity.display_name || identity.external_subject || identity.id;
|
||||
return (
|
||||
<span className="idm-identity">
|
||||
<span>{label}</span>
|
||||
<span className="idm-id">{identity.id}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function functionLabel(item: OrganizationFunctionItem, unitById: ReadonlyMap<string, OrganizationUnitItem>): string {
|
||||
const unitName = unitById.get(item.organization_unit_id)?.name;
|
||||
return unitName ? `${item.name} (${unitName})` : item.name;
|
||||
}
|
||||
|
||||
function assignmentOptionLabel(
|
||||
item: OrganizationFunctionAssignmentItem,
|
||||
identityById: ReadonlyMap<string, IdentityOption>,
|
||||
functionById: ReadonlyMap<string, OrganizationFunctionItem>,
|
||||
unitById: ReadonlyMap<string, OrganizationUnitItem>
|
||||
): string {
|
||||
const identity = identityById.get(item.identity_id);
|
||||
const identityLabel = identity?.display_name || identity?.external_subject || item.identity_id;
|
||||
const functionItem = functionById.get(item.function_id);
|
||||
const functionText = functionItem ? functionLabel(functionItem, unitById) : item.function_id;
|
||||
return `${identityLabel} - ${functionText}`;
|
||||
}
|
||||
|
||||
function activeStatus(active: boolean): JSX.Element {
|
||||
return <StatusBadge status={active ? "success" : "inactive"} label={active ? "i18n:govoplan-idm.active.7bd0e9f8" : "i18n:govoplan-idm.inactive.1baa5fba"} />;
|
||||
}
|
||||
|
||||
function sourceLabel(value: string): string {
|
||||
return SOURCE_OPTIONS.find((item) => item.value === value)?.label ?? value;
|
||||
}
|
||||
|
||||
export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
const [model, setModel] = useState<OrganizationModel>(EMPTY_MODEL);
|
||||
const [assignments, setAssignments] = useState<OrganizationFunctionAssignmentItem[]>([]);
|
||||
const [idmSettings, setIdmSettings] = useState<IdmSettings | null>(null);
|
||||
const [identityOptions, setIdentityOptions] = useState<IdentityOption[]>([]);
|
||||
const [identitySearch, setIdentitySearch] = useState("");
|
||||
const [identityLoading, setIdentityLoading] = useState(false);
|
||||
const [identityLookupAvailable, setIdentityLookupAvailable] = useState(true);
|
||||
const [actingForSearch, setActingForSearch] = useState("");
|
||||
const [actingForOptions, setActingForOptions] = useState<IdentityOption[]>([]);
|
||||
const [actingForLoading, setActingForLoading] = useState(false);
|
||||
const [assignmentDraft, setAssignmentDraft] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
|
||||
const [settingsDraft, setSettingsDraft] = useState<SettingsDraft>(() => settingsDraftFrom(null));
|
||||
const [editingAssignmentId, setEditingAssignmentId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
const canManage = hasScope(auth, "idm:organization_assignment:write") || hasScope(auth, "organizations:function:assign");
|
||||
const canSearchIdentities = canManage || hasScope(auth, "idm:organization_identity:read") || hasScope(auth, "admin:users:read");
|
||||
const canReadSettings = hasScope(auth, "idm:settings:read") || hasScope(auth, "idm:settings:write") || hasScope(auth, "idm:organization_assignment:read") || hasScope(auth, "idm:organization_assignment:write");
|
||||
const canManageSettings = hasScope(auth, "idm:settings:write");
|
||||
const unitById = useMemo(() => mapById(model.units), [model.units]);
|
||||
const functionById = useMemo(() => mapById(model.functions), [model.functions]);
|
||||
const identityOptionById = useMemo(() => mapById(identityOptions), [identityOptions]);
|
||||
const actingForOptionById = useMemo(() => mapById(actingForOptions), [actingForOptions]);
|
||||
const selectedIdentity = identityOptionById.get(assignmentDraft.identity_id);
|
||||
|
||||
const identitySelectOptions = useMemo(() => {
|
||||
if (!assignmentDraft.identity_id || identityOptionById.has(assignmentDraft.identity_id)) return identityOptions;
|
||||
return [
|
||||
{
|
||||
id: assignmentDraft.identity_id,
|
||||
display_name: assignmentDraft.identity_id,
|
||||
external_subject: null,
|
||||
source: "local",
|
||||
primary_account_id: assignmentDraft.account_id || null,
|
||||
account_ids: assignmentDraft.account_id ? [assignmentDraft.account_id] : [],
|
||||
status: "active"
|
||||
},
|
||||
...identityOptions
|
||||
];
|
||||
}, [assignmentDraft.account_id, assignmentDraft.identity_id, identityOptionById, identityOptions]);
|
||||
|
||||
const accountIds = useMemo(() => {
|
||||
const values = new Set(selectedIdentity?.account_ids ?? []);
|
||||
if (assignmentDraft.account_id) values.add(assignmentDraft.account_id);
|
||||
return Array.from(values).sort((left, right) => left.localeCompare(right));
|
||||
}, [assignmentDraft.account_id, selectedIdentity]);
|
||||
const assignmentOptions = useMemo(
|
||||
() => assignments.filter((item) => item.id !== editingAssignmentId && (!assignmentDraft.function_id || item.function_id === assignmentDraft.function_id)),
|
||||
[assignmentDraft.function_id, assignments, editingAssignmentId]
|
||||
);
|
||||
const assignmentById = useMemo(() => mapById(assignments), [assignments]);
|
||||
const sourceAssignment = assignmentDraft.delegated_from_assignment_id ? assignmentById.get(assignmentDraft.delegated_from_assignment_id) : undefined;
|
||||
const actingForAccountIds = useMemo(() => {
|
||||
const values = new Set<string>();
|
||||
if (sourceAssignment?.account_id) values.add(sourceAssignment.account_id);
|
||||
const sourceIdentity = sourceAssignment ? identityOptionById.get(sourceAssignment.identity_id) ?? actingForOptionById.get(sourceAssignment.identity_id) : undefined;
|
||||
for (const accountId of sourceIdentity?.account_ids ?? []) values.add(accountId);
|
||||
for (const identity of actingForOptions) {
|
||||
for (const accountId of identity.account_ids) values.add(accountId);
|
||||
}
|
||||
if (assignmentDraft.acting_for_account_id) values.add(assignmentDraft.acting_for_account_id);
|
||||
return Array.from(values).sort((left, right) => left.localeCompare(right));
|
||||
}, [actingForOptionById, actingForOptions, assignmentDraft.acting_for_account_id, identityOptionById, sourceAssignment]);
|
||||
const initialFunctionFilter = useMemo(() => {
|
||||
if (typeof window === "undefined") return "";
|
||||
return new URLSearchParams(window.location.search).get("function_id") || "";
|
||||
}, []);
|
||||
const hasDirtyAssignmentDraft = isAssignmentDirty(assignmentDraft);
|
||||
const hasDirtySettingsDraft = canReadSettings && isSettingsDirty(settingsDraft, idmSettings);
|
||||
const hasDirtyDraft = hasDirtyAssignmentDraft || hasDirtySettingsDraft;
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const [nextModel, nextAssignments, nextSettings] = await Promise.all([
|
||||
getOrganizationModel(settings),
|
||||
getOrganizationFunctionAssignments(settings),
|
||||
canReadSettings ? getIdmSettings(settings).catch(() => null) : Promise.resolve(null)
|
||||
]);
|
||||
setModel(nextModel);
|
||||
setAssignments(nextAssignments.assignments);
|
||||
setIdmSettings(nextSettings);
|
||||
if (nextSettings) setSettingsDraft(settingsDraftFrom(nextSettings));
|
||||
if (initialFunctionFilter && nextModel.functions.some((item) => item.id === initialFunctionFilter)) {
|
||||
setAssignmentDraft((draft) => draft.function_id ? draft : { ...draft, function_id: initialFunctionFilter });
|
||||
}
|
||||
if (canSearchIdentities) {
|
||||
try {
|
||||
const identities = await searchOrganizationIdentityOptions(settings, "", 100);
|
||||
setIdentityOptions(identities.identities);
|
||||
setIdentityLookupAvailable(true);
|
||||
} catch {
|
||||
setIdentityLookupAvailable(false);
|
||||
}
|
||||
} else {
|
||||
setIdentityLookupAvailable(false);
|
||||
}
|
||||
} catch (caught) {
|
||||
setError(apiErrorMessage(caught));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canReadSettings, canSearchIdentities, initialFunctionFilter, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
}, [loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canSearchIdentities) return;
|
||||
let disposed = false;
|
||||
setIdentityLoading(true);
|
||||
searchOrganizationIdentityOptions(settings, identitySearch, 50)
|
||||
.then((payload) => {
|
||||
if (!disposed) {
|
||||
setIdentityOptions(payload.identities);
|
||||
setIdentityLookupAvailable(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!disposed) setIdentityLookupAvailable(false);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!disposed) setIdentityLoading(false);
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, [canSearchIdentities, identitySearch, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canSearchIdentities || assignmentDraft.source !== "acting_for") return;
|
||||
let disposed = false;
|
||||
setActingForLoading(true);
|
||||
searchOrganizationIdentityOptions(settings, actingForSearch, 50)
|
||||
.then((payload) => {
|
||||
if (!disposed) setActingForOptions(payload.identities);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!disposed) setActingForOptions([]);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!disposed) setActingForLoading(false);
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, [actingForSearch, assignmentDraft.source, canSearchIdentities, settings]);
|
||||
|
||||
const runAction = useCallback(async (action: () => Promise<unknown>, successMessage = ""): Promise<boolean> => {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
await action();
|
||||
if (successMessage) setSuccess(successMessage);
|
||||
await loadData();
|
||||
return true;
|
||||
} catch (caught) {
|
||||
setError(apiErrorMessage(caught));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [loadData]);
|
||||
|
||||
const discardAssignmentDraft = useCallback(() => {
|
||||
setAssignmentDraft(emptyAssignmentDraft());
|
||||
setEditingAssignmentId(null);
|
||||
}, []);
|
||||
|
||||
const discardDrafts = useCallback(() => {
|
||||
discardAssignmentDraft();
|
||||
setSettingsDraft(settingsDraftFrom(idmSettings));
|
||||
}, [discardAssignmentDraft, idmSettings]);
|
||||
|
||||
const submitSettings = useCallback(async (): Promise<boolean> => {
|
||||
if (!hasDirtySettingsDraft) return true;
|
||||
if (!canManageSettings) {
|
||||
setError("i18n:govoplan-idm.settings_write_permission_required.37f37efa");
|
||||
return false;
|
||||
}
|
||||
const ok = await runAction(
|
||||
() => patchIdmSettings(settings, settingsPayload(settingsDraft, idmSettings)),
|
||||
"i18n:govoplan-idm.settings_updated.9bbd0867"
|
||||
);
|
||||
return ok;
|
||||
}, [canManageSettings, hasDirtySettingsDraft, idmSettings, runAction, settings, settingsDraft]);
|
||||
|
||||
const submitAssignment = useCallback(async (event?: FormEvent): Promise<boolean> => {
|
||||
event?.preventDefault();
|
||||
if (!canManage) {
|
||||
setError("i18n:govoplan-idm.write_permission_required.c7dde7c6");
|
||||
return false;
|
||||
}
|
||||
if (!assignmentDraft.identity_id) {
|
||||
setError("i18n:govoplan-idm.identity_is_required.6ad4ee23");
|
||||
return false;
|
||||
}
|
||||
if (!assignmentDraft.function_id) {
|
||||
setError("i18n:govoplan-idm.function_is_required.5cce5b41");
|
||||
return false;
|
||||
}
|
||||
const payload = assignmentPayload(assignmentDraft);
|
||||
const ok = await runAction(
|
||||
() => editingAssignmentId ? patchOrganizationFunctionAssignment(settings, editingAssignmentId, payload) : createOrganizationFunctionAssignment(settings, payload),
|
||||
editingAssignmentId ? "i18n:govoplan-idm.assignment_updated.fbbf9bd6" : "i18n:govoplan-idm.assignment_added.91f1ee42"
|
||||
);
|
||||
if (ok) discardAssignmentDraft();
|
||||
return ok;
|
||||
}, [assignmentDraft, canManage, discardAssignmentDraft, editingAssignmentId, runAction, settings]);
|
||||
|
||||
const saveDrafts = useCallback(async (): Promise<boolean> => {
|
||||
if (hasDirtyAssignmentDraft && !(await submitAssignment())) return false;
|
||||
if (hasDirtySettingsDraft && !(await submitSettings())) return false;
|
||||
return true;
|
||||
}, [hasDirtyAssignmentDraft, hasDirtySettingsDraft, submitAssignment, submitSettings]);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: hasDirtyDraft,
|
||||
onSave: saveDrafts,
|
||||
onDiscard: discardDrafts,
|
||||
title: "i18n:govoplan-core.unsaved_changes.29267269",
|
||||
message: "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b"
|
||||
});
|
||||
|
||||
const editAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
||||
setEditingAssignmentId(item.id);
|
||||
setAssignmentDraft(assignmentDraftFrom(item));
|
||||
}, []);
|
||||
|
||||
const toggleAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
||||
void runAction(() => patchOrganizationFunctionAssignment(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
function onIdentityChange(identityId: string) {
|
||||
const identity = identityOptionById.get(identityId);
|
||||
setAssignmentDraft({
|
||||
...assignmentDraft,
|
||||
identity_id: identityId,
|
||||
account_id: identity?.primary_account_id ?? identity?.account_ids[0] ?? ""
|
||||
});
|
||||
}
|
||||
|
||||
function onSourceChange(source: string) {
|
||||
setAssignmentDraft({
|
||||
...assignmentDraft,
|
||||
source,
|
||||
delegated_from_assignment_id: source === "delegated" || source === "acting_for" ? assignmentDraft.delegated_from_assignment_id : "",
|
||||
acting_for_account_id: source === "acting_for" ? assignmentDraft.acting_for_account_id : ""
|
||||
});
|
||||
}
|
||||
|
||||
const assignmentColumns: DataGridColumn<OrganizationFunctionAssignmentItem>[] = [
|
||||
{
|
||||
id: "identity",
|
||||
header: "i18n:govoplan-idm.identity.544a8347",
|
||||
minWidth: 220,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterValue: (row) => identityOptionById.get(row.identity_id)?.display_name ?? row.identity_id,
|
||||
render: (row) => identityDisplay(identityOptionById.get(row.identity_id), row.identity_id)
|
||||
},
|
||||
{
|
||||
id: "account",
|
||||
header: "i18n:govoplan-idm.account.2b2936f8",
|
||||
minWidth: 180,
|
||||
value: (row) => row.account_id ?? "",
|
||||
render: (row) => row.account_id ? <span className="idm-id">{row.account_id}</span> : <span className="idm-empty">i18n:govoplan-idm.none.2baf5c66</span>
|
||||
},
|
||||
{
|
||||
id: "function",
|
||||
header: "i18n:govoplan-idm.function.e67d5aba",
|
||||
minWidth: 220,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterValue: (row) => `${functionById.get(row.function_id)?.name ?? ""} ${row.function_id}`.trim(),
|
||||
render: (row) => {
|
||||
const item = functionById.get(row.function_id);
|
||||
return item ? functionLabel(item, unitById) : <span className="idm-id">{row.function_id}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "unit",
|
||||
header: "i18n:govoplan-idm.unit.a94c2fbd",
|
||||
minWidth: 180,
|
||||
render: (row) => unitById.get(row.organization_unit_id)?.name ?? <span className="idm-id">{row.organization_unit_id}</span>
|
||||
},
|
||||
{
|
||||
id: "subunits",
|
||||
header: "i18n:govoplan-idm.applies_to_subunits.2e31b50b",
|
||||
width: 150,
|
||||
render: (row) => activeStatus(row.applies_to_subunits)
|
||||
},
|
||||
{
|
||||
id: "source",
|
||||
header: "i18n:govoplan-idm.source.d15b50c9",
|
||||
width: 130,
|
||||
value: (row) => row.source,
|
||||
render: (row) => sourceLabel(row.source)
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "i18n:govoplan-idm.status.9acb445d",
|
||||
width: 120,
|
||||
render: (row) => activeStatus(row.is_active)
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
width: 220,
|
||||
sticky: "end",
|
||||
render: (row) => (
|
||||
<div className="idm-row-actions">
|
||||
<Button type="button" variant="ghost" disabled={!canManage || busy} onClick={() => editAssignment(row)} title="i18n:govoplan-idm.edit.a5a0f3cc">
|
||||
<Edit3 size={16} aria-hidden="true" /> i18n:govoplan-idm.edit.a5a0f3cc
|
||||
</Button>
|
||||
<Button type="button" variant={row.is_active ? "secondary" : "primary"} disabled={!canManage || busy} onClick={() => toggleAssignment(row)}>
|
||||
{row.is_active ? "i18n:govoplan-idm.deactivate.585777c8" : "i18n:govoplan-idm.reactivate.e4871a43"}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="content-pad idm-page">
|
||||
<div className="page-heading split idm-heading">
|
||||
<div>
|
||||
<PageTitle loading={loading}>i18n:govoplan-idm.idm.61f4a7a2</PageTitle>
|
||||
<p>i18n:govoplan-idm.identity_links_intro.45fed9dd</p>
|
||||
</div>
|
||||
<div className="idm-toolbar">
|
||||
<Button type="button" onClick={() => void loadData()} disabled={loading || busy} title="i18n:govoplan-idm.reload.870ca3ec">
|
||||
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-idm.reload.870ca3ec
|
||||
</Button>
|
||||
{hasDirtyDraft && (
|
||||
<>
|
||||
<Button type="button" onClick={() => void saveDrafts()} disabled={busy} title="i18n:govoplan-idm.save_drafts.32a0d60a">
|
||||
<Save size={16} aria-hidden="true" /> i18n:govoplan-idm.save_drafts.32a0d60a
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" onClick={discardDrafts} disabled={busy} title="i18n:govoplan-idm.discard_drafts.c0a86816">
|
||||
<XCircle size={16} aria-hidden="true" /> i18n:govoplan-idm.discard_drafts.c0a86816
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||
{success && !error && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
|
||||
{!canManage && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-idm.write_permission_required.c7dde7c6</DismissibleAlert>}
|
||||
|
||||
<LoadingFrame loading={loading || busy} label="i18n:govoplan-idm.loading_idm_assignments.0b1501bd">
|
||||
<div className="idm-layout">
|
||||
<div className="idm-editor-stack">
|
||||
<Card title="i18n:govoplan-idm.assignment_editor.e20598e7">
|
||||
<form className="idm-form-grid" onSubmit={(event) => void submitAssignment(event)}>
|
||||
<FormField label="i18n:govoplan-idm.identity_search.d3460fcf">
|
||||
<input
|
||||
value={identitySearch}
|
||||
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
||||
disabled={!canSearchIdentities || busy}
|
||||
onChange={(event) => setIdentitySearch(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.select_identity.91d31615">
|
||||
<select value={assignmentDraft.identity_id} disabled={!canManage || busy} onChange={(event) => onIdentityChange(event.target.value)}>
|
||||
<option value="">i18n:govoplan-idm.select_identity.91d31615</option>
|
||||
{identitySelectOptions.map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.display_name || item.external_subject || item.id}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.account.2b2936f8">
|
||||
<select value={assignmentDraft.account_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, account_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
|
||||
{accountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.select_function.2bec86e0">
|
||||
<select value={assignmentDraft.function_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, function_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-idm.select_function.2bec86e0</option>
|
||||
{model.functions.map((item) => <option key={item.id} value={item.id}>{functionLabel(item, unitById)}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.source.d15b50c9">
|
||||
<select value={assignmentDraft.source} disabled={!canManage || busy} onChange={(event) => onSourceChange(event.target.value)}>
|
||||
{SOURCE_OPTIONS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
{(assignmentDraft.source === "delegated" || assignmentDraft.source === "acting_for") && (
|
||||
<FormField label="i18n:govoplan-idm.delegated_from_assignment_id.20d4a548">
|
||||
<select value={assignmentDraft.delegated_from_assignment_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, delegated_from_assignment_id: event.target.value, acting_for_account_id: "" })}>
|
||||
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
|
||||
{assignmentOptions.map((item) => (
|
||||
<option key={item.id} value={item.id}>{assignmentOptionLabel(item, identityOptionById, functionById, unitById)}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
)}
|
||||
{assignmentDraft.source === "acting_for" && (
|
||||
<>
|
||||
<FormField label="i18n:govoplan-idm.acting_for_search.b7c526c7">
|
||||
<input
|
||||
value={actingForSearch}
|
||||
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
||||
disabled={!canSearchIdentities || busy}
|
||||
onChange={(event) => setActingForSearch(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.acting_for_account_id.5d7ade5b">
|
||||
<select value={assignmentDraft.acting_for_account_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, acting_for_account_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-idm.select_account.982ee1ad</option>
|
||||
{actingForAccountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
<div className="idm-check-list">
|
||||
<label>
|
||||
<input type="checkbox" checked={assignmentDraft.applies_to_subunits} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, applies_to_subunits: event.target.checked })} />
|
||||
<span>i18n:govoplan-idm.applies_to_subunits.2e31b50b</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" checked={assignmentDraft.is_active} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, is_active: event.target.checked })} />
|
||||
<span>i18n:govoplan-idm.active.7bd0e9f8</span>
|
||||
</label>
|
||||
</div>
|
||||
{!identityLookupAvailable && <p className="idm-muted wide">i18n:govoplan-idm.identity_lookup_unavailable.b76f7714</p>}
|
||||
{identityLoading && <p className="idm-muted wide">i18n:govoplan-idm.loading_identities.f3b84693</p>}
|
||||
{actingForLoading && <p className="idm-muted wide">i18n:govoplan-idm.loading_acting_for_accounts.c9894b1e</p>}
|
||||
{!model.functions.length && <p className="idm-muted wide">i18n:govoplan-idm.no_functions_available.51ba08eb</p>}
|
||||
<div className="idm-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canManage || busy}>
|
||||
{editingAssignmentId ? "i18n:govoplan-idm.update_assignment.e20f52aa" : "i18n:govoplan-idm.add_assignment.08f2a0d5"}
|
||||
</Button>
|
||||
{editingAssignmentId && (
|
||||
<Button type="button" variant="ghost" onClick={discardAssignmentDraft} disabled={busy}>i18n:govoplan-idm.cancel_edit.ea4781e0</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{canReadSettings && (
|
||||
<Card title="i18n:govoplan-idm.idm_governance.6e4f3251">
|
||||
<form className="idm-form-grid" onSubmit={(event) => { event.preventDefault(); void submitSettings(); }}>
|
||||
<div className="idm-check-list wide">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settingsDraft.require_assignment_change_requests}
|
||||
disabled={!canManageSettings || busy}
|
||||
onChange={(event) => setSettingsDraft({ ...settingsDraft, require_assignment_change_requests: event.target.checked })}
|
||||
/>
|
||||
<span>i18n:govoplan-idm.require_assignment_change_requests.697718a1</span>
|
||||
</label>
|
||||
</div>
|
||||
<FormField label="i18n:govoplan-idm.audit_detail_level.eb2e6fd2">
|
||||
<select
|
||||
value={settingsDraft.audit_detail_level}
|
||||
disabled={!canManageSettings || busy}
|
||||
onChange={(event) => setSettingsDraft({ ...settingsDraft, audit_detail_level: event.target.value as SettingsDraft["audit_detail_level"] })}
|
||||
>
|
||||
<option value="summary">i18n:govoplan-idm.summary.f1c5b7ab</option>
|
||||
<option value="standard">i18n:govoplan-idm.standard.6edc51aa</option>
|
||||
<option value="full">i18n:govoplan-idm.full.7f021a14</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.change_retention_days.4a91f7d3">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={settingsDraft.change_retention_days}
|
||||
disabled={!canManageSettings || busy}
|
||||
onChange={(event) => setSettingsDraft({ ...settingsDraft, change_retention_days: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<div className="idm-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canManageSettings || busy || !hasDirtySettingsDraft}>
|
||||
i18n:govoplan-idm.save_settings.4602c430
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card title="i18n:govoplan-idm.assignments.a0d19ec5">
|
||||
<DataGrid
|
||||
id="idm-organization-function-assignments"
|
||||
rows={assignments}
|
||||
columns={assignmentColumns}
|
||||
getRowKey={(row) => row.id}
|
||||
emptyText="i18n:govoplan-idm.no_assignments.41193ce8"
|
||||
initialFilters={initialFunctionFilter ? { function: initialFunctionFilter } : undefined}
|
||||
initialFit="container"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</LoadingFrame>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
126
webui/src/i18n/generatedTranslations.ts
Normal file
126
webui/src/i18n/generatedTranslations.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
en: {
|
||||
"i18n:govoplan-idm.account.2b2936f8": "Account",
|
||||
"i18n:govoplan-idm.active.7bd0e9f8": "Active",
|
||||
"i18n:govoplan-idm.acting_for.8650e6a6": "acting for",
|
||||
"i18n:govoplan-idm.acting_for_account_id.5d7ade5b": "Acting for account ID",
|
||||
"i18n:govoplan-idm.acting_for_search.b7c526c7": "Acting-for account search",
|
||||
"i18n:govoplan-idm.add_assignment.08f2a0d5": "Add assignment",
|
||||
"i18n:govoplan-idm.applies_to_subunits.2e31b50b": "Applies to subunits",
|
||||
"i18n:govoplan-idm.assignment_added.91f1ee42": "Assignment added.",
|
||||
"i18n:govoplan-idm.assignment_editor.e20598e7": "Assignment editor",
|
||||
"i18n:govoplan-idm.assignment_updated.fbbf9bd6": "Assignment updated.",
|
||||
"i18n:govoplan-idm.assignments.a0d19ec5": "Assignments",
|
||||
"i18n:govoplan-idm.cancel_edit.ea4781e0": "Cancel edit",
|
||||
"i18n:govoplan-idm.audit_detail_level.eb2e6fd2": "Audit detail level",
|
||||
"i18n:govoplan-idm.change_retention_days.4a91f7d3": "Change retention days",
|
||||
"i18n:govoplan-idm.deactivate.585777c8": "Deactivate",
|
||||
"i18n:govoplan-idm.delegated.b189f4b6": "delegated",
|
||||
"i18n:govoplan-idm.delegated_from_assignment_id.20d4a548": "Delegated from assignment",
|
||||
"i18n:govoplan-idm.direct.7b2b1f51": "direct",
|
||||
"i18n:govoplan-idm.directory.12e2e859": "directory",
|
||||
"i18n:govoplan-idm.discard_drafts.c0a86816": "Discard drafts",
|
||||
"i18n:govoplan-idm.edit.a5a0f3cc": "Edit",
|
||||
"i18n:govoplan-idm.function.e67d5aba": "Function",
|
||||
"i18n:govoplan-idm.function_is_required.5cce5b41": "Function is required.",
|
||||
"i18n:govoplan-idm.full.7f021a14": "Full",
|
||||
"i18n:govoplan-idm.governance.b989a277": "governance",
|
||||
"i18n:govoplan-idm.identity.544a8347": "Identity",
|
||||
"i18n:govoplan-idm.identity_is_required.6ad4ee23": "Identity is required.",
|
||||
"i18n:govoplan-idm.identity_links_intro.45fed9dd": "Link identities to organization functions. Organizations defines the functions; IDM owns who holds them.",
|
||||
"i18n:govoplan-idm.identity_lookup_unavailable.b76f7714": "Identity lookup is unavailable.",
|
||||
"i18n:govoplan-idm.identity_search.d3460fcf": "Identity search",
|
||||
"i18n:govoplan-idm.idm_governance.6e4f3251": "IDM governance",
|
||||
"i18n:govoplan-idm.idm.61f4a7a2": "IDM",
|
||||
"i18n:govoplan-idm.inactive.1baa5fba": "Inactive",
|
||||
"i18n:govoplan-idm.loading_idm_assignments.0b1501bd": "Loading IDM assignments...",
|
||||
"i18n:govoplan-idm.loading_acting_for_accounts.c9894b1e": "Loading acting-for accounts...",
|
||||
"i18n:govoplan-idm.loading_identities.f3b84693": "Loading identities...",
|
||||
"i18n:govoplan-idm.no_assignments.41193ce8": "No identity/function assignments found.",
|
||||
"i18n:govoplan-idm.no_functions_available.51ba08eb": "No organization functions are available yet.",
|
||||
"i18n:govoplan-idm.none.2baf5c66": "None",
|
||||
"i18n:govoplan-idm.reactivate.e4871a43": "Reactivate",
|
||||
"i18n:govoplan-idm.reload.870ca3ec": "Reload",
|
||||
"i18n:govoplan-idm.require_assignment_change_requests.697718a1": "Require assignment change requests",
|
||||
"i18n:govoplan-idm.save_drafts.32a0d60a": "Save drafts",
|
||||
"i18n:govoplan-idm.save_settings.4602c430": "Save settings",
|
||||
"i18n:govoplan-idm.search_identities.88a9ef15": "Search by name, subject, account, or ID",
|
||||
"i18n:govoplan-idm.select_account.982ee1ad": "Select account",
|
||||
"i18n:govoplan-idm.select_function.2bec86e0": "Select function",
|
||||
"i18n:govoplan-idm.select_identity.91d31615": "Select identity",
|
||||
"i18n:govoplan-idm.settings_updated.9bbd0867": "Settings updated.",
|
||||
"i18n:govoplan-idm.settings_write_permission_required.37f37efa": "You do not have permission to manage IDM settings.",
|
||||
"i18n:govoplan-idm.source.d15b50c9": "Source",
|
||||
"i18n:govoplan-idm.standard.6edc51aa": "Standard",
|
||||
"i18n:govoplan-idm.status.9acb445d": "Status",
|
||||
"i18n:govoplan-idm.summary.f1c5b7ab": "Summary",
|
||||
"i18n:govoplan-idm.system.70c07e3f": "system",
|
||||
"i18n:govoplan-idm.unit.a94c2fbd": "Unit",
|
||||
"i18n:govoplan-idm.update_assignment.e20f52aa": "Update assignment",
|
||||
"i18n:govoplan-idm.view_assignments.2d40d6a5": "View assignments",
|
||||
"i18n:govoplan-idm.write_permission_required.c7dde7c6": "You do not have permission to manage IDM organization assignments."
|
||||
},
|
||||
de: {
|
||||
"i18n:govoplan-idm.account.2b2936f8": "Konto",
|
||||
"i18n:govoplan-idm.active.7bd0e9f8": "Aktiv",
|
||||
"i18n:govoplan-idm.acting_for.8650e6a6": "in Vertretung",
|
||||
"i18n:govoplan-idm.acting_for_account_id.5d7ade5b": "Konto-ID der Vertretung",
|
||||
"i18n:govoplan-idm.acting_for_search.b7c526c7": "Suche nach vertretenem Konto",
|
||||
"i18n:govoplan-idm.add_assignment.08f2a0d5": "Zuordnung hinzufügen",
|
||||
"i18n:govoplan-idm.applies_to_subunits.2e31b50b": "Gilt für Untereinheiten",
|
||||
"i18n:govoplan-idm.assignment_added.91f1ee42": "Zuordnung hinzugefügt.",
|
||||
"i18n:govoplan-idm.assignment_editor.e20598e7": "Zuordnungseditor",
|
||||
"i18n:govoplan-idm.assignment_updated.fbbf9bd6": "Zuordnung aktualisiert.",
|
||||
"i18n:govoplan-idm.assignments.a0d19ec5": "Zuordnungen",
|
||||
"i18n:govoplan-idm.cancel_edit.ea4781e0": "Bearbeitung abbrechen",
|
||||
"i18n:govoplan-idm.audit_detail_level.eb2e6fd2": "Audit-Detailgrad",
|
||||
"i18n:govoplan-idm.change_retention_days.4a91f7d3": "Änderungsaufbewahrung in Tagen",
|
||||
"i18n:govoplan-idm.deactivate.585777c8": "Deaktivieren",
|
||||
"i18n:govoplan-idm.delegated.b189f4b6": "delegiert",
|
||||
"i18n:govoplan-idm.delegated_from_assignment_id.20d4a548": "Delegiert von Zuordnung",
|
||||
"i18n:govoplan-idm.direct.7b2b1f51": "direkt",
|
||||
"i18n:govoplan-idm.directory.12e2e859": "Verzeichnis",
|
||||
"i18n:govoplan-idm.discard_drafts.c0a86816": "Entwürfe verwerfen",
|
||||
"i18n:govoplan-idm.edit.a5a0f3cc": "Bearbeiten",
|
||||
"i18n:govoplan-idm.function.e67d5aba": "Funktion",
|
||||
"i18n:govoplan-idm.function_is_required.5cce5b41": "Funktion ist erforderlich.",
|
||||
"i18n:govoplan-idm.full.7f021a14": "Vollständig",
|
||||
"i18n:govoplan-idm.governance.b989a277": "Governance",
|
||||
"i18n:govoplan-idm.identity.544a8347": "Identität",
|
||||
"i18n:govoplan-idm.identity_is_required.6ad4ee23": "Identität ist erforderlich.",
|
||||
"i18n:govoplan-idm.identity_links_intro.45fed9dd": "Verknüpfe Identitäten mit Organisationsfunktionen. Organisationen definiert die Funktionen; IDM verwaltet, wer sie innehat.",
|
||||
"i18n:govoplan-idm.identity_lookup_unavailable.b76f7714": "Identitätssuche ist nicht verfügbar.",
|
||||
"i18n:govoplan-idm.identity_search.d3460fcf": "Identitätssuche",
|
||||
"i18n:govoplan-idm.idm_governance.6e4f3251": "IDM-Governance",
|
||||
"i18n:govoplan-idm.idm.61f4a7a2": "IDM",
|
||||
"i18n:govoplan-idm.inactive.1baa5fba": "Inaktiv",
|
||||
"i18n:govoplan-idm.loading_idm_assignments.0b1501bd": "IDM-Zuordnungen werden geladen...",
|
||||
"i18n:govoplan-idm.loading_acting_for_accounts.c9894b1e": "Vertretene Konten werden geladen...",
|
||||
"i18n:govoplan-idm.loading_identities.f3b84693": "Identitäten werden geladen...",
|
||||
"i18n:govoplan-idm.no_assignments.41193ce8": "Keine Identitäts-/Funktionszuordnungen gefunden.",
|
||||
"i18n:govoplan-idm.no_functions_available.51ba08eb": "Es sind noch keine Organisationsfunktionen verfügbar.",
|
||||
"i18n:govoplan-idm.none.2baf5c66": "Keine",
|
||||
"i18n:govoplan-idm.reactivate.e4871a43": "Reaktivieren",
|
||||
"i18n:govoplan-idm.reload.870ca3ec": "Neu laden",
|
||||
"i18n:govoplan-idm.require_assignment_change_requests.697718a1": "Änderungsanträge für Zuordnungen verlangen",
|
||||
"i18n:govoplan-idm.save_drafts.32a0d60a": "Entwürfe speichern",
|
||||
"i18n:govoplan-idm.save_settings.4602c430": "Einstellungen speichern",
|
||||
"i18n:govoplan-idm.search_identities.88a9ef15": "Nach Name, Subject, Konto oder ID suchen",
|
||||
"i18n:govoplan-idm.select_account.982ee1ad": "Konto auswählen",
|
||||
"i18n:govoplan-idm.select_function.2bec86e0": "Funktion auswählen",
|
||||
"i18n:govoplan-idm.select_identity.91d31615": "Identität auswählen",
|
||||
"i18n:govoplan-idm.settings_updated.9bbd0867": "Einstellungen aktualisiert.",
|
||||
"i18n:govoplan-idm.settings_write_permission_required.37f37efa": "Du hast keine Berechtigung, IDM-Einstellungen zu verwalten.",
|
||||
"i18n:govoplan-idm.source.d15b50c9": "Quelle",
|
||||
"i18n:govoplan-idm.standard.6edc51aa": "Standard",
|
||||
"i18n:govoplan-idm.status.9acb445d": "Status",
|
||||
"i18n:govoplan-idm.summary.f1c5b7ab": "Zusammenfassung",
|
||||
"i18n:govoplan-idm.system.70c07e3f": "System",
|
||||
"i18n:govoplan-idm.unit.a94c2fbd": "Einheit",
|
||||
"i18n:govoplan-idm.update_assignment.e20f52aa": "Zuordnung aktualisieren",
|
||||
"i18n:govoplan-idm.view_assignments.2d40d6a5": "Zuordnungen anzeigen",
|
||||
"i18n:govoplan-idm.write_permission_required.c7dde7c6": "Du hast keine Berechtigung, IDM-Organisationszuordnungen zu verwalten."
|
||||
}
|
||||
};
|
||||
1
webui/src/index.ts
Normal file
1
webui/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { idmModule, default } from "./module";
|
||||
72
webui/src/module.ts
Normal file
72
webui/src/module.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import { Button, type OrganizationFunctionActionsUiCapability, type PlatformWebModule } from "@govoplan/core-webui";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import "./styles/idm.css";
|
||||
|
||||
const IdmPage = lazy(() => import("./features/IdmPage"));
|
||||
|
||||
const idmReadScopes = [
|
||||
"idm:organization_assignment:read",
|
||||
"idm:organization_assignment:write",
|
||||
"organizations:function:assign"
|
||||
];
|
||||
|
||||
const translations = {
|
||||
en: generatedTranslations.en,
|
||||
de: generatedTranslations.de
|
||||
};
|
||||
|
||||
const organizationFunctionActions: OrganizationFunctionActionsUiCapability = {
|
||||
actions: [
|
||||
{
|
||||
id: "idm.view-function-assignments",
|
||||
label: "i18n:govoplan-idm.assignments.a0d19ec5",
|
||||
anyOf: idmReadScopes,
|
||||
order: 40,
|
||||
render: ({ function: item }) => createElement(
|
||||
Button,
|
||||
{
|
||||
type: "button",
|
||||
variant: "ghost",
|
||||
title: "i18n:govoplan-idm.view_assignments.2d40d6a5",
|
||||
onClick: () => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.href = `/idm?function_id=${encodeURIComponent(item.id)}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
"i18n:govoplan-idm.assignments.a0d19ec5"
|
||||
)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const idmModule: PlatformWebModule = {
|
||||
id: "idm",
|
||||
label: "i18n:govoplan-idm.idm.61f4a7a2",
|
||||
version: "0.1.6",
|
||||
dependencies: ["identity", "organizations"],
|
||||
translations,
|
||||
navItems: [
|
||||
{
|
||||
to: "/idm",
|
||||
label: "i18n:govoplan-idm.idm.61f4a7a2",
|
||||
iconName: "users",
|
||||
anyOf: idmReadScopes,
|
||||
order: 72
|
||||
}
|
||||
],
|
||||
routes: [
|
||||
{
|
||||
path: "/idm",
|
||||
anyOf: idmReadScopes,
|
||||
order: 72,
|
||||
render: ({ settings, auth }) => createElement(IdmPage, { settings, auth })
|
||||
}
|
||||
],
|
||||
uiCapabilities: {
|
||||
"organizations.functionActions": organizationFunctionActions
|
||||
}
|
||||
};
|
||||
|
||||
export default idmModule;
|
||||
100
webui/src/styles/idm.css
Normal file
100
webui/src/styles/idm.css
Normal file
@@ -0,0 +1,100 @@
|
||||
.idm-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
width: 100%;
|
||||
max-width: 1480px;
|
||||
}
|
||||
|
||||
.idm-heading {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.idm-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.idm-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(300px, 420px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.idm-editor-stack {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.idm-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.idm-form-grid .wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.idm-check-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.idm-check-list label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.idm-form-actions,
|
||||
.idm-row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.idm-row-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.idm-identity {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.idm-id {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.idm-muted {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.idm-empty {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.idm-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.idm-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user