Some fixes around environments

This commit is contained in:
Gregory Schier
2024-12-21 11:04:49 -08:00
parent c1d5881167
commit 61d094d9fd
20 changed files with 95 additions and 46 deletions

View File

@@ -1220,6 +1220,7 @@ async fn cmd_create_cookie_jar(
#[tauri::command] #[tauri::command]
async fn cmd_create_environment( async fn cmd_create_environment(
workspace_id: &str, workspace_id: &str,
environment_id: Option<&str>,
name: &str, name: &str,
variables: Vec<EnvironmentVariable>, variables: Vec<EnvironmentVariable>,
w: WebviewWindow, w: WebviewWindow,
@@ -1228,6 +1229,7 @@ async fn cmd_create_environment(
&w, &w,
Environment { Environment {
workspace_id: workspace_id.to_string(), workspace_id: workspace_id.to_string(),
environment_id: environment_id.map(|s| s.to_string()),
name: name.to_string(), name: name.to_string(),
variables, variables,
..Default::default() ..Default::default()

View File

@@ -7233,12 +7233,14 @@ function pluginHookImport(ctx, contents) {
}; };
const workspacesToImport = parsed.resources.filter(isWorkspace); const workspacesToImport = parsed.resources.filter(isWorkspace);
for (const workspaceToImport of workspacesToImport) { for (const workspaceToImport of workspacesToImport) {
console.log("IMPORT WORKSPACE", workspaceToImport);
resources.workspaces.push({ resources.workspaces.push({
id: convertId(workspaceToImport._id), id: convertId(workspaceToImport._id),
createdAt: new Date(workspacesToImport.created ?? Date.now()).toISOString().replace("Z", ""), createdAt: new Date(workspacesToImport.created ?? Date.now()).toISOString().replace("Z", ""),
updatedAt: new Date(workspacesToImport.updated ?? Date.now()).toISOString().replace("Z", ""), updatedAt: new Date(workspacesToImport.updated ?? Date.now()).toISOString().replace("Z", ""),
model: "workspace", model: "workspace",
name: workspaceToImport.name name: workspaceToImport.name,
description: workspacesToImport.description
}); });
const environmentsToImport = parsed.resources.filter( const environmentsToImport = parsed.resources.filter(
(r) => isEnvironment(r) (r) => isEnvironment(r)
@@ -7294,6 +7296,7 @@ function importFolder(f, workspaceId) {
updatedAt: new Date(f.updated ?? Date.now()).toISOString().replace("Z", ""), updatedAt: new Date(f.updated ?? Date.now()).toISOString().replace("Z", ""),
folderId: f.parentId === workspaceId ? null : convertId(f.parentId), folderId: f.parentId === workspaceId ? null : convertId(f.parentId),
workspaceId: convertId(workspaceId), workspaceId: convertId(workspaceId),
description: f.description ?? null,
model: "folder", model: "folder",
name: f.name name: f.name
}; };
@@ -7311,6 +7314,7 @@ function importGrpcRequest(r, workspaceId, sortPriority = 0) {
model: "grpc_request", model: "grpc_request",
sortPriority, sortPriority,
name: r.name, name: r.name,
description: r.description ?? null,
url: convertSyntax(r.url), url: convertSyntax(r.url),
service, service,
method, method,
@@ -7377,6 +7381,7 @@ function importHttpRequest(r, workspaceId, sortPriority = 0) {
model: "http_request", model: "http_request",
sortPriority, sortPriority,
name: r.name, name: r.name,
description: r.description ?? null,
url: convertSyntax(r.url), url: convertSyntax(r.url),
body, body,
bodyType, bodyType,

View File

@@ -145931,6 +145931,7 @@ function pluginHookImport(_ctx, contents) {
workspaceId: workspace.id, workspaceId: workspace.id,
folderId, folderId,
name: v.name, name: v.name,
description: v.description,
method: r.method || "GET", method: r.method || "GET",
url, url,
urlParameters, urlParameters,

View File

@@ -98,6 +98,7 @@ function pluginHookImport(_ctx, contents) {
workspaceId: workspace.id, workspaceId: workspace.id,
folderId, folderId,
name: v.name, name: v.name,
description: v.description,
method: r.method || "GET", method: r.method || "GET",
url, url,
urlParameters, urlParameters,

View File

@@ -152,6 +152,7 @@ pub async fn list_workspaces<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Wo
let (sql, params) = Query::select() let (sql, params) = Query::select()
.from(WorkspaceIden::Table) .from(WorkspaceIden::Table)
.column(Asterisk) .column(Asterisk)
.order_by(WorkspaceIden::Name, Order::Asc)
.build_rusqlite(SqliteQueryBuilder); .build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?; let mut stmt = db.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?; let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
@@ -746,7 +747,7 @@ pub async fn list_environments<R: Runtime>(
.from(EnvironmentIden::Table) .from(EnvironmentIden::Table)
.cond_where(Expr::col(EnvironmentIden::WorkspaceId).eq(workspace_id)) .cond_where(Expr::col(EnvironmentIden::WorkspaceId).eq(workspace_id))
.column(Asterisk) .column(Asterisk)
.order_by(EnvironmentIden::CreatedAt, Order::Desc) .order_by(EnvironmentIden::Name, Order::Asc)
.build_rusqlite(SqliteQueryBuilder); .build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?; let mut stmt = db.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?; let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
@@ -757,6 +758,7 @@ pub async fn list_environments<R: Runtime>(
environments.iter().find(|e| e.environment_id == None && e.workspace_id == workspace_id); environments.iter().find(|e| e.environment_id == None && e.workspace_id == workspace_id);
if let None = base_environment { if let None = base_environment {
info!("Creating base environment for {workspace_id}");
let base_environment = upsert_environment( let base_environment = upsert_environment(
window, window,
Environment { Environment {
@@ -766,7 +768,6 @@ pub async fn list_environments<R: Runtime>(
}, },
) )
.await?; .await?;
info!("Created base environment for {workspace_id}");
environments.push(base_environment); environments.push(base_environment);
} }

View File

@@ -41,9 +41,9 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
leftSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : <Icon icon="empty" />, leftSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: async () => { onSelect: async () => {
if (e.id !== activeEnvironment?.id) { if (e.id !== activeEnvironment?.id) {
setActiveEnvironmentId(e.id); await setActiveEnvironmentId(e.id);
} else { } else {
setActiveEnvironmentId(null); await setActiveEnvironmentId(null);
} }
}, },
}), }),

View File

@@ -40,7 +40,8 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
const selectedEnvironment = allEnvironments.find((e) => e.id === selectedEnvironmentId); const selectedEnvironment = allEnvironments.find((e) => e.id === selectedEnvironmentId);
const handleCreateEnvironment = async () => { const handleCreateEnvironment = async () => {
const e = await createEnvironment.mutateAsync(); if (baseEnvironment == null) return;
const e = await createEnvironment.mutateAsync(baseEnvironment);
if (e == null) return; if (e == null) return;
setSelectedEnvironmentId(e.id); setSelectedEnvironmentId(e.id);
}; };

View File

@@ -1,7 +1,9 @@
import { save } from '@tauri-apps/plugin-dialog'; import { save } from '@tauri-apps/plugin-dialog';
import type { Workspace } from '@yaakapp-internal/models';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import slugify from 'slugify'; import slugify from 'slugify';
import type { Workspace } from '@yaakapp-internal/models'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { count } from '../lib/pluralize'; import { count } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { Button } from './core/Button'; import { Button } from './core/Button';
@@ -11,16 +13,32 @@ import { HStack, VStack } from './core/Stacks';
interface Props { interface Props {
onHide: () => void; onHide: () => void;
onSuccess: (path: string) => void; onSuccess: (path: string) => void;
activeWorkspace: Workspace;
workspaces: Workspace[];
} }
export function ExportDataDialog({ export function ExportDataDialog({ onHide, onSuccess }: Props) {
const allWorkspaces = useWorkspaces();
const activeWorkspace = useActiveWorkspace();
if (activeWorkspace == null || allWorkspaces.length === 0) return null;
return (
<ExportDataDialogContent
onHide={onHide}
onSuccess={onSuccess}
allWorkspaces={allWorkspaces}
activeWorkspace={activeWorkspace}
/>
);
}
function ExportDataDialogContent({
onHide, onHide,
onSuccess, onSuccess,
activeWorkspace, activeWorkspace,
workspaces: allWorkspaces, allWorkspaces,
}: Props) { }: Props & {
allWorkspaces: Workspace[];
activeWorkspace: Workspace;
}) {
const [selectedWorkspaces, setSelectedWorkspaces] = useState<Record<string, boolean>>({ const [selectedWorkspaces, setSelectedWorkspaces] = useState<Record<string, boolean>>({
[activeWorkspace.id]: true, [activeWorkspace.id]: true,
}); });

View File

@@ -17,6 +17,7 @@ export function WorkspaceSettingsDialog({ workspaceId }: Props) {
return ( return (
<VStack space={3} className="pb-3 max-h-[50vh]"> <VStack space={3} className="pb-3 max-h-[50vh]">
{workspace.id}
<PlainInput <PlainInput
label="Workspace Name" label="Workspace Name"
defaultValue={workspace.name} defaultValue={workspace.name}

View File

@@ -35,11 +35,16 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null; const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
const deleteSendHistory = useDeleteSendHistory(); const deleteSendHistory = useDeleteSendHistory();
const orderedWorkspaces = useMemo(
() => [...workspaces].sort((a, b) => (a.name.localeCompare(b.name) > 0 ? 1 : -1)),
[workspaces],
);
const { workspaceItems, extraItems } = useMemo<{ const { workspaceItems, extraItems } = useMemo<{
workspaceItems: RadioDropdownItem[]; workspaceItems: RadioDropdownItem[];
extraItems: DropdownItem[]; extraItems: DropdownItem[];
}>(() => { }>(() => {
const workspaceItems: RadioDropdownItem[] = workspaces.map((w) => ({ const workspaceItems: RadioDropdownItem[] = orderedWorkspaces.map((w) => ({
key: w.id, key: w.id,
label: w.name, label: w.name,
value: w.id, value: w.id,
@@ -84,13 +89,13 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
return { workspaceItems, extraItems }; return { workspaceItems, extraItems };
}, [ }, [
activeWorkspace, activeWorkspace?.id,
activeWorkspaceId, activeWorkspaceId,
createWorkspace.mutate, createWorkspace.mutate,
deleteSendHistory.mutate, deleteSendHistory.mutate,
deleteWorkspace.mutate, deleteWorkspace.mutate,
dialog, dialog,
workspaces, orderedWorkspaces,
]); ]);
const handleChange = useCallback( const handleChange = useCallback(

View File

@@ -1,4 +1,4 @@
import { getRouteApi, useSearch } from '@tanstack/react-router'; import { useNavigate, useSearch } from '@tanstack/react-router';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useCookieJars } from './useCookieJars'; import { useCookieJars } from './useCookieJars';
@@ -38,17 +38,15 @@ export function useEnsureActiveCookieJar() {
}, [activeCookieJarId, cookieJars, setActiveCookieJarId]); }, [activeCookieJarId, cookieJars, setActiveCookieJarId]);
} }
const routeApi = getRouteApi('/workspaces/$workspaceId/');
function useActiveCookieJarId() { function useActiveCookieJarId() {
// NOTE: This query param is accessed from Rust side, so do not change // NOTE: This query param is accessed from Rust side, so do not change
const { cookieJarId: id } = useSearch({ strict: false }); const { cookie_jar_id: id } = useSearch({ strict: false });
const navigate = routeApi.useNavigate(); const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
const setId = useCallback( const setId = useCallback(
(id: string) => (id: string) =>
navigate({ navigate({
search: (prev) => ({ ...prev, cookieJarId: id }), search: (prev) => ({ ...prev, cookie_jar_id: id }),
}), }),
[navigate], [navigate],
); );

View File

@@ -1,4 +1,4 @@
import { getRouteApi, useSearch } from '@tanstack/react-router'; import { useNavigate, useSearch } from '@tanstack/react-router';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useEnvironments } from './useEnvironments'; import { useEnvironments } from './useEnvironments';
@@ -11,17 +11,15 @@ export function useActiveEnvironment() {
export const QUERY_ENVIRONMENT_ID = 'environment_id'; export const QUERY_ENVIRONMENT_ID = 'environment_id';
const routeApi = getRouteApi('/workspaces/$workspaceId/');
function useActiveEnvironmentId() { function useActiveEnvironmentId() {
// NOTE: This query param is accessed from Rust side, so do not change // NOTE: This query param is accessed from Rust side, so do not change
const { environmentId: id } = useSearch({ strict: false }); const { environment_id: id} = useSearch({ strict: false });
const navigate = routeApi.useNavigate(); const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
const setId = useCallback( const setId = useCallback(
(environment_id: string | null) => (environmentId: string | null) =>
navigate({ navigate({
search: (prev) => ({ ...prev, environment_id: environment_id ?? undefined }), search: (prev) => ({ ...prev, environment_id: environmentId ?? undefined }),
}), }),
[navigate], [navigate],
); );

View File

@@ -1,13 +1,13 @@
import { useFastMutation } from './useFastMutation';
import type { Environment } from '@yaakapp-internal/models'; import type { Environment } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironment } from './useActiveEnvironment'; import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';
import {environmentsAtom} from "./useEnvironments"; import { environmentsAtom } from './useEnvironments';
import { useFastMutation } from './useFastMutation';
import { usePrompt } from './usePrompt'; import { usePrompt } from './usePrompt';
import {updateModelList} from "./useSyncModelStores"; import { updateModelList } from './useSyncModelStores';
export function useCreateEnvironment() { export function useCreateEnvironment() {
const [, setActiveEnvironmentId] = useActiveEnvironment(); const [, setActiveEnvironmentId] = useActiveEnvironment();
@@ -15,9 +15,9 @@ export function useCreateEnvironment() {
const workspace = useActiveWorkspace(); const workspace = useActiveWorkspace();
const setEnvironments = useSetAtom(environmentsAtom); const setEnvironments = useSetAtom(environmentsAtom);
return useFastMutation<Environment | null, unknown, void>({ return useFastMutation<Environment | null, unknown, Environment>({
mutationKey: ['create_environment'], mutationKey: ['create_environment'],
mutationFn: async () => { mutationFn: async (baseEnvironment) => {
const name = await prompt({ const name = await prompt({
id: 'new-environment', id: 'new-environment',
title: 'New Environment', title: 'New Environment',
@@ -33,6 +33,7 @@ export function useCreateEnvironment() {
name, name,
variables: [], variables: [],
workspaceId: workspace?.id, workspaceId: workspace?.id,
environmentId: baseEnvironment.id,
}); });
}, },
onSettled: () => trackEvent('environment', 'create'), onSettled: () => trackEvent('environment', 'create'),

View File

@@ -7,9 +7,8 @@ export const environmentsAtom = atom<Environment[]>([]);
export function useEnvironments() { export function useEnvironments() {
const allEnvironments = useAtomValue(environmentsAtom); const allEnvironments = useAtomValue(environmentsAtom);
const baseEnvironment = allEnvironments.find((e) => e.environmentId == null); const baseEnvironment = allEnvironments.find((e) => e.environmentId == null);
const subEnvironments = allEnvironments.filter( const subEnvironments =
(e) => e.environmentId === (baseEnvironment?.id ?? 'n/a'), allEnvironments.filter((e) => e.environmentId === (baseEnvironment?.id ?? 'n/a')) ?? [];
);
return { baseEnvironment, subEnvironments, allEnvironments } as const; return { baseEnvironment, subEnvironments, allEnvironments } as const;
} }

View File

@@ -29,8 +29,6 @@ export function useExportData() {
render: ({ hide }) => ( render: ({ hide }) => (
<ExportDataDialog <ExportDataDialog
onHide={hide} onHide={hide}
workspaces={workspaces}
activeWorkspace={activeWorkspace}
onSuccess={() => { onSuccess={() => {
toast.show({ toast.show({
color: 'success', color: 'success',

View File

@@ -1,5 +1,6 @@
import type { MutationKey } from '@tanstack/react-query'; import type { MutationKey } from '@tanstack/react-query';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useToast } from './useToast';
export function useFastMutation<TData = unknown, TError = unknown, TVariables = void>({ export function useFastMutation<TData = unknown, TError = unknown, TVariables = void>({
mutationKey, mutationKey,
@@ -7,13 +8,17 @@ export function useFastMutation<TData = unknown, TError = unknown, TVariables =
onSuccess, onSuccess,
onError, onError,
onSettled, onSettled,
toastyError,
}: { }: {
mutationKey: MutationKey; mutationKey: MutationKey;
mutationFn: (vars: TVariables) => Promise<TData>; mutationFn: (vars: TVariables) => Promise<TData>;
onSettled?: () => void; onSettled?: () => void;
onError?: (err: TError) => void; onError?: (err: TError) => void;
onSuccess?: (data: TData) => void; onSuccess?: (data: TData) => void;
toastyError?: boolean;
}) { }) {
const toast = useToast();
const mutateAsync = useCallback( const mutateAsync = useCallback(
async (variables: TVariables) => { async (variables: TVariables) => {
try { try {
@@ -22,8 +27,14 @@ export function useFastMutation<TData = unknown, TError = unknown, TVariables =
return data; return data;
} catch (err: unknown) { } catch (err: unknown) {
const e = err as TError; const e = err as TError;
console.log('MUTATION FAILED', mutationKey, e); console.log('Fast mutation error', mutationKey, e);
onError?.(e); onError?.(e);
if (toastyError) {
toast.show({
id: 'error-' + mutationKey.join('.'),
message: String(e),
});
}
} finally { } finally {
onSettled?.(); onSettled?.();
} }

View File

@@ -20,8 +20,11 @@ export function useSyncWorkspaceChildModels() {
const setEnvironments = useSetAtom(environmentsAtom); const setEnvironments = useSetAtom(environmentsAtom);
const workspace = useActiveWorkspace(); const workspace = useActiveWorkspace();
const workspaceId = workspace?.id ?? 'n/a'; const workspaceId = workspace?.id;
useEffect(() => { useEffect(() => {
if (workspaceId == null) {
return;
}
(async function () { (async function () {
console.log('Syncing model stores', { workspaceId }); console.log('Syncing model stores', { workspaceId });
// Set the things we need first, first // Set the things we need first, first

View File

@@ -13,6 +13,7 @@ export function useUpdateEnvironment(id: string | null) {
unknown, unknown,
Partial<Environment> | ((r: Environment) => Environment) Partial<Environment> | ((r: Environment) => Environment)
>({ >({
toastyError: true,
mutationKey: ['update_environment', id], mutationKey: ['update_environment', id],
mutationFn: async (v) => { mutationFn: async (v) => {
const environment = await getEnvironment(id); const environment = await getEnvironment(id);

View File

@@ -80,5 +80,10 @@ type TauriCmd =
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> { export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
// console.log('RUN COMMAND', cmd, args); // console.log('RUN COMMAND', cmd, args);
return invoke(cmd, args); try {
return await invoke(cmd, args);
} catch (err) {
console.warn('Tauri command error', cmd, err);
throw err;
}
} }

View File

@@ -2,15 +2,15 @@ import { createFileRoute } from '@tanstack/react-router';
import { Workspace } from '../../../components/Workspace'; import { Workspace } from '../../../components/Workspace';
interface WorkspaceSearchSchema { interface WorkspaceSearchSchema {
cookieJarId?: string | null; cookie_jar_id?: string | null;
environmentId?: string | null; environment_id?: string | null;
} }
export const Route = createFileRoute('/workspaces/$workspaceId/')({ export const Route = createFileRoute('/workspaces/$workspaceId/')({
component: RouteComponent, component: RouteComponent,
validateSearch: (search: Record<string, unknown>): WorkspaceSearchSchema => ({ validateSearch: (search: Record<string, unknown>): WorkspaceSearchSchema => ({
environmentId: search.environment_id as string, environment_id: search.environment_id as string,
cookieJarId: search.cookie_jar_id as string, cookie_jar_id: search.cookie_jar_id as string,
}), }),
}); });