import type { Environment } from '@yaakapp-internal/models'; import { duplicateModel, patchModel } from '@yaakapp-internal/models'; import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import classNames from 'classnames'; import type { ReactNode } from 'react'; import React, { useCallback, useMemo, useState } from 'react'; import { useCreateEnvironment } from '../hooks/useCreateEnvironment'; import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled'; import { useKeyValue } from '../hooks/useKeyValue'; import { useRandomKey } from '../hooks/useRandomKey'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { analyzeTemplate, convertTemplateToSecure } from '../lib/encryption'; import { showPrompt } from '../lib/prompt'; import { resolvedModelName } from '../lib/resolvedModelName'; import { setupOrConfigureEncryption, withEncryptionEnabled, } from '../lib/setupOrConfigureEncryption'; import { BadgeButton } from './core/BadgeButton'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { DismissibleBanner } from './core/DismissibleBanner'; import type { DropdownItem } from './core/Dropdown'; import { ContextMenu } from './core/Dropdown'; import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; import { Heading } from './core/Heading'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; import { IconTooltip } from './core/IconTooltip'; import { InlineCode } from './core/InlineCode'; import type { PairWithId } from './core/PairEditor'; import { ensurePairId } from './core/PairEditor'; import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; import { VStack } from './core/Stacks'; interface Props { initialEnvironment: Environment | null; } export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { const createEnvironment = useCreateEnvironment(); const { baseEnvironment, otherBaseEnvironments, subEnvironments, allEnvironments } = useEnvironmentsBreakdown(); const [selectedEnvironmentId, setSelectedEnvironmentId] = useState( initialEnvironment?.id ?? null, ); const selectedEnvironment = selectedEnvironmentId != null ? allEnvironments.find((e) => e.id === selectedEnvironmentId) : baseEnvironment; const handleCreateEnvironment = async () => { if (baseEnvironment == null) return; const id = await createEnvironment.mutateAsync(baseEnvironment); if (id != null) setSelectedEnvironmentId(id); }; const handleDuplicateEnvironment = useCallback(async (environment: Environment) => { const name = await showPrompt({ id: 'duplicate-environment', title: 'Duplicate Environment', label: 'Name', defaultValue: environment.name, }); if (name) { const newId = await duplicateModel({ ...environment, name, public: false }); setSelectedEnvironmentId(newId); } }, []); const handleDeleteEnvironment = useCallback( async (environment: Environment) => { await deleteModelWithConfirm(environment); if (selectedEnvironmentId === environment.id) { setSelectedEnvironmentId(baseEnvironment?.id ?? null); } }, [baseEnvironment?.id, selectedEnvironmentId], ); if (baseEnvironment == null) { return null; } return ( ( )} secondSlot={() => selectedEnvironment == null ? (
Failed to find selected environment {selectedEnvironmentId}
) : ( ) } /> ); }; const EnvironmentEditor = function ({ environment: activeEnvironment, className, }: { environment: Environment; className?: string; }) { const activeWorkspaceId = activeEnvironment.workspaceId; const isEncryptionEnabled = useIsEncryptionEnabled(); const valueVisibility = useKeyValue({ namespace: 'global', key: ['environmentValueVisibility', activeWorkspaceId], fallback: false, }); const { allEnvironments } = useEnvironmentsBreakdown(); const handleChange = useCallback( (variables: PairWithId[]) => patchModel(activeEnvironment, { variables }), [activeEnvironment], ); const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey(); // Gather a list of env names from other environments to help the user get them aligned const nameAutocomplete = useMemo(() => { const options: GenericCompletionOption[] = []; if (activeEnvironment.base) { return { options }; } const allVariables = allEnvironments.flatMap((e) => e?.variables); const allVariableNames = new Set(allVariables.map((v) => v?.name)); for (const name of allVariableNames) { const containingEnvs = allEnvironments.filter((e) => e.variables.some((v) => v.name === name), ); const isAlreadyInActive = containingEnvs.find((e) => e.id === activeEnvironment.id); if (isAlreadyInActive) continue; options.push({ label: name, type: 'constant', detail: containingEnvs.map((e) => e.name).join(', '), }); } return { options }; }, [activeEnvironment.base, activeEnvironment.id, allEnvironments]); const validateName = useCallback((name: string) => { // Empty just means the variable doesn't have a name yet and is unusable if (name === '') return true; return name.match(/^[a-z_][a-z0-9_-]*$/i) != null; }, []); const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password'; const promptToEncrypt = useMemo(() => { if (!isEncryptionEnabled) { return true; } else { return !activeEnvironment.variables.every( (v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure', ); } }, [activeEnvironment.variables, isEncryptionEnabled]); const encryptEnvironment = (environment: Environment) => { withEncryptionEnabled(async () => { const encryptedVariables: PairWithId[] = []; for (const variable of environment.variables) { const value = variable.value ? await convertTemplateToSecure(variable.value) : ''; encryptedVariables.push(ensurePairId({ ...variable, value })); } await handleChange(encryptedVariables); regenerateForceUpdateKey(); }); }; return (
{activeEnvironment?.name}
{isEncryptionEnabled ? ( promptToEncrypt ? ( encryptEnvironment(activeEnvironment)}> Encrypt All Variables ) : ( Encryption Settings ) ) : ( <> valueVisibility.set((v) => !v)}> {valueVisibility.value ? 'Conceal Values' : 'Reveal Values'} )}
{activeEnvironment.public && promptToEncrypt && ( This environment is sharable. Ensure variable values are encrypted to avoid accidental leaking of secrets during directory sync or data export. )}
); }; function SidebarButton({ children, className, active, onClick, deleteEnvironment, rightSlot, outerRightSlot, duplicateEnvironment, environment, }: { className?: string; children: ReactNode; active: boolean; onClick: () => void; rightSlot?: ReactNode; outerRightSlot?: ReactNode; environment: Environment; deleteEnvironment: ((environment: Environment) => void) | null; duplicateEnvironment: ((environment: Environment) => void) | null; }) { const [showContextMenu, setShowContextMenu] = useState<{ x: number; y: number; } | null>(null); const handleContextMenu = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setShowContextMenu({ x: e.clientX, y: e.clientY }); }, []); return ( <>
{outerRightSlot}
setShowContextMenu(null)} items={[ { label: 'Rename', leftSlot: , hidden: environment.base, onSelect: async () => { const name = await showPrompt({ id: 'rename-environment', title: 'Rename Environment', description: ( <> Enter a new name for {environment.name} ), label: 'Name', confirmText: 'Save', placeholder: 'New Name', defaultValue: environment.name, }); if (name == null) return; await patchModel(environment, { name }); }, }, ...((duplicateEnvironment ? [ { label: 'Duplicate', leftSlot: , onSelect: () => { duplicateEnvironment?.(environment); }, }, ] : []) as DropdownItem[]), { label: `Make ${environment.public ? 'Private' : 'Sharable'}`, leftSlot: , rightSlot: ( Sharable environments will be included in Directory Sync or data export. It is recommended to encrypt all variable values within sharable environments to prevent accidentally leaking secrets. } /> ), onSelect: async () => { await patchModel(environment, { public: !environment.public }); }, }, ...((deleteEnvironment ? [ { color: 'danger', label: 'Delete', leftSlot: , onSelect: () => { deleteEnvironment(environment); }, }, ] : []) as DropdownItem[]), ]} /> ); } const sharableTooltip = ( );