Skip to main content
Glama
ComponentSecretAttribute.vue13.9 kB
<template> <div v-if="attributeTree.prop?.isOriginSecret && showSecretForm" ref="secretFormRef" > <AttributeChildLayout secret> <template #header> <div class="flex flex-row items-center gap-2xs"> <div>{{ displayName }}</div> </div> </template> <div :class=" clsx( 'p-xs flex flex-col gap-xs text-sm font-normal', themeClasses('bg-white', 'bg-neutral-900'), ) " > <div :class=" clsx( 'text-sm italic', themeClasses('text-neutral-600', 'text-neutral-400'), ) " > Secret data entered will be encrypted. Secret data can always be replaced, but only the name and description can be viewed. </div> <ul class="flex flex-col"> <li v-for="(fieldname, index) in Object.keys(secretFormData)" :key="fieldname" :class=" clsx( 'flex flex-col items-center gap-3xs text-sm [&>*]:w-full relative', index === Object.keys(secretFormData).length - 1 ? 'mb-xs' : 'mb-[-1px]', ) " > <secretForm.Field :name="fieldname"> <template #default="{ field }"> <div class="grid grid-cols-2"> <div class="py-2xs"> <AttributeInputRequiredProperty :text="fieldname" :showAsterisk="isFieldRequired(fieldname)" /> </div> <SecretInput :field="field" :fieldname="fieldname" :placeholder=" attributeTree.secret ? getPlaceholder(fieldname) : '' " /> </div> <!-- Validation errors are intentionally not displayed --> </template> </secretForm.Field> </li> <NewButton ref="addSecretButtonRef" :label="attributeTree.secret ? 'Replace Secret' : 'Add Secret'" :loading="wForm.bifrosting.value" loadingText="Saving Secret" tone="action" :tabIndex="0" :disabled="!secretForm.state.canSubmit" @click="submitSecretForm" @keydown.enter.stop.prevent="submitSecretForm" @keydown.tab.stop.prevent="onAddSecretTab" /> </ul> <label ref="defaultSubRef" tabindex="0" data-default-sub-checkbox="label" :for="`default-subs-checkbox-${attributeTree.prop?.id}`" :class=" clsx( 'border w-full flex flex-row items-center gap-xs px-xs py-2xs cursor-pointer', 'focus:outline-none rounded-sm', themeClasses( 'border-neutral-400 focus:border-action-500 hover:border-action-500', 'border-neutral-600 focus:border-action-300 hover:border-action-300', ), ) " @click.stop.prevent=" () => toggleIsDefaultSource(attributeTree.attributeValue.path, true) " @keydown.enter.stop.prevent=" () => toggleIsDefaultSource(attributeTree.attributeValue.path, true) " @keydown.tab.stop.prevent="onDefaultSubTab" > <input :id="`default-subs-checkbox-${attributeTree.prop?.id}`" ref="defaultSubCheckboxRef" data-default-sub-checkbox="input" type="checkbox" :checked="attributeTree.attributeValue.isDefaultSource" @click.stop.prevent=" () => toggleIsDefaultSource(attributeTree.attributeValue.path) " /> <div>Make this the default subscription for new components</div> </label> </div> </AttributeChildLayout> </div> <!-- TODO(nick): add the ability to remove a subscription --> <AttributeInput v-else :displayName="attributeTree.prop?.name ?? 'Secret Value'" :attributeValueId="attributeTree.attributeValue.id" :path="attributeTree.attributeValue.path ?? ''" :kind="attributeTree.prop?.widgetKind" :prop="attributeTree.prop" :validation="attributeTree.attributeValue.validation" :component="component" :externalSources="attributeTree.attributeValue.externalSources" :value=" interstitialSpinner ? 'subscribing...' : attributeTree.secret?.name?.toString() ?? '' " :canDelete="false" :disableInputWindow="attributeTree.prop?.isOriginSecret" isSecret @selected="openSecretForm" @save=" (path, value, _kind, connectingComponentId) => save(path, value, connectingComponentId) " @remove-subscription="removeSubscription" /> </template> <script setup lang="ts"> import { computed, nextTick, ref } from "vue"; import { NewButton, themeClasses } from "@si/vue-lib/design-system"; import { useRoute } from "vue-router"; import clsx from "clsx"; import { BifrostComponent, ComponentInList, } from "@/workers/types/entity_kind_types"; import { encryptMessage } from "@/utils/messageEncryption"; import { AttributePath, ComponentId } from "@/api/sdf/dal/component"; import AttributeChildLayout from "./AttributeChildLayout.vue"; import AttributeInput from "./AttributeInput.vue"; import AttributeInputRequiredProperty from "./AttributeInputRequiredProperty.vue"; import { AttrTree } from "../logic_composables/attribute_tree"; import { useApi, routes, componentTypes } from "../api_composables"; import { useWatchedForm } from "../logic_composables/watched_form"; import SecretInput from "./SecretInput.vue"; import { MouseDetails, mouseEmitter } from "../logic_composables/emitters"; import { handleTab } from "../logic_composables/controls"; const props = defineProps<{ component: BifrostComponent | ComponentInList; attributeTree: AttrTree; }>(); const emit = defineEmits<{ ( e: "setDefaultSubscriptionSource", path: AttributePath, setTo: boolean, ): void; }>(); const displayName = computed(() => { if (props.attributeTree.attributeValue.key) return props.attributeTree.attributeValue.key; else return props.attributeTree.prop?.name || "XXX"; }); const isFieldRequired = (fieldname: string): boolean => { // Only "Name" is required for secrets (no other secrets have validations) return fieldname === "Name"; }; const secretFormData = computed(() => { if ( props.attributeTree.prop?.isOriginSecret && props.attributeTree.prop?.secretDefinition ) { const form = props.attributeTree.prop.secretDefinition.formData .flatMap((row) => row.name) .reduce((obj, name) => { obj[name] = ""; return obj; }, {} as Record<string, string>); return { Name: props.attributeTree.secret?.name ?? "", Description: props.attributeTree.secret?.description ?? "", ...form, }; } else return {}; }); const saveApi = useApi(); const save = async ( path: AttributePath, value: string, connectingComponentId?: ComponentId, ) => { const call = saveApi.endpoint<{ success: boolean }>( routes.UpdateComponentAttributes, { id: props.component.id }, ); const payload: componentTypes.UpdateComponentAttributesArgs = {}; payload[path] = value; if (connectingComponentId) { payload[path] = { $source: { component: connectingComponentId, path: value }, }; } const { req, newChangeSetId } = await call.put<componentTypes.UpdateComponentAttributesArgs>(payload); if (saveApi.ok(req) && newChangeSetId) { saveApi.navigateToNewChangeSet( { name: "new-hotness-component", params: { workspacePk: route.params.workspacePk, changeSetId: newChangeSetId, componentId: props.component.id, }, }, newChangeSetId, ); } }; const removeSubscriptionApi = useApi(); const removeSubscription = async (path: AttributePath) => { const call = removeSubscriptionApi.endpoint<{ success: boolean }>( routes.UpdateComponentAttributes, { id: props.component.id }, ); const payload: componentTypes.UpdateComponentAttributesArgs = {}; payload[path] = { $source: null, }; const { req, newChangeSetId } = await call.put<componentTypes.UpdateComponentAttributesArgs>(payload); if (removeSubscriptionApi.ok(req) && newChangeSetId) { removeSubscriptionApi.navigateToNewChangeSet( { name: "new-hotness-component", params: { workspacePk: route.params.workspacePk, changeSetId: newChangeSetId, componentId: props.component.id, }, }, newChangeSetId, ); } }; const route = useRoute(); const secretApi = useApi(); const keyApi = useApi(); const wForm = useWatchedForm<Record<string, string>>( `component.av.secret.${props.attributeTree.prop?.id}`, ); const secretForm = wForm.newForm({ data: secretFormData, onSubmit: async ({ value }) => { const definition = props.attributeTree.prop?.secretDefinition?.label; const propId = props.attributeTree.prop?.id; if (!definition) throw new Error("Secret Definition Required"); if (!propId) throw new Error("Secret Definition Prop Id required"); const callApi = keyApi.endpoint<componentTypes.PublicKey>( routes.GetPublicKey, { id: props.component.id }, ); const resp = await callApi.get(); const publicKey = resp.data; const filteredValue = Object.fromEntries( Object.entries(value).filter(([_key, val]) => val !== ""), ); const name = filteredValue.Name ?? ""; delete filteredValue.Name; const description = filteredValue.Description ?? ""; delete filteredValue.Description; const crypted = await encryptMessage(filteredValue, publicKey); const payload: componentTypes.CreateSecret = { name, attributeValueId: props.attributeTree.attributeValue.id, propId, definition, description, crypted, keyPairPk: publicKey.pk, version: componentTypes.SecretVersion.V1, algorithm: componentTypes.SecretAlgorithm.Sealedbox, }; const newSecret = secretApi.endpoint<{ id: string }>(routes.CreateSecret, { id: props.component.id, }); const { req, newChangeSetId } = await newSecret.post<componentTypes.CreateSecret>(payload); if (secretApi.ok(req) && newChangeSetId) { secretApi.navigateToNewChangeSet( { name: "new-hotness-component", params: { workspacePk: route.params.workspacePk, changeSetId: newChangeSetId, componentId: props.component.id, }, }, newChangeSetId, ); } }, validators: { onSubmit: ({ value }) => { if (!value.Name || value.Name.trim() === "") { return "Each secret must have a name"; } return undefined; // Return undefined for successful validation }, }, // this is the actual value set by the API call, so this is what we watch watchFn: () => props.attributeTree.attributeValue.isControlledByDynamicFunc, }); const getPlaceholder = (fieldname: string) => { if (!props.attributeTree.secret) return ""; if (fieldname === "Name") { return props.attributeTree.secret.name; } else if (fieldname === "Description") { return props.attributeTree.secret.description; } else return "empty"; }; const secretFormOpen = ref(false); const interstitialSpinner = computed( () => !props.attributeTree.secret && props.attributeTree.attributeValue.isControlledByDynamicFunc && (props.attributeTree.prop?.isOriginSecret || (props.attributeTree.attributeValue.externalSources && props.attributeTree.attributeValue.externalSources.length > 0)), ); const showSecretForm = computed( () => (!props.attributeTree.secret && // this is filled in after DVU finishes !props.attributeTree.attributeValue.isControlledByDynamicFunc) || // this is filled in by the API call secretFormOpen.value, // this is controlled by user activity ); const openSecretForm = () => { secretFormOpen.value = true; addListeners(); nextTick(() => { const inputs = secretFormRef.value?.getElementsByTagName("input"); if (inputs && inputs[0]) { inputs[0].focus(); } }); }; const closeSecretForm = () => { secretFormOpen.value = false; removeListeners(); }; const secretFormRef = ref<HTMLDivElement>(); const onMouseDown = (e: MouseDetails["mousedown"]) => { const target = e.target; if (!(target instanceof Element)) { return; } const el = secretFormRef.value; if (el && !el.contains(target)) { closeSecretForm(); } }; const addListeners = () => { mouseEmitter.on("mousedown", onMouseDown); }; const removeListeners = () => { mouseEmitter.off("mousedown", onMouseDown); }; const submitSecretForm = async () => { await secretForm.handleSubmit(); closeSecretForm(); }; const addSecretButtonRef = ref<InstanceType<typeof NewButton>>(); const defaultSubRef = ref<HTMLLabelElement>(); const defaultSubCheckboxRef = ref<HTMLInputElement>(); const toggleIsDefaultSource = (path: AttributePath, flipped?: boolean) => { if (!defaultSubCheckboxRef.value) return; const checked = flipped ? !defaultSubCheckboxRef.value.checked : defaultSubCheckboxRef.value.checked; emit("setDefaultSubscriptionSource", path, checked); }; const onAddSecretTab = (e: KeyboardEvent) => { if (!addSecretButtonRef.value || !addSecretButtonRef.value.mainElRef) { return; } handleTab(e, addSecretButtonRef.value.mainElRef); }; const onDefaultSubTab = (e: KeyboardEvent) => { handleTab(e, defaultSubRef.value); }; </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