refactor(webui): use core access explanation
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"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",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"@govoplan/core-webui": "^0.1.8"
|
"@govoplan/core-webui": "^0.1.9"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@govoplan/core-webui": {
|
"@govoplan/core-webui": {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { ApiError, apiFetch, apiUrl, authHeaders, csrfToken, type ApiSettings, type DeltaDeletedItem, type FilesManagedFileLinkTarget } from "@govoplan/core-webui";
|
import { ApiError, apiFetch, apiUrl, authHeaders, csrfToken, type ApiSettings, type DeltaDeletedItem, type FilesManagedFileLinkTarget } from "@govoplan/core-webui";
|
||||||
|
export { fetchResourceAccessExplanation } from "@govoplan/core-webui";
|
||||||
|
export type {
|
||||||
|
AccessDecisionProvenanceItem,
|
||||||
|
ResourceAccessExplanationUser as AccessExplanationUser,
|
||||||
|
ResourceAccessExplanationResponse
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
|
|
||||||
export type FileSpace = {
|
export type FileSpace = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -559,44 +565,6 @@ export function bulkDeleteFiles(settings: ApiSettings, fileIds: string[]): Promi
|
|||||||
|
|
||||||
export type FileBulkShareResponse = {shares: FileShare[];shared_count: number;};
|
export type FileBulkShareResponse = {shares: FileShare[];shared_count: number;};
|
||||||
|
|
||||||
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 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 function virtualFolderResourceId(options: {tenantId: string;ownerType: "user" | "group";ownerId: string;path: string;}): string {
|
export function virtualFolderResourceId(options: {tenantId: string;ownerType: "user" | "group";ownerId: string;path: string;}): string {
|
||||||
return `virtual-folder:v1:${options.tenantId}:${options.ownerType}:${options.ownerId}:${base64Url(options.path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, ""))}`;
|
return `virtual-folder:v1:${options.tenantId}:${options.ownerType}:${options.ownerId}:${base64Url(options.path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, ""))}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
FileDropZone,
|
FileDropZone,
|
||||||
FormField,
|
FormField,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
|
ResourceAccessExplanation,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
hasScope,
|
hasScope,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
@@ -2211,8 +2212,8 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
|||||||
}
|
}
|
||||||
|
|
||||||
{accessExplanationTarget &&
|
{accessExplanationTarget &&
|
||||||
<FileDialog title="i18n:govoplan-files.access_explanation.75ee7f62" onClose={() => {if (!resourceAccessLoading) {setAccessExplanationTarget(null);setResourceAccessExplanation(null);}}}>
|
<FileDialog title="i18n:govoplan-core.access_explanation.75ee7f62" onClose={() => {if (!resourceAccessLoading) {setAccessExplanationTarget(null);setResourceAccessExplanation(null);}}}>
|
||||||
<ResourceAccessExplanationContent
|
<ResourceAccessExplanation
|
||||||
loading={resourceAccessLoading}
|
loading={resourceAccessLoading}
|
||||||
explanation={resourceAccessExplanation}
|
explanation={resourceAccessExplanation}
|
||||||
fallbackResourceLabel={accessExplanationTarget.label} />
|
fallbackResourceLabel={accessExplanationTarget.label} />
|
||||||
@@ -2508,71 +2509,6 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResourceAccessExplanationContent({
|
|
||||||
loading,
|
|
||||||
explanation,
|
|
||||||
fallbackResourceLabel
|
|
||||||
}: {loading: boolean;explanation: ResourceAccessExplanationResponse | null;fallbackResourceLabel: string;}) {
|
|
||||||
if (loading) return <p className="muted small-note">i18n:govoplan-files.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-files.user.9f8a2389</span><p>{userLabel}</p></div>
|
|
||||||
<div><span className="form-label">i18n:govoplan-files.resource.d1c626a9</span><p>{resourceLabel}</p></div>
|
|
||||||
<div><span className="form-label">i18n:govoplan-files.action.97c89a4d</span><p><code>{explanation.action}</code></p></div>
|
|
||||||
<div><span className="form-label">i18n:govoplan-files.evidence.8487d192</span><p>{explanation.provenance.length}</p></div>
|
|
||||||
</div>
|
|
||||||
{explanation.provenance.length === 0 ?
|
|
||||||
<p className="muted small-note">i18n:govoplan-files.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-files.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-files.resource.d1c626a9";
|
|
||||||
case "owner":
|
|
||||||
return "i18n:govoplan-files.owner.89ff3122";
|
|
||||||
case "share":
|
|
||||||
return "i18n:govoplan-files.share.09ca55ca";
|
|
||||||
case "policy":
|
|
||||||
return "i18n:govoplan-files.policy.0b779a05";
|
|
||||||
case "role":
|
|
||||||
return "i18n:govoplan-files.role.b5b4a5a2";
|
|
||||||
case "right":
|
|
||||||
return "i18n:govoplan-files.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-files.none.6eef6648";
|
|
||||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FileSearchRow({
|
function FileSearchRow({
|
||||||
value,
|
value,
|
||||||
caseSensitive,
|
caseSensitive,
|
||||||
|
|||||||
Reference in New Issue
Block a user