feat(webui): centralize resource access explanations

This commit is contained in:
2026-07-21 13:18:30 +02:00
parent 9dabd9356d
commit 6526f37aae
8 changed files with 257 additions and 1 deletions

View File

@@ -0,0 +1,35 @@
import type { ApiSettings } from "../types";
import { apiFetch, apiPath } from "./client";
import type {
AccessDecisionProvenanceItem,
ResourceAccessExplanationOptions,
ResourceAccessExplanationResponse,
ResourceAccessExplanationUser
} from "./resourceAccessContracts";
export type {
AccessDecisionProvenanceItem,
ResourceAccessExplanationOptions,
ResourceAccessExplanationResponse,
ResourceAccessExplanationUser
} from "./resourceAccessContracts";
/**
* Fetches the platform's optional resource-access explanation endpoint.
* Core owns the transport contract without requiring the Access module that
* contributes the endpoint at runtime.
*/
export function fetchResourceAccessExplanation<
TUser extends ResourceAccessExplanationUser = ResourceAccessExplanationUser,
TProvenance extends AccessDecisionProvenanceItem = AccessDecisionProvenanceItem
>(
settings: ApiSettings,
options: ResourceAccessExplanationOptions
): Promise<ResourceAccessExplanationResponse<TUser, TProvenance>> {
return apiFetch(settings, apiPath("/api/v1/admin/access/resource-explanation", {
user_id: options.userId,
resource_type: options.resourceType,
resource_id: options.resourceId,
action: options.action,
tenant_id: options.tenantId
}));
}

View File

@@ -0,0 +1,34 @@
export type ResourceAccessExplanationUser = {
id: string;
account_id?: string | null;
email?: string | null;
display_name?: string | null;
};
export type AccessDecisionProvenanceItem = {
kind: string;
id?: string | null;
label?: string | null;
tenant_id?: string | null;
source?: string | null;
details?: Record<string, unknown>;
};
export type ResourceAccessExplanationResponse<
TUser extends ResourceAccessExplanationUser = ResourceAccessExplanationUser,
TProvenance extends AccessDecisionProvenanceItem = AccessDecisionProvenanceItem
> = {
user: TUser;
resource_type: string;
resource_id: string;
action: string;
provenance: TProvenance[];
};
export type ResourceAccessExplanationOptions = {
userId: string;
resourceType: string;
resourceId: string;
action: string;
tenantId?: string | null;
};

View File

@@ -0,0 +1,84 @@
import type { ResourceAccessExplanationResponse } from "../api/resourceAccessContracts";
export type ResourceAccessExplanationProps = {
loading?: boolean;
explanation?: ResourceAccessExplanationResponse | null;
fallbackResourceLabel?: string;
};
export default function ResourceAccessExplanation({
loading = false,
explanation,
fallbackResourceLabel = ""
}: ResourceAccessExplanationProps) {
if (loading) {
return <p className="muted small-note">i18n:govoplan-core.loading_access_explanation.04a7c934</p>;
}
if (!explanation) return null;
const userLabel = explanation.user.display_name || explanation.user.email || explanation.user.id;
const resourceLabel = explanation.provenance.find((item) => item.kind === "resource")?.label ||
fallbackResourceLabel ||
explanation.resource_id;
return (
<>
<div className="form-grid compact responsive-form-grid">
<div><span className="form-label">i18n:govoplan-core.user.9f8a2389</span><p>{userLabel}</p></div>
<div><span className="form-label">i18n:govoplan-core.resource.d1c626a9</span><p>{resourceLabel}</p></div>
<div><span className="form-label">i18n:govoplan-core.action.97c89a4d</span><p><code>{explanation.action}</code></p></div>
<div><span className="form-label">i18n:govoplan-core.evidence.8487d192</span><p>{explanation.provenance.length}</p></div>
</div>
{explanation.provenance.length === 0 ? (
<p className="muted small-note">i18n:govoplan-core.no_access_evidence_was_returned.84a21e4e</p>
) : (
<div className="admin-assignment-grid">
{explanation.provenance.map((item, index) => (
<div key={`${item.kind}:${item.id ?? index}`}>
<strong>{resourceAccessProvenanceKindLabel(item.kind)}</strong>
<div className="muted small-note">
{item.source || "i18n:govoplan-core.no_source.6dcf9723"}
{item.id && <> · <code>{item.id}</code></>}
</div>
{item.label && <p>{item.label}</p>}
{Object.keys(item.details ?? {}).length > 0 && (
<p className="muted small-note">{formatResourceAccessProvenanceDetails(item.details ?? {})}</p>
)}
</div>
))}
</div>
)}
</>
);
}
export function resourceAccessProvenanceKindLabel(kind: string): string {
switch (kind) {
case "resource":
return "i18n:govoplan-core.resource.d1c626a9";
case "owner":
return "i18n:govoplan-core.owner.89ff3122";
case "share":
return "i18n:govoplan-core.share.09ca55ca";
case "policy":
return "i18n:govoplan-core.policy.0b779a05";
case "role":
return "i18n:govoplan-core.role.b5b4a5a2";
case "right":
return "i18n:govoplan-core.permission.2f81a22d";
default:
return kind;
}
}
export function formatResourceAccessProvenanceDetails(details: Record<string, unknown>): string {
return Object.entries(details)
.map(([key, value]) => `${key}: ${formatResourceAccessProvenanceValue(value)}`)
.join("; ");
}
function formatResourceAccessProvenanceValue(value: unknown): string {
if (value === null || value === undefined) return "i18n:govoplan-core.none.6eef6648";
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
return JSON.stringify(value) ?? String(value);
}

