gRPC Support (#20)

This commit is contained in:
Gregory Schier
2024-02-09 05:01:00 -08:00
committed by GitHub
parent 219a6b78da
commit 394beb374e
162 changed files with 6670 additions and 1770 deletions

View File

@@ -30,13 +30,13 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps)
};
return (
<HStack space={2} justifyContent="end" className="mt-2 mb-4">
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button className="focus" color={colors[variant]} onClick={handleSuccess}>
{confirmButtonTexts[variant]}
</Button>
<Button className="focus" color="gray" onClick={handleHide}>
Cancel
</Button>
<Button autoFocus className="focus" color={colors[variant]} onClick={handleSuccess}>
{confirmButtonTexts[variant]}
</Button>
</HStack>
);
}

View File

@@ -1,9 +1,28 @@
import type { HttpRequest } from '../lib/models';
import { r } from 'vitest/dist/types-94cfe4b4';
import type { GrpcRequest, HttpRequest } from '../lib/models';
import { useActiveRequestId } from './useActiveRequestId';
import { useRequests } from './useRequests';
import { useGrpcRequests } from './useGrpcRequests';
import { useHttpRequests } from './useHttpRequests';
export function useActiveRequest(): HttpRequest | null {
const requestId = useActiveRequestId();
const requests = useRequests();
return requests.find((r) => r.id === requestId) ?? null;
interface TypeMap {
http_request: HttpRequest;
grpc_request: GrpcRequest;
}
export function useActiveRequest<T extends keyof TypeMap>(
model?: T | undefined,
): TypeMap[T] | null {
const requestId = useActiveRequestId();
const httpRequests = useHttpRequests();
const grpcRequests = useGrpcRequests();
if (model === 'http_request') {
return (httpRequests.find((r) => r.id === requestId) ?? null) as TypeMap[T] | null;
} else if (model === 'grpc_request') {
return (grpcRequests.find((r) => r.id === requestId) ?? null) as TypeMap[T] | null;
} else {
return (grpcRequests.find((r) => r.id === requestId) ??
httpRequests.find((r) => r.id === requestId) ??
null) as TypeMap[T] | null;
}
}

View File

@@ -1,3 +1,4 @@
import { useCallback } from 'react';
import type { DialogProps } from '../components/core/Dialog';
import { useDialog } from '../components/DialogContext';
import type { AlertProps } from './Alert';
@@ -5,20 +6,16 @@ import { Alert } from './Alert';
export function useAlert() {
const dialog = useDialog();
return ({
id,
title,
body,
}: {
id: string;
title: DialogProps['title'];
body: AlertProps['body'];
}) =>
dialog.show({
id,
title,
hideX: true,
size: 'sm',
render: ({ hide }) => Alert({ onHide: hide, body }),
});
return useCallback(
({ id, title, body }: { id: string; title: DialogProps['title']; body: AlertProps['body'] }) =>
dialog.show({
id,
title,
hideX: true,
size: 'sm',
render: ({ hide }) => Alert({ onHide: hide, body }),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
}

View File

@@ -15,7 +15,7 @@ export function useCookieJars() {
queryKey: cookieJarsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invoke('list_cookie_jars', { workspaceId })) as CookieJar[];
return (await invoke('cmd_list_cookie_jars', { workspaceId })) as CookieJar[];
},
}).data ?? []
);

View File

