feat(webui): centralize resource access explanations
This commit is contained in:
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);
|
||||
}
|
||||
Reference in New Issue
Block a user