refactor(webui): use core access explanation
This commit is contained in:
@@ -22,7 +22,7 @@
|
|||||||
"read-excel-file": "9.2.0"
|
"read-excel-file": "9.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.8",
|
"@govoplan/core-webui": "^0.1.9",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"read-excel-file": "9.2.0"
|
"read-excel-file": "9.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.8",
|
"@govoplan/core-webui": "^0.1.9",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { ApiSettings, CampaignListItem, DeltaDeletedItem } from "../types";
|
import type { ApiSettings, CampaignListItem, DeltaDeletedItem } from "../types";
|
||||||
import { apiDownload, apiFetch } from "./client";
|
import { apiDownload, apiFetch } from "./client";
|
||||||
|
export { fetchResourceAccessExplanation } from "@govoplan/core-webui";
|
||||||
|
export type {
|
||||||
|
AccessDecisionProvenanceItem,
|
||||||
|
ResourceAccessExplanationUser as AccessExplanationUser,
|
||||||
|
ResourceAccessExplanationResponse
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
|
|
||||||
export type CampaignListResponse =
|
export type CampaignListResponse =
|
||||||
CampaignListItem[] |
|
CampaignListItem[] |
|
||||||
@@ -22,27 +28,6 @@ export type CampaignShare = {
|
|||||||
|
|
||||||
export type CampaignShareTarget = {id: string;name: string;secondary?: string | null;};
|
export type CampaignShareTarget = {id: string;name: string;secondary?: string | null;};
|
||||||
export type CampaignShareTargets = {users: CampaignShareTarget[];groups: CampaignShareTarget[];};
|
export type CampaignShareTargets = {users: CampaignShareTarget[];groups: CampaignShareTarget[];};
|
||||||
export type AccessExplanationUser = {
|
|
||||||
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 = {
|
|
||||||
user: AccessExplanationUser;
|
|
||||||
resource_type: string;
|
|
||||||
resource_id: string;
|
|
||||||
action: string;
|
|
||||||
provenance: AccessDecisionProvenanceItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CampaignUpdatePayload = {
|
export type CampaignUpdatePayload = {
|
||||||
external_id?: string | null;
|
external_id?: string | null;
|
||||||
@@ -995,20 +980,6 @@ export async function getCampaignShareTargets(settings: ApiSettings, campaignId:
|
|||||||
return apiFetch<CampaignShareTargets>(settings, `/api/v1/campaigns/${campaignId}/share-targets`);
|
return apiFetch<CampaignShareTargets>(settings, `/api/v1/campaigns/${campaignId}/share-targets`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchResourceAccessExplanation(
|
|
||||||
settings: ApiSettings,
|
|
||||||
options: {userId: string;resourceType: string;resourceId: string;action: string;tenantId?: string | null;})
|
|
||||||
: Promise<ResourceAccessExplanationResponse> {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
user_id: options.userId,
|
|
||||||
resource_type: options.resourceType,
|
|
||||||
resource_id: options.resourceId,
|
|
||||||
action: options.action
|
|
||||||
});
|
|
||||||
if (options.tenantId) params.set("tenant_id", options.tenantId);
|
|
||||||
return apiFetch<ResourceAccessExplanationResponse>(settings, `/api/v1/admin/access/resource-explanation?${params.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCampaignShares(settings: ApiSettings, campaignId: string): Promise<CampaignShare[]> {
|
export async function getCampaignShares(settings: ApiSettings, campaignId: string): Promise<CampaignShare[]> {
|
||||||
const response = await apiFetch<{shares: CampaignShare[];}>(settings, `/api/v1/campaigns/${campaignId}/shares`);
|
const response = await apiFetch<{shares: CampaignShare[];}>(settings, `/api/v1/campaigns/${campaignId}/shares`);
|
||||||
return response.shares;
|
return response.shares;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
type CampaignShareTargets,
|
type CampaignShareTargets,
|
||||||
type ResourceAccessExplanationResponse } from
|
type ResourceAccessExplanationResponse } from
|
||||||
"../../../api/campaigns";
|
"../../../api/campaigns";
|
||||||
import { Button } from "@govoplan/core-webui";
|
import { Button, ResourceAccessExplanation } from "@govoplan/core-webui";
|
||||||
import { Card } from "@govoplan/core-webui";
|
import { Card } from "@govoplan/core-webui";
|
||||||
import { ConfirmDialog } from "@govoplan/core-webui";
|
import { ConfirmDialog } from "@govoplan/core-webui";
|
||||||
import DataGrid, { type DataGridColumn } from "../../../components/table/DataGrid";
|
import DataGrid, { type DataGridColumn } from "../../../components/table/DataGrid";
|
||||||
@@ -221,76 +221,11 @@ export default function CampaignAccessCard({
|
|||||||
<FormField label="i18n:govoplan-campaign.access.2f81a22d"><select value={sharePermission} onChange={(event) => setSharePermission(event.target.value as "read" | "write")}><option value="read">i18n:govoplan-campaign.can_view.1b3e4006</option><option value="write">i18n:govoplan-campaign.can_edit_and_operate.77acb15e</option></select></FormField>
|
<FormField label="i18n:govoplan-campaign.access.2f81a22d"><select value={sharePermission} onChange={(event) => setSharePermission(event.target.value as "read" | "write")}><option value="read">i18n:govoplan-campaign.can_view.1b3e4006</option><option value="write">i18n:govoplan-campaign.can_edit_and_operate.77acb15e</option></select></FormField>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={accessExplanationOpen} className="campaign-access-dialog" title="i18n:govoplan-campaign.access_explanation.75ee7f62" onClose={() => { if (!accessExplanationLoading) { setAccessExplanationOpen(false); setAccessExplanation(null); } }} footer={<Button onClick={() => { setAccessExplanationOpen(false); setAccessExplanation(null); }} disabled={accessExplanationLoading}>i18n:govoplan-campaign.close.bbfa773e</Button>}>
|
<Dialog open={accessExplanationOpen} className="campaign-access-dialog" title="i18n:govoplan-core.access_explanation.75ee7f62" onClose={() => { if (!accessExplanationLoading) { setAccessExplanationOpen(false); setAccessExplanation(null); } }} footer={<Button onClick={() => { setAccessExplanationOpen(false); setAccessExplanation(null); }} disabled={accessExplanationLoading}>i18n:govoplan-campaign.close.bbfa773e</Button>}>
|
||||||
<ResourceAccessExplanationContent loading={accessExplanationLoading} explanation={accessExplanation} fallbackResourceLabel={campaign.name || campaign.external_id || campaign.id} />
|
<ResourceAccessExplanation loading={accessExplanationLoading} explanation={accessExplanation} fallbackResourceLabel={campaign.name || campaign.external_id || campaign.id} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog open={Boolean(revokeTarget)} title="i18n:govoplan-campaign.remove_campaign_share.2c2d42eb" message="i18n:govoplan-campaign.remove_this_user_s_or_group_s_explicit_access_to.5ff07672" confirmLabel="i18n:govoplan-campaign.remove_share.69c932e1" tone="danger" busy={busy} onCancel={() => setRevokeTarget(null)} onConfirm={() => void revoke()} />
|
<ConfirmDialog open={Boolean(revokeTarget)} title="i18n:govoplan-campaign.remove_campaign_share.2c2d42eb" message="i18n:govoplan-campaign.remove_this_user_s_or_group_s_explicit_access_to.5ff07672" confirmLabel="i18n:govoplan-campaign.remove_share.69c932e1" tone="danger" busy={busy} onCancel={() => setRevokeTarget(null)} onConfirm={() => void revoke()} />
|
||||||
</>);
|
</>);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResourceAccessExplanationContent({
|
|
||||||
loading,
|
|
||||||
explanation,
|
|
||||||
fallbackResourceLabel
|
|
||||||
}: {loading: boolean;explanation: ResourceAccessExplanationResponse | null;fallbackResourceLabel: string;}) {
|
|
||||||
if (loading) return <p className="muted small-note">i18n:govoplan-campaign.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-campaign.user.9f8a2389</span><p>{userLabel}</p></div>
|
|
||||||
<div><span className="form-label">i18n:govoplan-campaign.resource.d1c626a9</span><p>{resourceLabel}</p></div>
|
|
||||||
<div><span className="form-label">i18n:govoplan-campaign.action.97c89a4d</span><p><code>{explanation.action}</code></p></div>
|
|
||||||
<div><span className="form-label">i18n:govoplan-campaign.evidence.8487d192</span><p>{explanation.provenance.length}</p></div>
|
|
||||||
</div>
|
|
||||||
{explanation.provenance.length === 0 ?
|
|
||||||
<p className="muted small-note">i18n:govoplan-campaign.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>{provenanceKindLabel(item.kind)}</strong>
|
|
||||||
<div className="muted small-note">
|
|
||||||
{item.source || "i18n:govoplan-campaign.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">{formatProvenanceDetails(item.details ?? {})}</p>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>);
|
|
||||||
}
|
|
||||||
|
|
||||||
function provenanceKindLabel(kind: string): string {
|
|
||||||
switch (kind) {
|
|
||||||
case "resource":
|
|
||||||
return "i18n:govoplan-campaign.resource.d1c626a9";
|
|
||||||
case "owner":
|
|
||||||
return "i18n:govoplan-campaign.owner.89ff3122";
|
|
||||||
case "share":
|
|
||||||
return "i18n:govoplan-campaign.share.09ca55ca";
|
|
||||||
case "policy":
|
|
||||||
return "i18n:govoplan-campaign.policy.0b779a05";
|
|
||||||
case "role":
|
|
||||||
return "i18n:govoplan-campaign.role.b5b4a5a2";
|
|
||||||
case "right":
|
|
||||||
return "i18n:govoplan-campaign.permission.2f81a22d";
|
|
||||||
default:
|
|
||||||
return kind;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatProvenanceDetails(details: Record<string, unknown>): string {
|
|
||||||
return Object.entries(details).map(([key, value]) => `${key}: ${formatProvenanceValue(value)}`).join("; ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatProvenanceValue(value: unknown): string {
|
|
||||||
if (value === null || value === undefined) return "i18n:govoplan-campaign.none.6eef6648";
|
|
||||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user