feat(webui): govern actions, quick access, and metrics
Refs #264, #285, #289
This commit is contained in:
@@ -8,7 +8,9 @@ import type {
|
||||
import type { TemporalDataSelection } from "./temporal";
|
||||
|
||||
|
||||
export const QUICK_ACCESS_LAUNCH_CONTEXT_VERSION = "1" as const;
|
||||
export const QUICK_ACCESS_LAUNCH_CONTEXT_VERSION = "2" as const;
|
||||
export const QUICK_ACCESS_REFERENCE_CONTRACT_VERSION = "1" as const;
|
||||
export const QUICK_ACCESS_RESULT_CONTRACT_VERSION = "1" as const;
|
||||
export const QUICK_ACCESS_LAUNCH_STATE_KEY = "govoplanQuickAccessLaunch";
|
||||
export const QUICK_ACCESS_RESULT_EVENT = "govoplan:quick-access-result";
|
||||
|
||||
@@ -37,6 +39,7 @@ export function createQuickAccessLaunchContext({
|
||||
const principal = auth.principal;
|
||||
return {
|
||||
contractVersion: QUICK_ACCESS_LAUNCH_CONTEXT_VERSION,
|
||||
referenceContractVersion: QUICK_ACCESS_REFERENCE_CONTRACT_VERSION,
|
||||
origin: {
|
||||
pathname: normalizePathname(pathname),
|
||||
search: normalizeSearch(search),
|
||||
@@ -61,7 +64,9 @@ export function createQuickAccessLaunchContext({
|
||||
? {
|
||||
viewId: viewContext.activeViewId,
|
||||
revisionId: viewContext.activeRevisionId,
|
||||
name: viewContext.activeViewName
|
||||
name: viewContext.activeViewName,
|
||||
recommendedToolIds: viewContext.presentation?.quickAccessRecommendedToolIds ?? [],
|
||||
focusedToolIds: viewContext.presentation?.quickAccessFocusedToolIds ?? []
|
||||
}
|
||||
: null
|
||||
};
|
||||
@@ -78,7 +83,11 @@ export function quickAccessLaunchContextFromState(
|
||||
): QuickAccessLaunchContext | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const candidate = value[QUICK_ACCESS_LAUNCH_STATE_KEY];
|
||||
if (!isRecord(candidate) || candidate.contractVersion !== QUICK_ACCESS_LAUNCH_CONTEXT_VERSION) {
|
||||
if (
|
||||
!isRecord(candidate)
|
||||
|| candidate.contractVersion !== QUICK_ACCESS_LAUNCH_CONTEXT_VERSION
|
||||
|| candidate.referenceContractVersion !== QUICK_ACCESS_REFERENCE_CONTRACT_VERSION
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (!isRecord(candidate.origin) || typeof candidate.origin.pathname !== "string") {
|
||||
@@ -101,12 +110,48 @@ export function quickAccessReturnPath(context: QuickAccessLaunchContext): string
|
||||
export function dispatchQuickAccessResult(
|
||||
toolId: string,
|
||||
launchContext: QuickAccessLaunchContext,
|
||||
result: QuickAccessResult
|
||||
): void {
|
||||
if (typeof window === "undefined") return;
|
||||
result: QuickAccessResult,
|
||||
returnedReferenceKinds?: readonly string[]
|
||||
): boolean {
|
||||
if (
|
||||
typeof window === "undefined"
|
||||
|| !isQuickAccessResultAllowed(result, launchContext, returnedReferenceKinds)
|
||||
) return false;
|
||||
window.dispatchEvent(new CustomEvent(QUICK_ACCESS_RESULT_EVENT, {
|
||||
detail: { toolId, launchContext, result }
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isQuickAccessResultAllowed(
|
||||
result: QuickAccessResult,
|
||||
launchContext: QuickAccessLaunchContext,
|
||||
returnedReferenceKinds?: readonly string[]
|
||||
): boolean {
|
||||
if (result.contractVersion !== QUICK_ACCESS_RESULT_CONTRACT_VERSION) return false;
|
||||
if (result.outcome === "cancelled") {
|
||||
return result.action === undefined
|
||||
&& result.reference === undefined
|
||||
&& (result.reason === undefined
|
||||
|| (["user", "dismissed", "unavailable", "failed"] as const).includes(result.reason));
|
||||
}
|
||||
if (result.outcome !== "completed") return false;
|
||||
if (!(["created", "selected", "updated", "completed"] as const).includes(result.action)) return false;
|
||||
if (!result.reference) return true;
|
||||
if (
|
||||
result.reference.tenantId !== launchContext.tenantId
|
||||
|| !result.reference.ownerModule
|
||||
|| !result.reference.kind
|
||||
|| !result.reference.objectId
|
||||
|| (result.reference.path !== undefined
|
||||
&& result.reference.path !== null
|
||||
&& (!result.reference.path.startsWith("/") || result.reference.path.startsWith("//")))
|
||||
) return false;
|
||||
if (returnedReferenceKinds) {
|
||||
const returnedKind = `${result.reference.ownerModule}.${result.reference.kind}`;
|
||||
if (!returnedReferenceKinds.includes(returnedKind)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizePathname(value: string): string {
|
||||
|
||||
@@ -182,6 +182,7 @@ function productAreasFromMetadata(info: PlatformModuleInfo): ProductAreaContribu
|
||||
|
||||
function quickAccessToolsFromMetadata(info: PlatformModuleInfo): QuickAccessToolMetadata[] {
|
||||
return (info.frontend?.quick_access_tools ?? []).map((tool) => ({
|
||||
contractVersion: tool.contract_version,
|
||||
id: tool.id,
|
||||
moduleId: tool.module_id,
|
||||
categoryId: tool.category_id,
|
||||
@@ -194,7 +195,11 @@ function quickAccessToolsFromMetadata(info: PlatformModuleInfo): QuickAccessTool
|
||||
anyOf: tool.required_any,
|
||||
order: tool.order,
|
||||
defaultEnabled: tool.default_enabled,
|
||||
modes: tool.modes
|
||||
modes: tool.modes,
|
||||
availability: tool.availability,
|
||||
acceptedReferenceKinds: tool.accepted_reference_kinds,
|
||||
returnedReferenceKinds: tool.returned_reference_kinds,
|
||||
helpContextId: tool.help_context_id
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user