Obfuscate environment variables

This commit is contained in:
Gregory Schier
2024-03-16 10:42:46 -07:00
parent 98493a1167
commit 33c982b288
5 changed files with 46 additions and 11 deletions

View File

@@ -5,6 +5,7 @@ import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment'; import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment'; import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
import { useEnvironments } from '../hooks/useEnvironments'; import { useEnvironments } from '../hooks/useEnvironments';
import { useKeyValue } from '../hooks/useKeyValue';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment'; import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace'; import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
@@ -59,14 +60,16 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
<SidebarButton <SidebarButton
active={selectedEnvironment?.id == null} active={selectedEnvironment?.id == null}
onClick={() => setSelectedEnvironmentId(null)} onClick={() => setSelectedEnvironmentId(null)}
className="group"
environment={null} environment={null}
rightSlot={ rightSlot={
<IconButton <IconButton
size="sm" size="sm"
iconSize="md"
color="custom"
title="Add sub environment" title="Add sub environment"
icon="plusCircle" icon="plusCircle"
iconClassName="text-gray-500 group-hover:text-gray-700" iconClassName="text-gray-500 group-hover:text-gray-700"
className="group"
onClick={handleCreateEnvironment} onClick={handleCreateEnvironment}
/> />
} }
@@ -113,6 +116,11 @@ const EnvironmentEditor = function ({
workspace: Workspace; workspace: Workspace;
className?: string; className?: string;
}) { }) {
const valueVisibility = useKeyValue<boolean>({
namespace: 'global',
key: 'environmentValueVisibility',
fallback: true,
});
const environments = useEnvironments(); const environments = useEnvironments();
const updateEnvironment = useUpdateEnvironment(environment?.id ?? null); const updateEnvironment = useUpdateEnvironment(environment?.id ?? null);
const updateWorkspace = useUpdateWorkspace(workspace.id); const updateWorkspace = useUpdateWorkspace(workspace.id);
@@ -164,8 +172,17 @@ const EnvironmentEditor = function ({
return ( return (
<VStack space={4} className={classNames(className, 'pl-4')}> <VStack space={4} className={classNames(className, 'pl-4')}>
<HStack space={2} className="justify-between"> <HStack space={2} className="justify-between">
<Heading className="w-full flex items-center"> <Heading className="w-full flex items-center gap-1">
<div>{environment?.name ?? 'Global Variables'}</div> <div>{environment?.name ?? 'Global Variables'}</div>
<IconButton
iconClassName="text-gray-600"
size="sm"
icon={valueVisibility.value ? 'eye' : 'eyeClosed'}
title={valueVisibility.value ? 'Hide Values' : 'Reveal Values'}
onClick={() => {
return valueVisibility.set((v) => !v);
}}
/>
</Heading> </Heading>
</HStack> </HStack>
<PairEditor <PairEditor
@@ -173,6 +190,7 @@ const EnvironmentEditor = function ({
nameAutocompleteVariables={false} nameAutocompleteVariables={false}
namePlaceholder="VAR_NAME" namePlaceholder="VAR_NAME"
nameValidate={validateName} nameValidate={validateName}
valueType={valueVisibility.value ? 'text' : 'password'}
valueAutocompleteVariables={false} valueAutocompleteVariables={false}
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'} forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
pairs={variables} pairs={variables}
@@ -216,7 +234,7 @@ function SidebarButton({
<div <div
className={classNames( className={classNames(
className, className,
'w-full grid grid-cols-[minmax(0,1fr)_auto] items-center', 'w-full grid grid-cols-[minmax(0,1fr)_auto] items-center gap-0.5',
'px-1', // Padding to show focus border 'px-1', // Padding to show focus border
)} )}
> >
@@ -225,7 +243,7 @@ function SidebarButton({
size="xs" size="xs"
className={classNames( className={classNames(
'w-full', 'w-full',
active ? 'text-gray-800' : 'text-gray-600 hover:text-gray-700', active ? 'text-gray-800 bg-highlightSecondary' : 'text-gray-600 hover:text-gray-700',
)} )}
justify="start" justify="start"
onClick={onClick} onClick={onClick}

View File

@@ -9,7 +9,6 @@ import {
cloneElement, cloneElement,
forwardRef, forwardRef,
isValidElement, isValidElement,
memo,
useCallback, useCallback,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
@@ -57,7 +56,7 @@ export interface EditorProps {
actions?: ReactNode; actions?: ReactNode;
} }
const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor( export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
{ {
readOnly, readOnly,
type = 'text', type = 'text',
@@ -295,8 +294,6 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
); );
}); });
export const Editor = memo(_Editor);
function getExtensions({ function getExtensions({
container, container,
readOnly, readOnly,

View File

@@ -2,6 +2,7 @@ import classNames from 'classnames';
import type { EditorView } from 'codemirror'; import type { EditorView } from 'codemirror';
import type { HTMLAttributes, ReactNode } from 'react'; import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react'; import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
import { useStateSyncDefault } from '../../hooks/useStateSyncDefault';
import type { EditorProps } from './Editor'; import type { EditorProps } from './Editor';
import { Editor } from './Editor'; import { Editor } from './Editor';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
@@ -69,7 +70,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
}: InputProps, }: InputProps,
ref, ref,
) { ) {
const [obscured, setObscured] = useState(type === 'password'); const [obscured, setObscured] = useStateSyncDefault(type === 'password');
const [currentValue, setCurrentValue] = useState(defaultValue ?? ''); const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
@@ -181,9 +182,10 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
<IconButton <IconButton
title={obscured ? `Show ${label}` : `Obscure ${label}`} title={obscured ? `Show ${label}` : `Obscure ${label}`}
size="xs" size="xs"
className="mr-0.5" className="mr-0.5 group/obscure !h-auto my-0.5"
iconClassName="text-gray-500 group-hover/obscure:text-gray-800"
iconSize="sm" iconSize="sm"
icon={obscured ? 'eyeClosed' : 'eye'} icon={obscured ? 'eye' : 'eyeClosed'}
onClick={() => setObscured((o) => !o)} onClick={() => setObscured((o) => !o)}
/> />
)} )}

View File

@@ -22,6 +22,7 @@ export type PairEditorProps = {
className?: string; className?: string;
namePlaceholder?: string; namePlaceholder?: string;
valuePlaceholder?: string; valuePlaceholder?: string;
valueType?: 'text' | 'password';
nameAutocomplete?: GenericCompletionConfig; nameAutocomplete?: GenericCompletionConfig;
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined; valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
nameAutocompleteVariables?: boolean; nameAutocompleteVariables?: boolean;
@@ -51,6 +52,7 @@ export const PairEditor = memo(function PairEditor({
nameAutocompleteVariables, nameAutocompleteVariables,
namePlaceholder, namePlaceholder,
nameValidate, nameValidate,
valueType,
onChange, onChange,
pairs: originalPairs, pairs: originalPairs,
valueAutocomplete, valueAutocomplete,
@@ -176,6 +178,7 @@ export const PairEditor = memo(function PairEditor({
allowFileValues={allowFileValues} allowFileValues={allowFileValues}
nameAutocompleteVariables={nameAutocompleteVariables} nameAutocompleteVariables={nameAutocompleteVariables}
valueAutocompleteVariables={valueAutocompleteVariables} valueAutocompleteVariables={valueAutocompleteVariables}
valueType={valueType}
forceFocusPairId={forceFocusPairId} forceFocusPairId={forceFocusPairId}
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
nameAutocomplete={nameAutocomplete} nameAutocomplete={nameAutocomplete}
@@ -218,6 +221,7 @@ type FormRowProps = {
| 'valueAutocomplete' | 'valueAutocomplete'
| 'nameAutocompleteVariables' | 'nameAutocompleteVariables'
| 'valueAutocompleteVariables' | 'valueAutocompleteVariables'
| 'valueType'
| 'namePlaceholder' | 'namePlaceholder'
| 'valuePlaceholder' | 'valuePlaceholder'
| 'nameValidate' | 'nameValidate'
@@ -246,6 +250,7 @@ const FormRow = memo(function FormRow({
valueAutocompleteVariables, valueAutocompleteVariables,
valuePlaceholder, valuePlaceholder,
valueValidate, valueValidate,
valueType,
}: FormRowProps) { }: FormRowProps) {
const { id } = pairContainer; const { id } = pairContainer;
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@@ -397,6 +402,7 @@ const FormRow = memo(function FormRow({
name="value" name="value"
onChange={handleChangeValueText} onChange={handleChangeValueText}
onFocus={handleFocus} onFocus={handleFocus}
type={isLast ? 'text' : valueType}
placeholder={valuePlaceholder ?? 'value'} placeholder={valuePlaceholder ?? 'value'}
autocomplete={valueAutocomplete?.(pairContainer.pair.name)} autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
autocompleteVariables={valueAutocompleteVariables} autocompleteVariables={valueAutocompleteVariables}

View File

@@ -0,0 +1,12 @@
import { useEffect, useState } from 'react';
/**
* Like useState, except it will update the value when the default value changes
*/
export function useStateSyncDefault<T>(defaultValue: T) {
const [value, setValue] = useState(defaultValue);
useEffect(() => {
setValue(defaultValue);
}, [defaultValue]);
return [value, setValue] as const;
}