@@ -1,17 +1,17 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { HttpRequest } from '../lib/models';
import type { CookieJar } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { cookieJarsQueryKey } from './useCookieJars';
import { usePrompt } from './usePrompt';
import { requestsQueryKey } from './useRequests';
export function useCreateCookieJar() {
const workspaceId = useActiveWorkspaceId();
const queryClient = useQueryClient();
const prompt = usePrompt();
return useMutation<HttpRequest>({
return useMutation<CookieJar>({
mutationFn: async () => {
if (workspaceId === null) {
throw new Error("Cannot create cookie jar when there's no active workspace");
@@ -23,13 +23,13 @@ export function useCreateCookieJar() {
label: 'Name',
defaultValue: 'My Jar',
});
return invoke('create_cookie_jar', { workspaceId, name });
return invoke('cmd_create_cookie_jar', { workspaceId, name });
},
onSettled: () => trackEvent('CookieJar', 'Create'),
onSuccess: async (request) => {
queryClient.setQueryData<HttpRequest[]>(
requestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
onSuccess: async (cookieJar) => {
queryClient.setQueryData<CookieJar[]>(
cookieJarsQueryKey({ workspaceId: cookieJar.workspaceId }),
(items) => [...(items ?? []), cookieJar],
);
},
});

View File

@@ -22,7 +22,7 @@ export function useCreateEnvironment() {
label: 'Name',
defaultValue: 'My Environment',
});
return invoke('create_environment', { name, variables: [], workspaceId });
return invoke('cmd_create_environment', { name, variables: [], workspaceId });
},
onSettled: () => trackEvent('Environment', 'Create'),
onSuccess: async (environment) => {

View File

@@ -16,7 +16,7 @@ export function useCreateFolder() {
}
patch.name = patch.name || 'New Folder';
patch.sortPriority = patch.sortPriority || -Date.now();
return invoke('create_folder', { workspaceId, ...patch });
return invoke('cmd_create_folder', { workspaceId, ...patch });
},
onSettled: () => trackEvent('Folder', 'Create'),
onSuccess: async (request) => {

View File

@@ -0,0 +1,55 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { GrpcRequest, HttpRequest } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { grpcRequestsQueryKey } from './useGrpcRequests';
import { httpRequestsQueryKey } from './useHttpRequests';
export function useCreateGrpcRequest() {
const workspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
// const activeRequest = useActiveRequest();
const activeRequest = null;
const routes = useAppRoutes();
const queryClient = useQueryClient();
return useMutation<
GrpcRequest,
unknown,
Partial<Pick<GrpcRequest, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationFn: (patch) => {
if (workspaceId === null) {
throw new Error("Cannot create grpc request when there's no active workspace");
}
if (patch.sortPriority === undefined) {
if (activeRequest != null) {
// Place above currently-active request
// patch.sortPriority = activeRequest.sortPriority + 0.0001;
} else {
// Place at the very top
patch.sortPriority = -Date.now();
}
}
// patch.folderId = patch.folderId; // TODO: || activeRequest?.folderId;
return invoke('cmd_create_grpc_request', { workspaceId, name: '', ...patch });
},
onSettled: () => trackEvent('GrpcRequest', 'Create'),
onSuccess: async (request) => {
queryClient.setQueryData<GrpcRequest[]>(
grpcRequestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
);
// TODO: This should navigate to the new request
routes.navigate('request', {
workspaceId: request.workspaceId,
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
});
},
});
}

View File

@@ -6,9 +6,9 @@ import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { requestsQueryKey } from './useRequests';
import { httpRequestsQueryKey } from './useHttpRequests';
export function useCreateRequest() {
export function useCreateHttpRequest() {
const workspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const activeRequest = useActiveRequest();
@@ -34,12 +34,12 @@ export function useCreateRequest() {
}
}
patch.folderId = patch.folderId || activeRequest?.folderId;
return invoke('create_request', { workspaceId, name: '', ...patch });
return invoke('cmd_create_http_request', { workspaceId, name: '', ...patch });
},
onSettled: () => trackEvent('HttpRequest', 'Create'),
onSuccess: async (request) => {
queryClient.setQueryData<HttpRequest[]>(
requestsQueryKey({ workspaceId: request.workspaceId }),
httpRequestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
);
routes.navigate('request', {

View File

@@ -10,7 +10,7 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }
const queryClient = useQueryClient();
return useMutation<Workspace, unknown, Pick<Workspace, 'name'>>({
mutationFn: (patch) => {
return invoke('create_workspace', patch);
return invoke('cmd_create_workspace', patch);
},
onSettled: () => trackEvent('Workspace', 'Create'),
onSuccess: async (workspace) => {

View File

@@ -0,0 +1,43 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import type { GrpcRequest } from '../lib/models';
import { getGrpcRequest } from '../lib/store';
import { useConfirm } from './useConfirm';
import { grpcRequestsQueryKey } from './useGrpcRequests';
export function useDeleteAnyGrpcRequest() {
const queryClient = useQueryClient();
const confirm = useConfirm();
return useMutation<GrpcRequest | null, string, string>({
mutationFn: async (id) => {
const request = await getGrpcRequest(id);
if (request == null) return null;
const confirmed = await confirm({
id: 'delete-grpc-request',
title: 'Delete Request',
variant: 'delete',
description: (
<>
Permanently delete <InlineCode>{fallbackRequestName(request)}</InlineCode>?
</>
),
});
if (!confirmed) return null;
return invoke('cmd_delete_grpc_request', { requestId: id });
},
onSettled: () => trackEvent('GrpcRequest', 'Delete'),
onSuccess: async (request) => {
if (request === null) return;
const { workspaceId, id: requestId } = request;
queryClient.setQueryData<GrpcRequest[]>(grpcRequestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId),
);
},
});
}

View File

@@ -4,18 +4,20 @@ import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import type { HttpRequest } from '../lib/models';
import { getRequest } from '../lib/store';
import { getHttpRequest } from '../lib/store';
import { useConfirm } from './useConfirm';
import { requestsQueryKey } from './useRequests';
import { responsesQueryKey } from './useResponses';
import { httpRequestsQueryKey } from './useHttpRequests';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteAnyRequest() {
export function useDeleteAnyHttpRequest() {
const queryClient = useQueryClient();
const confirm = useConfirm();
return useMutation<HttpRequest | null, string, string>({
mutationFn: async (id) => {
const request = await getRequest(id);
const request = await getHttpRequest(id);
if (request == null) return null;
const confirmed = await confirm({
id: 'delete-request',
title: 'Delete Request',
@@ -27,7 +29,7 @@ export function useDeleteAnyRequest() {
),
});
if (!confirmed) return null;
return invoke('delete_request', { requestId: id });
return invoke('cmd_delete_http_request', { requestId: id });
},
onSettled: () => trackEvent('HttpRequest', 'Delete'),
onSuccess: async (request) => {
@@ -35,8 +37,8 @@ export function useDeleteAnyRequest() {
if (request === null) return;
const { workspaceId, id: requestId } = request;
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) =>
queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId),
);
},

View File

@@ -23,7 +23,7 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) {
),
});
if (!confirmed) return null;
return invoke('delete_cookie_jar', { cookieJarId: cookieJar?.id });
return invoke('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id });
},
onSettled: () => trackEvent('CookieJar', 'Delete'),
onSuccess: async (cookieJar) => {

View File

@@ -23,7 +23,7 @@ export function useDeleteEnvironment(environment: Environment | null) {
),
});
if (!confirmed) return null;
return invoke('delete_environment', { environmentId: environment?.id });
return invoke('cmd_delete_environment', { environmentId: environment?.id });
},
onSettled: () => trackEvent('Environment', 'Delete'),
onSuccess: async (environment) => {

View File

@@ -6,7 +6,7 @@ import type { Folder } from '../lib/models';
import { getFolder } from '../lib/store';
import { useConfirm } from './useConfirm';
import { foldersQueryKey } from './useFolders';
import { requestsQueryKey } from './useRequests';
import { httpRequestsQueryKey } from './useHttpRequests';
export function useDeleteFolder(id: string | null) {
const queryClient = useQueryClient();
@@ -26,7 +26,7 @@ export function useDeleteFolder(id: string | null) {
),
});
if (!confirmed) return null;
return invoke('delete_folder', { folderId: id });
return invoke('cmd_delete_folder', { folderId: id });
},
onSettled: () => trackEvent('Folder', 'Delete'),
onSuccess: async (folder) => {
@@ -36,7 +36,7 @@ export function useDeleteFolder(id: string | null) {
const { workspaceId } = folder;
// Nesting makes it hard to clean things up, so just clear everything that could have been deleted
await queryClient.invalidateQueries(requestsQueryKey({ workspaceId }));
await queryClient.invalidateQueries(httpRequestsQueryKey({ workspaceId }));
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId }));
},
});

