mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-23 09:51:10 +01:00
Ability to sync environments to folder (#207)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCopy } from '../hooks/useCopy';
|
||||
import { useTimedBoolean } from '../hooks/useTimedBoolean';
|
||||
import { copyToClipboard } from '../lib/copy';
|
||||
import { showToast } from '../lib/toast';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
@@ -9,7 +9,6 @@ interface Props extends Omit<ButtonProps, 'onClick'> {
|
||||
}
|
||||
|
||||
export function CopyButton({ text, ...props }: Props) {
|
||||
const copy = useCopy({ disableToast: true });
|
||||
const [copied, setCopied] = useTimedBoolean();
|
||||
return (
|
||||
<Button
|
||||
@@ -23,8 +22,8 @@ export function CopyButton({ text, ...props }: Props) {
|
||||
message: 'Failed to copy',
|
||||
});
|
||||
} else {
|
||||
copy(content);
|
||||
setCopied();
|
||||
copyToClipboard(content, { disableToast: true });
|
||||
setCopied();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCopy } from '../hooks/useCopy';
|
||||
import { useTimedBoolean } from '../hooks/useTimedBoolean';
|
||||
import { copyToClipboard } from '../lib/copy';
|
||||
import { showToast } from '../lib/toast';
|
||||
import type { IconButtonProps } from './core/IconButton';
|
||||
import { IconButton } from './core/IconButton';
|
||||
@@ -9,7 +9,6 @@ interface Props extends Omit<IconButtonProps, 'onClick' | 'icon'> {
|
||||
}
|
||||
|
||||
export function CopyIconButton({ text, ...props }: Props) {
|
||||
const copy = useCopy({ disableToast: true });
|
||||
const [copied, setCopied] = useTimedBoolean();
|
||||
return (
|
||||
<IconButton
|
||||
@@ -25,7 +24,7 @@ export function CopyIconButton({ text, ...props }: Props) {
|
||||
message: 'Failed to copy',
|
||||
});
|
||||
} else {
|
||||
copy(content);
|
||||
copyToClipboard(content, { disableToast: true });
|
||||
setCopied();
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function CreateWorkspaceDialog({ hide }: Props) {
|
||||
as="form"
|
||||
space={3}
|
||||
alignItems="start"
|
||||
className="pb-3 max-h-[50vh]"
|
||||
className="pb-3"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const workspaceId = await createGlobalModel({ model: 'workspace', name });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import { patchModel } 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';
|
||||
@@ -12,6 +12,7 @@ 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,
|
||||
@@ -19,18 +20,21 @@ import {
|
||||
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 { HStack, VStack } from './core/Stacks';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
initialEnvironment: Environment | null;
|
||||
@@ -38,7 +42,8 @@ interface Props {
|
||||
|
||||
export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const { baseEnvironment, subEnvironments, allEnvironments } = useEnvironmentsBreakdown();
|
||||
const { baseEnvironment, otherBaseEnvironments, subEnvironments, allEnvironments } =
|
||||
useEnvironmentsBreakdown();
|
||||
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
|
||||
initialEnvironment?.id ?? null,
|
||||
);
|
||||
@@ -51,9 +56,36 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
const handleCreateEnvironment = async () => {
|
||||
if (baseEnvironment == null) return;
|
||||
const id = await createEnvironment.mutateAsync(baseEnvironment);
|
||||
setSelectedEnvironmentId(id);
|
||||
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 (
|
||||
<SplitLayout
|
||||
name="env_editor"
|
||||
@@ -63,24 +95,33 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
firstSlot={() => (
|
||||
<aside className="w-full min-w-0 pt-2">
|
||||
<div className="min-w-0 h-full overflow-y-auto pt-1">
|
||||
<SidebarButton
|
||||
active={selectedEnvironment?.id == baseEnvironment?.id}
|
||||
onClick={() => setSelectedEnvironmentId(null)}
|
||||
environment={null}
|
||||
rightSlot={
|
||||
<IconButton
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
title="Add sub environment"
|
||||
icon="plus_circle"
|
||||
iconClassName="text-text-subtlest group-hover:text-text-subtle"
|
||||
className="group"
|
||||
onClick={handleCreateEnvironment}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{baseEnvironment?.name}
|
||||
</SidebarButton>
|
||||
{[baseEnvironment, ...otherBaseEnvironments].map((e) => (
|
||||
<SidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id == e.id}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
environment={e}
|
||||
duplicateEnvironment={handleDuplicateEnvironment}
|
||||
// Allow deleting base environment if there are multiples
|
||||
deleteEnvironment={
|
||||
otherBaseEnvironments.length > 0 ? handleDeleteEnvironment : null
|
||||
}
|
||||
rightSlot={e.public && sharableTooltip}
|
||||
outerRightSlot={
|
||||
<IconButton
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
title="Add sub environment"
|
||||
icon="plus_circle"
|
||||
iconClassName="text-text-subtlest group-hover:text-text-subtle"
|
||||
className="group mr-0.5"
|
||||
onClick={handleCreateEnvironment}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{resolvedModelName(e)}
|
||||
</SidebarButton>
|
||||
))}
|
||||
{subEnvironments.length > 0 && (
|
||||
<div className="px-2">
|
||||
<Separator className="my-3"></Separator>
|
||||
@@ -92,11 +133,9 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
active={selectedEnvironment?.id === e.id}
|
||||
environment={e}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
onDelete={() => {
|
||||
if (e.id === selectedEnvironmentId) {
|
||||
setSelectedEnvironmentId(null);
|
||||
}
|
||||
}}
|
||||
rightSlot={e.public && sharableTooltip}
|
||||
duplicateEnvironment={handleDuplicateEnvironment}
|
||||
deleteEnvironment={handleDeleteEnvironment}
|
||||
>
|
||||
{e.name}
|
||||
</SidebarButton>
|
||||
@@ -143,11 +182,10 @@ const EnvironmentEditor = function ({
|
||||
);
|
||||
const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey();
|
||||
|
||||
// Gather a list of env names from other environments, to help the user get them aligned
|
||||
// Gather a list of env names from other environments to help the user get them aligned
|
||||
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
|
||||
const options: GenericCompletionOption[] = [];
|
||||
const isBaseEnv = activeEnvironment.environmentId == null;
|
||||
if (isBaseEnv) {
|
||||
if (activeEnvironment.base) {
|
||||
return { options };
|
||||
}
|
||||
|
||||
@@ -166,10 +204,10 @@ const EnvironmentEditor = function ({
|
||||
});
|
||||
}
|
||||
return { options };
|
||||
}, [activeEnvironment.environmentId, activeEnvironment.id, allEnvironments]);
|
||||
}, [activeEnvironment.base, activeEnvironment.id, allEnvironments]);
|
||||
|
||||
const validateName = useCallback((name: string) => {
|
||||
// Empty just means the variable doesn't have a name yet, and is unusable
|
||||
// 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;
|
||||
}, []);
|
||||
@@ -177,7 +215,7 @@ const EnvironmentEditor = function ({
|
||||
const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password';
|
||||
const promptToEncrypt = useMemo(() => {
|
||||
if (!isEncryptionEnabled) {
|
||||
return false;
|
||||
return true;
|
||||
} else {
|
||||
return !activeEnvironment.variables.every(
|
||||
(v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure',
|
||||
@@ -199,28 +237,33 @@ const EnvironmentEditor = function ({
|
||||
|
||||
return (
|
||||
<VStack space={4} className={classNames(className, 'pl-4')}>
|
||||
<HStack space={2} className="justify-between">
|
||||
<Heading className="w-full flex items-center gap-1">
|
||||
<div>{activeEnvironment?.name}</div>
|
||||
{promptToEncrypt ? (
|
||||
<Heading className="w-full flex items-center gap-0.5">
|
||||
<div className="mr-2">{activeEnvironment?.name}</div>
|
||||
{isEncryptionEnabled ? (
|
||||
promptToEncrypt ? (
|
||||
<BadgeButton color="notice" onClick={() => encryptEnvironment(activeEnvironment)}>
|
||||
Encrypt All Variables
|
||||
</BadgeButton>
|
||||
) : isEncryptionEnabled ? (
|
||||
) : (
|
||||
<BadgeButton color="secondary" onClick={setupOrConfigureEncryption}>
|
||||
Encryption Settings
|
||||
</BadgeButton>
|
||||
) : (
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon={valueVisibility.value ? 'eye' : 'eye_closed'}
|
||||
title={valueVisibility.value ? 'Hide Values' : 'Reveal Values'}
|
||||
onClick={() => valueVisibility.set((v) => !v)}
|
||||
/>
|
||||
)}
|
||||
</Heading>
|
||||
</HStack>
|
||||
<div className="h-full pr-2 pb-2">
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<BadgeButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}>
|
||||
{valueVisibility.value ? 'Conceal Values' : 'Reveal Values'}
|
||||
</BadgeButton>
|
||||
</>
|
||||
)}
|
||||
</Heading>
|
||||
{activeEnvironment.public && promptToEncrypt && (
|
||||
<DismissibleBanner id={activeEnvironment.id} color="notice" className="mr-3">
|
||||
This environment is sharable. Ensure variable values are encrypted to avoid accidental
|
||||
leaking of secrets during directory sync or data export.
|
||||
</DismissibleBanner>
|
||||
)}
|
||||
<div className="h-full pr-2 pb-2 grid grid-rows-[minmax(0,1fr)] overflow-auto">
|
||||
<PairOrBulkEditor
|
||||
allowMultilineValues
|
||||
preferenceName="environment"
|
||||
@@ -230,6 +273,7 @@ const EnvironmentEditor = function ({
|
||||
valueType={valueType}
|
||||
valueAutocompleteVariables
|
||||
valueAutocompleteFunctions
|
||||
forcedEnvironmentId={activeEnvironment.id}
|
||||
forceUpdateKey={`${activeEnvironment.id}::${forceUpdateKey}`}
|
||||
pairs={activeEnvironment.variables}
|
||||
onChange={handleChange}
|
||||
@@ -245,17 +289,21 @@ function SidebarButton({
|
||||
className,
|
||||
active,
|
||||
onClick,
|
||||
onDelete,
|
||||
deleteEnvironment,
|
||||
rightSlot,
|
||||
outerRightSlot,
|
||||
duplicateEnvironment,
|
||||
environment,
|
||||
}: {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
onDelete?: () => void;
|
||||
rightSlot?: ReactNode;
|
||||
environment: Environment | null;
|
||||
outerRightSlot?: ReactNode;
|
||||
environment: Environment;
|
||||
deleteEnvironment: ((environment: Environment) => void) | null;
|
||||
duplicateEnvironment: ((environment: Environment) => void) | null;
|
||||
}) {
|
||||
const [showContextMenu, setShowContextMenu] = useState<{
|
||||
x: number;
|
||||
@@ -287,49 +335,88 @@ function SidebarButton({
|
||||
justify="start"
|
||||
onClick={onClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
rightSlot={rightSlot}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
{rightSlot}
|
||||
{outerRightSlot}
|
||||
</div>
|
||||
{environment != null && (
|
||||
<ContextMenu
|
||||
triggerPosition={showContextMenu}
|
||||
onClose={() => setShowContextMenu(null)}
|
||||
items={[
|
||||
{
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" size="sm" />,
|
||||
onSelect: async () => {
|
||||
const name = await showPrompt({
|
||||
id: 'rename-environment',
|
||||
title: 'Rename Environment',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{environment.name}</InlineCode>
|
||||
</>
|
||||
),
|
||||
label: 'Name',
|
||||
confirmText: 'Save',
|
||||
placeholder: 'New Name',
|
||||
defaultValue: environment.name,
|
||||
});
|
||||
if (name == null) return;
|
||||
await patchModel(environment, { name });
|
||||
},
|
||||
<ContextMenu
|
||||
triggerPosition={showContextMenu}
|
||||
onClose={() => setShowContextMenu(null)}
|
||||
items={[
|
||||
{
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
hidden: environment.base,
|
||||
onSelect: async () => {
|
||||
const name = await showPrompt({
|
||||
id: 'rename-environment',
|
||||
title: 'Rename Environment',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{environment.name}</InlineCode>
|
||||
</>
|
||||
),
|
||||
label: 'Name',
|
||||
confirmText: 'Save',
|
||||
placeholder: 'New Name',
|
||||
defaultValue: environment.name,
|
||||
});
|
||||
if (name == null) return;
|
||||
await patchModel(environment, { name });
|
||||
},
|
||||
{
|
||||
color: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" size="sm" />,
|
||||
onSelect: async () => {
|
||||
await deleteModelWithConfirm(environment);
|
||||
onDelete?.();
|
||||
},
|
||||
},
|
||||
...((duplicateEnvironment
|
||||
? [
|
||||
{
|
||||
label: 'Duplicate',
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
onSelect: () => {
|
||||
duplicateEnvironment?.(environment);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []) as DropdownItem[]),
|
||||
{
|
||||
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
|
||||
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,
|
||||
rightSlot: (
|
||||
<IconTooltip
|
||||
content={
|
||||
<>
|
||||
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: <Icon icon="trash" />,
|
||||
onSelect: () => {
|
||||
deleteEnvironment(environment);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []) as DropdownItem[]),
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const sharableTooltip = (
|
||||
<IconTooltip
|
||||
icon="eye"
|
||||
content="This environment will be included in Directory Sync and data exports"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
import type { Workspace} from '@yaakapp-internal/models';
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
import { workspacesAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@@ -41,12 +41,12 @@ function ExportDataDialogContent({
|
||||
allWorkspaces: Workspace[];
|
||||
activeWorkspace: Workspace;
|
||||
}) {
|
||||
const [includeEnvironments, setIncludeEnvironments] = useState<boolean>(false);
|
||||
const [includePrivateEnvironments, setIncludePrivateEnvironments] = useState<boolean>(false);
|
||||
const [selectedWorkspaces, setSelectedWorkspaces] = useState<Record<string, boolean>>({
|
||||
[activeWorkspace.id]: true,
|
||||
});
|
||||
|
||||
// Put active workspace first
|
||||
// Put the active workspace first
|
||||
const workspaces = useMemo(
|
||||
() => [activeWorkspace, ...allWorkspaces.filter((w) => w.id !== activeWorkspace.id)],
|
||||
[activeWorkspace, allWorkspaces],
|
||||
@@ -73,11 +73,11 @@ function ExportDataDialogContent({
|
||||
await invokeCmd('cmd_export_data', {
|
||||
workspaceIds: ids,
|
||||
exportPath,
|
||||
includeEnvironments: includeEnvironments,
|
||||
includePrivateEnvironments: includePrivateEnvironments,
|
||||
});
|
||||
onHide();
|
||||
onSuccess(exportPath);
|
||||
}, [includeEnvironments, onHide, onSuccess, selectedWorkspaces, workspaces]);
|
||||
}, [includePrivateEnvironments, onHide, onSuccess, selectedWorkspaces, workspaces]);
|
||||
|
||||
const allSelected = workspaces.every((w) => selectedWorkspaces[w.id]);
|
||||
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
|
||||
@@ -129,9 +129,10 @@ function ExportDataDialogContent({
|
||||
<summary className="px-3 py-2">Extra Settings</summary>
|
||||
<div className="px-3 pb-2">
|
||||
<Checkbox
|
||||
checked={includeEnvironments}
|
||||
onChange={setIncludeEnvironments}
|
||||
title="Include environments"
|
||||
checked={includePrivateEnvironments}
|
||||
onChange={setIncludePrivateEnvironments}
|
||||
title="Include private environments"
|
||||
help='Environments marked as "sharable" will be exported by default'
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { GrpcEvent, GrpcRequest } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { format } from 'date-fns';
|
||||
import { useAtomValue , useSetAtom } from 'jotai';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCopy } from '../hooks/useCopy';
|
||||
import {
|
||||
activeGrpcConnectionAtom,
|
||||
activeGrpcConnections,
|
||||
@@ -12,12 +11,14 @@ import {
|
||||
useGrpcEvents,
|
||||
} from '../hooks/usePinnedGrpcConnection';
|
||||
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||
import { copyToClipboard } from '../lib/copy';
|
||||
import { AutoScroller } from './core/AutoScroller';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { JsonAttributeTree } from './core/JsonAttributeTree';
|
||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||
import { LoadingIcon } from './core/LoadingIcon';
|
||||
import { Separator } from './core/Separator';
|
||||
@@ -25,7 +26,6 @@ import { SplitLayout } from './core/SplitLayout';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { RecentGrpcConnectionsDropdown } from './RecentGrpcConnectionsDropdown';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
@@ -48,7 +48,6 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
|
||||
const activeConnection = useAtomValue(activeGrpcConnectionAtom);
|
||||
const events = useGrpcEvents(activeConnection?.id ?? null);
|
||||
const setPinnedGrpcConnectionId = useSetAtom(pinnedGrpcConnectionIdAtom);
|
||||
const copy = useCopy();
|
||||
|
||||
const activeEvent = useMemo(
|
||||
() => events.find((m) => m.id === activeEventId) ?? null,
|
||||
@@ -136,7 +135,7 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
|
||||
title="Copy message"
|
||||
icon="copy"
|
||||
size="xs"
|
||||
onClick={() => copy(activeEvent.content)}
|
||||
onClick={() => copyToClipboard(activeEvent.content)}
|
||||
/>
|
||||
</div>
|
||||
{!showLarge && activeEvent.content.length > 1000 * 1000 ? (
|
||||
@@ -161,7 +160,13 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
|
||||
</div>
|
||||
</VStack>
|
||||
) : (
|
||||
<JsonAttributeTree attrValue={JSON.parse(activeEvent?.content ?? '{}')} />
|
||||
<Editor
|
||||
language="json"
|
||||
defaultValue={activeEvent.content ?? ''}
|
||||
wrapLines={false}
|
||||
readOnly={true}
|
||||
stateKey={null}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -74,13 +74,13 @@ export function SettingsDropdown() {
|
||||
{
|
||||
label: 'Feedback',
|
||||
leftSlot: <Icon icon="chat" />,
|
||||
rightSlot: <Icon icon="external_link" />,
|
||||
rightSlot: <Icon icon="external_link" color="secondary" />,
|
||||
onSelect: () => openUrl('https://yaak.app/feedback'),
|
||||
},
|
||||
{
|
||||
label: 'Changelog',
|
||||
leftSlot: <Icon icon="cake" />,
|
||||
rightSlot: <Icon icon="external_link" />,
|
||||
rightSlot: <Icon icon="external_link" color="secondary" />,
|
||||
onSelect: () => openUrl(`https://yaak.app/changelog/${appInfo.version}`),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { format } from 'date-fns';
|
||||
import { hexy } from 'hexy';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useCopy } from '../hooks/useCopy';
|
||||
import { useFormatText } from '../hooks/useFormatText';
|
||||
import {
|
||||
activeWebsocketConnectionAtom,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
} from '../hooks/usePinnedWebsocketConnection';
|
||||
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||
import { languageFromContentType } from '../lib/contentType';
|
||||
import { copyToClipboard } from '../lib/copy';
|
||||
import { AutoScroller } from './core/AutoScroller';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
@@ -41,7 +41,6 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
|
||||
|
||||
const activeConnection = useAtomValue(activeWebsocketConnectionAtom);
|
||||
const connections = useAtomValue(activeWebsocketConnectionsAtom);
|
||||
|
||||
const events = useWebsocketEvents(activeConnection?.id ?? null);
|
||||
|
||||
const activeEvent = useMemo(
|
||||
@@ -63,7 +62,6 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
|
||||
|
||||
const language = languageFromContentType(null, message);
|
||||
const formattedMessage = useFormatText({ language, text: message, pretty: true });
|
||||
const copy = useCopy();
|
||||
|
||||
return (
|
||||
<SplitLayout
|
||||
@@ -151,7 +149,7 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
|
||||
title="Copy message"
|
||||
icon="copy"
|
||||
size="xs"
|
||||
onClick={() => copy(formattedMessage.data ?? '')}
|
||||
onClick={() => copyToClipboard(formattedMessage.data ?? '')}
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
@@ -29,15 +29,41 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
||||
|
||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
||||
const [key, setKey] = useState<{ key: string | null; error: string | null } | null>(null);
|
||||
|
||||
if (workspace == null || workspaceMeta == null) {
|
||||
useEffect(() => {
|
||||
if (workspaceMeta == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workspaceMeta?.encryptionKey == null) {
|
||||
setKey({ key: null, error: null });
|
||||
return;
|
||||
}
|
||||
|
||||
revealWorkspaceKey(workspaceMeta.workspaceId).then(
|
||||
(key) => {
|
||||
setKey({ key, error: null });
|
||||
},
|
||||
(err) => {
|
||||
setKey({ key: null, error: `${err}` });
|
||||
},
|
||||
);
|
||||
}, [setKey, workspaceMeta, workspaceMeta?.encryptionKey]);
|
||||
|
||||
if (key == null || workspace == null || workspaceMeta == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (workspace.encryptionKeyChallenge && workspaceMeta.encryptionKey == null) {
|
||||
// Prompt for key if it doesn't exist or could not be decrypted
|
||||
if (
|
||||
key.error != null ||
|
||||
(workspace.encryptionKeyChallenge && workspaceMeta.encryptionKey == null)
|
||||
) {
|
||||
return (
|
||||
<EnterWorkspaceKey
|
||||
workspaceMeta={workspaceMeta}
|
||||
error={key.error}
|
||||
onEnabled={() => {
|
||||
onDone?.();
|
||||
onEnabledEncryption?.();
|
||||
@@ -46,12 +72,13 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceMeta.encryptionKey) {
|
||||
// Show the key if it exists
|
||||
if (workspaceMeta.encryptionKey && key.key != null) {
|
||||
const keyRevealer = (
|
||||
<KeyRevealer
|
||||
disableLabel={justEnabledEncryption}
|
||||
defaultShow={justEnabledEncryption}
|
||||
workspaceId={workspaceMeta.workspaceId}
|
||||
encryptionKey={key.key}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
@@ -63,10 +90,13 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
||||
)}
|
||||
{keyRevealer}
|
||||
{onDone && (
|
||||
<Button color="secondary" onClick={() => {
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
onDone();
|
||||
onEnabledEncryption?.();
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
)}
|
||||
@@ -74,6 +104,7 @@ export function WorkspaceEncryptionSetting({ size, expanded, onDone, onEnabledEn
|
||||
);
|
||||
}
|
||||
|
||||
// Show button to enable encryption
|
||||
return (
|
||||
<div className="mb-auto flex flex-col-reverse">
|
||||
<Button
|
||||
@@ -107,17 +138,23 @@ const setWorkspaceKeyMut = createFastMutation({
|
||||
function EnterWorkspaceKey({
|
||||
workspaceMeta,
|
||||
onEnabled,
|
||||
error,
|
||||
}: {
|
||||
workspaceMeta: WorkspaceMeta;
|
||||
onEnabled?: () => void;
|
||||
error?: string | null;
|
||||
}) {
|
||||
const [key, setKey] = useState<string>('');
|
||||
return (
|
||||
<VStack space={4}>
|
||||
<Banner color="info">
|
||||
This workspace contains encrypted values but no key is configured. Please enter the
|
||||
workspace key to access the encrypted data.
|
||||
</Banner>
|
||||
<VStack space={4} className="w-full">
|
||||
{error ? (
|
||||
<Banner color="danger">{error}</Banner>
|
||||
) : (
|
||||
<Banner color="info">
|
||||
This workspace contains encrypted values but no key is configured. Please enter the
|
||||
workspace key to access the encrypted data.
|
||||
</Banner>
|
||||
)}
|
||||
<HStack
|
||||
as="form"
|
||||
alignItems="end"
|
||||
@@ -149,23 +186,16 @@ function EnterWorkspaceKey({
|
||||
}
|
||||
|
||||
function KeyRevealer({
|
||||
workspaceId,
|
||||
defaultShow = false,
|
||||
disableLabel = false,
|
||||
encryptionKey,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
defaultShow?: boolean;
|
||||
disableLabel?: boolean;
|
||||
encryptionKey: string;
|
||||
}) {
|
||||
const [key, setKey] = useState<string | null>(null);
|
||||
const [show, setShow] = useStateWithDeps<boolean>(defaultShow, [defaultShow]);
|
||||
|
||||
useEffect(() => {
|
||||
revealWorkspaceKey(workspaceId).then(setKey);
|
||||
}, [setKey, workspaceId]);
|
||||
|
||||
if (key == null) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -180,10 +210,10 @@ function KeyRevealer({
|
||||
<IconTooltip iconSize="sm" size="lg" content={helpAfterEncryption} />
|
||||
</span>
|
||||
)}
|
||||
{key && <HighlightedKey keyText={key} show={show} />}
|
||||
{encryptionKey && <HighlightedKey keyText={encryptionKey} show={show} />}
|
||||
</VStack>
|
||||
<HStack>
|
||||
{key && <CopyIconButton text={key} title="Copy workspace key" />}
|
||||
{encryptionKey && <CopyIconButton text={encryptionKey} title="Copy workspace key" />}
|
||||
<IconButton
|
||||
title={show ? 'Hide' : 'Reveal' + 'workspace key'}
|
||||
icon={show ? 'eye_closed' : 'eye'}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from './core/Button';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { Separator } from './core/Separator';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { SyncToFilesystemSetting } from './SyncToFilesystemSetting';
|
||||
import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting';
|
||||
@@ -67,20 +67,23 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const didDelete = await deleteModelWithConfirm(workspace);
|
||||
if (didDelete) {
|
||||
hide(); // Only hide if actually deleted workspace
|
||||
await router.navigate({ to: '/' });
|
||||
}
|
||||
}}
|
||||
color="danger"
|
||||
variant="border"
|
||||
size="xs"
|
||||
>
|
||||
Delete Workspace
|
||||
</Button>
|
||||
<HStack alignItems="center" justifyContent="between" className="w-full">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const didDelete = await deleteModelWithConfirm(workspace);
|
||||
if (didDelete) {
|
||||
hide(); // Only hide if actually deleted workspace
|
||||
await router.navigate({ to: '/' });
|
||||
}
|
||||
}}
|
||||
color="danger"
|
||||
variant="border"
|
||||
size="xs"
|
||||
>
|
||||
Delete Workspace
|
||||
</Button>
|
||||
<InlineCode className="select-text cursor-text">{workspaceId}</InlineCode>
|
||||
</HStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export function BulkPairEditor({
|
||||
namePlaceholder,
|
||||
valuePlaceholder,
|
||||
forceUpdateKey,
|
||||
forcedEnvironmentId,
|
||||
stateKey,
|
||||
}: Props) {
|
||||
const pairsText = useMemo(() => {
|
||||
@@ -36,6 +37,7 @@ export function BulkPairEditor({
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
stateKey={`bulk_pair.${stateKey}`}
|
||||
forcedEnvironmentId={forcedEnvironmentId}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`}
|
||||
defaultValue={pairsText}
|
||||
|
||||
@@ -2,7 +2,6 @@ import classNames from 'classnames';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useKey } from 'react-use';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Heading } from './Heading';
|
||||
import { IconButton } from './IconButton';
|
||||
@@ -42,18 +41,9 @@ export function Dialog({
|
||||
[description],
|
||||
);
|
||||
|
||||
useKey(
|
||||
'Escape',
|
||||
() => {
|
||||
if (!open) return;
|
||||
onClose?.();
|
||||
},
|
||||
{},
|
||||
[open],
|
||||
);
|
||||
|
||||
return (
|
||||
<Overlay open={open} onClose={disableBackdropClose ? undefined : onClose} portalName="dialog">
|
||||
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<div
|
||||
role="dialog"
|
||||
className={classNames(
|
||||
@@ -64,6 +54,16 @@ export function Dialog({
|
||||
)}
|
||||
aria-labelledby={titleId}
|
||||
aria-describedby={descriptionId}
|
||||
tabIndex={-1}
|
||||
onKeyDown={(e) => {
|
||||
// NOTE: We handle Escape on the element itself so that it doesn't close multiple
|
||||
// dialogs and can be intercepted by children if needed.
|
||||
if (e.key === 'Escape') {
|
||||
onClose?.();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<m.div
|
||||
initial={{ top: 5, scale: 0.97 }}
|
||||
|
||||
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import { useKeyValue } from '../../hooks/useKeyValue';
|
||||
import type { BannerProps } from './Banner';
|
||||
import { Banner } from './Banner';
|
||||
import { IconButton } from './IconButton';
|
||||
import { Button } from './Button';
|
||||
|
||||
export function DismissibleBanner({
|
||||
children,
|
||||
@@ -19,14 +19,17 @@ export function DismissibleBanner({
|
||||
if (dismissed) return null;
|
||||
|
||||
return (
|
||||
<Banner className={classNames(className, 'relative pr-8')} {...props}>
|
||||
<IconButton
|
||||
className="!absolute right-0 top-0"
|
||||
icon="x"
|
||||
<Banner className={classNames(className, 'relative grid grid-cols-[1fr_auto] gap-3')} {...props}>
|
||||
{children}
|
||||
<Button
|
||||
variant="border"
|
||||
color={props.color}
|
||||
size="xs"
|
||||
onClick={() => setDismissed((d) => !d)}
|
||||
title="Dismiss message"
|
||||
/>
|
||||
{children}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import * as m from 'motion/react-m';
|
||||
import { atom } from 'jotai';
|
||||
import * as m from 'motion/react-m';
|
||||
import type {
|
||||
CSSProperties,
|
||||
FocusEvent as ReactFocusEvent,
|
||||
@@ -34,9 +34,9 @@ import { Overlay } from '../Overlay';
|
||||
import { Button } from './Button';
|
||||
import { HotKey } from './HotKey';
|
||||
import { Icon } from './Icon';
|
||||
import { LoadingIcon } from './LoadingIcon';
|
||||
import { Separator } from './Separator';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
import { LoadingIcon } from './LoadingIcon';
|
||||
|
||||
export type DropdownItemSeparator = {
|
||||
type: 'separator';
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables';
|
||||
import { activeEnvironmentIdAtom } from '../../../hooks/useActiveEnvironment';
|
||||
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
|
||||
import { useRequestEditor } from '../../../hooks/useRequestEditor';
|
||||
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
|
||||
import { showDialog } from '../../../lib/dialog';
|
||||
@@ -69,6 +70,7 @@ export interface EditorProps {
|
||||
disableTabIndent?: boolean;
|
||||
disabled?: boolean;
|
||||
extraExtensions?: Extension[];
|
||||
forcedEnvironmentId?: string;
|
||||
forceUpdateKey?: string | number;
|
||||
format?: (v: string) => Promise<string>;
|
||||
heightMode?: 'auto' | 'full';
|
||||
@@ -108,6 +110,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
disableTabIndent,
|
||||
disabled,
|
||||
extraExtensions,
|
||||
forcedEnvironmentId,
|
||||
forceUpdateKey,
|
||||
format,
|
||||
heightMode,
|
||||
@@ -130,7 +133,9 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
) {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
|
||||
const allEnvironmentVariables = useActiveEnvironmentVariables();
|
||||
const activeEnvironmentId = useAtomValue(activeEnvironmentIdAtom);
|
||||
const environmentId = forcedEnvironmentId ?? activeEnvironmentId ?? null;
|
||||
const allEnvironmentVariables = useEnvironmentVariables(environmentId);
|
||||
const environmentVariables = autocompleteVariables ? allEnvironmentVariables : emptyVariables;
|
||||
const useTemplating = !!(autocompleteFunctions || autocompleteVariables || autocomplete);
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ const icons = {
|
||||
eye_closed: lucide.EyeOffIcon,
|
||||
file_code: lucide.FileCodeIcon,
|
||||
filter: lucide.FilterIcon,
|
||||
flame: lucide.FlameIcon,
|
||||
flask: lucide.FlaskConicalIcon,
|
||||
folder: lucide.FolderIcon,
|
||||
folder_git: lucide.FolderGitIcon,
|
||||
@@ -138,7 +139,7 @@ export const Icon = memo(function Icon({
|
||||
size === 'xs' && 'h-3 w-3',
|
||||
size === '2xs' && 'h-2.5 w-2.5',
|
||||
color === 'default' && 'inherit',
|
||||
color === 'danger' && 'text-danger!',
|
||||
color === 'danger' && 'text-danger',
|
||||
color === 'warning' && 'text-warning',
|
||||
color === 'notice' && 'text-notice',
|
||||
color === 'info' && 'text-info',
|
||||
|
||||
@@ -7,13 +7,25 @@ import { Tooltip } from './Tooltip';
|
||||
type Props = Omit<TooltipProps, 'children'> & {
|
||||
icon?: IconProps['icon'];
|
||||
iconSize?: IconProps['size'];
|
||||
iconColor?: IconProps['color'];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function IconTooltip({ content, icon = 'info', iconSize, ...tooltipProps }: Props) {
|
||||
export function IconTooltip({
|
||||
content,
|
||||
icon = 'info',
|
||||
iconColor,
|
||||
iconSize,
|
||||
...tooltipProps
|
||||
}: Props) {
|
||||
return (
|
||||
<Tooltip content={content} {...tooltipProps}>
|
||||
<Icon className="opacity-60 hover:opacity-100" icon={icon} size={iconSize} />
|
||||
<Icon
|
||||
className="opacity-60 hover:opacity-100"
|
||||
icon={icon}
|
||||
size={iconSize}
|
||||
color={iconColor}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,11 +30,13 @@ import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import { Label } from './Label';
|
||||
import { HStack } from './Stacks';
|
||||
import { copyToClipboard } from '../../lib/copy';
|
||||
|
||||
export type InputProps = Pick<
|
||||
EditorProps,
|
||||
| 'language'
|
||||
| 'autocomplete'
|
||||
| 'forcedEnvironmentId'
|
||||
| 'forceUpdateKey'
|
||||
| 'disabled'
|
||||
| 'autoFocus'
|
||||
@@ -387,19 +389,32 @@ function EncryptionInput({
|
||||
const dropdownItems = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
label: state.obscured ? 'Reveal value' : 'Conceal value',
|
||||
label: state.obscured ? 'Reveal' : 'Conceal',
|
||||
disabled: isEncryptionEnabled && state.fieldType === 'text',
|
||||
leftSlot: <Icon icon={state.obscured ? 'eye' : 'eye_closed'} />,
|
||||
onSelect: () => setState((s) => ({ ...s, obscured: !s.obscured })),
|
||||
},
|
||||
{
|
||||
label: 'Copy',
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
hidden: !state.value,
|
||||
onSelect: () => copyToClipboard(state.value ?? ''),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: state.fieldType === 'text' ? 'Encrypt Value' : 'Decrypt Value',
|
||||
label: state.fieldType === 'text' ? 'Encrypt Field' : 'Decrypt Field',
|
||||
leftSlot: <Icon icon={state.fieldType === 'text' ? 'lock' : 'lock_open'} />,
|
||||
onSelect: () => handleFieldTypeChange(state.fieldType === 'text' ? 'encrypted' : 'text'),
|
||||
},
|
||||
],
|
||||
[handleFieldTypeChange, isEncryptionEnabled, setState, state.fieldType, state.obscured],
|
||||
[
|
||||
handleFieldTypeChange,
|
||||
isEncryptionEnabled,
|
||||
setState,
|
||||
state.fieldType,
|
||||
state.obscured,
|
||||
state.value,
|
||||
],
|
||||
);
|
||||
|
||||
let tint: InputProps['tint'];
|
||||
|
||||
@@ -41,6 +41,7 @@ export type PairEditorProps = {
|
||||
allowFileValues?: boolean;
|
||||
allowMultilineValues?: boolean;
|
||||
className?: string;
|
||||
forcedEnvironmentId?: string;
|
||||
forceUpdateKey?: string;
|
||||
nameAutocomplete?: GenericCompletionConfig;
|
||||
nameAutocompleteFunctions?: boolean;
|
||||
@@ -81,6 +82,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
allowFileValues,
|
||||
allowMultilineValues,
|
||||
className,
|
||||
forcedEnvironmentId,
|
||||
forceUpdateKey,
|
||||
nameAutocomplete,
|
||||
nameAutocompleteFunctions,
|
||||
@@ -235,6 +237,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
allowFileValues={allowFileValues}
|
||||
allowMultilineValues={allowMultilineValues}
|
||||
className="py-1"
|
||||
forcedEnvironmentId={forcedEnvironmentId}
|
||||
forceFocusNamePairId={forceFocusNamePairId}
|
||||
forceFocusValuePairId={forceFocusValuePairId}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
@@ -292,6 +295,7 @@ type PairEditorRowProps = {
|
||||
PairEditorProps,
|
||||
| 'allowFileValues'
|
||||
| 'allowMultilineValues'
|
||||
| 'forcedEnvironmentId'
|
||||
| 'forceUpdateKey'
|
||||
| 'nameAutocomplete'
|
||||
| 'nameAutocompleteVariables'
|
||||
@@ -311,6 +315,7 @@ function PairEditorRow({
|
||||
allowFileValues,
|
||||
allowMultilineValues,
|
||||
className,
|
||||
forcedEnvironmentId,
|
||||
forceFocusNamePairId,
|
||||
forceFocusValuePairId,
|
||||
forceUpdateKey,
|
||||
@@ -502,6 +507,7 @@ function PairEditorRow({
|
||||
size="sm"
|
||||
required={!isLast && !!pair.enabled && !!pair.value}
|
||||
validate={nameValidate}
|
||||
forcedEnvironmentId={forcedEnvironmentId}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
containerClassName={classNames(isLast && 'border-dashed')}
|
||||
defaultValue={pair.name}
|
||||
@@ -549,6 +555,7 @@ function PairEditorRow({
|
||||
size="sm"
|
||||
containerClassName={classNames(isLast && 'border-dashed')}
|
||||
validate={valueValidate}
|
||||
forcedEnvironmentId={forcedEnvironmentId}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
defaultValue={pair.value}
|
||||
label="Value"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PairEditor } from './PairEditor';
|
||||
|
||||
interface Props extends PairEditorProps {
|
||||
preferenceName: string;
|
||||
forcedEnvironmentId?: string;
|
||||
}
|
||||
|
||||
export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOrBulkEditor(
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Portal } from '../Portal';
|
||||
export interface TooltipProps {
|
||||
children: ReactNode;
|
||||
content: ReactNode;
|
||||
tabIndex?: number,
|
||||
size?: 'md' | 'lg';
|
||||
}
|
||||
|
||||
@@ -18,7 +19,7 @@ const hiddenStyles: CSSProperties = {
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
export function Tooltip({ children, content, size = 'md' }: TooltipProps) {
|
||||
export function Tooltip({ children, content, tabIndex, size = 'md' }: TooltipProps) {
|
||||
const [isOpen, setIsOpen] = useState<CSSProperties>();
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
@@ -89,11 +90,12 @@ export function Tooltip({ children, content, size = 'md' }: TooltipProps) {
|
||||
<Triangle className="text-border mb-2" />
|
||||
</div>
|
||||
</Portal>
|
||||
<button
|
||||
<span
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
role="button"
|
||||
aria-describedby={isOpen ? id.current : undefined}
|
||||
className="flex-grow-0 inline-flex items-center"
|
||||
tabIndex={tabIndex ?? 0}
|
||||
className="flex-grow-0 flex items-center"
|
||||
onClick={handleToggleImmediate}
|
||||
onMouseEnter={handleOpen}
|
||||
onMouseLeave={handleClose}
|
||||
@@ -102,7 +104,7 @@ export function Tooltip({ children, content, size = 'md' }: TooltipProps) {
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user