Skip to main content
Glama
logs.store.ts9.16 kB
import { defineStore } from "pinia"; import * as _ from "lodash-es"; import { ApiRequest, addStoreHooks } from "@si/vue-lib/pinia"; import { useWorkspacesStore } from "@/store/workspaces.store"; import { ChangeSetId } from "@/api/sdf/dal/change_set"; import keyedDebouncer from "@/utils/keyedDebouncer"; import { useChangeSetsStore } from "./change_sets.store"; import { UserId } from "./auth.store"; import { useRealtimeStore } from "./realtime/realtime.store"; export type AuditLogFilters = { changeSetFilter: string[]; entityNameFilter: string[]; entityTypeFilter: string[]; titleFilter: string[]; userFilter: string[]; }; export type AuditLogHeaderOptions = { changeSet: { label: string; value: string }[]; entityName: { label: string; value: string }[]; entityType: { label: string; value: string }[]; title: { label: string; value: string }[]; user: { label: string; value: string }[]; }; interface AuditLogCommon { title: string; userId?: UserId; userEmail?: string; kind: string; entityName: string; entityType: string; timestamp: string; changeSetId?: ChangeSetId; metadata: Record<string, unknown>; authenticationMethod: | { method: "System"; } | { method: "Jwt"; role: "Web" | "Automation"; tokenId?: string; }; } export interface AuditLog extends AuditLogCommon { changeSetName?: string; userName?: string; } export interface AuditLogDisplay extends AuditLogCommon { changeSetName: string; userName: string; } export const useLogsStore = (forceChangeSetId?: ChangeSetId) => { // this needs some work... but we'll probably want a way to force using HEAD // so we can load HEAD data in some scenarios while also loading a change set? let changeSetId: ChangeSetId | undefined; if (forceChangeSetId) { changeSetId = forceChangeSetId; } else { const changeSetsStore = useChangeSetsStore(); changeSetId = changeSetsStore.selectedChangeSetId; } const workspacesStore = useWorkspacesStore(); const workspaceId = workspacesStore.selectedWorkspacePk; const changeSetsStore = useChangeSetsStore(); const selectedChangeSetId = changeSetsStore.selectedChangeSet?.id; let debouncer: ReturnType<typeof keyedDebouncer> | undefined; const API_PREFIX = [ "v2", "workspaces", { workspaceId }, "change-sets", { selectedChangeSetId }, "audit-logs", ]; const visibility = { // changeSetId should not be empty if we are actually using this store // so we can give it a bad value and let it throw an error visibility_change_set_pk: changeSetId || "XXX", }; return addStoreHooks( workspaceId, changeSetId, defineStore( `ws${workspaceId || "NONE"}/cs${changeSetId || "NONE"}/audit-logs`, { state: () => ({ size: 50 as number, sortAscending: false as boolean, logs: [] as AuditLogDisplay[], canLoadMore: true as boolean, filters: { changeSetFilter: [], entityNameFilter: [], entityTypeFilter: [], titleFilter: [], userFilter: [], } as AuditLogFilters, headerOptions: { changeSet: [], entityName: [], entityType: [], title: [], user: [], } as AuditLogHeaderOptions, }), actions: { async LOAD_PAGE( size: number, sortAscending: boolean, identifier?: string, ) { return new ApiRequest<{ logs: AuditLog[]; canLoadMore: boolean }>({ url: API_PREFIX, params: { ...visibility, size, sortAscending }, keyRequestStatusBy: identifier, method: "get", onSuccess: (response) => { this.canLoadMore = response.canLoadMore; this.logs = response.logs.map( (log) => ({ ...log, userName: log.userName ?? "System", changeSetName: log.changeSetName ?? "- none -", } as AuditLogDisplay), ); // TODO(nick): make everything below this comment more efficient and automatic. const inProgressChangeSets: Record<string, string> = {}; const inProgressEntityNames: Record<string, string> = {}; const inProgressEntityTypes: Record<string, string> = {}; const inProgressTitles: Record<string, string> = {}; const inProgressUsers: Record<string, string> = {}; for (const log of this.logs) { inProgressChangeSets[log.changeSetName] = log.changeSetName; inProgressEntityNames[log.entityName] = log.entityName; inProgressEntityTypes[log.entityType] = log.entityType; inProgressTitles[log.title] = log.title; inProgressUsers[log.userName] = log.userName; } const resultForChangeSet: { label: string; value: string }[] = []; Object.keys(inProgressChangeSets).forEach((key) => { resultForChangeSet.push({ label: key, value: key }); }); resultForChangeSet.sort((a, b) => a.label.localeCompare(b.label), ); this.headerOptions.changeSet = resultForChangeSet; const resultForEntityName: { label: string; value: string }[] = []; Object.keys(inProgressEntityNames).forEach((key) => { resultForEntityName.push({ label: key, value: key }); }); resultForEntityName.sort((a, b) => a.label.localeCompare(b.label), ); this.headerOptions.entityName = resultForEntityName; const resultForEntityType: { label: string; value: string }[] = []; Object.keys(inProgressEntityTypes).forEach((key) => { resultForEntityType.push({ label: key, value: key }); }); resultForEntityType.sort((a, b) => a.label.localeCompare(b.label), ); this.headerOptions.entityType = resultForEntityType; const resultForTitle: { label: string; value: string }[] = []; Object.keys(inProgressTitles).forEach((key) => { resultForTitle.push({ label: key, value: key }); }); resultForTitle.sort((a, b) => a.label.localeCompare(b.label)); this.headerOptions.title = resultForTitle; const resultForUser: { label: string; value: string }[] = []; Object.keys(inProgressUsers).forEach((key) => { resultForUser.push({ label: key, value: key }); }); resultForUser.sort((a, b) => a.label.localeCompare(b.label)); this.headerOptions.user = resultForUser; }, }); }, enqueueLoadPage( size: number, sortAscending: boolean, identifier: string, ) { if (!debouncer) { debouncer = keyedDebouncer((identifier: string) => { this.LOAD_PAGE(size, sortAscending, identifier); }, 500); } const loadPage = debouncer(identifier); if (loadPage) { loadPage(identifier); } }, }, onActivated() { if (workspaceId) { const realtimeStore = useRealtimeStore(); realtimeStore.subscribe(this.$id, `workspace/${workspaceId}`, [ { eventType: "AuditLogsPublished", callback: (payload) => { if (changeSetId) { // If the change set of the event is the same as ours, then let's reload. Otherwise, let's only // reload if we are on HEAD and the change set has been applied or abandoned. if (changeSetId === payload.changeSetId) { this.enqueueLoadPage( this.size, this.sortAscending, "event", ); } else if ( changeSetId === changeSetsStore.headChangeSetId && (payload.changeSetStatus === "Applied" || payload.changeSetStatus === "Abandoned") ) { this.enqueueLoadPage( this.size, this.sortAscending, "event", ); } } }, }, ]); return () => { realtimeStore.unsubscribe(this.$id); }; } }, }, ), )(); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server