mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-19 23:31:21 +02:00
Environment dropdown and actions
This commit is contained in:
@@ -378,15 +378,15 @@
|
|||||||
},
|
},
|
||||||
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n "
|
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n "
|
||||||
},
|
},
|
||||||
"80bc37d283b67a70919c7b03a106fe563829741fb2c2fbd34ae4d8f581ecb697": {
|
"6f12b56113b09966b472431b6cb95c354bea51b4dfb22a96517655c0fca0ab05": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"nullable": [],
|
"nullable": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 2
|
"Right": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"query": "\n UPDATE environments\n SET (data, updated_at) = (?, CURRENT_TIMESTAMP)\n WHERE id = ?;\n "
|
"query": "\n UPDATE environments\n SET (name, data, updated_at) = (?, ?, CURRENT_TIMESTAMP)\n WHERE id = ?;\n "
|
||||||
},
|
},
|
||||||
"84be2b954870ab181738656ecd4d03fca2ff21012947014c79626abfce8e999b": {
|
"84be2b954870ab181738656ecd4d03fca2ff21012947014c79626abfce8e999b": {
|
||||||
"describe": {
|
"describe": {
|
||||||
|
|||||||
@@ -425,10 +425,14 @@ async fn update_environment(
|
|||||||
) -> Result<models::Environment, String> {
|
) -> Result<models::Environment, String> {
|
||||||
let pool = &*db_instance.lock().await;
|
let pool = &*db_instance.lock().await;
|
||||||
|
|
||||||
let updated_environment =
|
let updated_environment = models::update_environment(
|
||||||
models::update_environment(environment.id.as_str(), environment.data.0, pool)
|
environment.id.as_str(),
|
||||||
.await
|
environment.name.as_str(),
|
||||||
.expect("Failed to update request");
|
environment.data.0,
|
||||||
|
pool,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to update request");
|
||||||
|
|
||||||
emit_and_return(&window, "updated_model", updated_environment)
|
emit_and_return(&window, "updated_model", updated_environment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ pub async fn create_environment(
|
|||||||
|
|
||||||
pub async fn update_environment(
|
pub async fn update_environment(
|
||||||
id: &str,
|
id: &str,
|
||||||
|
name: &str,
|
||||||
data: HashMap<String, JsonValue>,
|
data: HashMap<String, JsonValue>,
|
||||||
pool: &Pool<Sqlite>,
|
pool: &Pool<Sqlite>,
|
||||||
) -> Result<Environment, sqlx::Error> {
|
) -> Result<Environment, sqlx::Error> {
|
||||||
@@ -262,9 +263,10 @@ pub async fn update_environment(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE environments
|
UPDATE environments
|
||||||
SET (data, updated_at) = (?, CURRENT_TIMESTAMP)
|
SET (name, data, updated_at) = (?, ?, CURRENT_TIMESTAMP)
|
||||||
WHERE id = ?;
|
WHERE id = ?;
|
||||||
"#,
|
"#,
|
||||||
|
name,
|
||||||
json_data,
|
json_data,
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
|
|||||||
129
src-web/components/EnvironmentActionsDropdown.tsx
Normal file
129
src-web/components/EnvironmentActionsDropdown.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import classnames from 'classnames';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { Button } from './core/Button';
|
||||||
|
import type { DropdownItem } from './core/Dropdown';
|
||||||
|
import { Dropdown } from './core/Dropdown';
|
||||||
|
import { Icon } from './core/Icon';
|
||||||
|
import { InlineCode } from './core/InlineCode';
|
||||||
|
import { useEnvironments } from '../hooks/useEnvironments';
|
||||||
|
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||||
|
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
|
||||||
|
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||||
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
|
import { useDialog } from './DialogContext';
|
||||||
|
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdown({
|
||||||
|
className,
|
||||||
|
}: Props) {
|
||||||
|
const environments = useEnvironments();
|
||||||
|
const [activeEnvironment, setActiveEnvironment] = useActiveEnvironment();
|
||||||
|
const updateEnvironment = useUpdateEnvironment(activeEnvironment?.id ?? null);
|
||||||
|
const createEnvironment = useCreateEnvironment();
|
||||||
|
const prompt = usePrompt();
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
const items: DropdownItem[] = useMemo(() => {
|
||||||
|
const environmentItems = environments.map(
|
||||||
|
(e) => ({
|
||||||
|
key: e.id,
|
||||||
|
label: e.name,
|
||||||
|
onSelect: async () => {
|
||||||
|
setActiveEnvironment(e);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const activeEnvironmentItems: DropdownItem[] =
|
||||||
|
environments.length <= 1
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
...environmentItems,
|
||||||
|
{
|
||||||
|
type: 'separator',
|
||||||
|
label: activeEnvironment?.name,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...activeEnvironmentItems,
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
leftSlot: <Icon icon="sun" />,
|
||||||
|
onSelect: async () => {
|
||||||
|
dialog.show({
|
||||||
|
title: 'Environments',
|
||||||
|
render: () => <EnvironmentEditDialog />,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rename',
|
||||||
|
label: 'Rename',
|
||||||
|
leftSlot: <Icon icon="pencil" />,
|
||||||
|
onSelect: async () => {
|
||||||
|
const name = await prompt({
|
||||||
|
title: 'Rename Environment',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Enter a new name for <InlineCode>{activeEnvironment?.name}</InlineCode>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
defaultValue: activeEnvironment?.name,
|
||||||
|
});
|
||||||
|
updateEnvironment.mutate({ name });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// key: 'delete',
|
||||||
|
// label: 'Delete',
|
||||||
|
// leftSlot: <Icon icon="trash" />,
|
||||||
|
// onSelect: deleteEnv.mutate,
|
||||||
|
// variant: 'danger',
|
||||||
|
// },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
key: 'create-environment',
|
||||||
|
label: 'Create Environment',
|
||||||
|
leftSlot: <Icon icon="plus" />,
|
||||||
|
onSelect: async () => {
|
||||||
|
const name = await prompt({
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
defaultValue: '',
|
||||||
|
description: 'Enter a name for the new environment',
|
||||||
|
title: 'Create Environment',
|
||||||
|
});
|
||||||
|
createEnvironment.mutate({ name });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [
|
||||||
|
environments,
|
||||||
|
activeEnvironment?.name,
|
||||||
|
// deleteEnvironment.mutate,
|
||||||
|
dialog,
|
||||||
|
prompt,
|
||||||
|
updateEnvironment,
|
||||||
|
createEnvironment,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown items={items}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={classnames(className, 'text-gray-800 !px-2 truncate')}
|
||||||
|
forDropdown
|
||||||
|
>
|
||||||
|
{activeEnvironment?.name ?? 'No Env'}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||||
import { useEnvironments } from '../hooks/useEnvironments';
|
import { useEnvironments } from '../hooks/useEnvironments';
|
||||||
import { usePrompt } from '../hooks/usePrompt';
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
@@ -7,19 +6,24 @@ import type { Environment } from '../lib/models';
|
|||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { Editor } from './core/Editor';
|
import { Editor } from './core/Editor';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||||
|
|
||||||
export const EnvironmentEditDialog = function () {
|
export const EnvironmentEditDialog = function() {
|
||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
const environments = useEnvironments();
|
const environments = useEnvironments();
|
||||||
const createEnvironment = useCreateEnvironment();
|
const createEnvironment = useCreateEnvironment();
|
||||||
const [activeEnvironment, setActiveEnvironment] = useState<Environment | null>(null);
|
const [activeEnvironment, setActiveEnvironment] = useActiveEnvironment();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid gap-3 grid-cols-[auto_minmax(0,1fr)]">
|
<div className="h-full grid gap-3 grid-cols-[auto_minmax(0,1fr)]">
|
||||||
<aside className="h-full min-w-[120px] pr-3 border-r border-gray-200">
|
<aside className="relative h-full min-w-[200px] pr-3 border-r border-gray-200">
|
||||||
{environments.map((e) => (
|
{environments.map((e) => (
|
||||||
<Button
|
<Button
|
||||||
className={classnames('w-full', activeEnvironment?.id === e.id && 'bg-highlight')}
|
size="sm"
|
||||||
|
className={classnames(
|
||||||
|
'w-full',
|
||||||
|
activeEnvironment?.id === e.id && 'bg-gray-100 text-gray-1000',
|
||||||
|
)}
|
||||||
justify="start"
|
justify="start"
|
||||||
key={e.id}
|
key={e.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -30,6 +34,8 @@ export const EnvironmentEditDialog = function () {
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="mr-5 absolute bottom-0 left-0 right-0"
|
||||||
color="gray"
|
color="gray"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const name = await prompt({
|
const name = await prompt({
|
||||||
@@ -41,7 +47,7 @@ export const EnvironmentEditDialog = function () {
|
|||||||
createEnvironment.mutate({ name });
|
createEnvironment.mutate({ name });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create Environment
|
New Environment
|
||||||
</Button>
|
</Button>
|
||||||
</aside>
|
</aside>
|
||||||
{activeEnvironment != null && <EnvironmentEditor environment={activeEnvironment} />}
|
{activeEnvironment != null && <EnvironmentEditor environment={activeEnvironment} />}
|
||||||
@@ -49,7 +55,7 @@ export const EnvironmentEditDialog = function () {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnvironmentEditor = function ({ environment }: { environment: Environment }) {
|
const EnvironmentEditor = function({ environment }: { environment: Environment }) {
|
||||||
const updateEnvironment = useUpdateEnvironment(environment.id);
|
const updateEnvironment = useUpdateEnvironment(environment.id);
|
||||||
return (
|
return (
|
||||||
<Editor
|
<Editor
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ className }: Props) {
|
export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ className }: Props) {
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
|||||||
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
||||||
import { SidebarActions } from './SidebarActions';
|
import { SidebarActions } from './SidebarActions';
|
||||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||||
import { Button } from './core/Button';
|
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||||
import { useDialog } from './DialogContext';
|
|
||||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -17,7 +15,6 @@ interface Props {
|
|||||||
|
|
||||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||||
const activeRequest = useActiveRequest();
|
const activeRequest = useActiveRequest();
|
||||||
const dialog = useDialog();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
@@ -28,15 +25,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
|||||||
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
|
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
|
||||||
<SidebarActions />
|
<SidebarActions />
|
||||||
<WorkspaceActionsDropdown className="pointer-events-auto" />
|
<WorkspaceActionsDropdown className="pointer-events-auto" />
|
||||||
<Button onClick={() => {
|
<EnvironmentActionsDropdown className="pointer-events-auto" />
|
||||||
dialog.show({
|
|
||||||
title: 'Environments',
|
|
||||||
size: 'full',
|
|
||||||
render: () => <EnvironmentEditDialog />,
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
Environments
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
<div className="pointer-events-none">
|
<div className="pointer-events-none">
|
||||||
<RecentRequestsDropdown />
|
<RecentRequestsDropdown />
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export function Dialog({
|
|||||||
animate={{ top: 0, scale: 1 }}
|
animate={{ top: 0, scale: 1 }}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
className,
|
className,
|
||||||
|
'h-full gap-2 grid grid-rows-[auto_minmax(0,1fr)]',
|
||||||
'relative bg-gray-50 pointer-events-auto',
|
'relative bg-gray-50 pointer-events-auto',
|
||||||
'p-5 rounded-lg overflow-auto',
|
'p-5 rounded-lg overflow-auto',
|
||||||
'dark:border border-highlight shadow shadow-black/10',
|
'dark:border border-highlight shadow shadow-black/10',
|
||||||
@@ -66,8 +67,7 @@ export function Dialog({
|
|||||||
{title}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
{description && <p id={descriptionId}>{description}</p>}
|
{description && <p id={descriptionId}>{description}</p>}
|
||||||
<div className="h-full w-full mt-4">{children}</div>
|
<div className="h-full w-full">{children}</div>
|
||||||
|
|
||||||
{/*Put close at the end so that it's the last thing to be tabbed to*/}
|
{/*Put close at the end so that it's the last thing to be tabbed to*/}
|
||||||
{!hideX && (
|
{!hideX && (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
19
src-web/hooks/useActiveEnvironment.ts
Normal file
19
src-web/hooks/useActiveEnvironment.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import type { Environment } from '../lib/models';
|
||||||
|
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||||
|
import { useEnvironments } from './useEnvironments';
|
||||||
|
|
||||||
|
export function useActiveEnvironment(): [Environment | null, (environment: Environment) => void] {
|
||||||
|
const [id, setId] = useActiveEnvironmentId();
|
||||||
|
const environments = useEnvironments();
|
||||||
|
const environment = useMemo(
|
||||||
|
() => environments.find((w) => w.id === id) ?? null,
|
||||||
|
[environments, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setActiveEnvironment = useCallback((e: Environment) => {
|
||||||
|
setId(e.id)
|
||||||
|
}, [setId]);
|
||||||
|
|
||||||
|
return [environment, setActiveEnvironment];
|
||||||
|
}
|
||||||
14
src-web/hooks/useActiveEnvironmentId.ts
Normal file
14
src-web/hooks/useActiveEnvironmentId.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function useActiveEnvironmentId(): [string | null, (id: string) => void] {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const id = searchParams.get('environmentId') ?? null;
|
||||||
|
|
||||||
|
const setId = useCallback((id: string) => {
|
||||||
|
searchParams.set('environmentId', id)
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}, [searchParams, setSearchParams])
|
||||||
|
|
||||||
|
return [id, setId];
|
||||||
|
}
|
||||||
@@ -3,20 +3,23 @@ import { invoke } from '@tauri-apps/api';
|
|||||||
import type { Environment } from '../lib/models';
|
import type { Environment } from '../lib/models';
|
||||||
import { environmentsQueryKey } from './useEnvironments';
|
import { environmentsQueryKey } from './useEnvironments';
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
|
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||||
|
|
||||||
export function useCreateEnvironment() {
|
export function useCreateEnvironment() {
|
||||||
const workspaceId = useActiveWorkspaceId();
|
const workspaceId = useActiveWorkspaceId();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [, setActiveEnvironmentId ] = useActiveEnvironmentId();
|
||||||
return useMutation<Environment, unknown, Pick<Environment, 'name'>>({
|
return useMutation<Environment, unknown, Pick<Environment, 'name'>>({
|
||||||
mutationFn: (patch) => {
|
mutationFn: (patch) => {
|
||||||
return invoke('create_environment', { ...patch, workspaceId });
|
return invoke('create_environment', { ...patch, workspaceId });
|
||||||
},
|
},
|
||||||
onSuccess: async (environment) => {
|
onSuccess: async (environment) => {
|
||||||
if (workspaceId == null) return;
|
if (workspaceId == null) return;
|
||||||
queryClient.setQueryData<Environment[]>(environmentsQueryKey({ workspaceId }), (environments) => [
|
setActiveEnvironmentId(environment.id);
|
||||||
...(environments ?? []),
|
queryClient.setQueryData<Environment[]>(
|
||||||
environment,
|
environmentsQueryKey({ workspaceId }),
|
||||||
]);
|
(environments) => [...(environments ?? []), environment],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user