Extract base environment (#149)

This commit is contained in:
Gregory Schier
2024-12-21 05:44:55 -08:00
committed by GitHub
parent ecabe9b6ef
commit dd8ccfe21f
28 changed files with 425 additions and 387 deletions

View File

@@ -59,7 +59,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment();
const httpRequestActions = useHttpRequestActions();
const workspaces = useWorkspaces();
const environments = useEnvironments();
const { subEnvironments } = useEnvironments();
const recentEnvironments = useRecentEnvironments();
const recentWorkspaces = useRecentWorkspaces();
const requests = useRequests();
@@ -211,7 +211,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
}, [recentRequests, requests]);
const sortedEnvironments = useMemo(() => {
return [...environments].sort((a, b) => {
return [...subEnvironments].sort((a, b) => {
const aRecentIndex = recentEnvironments.indexOf(a.id);
const bRecentIndex = recentEnvironments.indexOf(b.id);
@@ -225,7 +225,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
return a.createdAt.localeCompare(b.createdAt);
}
});
}, [environments, recentEnvironments]);
}, [subEnvironments, recentEnvironments]);
const sortedWorkspaces = useMemo(() => {
return [...workspaces].sort((a, b) => {

View File

@@ -1,7 +1,6 @@
import classNames from 'classnames';
import { memo, useCallback, useMemo } from 'react';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useEnvironments } from '../hooks/useEnvironments';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
@@ -19,8 +18,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
className,
...buttonProps
}: Props) {
const environments = useEnvironments();
const activeWorkspace = useActiveWorkspace();
const { subEnvironments, baseEnvironment } = useEnvironments();
const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment();
const dialog = useDialog();
@@ -36,7 +34,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
const items: DropdownItem[] = useMemo(
() => [
...environments.map(
...subEnvironments.map(
(e) => ({
key: e.id,
label: e.name,
@@ -51,7 +49,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
}),
[activeEnvironment?.id],
),
...((environments.length > 0
...((subEnvironments.length > 0
? [{ type: 'separator', label: 'Environments' }]
: []) as DropdownItem[]),
{
@@ -62,11 +60,11 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
onSelect: showEnvironmentDialog,
},
],
[activeEnvironment?.id, environments, setActiveEnvironmentId, showEnvironmentDialog],
[activeEnvironment?.id, subEnvironments, setActiveEnvironmentId, showEnvironmentDialog],
);
const hasWorkspaceVars =
(activeWorkspace?.variables ?? []).filter((v) => v.enabled && (v.name || v.value)).length > 0;
const hasBaseVars =
(baseEnvironment?.variables ?? []).filter((v) => v.enabled && (v.name || v.value)).length > 0;
return (
<Dropdown items={items}>
@@ -75,14 +73,14 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
className={classNames(
className,
'text !px-2 truncate',
!activeEnvironment && !hasWorkspaceVars && 'text-text-subtlest italic',
!activeEnvironment && !hasBaseVars && 'text-text-subtlest italic',
)}
// 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={environments.length === 0 ? showEnvironmentDialog : undefined}
onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined}
{...buttonProps}
>
{activeEnvironment?.name ?? (hasWorkspaceVars ? 'Environment' : 'No Environment')}
{activeEnvironment?.name ?? (hasBaseVars ? 'Environment' : 'No Environment')}
</Button>
</Dropdown>
);

View File

@@ -1,15 +1,14 @@
import type { Environment } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
import { useEnvironments } from '../hooks/useEnvironments';
import { useKeyValue } from '../hooks/useKeyValue';
import { usePrompt } from '../hooks/usePrompt';
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
import type { Environment, Workspace } from '@yaakapp-internal/models';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { ContextMenu } from './core/Dropdown';
import type {
@@ -31,18 +30,15 @@ interface Props {
}
export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
initialEnvironment?.id ?? null,
);
const environments = useEnvironments();
const createEnvironment = useCreateEnvironment();
const activeWorkspace = useActiveWorkspace();
const { baseEnvironment, subEnvironments, allEnvironments } = useEnvironments();
const selectedEnvironment = useMemo(
() => environments.find((e) => e.id === selectedEnvironmentId) ?? null,
[environments, selectedEnvironmentId],
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
initialEnvironment?.id ?? baseEnvironment?.id ?? null,
);
const selectedEnvironment = allEnvironments.find((e) => e.id === selectedEnvironmentId);
const handleCreateEnvironment = async () => {
const e = await createEnvironment.mutateAsync();
if (e == null) return;
@@ -59,8 +55,8 @@ 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">
<SidebarButton
active={selectedEnvironment?.id == null}
onClick={() => setSelectedEnvironmentId(null)}
active={selectedEnvironment?.id == baseEnvironment?.id}
onClick={() => setSelectedEnvironmentId(baseEnvironment?.id ?? null)}
environment={null}
rightSlot={
<IconButton
@@ -75,14 +71,14 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
/>
}
>
Global Variables
{baseEnvironment?.name}
</SidebarButton>
{environments.length > 0 && (
{subEnvironments.length > 0 && (
<div className="px-2">
<Separator className="my-3"></Separator>
</div>
)}
{environments.map((e) => (
{subEnvironments.map((e) => (
<SidebarButton
key={e.id}
active={selectedEnvironment?.id === e.id}
@@ -96,11 +92,20 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
</aside>
)}
secondSlot={() =>
activeWorkspace != null && (
selectedEnvironmentId == null ? (
<div className="p-3 mt-10">
<Banner color="danger">No selected environment</Banner>
</div>
) : selectedEnvironment == null ? (
<div className="p-3 mt-10">
<Banner color="danger">
Failed to find selected environment <InlineCode>{selectedEnvironmentId}</InlineCode>
</Banner>
</div>
) : (
<EnvironmentEditor
className="pt-2 border-l border-border-subtle"
environment={selectedEnvironment}
workspace={activeWorkspace}
/>
)
}
@@ -110,11 +115,9 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
const EnvironmentEditor = function ({
environment,
workspace,
className,
}: {
environment: Environment | null;
workspace: Workspace;
environment: Environment;
className?: string;
}) {
const valueVisibility = useKeyValue<boolean>({
@@ -122,37 +125,25 @@ const EnvironmentEditor = function ({
key: 'environmentValueVisibility',
fallback: true,
});
const environments = useEnvironments();
const { subEnvironments } = useEnvironments();
const updateEnvironment = useUpdateEnvironment(environment?.id ?? null);
const updateWorkspace = useUpdateWorkspace(workspace.id);
const variables = environment == null ? workspace.variables : environment.variables;
const handleChange = useCallback<PairEditorProps['onChange']>(
(variables) => {
if (environment != null) {
updateEnvironment.mutate({ variables });
} else {
updateWorkspace.mutate({ variables });
}
},
[updateWorkspace, updateEnvironment, environment],
(variables) => updateEnvironment.mutate({ variables }),
[updateEnvironment],
);
// Gather a list of env names from other environments, to help the user get them aligned
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
const otherEnvironments = environments.filter((e) => e.id !== environment?.id);
const allVariableNames =
environment == null
? [
// Nothing to autocomplete if we're in the base environment
]
: [
...workspace.variables.map((v) => v.name),
...otherEnvironments.flatMap((e) => e.variables.map((v) => v.name)),
];
? [] // Nothing to autocomplete if we're in the base environment
: subEnvironments
.filter((e) => e.environmentId != null)
.flatMap((e) => e.variables.map((v) => v.name));
// Filter out empty strings and variables that already exist
const variableNames = allVariableNames.filter(
(name) => name != '' && !variables.find((v) => v.name === name),
(name) => name != '' && !environment.variables.find((v) => v.name === name),
);
const uniqueVariableNames = [...new Set(variableNames)];
const options = uniqueVariableNames.map(
@@ -162,7 +153,7 @@ const EnvironmentEditor = function ({
}),
);
return { options };
}, [environments, variables, workspace, environment]);
}, [subEnvironments, environment]);
const validateName = useCallback((name: string) => {
// Empty just means the variable doesn't have a name yet, and is unusable
@@ -174,7 +165,7 @@ const EnvironmentEditor = function ({
<VStack space={4} className={classNames(className, 'pl-4')}>
<HStack space={2} className="justify-between">
<Heading className="w-full flex items-center gap-1">
<div>{environment?.name ?? 'Global Variables'}</div>
<div>{environment?.name}</div>
<IconButton
iconClassName="text-text-subtlest"
size="sm"
@@ -195,8 +186,8 @@ const EnvironmentEditor = function ({
nameValidate={validateName}
valueType={valueVisibility.value ? 'text' : 'password'}
valueAutocompleteVariables={true}
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
pairs={variables}
forceUpdateKey={environment.id}
pairs={environment.variables}
onChange={handleChange}
/>
</div>

View File

@@ -28,7 +28,7 @@ export function FolderSettingsDialog({ folderId }: Props) {
<MarkdownEditor
name="folder-description"
placeholder="A Markdown description of this folder."
placeholder="Folder description"
className="min-h-[10rem] border border-border px-2"
defaultValue={folder.description}
onChange={(description) => {

View File

@@ -344,7 +344,7 @@ export function GrpcConnectionSetupPane({
<TabContent value={TAB_DESCRIPTION}>
<MarkdownEditor
name="request-description"
placeholder="A Markdown description of this request."
placeholder="Request description"
defaultValue={activeRequest.description}
onChange={handleDescriptionChange}
/>

View File

@@ -461,7 +461,7 @@ export const RequestPane = memo(function RequestPane({
/>
<MarkdownEditor
name="request-description"
placeholder="A Markdown description of this request."
placeholder="Request description"
defaultValue={activeRequest.description}
onChange={(description) =>
updateRequest.mutate({ id: activeRequestId, update: { description } })

View File

@@ -25,7 +25,7 @@ export function WorkspaceSettingsDialog({ workspaceId }: Props) {
<MarkdownEditor
name="workspace-description"
placeholder="A Markdown description of this workspace."
placeholder="Workspace description"
className="min-h-[10rem] border border-border px-2"
defaultValue={workspace.description}
onChange={(description) => updateWorkspace.mutate({ description })}