Use req/conn/msg models in unary/server

This commit is contained in:
Gregory Schier
2024-02-04 11:57:12 -08:00
parent 3a340999ec
commit 1abba4980a
27 changed files with 497 additions and 233 deletions

View File

@@ -3,20 +3,23 @@ import { appWindow } from '@tauri-apps/api/window';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcMessagesQueryKey } from '../hooks/useGrpcMessages';
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { responsesQueryKey } from '../hooks/useResponses';
import { settingsQueryKey } from '../hooks/useSettings';
import { useSyncAppearance } from '../hooks/useSyncAppearance';
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
import type { Model } from '../lib/models';
import { modelsEq } from '../lib/models';
import { setPathname } from '../lib/persistPathname';
@@ -49,7 +52,13 @@ export function GlobalHooks() {
payload.model === 'http_request'
? httpRequestsQueryKey(payload)
: payload.model === 'http_response'
? responsesQueryKey(payload)
? httpResponsesQueryKey(payload)
: payload.model === 'grpc_connection'
? grpcConnectionsQueryKey(payload)
: payload.model === 'grpc_message'
? grpcMessagesQueryKey(payload)
: payload.model === 'grpc_request'
? grpcRequestsQueryKey(payload)
: payload.model === 'workspace'
? workspacesQueryKey(payload)
: payload.model === 'key_value'
@@ -78,7 +87,13 @@ export function GlobalHooks() {
payload.model === 'http_request'
? httpRequestsQueryKey(payload)
: payload.model === 'http_response'
? responsesQueryKey(payload)
? httpResponsesQueryKey(payload)
: payload.model === 'grpc_connection'
? grpcConnectionsQueryKey(payload)
: payload.model === 'grpc_message'
? grpcMessagesQueryKey(payload)
: payload.model === 'grpc_request'
? grpcRequestsQueryKey(payload)
: payload.model === 'workspace'
? workspacesQueryKey(payload)
: payload.model === 'key_value'
@@ -113,11 +128,17 @@ export function GlobalHooks() {
if (shouldIgnoreModel(payload)) return;
if (payload.model === 'workspace') {
queryClient.setQueryData<Workspace[]>(workspacesQueryKey(), removeById(payload));
queryClient.setQueryData(workspacesQueryKey(), removeById(payload));
} else if (payload.model === 'http_request') {
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey(payload), removeById(payload));
queryClient.setQueryData(httpRequestsQueryKey(payload), removeById(payload));
} else if (payload.model === 'http_response') {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
queryClient.setQueryData(httpResponsesQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_request') {
queryClient.setQueryData(grpcRequestsQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_connection') {
queryClient.setQueryData(grpcConnectionsQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_message') {
queryClient.setQueryData(grpcMessagesQueryKey(payload), removeById(payload));
} else if (payload.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
} else if (payload.model === 'cookie_jar') {

View File

@@ -5,12 +5,12 @@ import type { CSSProperties, FormEvent } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useAlert } from '../hooks/useAlert';
import type { GrpcMessage } from '../hooks/useGrpc';
import { useGrpc } from '../hooks/useGrpc';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useGrpcMessages } from '../hooks/useGrpcMessages';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { Editor } from './core/Editor';
import { HotKeyList } from './core/HotKeyList';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
@@ -30,9 +30,11 @@ export function GrpcConnectionLayout({ style }: Props) {
const activeRequest = useActiveRequest('grpc_request');
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
const alert = useAlert();
const [activeMessage, setActiveMessage] = useState<GrpcMessage | null>(null);
const [resp, setResp] = useState<string>('');
const [activeMessageId, setActiveMessageId] = useState<string | null>(null);
const grpc = useGrpc(activeRequest?.url ?? null, activeRequest?.id ?? null);
const connections = useGrpcConnections(activeRequest?.id ?? null);
const activeConnection = connections[0] ?? null;
const messages = useGrpcMessages(activeConnection?.id ?? null);
const activeMethod = useMemo(() => {
if (grpc.services == null || activeRequest == null) return null;
@@ -61,9 +63,9 @@ export function GrpcConnectionLayout({ style }: Props) {
if (activeMethod.clientStreaming && activeMethod.serverStreaming) {
await grpc.bidiStreaming.mutateAsync(activeRequest);
} else if (activeMethod.serverStreaming && !activeMethod.clientStreaming) {
await grpc.serverStreaming.mutateAsync(activeRequest);
await grpc.serverStreaming.mutateAsync(activeRequest.id);
} else {
setResp(await grpc.unary.mutateAsync(activeRequest));
await grpc.unary.mutateAsync(activeRequest.id);
}
},
[activeMethod, activeRequest, alert, grpc.bidiStreaming, grpc.serverStreaming, grpc.unary],
@@ -127,8 +129,13 @@ export function GrpcConnectionLayout({ style }: Props) {
setPaneSize(entry.contentRect.width);
});
const activeMessage = useMemo(
() => messages.find((m) => m.id === activeMessageId) ?? null,
[activeMessageId, messages],
);
if (activeRequest == null) {
return;
return null;
}
return (
@@ -136,7 +143,7 @@ export function GrpcConnectionLayout({ style }: Props) {
name="grpc_layout"
className="p-3 gap-1.5"
style={style}
leftSlot={() => (
firstSlot={() => (
<VStack space={2}>
<div
ref={urlContainerEl}
@@ -218,7 +225,7 @@ export function GrpcConnectionLayout({ style }: Props) {
/>
</VStack>
)}
rightSlot={() =>
secondSlot={() =>
!grpc.unary.isLoading && (
<div
className={classNames(
@@ -231,20 +238,20 @@ export function GrpcConnectionLayout({ style }: Props) {
<Banner color="danger" className="m-2">
{grpc.unary.error}
</Banner>
) : (grpc.messages.value ?? []).length > 0 ? (
) : messages.length >= 0 ? (
<SplitLayout
name="grpc_messages2"
name="grpc_messages"
minHeightPx={20}
defaultRatio={0.25}
leftSlot={() => (
firstSlot={() => (
<div className="overflow-y-auto">
{...(grpc.messages.value ?? []).map((m, i) => (
{...messages.map((m) => (
<HStack
key={`${m.timestamp}::${m.message}::${i}`}
key={m.id}
space={2}
onClick={() => {
if (m === activeMessage) setActiveMessage(null);
else setActiveMessage(m);
if (m.id === activeMessageId) setActiveMessageId(null);
else setActiveMessageId(m.id);
}}
alignItems="center"
className={classNames(
@@ -254,29 +261,25 @@ export function GrpcConnectionLayout({ style }: Props) {
>
<Icon
className={
m.type === 'server'
m.isInfo
? 'text-gray-600'
: m.isServer
? 'text-blue-600'
: m.type === 'client'
? 'text-green-600'
: 'text-gray-600'
: 'text-green-600'
}
icon={
m.type === 'server'
? 'arrowBigDownDash'
: m.type === 'client'
? 'arrowBigUpDash'
: 'info'
m.isInfo ? 'info' : m.isServer ? 'arrowBigDownDash' : 'arrowBigUpDash'
}
/>
<div className="w-full truncate text-gray-800 text-2xs">{m.message}</div>
<div className="text-gray-600 text-2xs">
{format(m.timestamp, 'HH:mm:ss')}
{format(m.createdAt, 'HH:mm:ss')}
</div>
</HStack>
))}
</div>
)}
rightSlot={
secondSlot={
!activeMessage
? null
: () => (
@@ -293,15 +296,15 @@ export function GrpcConnectionLayout({ style }: Props) {
)
}
/>
) : resp ? (
<Editor
className="bg-gray-50 dark:bg-gray-100"
contentType="application/json"
defaultValue={resp}
readOnly
forceUpdateKey={resp}
/>
) : (
// ) : ? (
// <Editor
// readOnly
// className="bg-gray-50 dark:bg-gray-100"
// contentType="application/json"
// defaultValue={resp.message}
// forceUpdateKey={resp.id}
// />
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.toggle', 'urlBar.focus']} />
)}
</div>

View File

@@ -14,10 +14,10 @@ export function HttpRequestLayout({ style }: Props) {
name="http_layout"
className="p-3 gap-1.5"
style={style}
leftSlot={({ orientation, style }) => (
firstSlot={({ orientation, style }) => (
<RequestPane style={style} fullHeight={orientation === 'horizontal'} />
)}
rightSlot={({ style }) => <ResponsePane style={style} />}
secondSlot={({ style }) => <ResponsePane style={style} />}
/>
);
}

View File

@@ -5,7 +5,7 @@ import { createGlobalState } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useLatestResponse } from '../hooks/useLatestResponse';
import { useResponseContentType } from '../hooks/useResponseContentType';
import { useResponses } from '../hooks/useResponses';
import { useHttpResponses } from '../hooks/useHttpResponses';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
import type { HttpResponse } from '../lib/models';
import { isResponseLoading } from '../lib/models';
@@ -39,7 +39,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
const activeRequest = useActiveRequest();
const latestResponse = useLatestResponse(activeRequest?.id ?? null);
const responses = useResponses(activeRequest?.id ?? null);
const responses = useHttpResponses(activeRequest?.id ?? null);
const activeResponse: HttpResponse | null = pinnedResponseId
? responses.find((r) => r.id === pinnedResponseId) ?? null
: latestResponse ?? null;

View File

@@ -7,7 +7,6 @@ import { useKey, useKeyPressEvent } from 'react-use';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateFolder } from '../hooks/useCreateFolder';
@@ -15,7 +14,8 @@ import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useFolders } from '../hooks/useFolders';
import { useGrpcRequests } from '../hooks/useGrpcRequests';
import { useHotKey } from '../hooks/useHotKey';
@@ -29,6 +29,7 @@ import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
@@ -58,14 +59,21 @@ interface TreeNode {
export function Sidebar({ className }: Props) {
const { hidden } = useSidebarHidden();
const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequestId = useActiveRequestId();
const activeRequest = useActiveRequest();
const activeEnvironmentId = useActiveEnvironmentId();
const httpRequests = useHttpRequests();
const grpcRequests = useGrpcRequests();
const folders = useFolders();
const deleteAnyRequest = useDeleteAnyRequest();
const activeWorkspace = useActiveWorkspace();
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true });
const duplicateHttpRequest = useDuplicateHttpRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const duplicateGrpcRequest = useDuplicateGrpcRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const routes = useAppRoutes();
const [hasFocus, setHasFocus] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -82,8 +90,12 @@ export function Sidebar({ className }: Props) {
namespace: NAMESPACE_NO_SYNC,
});
useHotKey('http_request.duplicate', () => {
duplicateRequest.mutate();
useHotKey('http_request.duplicate', async () => {
if (activeRequest?.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {
await duplicateGrpcRequest.mutateAsync();
}
});
const isCollapsed = useCallback(
@@ -146,9 +158,10 @@ export function Sidebar({ className }: Props) {
} = {},
) => {
const { forced, noFocusSidebar } = args;
const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null;
const tree = forced?.tree ?? treeParentMap[activeRequest?.id ?? 'n/a'] ?? null;
const children = tree?.children ?? [];
const id = forced?.id ?? children.find((m) => m.item.id === activeRequestId)?.item.id ?? null;
const id =
forced?.id ?? children.find((m) => m.item.id === activeRequest?.id)?.item.id ?? null;
if (id == null) {
return;
}
@@ -160,7 +173,7 @@ export function Sidebar({ className }: Props) {
sidebarRef.current?.focus();
}
},
[activeRequestId, treeParentMap],
[activeRequest, treeParentMap],
);
const handleSelect = useCallback(
@@ -230,7 +243,7 @@ export function Sidebar({ className }: Props) {
useKeyPressEvent('Enter', (e) => {
if (!hasFocus) return;
const selected = selectableRequests.find((r) => r.id === selectedId);
if (!selected || selected.id === activeRequestId || activeWorkspace == null) {
if (!selected || selected.id === activeRequest?.id || activeWorkspace == null) {
return;
}
@@ -541,11 +554,13 @@ const SidebarItem = forwardRef(function SidebarItem(
const createFolder = useCreateFolder();
const deleteFolder = useDeleteFolder(itemId);
const deleteRequest = useDeleteRequest(itemId);
const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true });
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const sendRequest = useSendRequest(itemId);
const sendManyRequests = useSendManyRequests();
const latestResponse = useLatestResponse(itemId);
const updateRequest = useUpdateHttpRequest(itemId);
const updateHttpRequest = useUpdateHttpRequest(itemId);
const updateGrpcRequest = useUpdateGrpcRequest(itemId);
const updateAnyFolder = useUpdateAnyFolder();
const prompt = usePrompt();
const [editing, setEditing] = useState<boolean>(false);
@@ -553,10 +568,15 @@ const SidebarItem = forwardRef(function SidebarItem(
const handleSubmitNameEdit = useCallback(
(el: HTMLInputElement) => {
updateRequest.mutate((r) => ({ ...r, name: el.value }));
if (activeRequest == null) return;
if (activeRequest.model === 'http_request') {
updateHttpRequest.mutate((r) => ({ ...r, name: el.value }));
} else if (activeRequest.model === 'grpc_request') {
updateGrpcRequest.mutate((r) => ({ ...r, name: el.value }));
}
setEditing(false);
},
[updateRequest],
[activeRequest, updateGrpcRequest, updateHttpRequest],
);
const handleFocus = useCallback((el: HTMLInputElement | null) => {
@@ -677,7 +697,9 @@ const SidebarItem = forwardRef(function SidebarItem(
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />,
onSelect: () => {
duplicateRequest.mutate();
itemModel === 'http_request'
? duplicateHttpRequest.mutate()
: duplicateGrpcRequest.mutate();
},
},
{

View File

@@ -34,7 +34,6 @@ export default function Workspace() {
const { setWidth, width, resetWidth } = useSidebarWidth();
const { hide, show, hidden } = useSidebarHidden();
const activeRequest = useActiveRequest();
const windowSize = useWindowSize();
const [floating, setFloating] = useState<boolean>(false);
const [isResizing, setIsResizing] = useState<boolean>(false);
@@ -47,7 +46,7 @@ export default function Workspace() {
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
if (shouldHide && !floating) {
setFloating(true);
hide();
hide().catch(console.error);
} else if (!shouldHide && floating) {
setFloating(false);
}
@@ -72,10 +71,10 @@ export default function Workspace() {
e.preventDefault(); // Prevent text selection and things
const newWidth = startWidth + (e.clientX - mouseStartX);
if (newWidth < 100) {
hide();
await hide();
resetWidth();
} else {
show();
await show();
setWidth(newWidth);
}
},

View File

@@ -16,8 +16,8 @@ interface SlotProps {
interface Props {
name: string;
leftSlot: (props: SlotProps) => ReactNode;
rightSlot: null | ((props: SlotProps) => ReactNode);
firstSlot: (props: SlotProps) => ReactNode;
secondSlot: null | ((props: SlotProps) => ReactNode);
style?: CSSProperties;
className?: string;
defaultRatio?: number;
@@ -33,8 +33,8 @@ const STACK_VERTICAL_WIDTH = 700;
export function SplitLayout({
style,
leftSlot,
rightSlot,
firstSlot,
secondSlot,
className,
name,
defaultRatio = 0.5,
@@ -54,7 +54,7 @@ export function SplitLayout({
null,
);
if (!rightSlot) {
if (!secondSlot) {
height = 0;
minHeightPx = 0;
}
@@ -145,8 +145,8 @@ export function SplitLayout({
return (
<div ref={containerRef} className={classNames(className, 'grid w-full h-full')} style={styles}>
{leftSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
{rightSlot && (
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
{secondSlot && (
<>
<ResizeHandle
style={areaD}
@@ -158,7 +158,7 @@ export function SplitLayout({
side={vertical ? 'top' : 'left'}
justify="center"
/>
{rightSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
{secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
</>
)}
</div>

View File

@@ -7,7 +7,7 @@ import type { HttpRequest } from '../lib/models';
import { getHttpRequest } from '../lib/store';
import { useConfirm } from './useConfirm';
import { httpRequestsQueryKey } from './useHttpRequests';
import { responsesQueryKey } from './useResponses';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteAnyRequest() {
const queryClient = useQueryClient();
@@ -35,7 +35,7 @@ export function useDeleteAnyRequest() {
if (request === null) return;
const { workspaceId, id: requestId } = request;
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId),
);

View File

@@ -2,7 +2,7 @@ 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) {
const queryClient = useQueryClient();
@@ -12,7 +12,7 @@ export function useDeleteResponse(id: string | null) {
},
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,7 +1,7 @@
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) {
const queryClient = useQueryClient();
@@ -13,7 +13,7 @@ export function useDeleteResponses(requestId?: string) {
onSettled: () => trackEvent('HttpResponse', 'DeleteMany'),
onSuccess: async () => {
if (requestId === undefined) return;
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []);
},
});
}

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

@@ -7,7 +7,7 @@ import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { httpRequestsQueryKey } from './useHttpRequests';
export function useDuplicateRequest({
export function useDuplicateHttpRequest({
id,
navigateAfter,
}: {

View File

@@ -3,8 +3,7 @@ import { invoke } from '@tauri-apps/api';
import type { UnlistenFn } from '@tauri-apps/api/event';
import { emit, listen } from '@tauri-apps/api/event';
import { useEffect, useRef, useState } from 'react';
import { tryFormatJson } from '../lib/formatters';
import type { GrpcRequest } from '../lib/models';
import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models';
import { useKeyValue } from './useKeyValue';
interface ReflectResponseService {
@@ -12,12 +11,6 @@ interface ReflectResponseService {
methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[];
}
export interface GrpcMessage {
message: string;
timestamp: string;
type: 'server' | 'client' | 'info';
}
export function useGrpc(url: string | null, requestId: string | null) {
const messages = useKeyValue<GrpcMessage[]>({
namespace: 'debug',
@@ -25,54 +18,30 @@ export function useGrpc(url: string | null, requestId: string | null) {
defaultValue: [],
});
const [activeConnectionId, setActiveConnectionId] = useState<string | null>(null);
const unlisten = useRef<UnlistenFn | null>(null);
useEffect(() => {
setActiveConnectionId(null);
unlisten.current?.();
}, [requestId]);
const unary = useMutation<string, string, GrpcRequest>({
const unary = useMutation<GrpcMessage, string, string>({
mutationKey: ['grpc_unary', url],
mutationFn: async ({ service, method, message, url }) => {
if (url === null) throw new Error('No URL provided');
return (await invoke('cmd_grpc_call_unary', {
endpoint: url,
service,
method,
message,
})) as string;
mutationFn: async (id) => {
const message = (await invoke('cmd_grpc_call_unary', {
requestId: id,
})) as GrpcMessage;
await messages.set([message]);
console.log('MESSAGE', message);
return message;
},
});
const serverStreaming = useMutation<void, string, GrpcRequest>({
const serverStreaming = useMutation<void, string, string>({
mutationKey: ['grpc_server_streaming', url],
mutationFn: async ({ service, method, message, url }) => {
mutationFn: async (requestId) => {
if (url === null) throw new Error('No URL provided');
await messages.set([
{
type: 'client',
message: JSON.stringify(JSON.parse(message)),
timestamp: new Date().toISOString(),
},
]);
const id: string = await invoke('cmd_grpc_server_streaming', {
endpoint: url,
service,
method,
message,
});
unlisten.current = await listen(`grpc_server_msg_${id}`, async (event) => {
await messages.set((prev) => [
...prev,
{
message: tryFormatJson(event.payload as string, false),
timestamp: new Date().toISOString(),
type: 'server',
},
]);
});
setActiveConnectionId(id);
await messages.set([]);
const c = (await invoke('cmd_grpc_server_streaming', { requestId })) as GrpcConnection;
setActiveConnectionId(c.id);
},
});
@@ -86,20 +55,8 @@ export function useGrpc(url: string | null, requestId: string | null) {
method,
message,
});
await messages.set([
{ type: 'info', message: `Started connection ${id}`, timestamp: new Date().toISOString() },
]);
await messages.set([]);
setActiveConnectionId(id);
unlisten.current = await listen(`grpc_server_msg_${id}`, (event) => {
messages.set((prev) => [
...prev,
{
message: tryFormatJson(event.payload as string, false),
timestamp: new Date().toISOString(),
type: 'server',
},
]);
});
},
});
@@ -107,9 +64,10 @@ export function useGrpc(url: string | null, requestId: string | null) {
mutationKey: ['grpc_send', url],
mutationFn: async ({ message }: { message: string }) => {
if (activeConnectionId == null) throw new Error('No active connection');
await messages.set((m) => {
return [...m, { type: 'client', message, timestamp: new Date().toISOString() }];
});
await messages.set([]);
// await messages.set((m) => {
// return [...m, { type: 'client', message, timestamp: new Date().toISOString() }];
// });
await emit(`grpc_client_msg_${activeConnectionId}`, { Message: message });
},
});
@@ -118,12 +76,7 @@ export function useGrpc(url: string | null, requestId: string | null) {
mutationKey: ['grpc_cancel', url],
mutationFn: async () => {
setActiveConnectionId(null);
unlisten.current?.();
await emit('grpc_message_in', 'Cancel');
await messages.set((m) => [
...m,
{ type: 'info', message: 'Cancelled by client', timestamp: new Date().toISOString() },
]);
await emit(`grpc_client_msg_${activeConnectionId}`, 'Cancel');
},
});
@@ -141,7 +94,6 @@ export function useGrpc(url: string | null, requestId: string | null) {
bidiStreaming,
services: reflect.data,
cancel,
messages,
isStreaming: activeConnectionId !== null,
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

@@ -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('cmd_list_responses', { requestId, limit: 200 })) as HttpResponse[];
return (await invoke('cmd_list_http_responses', { requestId, limit: 200 })) as HttpResponse[];
},
}).data ?? []
);

View File

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

View File

@@ -12,7 +12,9 @@ export const AUTH_TYPE_BEARER = 'bearer';
export type Model =
| Settings
| Workspace
| GrpcConnection
| GrpcRequest
| GrpcMessage
| HttpRequest
| HttpResponse
| KeyValue
@@ -114,6 +116,24 @@ export interface GrpcRequest extends BaseModel {
message: string;
}
export interface GrpcMessage extends BaseModel {
readonly workspaceId: string;
readonly requestId: string;
readonly connectionId: string;
readonly model: 'grpc_message';
message: string;
isServer: boolean;
isInfo: boolean;
}
export interface GrpcConnection extends BaseModel {
readonly workspaceId: string;
readonly requestId: string;
readonly model: 'grpc_connection';
service: string;
method: string;
}
export interface HttpRequest extends BaseModel {
readonly workspaceId: string;
readonly model: 'http_request';