View File

@@ -0,0 +1,21 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { GrpcConnection } from '../lib/models';
import { grpcConnectionsQueryKey } from './useGrpcConnections';
export function useDeleteGrpcConnection(id: string | null) {
const queryClient = useQueryClient();
return useMutation<GrpcConnection>({
mutationFn: async () => {
return await invoke('cmd_delete_grpc_connection', { id: id });
},
onSettled: () => trackEvent('GrpcConnection', 'Delete'),
onSuccess: ({ requestId, id: connectionId }) => {
queryClient.setQueryData<GrpcConnection[]>(
grpcConnectionsQueryKey({ requestId }),
(connections) => (connections ?? []).filter((c) => c.id !== connectionId),
);
},
});
}

View File

@@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import { grpcConnectionsQueryKey } from './useGrpcConnections';
export function useDeleteGrpcConnections(requestId?: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
if (requestId === undefined) return;
await invoke('cmd_delete_all_grpc_connections', { requestId });
},
onSettled: () => trackEvent('GrpcConnection', 'DeleteMany'),
onSuccess: async () => {
if (requestId === undefined) return;
queryClient.setQueryData(grpcConnectionsQueryKey({ requestId }), []);
},
});
}

View File

@@ -2,17 +2,17 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { HttpResponse } from '../lib/models';
import { responsesQueryKey } from './useResponses';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteResponse(id: string | null) {
export function useDeleteHttpResponse(id: string | null) {
const queryClient = useQueryClient();
return useMutation<HttpResponse>({
mutationFn: async () => {
return await invoke('delete_response', { id: id });
return await invoke('cmd_delete_http_response', { id: id });
},
onSettled: () => trackEvent('HttpResponse', 'Delete'),
onSuccess: ({ requestId, id: responseId }) => {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) =>
queryClient.setQueryData<HttpResponse[]>(httpResponsesQueryKey({ requestId }), (responses) =>
(responses ?? []).filter((response) => response.id !== responseId),
);
},

View File

@@ -1,19 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import { responsesQueryKey } from './useResponses';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteResponses(requestId?: string) {
export function useDeleteHttpResponses(requestId?: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
if (requestId === undefined) return;
await invoke('delete_all_responses', { requestId });
await invoke('cmd_delete_all_http_responses', { requestId });
},
onSettled: () => trackEvent('HttpResponse', 'DeleteMany'),
onSuccess: async () => {
if (requestId === undefined) return;
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []);
},
});
}

