Skip to main content
Glama
actions.store.ts14 kB
import { defineStore } from "pinia"; import * as _ from "lodash-es"; import { addStoreHooks, ApiRequest } from "@si/vue-lib/pinia"; import { IconNames } from "@si/vue-lib/design-system"; import { Resource } from "@/api/sdf/dal/resource"; import { useWorkspacesStore } from "@/store/workspaces.store"; import { DefaultMap } from "@/utils/defaultmap"; import { ChangeSetId } from "@/api/sdf/dal/change_set"; import { ComponentId } from "@/api/sdf/dal/component"; import { ActionId, ActionKind, ActionPrototype, ActionPrototypeId, ActionState, ActionResultState, } from "@/api/sdf/dal/action"; import { useChangeSetsStore } from "./change_sets.store"; import { useRealtimeStore } from "./realtime/realtime.store"; export type DeprecatedActionStatus = | "success" | "failure" | "running" | "error" | "unstarted"; export type DeprecatedActionRunnerId = string; export type DeprecatedActionRunner = { id: DeprecatedActionRunnerId; status: DeprecatedActionStatus; actionKind: string; schemaName: string; componentName: string; componentId: ComponentId; resource?: Resource | null; startedAt?: string; finishedAt?: string; displayName?: string; }; export type DeprecatedActionBatchId = string; export type DeprecatedActionBatch = { id: DeprecatedActionBatchId; status?: DeprecatedActionStatus; author: string; actors?: string[]; actions: DeprecatedActionRunner[]; startedAt: string | null; finishedAt: string | null; }; export type ComponentAndAction = { actionPrototypeId: ActionPrototypeId; actionInstanceId?: ActionId; componentId?: ComponentId | null; actor?: string; } & Omit<ActionPrototype, "id">; export interface ActionView { id: ActionId; actor?: string; // TODO i dont see this on the backend prototypeId: ActionPrototypeId; componentId: ComponentId | null; name: string; description?: string; kind: ActionKind; originatingChangeSetId: ChangeSetId; funcRunId?: FuncRunId; } export interface ActionProposedView extends ActionView { state: ActionState; myDependencies: ActionId[]; dependentOn: ActionId[]; holdStatusInfluencedBy: ActionId[]; componentSchemaName?: string; componentName?: string; } export interface ActionHistoryView extends ActionView { funcRunId: FuncRunId; result: ActionResultState; originatingChangeSetName: string; updatedAt: string; resourceResult?: string; codeExecuted?: string; logs?: string; arguments?: string; componentName: string; schemaName: string; } export interface ChangeSetDetail { changeSetId: ChangeSetId; changeSetName: string; timestamp?: Date; } export const actionKindToAbbreviation = (actionKind: ActionKind) => { return { Create: "CRT", Destroy: "DLT", Refresh: "RFH", Manual: "MNL", Update: "UPT", }[actionKind]; }; export const actionIconClass = (kind: ActionKind) => { return { Create: "text-success-600", Destroy: "text-destructive-500 dark:text-destructive-600", Refresh: "text-action-600", Manual: "text-action-600", Update: "text-warning-600", }[kind]; }; export const actionIcon = (kind: ActionKind) => { return { Create: "plus", Destroy: "trash", Refresh: "refresh", Manual: "play", Update: "tilde", }[kind] as IconNames; }; export const resultIconClass = (result: ActionResultState) => { return { Success: "text-success-600", Failure: "text-destructive-500 dark:text-destructive-600", Unknown: "text-warning-600", }[result]; }; export const resultIcon = (result: ActionResultState) => { return { // outlined icons represent status about the simulation // filled icons represent status about resources in the real world // so we used filled in icons here Success: "check-hex", Failure: "x-hex", Unknown: "question-hex-outline", // TODO, get a non-outlined icon here }[result] as IconNames; }; // STUFF FOR BOTH ACTIONS V1 AND V2 export type FuncRunId = string; // END STUFF export const useActionsStore = () => { const workspacesStore = useWorkspacesStore(); const workspaceId = workspacesStore.selectedWorkspacePk; const changeSetsStore = useChangeSetsStore(); const changeSetId = changeSetsStore.selectedChangeSetId; const realtimeStore = useRealtimeStore(); return addStoreHooks( workspaceId, changeSetId, defineStore( `ws${workspaceId || "NONE"}/cs${changeSetId || "NONE"}/actions`, { state: () => ({ // used in the right rail when looking at a specific asset rawActionsByComponentId: {} as Record<ComponentId, ActionPrototype[]>, actions: [] as ActionProposedView[], actionHistory: [] as ActionHistoryView[], }), getters: { actionsAreInProgress(): boolean { return (this.countActionsByState?.[ActionState.Running] || 0) > 0; }, countActionsByState(): Record<string, number> { const counts = new DefaultMap<string, number>(() => 0); for (const action of this.proposedActions) { counts.set(action.state, counts.get(action.kind) + 1); } return Object.fromEntries(counts); }, countActionsByKind(): Record<string, number> { const counts = new DefaultMap<string, number>(() => 0); for (const action of this.proposedActions) { counts.set(action.kind, counts.get(action.kind) + 1); } return Object.fromEntries(counts); }, proposedActions(state): ActionProposedView[] { if (changeSetsStore.headSelected) return Object.values(state.actions); return Object.values(state.actions).filter( (av) => av.originatingChangeSetId === changeSetId, ); }, headActions(state): ActionProposedView[] { return Object.values(state.actions).filter( (av) => av.originatingChangeSetId !== changeSetId, ); }, historyActions(): ActionHistoryView[] { return this.actionHistory; }, historyActionsById(): Map<ActionId, ActionHistoryView> { const m = new Map(); for (const a of this.historyActions) { m.set(a.id, a); } return m; }, historyActionsByFuncRunId(): Map<FuncRunId, ActionHistoryView> { const m = new Map(); for (const a of this.historyActions) { m.set(a.funcRunId, a); } return m; }, historyActionsGrouped(): Map< ChangeSetDetail, Array<ActionHistoryView> > { // display actions, in order, and if the action is from a different change set than the previous action display the header const r = new DefaultMap<ChangeSetDetail, Array<ActionHistoryView>>( () => [], ); let last: ChangeSetDetail | undefined; for (const action of this.actionHistory) { // this creates the desired header behavior above if (action.originatingChangeSetId !== last?.changeSetId) { last = { changeSetId: action.originatingChangeSetId, changeSetName: action.originatingChangeSetName, } as ChangeSetDetail; } // always check for the newest timestamp const u = new Date(action.updatedAt); if (!last.timestamp || last.timestamp < u) last.timestamp = u; const arr = r.get(last); arr.push(action); r.set(last, arr); } return r; }, listActionsByComponentId( state, ): DefaultMap<ComponentId, Array<ActionProposedView>> { const d = new DefaultMap<ComponentId, Array<ActionProposedView>>( () => [], ); state.actions.forEach((a) => { const componentId = a.componentId as ComponentId | null; if (componentId) { const arr = d.get(componentId); arr.push(a); d.set(componentId, arr); } }); return d; }, actionsById(state): Map<ActionId, ActionView> { const m = new Map(); for (const a of state.actions) { m.set(a.id, a); } return m; }, }, actions: { async ADD_ACTION( componentId: ComponentId, actionPrototypeId: ActionPrototypeId, ) { return new ApiRequest({ method: "post", url: "change_set/add_action", keyRequestStatusBy: [componentId, actionPrototypeId], params: { prototypeId: actionPrototypeId, componentId, visibility_change_set_pk: changeSetId, }, }); }, // This is proposed/queued actions async LOAD_ACTIONS() { return new ApiRequest<Array<ActionProposedView>>({ url: "/action/list", headers: { accept: "application/json" }, params: { visibility_change_set_pk: changeSetId, }, onSuccess: (response) => { this.actions = response; }, }); }, async LOAD_ACTION_HISTORY() { return new ApiRequest<Array<ActionHistoryView>>({ url: "/action/history", headers: { accept: "application/json" }, params: { visibility_change_set_pk: changeSetId, }, onSuccess: (response) => { this.actionHistory = response; }, }); }, async PUT_ACTION_ON_HOLD(ids: ActionId[]) { return new ApiRequest<null>({ method: "post", url: "action/put_on_hold", keyRequestStatusBy: ids, params: { ids, visibility_change_set_pk: changeSetId, }, optimistic: () => { const held = {} as Record<string, string>; this.actions.forEach((a) => { if (ids.includes(a.id)) { held[a.id] = a.state; a.state = ActionState.OnHold; } }); return () => { for (const id of Object.keys(held)) { const a = this.actions.filter((a) => a.id === id).pop(); const state = held[id] as ActionState; if (a && state) a.state = state; } }; }, }); }, async CANCEL(ids: ActionId[]) { return new ApiRequest<null>({ method: "post", url: "action/cancel", keyRequestStatusBy: ids, params: { ids, visibility_change_set_pk: changeSetId, }, optimistic: () => { const removed = [] as ActionProposedView[]; this.actions.forEach((a, idx) => { if (a && ids.includes(a.id)) { removed[idx] = a; delete this.actions[idx]; } }); return () => { removed.forEach((a, idx) => { this.actions.splice(idx, 0, a); }); }; }, }); }, async RETRY(ids: ActionId[]) { return new ApiRequest<null>({ method: "post", url: "action/retry", keyRequestStatusBy: ids, params: { ids, visibility_change_set_pk: changeSetId, }, optimistic: () => { for (const a of this.actions) { if (ids.includes(a.id)) { // its either moving from failed or hold to queued // if its failed it will go queued and start running a.state = ActionState.Queued; } } }, }); }, registerRequestsBegin(requestUlid: string, actionName: string) { realtimeStore.inflightRequests.set(requestUlid, actionName); }, registerRequestsEnd(requestUlid: string) { realtimeStore.inflightRequests.delete(requestUlid); }, }, onActivated() { realtimeStore.subscribe(this.$id, `changeset/${changeSetId}`, [ { eventType: "ActionsListUpdated", callback: () => { this.LOAD_ACTIONS(); this.LOAD_ACTION_HISTORY(); }, }, { eventType: "ChangeSetWritten", callback: () => { this.LOAD_ACTIONS(); }, }, { eventType: "ChangeSetApplied", callback: (_update) => { this.LOAD_ACTIONS(); // Short term fix for reactivity issue on apply, since the // first load won't have the actions since the rebaser isnt done // Updated: We may not need this TIMEOUT if new WsEvents fix it! // setTimeout(() => this.LOAD_ACTIONS(), 500); // WsEvents in place ought to resolve this }, }, ]); 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