mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
feat(envvars): use cache for envvar dropdown key names, prevent autofill & suggestions in the settings (#1769)
* feat(envvars): use cache for envvar dropdown key names, prevent autofill & suggestions in the settings * add the same prevention for autocomplete and suggestions to sso and webhook
This commit is contained in:
@@ -505,7 +505,32 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleCloseModal}>
|
||||
<DialogContent className='flex h-[70vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[800px]'>
|
||||
<DialogContent className='relative flex h-[70vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[800px]'>
|
||||
{/* Hidden dummy inputs to prevent browser password manager autofill */}
|
||||
<input
|
||||
type='text'
|
||||
name='fakeusernameremembered'
|
||||
autoComplete='username'
|
||||
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type='password'
|
||||
name='fakepasswordremembered'
|
||||
autoComplete='current-password'
|
||||
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type='email'
|
||||
name='fakeemailremembered'
|
||||
autoComplete='email'
|
||||
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<DialogHeader className='flex-shrink-0 border-b px-6 py-4'>
|
||||
<DialogTitle className='font-medium text-lg'>Webhook Notifications</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -817,7 +842,7 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti
|
||||
<div className='relative'>
|
||||
<Input
|
||||
id='secret'
|
||||
type={showSecret ? 'text' : 'password'}
|
||||
type='text'
|
||||
placeholder='Webhook secret for signature verification'
|
||||
value={newWebhook.secret}
|
||||
onChange={(e) => {
|
||||
@@ -825,11 +850,16 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti
|
||||
setFieldErrors({ ...fieldErrors, general: undefined })
|
||||
}}
|
||||
className='h-9 rounded-[8px] pr-32'
|
||||
autoComplete='new-password'
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
spellCheck='false'
|
||||
data-form-type='other'
|
||||
style={
|
||||
!showSecret
|
||||
? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className='absolute top-0.5 right-0.5 flex h-8 items-center gap-1 pr-1'>
|
||||
<Tooltip>
|
||||
|
||||
@@ -464,6 +464,7 @@ export function ShortInput({
|
||||
setShowEnvVars(false)
|
||||
setSearchTerm('')
|
||||
}}
|
||||
maxHeight='192px'
|
||||
/>
|
||||
<TagDropdown
|
||||
visible={showTags}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { useStreamCleanup } from '@/hooks/use-stream-cleanup'
|
||||
import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions'
|
||||
import { useCopilotStore } from '@/stores/copilot/store'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment/store'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import { hasWorkflowsInitiallyLoaded, useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
@@ -1145,6 +1146,26 @@ const WorkflowContent = React.memo(() => {
|
||||
setIsWorkflowReady(shouldBeReady)
|
||||
}, [activeWorkflowId, params.workflowId, workflows, isLoading])
|
||||
|
||||
// Preload workspace environment variables when workflow is ready
|
||||
const loadWorkspaceEnvironment = useEnvironmentStore((state) => state.loadWorkspaceEnvironment)
|
||||
const clearWorkspaceEnvCache = useEnvironmentStore((state) => state.clearWorkspaceEnvCache)
|
||||
const prevWorkspaceIdRef = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Only preload if workflow is ready and workspaceId is available
|
||||
if (!isWorkflowReady || !workspaceId) return
|
||||
|
||||
// Clear cache if workspace changed
|
||||
if (prevWorkspaceIdRef.current && prevWorkspaceIdRef.current !== workspaceId) {
|
||||
clearWorkspaceEnvCache(prevWorkspaceIdRef.current)
|
||||
}
|
||||
|
||||
// Preload workspace environment (will use cache if available)
|
||||
void loadWorkspaceEnvironment(workspaceId)
|
||||
|
||||
prevWorkspaceIdRef.current = workspaceId
|
||||
}, [isWorkflowReady, workspaceId, loadWorkspaceEnvironment, clearWorkspaceEnvCache])
|
||||
|
||||
// Handle navigation and validation
|
||||
useEffect(() => {
|
||||
const validateAndNavigate = async () => {
|
||||
|
||||
@@ -407,7 +407,7 @@ export function EnvironmentVariables({
|
||||
data-input-type='value'
|
||||
value={envVar.value}
|
||||
onChange={(e) => updateEnvVar(originalIndex, 'value', e.target.value)}
|
||||
type={focusedValueIndex === originalIndex ? 'text' : 'password'}
|
||||
type='text'
|
||||
onFocus={(e) => {
|
||||
if (!isConflict) {
|
||||
e.target.removeAttribute('readOnly')
|
||||
@@ -421,10 +421,15 @@ export function EnvironmentVariables({
|
||||
disabled={isConflict}
|
||||
aria-disabled={isConflict}
|
||||
name={`env_variable_value_${envVar.id || originalIndex}_${Math.random()}`}
|
||||
autoComplete='new-password'
|
||||
autoComplete='off'
|
||||
autoCapitalize='off'
|
||||
spellCheck='false'
|
||||
readOnly={isConflict}
|
||||
style={
|
||||
focusedValueIndex !== originalIndex && !isConflict
|
||||
? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
|
||||
: undefined
|
||||
}
|
||||
className={`allow-scroll h-9 rounded-[8px] border-none px-3 font-normal text-sm ring-0 ring-offset-0 placeholder:text-muted-foreground focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 ${isConflict ? 'cursor-not-allowed border border-red-500 bg-[#F6D2D2] outline-none ring-0 disabled:bg-[#F6D2D2] disabled:opacity-100 dark:bg-[#442929] disabled:dark:bg-[#442929]' : 'bg-muted'}`}
|
||||
/>
|
||||
<div className='flex items-center justify-end gap-2'>
|
||||
@@ -476,8 +481,31 @@ export function EnvironmentVariables({
|
||||
|
||||
return (
|
||||
<div className='relative flex h-full flex-col'>
|
||||
{/* Hidden dummy input to prevent autofill */}
|
||||
<input type='text' name='hidden' style={{ display: 'none' }} autoComplete='false' />
|
||||
{/* Hidden dummy inputs to prevent browser password manager autofill */}
|
||||
<input
|
||||
type='text'
|
||||
name='fakeusernameremembered'
|
||||
autoComplete='username'
|
||||
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type='password'
|
||||
name='fakepasswordremembered'
|
||||
autoComplete='current-password'
|
||||
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type='email'
|
||||
name='fakeemailremembered'
|
||||
autoComplete='email'
|
||||
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
{/* Fixed Header */}
|
||||
<div className='px-6 pt-4 pb-2'>
|
||||
{/* Search Input */}
|
||||
|
||||
@@ -497,7 +497,32 @@ export function SSO() {
|
||||
const showStatus = hasProviders && !showConfigForm
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='relative flex h-full flex-col'>
|
||||
{/* Hidden dummy inputs to prevent browser password manager autofill */}
|
||||
<input
|
||||
type='text'
|
||||
name='fakeusernameremembered'
|
||||
autoComplete='username'
|
||||
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type='password'
|
||||
name='fakepasswordremembered'
|
||||
autoComplete='current-password'
|
||||
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
type='email'
|
||||
name='fakeemailremembered'
|
||||
autoComplete='email'
|
||||
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<div className='flex-1 overflow-y-auto px-6 pt-4 pb-4'>
|
||||
<div className='space-y-6'>
|
||||
{error && (
|
||||
@@ -757,11 +782,11 @@ export function SSO() {
|
||||
<div className='relative'>
|
||||
<Input
|
||||
id='client-secret'
|
||||
type={showClientSecret ? 'text' : 'password'}
|
||||
type='text'
|
||||
placeholder='Enter Client Secret'
|
||||
value={formData.clientSecret}
|
||||
name='sso_client_key'
|
||||
autoComplete='new-password'
|
||||
autoComplete='off'
|
||||
autoCapitalize='none'
|
||||
spellCheck={false}
|
||||
readOnly
|
||||
@@ -771,6 +796,11 @@ export function SSO() {
|
||||
}}
|
||||
onBlurCapture={() => setShowClientSecret(false)}
|
||||
onChange={(e) => handleInputChange('clientSecret', e.target.value)}
|
||||
style={
|
||||
!showClientSecret
|
||||
? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
|
||||
showErrors &&
|
||||
|
||||
@@ -42,7 +42,6 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
|
||||
}>({ workspace: {}, personal: {}, conflicts: [] })
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
// Load workspace environment variables when workspaceId changes
|
||||
useEffect(() => {
|
||||
if (workspaceId && visible) {
|
||||
loadWorkspaceEnvironment(workspaceId).then((data) => {
|
||||
@@ -51,36 +50,26 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
|
||||
}
|
||||
}, [workspaceId, visible, loadWorkspaceEnvironment])
|
||||
|
||||
// Combine and organize environment variables
|
||||
const envVarGroups: EnvVarGroup[] = []
|
||||
|
||||
if (workspaceId) {
|
||||
// When workspaceId is provided, show both workspace and user env vars
|
||||
const workspaceVars = Object.keys(workspaceEnvData.workspace)
|
||||
const personalVars = Object.keys(workspaceEnvData.personal)
|
||||
|
||||
if (workspaceVars.length > 0) {
|
||||
envVarGroups.push({ label: 'Workspace', variables: workspaceVars })
|
||||
}
|
||||
if (personalVars.length > 0) {
|
||||
envVarGroups.push({ label: 'Personal', variables: personalVars })
|
||||
}
|
||||
envVarGroups.push({ label: 'Workspace', variables: workspaceVars })
|
||||
envVarGroups.push({ label: 'Personal', variables: personalVars })
|
||||
} else {
|
||||
// Fallback to user env vars only
|
||||
if (userEnvVars.length > 0) {
|
||||
envVarGroups.push({ label: 'Personal', variables: userEnvVars })
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten all variables for filtering and selection
|
||||
const allEnvVars = envVarGroups.flatMap((group) => group.variables)
|
||||
|
||||
// Filter env vars based on search term
|
||||
const filteredEnvVars = allEnvVars.filter((envVar) =>
|
||||
envVar.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
// Create filtered groups for display
|
||||
const filteredGroups = envVarGroups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
@@ -90,41 +79,30 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
|
||||
}))
|
||||
.filter((group) => group.variables.length > 0)
|
||||
|
||||
// Reset selection when filtered results change
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0)
|
||||
}, [searchTerm])
|
||||
|
||||
// Handle environment variable selection
|
||||
const handleEnvVarSelect = (envVar: string) => {
|
||||
const textBeforeCursor = inputValue.slice(0, cursorPosition)
|
||||
const textAfterCursor = inputValue.slice(cursorPosition)
|
||||
|
||||
// Find the start of the env var syntax (last '{{' before cursor)
|
||||
const lastOpenBraces = textBeforeCursor.lastIndexOf('{{')
|
||||
|
||||
// Check if we're in a standard env var context (with braces) or direct typing mode
|
||||
const isStandardEnvVarContext = lastOpenBraces !== -1
|
||||
|
||||
if (isStandardEnvVarContext) {
|
||||
// Standard behavior with {{ }} syntax
|
||||
const startText = textBeforeCursor.slice(0, lastOpenBraces)
|
||||
|
||||
// Find the end of any existing env var syntax after cursor
|
||||
const closeIndex = textAfterCursor.indexOf('}}')
|
||||
const endText = closeIndex !== -1 ? textAfterCursor.slice(closeIndex + 2) : textAfterCursor
|
||||
|
||||
// Construct the new value with proper env var syntax
|
||||
const newValue = `${startText}{{${envVar}}}${endText}`
|
||||
onSelect(newValue)
|
||||
} else {
|
||||
// For direct typing mode (API key fields), check if we need to replace existing text
|
||||
// This handles the case where user has already typed part of a variable name
|
||||
if (inputValue.trim() !== '') {
|
||||
// Replace the entire input with the selected env var
|
||||
onSelect(`{{${envVar}}}`)
|
||||
} else {
|
||||
// Empty input, just insert the env var
|
||||
onSelect(`{{${envVar}}}`)
|
||||
}
|
||||
}
|
||||
@@ -132,7 +110,6 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
// Add and remove keyboard event listener
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const handleKeyboardEvent = (e: KeyboardEvent) => {
|
||||
@@ -201,14 +178,14 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cn('py-1', maxHeight !== 'none' && 'overflow-y-auto')}
|
||||
className={cn('py-1', maxHeight !== 'none' && 'allow-scroll max-h-48 overflow-y-auto')}
|
||||
style={{
|
||||
maxHeight: maxHeight !== 'none' ? maxHeight : undefined,
|
||||
scrollbarWidth: maxHeight !== 'none' ? 'thin' : undefined,
|
||||
}}
|
||||
>
|
||||
{filteredGroups.map((group) => (
|
||||
<div key={group.label}>
|
||||
{filteredGroups.length > 1 && (
|
||||
{workspaceId && (
|
||||
<div className='border-border/50 border-b px-3 py-1 font-medium text-muted-foreground/70 text-xs uppercase tracking-wide'>
|
||||
{group.label}
|
||||
</div>
|
||||
@@ -226,7 +203,7 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
|
||||
)}
|
||||
onMouseEnter={() => setSelectedIndex(globalIndex)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent input blur
|
||||
e.preventDefault()
|
||||
handleEnvVarSelect(envVar)
|
||||
}}
|
||||
>
|
||||
@@ -242,21 +219,17 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to check for '{{' trigger and get search term
|
||||
export const checkEnvVarTrigger = (
|
||||
text: string,
|
||||
cursorPosition: number
|
||||
): { show: boolean; searchTerm: string } => {
|
||||
if (cursorPosition >= 2) {
|
||||
const textBeforeCursor = text.slice(0, cursorPosition)
|
||||
// Look for {{ pattern followed by optional text
|
||||
const match = textBeforeCursor.match(/\{\{(\w*)$/)
|
||||
if (match) {
|
||||
return { show: true, searchTerm: match[1] }
|
||||
}
|
||||
|
||||
// Also check for exact {{ without any text after it
|
||||
// This ensures all env vars show when user just types {{
|
||||
if (textBeforeCursor.endsWith('{{')) {
|
||||
return { show: true, searchTerm: '' }
|
||||
}
|
||||
|
||||
@@ -217,11 +217,7 @@ export const resetAllStores = () => {
|
||||
})
|
||||
useWorkflowStore.getState().clear()
|
||||
useSubBlockStore.getState().clear()
|
||||
useEnvironmentStore.setState({
|
||||
variables: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
useEnvironmentStore.getState().reset()
|
||||
useExecutionStore.getState().reset()
|
||||
useConsoleStore.setState({ entries: [], isOpen: false })
|
||||
useCopilotStore.setState({ messages: [], isSendingMessage: false, error: null })
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { create } from 'zustand'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { API_ENDPOINTS } from '@/stores/constants'
|
||||
import type { EnvironmentStore, EnvironmentVariable } from '@/stores/settings/environment/types'
|
||||
import type {
|
||||
CachedWorkspaceEnvData,
|
||||
EnvironmentStore,
|
||||
EnvironmentVariable,
|
||||
} from '@/stores/settings/environment/types'
|
||||
|
||||
const logger = createLogger('EnvironmentStore')
|
||||
|
||||
@@ -9,6 +13,7 @@ export const useEnvironmentStore = create<EnvironmentStore>()((set, get) => ({
|
||||
variables: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
workspaceEnvCache: new Map<string, CachedWorkspaceEnvData>(),
|
||||
|
||||
loadEnvironmentVariables: async () => {
|
||||
try {
|
||||
@@ -77,6 +82,8 @@ export const useEnvironmentStore = create<EnvironmentStore>()((set, get) => ({
|
||||
}
|
||||
|
||||
set({ isLoading: false })
|
||||
|
||||
get().clearWorkspaceEnvCache()
|
||||
} catch (error) {
|
||||
logger.error('Error saving environment variables:', { error })
|
||||
set({
|
||||
@@ -89,6 +96,16 @@ export const useEnvironmentStore = create<EnvironmentStore>()((set, get) => ({
|
||||
},
|
||||
|
||||
loadWorkspaceEnvironment: async (workspaceId: string) => {
|
||||
// Check cache first
|
||||
const cached = get().workspaceEnvCache.get(workspaceId)
|
||||
if (cached) {
|
||||
return {
|
||||
workspace: cached.workspace,
|
||||
personal: cached.personal,
|
||||
conflicts: cached.conflicts,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
set({ isLoading: true, error: null })
|
||||
|
||||
@@ -98,12 +115,21 @@ export const useEnvironmentStore = create<EnvironmentStore>()((set, get) => ({
|
||||
}
|
||||
|
||||
const { data } = await response.json()
|
||||
set({ isLoading: false })
|
||||
return data as {
|
||||
const envData = data as {
|
||||
workspace: Record<string, string>
|
||||
personal: Record<string, string>
|
||||
conflicts: string[]
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
const cache = new Map(get().workspaceEnvCache)
|
||||
cache.set(workspaceId, {
|
||||
...envData,
|
||||
cachedAt: Date.now(),
|
||||
})
|
||||
set({ workspaceEnvCache: cache, isLoading: false })
|
||||
|
||||
return envData
|
||||
} catch (error) {
|
||||
logger.error('Error loading workspace environment:', { error })
|
||||
set({ error: error instanceof Error ? error.message : 'Unknown error', isLoading: false })
|
||||
@@ -123,6 +149,9 @@ export const useEnvironmentStore = create<EnvironmentStore>()((set, get) => ({
|
||||
throw new Error(`Failed to update workspace environment: ${response.statusText}`)
|
||||
}
|
||||
set({ isLoading: false })
|
||||
|
||||
// Invalidate cache for this workspace
|
||||
get().clearWorkspaceEnvCache(workspaceId)
|
||||
} catch (error) {
|
||||
logger.error('Error updating workspace environment:', { error })
|
||||
set({ error: error instanceof Error ? error.message : 'Unknown error', isLoading: false })
|
||||
@@ -141,6 +170,9 @@ export const useEnvironmentStore = create<EnvironmentStore>()((set, get) => ({
|
||||
throw new Error(`Failed to remove workspace environment keys: ${response.statusText}`)
|
||||
}
|
||||
set({ isLoading: false })
|
||||
|
||||
// Invalidate cache for this workspace
|
||||
get().clearWorkspaceEnvCache(workspaceId)
|
||||
} catch (error) {
|
||||
logger.error('Error removing workspace environment keys:', { error })
|
||||
set({ error: error instanceof Error ? error.message : 'Unknown error', isLoading: false })
|
||||
@@ -150,4 +182,24 @@ export const useEnvironmentStore = create<EnvironmentStore>()((set, get) => ({
|
||||
getAllVariables: (): Record<string, EnvironmentVariable> => {
|
||||
return get().variables
|
||||
},
|
||||
|
||||
clearWorkspaceEnvCache: (workspaceId?: string) => {
|
||||
const cache = new Map(get().workspaceEnvCache)
|
||||
if (workspaceId) {
|
||||
cache.delete(workspaceId)
|
||||
set({ workspaceEnvCache: cache })
|
||||
} else {
|
||||
// Clear all caches
|
||||
set({ workspaceEnvCache: new Map() })
|
||||
}
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
variables: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
workspaceEnvCache: new Map(),
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -3,10 +3,18 @@ export interface EnvironmentVariable {
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface CachedWorkspaceEnvData {
|
||||
workspace: Record<string, string>
|
||||
personal: Record<string, string>
|
||||
conflicts: string[]
|
||||
cachedAt: number
|
||||
}
|
||||
|
||||
export interface EnvironmentState {
|
||||
variables: Record<string, EnvironmentVariable>
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
workspaceEnvCache: Map<string, CachedWorkspaceEnvData>
|
||||
}
|
||||
|
||||
export interface EnvironmentStore extends EnvironmentState {
|
||||
@@ -25,4 +33,6 @@ export interface EnvironmentStore extends EnvironmentState {
|
||||
removeWorkspaceEnvironmentKeys: (workspaceId: string, keys: string[]) => Promise<void>
|
||||
|
||||
getAllVariables: () => Record<string, EnvironmentVariable>
|
||||
clearWorkspaceEnvCache: (workspaceId?: string) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user