View File

@@ -1,9 +1,9 @@
import { useMutation } from '@tanstack/react-query';
import type { HttpRequest } from '../lib/models';
import { useDeleteAnyRequest } from './useDeleteAnyRequest';
import { useDeleteAnyHttpRequest } from './useDeleteAnyHttpRequest';
export function useDeleteRequest(id: string | null) {
const deleteAnyRequest = useDeleteAnyRequest();
const deleteAnyRequest = useDeleteAnyHttpRequest();
return useMutation<HttpRequest | null, string>({
mutationFn: () => deleteAnyRequest.mutateAsync(id ?? 'n/a'),

View File

@@ -6,7 +6,7 @@ import type { Workspace } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { useConfirm } from './useConfirm';
import { requestsQueryKey } from './useRequests';
import { httpRequestsQueryKey } from './useHttpRequests';
import { workspacesQueryKey } from './useWorkspaces';
export function useDeleteWorkspace(workspace: Workspace | null) {
@@ -28,7 +28,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
),
});
if (!confirmed) return null;
return invoke('delete_workspace', { workspaceId: workspace?.id });
return invoke('cmd_delete_workspace', { workspaceId: workspace?.id });
},
onSettled: () => trackEvent('Workspace', 'Delete'),
onSuccess: async (workspace) => {
@@ -43,8 +43,8 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
}
// Also clean up other things that may have been deleted
queryClient.setQueryData(requestsQueryKey({ workspaceId }), []);
await queryClient.invalidateQueries(requestsQueryKey({ workspaceId }));
queryClient.setQueryData(httpRequestsQueryKey({ workspaceId }), []);
await queryClient.invalidateQueries(httpRequestsQueryKey({ workspaceId }));
},
});
}

View File

@@ -0,0 +1,41 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { GrpcRequest } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { grpcRequestsQueryKey } from './useGrpcRequests';
export function useDuplicateGrpcRequest({
id,
navigateAfter,
}: {
id: string | null;
navigateAfter: boolean;
}) {
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const routes = useAppRoutes();
const queryClient = useQueryClient();
return useMutation<GrpcRequest, string>({
mutationFn: async () => {
if (id === null) throw new Error("Can't duplicate a null grpc request");
return invoke('cmd_duplicate_grpc_request', { id });
},
onSettled: () => trackEvent('GrpcRequest', 'Duplicate'),
onSuccess: async (request) => {
queryClient.setQueryData<GrpcRequest[]>(
grpcRequestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
);
if (navigateAfter && activeWorkspaceId !== null) {
routes.navigate('request', {
workspaceId: activeWorkspaceId,
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
});
}
},
});
}

View File

@@ -5,9 +5,9 @@ import type { HttpRequest } from '../lib/models';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { requestsQueryKey } from './useRequests';
import { httpRequestsQueryKey } from './useHttpRequests';
export function useDuplicateRequest({
export function useDuplicateHttpRequest({
id,
navigateAfter,
}: {
@@ -21,12 +21,12 @@ export function useDuplicateRequest({
return useMutation<HttpRequest, string>({
mutationFn: async () => {
if (id === null) throw new Error("Can't duplicate a null request");
return invoke('duplicate_request', { id });
return invoke('cmd_duplicate_http_request', { id });
},
onSettled: () => trackEvent('HttpRequest', 'Duplicate'),
onSuccess: async (request) => {
queryClient.setQueryData<HttpRequest[]>(
requestsQueryKey({ workspaceId: request.workspaceId }),
httpRequestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
);
if (navigateAfter && activeWorkspaceId !== null) {

View File

@@ -15,7 +15,7 @@ export function useEnvironments() {
queryKey: environmentsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invoke('list_environments', { workspaceId })) as Environment[];
return (await invoke('cmd_list_environments', { workspaceId })) as Environment[];
},
}).data ?? []
);

View File

@@ -25,7 +25,7 @@ export function useExportData() {
return;
}
await invoke('export_data', { workspaceId: workspace.id, exportPath });
await invoke('cmd_export_data', { workspaceId: workspace.id, exportPath });
},
});
}

View File

@@ -16,7 +16,7 @@ export function useFilterResponse({
return null;
}
return (await invoke('filter_response', { responseId, filter })) as string | null;
return (await invoke('cmd_filter_response', { responseId, filter })) as string | null;
},
}).data ?? null
);

View File

@@ -15,7 +15,7 @@ export function useFolders() {
queryKey: foldersQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invoke('list_folders', { workspaceId })) as Folder[];
return (await invoke('cmd_list_folders', { workspaceId })) as Folder[];
},
}).data ?? []
);

79
src-web/hooks/useGrpc.ts Normal file
View File