View File

@@ -6,12 +6,14 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.about.6b21fb79": "About",
"i18n:govoplan-core.accent_color.e49578ed": "Accent color",
"i18n:govoplan-core.access_is_restricted_sign_in_to_open_available_m.2a47a549": "Access is restricted. Sign in to open available modules and administration.",
"i18n:govoplan-core.access_explanation.75ee7f62": "Access explanation",
"i18n:govoplan-core.accessible_to_you.266eca4e": "Accessible to you",
"i18n:govoplan-core.account_context.5bb0a918": "Account context",
"i18n:govoplan-core.account_display_name.03ed8fc5": "Account display name",
"i18n:govoplan-core.account_id.c5e0db28": "Account ID",
"i18n:govoplan-core.account_settings.82cf8a5f": "Account settings",
"i18n:govoplan-core.account.f967543b": "ACCOUNT",
"i18n:govoplan-core.action.97c89a4d": "Action",
"i18n:govoplan-core.action_or_review_severity_when_this_rule_finds_m.2e6863d1": "Action or review severity when this rule finds multiple possible matches.",
"i18n:govoplan-core.action_or_review_severity_when_this_rule_finds_n.bc500ee1": "Action or review severity when this rule finds no matching file.",
"i18n:govoplan-core.actions.c3cd636a": "Actions",
@@ -167,6 +169,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.entry_did_not_export_a_platformwebmodule.9a157741": "entry did not export a PlatformWebModule",
"i18n:govoplan-core.entry_integrity_check_failed.1d8f6b89": "entry integrity check failed",
"i18n:govoplan-core.equals.09b6a6dc": "Equals",
"i18n:govoplan-core.evidence.8487d192": "Evidence",
"i18n:govoplan-core.excel_spreadsheet.7107fea2": "Excel spreadsheet",
"i18n:govoplan-core.expand.9869e506": "Expand",
"i18n:govoplan-core.explicit_api_url.b117b4b8": "Explicit API URL",
@@ -253,6 +256,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.less_or_equal.2860e695": "Less or equal",
"i18n:govoplan-core.less_than.1d3d412a": "Less than",
"i18n:govoplan-core.library_template.c62eb776": "Library template",
"i18n:govoplan-core.loading_access_explanation.04a7c934": "Loading access explanation...",
"i18n:govoplan-core.loading_administration_data.643bd894": "Loading administration data...",
"i18n:govoplan-core.loading_campaigns.43ea3afa": "Loading campaigns...",
"i18n:govoplan-core.loading_data.089f19c5": "Loading data…",
@@ -314,6 +318,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.next_month.8abf7cf1": "Next month",
"i18n:govoplan-core.next_page.4bfc194b": "Next page",
"i18n:govoplan-core.next_passes_will_add_functionality_here.c13caade": "Next passes will add functionality here.",
"i18n:govoplan-core.no_access_evidence_was_returned.84a21e4e": "No access evidence was returned.",
"i18n:govoplan-core.no_accessible_campaigns_found.0b74419a": "No accessible campaigns found.",
"i18n:govoplan-core.no_address_added_yet.809c4247": "No address added yet.",
"i18n:govoplan-core.no_entries_configured.48e7b97c": "No entries configured.",
@@ -323,6 +328,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.no_value_available": "No {value0} available.",
"i18n:govoplan-core.no_readable_body_content.37643a01": "No readable body content.",
"i18n:govoplan-core.no_rows_found.0c1a8d34": "No rows found.",
"i18n:govoplan-core.no_source.6dcf9723": "No source",
"i18n:govoplan-core.no_subject.49b20da0": "(no subject)",
"i18n:govoplan-core.no_values_are_configured_for_this_column.16e935e8": "No values are configured for this column.",
"i18n:govoplan-core.none.6eef6648": "None",
@@ -343,6 +349,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.optional_html_version_of_the_email_body_for_rich.3f01db97": "Optional HTML version of the email body for richer formatting.",
"i18n:govoplan-core.optional_local_alias_for_the_active_tenant_it_do.edbb7b14": "Optional local alias for the active tenant. It does not replace the global account name in the title bar.",
"i18n:govoplan-core.or_click_to_select_files.91b05dc1": "or click to select files",
"i18n:govoplan-core.owner.89ff3122": "Owner",
"i18n:govoplan-core.owner_policy.1e8df143": "Owner policy",
"i18n:govoplan-core.p_no_html_body_content_p.c305bfc6": "<p>No HTML body content.</p>",
"i18n:govoplan-core.page.fb06270f": "Page",
@@ -351,6 +358,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.password.8be3c943": "Password",
"i18n:govoplan-core.path_or_identifier_for_an_external_recipient_sou.f7da6e8f": "Path or identifier for an external recipient source such as a CSV file.",
"i18n:govoplan-core.pdf.d613d88c": "PDF",
"i18n:govoplan-core.permission.2f81a22d": "Permission",
"i18n:govoplan-core.permit_per_recipient_sender_overrides_when_build.62e46d13": "Permit per-recipient sender overrides when building messages.",
"i18n:govoplan-core.permit_recipient_rows_to_define_their_own_bcc_re.46d737dc": "Permit recipient rows to define their own BCC recipients.",
"i18n:govoplan-core.permit_recipient_rows_to_define_their_own_cc_rec.0247b3cd": "Permit recipient rows to define their own CC recipients.",
@@ -366,6 +374,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.please_wait_while_the_local_session_is_verified.1dbd8fd3": "Please wait while the local session is verified.",
"i18n:govoplan-core.png_image.6715d4b8": "PNG image",
"i18n:govoplan-core.preferences_saved.c8cd3501": "Preferences saved.",
"i18n:govoplan-core.policy.0b779a05": "Policy",
"i18n:govoplan-core.policies.8d611849": "Policies",
"i18n:govoplan-core.port.fe035157": "Port",
"i18n:govoplan-core.powerpoint_presentation.b32b7115": "PowerPoint presentation",
@@ -403,10 +412,12 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.request_failed.9fcda32c": "Request failed",
"i18n:govoplan-core.required.eed6bfb4": "Required",
"i18n:govoplan-core.resize.f52dc753": "Resize",
"i18n:govoplan-core.resource.d1c626a9": "Resource",
"i18n:govoplan-core.retention_policy_saved.eb577758": "Retention policy saved.",
"i18n:govoplan-core.retention_policy.962fb418": "Retention policy",
"i18n:govoplan-core.reusable_template_record_this_campaign_should_re.5896529b": "Reusable template record this campaign should refer to once the template backend is available.",
"i18n:govoplan-core.review_send.1627617d": "Review & Send",
"i18n:govoplan-core.role.b5b4a5a2": "Role",
"i18n:govoplan-core.rows_per_page.af2f9c1b": "Rows per page",
"i18n:govoplan-core.sa.50cf95ce": "Sa",
"i18n:govoplan-core.same_origin_proxied.c39e6e2b": "Same-origin / proxied",
@@ -428,6 +439,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.server.cb0cb170": "Server",
"i18n:govoplan-core.session_expired.b828190e": "Session expired",
"i18n:govoplan-core.session_state.b7a3d0f4": "Session state",
"i18n:govoplan-core.share.09ca55ca": "Share",
"i18n:govoplan-core.set_concrete_system_retention_values_blank_day_f.98b9a627": "Set concrete system retention values. Blank day fields mean unlimited retention.",
"i18n:govoplan-core.settings.c7f73bb5": "Settings",
"i18n:govoplan-core.show_content.0528d8d2": "Show content",
@@ -563,12 +575,14 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.about.6b21fb79": "About",
"i18n:govoplan-core.accent_color.e49578ed": "Accent color",
"i18n:govoplan-core.access_is_restricted_sign_in_to_open_available_m.2a47a549": "Access is restricted. Sign in to open available modules and administration.",
"i18n:govoplan-core.access_explanation.75ee7f62": "Zugriffserklärung",
"i18n:govoplan-core.accessible_to_you.266eca4e": "Accessible to you",
"i18n:govoplan-core.account_context.5bb0a918": "Account context",
"i18n:govoplan-core.account_display_name.03ed8fc5": "Account display name",
"i18n:govoplan-core.account_id.c5e0db28": "Account ID",
"i18n:govoplan-core.account_settings.82cf8a5f": "Account settings",
"i18n:govoplan-core.account.f967543b": "ACCOUNT",
"i18n:govoplan-core.action.97c89a4d": "Aktion",
"i18n:govoplan-core.action_or_review_severity_when_this_rule_finds_m.2e6863d1": "Action or review severity when this rule finds multiple possible matches.",
"i18n:govoplan-core.action_or_review_severity_when_this_rule_finds_n.bc500ee1": "Action or review severity when this rule finds no matching file.",
"i18n:govoplan-core.actions.c3cd636a": "Aktionen",
@@ -724,6 +738,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.entry_did_not_export_a_platformwebmodule.9a157741": "entry did not export a PlatformWebModule",
"i18n:govoplan-core.entry_integrity_check_failed.1d8f6b89": "entry integrity check failed",
"i18n:govoplan-core.equals.09b6a6dc": "Equals",
"i18n:govoplan-core.evidence.8487d192": "Nachweise",
"i18n:govoplan-core.excel_spreadsheet.7107fea2": "Excel spreadsheet",
"i18n:govoplan-core.expand.9869e506": "Expand",
"i18n:govoplan-core.explicit_api_url.b117b4b8": "Explicit API URL",
@@ -810,6 +825,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.less_or_equal.2860e695": "Less or equal",
"i18n:govoplan-core.less_than.1d3d412a": "Less than",
"i18n:govoplan-core.library_template.c62eb776": "Library template",
"i18n:govoplan-core.loading_access_explanation.04a7c934": "Zugriffserklärung wird geladen...",
"i18n:govoplan-core.loading_administration_data.643bd894": "Loading administration data...",
"i18n:govoplan-core.loading_campaigns.43ea3afa": "Loading campaigns...",
"i18n:govoplan-core.loading_data.089f19c5": "Loading data…",
@@ -871,6 +887,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.next_month.8abf7cf1": "Next month",
"i18n:govoplan-core.next_page.4bfc194b": "Next page",
"i18n:govoplan-core.next_passes_will_add_functionality_here.c13caade": "Die nächsten Durchläufe ergänzen hier die Funktionalität.",
"i18n:govoplan-core.no_access_evidence_was_returned.84a21e4e": "Es wurden keine Zugriffsnachweise zurückgegeben.",
"i18n:govoplan-core.no_accessible_campaigns_found.0b74419a": "No accessible campaigns found.",
"i18n:govoplan-core.no_address_added_yet.809c4247": "No address added yet.",
"i18n:govoplan-core.no_entries_configured.48e7b97c": "No entries configured.",
@@ -880,6 +897,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.no_value_available": "Keine {value0} verfügbar.",
"i18n:govoplan-core.no_readable_body_content.37643a01": "No readable body content.",
"i18n:govoplan-core.no_rows_found.0c1a8d34": "No rows found.",
"i18n:govoplan-core.no_source.6dcf9723": "Keine Quelle",
"i18n:govoplan-core.no_subject.49b20da0": "(no subject)",
"i18n:govoplan-core.no_values_are_configured_for_this_column.16e935e8": "No values are configured for this column.",
"i18n:govoplan-core.none.6eef6648": "Keine",
@@ -900,6 +918,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.optional_html_version_of_the_email_body_for_rich.3f01db97": "Optional HTML version of the email body for richer formatting.",
"i18n:govoplan-core.optional_local_alias_for_the_active_tenant_it_do.edbb7b14": "Optional local alias for the active tenant. It does not replace the global account name in the title bar.",
"i18n:govoplan-core.or_click_to_select_files.91b05dc1": "or click to select files",
"i18n:govoplan-core.owner.89ff3122": "Eigentümer",
"i18n:govoplan-core.owner_policy.1e8df143": "Owner policy",
"i18n:govoplan-core.p_no_html_body_content_p.c305bfc6": "<p>No HTML body content.</p>",
"i18n:govoplan-core.page.fb06270f": "Page",
@@ -908,6 +927,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.password.8be3c943": "Passwort",
"i18n:govoplan-core.path_or_identifier_for_an_external_recipient_sou.f7da6e8f": "Path or identifier for an external recipient source such as a CSV file.",
"i18n:govoplan-core.pdf.d613d88c": "PDF",
"i18n:govoplan-core.permission.2f81a22d": "Berechtigung",
"i18n:govoplan-core.permit_per_recipient_sender_overrides_when_build.62e46d13": "Permit per-recipient sender overrides when building messages.",
"i18n:govoplan-core.permit_recipient_rows_to_define_their_own_bcc_re.46d737dc": "Permit recipient rows to define their own BCC recipients.",
"i18n:govoplan-core.permit_recipient_rows_to_define_their_own_cc_rec.0247b3cd": "Permit recipient rows to define their own CC recipients.",
@@ -923,6 +943,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.please_wait_while_the_local_session_is_verified.1dbd8fd3": "Bitte warten Sie, während die lokale Sitzung geprüft wird.",
"i18n:govoplan-core.png_image.6715d4b8": "PNG image",
"i18n:govoplan-core.preferences_saved.c8cd3501": "Einstellungen gespeichert.",
"i18n:govoplan-core.policy.0b779a05": "Richtlinie",
"i18n:govoplan-core.policies.8d611849": "Richtlinien",
"i18n:govoplan-core.port.fe035157": "Port",
"i18n:govoplan-core.powerpoint_presentation.b32b7115": "PowerPoint presentation",
@@ -960,10 +981,12 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.request_failed.9fcda32c": "Request failed",
"i18n:govoplan-core.required.eed6bfb4": "Required",
"i18n:govoplan-core.resize.f52dc753": "Resize",
"i18n:govoplan-core.resource.d1c626a9": "Ressource",
"i18n:govoplan-core.retention_policy_saved.eb577758": "Retention policy saved.",
"i18n:govoplan-core.retention_policy.962fb418": "Retention policy",
"i18n:govoplan-core.reusable_template_record_this_campaign_should_re.5896529b": "Reusable template record this campaign should refer to once the template backend is available.",
"i18n:govoplan-core.review_send.1627617d": "Prüfen & Senden",
"i18n:govoplan-core.role.b5b4a5a2": "Rolle",
"i18n:govoplan-core.rows_per_page.af2f9c1b": "Rows per page",
"i18n:govoplan-core.sa.50cf95ce": "Sa",
"i18n:govoplan-core.same_origin_proxied.c39e6e2b": "Same-origin / proxied",
@@ -985,6 +1008,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.server.cb0cb170": "Server",
"i18n:govoplan-core.session_expired.b828190e": "Sitzung abgelaufen",
"i18n:govoplan-core.session_state.b7a3d0f4": "Session state",
"i18n:govoplan-core.share.09ca55ca": "Freigabe",
"i18n:govoplan-core.set_concrete_system_retention_values_blank_day_f.98b9a627": "Set concrete system retention values. Blank day fields mean unlimited retention.",
"i18n:govoplan-core.settings.c7f73bb5": "Einstellungen",
"i18n:govoplan-core.show_content.0528d8d2": "Show content",

View File

@@ -24,6 +24,7 @@ export type {
MockMailboxMessageResponse
} from "./api/mailContracts";
export * from "./api/privacyRetention";
export * from "./api/resourceAccess";
export * from "./platform/modules";
export * from "./platform/ModuleContext";
export * from "./platform/moduleEvents";
@@ -80,6 +81,8 @@ export { default as MessageDisplayPanel } from "./components/MessageDisplayPanel
export type { MessageDisplayAttachment, MessageDisplayField } from "./components/MessageDisplayPanel";
export { default as PageTitle } from "./components/PageTitle";
export { default as PasswordField } from "./components/PasswordField";
export { default as ResourceAccessExplanation, formatResourceAccessProvenanceDetails, resourceAccessProvenanceKindLabel } from "./components/ResourceAccessExplanation";
export type { ResourceAccessExplanationProps } from "./components/ResourceAccessExplanation";
export type { PasswordFieldProps } from "./components/PasswordField";
export { PolicyRow, PolicySection, PolicyTable } from "./components/PolicyTable";
export { default as PolicyPathHelp, normalizePolicySourcePathItems } from "./components/PolicyPathHelp";