import type { Environment } from '@yaakapp-internal/models'; import { 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 { setupOrConfigureEncryption, withEncryptionEnabled, } from '../lib/setupOrConfigureEncryption'; import { BadgeButton } from './core/BadgeButton'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; 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 { 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 { HStack, VStack } from './core/Stacks'; interface Props { initialEnvironment: Environment | null; } export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { const createEnvironment = useCreateEnvironment(); const { baseEnvironment, 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); setSelectedEnvironmentId(id); }; 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[] = []; const isBaseEnv = activeEnvironment.environmentId == null; if (isBaseEnv) { 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.environmentId, 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 false; } 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}
{promptToEncrypt ? ( encryptEnvironment(activeEnvironment)}> Encrypt All Variables ) : isEncryptionEnabled ? ( Encryption Settings ) : ( valueVisibility.set((v) => !v)} /> )}
); }; function SidebarButton({ children, className, active, onClick, onDelete, rightSlot, environment, }: { className?: string; children: ReactNode; active: boolean; onClick: () => void; onDelete?: () => void; rightSlot?: ReactNode; environment: Environment | 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 ( <>
{rightSlot}
{environment != null && ( setShowContextMenu(null)} items={[ { label: 'Rename', leftSlot: , 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 }); }, }, { color: 'danger', label: 'Delete', leftSlot: , onSelect: async () => { await deleteModelWithConfirm(environment); onDelete?.(); }, }, ]} /> )} ); }