@@ -0,0 +1,79 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { emit } from '@tauri-apps/api/event';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models';
import { useDebouncedValue } from './useDebouncedValue';
export interface ReflectResponseService {
name: string;
methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[];
}
export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) {
const requestId = req?.id ?? 'n/a';
const unary = useMutation<GrpcMessage, string>({
mutationKey: ['grpc_unary', conn?.id ?? 'n/a'],
mutationFn: async () =>
(await invoke('cmd_grpc_call_unary', {
requestId,
})) as GrpcMessage,
});
const clientStreaming = useMutation<void, string>({
mutationKey: ['grpc_client_streaming', conn?.id ?? 'n/a'],
mutationFn: async () => await invoke('cmd_grpc_client_streaming', { requestId }),
});
const serverStreaming = useMutation<void, string>({
mutationKey: ['grpc_server_streaming', conn?.id ?? 'n/a'],
mutationFn: async () => await invoke('cmd_grpc_server_streaming', { requestId }),
});
const streaming = useMutation<void, string>({
mutationKey: ['grpc_streaming', conn?.id ?? 'n/a'],
mutationFn: async () => await invoke('cmd_grpc_streaming', { requestId }),
});
const send = useMutation({
mutationFn: async ({ message }: { message: string }) =>
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, { Message: message }),
});
const cancel = useMutation({
mutationKey: ['grpc_cancel', conn?.id ?? 'n/a'],
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Cancel'),
});
const commit = useMutation({
mutationKey: ['grpc_commit', conn?.id ?? 'n/a'],
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
});
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 1000);
const reflect = useQuery<ReflectResponseService[] | null, string>({
enabled: req != null,
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl],
refetchOnWindowFocus: false,
queryFn: async () => {
console.log('useGrpc.reflect', { requestId });
return (await minPromiseMillis(
invoke('cmd_grpc_reflect', { requestId }),
300,
)) as ReflectResponseService[];
},
});
return {
unary,
clientStreaming,
serverStreaming,
streaming,
reflect,
cancel,
commit,
isStreaming: conn?.elapsed === 0,
send,
};
}

View File

@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { GrpcConnection } from '../lib/models';
export function grpcConnectionsQueryKey({ requestId }: { requestId: string }) {
return ['grpc_connections', { requestId }];
}
export function useGrpcConnections(requestId: string | null) {
return (
useQuery<GrpcConnection[]>({
enabled: requestId !== null,
initialData: [],
queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
return (await invoke('cmd_list_grpc_connections', {
requestId,
limit: 200,
})) as GrpcConnection[];
},
}).data ?? []
);
}

View File

@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { GrpcMessage } from '../lib/models';
export function grpcMessagesQueryKey({ connectionId }: { connectionId: string }) {
return ['grpc_messages', { connectionId }];
}
export function useGrpcMessages(connectionId: string | null) {
return (
useQuery<GrpcMessage[]>({
enabled: connectionId !== null,
initialData: [],
queryKey: grpcMessagesQueryKey({ connectionId: connectionId ?? 'n/a' }),
queryFn: async () => {
return (await invoke('cmd_list_grpc_messages', {
connectionId,
limit: 200,
})) as GrpcMessage[];
},
}).data ?? []
);
}

View File

@@ -0,0 +1,7 @@
import type { GrpcRequest } from '../lib/models';
import { useGrpcRequests } from './useGrpcRequests';
export function useGrpcRequest(id: string | null): GrpcRequest | null {
const requests = useGrpcRequests();
return requests.find((r) => r.id === id) ?? null;
}

View File

@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { GrpcRequest } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
export function grpcRequestsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['grpc_requests', { workspaceId }];
}
export function useGrpcRequests() {
const workspaceId = useActiveWorkspaceId();
return (
useQuery({
enabled: workspaceId != null,
queryKey: grpcRequestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invoke('cmd_list_grpc_requests', { workspaceId })) as GrpcRequest[];
},
}).data ?? []
);
}

View File

