Skip to main content
Glama
FuncRunDetails.vue11.8 kB
<template> <FuncRunDetailsLayout v-if="funcRun?.id" :displayName=" funcRun.functionDisplayName || funcRun.functionName || 'Function Run' " :funcRun="funcRun" :status="funcRunStatus(funcRun, managementFuncJobState?.state) || ''" :logText="logText" :logsTooLarge="logsTooLarge" :errorHint=" successWithFailedOperations ? 'The management function ran successfully, but some component operations failed.' : undefined " :errorMessageRaw="managementFuncJobState?.message" :isLive="isLive" :collapsingStyles="collapsingStyles" > <template #headerList> <template v-if="funcRun.functionKind"> <dt><Icon name="func" size="xs" /></dt> <dd>{{ funcRun.functionKind }}</dd> </template> <template v-if="funcRun.componentName"> <dt></dt> <dd>{{ funcRun.componentName }}</dd> </template> <template v-if="funcRun.actionKind"> <dt><Icon name="play" size="xs" /></dt> <dd>{{ funcRun.actionKind }}</dd> </template> </template> <template #actions> <NewButton v-if="funcRun && funcRun.componentId && componentExists" label="Go to Component" @click="navigateToComponent" /> <NewButton v-if=" funcRun && funcRun.actionId && ['Failure', 'ActionFailure', 'Running'].includes( funcRunStatus(funcRun) || '', ) " tone="destructive" label="Remove" @click="removeAction" /> <NewButton v-if=" funcRun && funcRun.actionId && ['Failure', 'ActionFailure'].includes(funcRunStatus(funcRun) || '') " tone="action" label="Retry" @click="retryAction" /> </template> <template #grid> <GridItemWithLiveHeader ref="codeRef" title="Code" :live="false"> <CodeViewer v-if="functionCode" :code="functionCode" language="javascript" allowCopy /> <div v-else class="text-neutral-400 italic text-xs p-xs"> No code available </div> </GridItemWithLiveHeader> <GridItemWithLiveHeader ref="argsRef" title="Arguments" :live="false"> <CodeViewer v-if="argsJson" :code="argsJson" language="json" allowCopy /> <div v-else class="text-neutral-400 italic text-xs p-xs"> No arguments available </div> </GridItemWithLiveHeader> <GridItemWithLiveHeader ref="resultRef" title="Result" :live="false"> <CodeViewer v-if="resultJson" :code="resultJson" language="json" allowCopy /> <div v-else class="text-neutral-400 italic text-xs p-xs"> No result available </div> </GridItemWithLiveHeader> </template> </FuncRunDetailsLayout> <DelayedLoader v-else-if="funcRunQuery.isLoading" :size="'full'" /> <h1 v-else class="text-">Func Run {{ funcRunId }} not found</h1> </template> <script lang="ts" setup> import { computed, onMounted, onBeforeUnmount, ref, inject, unref } from "vue"; import { useQuery } from "@tanstack/vue-query"; import { NewButton, Icon } from "@si/vue-lib/design-system"; import * as _ from "lodash-es"; import { useRouter } from "vue-router"; import { bifrost, useMakeKey, useMakeArgs } from "@/store/realtime/heimdall"; import { BifrostComponent, EntityKind, } from "@/workers/types/entity_kind_types"; import CodeViewer from "@/components/CodeViewer.vue"; import DelayedLoader from "@/newhotness/layout_components/DelayedLoader.vue"; import FuncRunDetailsLayout from "./layout_components/FuncRunDetailsLayout.vue"; import GridItemWithLiveHeader from "./layout_components/GridItemWithLiveHeader.vue"; import { assertIsDefined, Context } from "./types"; import { useApi, routes, funcRunTypes } from "./api_composables"; import { keyEmitter } from "./logic_composables/emitters"; import { useManagementFuncJobState } from "./logic_composables/management"; import { FuncRun, funcRunStatus, FuncRunLog } from "./api_composables/func_run"; const props = defineProps<{ funcRunId: string; }>(); const ctx = inject<Context>("CONTEXT"); assertIsDefined(ctx); const router = useRouter(); const isLive = ref(false); const key = useMakeKey(); const args = useMakeArgs(); const back = () => { const params = router.currentRoute?.value.params ?? {}; router.push({ name: "new-hotness", params, query: { retainSessionState: 1 }, }); }; // Action handlers const removeApi = useApi(); const removeAction = async () => { if (funcRun.value?.actionId) { const call = removeApi.endpoint(routes.ActionCancel, { id: funcRun.value.actionId, }); // This route can mutate head, so we do not need to handle new change set semantics. await call.put({}); router.push({ name: "new-hotness", params: { workspacePk: unref(ctx.workspacePk), changeSetId: unref(ctx.changeSetId), }, }); } }; const retryApi = useApi(); const retryAction = async () => { if (funcRun.value?.actionId) { const call = retryApi.endpoint(routes.ActionRetry, { id: funcRun.value.actionId, }); // This route can mutate head, so we do not need to handle new change set semantics. await call.put({}); } }; const navigateToComponent = () => { if (funcRun.value?.componentId) { const params = { ...router.currentRoute.value.params }; const query = { ...router.currentRoute.value.query }; params.componentId = funcRun.value.componentId; router.push({ name: "new-hotness-component", params, query, }); } }; const api = useApi(); const pollInterval = ref<number | false>(0); // initial calls const funcRunQuery = useQuery<Omit<FuncRun, "logs"> | undefined>({ queryKey: computed(() => [ctx.changeSetId.value, "funcRun", props.funcRunId]), queryFn: async () => { const call = api.endpoint<funcRunTypes.FuncRunResponse>(routes.FuncRun, { id: props.funcRunId, }); const req = await call.get(); if (api.ok(req)) { pollInterval.value = [ "Running", "Dispatched", "Created", "Failed", ].includes(req.data.funcRun.state) ? 5000 : false; return req.data.funcRun; } }, refetchInterval: () => pollInterval.value, }); const funcRun = computed(() => funcRunQuery.data.value); const managementFuncJobStateComposable = useManagementFuncJobState(funcRun); const managementFuncJobState = computed( () => managementFuncJobStateComposable.value.value, ); const successWithFailedOperations = computed( () => funcRun.value?.state === "Success" && managementFuncJobState.value?.state === "failure", ); // Check if the component still exists const componentId = computed(() => funcRun.value?.componentId); const { data: componentQuery } = useQuery<BifrostComponent | undefined>({ queryKey: computed(() => { if (!componentId.value) return ["no-component"]; return key(EntityKind.Component, componentId.value).value; }), queryFn: async (queryContext) => { if (!componentId.value) return undefined; return ( (await bifrost<BifrostComponent>( args(EntityKind.Component, componentId.value), )) ?? queryContext.client.getQueryData( key(EntityKind.Component, componentId.value).value, ) ); }, enabled: computed(() => !!componentId.value), }); const componentExists = computed(() => !!componentQuery.value); const { data: funcRunLogsQuery } = useQuery<FuncRunLog | undefined>({ queryKey: [ctx.changeSetId.value, "funcRunLogs", props.funcRunId], queryFn: async () => { isLive.value = true; const call = api.endpoint<funcRunTypes.FuncRunLogsResponse>( routes.FuncRunLogs, { id: props.funcRunId, }, ); const req = await call.get(); if (api.ok(req)) { if (req.data.logs.finalized) { pollInterval.value = false; isLive.value = false; } return req.data.logs; } }, // Automatic polling for running functions every 5 seconds refetchInterval: () => pollInterval.value, refetchIntervalInBackground: true, }); const funcRunLogs = computed(() => funcRunLogsQuery.value); const logsTooLarge = computed( () => (funcRunLogs.value?.logs.length ?? 0) > 175, ); // Format logs as text for CodeViewer const logText = computed<string>(() => { if (!funcRunLogs.value?.logs?.length) return ""; return funcRunLogs.value.logs .map((log) => { let timestamp: string; // Check if log.timestamp is valid and not empty if (!log.timestamp || log.timestamp === "" || log.timestamp === "0") { timestamp = "No timestamp"; } else { let date: Date; // Handle timestamps that are Unix timestamps in seconds (numbers) vs milliseconds (strings) if (typeof log.timestamp === "number") { // If it's a number, assume it's Unix timestamp in seconds and convert to milliseconds date = new Date(log.timestamp * 1000); } else { // If it's a string, try parsing as-is first date = new Date(log.timestamp); // If the parsed date is in 1970 and the original value looks like Unix seconds const timeValue = date.getTime(); if ( timeValue > 0 && timeValue < 2147483647000 && !log.timestamp.includes("-") && !log.timestamp.includes("T") ) { // Looks like Unix timestamp in seconds as string, convert it const numericTimestamp = parseInt(log.timestamp, 10); if (!Number.isNaN(numericTimestamp)) { date = new Date(numericTimestamp * 1000); } } } const timeValue = date.getTime(); if (Number.isNaN(timeValue) || timeValue < 0) { timestamp = `Invalid: ${log.timestamp}`; } else { timestamp = date.toUTCString(); } } return `[${timestamp}] [${log.level}] ${log.message}`; }) .join("\n"); }); // Format function code for CodeViewer const functionCode = computed<string>(() => { if (!funcRun.value?.functionCodeBase64) return ""; try { const decodedCode = atob(funcRun.value.functionCodeBase64); return decodedCode; } catch (e) { return "// Error decoding function code"; } }); // Format arguments for CodeViewer const argsJson = computed<string>(() => { if (!funcRun.value?.functionArgs) return ""; try { return JSON.stringify(funcRun.value.functionArgs, null, 2); } catch (e) { return "// Error formatting arguments"; } }); // Format result for CodeViewer const resultJson = computed<string>(() => { if (!funcRun.value?.resultValue) return ""; try { return JSON.stringify(funcRun.value.resultValue, null, 2); } catch (e) { return "// Error formatting result"; } }); // Set up subscription on component mount onMounted(() => { keyEmitter.on("Escape", () => { back(); }); }); // Ensure cleanup on component unmount onBeforeUnmount(() => { keyEmitter.off("Escape"); }); const codeRef = ref<InstanceType<typeof GridItemWithLiveHeader>>(); const argsRef = ref<InstanceType<typeof GridItemWithLiveHeader>>(); const resultRef = ref<InstanceType<typeof GridItemWithLiveHeader>>(); // Calculate collapsing styles const collapsingStyles = computed(() => { if (!codeRef.value || !argsRef.value || !resultRef.value) return undefined; return `grid-template-rows: ${codeRef.value.collapseStyle} ${argsRef.value.collapseStyle} ${resultRef.value.collapseStyle};`; }); </script>

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