Skip to main content
Glama
ComponentDetails.vue36.7 kB
<template> <ComponentDetailsSkeleton v-if="showSkeleton" /> <section v-else :class="clsx('grid gap-sm h-full p-sm', gridStateClass)"> <!-- Single banner area for all banner types --> <div v-if=" showComponentStateBanner || showStatusBanner || (component && component.toDelete) || (component && hasSocketConnection) " class="banner flex flex-col" > <!-- Status and deletion banners for existing components --> <template v-if="component"> <!-- Status banner (template functions) --> <StatusBox v-if="showStatusBanner" :kind=" specialCaseManagementExecutionStatus === 'Running' || attributePanelRef?.importing ? 'loading' : 'error' " :text="statusBannerText" > <template #right> <NewButton v-if="specialCaseManagementFuncRun" :label="seeFuncRunLabel" @click="navigateToFuncRunDetails(specialCaseManagementFuncRun.id)" /> </template> </StatusBox> <!-- Deletion banner (second highest priority) --> <div v-else-if="component.toDelete" :class=" clsx( 'flex flex-row items-center gap-xs px-sm py-xs', themeClasses('bg-neutral-300', 'bg-neutral-600'), ) " > <NewButton v-tooltip="'Close (Esc)'" label="Marked for deletion" class="flex-none" @click="close" /> <TruncateWithTooltip class="py-2xs text-sm"> This component will be removed from HEAD once the current change set is applied. </TruncateWithTooltip> </div> <!-- Socket connections banner (lowest priority) --> <!-- TODO: Paul to get the light mode bg color --> <div v-else-if="hasSocketConnection" :class=" clsx( 'flex flex-row items-center gap-xs px-sm py-xs border', themeClasses( 'bg-warning-50 border-warning-400 text-neutral-900', 'border-warning-400 text-neutral-100', ), ) " :style="{ backgroundColor: 'rgba(125, 74, 23, 0.25)' }" > <Icon name="alert-triangle-filled" size="sm" :class="themeClasses('text-warning-600', 'text-warning-300')" /> <TruncateWithTooltip class="py-2xs text-sm flex-1"> Some settings in this component are incompatible with the new experience. To learn how to update them, check out our documentation. </TruncateWithTooltip> <NewButton label="Learn more" tone="action" @click="openWorkspaceMigrationDocumentation" /> </div> </template> <!-- No component banner --> <template v-else-if="showComponentExists"> <div :class=" clsx( 'flex flex-row items-center gap-xs px-sm py-xs', themeClasses('bg-neutral-300', 'bg-neutral-600'), ) " > <NewButton tooltip="Close (Esc)" tooltipPlacement="top" icon="x" tone="empty" :class=" clsx( 'active:bg-white active:text-black', themeClasses('hover:bg-neutral-400', 'hover:bg-neutral-500'), ) " @click="close" /> <TruncateWithTooltip v-if="!componentExists" class="py-xs text-sm"> No component with id "{{ componentId }}" exists on this change set. </TruncateWithTooltip> <TruncateWithTooltip v-else class="py-xs text-sm"> Component data is being retrieved... <Icon size="sm" name="loader" /> </TruncateWithTooltip> </div> </template> </div> <template v-if="component"> <div data-testid="component-name-section" :class=" clsx( 'name flex flex-row items-center gap-xs border-b', themeClasses( 'bg-white border-neutral-300', 'bg-neutral-800 border-neutral-600', ), ) " > <NewButton tooltip="Close (Esc)" tooltipPlacement="top" icon="x" tone="empty" :class=" clsx( 'active:bg-white active:text-black', themeClasses('hover:bg-neutral-200', 'hover:bg-neutral-600'), ) " @click="close" /> <div class="flex-none text-sm">{{ component.schemaVariantName }}</div> <div class="flex-none">/</div> <TruncateWithTooltip class="flex-1 min-w-0 m-[-4px] py-2xs px-xs text-sm" > {{ component.name }} </TruncateWithTooltip> <div class="ml-auto flex flex-row gap-xs"> <NewButton v-if="component.toDelete" v-tooltip="'Restore (R)'" label="Restore" tone="action" :loading="restoreLoading" loadingText="Restoring..." loadingIcon="loader" :disabled="restoreLoading" @click="restoreComponent" /> <template v-else> <NewButton v-tooltip="'Erase (E)'" label="Erase" tone="destructive" @click="eraseComponent" /> <NewButton v-tooltip="'Delete (⌫)'" label="Delete" tone="destructive" @click="deleteComponent" /> <template v-if="isUpgradeable"> <div class="w-px h-6 bg-neutral-600 self-center" /> <NewButton v-tooltip="'Upgrade (U)'" :label="upgradeLoading ? 'Upgrading...' : 'Upgrade'" :icon="upgradeLoading ? 'loader' : 'bolt-outline'" :loading="upgradeLoading" :disabled="upgradeLoading" loadingIcon="loader" @click="upgradeComponent" /> </template> </template> </div> </div> <div :class=" clsx( 'attrs flex flex-col', !showStatusBanner && !component?.toDelete && 'attrs-no-banner', ) " > <CollapsingFlexItem ref="attrRef" :expandable="false" open> <template #header> <span class="text-sm">Attributes</span> </template> <template #headerIcons> <NewButton v-if="specialCaseManagementFuncKind === 'import'" v-tooltip=" attributePanelRef?.importing ? undefined : 'Import Existing Resource (𝙸)' " label="Import a resource" :loading="attributePanelRef?.importing" loadingText="Importing..." @click.stop.prevent="openImportModal" /> <NewButton v-else-if=" specialCaseManagementFuncKind === 'runTemplate' && specialCaseManagementFunc?.id " :label=" specialCaseManagementExecutionStatus === 'Failure' ? 'Re-run template' : 'Run template' " tone="action" :loading="specialCaseManagementExecutionStatus === 'Running'" loadingText="Running template" :disabled="specialCaseManagementExecutionStatus === 'Running'" loadingIcon="loader" @click.stop="runMgmtFunc(specialCaseManagementFunc?.id)" /> </template> <AttributePanel ref="attributePanelRef" :component="component" :attributeTree="attributeTree || undefined" :importFunc="importFunc" :importFuncRun="importFuncRun" @focused="(path) => focused(path)" /> </CollapsingFlexItem> <CollapsingFlexItem v-if="hasResourceValueProps" ref="resourceRef" :expandable="false" > <template #header> <div class="flex place-content-between w-full"> <span class="text-sm">Resource Values</span> </div> </template> <ResourceValuesPanel :component="component" :attributeTree="attributeTree || undefined" /> </CollapsingFlexItem> <CollapsingFlexItem ref="actionRef" :expandable="false"> <template #header><span class="text-sm">Actions</span></template> <ActionsPanel :component="component" :attributeValueId="component.rootAttributeValueId" /> </CollapsingFlexItem> <CollapsingFlexItem ref="mgmtRef" :expandable="false"> <template #header><span class="text-sm">Management</span></template> <ManagementPanel :component="component" :latestFuncRuns="latestFuncRuns" /> </CollapsingFlexItem> </div> <div v-if="docsOpen" :class=" clsx( 'docs flex flex-col', !showStatusBanner && !component?.toDelete && 'docs-no-banner', ) " > <DocumentationPanel :component="component" :docs="docs" :docLink="docLink" open @toggle="() => (docsOpen = false)" @cleardocs="() => (docs = '')" /> </div> <div :class=" clsx( 'right flex flex-col', !showStatusBanner && !component?.toDelete && 'right-no-banner', ) " > <CollapsingFlexItem> <template #header ><span class="text-sm">Qualifications</span></template > <template #headerIcons> <MinimizedComponentQualificationStatus :component="component" /> </template> <QualificationPanel :component="component" :attributeTree="attributeTree || undefined" /> </CollapsingFlexItem> <CollapsingFlexItem ref="connectionsFlex" expandable> <template #header ><span class="text-sm">Subscriptions</span></template > <template #headerIcons> <NewButton label="Visualize Subscriptions" truncateText @click="navigateToMap" /> </template> <ConnectionsPanel v-if="componentConnections && component" ref="connectionsPanel" :component="component" :connections="componentConnections ?? undefined" /> </CollapsingFlexItem> <CollapsingFlexItem expandable> <template #header><span class="text-sm">Code Gen</span></template> <CodePanel v-if="attributeTree" :component="component" :attributeTree="attributeTree" /> </CollapsingFlexItem> <CollapsingFlexItem expandable> <template #header><span class="text-sm">Diff</span></template> <DiffPanel :component="component" /> </CollapsingFlexItem> <CollapsingFlexItem expandable> <template #header><span class="text-sm">Resource</span></template> <template #headerIcons> <NewButton v-if="refreshEnabled" label="Refresh" :loading="refreshActionRunning" loadingIcon="loader" loadingText="Refreshing..." :disabled="refreshBifrosting" @click.stop="executeRefresh" /> <Icon v-if="component.hasResource" name="check-hex" size="sm" tone="success" /> </template> <ResourcePanel :component="component" :attributeTree="attributeTree ?? undefined" /> </CollapsingFlexItem> <CollapsingFlexItem v-if="useFeatureFlagsStore().COMPONENT_HISTORY_FUNCS" > <template #header><span class="text-sm">History</span></template> <ComponentHistory :componentId="component.id" :enabled="componentExists" /> </CollapsingFlexItem> <CollapsingFlexItem v-if="useFeatureFlagsStore().COMPONENT_HISTORY_FUNCS" > <template #header ><span class="text-sm">Function Runs</span></template > <ComponentFuncRunList :componentId="component.id" :enabled="componentExists" /> </CollapsingFlexItem> <CollapsingFlexItem v-if="useFeatureFlagsStore().SQLITE_TOOLS" expandable > <template #header> <span class="text-sm">Debug</span> </template> <ComponentDebugPanel :componentId="component.id" /> </CollapsingFlexItem> <DocumentationPanel v-if="!docsOpen" :component="component" :docs="docs" :docLink="docLink" @toggle="() => (docsOpen = true)" /> </div> </template> <EraseModal ref="eraseModalRef" @confirm="componentsFinishErase" /> <DeleteModal ref="deleteModalRef" @delete="(mode) => componentsFinishDelete(mode)" /> <ComponentImportModal ref="importModalRef" @submit="doImport" /> </section> </template> <!-- eslint-disable vue/component-tags-order,import/first --> <script lang="ts" setup> import { useQuery, useQueryClient } from "@tanstack/vue-query"; import { Icon, themeClasses, TruncateWithTooltip, NewButton, } from "@si/vue-lib/design-system"; import { computed, ref, onMounted, onBeforeUnmount, inject, watch, useTemplateRef, nextTick, } from "vue"; import { useRoute, useRouter } from "vue-router"; import clsx from "clsx"; import { bifrost, bifrostExists, useMakeArgs, useMakeKey, } from "@/store/realtime/heimdall"; import { AttributeTree, BifrostComponent, EntityKind, IncomingConnections, MgmtFuncKind, } from "@/workers/types/entity_kind_types"; import { ExploreContext } from "@/newhotness/types"; import { funcRunStatus, FuncRun } from "@/newhotness/api_composables/func_run"; import { useRealtimeStore } from "@/store/realtime/realtime.store"; import { useFeatureFlagsStore } from "@/store/feature_flags.store"; import ComponentDetailsSkeleton from "@/newhotness/skeletons/ComponentDetailsSkeleton.vue"; import AttributePanel from "./AttributePanel.vue"; import ResourceValuesPanel from "./ResourceValuesPanel.vue"; import { attributeEmitter, KeyDetails, keyEmitter, } from "./logic_composables/emitters"; import CollapsingFlexItem from "./layout_components/CollapsingFlexItem.vue"; import StatusBox from "./layout_components/StatusBox.vue"; import { useApi, routes } from "./api_composables"; import QualificationPanel from "./QualificationPanel.vue"; import ResourcePanel from "./ResourcePanel.vue"; import CodePanel from "./CodePanel.vue"; import DiffPanel from "./DiffPanel.vue"; import ComponentHistory from "./ComponentHistory.vue"; import ComponentFuncRunList from "./ComponentFuncRunList.vue"; import ComponentDebugPanel from "./ComponentDebugPanel.vue"; import ActionsPanel from "./ActionsPanel.vue"; import ConnectionsPanel from "./ConnectionsPanel.vue"; import DocumentationPanel from "./DocumentationPanel.vue"; import ManagementPanel from "./ManagementPanel.vue"; import DeleteModal, { DeleteMode } from "./DeleteModal.vue"; import EraseModal from "./EraseModal.vue"; import MinimizedComponentQualificationStatus from "./MinimizedComponentQualificationStatus.vue"; import { useComponentDeletion } from "./composables/useComponentDeletion"; import { useComponentUpgrade } from "./composables/useComponentUpgrade"; import { useManagementFuncJobState } from "./logic_composables/management"; import { useComponentActions } from "./logic_composables/component_actions"; import { prevPage } from "./logic_composables/navigation_stack"; import { openWorkspaceMigrationDocumentation } from "./util"; import { useContext } from "./logic_composables/context"; import ComponentImportModal from "./ComponentImportModal.vue"; const showSkeleton = computed( () => attributeTreeQuery.isLoading.value || (!attributeTree.value && componentQuery.isLoading.value), // Prevent going back to skeleton after user changes ); const props = defineProps<{ componentId: string; }>(); const realtimeStore = useRealtimeStore(); const ctx = useContext(); const explore = inject<ExploreContext | null>("EXPLORE_CONTEXT", null); const key = useMakeKey(); const args = useMakeArgs(); const queryClient = useQueryClient(); const docsOpen = ref(true); const componentId = computed(() => props.componentId); const attributePanelRef = ref<InstanceType<typeof AttributePanel>>(); const componentQuery = useQuery<BifrostComponent | null>({ queryKey: key(EntityKind.Component, componentId), queryFn: async (queryContext) => { const c = await bifrost<BifrostComponent>( args(EntityKind.Component, componentId.value), ); if (c) return c; const d = queryContext.client.getQueryData( key(EntityKind.Component, componentId).value, ) as BifrostComponent | undefined; if (d) return d; return null; }, }); const enableExists = computed( () => componentQuery.isFetched && !componentQuery.data.value, ); const componentExistsInIndexQuery = useQuery<boolean>({ queryKey: key(EntityKind.Component, componentId, "exists"), queryFn: async () => { return await bifrostExists(args(EntityKind.Component, componentId.value)); }, enabled: enableExists, }); const attributeTreeQuery = useQuery<AttributeTree | null>({ queryKey: key(EntityKind.AttributeTree, componentId), queryFn: async (queryContext) => { const a = await bifrost<AttributeTree>( args(EntityKind.AttributeTree, componentId.value), ); if (a) return a; const d = queryContext.client.getQueryData( key(EntityKind.AttributeTree, componentId).value, ) as AttributeTree | undefined; if (d) return d; return null; }, }); const attributeTree = computed(() => attributeTreeQuery.data.value); // Determines if the component is able to have a resource. For example, for "AWS::EC2::KeyPair", // this would return 'true', but fore "AWS Region", this would return false. const hasResourceValueProps = computed(() => { if (!attributeTree.value) return false; const propMatch = Object.entries(attributeTree.value.props).find( ([_, prop]) => prop.path === "root/resource_value", ); if (!propMatch) return false; const attributeValueMatch = Object.entries( attributeTree.value.attributeValues, ).find(([_, attributeValue]) => attributeValue.propId === propMatch[0]); if (!attributeValueMatch) return false; const resourceValueSubtree = attributeTree.value.treeInfo[attributeValueMatch[0]]; const subtreeChildCount = resourceValueSubtree?.children?.length; if (!subtreeChildCount) return false; return subtreeChildCount > 0; }); const hasSocketConnection = computed(() => { if (!attributeTree.value) return false; return Object.values(attributeTree.value.attributeValues).some( (av) => av.hasSocketConnection, ); }); const component = computed(() => componentQuery.data.value || null); const componentExists = computed( () => !!component.value || (componentExistsInIndexQuery.data.value ?? false), ); const showComponentExists = computed( () => !component.value && componentExistsInIndexQuery.isFetched.value, ); // Actions composable - reactive to component changes const { refreshEnabled, refreshActionRunning, runRefreshHandler } = useComponentActions(component); const { executeRefresh, bifrosting: refreshBifrosting } = runRefreshHandler(); const mgmtFuncs = computed( () => component.value?.schemaVariant.mgmtFunctions ?? [], ); const componentConnectionsQuery = useQuery<IncomingConnections | null>({ queryKey: key(EntityKind.IncomingConnections, componentId), queryFn: async () => { const incomingConnections = await bifrost<IncomingConnections>( args(EntityKind.IncomingConnections, componentId.value), ); return incomingConnections; }, }); const componentConnections = computed( () => componentConnectionsQuery.data.value, ); const connectionsPanel = useTemplateRef<typeof ConnectionsPanel>("connectionsPanel"); const connectionsFlex = useTemplateRef<typeof CollapsingFlexItem>("connectionsFlex"); const focused = async (path: string) => { if (connectionsFlex.value) connectionsFlex.value.openState.open.value = true; await nextTick(); connectionsPanel.value?.highlight(path); }; const docs = ref(""); const docLink = ref(""); attributeEmitter.on("selectedDocs", (data) => { if (!data) docs.value = ""; else { docs.value = data.docs; docLink.value = data.link; } }); const attrRef = ref<typeof CollapsingFlexItem>(); const resourceRef = ref<typeof CollapsingFlexItem>(); const actionRef = ref<typeof CollapsingFlexItem>(); const mgmtRef = ref<typeof CollapsingFlexItem>(); const router = useRouter(); const close = () => { const lastPage = prevPage(); // If we have a previous page and it's the explore view, go back to it with full context if (lastPage && lastPage.name === "new-hotness") { router.push({ name: lastPage.name, params: lastPage.params, query: lastPage.query, }); } else { // Fallback to default navigation if no history const params = router.currentRoute?.value.params ?? {}; delete params.componentId; router.push({ name: "new-hotness", params, query: { retainSessionState: 1 }, }); } }; const navigateToMap = () => { const params = router.currentRoute?.value.params ?? {}; const query = { ...router.currentRoute?.value.query }; delete params.componentId; delete query.grid; delete query.retainSessionState; query.map = "1"; if (component.value?.id) { query.c = component.value.id; // Add flag to hide unconnected components // This is a specific path through to the Map to show only the items // that would effectively show in a pin query.hideSubscriptions = "1"; } router.push({ name: "new-hotness", params, query, }); }; const api = useApi(); export type NameFormData = { name: string; }; // Special case management funcs const specialCaseManagementFunc = computed(() => { const importFunc = mgmtFuncs.value.find( (f) => f.kind === MgmtFuncKind.Import, ); const runTemplateFunc = mgmtFuncs.value.find( (f) => f.kind === MgmtFuncKind.RunTemplate, ); if (importFunc) return importFunc; // chose import first if both appear (they shouldn't!) if (runTemplateFunc) return runTemplateFunc; return undefined; }); const specialCaseManagementFuncKind = computed(() => { if (specialCaseManagementFunc.value?.kind === MgmtFuncKind.Import) return "import"; if (specialCaseManagementFunc.value?.kind === MgmtFuncKind.RunTemplate) return "runTemplate"; return undefined; }); const dispatchedSpecialCaseManagementFunc = ref(false); const importFunc = computed(() => specialCaseManagementFuncKind.value === "import" ? specialCaseManagementFunc.value : undefined, ); const importFuncRun = computed(() => specialCaseManagementFuncKind.value === "import" ? specialCaseManagementFuncRun.value : undefined, ); // API to run special case management funcs const mgmtRunApi = useApi(); const route = useRoute(); const runMgmtFunc = async (funcId: string) => { const call = mgmtRunApi.endpoint<{ success: boolean }>(routes.MgmtFuncRun, { prototypeId: funcId, componentId: props.componentId, viewId: "DEFAULT", }); const { req, newChangeSetId } = await call.post({}); dispatchedSpecialCaseManagementFunc.value = true; setTimeout(() => { dispatchedSpecialCaseManagementFunc.value = false; }, 2000); // NOTE(nick): need to make sure this makes sense after the timeout. if (mgmtRunApi.ok(req) && newChangeSetId) { mgmtRunApi.navigateToNewChangeSet( { name: "new-hotness-component", params: { workspacePk: route.params.workspacePk, changeSetId: newChangeSetId, componentId: props.componentId, }, }, newChangeSetId, ); } }; const importModalRef = ref<InstanceType<typeof ComponentImportModal>>(); const openImportModal = () => { if (attributePanelRef.value?.resourceIdValue) { importModalRef.value?.open( attributePanelRef.value?.resourceIdValue as string, ); } else { importModalRef.value?.open(); } }; // MGMT funcs const MGMT_RUN_KEY = "latestMgmtFuncRuns"; const funcRunQuery = useQuery({ enabled: () => componentExists.value, queryKey: [ctx.changeSetId, MGMT_RUN_KEY], queryFn: async () => api .endpoint<FuncRun[]>(routes.MgmtFuncGetLatest, { componentId: componentId.value, }) .get(), }); const funcRuns = computed<FuncRun[]>(() => { if (!funcRunQuery.data.value) return []; return funcRunQuery.data.value.data; }); // The latest funcrun for this each mgmt prototype of this component, keyed by the prototypeId const latestFuncRuns = computed(() => { const runs = {} as Record<string, FuncRun>; if (!componentId.value || !componentExists.value) return runs; for (const funcRun of funcRuns.value) { if (funcRun.functionKind !== "Management") continue; if (funcRun.componentId !== componentId.value) continue; if (!funcRun.actionPrototypeId) continue; const maybeRun = runs[funcRun.actionPrototypeId]; if (!maybeRun) { runs[funcRun.actionPrototypeId] = funcRun; } else { if (maybeRun.createdAt < funcRun.createdAt) { runs[funcRun.actionPrototypeId] = funcRun; } } } return runs; }); // If any mgmt func for this component is running, query the status every 5 seconds // Ideally the websocket requests will give us faster updates, but this is a failsafe watch([funcRuns], () => { if (funcRuns.value.find((run) => run.state === "Running")) { setTimeout(() => { queryClient.invalidateQueries({ queryKey: [ctx.changeSetId, MGMT_RUN_KEY], }); }, 5000); } }); // Special case func run and state const specialCaseManagementFuncRun = computed(() => { const specialCaseManagementFuncId = specialCaseManagementFunc.value?.id; if (!specialCaseManagementFuncId) return undefined; return latestFuncRuns.value[specialCaseManagementFuncId]; }); const specialCaseManagementFuncJobStateComposable = useManagementFuncJobState( specialCaseManagementFuncRun, ); const specialCaseManagementFuncJobState = computed( () => specialCaseManagementFuncJobStateComposable.value.value, ); const specialCaseManagementExecutionStatus = computed(() => { if (dispatchedSpecialCaseManagementFunc.value) return "Running"; return funcRunStatus( specialCaseManagementFuncRun.value, specialCaseManagementFuncJobState.value?.state, ); }); // When absolutely anything in the component changes, invalidate the audit logs query for that component. watch( component, (newComponent) => { if (newComponent) { queryClient.invalidateQueries({ queryKey: key(EntityKind.AuditLogsForComponent, newComponent.id).value, }); } }, { deep: true }, ); const onBackspace = () => { if (!component.value?.toDelete) { deleteComponent(); } }; const shortcuts: { [Key in string]: (e: KeyDetails[Key]) => void } = { // a: used for select all in Explore, not used here // b: undefined, c: (e) => { if (e.metaKey || e.ctrlKey) return; // copy e.preventDefault(); emit("openChangesetModal"); }, // d: used for duplicate in Explore, not used here e: (e) => { e.preventDefault(); if (!component.value?.toDelete) { eraseComponent(); } }, f: (e) => { if (e.metaKey || e.ctrlKey) return; // find e.preventDefault(); if (component.value?.toDelete) { restoreComponent(); } }, // g: undefined, // h: undefined, i: (e) => { if (e.metaKey || e.ctrlKey) return; e.preventDefault(); if (specialCaseManagementFuncKind.value === "import") { openImportModal(); } }, // j: undefined, k: (e) => { e.preventDefault(); // k focuses the search on Explore, so why not have it focus the search here too? attributePanelRef.value?.focusSearch(); }, // l: undefined, // m: used to toggle the minimap in the Map view // n: used to open the add component modal in Explore // o: undefined, // p: used to pin a component in the Grid view // q: undefined, r: (e) => { if (e.metaKey || e.ctrlKey) { // This is the chrome hotkey combo for refreshing the page! Let it happen! return; } else if (ctx.onHead.value) { // Can't open the review screen on Head return; } e.preventDefault(); router.push({ name: "new-hotness-review", }); }, // s: undefined, // t: used on the Grid view to open the create template modal u: () => { if (!component.value?.toDelete && isUpgradeable.value) { upgradeComponent(); } }, // w: undefined, // x: undefined, // y: undefined, // z: undefined, Backspace: onBackspace, Delete: onBackspace, Escape: () => { close(); }, }; onMounted(() => { for (const [key, func] of Object.entries(shortcuts)) { keyEmitter.on(key, func); } realtimeStore.subscribe(MGMT_RUN_KEY, `changeset/${ctx.changeSetId.value}`, [ { eventType: "FuncRunLogUpdated", callback: async (payload) => { if (mgmtFuncs.value.find((m) => m.funcId === payload.actionId)) { setTimeout(() => { queryClient.invalidateQueries({ queryKey: [ctx.changeSetId, MGMT_RUN_KEY], }); }, 500); } }, }, ]); }); onBeforeUnmount(() => { for (const [key, func] of Object.entries(shortcuts)) { keyEmitter.off(key, func); } realtimeStore.unsubscribe(MGMT_RUN_KEY); }); const importFailure = computed( () => specialCaseManagementFuncKind.value === "import" && // eslint-disable-next-line @typescript-eslint/no-explicit-any (specialCaseManagementFuncRun.value?.resultValue as any)?.health === "error", ); const showComponentStateBanner = computed(() => !component.value); const showStatusBanner = computed( () => importFailure.value || specialCaseManagementExecutionStatus.value === "Failure" || specialCaseManagementExecutionStatus.value === "ActionFailure" || specialCaseManagementExecutionStatus.value === "Running", ); const seeFuncRunLabel = "See Func Run"; const statusBannerText = computed(() => { if (specialCaseManagementFuncKind.value === "import") if ( specialCaseManagementExecutionStatus.value === "Running" || attributePanelRef.value?.importing ) return "Importing..."; else return `Error executing Import function. Click "${seeFuncRunLabel}" for more details.`; if (specialCaseManagementFuncKind.value === "runTemplate") if (specialCaseManagementExecutionStatus.value === "Running") return "Extracting components from the template..."; else return `Error executing Run Template function. Click "${seeFuncRunLabel}" for more details.`; return ""; }); const gridStateClass = computed(() => { // When there's no component, we only show the banner if (showComponentStateBanner.value) { return "no-component"; } const hasBanner = showStatusBanner.value || component.value?.toDelete || hasSocketConnection.value; if (docsOpen.value) { return hasBanner ? "docs-open-with-banner" : "docs-open-without-banner"; } else { return hasBanner ? "docs-closed-with-banner" : "docs-closed-without-banner"; } }); const deleteModalRef = ref<InstanceType<typeof DeleteModal>>(); const eraseModalRef = ref<InstanceType<typeof EraseModal>>(); const { convertBifrostToComponentInList, deleteComponents, eraseComponents, restoreComponents, } = useComponentDeletion(undefined, true); const { upgradeComponents } = useComponentUpgrade(); const isUpgradeable = computed(() => { if (!component.value) return false; if (component.value.canBeUpgraded !== undefined) { return component.value.canBeUpgraded; } // Fallback to explore context for upgrade info if (explore) { return explore.upgradeableComponents.value.has(component.value.id); } return false; }); const deleteComponent = () => { if (!component.value) return; const componentForModal = convertBifrostToComponentInList(component.value); deleteModalRef.value?.open([componentForModal]); }; const eraseComponent = () => { if (!component.value) return; const componentForModal = convertBifrostToComponentInList(component.value); eraseModalRef.value?.open([componentForModal]); }; const upgradeLoading = ref(false); const restoreLoading = ref(false); const upgradeComponent = async () => { if (!component.value || upgradeLoading.value) return; upgradeLoading.value = true; await upgradeComponents([component.value.id]); upgradeLoading.value = false; }; const restoreComponent = async () => { if (!component.value || restoreLoading.value) return; restoreLoading.value = true; const result = await restoreComponents([component.value.id]); if (result.success) { if (result.newChangeSetId) { // Navigate to the same component details page in the new change set router.push({ name: "new-hotness-component", params: { workspacePk: route.params.workspacePk, changeSetId: result.newChangeSetId, componentId: component.value.id, }, }); } else { close(); } } restoreLoading.value = false; }; const componentsFinishErase = async () => { if (!component.value) return; const result = await eraseComponents([component.value.id]); if (result.success) { eraseModalRef.value?.close(); close(); } }; const componentsFinishDelete = async (mode: DeleteMode) => { if (!component.value) return; const result = await deleteComponents([component.value.id], mode); if (result.success) { deleteModalRef.value?.close(); close(); } }; const navigateToFuncRunDetails = (funcRunId: string) => { router.push({ name: "new-hotness-func-run", params: { workspacePk: route.params.workspacePk, changeSetId: route.params.changeSetId, funcRunId, }, }); }; const doImport = (resourceId: string) => { attributePanelRef.value?.doImport(resourceId); }; const emit = defineEmits<{ (e: "openChangesetModal"): void; }>(); </script> <style lang="less" scoped> section.grid.docs-open-with-banner { grid-template-areas: "name name name" "banner banner banner" "attrs docs right"; grid-template-rows: 2.5rem auto minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) minmax(0, 25%) minmax(0, 25%); } section.grid.docs-closed-with-banner { grid-template-areas: "name name" "banner banner" "attrs right"; grid-template-rows: 2.5rem auto minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) minmax(0, 33%); } section.grid.docs-open-without-banner { grid-template-areas: "name name name" "attrs docs right"; grid-template-rows: 2.5rem minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) minmax(0, 25%) minmax(0, 25%); } section.grid.docs-closed-without-banner { grid-template-areas: "name name" "attrs right"; grid-template-rows: 2.5rem minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) minmax(0, 33%); } section.grid.no-component { grid-template-areas: "banner"; grid-template-rows: auto; grid-template-columns: 1fr; } .docs { grid-area: docs; } .right { grid-area: right; } .name { grid-area: name; margin: -0.75rem -1rem 0 -1rem; margin-top: -1em; padding: 0 0.5rem 0 0.5rem; height: 2.75rem; } .attrs { grid-area: attrs; } .attrs-no-banner { margin-top: -0.75rem; } .docs-no-banner { margin-top: -0.75rem; } .right-no-banner { margin-top: -0.75rem; } .banner { grid-area: banner; margin-top: -0.75rem; } </style>

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