@@ -5,44 +5,47 @@ import { debounce } from '../lib/debounce';
import { useOsInfo } from './useOsInfo';
export type HotkeyAction =
| 'request.send'
| 'request.create'
| 'request.duplicate'
| 'sidebar.toggle'
| 'sidebar.focus'
| 'urlBar.focus'
| 'environmentEditor.toggle'
| 'hotkeys.showHelp'
| 'requestSwitcher.prev'
| 'grpc_request.send'
| 'http_request.create'
| 'http_request.duplicate'
| 'http_request.send'
| 'requestSwitcher.next'
| 'settings.show';
| 'requestSwitcher.prev'
| 'settings.show'
| 'sidebar.focus'
| 'sidebar.toggle'
| 'urlBar.focus';
const hotkeys: Record<HotkeyAction, string[]> = {
'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
'request.create': ['CmdCtrl+n'],
'request.duplicate': ['CmdCtrl+d'],
'sidebar.toggle': ['CmdCtrl+b'],
'sidebar.focus': ['CmdCtrl+1'],
'urlBar.focus': ['CmdCtrl+l'],
'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
'grpc_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
'hotkeys.showHelp': ['CmdCtrl+Shift+/'],
'settings.show': ['CmdCtrl+,'],
'requestSwitcher.prev': ['Control+Tab'],
'http_request.create': ['CmdCtrl+n'],
'http_request.duplicate': ['CmdCtrl+d'],
'http_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
'requestSwitcher.next': ['Control+Shift+Tab'],
'requestSwitcher.prev': ['Control+Tab'],
'settings.show': ['CmdCtrl+,'],
'sidebar.focus': ['CmdCtrl+1'],
'sidebar.toggle': ['CmdCtrl+b'],
'urlBar.focus': ['CmdCtrl+l'],
};
const hotkeyLabels: Record<HotkeyAction, string> = {
'request.send': 'Send Request',
'request.create': 'New Request',
'request.duplicate': 'Duplicate Request',
'sidebar.toggle': 'Toggle Sidebar',
'sidebar.focus': 'Focus Sidebar',
'urlBar.focus': 'Focus URL',
'environmentEditor.toggle': 'Edit Environments',
'grpc_request.send': 'Send Message',
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
'requestSwitcher.prev': 'Go To Next Request',
'http_request.create': 'New Request',
'http_request.duplicate': 'Duplicate Request',
'http_request.send': 'Send Request',
'requestSwitcher.next': 'Go To Previous Request',
'requestSwitcher.prev': 'Go To Next Request',
'settings.show': 'Open Settings',
'sidebar.focus': 'Focus Sidebar',
'sidebar.toggle': 'Toggle Sidebar',
'urlBar.focus': 'Focus URL',
};
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
@@ -109,11 +112,11 @@ export function useAnyHotkey(
}
currentKeys.current.delete(normalizeKey(e.key, os));
};
window.addEventListener('keydown', down);
window.addEventListener('keyup', up);
document.addEventListener('keydown', down, { capture: true });
document.addEventListener('keyup', up, { capture: true });
return () => {
window.removeEventListener('keydown', down);
window.removeEventListener('keyup', up);
document.removeEventListener('keydown', down, { capture: true });
document.removeEventListener('keyup', up, { capture: true });
};
}, [options.enable, os]);
}

View File

@@ -0,0 +1,7 @@
import type { HttpRequest } from '../lib/models';
import { useHttpRequests } from './useHttpRequests';
export function useHttpRequest(id: string | null): HttpRequest | null {
const requests = useHttpRequests();
return requests.find((r) => r.id === id) ?? null;
}

View File

@@ -3,19 +3,19 @@ import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
export function requestsQueryKey({ workspaceId }: { workspaceId: string }) {
export function httpRequestsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['http_requests', { workspaceId }];
}
export function useRequests() {
export function useHttpRequests() {
const workspaceId = useActiveWorkspaceId();
return (
useQuery({
enabled: workspaceId != null,
queryKey: requestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
queryKey: httpRequestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invoke('list_requests', { workspaceId })) as HttpRequest[];
return (await invoke('cmd_list_http_requests', { workspaceId })) as HttpRequest[];
},
}).data ?? []
);

View File

