mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-27 11:51:13 +01:00
Merge pull request #256
* Update environment model to get ready for request/folder environments * Folder environments in UI * Folder environments working * Tweaks and fixes * Tweak environment encryption UX * Tweak environment encryption UX * Address comments * Update fn name * Add tsc back to lint rules * Update src-web/components/EnvironmentEditor.tsx * Merge remote-tracking branch 'origin/folder-environments' into folder…
This commit is contained in:
@@ -27,7 +27,8 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { createRequestAndNavigate } from '../lib/createRequestAndNavigate';
|
||||
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
||||
import { showDialog, toggleDialog } from '../lib/dialog';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
import { editEnvironment } from '../lib/editEnvironment';
|
||||
import { renameModelWithPrompt } from '../lib/renameModelWithPrompt';
|
||||
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
|
||||
import { router } from '../lib/router';
|
||||
@@ -40,7 +41,6 @@ import { HttpMethodTag } from './core/HttpMethodTag';
|
||||
import { Icon } from './core/Icon';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
||||
|
||||
interface CommandPaletteGroup {
|
||||
key: string;
|
||||
@@ -125,15 +125,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
||||
key: 'environment.edit',
|
||||
label: 'Edit Environment',
|
||||
action: 'environmentEditor.toggle',
|
||||
onSelect: () => {
|
||||
toggleDialog({
|
||||
id: 'environment-editor',
|
||||
noPadding: true,
|
||||
size: 'lg',
|
||||
className: 'h-[80vh]',
|
||||
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
|
||||
});
|
||||
},
|
||||
onSelect: () => editEnvironment(activeEnvironment),
|
||||
},
|
||||
{
|
||||
key: 'environment.create',
|
||||
|
||||
@@ -51,7 +51,7 @@ export function ConfirmLargeResponse({ children, response }: Props) {
|
||||
color="secondary"
|
||||
variant="border"
|
||||
size="xs"
|
||||
text={() => getResponseBodyText(response)}
|
||||
text={() => getResponseBodyText({ responseId: response.id, filter: null })}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import classNames from 'classnames';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||
import { toggleDialog } from '../lib/dialog';
|
||||
import { editEnvironment } from '../lib/editEnvironment';
|
||||
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
|
||||
import { Button } from './core/Button';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -23,16 +22,6 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
const { subEnvironments, baseEnvironment } = useEnvironmentsBreakdown();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
|
||||
const showEnvironmentDialog = useCallback(() => {
|
||||
toggleDialog({
|
||||
id: 'environment-editor',
|
||||
noPadding: true,
|
||||
size: 'lg',
|
||||
className: 'h-[80vh]',
|
||||
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
|
||||
});
|
||||
}, [activeEnvironment]);
|
||||
|
||||
const items: DropdownItem[] = useMemo(
|
||||
() => [
|
||||
...subEnvironments.map(
|
||||
@@ -55,14 +44,13 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
? [{ type: 'separator', label: 'Environments' }]
|
||||
: []) as DropdownItem[]),
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
hotKeyAction: 'environmentEditor.toggle',
|
||||
leftSlot: <Icon icon="box" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
onSelect: () => editEnvironment(activeEnvironment),
|
||||
},
|
||||
],
|
||||
[activeEnvironment?.id, subEnvironments, showEnvironmentDialog],
|
||||
[subEnvironments, activeEnvironment],
|
||||
);
|
||||
|
||||
const hasBaseVars =
|
||||
@@ -79,7 +67,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
)}
|
||||
// If no environments, the button simply opens the dialog.
|
||||
// NOTE: We don't create a new button because we want to reuse the hotkey from the menu items
|
||||
onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined}
|
||||
onClick={subEnvironments.length === 0 ? () => editEnvironment(null) : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
|
||||
|
||||
@@ -1,42 +1,28 @@
|
||||
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 React, { useCallback, useState } from 'react';
|
||||
import { createEnvironmentAndActivate } from '../commands/createEnvironment';
|
||||
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 { isBaseEnvironment } from '../lib/model_util';
|
||||
import { showPrompt } from '../lib/prompt';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import {
|
||||
setupOrConfigureEncryption,
|
||||
withEncryptionEnabled,
|
||||
} from '../lib/setupOrConfigureEncryption';
|
||||
import { showColorPicker } from '../lib/showColorPicker';
|
||||
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';
|
||||
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||
import { EnvironmentEditor } from './EnvironmentEditor';
|
||||
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
|
||||
|
||||
interface Props {
|
||||
initialEnvironment: Environment | null;
|
||||
@@ -97,13 +83,13 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
<aside className="w-full min-w-0 pt-2">
|
||||
<div className="min-w-0 h-full overflow-y-auto pt-1">
|
||||
{[baseEnvironment, ...otherBaseEnvironments].map((e) => (
|
||||
<SidebarButton
|
||||
<EnvironmentDialogSidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id == e.id}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
environment={e}
|
||||
duplicateEnvironment={handleDuplicateEnvironment}
|
||||
// Allow deleting base environment if there are multiples
|
||||
// Allow deleting the base environment if there are multiples
|
||||
deleteEnvironment={
|
||||
otherBaseEnvironments.length > 0 ? handleDeleteEnvironment : null
|
||||
}
|
||||
@@ -121,7 +107,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
}
|
||||
>
|
||||
{resolvedModelName(e)}
|
||||
</SidebarButton>
|
||||
</EnvironmentDialogSidebarButton>
|
||||
))}
|
||||
{subEnvironments.length > 0 && (
|
||||
<div className="px-2">
|
||||
@@ -129,7 +115,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
</div>
|
||||
)}
|
||||
{subEnvironments.map((e) => (
|
||||
<SidebarButton
|
||||
<EnvironmentDialogSidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id === e.id}
|
||||
environment={e}
|
||||
@@ -139,7 +125,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
deleteEnvironment={handleDeleteEnvironment}
|
||||
>
|
||||
{e.name}
|
||||
</SidebarButton>
|
||||
</EnvironmentDialogSidebarButton>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
@@ -153,7 +139,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
</div>
|
||||
) : (
|
||||
<EnvironmentEditor
|
||||
className="pt-2 border-l border-border-subtle"
|
||||
className="pl-4 pt-3 border-l border-border-subtle"
|
||||
environment={selectedEnvironment}
|
||||
/>
|
||||
)
|
||||
@@ -162,139 +148,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
);
|
||||
};
|
||||
|
||||
const EnvironmentEditor = function ({
|
||||
environment: selectedEnvironment,
|
||||
className,
|
||||
}: {
|
||||
environment: Environment;
|
||||
className?: string;
|
||||
}) {
|
||||
const workspaceId = selectedEnvironment.workspaceId;
|
||||
const isEncryptionEnabled = useIsEncryptionEnabled();
|
||||
const valueVisibility = useKeyValue<boolean>({
|
||||
namespace: 'global',
|
||||
key: ['environmentValueVisibility', workspaceId],
|
||||
fallback: false,
|
||||
});
|
||||
const { allEnvironments } = useEnvironmentsBreakdown();
|
||||
const handleChange = useCallback(
|
||||
(variables: PairWithId[]) => patchModel(selectedEnvironment, { variables }),
|
||||
[selectedEnvironment],
|
||||
);
|
||||
const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey();
|
||||
|
||||
// Gather a list of env names from other environments to help the user get them aligned
|
||||
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
|
||||
const options: GenericCompletionOption[] = [];
|
||||
if (selectedEnvironment.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 === selectedEnvironment.id);
|
||||
if (isAlreadyInActive) continue;
|
||||
options.push({
|
||||
label: name,
|
||||
type: 'constant',
|
||||
detail: containingEnvs.map((e) => e.name).join(', '),
|
||||
});
|
||||
}
|
||||
return { options };
|
||||
}, [selectedEnvironment.base, selectedEnvironment.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 !selectedEnvironment.variables.every(
|
||||
(v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure',
|
||||
);
|
||||
}
|
||||
}, [selectedEnvironment.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 (
|
||||
<VStack space={4} className={classNames(className, 'pl-4')}>
|
||||
<Heading className="w-full flex items-center gap-0.5">
|
||||
<EnvironmentColorIndicator clickToEdit environment={selectedEnvironment ?? null} />
|
||||
<div className="mr-2">{selectedEnvironment?.name}</div>
|
||||
{isEncryptionEnabled ? (
|
||||
promptToEncrypt ? (
|
||||
<BadgeButton color="notice" onClick={() => encryptEnvironment(selectedEnvironment)}>
|
||||
Encrypt All Variables
|
||||
</BadgeButton>
|
||||
) : (
|
||||
<BadgeButton color="secondary" onClick={setupOrConfigureEncryption}>
|
||||
Encryption Settings
|
||||
</BadgeButton>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<BadgeButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}>
|
||||
{valueVisibility.value ? 'Hide Values' : 'Show Values'}
|
||||
</BadgeButton>
|
||||
</>
|
||||
)}
|
||||
</Heading>
|
||||
{selectedEnvironment.public && promptToEncrypt && (
|
||||
<DismissibleBanner
|
||||
id={`warn-unencrypted-${selectedEnvironment.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"
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
namePlaceholder="VAR_NAME"
|
||||
nameValidate={validateName}
|
||||
valueType={valueType}
|
||||
valueAutocompleteVariables
|
||||
valueAutocompleteFunctions
|
||||
forceUpdateKey={`${selectedEnvironment.id}::${forceUpdateKey}`}
|
||||
pairs={selectedEnvironment.variables}
|
||||
onChange={handleChange}
|
||||
stateKey={`environment.${selectedEnvironment.id}`}
|
||||
forcedEnvironmentId={
|
||||
// Editing the base environment should resolve variables using the active environment.
|
||||
// Editing a sub environment should resolve variables as if it's the active environment
|
||||
selectedEnvironment.base ? undefined : selectedEnvironment.id
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
function SidebarButton({
|
||||
function EnvironmentDialogSidebarButton({
|
||||
children,
|
||||
className,
|
||||
active,
|
||||
@@ -359,7 +213,7 @@ function SidebarButton({
|
||||
{
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
hidden: environment.base,
|
||||
hidden: isBaseEnvironment(environment),
|
||||
onSelect: async () => {
|
||||
const name = await showPrompt({
|
||||
id: 'rename-environment',
|
||||
@@ -392,23 +246,13 @@ function SidebarButton({
|
||||
{
|
||||
label: environment.color ? 'Change Color' : 'Assign Color',
|
||||
leftSlot: <Icon icon="palette" />,
|
||||
hidden: environment.base,
|
||||
hidden: isBaseEnvironment(environment),
|
||||
onSelect: async () => showColorPicker(environment),
|
||||
},
|
||||
{
|
||||
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.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
rightSlot: <EnvironmentSharableTooltip />,
|
||||
onSelect: async () => {
|
||||
await patchModel(environment, { public: !environment.public });
|
||||
},
|
||||
|
||||
169
src-web/components/EnvironmentEditor.tsx
Normal file
169
src-web/components/EnvironmentEditor.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import { patchModel } from '@yaakapp-internal/models';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||
import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useRandomKey } from '../hooks/useRandomKey';
|
||||
import { analyzeTemplate, convertTemplateToSecure } from '../lib/encryption';
|
||||
import { isBaseEnvironment } from '../lib/model_util';
|
||||
import {
|
||||
setupOrConfigureEncryption,
|
||||
withEncryptionEnabled,
|
||||
} from '../lib/setupOrConfigureEncryption';
|
||||
import { BadgeButton } from './core/BadgeButton';
|
||||
import { DismissibleBanner } from './core/DismissibleBanner';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
import { Heading } from './core/Heading';
|
||||
import type { PairWithId } from './core/PairEditor';
|
||||
import { ensurePairId } from './core/PairEditor';
|
||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
|
||||
|
||||
export function EnvironmentEditor({
|
||||
environment: selectedEnvironment,
|
||||
hideName,
|
||||
className,
|
||||
}: {
|
||||
environment: Environment;
|
||||
hideName?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const workspaceId = selectedEnvironment.workspaceId;
|
||||
const isEncryptionEnabled = useIsEncryptionEnabled();
|
||||
const valueVisibility = useKeyValue<boolean>({
|
||||
namespace: 'global',
|
||||
key: ['environmentValueVisibility', workspaceId],
|
||||
fallback: false,
|
||||
});
|
||||
const { allEnvironments } = useEnvironmentsBreakdown();
|
||||
const handleChange = useCallback(
|
||||
(variables: PairWithId[]) => patchModel(selectedEnvironment, { variables }),
|
||||
[selectedEnvironment],
|
||||
);
|
||||
const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey();
|
||||
|
||||
// Gather a list of env names from other environments to help the user get them aligned
|
||||
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
|
||||
const options: GenericCompletionOption[] = [];
|
||||
if (isBaseEnvironment(selectedEnvironment)) {
|
||||
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 === selectedEnvironment.id);
|
||||
if (isAlreadyInActive) continue;
|
||||
options.push({
|
||||
label: name,
|
||||
type: 'constant',
|
||||
detail: containingEnvs.map((e) => e.name).join(', '),
|
||||
});
|
||||
}
|
||||
return { options };
|
||||
}, [selectedEnvironment, 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 allVariableAreEncrypted = useMemo(
|
||||
() =>
|
||||
selectedEnvironment.variables.every(
|
||||
(v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure',
|
||||
),
|
||||
[selectedEnvironment.variables],
|
||||
);
|
||||
|
||||
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 (
|
||||
<VStack space={4} className={className}>
|
||||
<Heading className="w-full flex items-center gap-0.5">
|
||||
<EnvironmentColorIndicator clickToEdit environment={selectedEnvironment ?? null} />
|
||||
{!hideName && <div className="mr-2">{selectedEnvironment?.name}</div>}
|
||||
{isEncryptionEnabled ? (
|
||||
!allVariableAreEncrypted ? (
|
||||
<BadgeButton color="notice" onClick={() => encryptEnvironment(selectedEnvironment)}>
|
||||
Encrypt All Variables
|
||||
</BadgeButton>
|
||||
) : (
|
||||
<BadgeButton color="secondary" onClick={setupOrConfigureEncryption}>
|
||||
Encryption Settings
|
||||
</BadgeButton>
|
||||
)
|
||||
) : (
|
||||
<BadgeButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}>
|
||||
{valueVisibility.value ? 'Hide Values' : 'Show Values'}
|
||||
</BadgeButton>
|
||||
)}
|
||||
<BadgeButton
|
||||
color="secondary"
|
||||
rightSlot={<EnvironmentSharableTooltip />}
|
||||
onClick={async () => {
|
||||
await patchModel(selectedEnvironment, { public: !selectedEnvironment.public });
|
||||
}}
|
||||
>
|
||||
{selectedEnvironment.public ? 'Sharable' : 'Private'}
|
||||
</BadgeButton>
|
||||
</Heading>
|
||||
{selectedEnvironment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && (
|
||||
<DismissibleBanner
|
||||
id={`warn-unencrypted-${selectedEnvironment.id}`}
|
||||
color="notice"
|
||||
className="mr-3"
|
||||
actions={[
|
||||
{
|
||||
label: 'Encrypt Variables',
|
||||
onClick: () => encryptEnvironment(selectedEnvironment),
|
||||
color: 'primary',
|
||||
},
|
||||
]}
|
||||
>
|
||||
This sharable environment contains plain-text secrets
|
||||
</DismissibleBanner>
|
||||
)}
|
||||
<div className="h-full pr-2 pb-2 grid grid-rows-[minmax(0,1fr)] overflow-auto">
|
||||
<PairOrBulkEditor
|
||||
allowMultilineValues
|
||||
preferenceName="environment"
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
namePlaceholder="VAR_NAME"
|
||||
nameValidate={validateName}
|
||||
valueType={valueType}
|
||||
valueAutocompleteVariables
|
||||
valueAutocompleteFunctions
|
||||
forceUpdateKey={`${selectedEnvironment.id}::${forceUpdateKey}`}
|
||||
pairs={selectedEnvironment.variables}
|
||||
onChange={handleChange}
|
||||
stateKey={`environment.${selectedEnvironment.id}`}
|
||||
forcedEnvironmentId={
|
||||
// Editing the base environment should resolve variables using the active environment.
|
||||
// Editing a sub environment should resolve variables as if it's the active environment
|
||||
isBaseEnvironment(selectedEnvironment) ? undefined : selectedEnvironment.id
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
8
src-web/components/EnvironmentSharableTooltip.tsx
Normal file
8
src-web/components/EnvironmentSharableTooltip.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { IconTooltip } from './core/IconTooltip';
|
||||
|
||||
export function EnvironmentSharableTooltip() {
|
||||
return (
|
||||
<IconTooltip content="Sharable environments are included in Directory Sync and data export." />
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
import { foldersAtom, patchModel } from '@yaakapp-internal/models';
|
||||
import { createWorkspaceModel, foldersAtom, patchModel } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useAuthTab } from '../hooks/useAuthTab';
|
||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||
import { Button } from './core/Button';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Input } from './core/Input';
|
||||
import { Link } from './core/Link';
|
||||
import { VStack } from './core/Stacks';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { EnvironmentEditor } from './EnvironmentEditor';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
@@ -19,9 +25,10 @@ interface Props {
|
||||
|
||||
const TAB_AUTH = 'auth';
|
||||
const TAB_HEADERS = 'headers';
|
||||
const TAB_VARIABLES = 'variables';
|
||||
const TAB_GENERAL = 'general';
|
||||
|
||||
export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL;
|
||||
export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL | typeof TAB_VARIABLES;
|
||||
|
||||
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
const folders = useAtomValue(foldersAtom);
|
||||
@@ -30,6 +37,11 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
const authTab = useAuthTab(TAB_AUTH, folder);
|
||||
const headersTab = useHeadersTab(TAB_HEADERS, folder);
|
||||
const inheritedHeaders = useInheritedHeaders(folder);
|
||||
const environments = useEnvironmentsBreakdown();
|
||||
const folderEnvironment = environments.allEnvironments.find(
|
||||
(e) => e.parentModel === 'folder' && e.parentId === folderId,
|
||||
);
|
||||
const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length;
|
||||
|
||||
const tabs = useMemo<TabItem[]>(() => {
|
||||
if (folder == null) return [];
|
||||
@@ -39,10 +51,15 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
value: TAB_GENERAL,
|
||||
label: 'General',
|
||||
},
|
||||
...authTab,
|
||||
...headersTab,
|
||||
...authTab,
|
||||
{
|
||||
value: TAB_VARIABLES,
|
||||
label: 'Variables',
|
||||
rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null,
|
||||
},
|
||||
];
|
||||
}, [authTab, folder, headersTab]);
|
||||
}, [authTab, folder, headersTab, numVars]);
|
||||
|
||||
if (folder == null) return null;
|
||||
|
||||
@@ -85,6 +102,38 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
stateKey={`headers.${folder.id}`}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_VARIABLES} className="pt-3 overflow-y-auto h-full px-4">
|
||||
{folderEnvironment == null ? (
|
||||
<EmptyStateText>
|
||||
<VStack alignItems="center" space={1.5}>
|
||||
<p>
|
||||
Override{' '}
|
||||
<Link href="https://feedback.yaak.app/help/articles/3284139-environments-and-variables">
|
||||
Variables
|
||||
</Link>{' '}
|
||||
for requests within this folder.
|
||||
</p>
|
||||
<Button
|
||||
variant="border"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
await createWorkspaceModel({
|
||||
workspaceId: folder.workspaceId,
|
||||
parentModel: 'folder',
|
||||
parentId: folder.id,
|
||||
model: 'environment',
|
||||
name: 'Folder Environment',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create Folder Environment
|
||||
</Button>
|
||||
</VStack>
|
||||
</EmptyStateText>
|
||||
) : (
|
||||
<EnvironmentEditor hideName environment={folderEnvironment} />
|
||||
)}
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
||||
const authConfig = useHttpAuthenticationConfig(
|
||||
model.authenticationType,
|
||||
model.authentication,
|
||||
model.id,
|
||||
model,
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
|
||||
@@ -75,8 +75,8 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
||||
value: TAB_DATA,
|
||||
label: 'Directory Sync',
|
||||
},
|
||||
...authTab,
|
||||
...headersTab,
|
||||
...authTab,
|
||||
]}
|
||||
>
|
||||
<TabContent value={TAB_AUTH} className="overflow-y-auto h-full px-4">
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import { useKeyValue } from '../../hooks/useKeyValue';
|
||||
import type { BannerProps } from './Banner';
|
||||
import { Banner } from './Banner';
|
||||
import { Button } from './Button';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
export function DismissibleBanner({
|
||||
children,
|
||||
className,
|
||||
id,
|
||||
actions,
|
||||
...props
|
||||
}: BannerProps & { id: string }) {
|
||||
}: BannerProps & { id: string; actions?: { label: string; onClick: () => void; color?: Color }[] }) {
|
||||
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
|
||||
namespace: 'global',
|
||||
key: ['dismiss-banner', id],
|
||||
@@ -19,17 +22,34 @@ export function DismissibleBanner({
|
||||
if (dismissed) return null;
|
||||
|
||||
return (
|
||||
<Banner className={classNames(className, 'relative grid grid-cols-[1fr_auto] gap-3')} {...props}>
|
||||
<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"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
<HStack space={1.5}>
|
||||
{actions?.map((a, i) => (
|
||||
<Button
|
||||
key={a.label + i}
|
||||
variant="border"
|
||||
color={a.color ?? props.color}
|
||||
size="xs"
|
||||
onClick={a.onClick}
|
||||
title="Dismiss message"
|
||||
>
|
||||
{a.label}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="border"
|
||||
color={props.color}
|
||||
size="xs"
|
||||
onClick={() => setDismissed((d) => !d)}
|
||||
title="Dismiss message"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</HStack>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -519,7 +519,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
className={classNames(
|
||||
className,
|
||||
'x-theme-menu',
|
||||
'outline-none my-1 pointer-events-auto fixed z-50',
|
||||
'outline-none my-1 pointer-events-auto fixed z-40',
|
||||
)}
|
||||
>
|
||||
{showTriangle && (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import type { EditorKeymap, EnvironmentVariable } from '@yaakapp-internal/models';
|
||||
import type { EditorKeymap } from '@yaakapp-internal/models';
|
||||
import { settingsAtom } from '@yaakapp-internal/models';
|
||||
import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import { parseTemplate } from '@yaakapp-internal/templates';
|
||||
@@ -28,10 +28,12 @@ import {
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { activeEnvironmentIdAtom } from '../../../hooks/useActiveEnvironment';
|
||||
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
|
||||
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
|
||||
import { useRequestEditor } from '../../../hooks/useRequestEditor';
|
||||
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
|
||||
import { showDialog } from '../../../lib/dialog';
|
||||
import { editEnvironment } from '../../../lib/editEnvironment';
|
||||
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
|
||||
import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption';
|
||||
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
||||
@@ -96,7 +98,7 @@ export interface EditorProps {
|
||||
|
||||
const stateFields = { history: historyField, folds: foldState };
|
||||
|
||||
const emptyVariables: EnvironmentVariable[] = [];
|
||||
const emptyVariables: WrappedEnvironmentVariable[] = [];
|
||||
const emptyExtension: Extension = [];
|
||||
|
||||
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
@@ -306,24 +308,9 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
);
|
||||
|
||||
const onClickVariable = useCallback(
|
||||
async (_v: EnvironmentVariable, tagValue: string, startPos: number) => {
|
||||
const initialTokens = parseTemplate(tagValue);
|
||||
showDialog({
|
||||
size: 'dynamic',
|
||||
id: 'template-variable',
|
||||
title: 'Change Variable',
|
||||
render: ({ hide }) => (
|
||||
<TemplateVariableDialog
|
||||
hide={hide}
|
||||
initialTokens={initialTokens}
|
||||
onChange={(insert) => {
|
||||
cm.current?.view.dispatch({
|
||||
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async (v: WrappedEnvironmentVariable, _tagValue: string, _startPos: number) => {
|
||||
editEnvironment(v.environment);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -35,10 +35,10 @@ import {
|
||||
rectangularSelection,
|
||||
} from '@codemirror/view';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
||||
import { graphql } from 'cm6-graphql';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId';
|
||||
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import { renderMarkdown } from '../../../lib/markdown';
|
||||
import { pluralizeCount } from '../../../lib/pluralize';
|
||||
@@ -110,8 +110,8 @@ export function getLanguageExtension({
|
||||
graphQLSchema,
|
||||
}: {
|
||||
useTemplating: boolean;
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||
environmentVariables: WrappedEnvironmentVariable[];
|
||||
onClickVariable: (option: WrappedEnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||
onClickPathParameter: (name: string) => void;
|
||||
completionOptions: TwigCompletionOption[];
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { LanguageSupport } from '@codemirror/language';
|
||||
import { LRLanguage } from '@codemirror/language';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { parseMixed } from '@lezer/common';
|
||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
||||
import type { WrappedEnvironmentVariable } from '../../../../hooks/useEnvironmentVariables';
|
||||
import type { GenericCompletionConfig } from '../genericCompletion';
|
||||
import { genericCompletion } from '../genericCompletion';
|
||||
import { textLanguage } from '../text/extension';
|
||||
@@ -21,10 +21,10 @@ export function twig({
|
||||
extraExtensions,
|
||||
}: {
|
||||
base: LanguageSupport;
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
environmentVariables: WrappedEnvironmentVariable[];
|
||||
completionOptions: TwigCompletionOption[];
|
||||
autocomplete?: GenericCompletionConfig;
|
||||
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||
onClickVariable: (option: WrappedEnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||
onClickPathParameter: (name: string) => void;
|
||||
extraExtensions: Extension[];
|
||||
@@ -33,9 +33,11 @@ export function twig({
|
||||
|
||||
const variableOptions: TwigCompletionOption[] =
|
||||
environmentVariables.map((v) => ({
|
||||
...v,
|
||||
name: v.variable.name,
|
||||
value: v.variable.value,
|
||||
type: 'variable',
|
||||
label: v.name,
|
||||
label: v.variable.name,
|
||||
description: `Inherited from ${v.source}`,
|
||||
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
|
||||
})) ?? [];
|
||||
|
||||
|
||||
@@ -94,13 +94,16 @@ function templateTags(
|
||||
(o) => o.name === name || (o.type === 'function' && o.aliases?.includes(name)),
|
||||
);
|
||||
if (option == null) {
|
||||
const from = node.from; // Cache here so the reference doesn't change
|
||||
option = {
|
||||
invalid: true,
|
||||
type: 'variable',
|
||||
name: inner,
|
||||
value: null,
|
||||
label: inner,
|
||||
onClick: () => onClickMissingVariable(name, rawTag, node.from),
|
||||
onClick: () => {
|
||||
onClickMissingVariable(name, rawTag, from);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function CsvViewer({ response, className }: Props) {
|
||||
const body = useResponseBodyText(response);
|
||||
const body = useResponseBodyText({ response, filter: null });
|
||||
|
||||
const parsed = useMemo(() => {
|
||||
if (body.data == null) return null;
|
||||
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) {
|
||||
const rawTextBody = useResponseBodyText({ responseId: response.id, filter: null });
|
||||
const rawTextBody = useResponseBodyText({ response, filter: null });
|
||||
const contentType = getContentTypeFromHeaders(response.headers);
|
||||
const language = languageFromContentType(contentType, rawTextBody.data ?? '');
|
||||
|
||||
@@ -32,7 +32,7 @@ export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Prop
|
||||
text={rawTextBody.data}
|
||||
pretty={pretty}
|
||||
className={textViewerClassName}
|
||||
responseId={response.id}
|
||||
response={response}
|
||||
requestId={response.requestId}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function JsonViewer({ response, className }: Props) {
|
||||
const rawBody = useResponseBodyText(response);
|
||||
const rawBody = useResponseBodyText({ response, filter: null });
|
||||
|
||||
if (rawBody.isLoading || rawBody.data == null) return null;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function SvgViewer({ response }: Props) {
|
||||
const rawTextBody = useResponseBodyText(response);
|
||||
const rawTextBody = useResponseBodyText({ response, filter: null });
|
||||
const [src, setSrc] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@@ -18,13 +19,13 @@ interface Props {
|
||||
className?: string;
|
||||
text: string;
|
||||
language: EditorProps['language'];
|
||||
responseId: string;
|
||||
response: HttpResponse;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
const useFilterText = createGlobalState<Record<string, string | null>>({});
|
||||
|
||||
export function TextViewer({ language, text, responseId, requestId, pretty, className }: Props) {
|
||||
export function TextViewer({ language, text, response, requestId, pretty, className }: Props) {
|
||||
const [filterTextMap, setFilterTextMap] = useFilterText();
|
||||
const filterText = filterTextMap[requestId] ?? null;
|
||||
const debouncedFilterText = useDebouncedValue(filterText);
|
||||
@@ -36,7 +37,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
||||
);
|
||||
|
||||
const isSearching = filterText != null;
|
||||
const filteredResponse = useResponseBodyText({ responseId, filter: debouncedFilterText ?? null });
|
||||
const filteredResponse = useResponseBodyText({ response, filter: debouncedFilterText ?? null });
|
||||
|
||||
const toggleSearch = useCallback(() => {
|
||||
if (isSearching) {
|
||||
@@ -69,7 +70,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
||||
defaultValue={filterText}
|
||||
onKeyDown={(e) => e.key === 'Escape' && toggleSearch()}
|
||||
onChange={setFilterText}
|
||||
stateKey={`filter.${responseId}`}
|
||||
stateKey={`filter.${response.id}`}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
@@ -96,7 +97,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
||||
isSearching,
|
||||
language,
|
||||
requestId,
|
||||
responseId,
|
||||
response,
|
||||
setFilterText,
|
||||
toggleSearch,
|
||||
]);
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
|
||||
export function WebPageViewer({ response }: Props) {
|
||||
const { url } = response;
|
||||
const body = useResponseBodyText(response).data ?? '';
|
||||
const body = useResponseBodyText({ response, filter: null }).data ?? '';
|
||||
|
||||
const contentForIframe: string | undefined = useMemo(() => {
|
||||
if (body.includes('<head>')) {
|
||||
|
||||
@@ -37,11 +37,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
const items = useMemo((): DropdownItem[] => {
|
||||
if (child.model === 'folder') {
|
||||
return [
|
||||
{
|
||||
label: 'Send All',
|
||||
leftSlot: <Icon icon="send_horizontal" />,
|
||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)),
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
@@ -52,6 +47,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
onSelect: () => duplicateModelById(child.model, child.id),
|
||||
},
|
||||
{
|
||||
label: 'Send All',
|
||||
leftSlot: <Icon icon="send_horizontal" />,
|
||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)),
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
color: 'danger',
|
||||
|
||||
Reference in New Issue
Block a user