feat(webui): centralize resource access explanations
This commit is contained in:
@@ -26,7 +26,8 @@
|
||||
"test:dialog-focus": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/dialog-focus.test.js && node scripts/test-dialog-focus-structure.mjs",
|
||||
"test:module-capabilities": "rm -rf .module-test-build && mkdir -p .module-test-build && printf '{\"type\":\"commonjs\"}\n' > .module-test-build/package.json && tsc -p tsconfig.module-tests.json && node .module-test-build/tests/module-capabilities.test.js && node .module-test-build/tests/privacy-policy.test.js",
|
||||
"test:module-permutations": "node scripts/test-module-permutations.mjs",
|
||||
"test:mail-components": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/mail-components.test.js"
|
||||
"test:mail-components": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/mail-components.test.js",
|
||||
"test:resource-access": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/resource-access-explanation.test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||
|
||||
35
webui/src/api/resourceAccess.ts
Normal file
35
webui/src/api/resourceAccess.ts
Normal 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
|
||||
}));
|
||||
}
|
||||
34
webui/src/api/resourceAccessContracts.ts
Normal file
34
webui/src/api/resourceAccessContracts.ts
Normal 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;
|
||||
};
|
||||
84
webui/src/components/ResourceAccessExplanation.tsx
Normal file
84
webui/src/components/ResourceAccessExplanation.tsx
Normal 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);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
73
webui/tests/resource-access-explanation.test.tsx
Normal file
73
webui/tests/resource-access-explanation.test.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
function assert(condition: unknown, message = "assertion failed"): void {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function assertEqual<T>(actual: T, expected: T, message = "values should be equal"): void {
|
||||
if (actual !== expected) throw new Error(`${message}: expected ${String(expected)}, got ${String(actual)}`);
|
||||
}
|
||||
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import ResourceAccessExplanation, {
|
||||
formatResourceAccessProvenanceDetails,
|
||||
resourceAccessProvenanceKindLabel
|
||||
} from "../src/components/ResourceAccessExplanation";
|
||||
|
||||
const loadingMarkup = renderToStaticMarkup(<ResourceAccessExplanation loading />);
|
||||
assert(
|
||||
loadingMarkup.includes("i18n:govoplan-core.loading_access_explanation.04a7c934"),
|
||||
"loading copy is owned by Core"
|
||||
);
|
||||
|
||||
const explanationMarkup = renderToStaticMarkup(
|
||||
<ResourceAccessExplanation
|
||||
fallbackResourceLabel="Fallback resource"
|
||||
explanation={{
|
||||
user: { id: "user-1", email: "reader@example.test", display_name: "Reader" },
|
||||
resource_type: "campaign",
|
||||
resource_id: "campaign-1",
|
||||
action: "campaigns:campaign:read",
|
||||
provenance: [
|
||||
{
|
||||
kind: "resource",
|
||||
id: "campaign-1",
|
||||
label: "Quarterly notice",
|
||||
source: "campaigns",
|
||||
details: { inherited: true, limit: null }
|
||||
},
|
||||
{ kind: "right" }
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
assert(explanationMarkup.includes("Reader"), "the preferred user label is rendered");
|
||||
assert(explanationMarkup.includes("Quarterly notice"), "resource provenance overrides the fallback label");
|
||||
assert(explanationMarkup.includes("campaigns:campaign:read"), "the evaluated action is rendered");
|
||||
assert(explanationMarkup.includes("i18n:govoplan-core.permission.2f81a22d"), "known provenance kinds use Core wording");
|
||||
assert(explanationMarkup.includes("i18n:govoplan-core.no_source.6dcf9723"), "missing sources use Core wording");
|
||||
assert(!explanationMarkup.includes("govoplan-campaign"), "the shared component does not depend on Campaign wording");
|
||||
assert(!explanationMarkup.includes("govoplan-files"), "the shared component does not depend on Files wording");
|
||||
|
||||
const emptyMarkup = renderToStaticMarkup(
|
||||
<ResourceAccessExplanation
|
||||
fallbackResourceLabel="Fallback resource"
|
||||
explanation={{
|
||||
user: { id: "user-1" },
|
||||
resource_type: "file",
|
||||
resource_id: "file-1",
|
||||
action: "files:file:read",
|
||||
provenance: []
|
||||
}}
|
||||
/>
|
||||
);
|
||||
assert(emptyMarkup.includes("Fallback resource"), "the caller fallback labels resources without resource provenance");
|
||||
assert(
|
||||
emptyMarkup.includes("i18n:govoplan-core.no_access_evidence_was_returned.84a21e4e"),
|
||||
"empty evidence uses Core wording"
|
||||
);
|
||||
|
||||
assertEqual(resourceAccessProvenanceKindLabel("custom"), "custom", "unknown provenance kinds remain visible");
|
||||
assertEqual(
|
||||
formatResourceAccessProvenanceDetails({ count: 2, inherited: false, source: null, nested: { id: "role-1" } }),
|
||||
'count: 2; inherited: false; source: i18n:govoplan-core.none.6eef6648; nested: {"id":"role-1"}',
|
||||
"provenance details retain the established compact format"
|
||||
);
|
||||
@@ -21,10 +21,12 @@
|
||||
"tests/data-grid-actions.test.tsx",
|
||||
"tests/dialog-focus.test.tsx",
|
||||
"tests/mail-components.test.tsx",
|
||||
"tests/resource-access-explanation.test.tsx",
|
||||
"src/components/CredentialPanel.tsx",
|
||||
"src/components/email/EmailAddressInput.tsx",
|
||||
"src/components/PasswordField.tsx",
|
||||
"src/components/MessageDisplayPanel.tsx",
|
||||
"src/components/ResourceAccessExplanation.tsx",
|
||||
"src/components/mail/MailServerSettingsPanel.tsx",
|
||||
"src/components/Button.tsx",
|
||||
"src/components/DismissibleAlert.tsx",
|
||||
|
||||
Reference in New Issue
Block a user