@@ -2,18 +2,18 @@ import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpResponse } from '../lib/models';
export function responsesQueryKey({ requestId }: { requestId: string }) {
export function httpResponsesQueryKey({ requestId }: { requestId: string }) {
return ['http_responses', { requestId }];
}
export function useResponses(requestId: string | null) {
export function useHttpResponses(requestId: string | null) {
return (
useQuery<HttpResponse[]>({
enabled: requestId !== null,
initialData: [],
queryKey: responsesQueryKey({ requestId: requestId ?? 'n/a' }),
queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
return (await invoke('list_responses', { requestId, limit: 200 })) as HttpResponse[];
return (await invoke('cmd_list_http_responses', { requestId, limit: 200 })) as HttpResponse[];
},
}).data ?? []
);

View File

@@ -35,7 +35,7 @@ export function useImportData() {
environments: Environment[];
folders: Folder[];
requests: HttpRequest[];
} = await invoke('import_data', {
} = await invoke('cmd_import_data', {
filePaths: Array.isArray(selected) ? selected : [selected],
});
const importedWorkspace = imported.workspaces[0];

View File

@@ -1,8 +1,8 @@
import { isResponseLoading } from '../lib/models';
import { useLatestResponse } from './useLatestResponse';
import { useLatestHttpResponse } from './useLatestHttpResponse';
export function useIsResponseLoading(requestId: string | null): boolean {
const response = useLatestResponse(requestId);
const response = useLatestHttpResponse(requestId);
if (response === null) return false;
return isResponseLoading(response);
}

View File

@@ -28,6 +28,7 @@ export function useKeyValue<T extends Object | null>({
const query = useQuery<T>({
queryKey: keyValueQueryKey({ namespace, key }),
queryFn: async () => getKeyValue({ namespace, key, fallback: defaultValue }),
refetchOnWindowFocus: false,
});
const mutate = useMutation<void, unknown, T>({
@@ -37,19 +38,21 @@ export function useKeyValue<T extends Object | null>({
});
const set = useCallback(
(value: ((v: T) => T) | T) => {
async (value: ((v: T) => T) | T) => {
if (typeof value === 'function') {
getKeyValue({ namespace, key, fallback: defaultValue }).then((kv) => {
mutate.mutate(value(kv));
await getKeyValue({ namespace, key, fallback: defaultValue }).then((kv) => {
const newV = value(kv);
if (newV === kv) return;
return mutate.mutateAsync(newV);
});
} else {
mutate.mutate(value);
} else if (value !== query.data) {
await mutate.mutateAsync(value);
}
},
[defaultValue, key, mutate, namespace],
[defaultValue, key, mutate, namespace, query.data],
);
const reset = useCallback(() => mutate.mutate(defaultValue), [mutate, defaultValue]);
const reset = useCallback(async () => mutate.mutateAsync(defaultValue), [mutate, defaultValue]);
return useMemo(
() => ({

View File

@@ -0,0 +1,7 @@
import type { GrpcConnection } from '../lib/models';
import { useGrpcConnections } from './useGrpcConnections';
export function useLatestGrpcConnection(requestId: string | null): GrpcConnection | null {
const connections = useGrpcConnections(requestId);
return connections[0] ?? null;
}

View File

@@ -0,0 +1,7 @@
import type { HttpResponse } from '../lib/models';
import { useHttpResponses } from './useHttpResponses';
export function useLatestHttpResponse(requestId: string | null): HttpResponse | null {
const responses = useHttpResponses(requestId);
return responses[0] ?? null;
}

View File

@@ -1,7 +0,0 @@
import type { HttpResponse } from '../lib/models';
import { useResponses } from './useResponses';
export function useLatestResponse(requestId: string | null): HttpResponse | null {
const responses = useResponses(requestId);
return responses[0] ?? null;
}

View File

@@ -1,7 +0,0 @@
import type { HttpRequest } from '../lib/models';
import { useRequests } from './useRequests';
export function useRequest(id: string | null): HttpRequest | null {
const requests = useRequests();
return requests.find((r) => r.id === id) ?? null;
}

View File

@@ -4,7 +4,7 @@ import { save } from '@tauri-apps/api/dialog';
import slugify from 'slugify';
import { trackEvent } from '../lib/analytics';
import type { HttpResponse } from '../lib/models';
import { getRequest } from '../lib/store';
import { getHttpRequest } from '../lib/store';
import { useActiveCookieJar } from './useActiveCookieJar';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useAlert } from './useAlert';
@@ -15,7 +15,7 @@ export function useSendAnyRequest(options: { download?: boolean } = {}) {
const { activeCookieJar } = useActiveCookieJar();
return useMutation<HttpResponse | null, string, string | null>({
mutationFn: async (id) => {
const request = await getRequest(id);
const request = await getHttpRequest(id);
if (request == null) {
return null;
}
@@ -31,7 +31,7 @@ export function useSendAnyRequest(options: { download?: boolean } = {}) {
}
}
return invoke('send_request', {
return invoke('cmd_send_request', {
requestId: id,
environmentId,
downloadDir: downloadDir,

View File

@@ -11,7 +11,7 @@ export function useSettings() {
useQuery({
queryKey: settingsQueryKey(),
queryFn: async () => {
return (await invoke('get_settings')) as Settings;
return (await invoke('cmd_get_settings')) as Settings;
},
}).data ?? undefined
);

View File

@@ -14,7 +14,7 @@ export function useUpdateAnyFolder() {
throw new Error("Can't update a null folder");
}
await invoke('update_folder', { folder: update(folder) });
await invoke('cmd_update_folder', { folder: update(folder) });
},
onMutate: async ({ id, update }) => {
const folder = await getFolder(id);

View File

@@ -0,0 +1,36 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { GrpcRequest } from '../lib/models';
import { sleep } from '../lib/sleep';
import { getGrpcRequest } from '../lib/store';
import { grpcRequestsQueryKey } from './useGrpcRequests';
export function useUpdateAnyGrpcRequest() {
const queryClient = useQueryClient();
return useMutation<
void,
unknown,
{ id: string; update: Partial<GrpcRequest> | ((r: GrpcRequest) => GrpcRequest) }
>({
mutationFn: async ({ id, update }) => {
const request = await getGrpcRequest(id);
if (request === null) {
throw new Error("Can't update a null request");
}
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
await invoke('cmd_update_grpc_request', { request: patchedRequest });
},
onMutate: async ({ id, update }) => {
const request = await getGrpcRequest(id);
if (request === null) return;
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
queryClient.setQueryData<GrpcRequest[]>(grpcRequestsQueryKey(request), (requests) =>
(requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)),
);
},
});
}

View File

@@ -1,10 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models';
import { getRequest } from '../lib/store';
import { requestsQueryKey } from './useRequests';
import { getHttpRequest } from '../lib/store';
import { httpRequestsQueryKey } from './useHttpRequests';
export function useUpdateAnyRequest() {
export function useUpdateAnyHttpRequest() {
const queryClient = useQueryClient();
return useMutation<
@@ -13,21 +13,21 @@ export function useUpdateAnyRequest() {
{ id: string; update: Partial<HttpRequest> | ((r: HttpRequest) => HttpRequest) }
>({
mutationFn: async ({ id, update }) => {
const request = await getRequest(id);
const request = await getHttpRequest(id);
if (request === null) {
throw new Error("Can't update a null request");
}
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
await invoke('update_request', { request: patchedRequest });
await invoke('cmd_update_http_request', { request: patchedRequest });
},
onMutate: async ({ id, update }) => {
const request = await getRequest(id);
const request = await getHttpRequest(id);
if (request === null) return;
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey(request), (requests) =>
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey(request), (requests) =>
(requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)),
);
},

View File

@@ -15,7 +15,7 @@ export function useUpdateCookieJar(id: string | null) {
const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v };
console.log('NEW COOKIE JAR', newCookieJar.cookies.length);
await invoke('update_cookie_jar', { cookieJar: newCookieJar });
await invoke('cmd_update_cookie_jar', { cookieJar: newCookieJar });
},
onMutate: async (v) => {
const cookieJar = await getCookieJar(id);

View File

@@ -14,7 +14,7 @@ export function useUpdateEnvironment(id: string | null) {
}
const newEnvironment = typeof v === 'function' ? v(environment) : { ...environment, ...v };
await invoke('update_environment', { environment: newEnvironment });
await invoke('cmd_update_environment', { environment: newEnvironment });
},
onMutate: async (v) => {
const environment = await getEnvironment(id);

View File

@@ -0,0 +1,12 @@
import { useMutation } from '@tanstack/react-query';
import type { GrpcRequest } from '../lib/models';
import { useUpdateAnyGrpcRequest } from './useUpdateAnyGrpcRequest';
export function useUpdateGrpcRequest(id: string | null) {
const updateAnyGrpcRequest = useUpdateAnyGrpcRequest();
return useMutation<void, unknown, Partial<GrpcRequest> | ((r: GrpcRequest) => GrpcRequest)>({
mutationFn: async (update) => {
return updateAnyGrpcRequest.mutateAsync({ id: id ?? 'n/a', update });
},
});
}

View File

@@ -1,9 +1,9 @@
import { useMutation } from '@tanstack/react-query';
import type { HttpRequest } from '../lib/models';
import { useUpdateAnyRequest } from './useUpdateAnyRequest';
import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest';
export function useUpdateRequest(id: string | null) {
const updateAnyRequest = useUpdateAnyRequest();
export function useUpdateHttpRequest(id: string | null) {
const updateAnyRequest = useUpdateAnyHttpRequest();
return useMutation<void, unknown, Partial<HttpRequest> | ((r: HttpRequest) => HttpRequest)>({
mutationFn: async (update) => updateAnyRequest.mutateAsync({ id: id ?? 'n/a', update }),
});

View File

@@ -8,7 +8,7 @@ export function useUpdateSettings() {
return useMutation<void, unknown, Settings>({
mutationFn: async (settings) => {
await invoke('update_settings', { settings });
await invoke('cmd_update_settings', { settings });
},
onMutate: async (settings) => {
queryClient.setQueryData<Settings>(settingsQueryKey(), settings);

View File

@@ -14,7 +14,7 @@ export function useUpdateWorkspace(id: string | null) {
}
const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v };
await invoke('update_workspace', { workspace: newWorkspace });
await invoke('cmd_update_workspace', { workspace: newWorkspace });
},
onMutate: async (v) => {
const workspace = await getWorkspace(id);

View File

@@ -11,7 +11,7 @@ export function useVariables({ environmentId }: { environmentId: string }) {
useQuery({
queryKey: variablesQueryKey({ environmentId }),
queryFn: async () => {
return (await invoke('list_variables', { environmentId })) as EnvironmentVariable[];
return (await invoke('cmd_list_variables', { environmentId })) as EnvironmentVariable[];
},
}).data ?? []
);

View File

@@ -10,7 +10,7 @@ export function workspacesQueryKey(_?: {}) {
export function useWorkspaces() {
return (
useQuery(workspacesQueryKey(), async () => {
return (await invoke('list_workspaces')) as Workspace[];
return (await invoke('cmd_list_workspaces')) as Workspace[];
}).data ?? []
);
}