diff --git a/src-web/components/GrpcConnectionLayout.tsx b/src-web/components/GrpcConnectionLayout.tsx index 5b17286e..c059cd9a 100644 --- a/src-web/components/GrpcConnectionLayout.tsx +++ b/src-web/components/GrpcConnectionLayout.tsx @@ -3,11 +3,11 @@ import classNames from 'classnames'; import { format } from 'date-fns'; import type { CSSProperties, FormEvent } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useActiveRequestId } from '../hooks/useActiveRequestId'; +import { useActiveRequest } from '../hooks/useActiveRequest'; import { useAlert } from '../hooks/useAlert'; import type { GrpcMessage } from '../hooks/useGrpc'; import { useGrpc } from '../hooks/useGrpc'; -import { useKeyValue } from '../hooks/useKeyValue'; +import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { Editor } from './core/Editor'; @@ -27,38 +27,20 @@ interface Props { } export function GrpcConnectionLayout({ style }: Props) { - const activeRequestId = useActiveRequestId(); - const url = useKeyValue({ - namespace: 'debug', - key: ['grpc_url', activeRequestId ?? ''], - defaultValue: '', - }); + const activeRequest = useActiveRequest('grpc_request'); + const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null); const alert = useAlert(); - const service = useKeyValue({ - namespace: 'debug', - key: ['grpc_service', activeRequestId ?? ''], - defaultValue: null, - }); - const method = useKeyValue({ - namespace: 'debug', - key: ['grpc_method', activeRequestId ?? ''], - defaultValue: null, - }); - const message = useKeyValue({ - namespace: 'debug', - key: ['grpc_message', activeRequestId ?? ''], - defaultValue: '', - }); const [activeMessage, setActiveMessage] = useState(null); const [resp, setResp] = useState(''); - const grpc = useGrpc(url.value ?? null, activeRequestId); + const grpc = useGrpc(activeRequest?.url ?? null, activeRequest?.id ?? null); const activeMethod = useMemo(() => { - if (grpc.services == null) return null; - const s = grpc.services.find((s) => s.name === service.value); + if (grpc.services == null || activeRequest == null) return null; + + const s = grpc.services.find((s) => s.name === activeRequest.service); if (s == null) return null; - return s.methods.find((m) => m.name === method.value); - }, [grpc.services, method.value, service.value]); + return s.methods.find((m) => m.name === activeRequest.method); + }, [activeRequest, grpc.services]); const handleCancel = useCallback(() => { grpc.cancel.mutateAsync().catch(console.error); @@ -67,9 +49,9 @@ export function GrpcConnectionLayout({ style }: Props) { const handleConnect = useCallback( async (e: FormEvent) => { e.preventDefault(); - if (activeMethod == null) return; + if (activeMethod == null || activeRequest == null) return; - if (service.value == null || method.value == null) { + if (activeRequest.service == null || activeRequest.method == null) { alert({ id: 'grpc-invalid-service-method', title: 'Error', @@ -77,63 +59,54 @@ export function GrpcConnectionLayout({ style }: Props) { }); } if (activeMethod.clientStreaming && activeMethod.serverStreaming) { - await grpc.bidiStreaming.mutateAsync({ - service: service.value ?? 'n/a', - method: method.value ?? 'n/a', - message: message.value ?? '', - }); + await grpc.bidiStreaming.mutateAsync(activeRequest); } else if (activeMethod.serverStreaming && !activeMethod.clientStreaming) { - await grpc.serverStreaming.mutateAsync({ - service: service.value ?? 'n/a', - method: method.value ?? 'n/a', - message: message.value ?? '', - }); + await grpc.serverStreaming.mutateAsync(activeRequest); } else { - setResp( - await grpc.unary.mutateAsync({ - service: service.value ?? 'n/a', - method: method.value ?? 'n/a', - message: message.value ?? '', - }), - ); + setResp(await grpc.unary.mutateAsync(activeRequest)); } }, - [ - activeMethod, - alert, - grpc.bidiStreaming, - grpc.serverStreaming, - grpc.unary, - message.value, - method.value, - service.value, - ], + [activeMethod, activeRequest, alert, grpc.bidiStreaming, grpc.serverStreaming, grpc.unary], ); useEffect(() => { - if (grpc.services == null) return; - const s = grpc.services.find((s) => s.name === service.value); + if (grpc.services == null || activeRequest == null) return; + const s = grpc.services.find((s) => s.name === activeRequest.service); if (s == null) { - service.set(grpc.services[0]?.name ?? null); - method.set(grpc.services[0]?.methods[0]?.name ?? null); + updateRequest.mutate({ + service: grpc.services[0]?.name ?? null, + method: grpc.services[0]?.methods[0]?.name ?? null, + }); return; } - const m = s.methods.find((m) => m.name === method.value); + const m = s.methods.find((m) => m.name === activeRequest.method); if (m == null) { - method.set(s.methods[0]?.name ?? null); + updateRequest.mutate({ method: s.methods[0]?.name ?? null }); return; } - }, [grpc.services, method, service]); + }, [activeRequest, grpc.services, updateRequest]); const handleChangeService = useCallback( - (v: string) => { + async (v: string) => { const [serviceName, methodName] = v.split('/', 2); if (serviceName == null || methodName == null) throw new Error('Should never happen'); - method.set(methodName); - service.set(serviceName); + await updateRequest.mutateAsync({ + service: serviceName, + method: methodName, + }); }, - [method, service], + [updateRequest], + ); + + const handleChangeUrl = useCallback( + (url: string) => updateRequest.mutateAsync({ url }), + [updateRequest], + ); + + const handleChangeMessage = useCallback( + (message: string) => updateRequest.mutateAsync({ message }), + [updateRequest], ); const select = useMemo(() => { @@ -144,9 +117,9 @@ export function GrpcConnectionLayout({ style }: Props) { value: `${s.name}/${m.name}`, })), ) ?? []; - const value = `${service.value ?? ''}/${method.value ?? ''}`; + const value = `${activeRequest?.service ?? ''}/${activeRequest?.method ?? ''}`; return { value, options }; - }, [grpc.services, method.value, service.value]); + }, [activeRequest?.method, activeRequest?.service, grpc.services]); const [paneSize, setPaneSize] = useState(99999); const urlContainerEl = useRef(null); @@ -154,8 +127,8 @@ export function GrpcConnectionLayout({ style }: Props) { setPaneSize(entry.contentRect.width); }); - if (url.isLoading || url.value == null) { - return null; + if (activeRequest == null) { + return; } return ( @@ -173,14 +146,14 @@ export function GrpcConnectionLayout({ style }: Props) { )} > grpc.send.mutateAsync({ message: message.value ?? '' })} + onClick={() => grpc.send.mutateAsync({ message: activeRequest.message ?? '' })} icon="sendHorizontal" /> )} - {!service.isLoading && !method.isLoading && ( - - )} + )} rightSlot={() => diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 3cfaa432..6cf1f13a 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -19,14 +19,15 @@ import { useDuplicateRequest } from '../hooks/useDuplicateRequest'; import { useFolders } from '../hooks/useFolders'; import { useGrpcRequests } from '../hooks/useGrpcRequests'; import { useHotKey } from '../hooks/useHotKey'; +import { useHttpRequests } from '../hooks/useHttpRequests'; import { useKeyValue } from '../hooks/useKeyValue'; import { useLatestResponse } from '../hooks/useLatestResponse'; import { usePrompt } from '../hooks/usePrompt'; -import { useHttpRequests } from '../hooks/useHttpRequests'; import { useSendManyRequests } from '../hooks/useSendFolder'; import { useSendRequest } from '../hooks/useSendRequest'; import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder'; +import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; import { fallbackRequestName } from '../lib/fallbackRequestName'; @@ -69,7 +70,8 @@ export function Sidebar({ className }: Props) { const [hasFocus, setHasFocus] = useState(false); const [selectedId, setSelectedId] = useState(null); const [selectedTree, setSelectedTree] = useState(null); - const updateAnyRequest = useUpdateAnyHttpRequest(); + const updateAnyHttpRequest = useUpdateAnyHttpRequest(); + const updateAnyGrpcRequest = useUpdateAnyGrpcRequest(); const updateAnyFolder = useUpdateAnyFolder(); const [draggingId, setDraggingId] = useState(null); const [hoveredTree, setHoveredTree] = useState(null); @@ -119,6 +121,7 @@ export function Sidebar({ className }: Props) { childItems.sort((a, b) => a.sortPriority - b.sortPriority); const depth = node.depth + 1; for (const item of childItems) { + console.log('ADD ITEM', item.id, item); treeParentMap[item.id] = node; node.children.push(next({ item, children: [], depth })); if (item.model !== 'folder') { @@ -342,12 +345,11 @@ export function Sidebar({ className }: Props) { const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId }); return updateAnyFolder.mutateAsync({ id: child.item.id, update: updateFolder }); } else if (child.item.model === 'grpc_request') { - // TODO - // const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId }); - // return updateAnyRequest.mutateAsync({ id: child.item.id, update: updateRequest }); + const updateRequest = (r: GrpcRequest) => ({ ...r, sortPriority, folderId }); + return updateAnyGrpcRequest.mutateAsync({ id: child.item.id, update: updateRequest }); } else if (child.item.model === 'http_request') { const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId }); - return updateAnyRequest.mutateAsync({ id: child.item.id, update: updateRequest }); + return updateAnyHttpRequest.mutateAsync({ id: child.item.id, update: updateRequest }); } }), ); @@ -357,23 +359,23 @@ export function Sidebar({ className }: Props) { const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId }); await updateAnyFolder.mutateAsync({ id: child.item.id, update: updateFolder }); } else if (child.item.model === 'grpc_request') { - // TODO - // const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId }); - // await updateAnyRequest.mutateAsync({ id: child.item.id, update: updateRequest }); + const updateRequest = (r: GrpcRequest) => ({ ...r, sortPriority, folderId }); + await updateAnyGrpcRequest.mutateAsync({ id: child.item.id, update: updateRequest }); } else if (child.item.model === 'http_request') { const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId }); - await updateAnyRequest.mutateAsync({ id: child.item.id, update: updateRequest }); + await updateAnyHttpRequest.mutateAsync({ id: child.item.id, update: updateRequest }); } } setDraggingId(null); }, [ - hoveredIndex, - hoveredTree, handleClearSelected, + hoveredTree, + hoveredIndex, treeParentMap, updateAnyFolder, - updateAnyRequest, + updateAnyGrpcRequest, + updateAnyHttpRequest, ], ); diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index b0b9b1e8..c8e1d061 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -34,7 +34,6 @@ export default function Workspace() { const { setWidth, width, resetWidth } = useSidebarWidth(); const { hide, show, hidden } = useSidebarHidden(); const activeRequest = useActiveRequest(); - console.log('ACTIVE REQUEST', activeRequest); const windowSize = useWindowSize(); const [floating, setFloating] = useState(false); diff --git a/src-web/hooks/useDeleteAnyRequest.tsx b/src-web/hooks/useDeleteAnyRequest.tsx index 58ba62c0..48df4266 100644 --- a/src-web/hooks/useDeleteAnyRequest.tsx +++ b/src-web/hooks/useDeleteAnyRequest.tsx @@ -4,7 +4,7 @@ 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 { httpRequestsQueryKey } from './useHttpRequests'; import { responsesQueryKey } from './useResponses'; @@ -15,7 +15,7 @@ export function useDeleteAnyRequest() { return useMutation({ mutationFn: async (id) => { - const request = await getRequest(id); + const request = await getHttpRequest(id); const confirmed = await confirm({ id: 'delete-request', title: 'Delete Request', diff --git a/src-web/hooks/useGrpc.ts b/src-web/hooks/useGrpc.ts index f10376d3..f8e833a2 100644 --- a/src-web/hooks/useGrpc.ts +++ b/src-web/hooks/useGrpc.ts @@ -4,6 +4,7 @@ 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 { useKeyValue } from './useKeyValue'; interface ReflectResponseService { @@ -31,9 +32,9 @@ export function useGrpc(url: string | null, requestId: string | null) { unlisten.current?.(); }, [requestId]); - const unary = useMutation({ + const unary = useMutation({ mutationKey: ['grpc_unary', url], - mutationFn: async ({ service, method, message }) => { + mutationFn: async ({ service, method, message, url }) => { if (url === null) throw new Error('No URL provided'); return (await invoke('cmd_grpc_call_unary', { endpoint: url, @@ -44,13 +45,9 @@ export function useGrpc(url: string | null, requestId: string | null) { }, }); - const serverStreaming = useMutation< - void, - string, - { service: string; method: string; message: string } - >({ + const serverStreaming = useMutation({ mutationKey: ['grpc_server_streaming', url], - mutationFn: async ({ service, method, message }) => { + mutationFn: async ({ service, method, message, url }) => { if (url === null) throw new Error('No URL provided'); await messages.set([ { @@ -79,13 +76,9 @@ export function useGrpc(url: string | null, requestId: string | null) { }, }); - const bidiStreaming = useMutation< - void, - string, - { service: string; method: string; message: string } - >({ + const bidiStreaming = useMutation({ mutationKey: ['grpc_bidi_streaming', url], - mutationFn: async ({ service, method, message }) => { + mutationFn: async ({ service, method, message, url }) => { if (url === null) throw new Error('No URL provided'); const id: string = await invoke('cmd_grpc_bidi_streaming', { endpoint: url, @@ -93,7 +86,7 @@ export function useGrpc(url: string | null, requestId: string | null) { method, message, }); - messages.set([ + await messages.set([ { type: 'info', message: `Started connection ${id}`, timestamp: new Date().toISOString() }, ]); setActiveConnectionId(id); diff --git a/src-web/hooks/useSendAnyRequest.ts b/src-web/hooks/useSendAnyRequest.ts index 9ff23bc8..67dd75f7 100644 --- a/src-web/hooks/useSendAnyRequest.ts +++ b/src-web/hooks/useSendAnyRequest.ts @@ -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({ mutationFn: async (id) => { - const request = await getRequest(id); + const request = await getHttpRequest(id); if (request == null) { return null; } diff --git a/src-web/hooks/useUpdateAnyGrpcRequest.ts b/src-web/hooks/useUpdateAnyGrpcRequest.ts new file mode 100644 index 00000000..2a5c630c --- /dev/null +++ b/src-web/hooks/useUpdateAnyGrpcRequest.ts @@ -0,0 +1,35 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { GrpcRequest } from '../lib/models'; +import { getGrpcRequest } from '../lib/store'; +import { grpcRequestsQueryKey } from './useGrpcRequests'; + +export function useUpdateAnyGrpcRequest() { + const queryClient = useQueryClient(); + + return useMutation< + void, + unknown, + { id: string; update: Partial | ((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(grpcRequestsQueryKey(request), (requests) => + (requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)), + ); + }, + }); +} diff --git a/src-web/hooks/useUpdateAnyHttpRequest.ts b/src-web/hooks/useUpdateAnyHttpRequest.ts index a18bddc3..08425fa0 100644 --- a/src-web/hooks/useUpdateAnyHttpRequest.ts +++ b/src-web/hooks/useUpdateAnyHttpRequest.ts @@ -1,7 +1,7 @@ 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 { getHttpRequest } from '../lib/store'; import { httpRequestsQueryKey } from './useHttpRequests'; export function useUpdateAnyHttpRequest() { @@ -13,7 +13,7 @@ export function useUpdateAnyHttpRequest() { { id: string; update: Partial | ((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"); } @@ -23,7 +23,7 @@ export function useUpdateAnyHttpRequest() { 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 }; diff --git a/src-web/hooks/useUpdateGrpcRequest.ts b/src-web/hooks/useUpdateGrpcRequest.ts new file mode 100644 index 00000000..da8bd89a --- /dev/null +++ b/src-web/hooks/useUpdateGrpcRequest.ts @@ -0,0 +1,10 @@ +import { useMutation } from '@tanstack/react-query'; +import type { GrpcRequest } from '../lib/models'; +import { useUpdateAnyGrpcRequest } from './useUpdateAnyGrpcRequest'; + +export function useUpdateGrpcRequest(id: string | null) { + const updateAnyRequest = useUpdateAnyGrpcRequest(); + return useMutation | ((r: GrpcRequest) => GrpcRequest)>({ + mutationFn: async (update) => updateAnyRequest.mutateAsync({ id: id ?? 'n/a', update }), + }); +} diff --git a/src-web/lib/store.ts b/src-web/lib/store.ts index 97bc437f..a8967b6b 100644 --- a/src-web/lib/store.ts +++ b/src-web/lib/store.ts @@ -1,11 +1,28 @@ import { invoke } from '@tauri-apps/api'; -import type { CookieJar, Environment, Folder, HttpRequest, Settings, Workspace } from './models'; +import type { + CookieJar, + Environment, + Folder, + GrpcRequest, + HttpRequest, + Settings, + Workspace, +} from './models'; export async function getSettings(): Promise { return invoke('cmd_get_settings', {}); } -export async function getRequest(id: string | null): Promise { +export async function getGrpcRequest(id: string | null): Promise { + if (id === null) return null; + const request: GrpcRequest = (await invoke('cmd_get_grpc_request', { id })) ?? null; + if (request == null) { + return null; + } + return request; +} + +export async function getHttpRequest(id: string | null): Promise { if (id === null) return null; const request: HttpRequest = (await invoke('cmd_get_http_request', { id })) ?? null; if (request == null) {