Skip to main content
Glama
component_actions.ts8.86 kB
import { computed, MaybeRefOrGetter, ref, toValue } from "vue"; import { useQuery } from "@tanstack/vue-query"; import { useRoute } from "vue-router"; import { ActionId, ActionKind, ActionPrototypeId, ActionState, } from "@/api/sdf/dal/action"; import { BifrostComponent, ActionPrototypeView, EntityKind, ActionPrototypeViewList, BifrostActionViewList, ComponentInList, } from "@/workers/types/entity_kind_types"; import { bifrost, bifrostExists, useMakeArgs, useMakeArgsForHead, useMakeKey, useMakeKeyForHead, } from "@/store/realtime/heimdall"; import { routes, useApi } from "../api_composables"; import { ActionProposedView } from "../types"; import { useContext } from "./context"; export const useComponentActions = ( componentRef: MaybeRefOrGetter<BifrostComponent | ComponentInList | null>, ) => { const makeKey = useMakeKey(); const makeKeyForHead = useMakeKeyForHead(); const makeArgs = useMakeArgs(); const makeArgsForHead = useMakeArgsForHead(); const ctx = useContext(); // NOTE(nick): this should likely be passed in... const route = useRoute(); const component = computed(() => toValue(componentRef)); const schemaVariantId = computed(() => { const comp = component.value; if (!comp) return ""; // Handle both string (ComponentInList) and WeakReference (BifrostComponent) types const variantId = comp.schemaVariantId; if (typeof variantId === "string") { return variantId; } return variantId.id; }); const queryKeyForActionPrototypeViews = makeKey( EntityKind.ActionPrototypeViewList, schemaVariantId, ); const actionPrototypeViewsRaw = useQuery<ActionPrototypeViewList | null>({ enabled: computed(() => ctx.queriesEnabled.value && !!component.value), queryKey: queryKeyForActionPrototypeViews, queryFn: async () => { if (!schemaVariantId.value) return null; return await bifrost<ActionPrototypeViewList>( makeArgs(EntityKind.ActionPrototypeViewList, schemaVariantId.value), ); }, }); const actionPrototypeViews = computed( () => actionPrototypeViewsRaw.data.value?.actionPrototypes ?? [], ); const queryKeyForComponentOnHead = makeKeyForHead( EntityKind.ComponentInList, component.value?.id, ); const componentOnHeadRaw = useQuery({ enabled: computed(() => ctx.queriesEnabled.value && !!component.value), queryKey: queryKeyForComponentOnHead, queryFn: async () => { if (!component.value) return null; return await bifrostExists( makeArgsForHead(EntityKind.ComponentInList, component.value.id), ); }, }); const componentExistsOnHead = computed(() => !!componentOnHeadRaw.data.value); // Use the materialized view for actions to know what actions exist for a given prototype and the // selected component. const queryKeyForActionViewList = makeKey(EntityKind.ActionViewList); const actionViewList = useQuery<BifrostActionViewList | null>({ queryKey: queryKeyForActionViewList, queryFn: async () => await bifrost<BifrostActionViewList>(makeArgs(EntityKind.ActionViewList)), }); const actionByPrototype = computed(() => { if (!actionViewList.data.value?.actions?.length || !component.value?.id) { return {}; } const result: Record<ActionPrototypeId, ActionProposedView> = {}; for (const action of actionViewList.data.value.actions) { if (action.componentId === component.value.id) { // When in a change set (not HEAD), only show actions that originated in the current change set if ( !ctx.onHead.value && action.originatingChangeSetId !== ctx.changeSetId.value ) { continue; } // NOTE(nick): this assumes that there can be one action for a given prototype and component. // As of the time of writing, this is true, but multiple actions per prototype and component // aren't disallowed from the underlying graph's perspective. Theorhetically, you could // enqueue two refreshes back-to-back. What then? I don't think we'll expose an interface to // do that for awhile, so this should be sufficient. result[action.prototypeId] = action; } } return result; }); const actionIsRunning = (actionId?: ActionId | null) => { if (!actionId) return false; const state = actionViewList.data.value?.actions.find( (action) => action.id === actionId, )?.state; if (!state) return false; return [ActionState.Dispatched, ActionState.Running].includes(state); }; // Helpers for Refresh Actions specifically const refreshActionPrototype = computed(() => { return ( actionPrototypeViews.value.find( (action: ActionPrototypeView) => action.kind === ActionKind.Refresh, ) ?? null ); }); const refreshAction = computed(() => { if (!refreshActionPrototype.value?.id) return null; return actionByPrototype.value[refreshActionPrototype.value.id] ?? null; }); const refreshActionRunning = computed(() => { return refreshAction.value ? actionIsRunning(refreshAction.value?.id) : false; }); const refreshEnabled = computed( () => !!( refreshActionPrototype.value && component.value?.hasResource && (ctx.onHead.value || componentExistsOnHead.value) ), ); const runRefreshHandler = () => { const directRefreshApi = useApi(ctx); const executeRefresh = async () => { if (!component.value?.id || refreshActionRunning.value) return; const call = directRefreshApi.endpoint(routes.RefreshAction, { componentId: component.value.id, }); directRefreshApi.setWatchFn(() => refreshAction.value?.state); await call.put({}); }; const bifrosting = computed(() => directRefreshApi.bifrosting.value); return { executeRefresh, bifrosting, }; }; const toggleActionHandler = ( actionPrototypeView: ActionPrototypeView, actionId?: MaybeRefOrGetter<ActionId | undefined>, ) => { const removeApi = useApi(ctx); const addApi = useApi(ctx); const refreshApi = useApi(ctx); // Track which API is currently active as we could call 3 different APIs and need to propagate // the bifrosting status reactively for the correct one const activeApi = ref(addApi); const handleToggle = async () => { if (!component.value) return; const action = toValue(actionId); if (action) { activeApi.value = removeApi; // Removing/canceling existing action const call = removeApi.endpoint(routes.ActionCancel, { id: action, }); removeApi.setWatchFn(() => toValue(actionId)); await call.put({}); } else { // Adding new action - check if it's a refresh action const onHead = ctx.onHead.value; const isRefresh = actionPrototypeView.kind === ActionKind.Refresh; if (onHead && isRefresh) { // For refresh actions on HEAD, we enqueue using another route until we can get rid of Force Change Set if (!component.value?.id || actionIsRunning(toValue(actionId))) return; activeApi.value = refreshApi; const call = refreshApi.endpoint(routes.RefreshAction, { componentId: component.value.id, }); refreshApi.setWatchFn(() => toValue(actionId)); await call.put({}); } else { // Handle regular action add const call = addApi.endpoint(routes.ActionAdd); addApi.setWatchFn(() => toValue(actionId)); const { req, newChangeSetId } = await call.post<{ componentId: string; prototypeId: string; }>({ componentId: component.value.id, prototypeId: actionPrototypeView.id, }); if (newChangeSetId && addApi.ok(req)) { addApi.navigateToNewChangeSet( { name: "new-hotness-component", params: { workspacePk: route.params.workspacePk, changeSetId: newChangeSetId, componentId: component.value.id, }, }, newChangeSetId, ); } } } }; const bifrosting = computed(() => activeApi.value.bifrosting ?? false); return { handleToggle, bifrosting, }; }; return { // Lists for the component actionPrototypeViews, actionByPrototype, // Whether an action is currently running actionIsRunning, // Specific refresh action properties refreshActionRunning, refreshEnabled, // Click Handlers for toggling actions and refreshing a resource toggleActionHandler, runRefreshHandler, }; };

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