diff --git a/src-web/components/AppRouter.tsx b/src-web/components/AppRouter.tsx
index 7e919036..40939be4 100644
--- a/src-web/components/AppRouter.tsx
+++ b/src-web/components/AppRouter.tsx
@@ -1,10 +1,9 @@
-import { createBrowserRouter, Navigate, Outlet, RouterProvider, useParams } from 'react-router-dom';
+import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
-import { DialogProvider } from './DialogContext';
-import { GlobalHooks } from './GlobalHooks';
+import { DefaultLayout } from './DefaultLayout';
+import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
import RouteError from './RouteError';
import Workspace from './Workspace';
-import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
const router = createBrowserRouter([
{
@@ -58,7 +57,7 @@ function RedirectLegacyEnvironmentURLs() {
}>();
const environmentId = rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId;
- let to = '/';
+ let to;
if (workspaceId != null && requestId != null) {
to = routes.paths.request({ workspaceId, environmentId, requestId });
} else if (workspaceId != null) {
@@ -69,12 +68,3 @@ function RedirectLegacyEnvironmentURLs() {
return ;
}
-
-function DefaultLayout() {
- return (
-
-
-
-
- );
-}
diff --git a/src-web/components/DefaultLayout.tsx b/src-web/components/DefaultLayout.tsx
new file mode 100644
index 00000000..31e80c1a
--- /dev/null
+++ b/src-web/components/DefaultLayout.tsx
@@ -0,0 +1,12 @@
+import { Outlet } from 'react-router-dom';
+import { DialogProvider } from './DialogContext';
+import { GlobalHooks } from './GlobalHooks';
+
+export function DefaultLayout() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx
index dccb6baa..f9991c02 100644
--- a/src-web/components/GlobalHooks.tsx
+++ b/src-web/components/GlobalHooks.tsx
@@ -3,6 +3,7 @@ import { appWindow } from '@tauri-apps/api/window';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
+import { useGlobalCommands } from '../hooks/useGlobalCommands';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
@@ -32,8 +33,8 @@ export function GlobalHooks() {
useRecentRequests();
useSyncAppearance();
-
useSyncWindowTitle();
+ useGlobalCommands();
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
diff --git a/src-web/components/RedirectToLatestWorkspace.tsx b/src-web/components/RedirectToLatestWorkspace.tsx
index e80af3c2..6b561972 100644
--- a/src-web/components/RedirectToLatestWorkspace.tsx
+++ b/src-web/components/RedirectToLatestWorkspace.tsx
@@ -24,7 +24,7 @@ export function RedirectToLatestWorkspace() {
navigate(routes.paths.workspace({ workspaceId, environmentId }));
}
})();
- }, [navigate, routes.paths, workspaces, workspaces.length]);
+ }, [navigate, recentWorkspaces, routes.paths, workspaces, workspaces.length]);
return <>>;
}
diff --git a/src-web/components/WorkspaceActionsDropdown.tsx b/src-web/components/WorkspaceActionsDropdown.tsx
index 08ce07a1..2795aeee 100644
--- a/src-web/components/WorkspaceActionsDropdown.tsx
+++ b/src-web/components/WorkspaceActionsDropdown.tsx
@@ -3,7 +3,7 @@ import classNames from 'classnames';
import { memo, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
-import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
+import { useCommand } from '../hooks/useCommands';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
import { usePrompt } from '../hooks/usePrompt';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
@@ -30,7 +30,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const activeWorkspaceId = activeWorkspace?.id ?? null;
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
- const createWorkspace = useCreateWorkspace({ navigateAfter: true });
+ const createWorkspace = useCommand('workspace.create');
const dialog = useDialog();
const prompt = usePrompt();
const routes = useAppRoutes();
diff --git a/src-web/hooks/useCommands.ts b/src-web/hooks/useCommands.ts
new file mode 100644
index 00000000..3e3ab2af
--- /dev/null
+++ b/src-web/hooks/useCommands.ts
@@ -0,0 +1,41 @@
+import type { UseMutationOptions } from '@tanstack/react-query';
+import { useMutation } from '@tanstack/react-query';
+import { useEffect } from 'react';
+import { createGlobalState } from 'react-use';
+import type { TrackAction, TrackResource } from '../lib/analytics';
+import type { Workspace } from '../lib/models';
+
+interface CommandInstance extends UseMutationOptions {
+ track?: [TrackResource, TrackAction];
+ name: string;
+}
+
+export type Commands = {
+ 'workspace.create': CommandInstance>, Workspace>;
+};
+
+const useCommandState = createGlobalState();
+
+export function useRegisterCommand(action: K, command: Commands[K]) {
+ const [, setState] = useCommandState();
+
+ useEffect(() => {
+ setState((commands) => {
+ return { ...commands, [action]: command };
+ });
+
+ // Remove action when it goes out of scope
+ return () => {
+ setState((commands) => {
+ return { ...commands, [action]: undefined };
+ });
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [action]);
+}
+
+export function useCommand(action: K) {
+ const [commands] = useCommandState();
+ const cmd = commands[action];
+ return useMutation({ ...cmd });
+}
diff --git a/src-web/hooks/useCreateWorkspace.ts b/src-web/hooks/useGlobalCommands.ts
similarity index 58%
rename from src-web/hooks/useCreateWorkspace.ts
rename to src-web/hooks/useGlobalCommands.ts
index 2c785341..f9ed95b6 100644
--- a/src-web/hooks/useCreateWorkspace.ts
+++ b/src-web/hooks/useGlobalCommands.ts
@@ -1,14 +1,18 @@
-import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
-import { trackEvent } from '../lib/analytics';
-import type { Workspace } from '../lib/models';
import { useAppRoutes } from './useAppRoutes';
+import { useRegisterCommand } from './useCommands';
import { usePrompt } from './usePrompt';
-export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }) {
- const routes = useAppRoutes();
+export function useGlobalCommands() {
const prompt = usePrompt();
- return useMutation>>({
+ const routes = useAppRoutes();
+
+ useRegisterCommand('workspace.create', {
+ name: 'New Workspace',
+ track: ['workspace', 'create'],
+ onSuccess: async (workspace) => {
+ routes.navigate('workspace', { workspaceId: workspace.id });
+ },
mutationFn: async ({ name: patchName }) => {
const name =
patchName ??
@@ -23,11 +27,5 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }
}));
return invoke('cmd_create_workspace', { name });
},
- onSettled: () => trackEvent('workspace', 'create'),
- onSuccess: async (workspace) => {
- if (navigateAfter) {
- routes.navigate('workspace', { workspaceId: workspace.id });
- }
- },
});
}
diff --git a/src-web/lib/analytics.ts b/src-web/lib/analytics.ts
index 69263cb1..a8e00b8c 100644
--- a/src-web/lib/analytics.ts
+++ b/src-web/lib/analytics.ts
@@ -1,35 +1,38 @@
import { invoke } from '@tauri-apps/api';
-export function trackEvent(
- resource:
- | 'app'
- | 'cookie_jar'
- | 'dialog'
- | 'environment'
- | 'folder'
- | 'grpc_connection'
- | 'grpc_event'
- | 'grpc_request'
- | 'http_request'
- | 'http_response'
- | 'key_value'
- | 'setting'
- | 'sidebar'
- | 'workspace',
- action:
- | 'cancel'
- | 'commit'
- | 'create'
- | 'delete'
- | 'delete_many'
- | 'duplicate'
- | 'hide'
- | 'launch'
- | 'send'
- | 'show'
- | 'toggle'
- | 'update',
+export type TrackResource =
+ | 'app'
+ | 'cookie_jar'
+ | 'dialog'
+ | 'environment'
+ | 'folder'
+ | 'grpc_connection'
+ | 'grpc_event'
+ | 'grpc_request'
+ | 'http_request'
+ | 'http_response'
+ | 'key_value'
+ | 'setting'
+ | 'sidebar'
+ | 'workspace';
+export type TrackAction =
+ | 'cancel'
+ | 'commit'
+ | 'create'
+ | 'delete'
+ | 'delete_many'
+ | 'duplicate'
+ | 'hide'
+ | 'launch'
+ | 'send'
+ | 'show'
+ | 'toggle'
+ | 'update';
+
+export function trackEvent(
+ resource: TrackResource,
+ action: TrackAction,
attributes: Record = {},
) {
invoke('cmd_track_event', {