mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-18 07:23:51 +01:00
Refactor model emit, and recent conn dropdown
This commit is contained in:
@@ -9,6 +9,7 @@ import { useGrpc } from '../hooks/useGrpc';
|
||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||
import { useGrpcMessages } from '../hooks/useGrpcMessages';
|
||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||
import { count, pluralize } from '../lib/pluralize';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
@@ -20,6 +21,7 @@ import { Separator } from './core/Separator';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { GrpcEditor } from './GrpcEditor';
|
||||
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
|
||||
import { UrlBar } from './UrlBar';
|
||||
|
||||
interface Props {
|
||||
@@ -266,7 +268,7 @@ export function GrpcConnectionLayout({ style }: Props) {
|
||||
className={classNames(
|
||||
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1',
|
||||
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight',
|
||||
'shadow shadow-gray-100 dark:shadow-gray-0 relative pt-1',
|
||||
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
|
||||
)}
|
||||
>
|
||||
{grpc.unary.error ? (
|
||||
@@ -286,18 +288,23 @@ export function GrpcConnectionLayout({ style }: Props) {
|
||||
}
|
||||
minHeightPx={20}
|
||||
firstSlot={() => (
|
||||
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<HStack className="px-3 mb-2">
|
||||
<div className="font-mono">
|
||||
{grpc.isStreaming ? (
|
||||
<HStack alignItems="center" space={2}>
|
||||
<Icon icon="refresh" size="sm" spin />
|
||||
Connected
|
||||
</HStack>
|
||||
) : (
|
||||
'Done'
|
||||
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
|
||||
<HStack className="pl-3 mb-1 font-mono" alignItems="center">
|
||||
<HStack alignItems="center" space={2}>
|
||||
{count('message', messages.filter((m) => !m.isInfo).length)}
|
||||
{grpc.isStreaming && (
|
||||
<Icon icon="refresh" size="sm" spin className="text-gray-600" />
|
||||
)}
|
||||
</div>
|
||||
</HStack>
|
||||
{activeConnection && (
|
||||
<RecentConnectionsDropdown
|
||||
connections={connections}
|
||||
activeConnection={activeConnection}
|
||||
onPinned={() => {
|
||||
// todo
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
<div className="overflow-y-auto h-full">
|
||||
{...messages.map((m) => (
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import React from 'react';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { RequestPane } from './RequestPane';
|
||||
import { ResponsePane } from './ResponsePane';
|
||||
|
||||
interface Props {
|
||||
activeRequest: HttpRequest;
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
export function HttpRequestLayout({ style }: Props) {
|
||||
export function HttpRequestLayout({ activeRequest, style }: Props) {
|
||||
return (
|
||||
<SplitLayout
|
||||
name="http_layout"
|
||||
className="p-3 gap-1.5"
|
||||
style={style}
|
||||
firstSlot={({ orientation, style }) => (
|
||||
<RequestPane style={style} fullHeight={orientation === 'horizontal'} />
|
||||
<RequestPane
|
||||
style={style}
|
||||
activeRequest={activeRequest}
|
||||
fullHeight={orientation === 'horizontal'}
|
||||
/>
|
||||
)}
|
||||
secondSlot={({ style }) => <ResponsePane style={style} />}
|
||||
secondSlot={({ style }) => <ResponsePane activeRequest={activeRequest} style={style} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
62
src-web/components/RecentConnectionsDropdown.tsx
Normal file
62
src-web/components/RecentConnectionsDropdown.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useDeleteGrpcConnection } from '../hooks/useDeleteGrpcConnection';
|
||||
import { useDeleteGrpcConnections } from '../hooks/useDeleteGrpcConnections';
|
||||
import type { GrpcConnection } from '../lib/models';
|
||||
import { pluralize } from '../lib/pluralize';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
connections: GrpcConnection[];
|
||||
activeConnection: GrpcConnection;
|
||||
onPinned: (r: GrpcConnection) => void;
|
||||
}
|
||||
|
||||
export const RecentConnectionsDropdown = function ResponsePane({
|
||||
activeConnection,
|
||||
connections,
|
||||
onPinned,
|
||||
}: Props) {
|
||||
const deleteResponse = useDeleteGrpcConnection(activeConnection?.id ?? null);
|
||||
const deleteAllResponses = useDeleteGrpcConnections(activeConnection?.requestId);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
key: 'clear-single',
|
||||
label: 'Clear Response',
|
||||
onSelect: deleteResponse.mutate,
|
||||
disabled: connections.length === 0,
|
||||
},
|
||||
{
|
||||
key: 'clear-all',
|
||||
label: `Clear ${connections.length} ${pluralize('Response', connections.length)}`,
|
||||
onSelect: deleteAllResponses.mutate,
|
||||
hidden: connections.length <= 1,
|
||||
disabled: connections.length === 0,
|
||||
},
|
||||
{ type: 'separator', label: 'History' },
|
||||
...connections.slice(0, 20).map((c) => ({
|
||||
key: c.id,
|
||||
label: (
|
||||
<HStack space={2} alignItems="center">
|
||||
<span className="font-mono text-xs">{c.elapsed}ms</span>
|
||||
</HStack>
|
||||
),
|
||||
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
||||
onSelect: () => onPinned(c),
|
||||
})),
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
title="Show response history"
|
||||
icon="chevronDown"
|
||||
className="ml-auto"
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useDeleteResponse } from '../hooks/useDeleteResponse';
|
||||
import { useDeleteResponses } from '../hooks/useDeleteResponses';
|
||||
import { useDeleteHttpResponse } from '../hooks/useDeleteHttpResponse';
|
||||
import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { pluralize } from '../lib/pluralize';
|
||||
@@ -19,8 +19,8 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
responses,
|
||||
onPinnedResponse,
|
||||
}: Props) {
|
||||
const deleteResponse = useDeleteResponse(activeResponse?.id ?? null);
|
||||
const deleteAllResponses = useDeleteResponses(activeResponse?.requestId);
|
||||
const deleteResponse = useDeleteHttpResponse(activeResponse?.id ?? null);
|
||||
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
|
||||
@@ -2,7 +2,6 @@ import classNames from 'classnames';
|
||||
import type { CSSProperties, FormEvent } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
@@ -38,128 +37,128 @@ interface Props {
|
||||
style: CSSProperties;
|
||||
fullHeight: boolean;
|
||||
className?: string;
|
||||
activeRequest: HttpRequest;
|
||||
}
|
||||
|
||||
const useActiveTab = createGlobalState<string>('body');
|
||||
|
||||
export const RequestPane = memo(function RequestPane({ style, fullHeight, className }: Props) {
|
||||
const activeRequest = useActiveRequest('http_request');
|
||||
const activeRequestId = activeRequest?.id ?? null;
|
||||
export const RequestPane = memo(function RequestPane({
|
||||
style,
|
||||
fullHeight,
|
||||
className,
|
||||
activeRequest,
|
||||
}: Props) {
|
||||
const activeRequestId = activeRequest.id;
|
||||
const updateRequest = useUpdateHttpRequest(activeRequestId);
|
||||
const [activeTab, setActiveTab] = useActiveTab();
|
||||
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest?.id ?? null);
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() =>
|
||||
activeRequest === null
|
||||
? []
|
||||
: [
|
||||
{
|
||||
value: 'body',
|
||||
options: {
|
||||
value: activeRequest.bodyType,
|
||||
items: [
|
||||
{ type: 'separator', label: 'Form Data' },
|
||||
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
|
||||
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
|
||||
{ type: 'separator', label: 'Text Content' },
|
||||
{ label: 'JSON', value: BODY_TYPE_JSON },
|
||||
{ label: 'XML', value: BODY_TYPE_XML },
|
||||
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
||||
{ type: 'separator', label: 'Other' },
|
||||
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
|
||||
],
|
||||
onChange: async (bodyType) => {
|
||||
const patch: Partial<HttpRequest> = { bodyType };
|
||||
if (bodyType === BODY_TYPE_NONE) {
|
||||
patch.headers = activeRequest?.headers.filter(
|
||||
(h) => h.name.toLowerCase() !== 'content-type',
|
||||
);
|
||||
} else if (
|
||||
bodyType === BODY_TYPE_FORM_URLENCODED ||
|
||||
bodyType === BODY_TYPE_FORM_MULTIPART ||
|
||||
bodyType === BODY_TYPE_JSON ||
|
||||
bodyType === BODY_TYPE_XML
|
||||
) {
|
||||
patch.method = 'POST';
|
||||
patch.headers = [
|
||||
...(activeRequest?.headers.filter(
|
||||
(h) => h.name.toLowerCase() !== 'content-type',
|
||||
) ?? []),
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: bodyType,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
} else if (bodyType == BODY_TYPE_GRAPHQL) {
|
||||
patch.method = 'POST';
|
||||
patch.headers = [
|
||||
...(activeRequest?.headers.filter(
|
||||
(h) => h.name.toLowerCase() !== 'content-type',
|
||||
) ?? []),
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Force update header editor so any changed headers are reflected
|
||||
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
||||
|
||||
updateRequest.mutate(patch);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'params',
|
||||
label: (
|
||||
<div className="flex items-center">
|
||||
Params
|
||||
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'headers',
|
||||
label: (
|
||||
<div className="flex items-center">
|
||||
Headers
|
||||
<CountBadge count={activeRequest.headers.filter((h) => h.name).length} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'auth',
|
||||
label: 'Auth',
|
||||
options: {
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
{ label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
|
||||
{ label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER },
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
|
||||
],
|
||||
onChange: async (authenticationType) => {
|
||||
let authentication: HttpRequest['authentication'] = activeRequest?.authentication;
|
||||
if (authenticationType === AUTH_TYPE_BASIC) {
|
||||
authentication = {
|
||||
username: authentication.username ?? '',
|
||||
password: authentication.password ?? '',
|
||||
};
|
||||
} else if (authenticationType === AUTH_TYPE_BEARER) {
|
||||
authentication = {
|
||||
token: authentication.token ?? '',
|
||||
};
|
||||
}
|
||||
updateRequest.mutate({ authenticationType, authentication });
|
||||
},
|
||||
},
|
||||
},
|
||||
() => [
|
||||
{
|
||||
value: 'body',
|
||||
options: {
|
||||
value: activeRequest.bodyType,
|
||||
items: [
|
||||
{ type: 'separator', label: 'Form Data' },
|
||||
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
|
||||
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
|
||||
{ type: 'separator', label: 'Text Content' },
|
||||
{ label: 'JSON', value: BODY_TYPE_JSON },
|
||||
{ label: 'XML', value: BODY_TYPE_XML },
|
||||
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
||||
{ type: 'separator', label: 'Other' },
|
||||
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
|
||||
],
|
||||
onChange: async (bodyType) => {
|
||||
const patch: Partial<HttpRequest> = { bodyType };
|
||||
if (bodyType === BODY_TYPE_NONE) {
|
||||
patch.headers = activeRequest.headers.filter(
|
||||
(h) => h.name.toLowerCase() !== 'content-type',
|
||||
);
|
||||
} else if (
|
||||
bodyType === BODY_TYPE_FORM_URLENCODED ||
|
||||
bodyType === BODY_TYPE_FORM_MULTIPART ||
|
||||
bodyType === BODY_TYPE_JSON ||
|
||||
bodyType === BODY_TYPE_XML
|
||||
) {
|
||||
patch.method = 'POST';
|
||||
patch.headers = [
|
||||
...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
|
||||
[]),
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: bodyType,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
} else if (bodyType == BODY_TYPE_GRAPHQL) {
|
||||
patch.method = 'POST';
|
||||
patch.headers = [
|
||||
...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
|
||||
[]),
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Force update header editor so any changed headers are reflected
|
||||
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
||||
|
||||
updateRequest.mutate(patch);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'params',
|
||||
label: (
|
||||
<div className="flex items-center">
|
||||
Params
|
||||
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'headers',
|
||||
label: (
|
||||
<div className="flex items-center">
|
||||
Headers
|
||||
<CountBadge count={activeRequest.headers.filter((h) => h.name).length} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'auth',
|
||||
label: 'Auth',
|
||||
options: {
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
{ label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
|
||||
{ label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER },
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
|
||||
],
|
||||
onChange: async (authenticationType) => {
|
||||
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
|
||||
if (authenticationType === AUTH_TYPE_BASIC) {
|
||||
authentication = {
|
||||
username: authentication.username ?? '',
|
||||
password: authentication.password ?? '',
|
||||
};
|
||||
} else if (authenticationType === AUTH_TYPE_BEARER) {
|
||||
authentication = {
|
||||
token: authentication.token ?? '',
|
||||
};
|
||||
}
|
||||
updateRequest.mutate({ authenticationType, authentication });
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[activeRequest, updateRequest],
|
||||
);
|
||||
|
||||
@@ -180,7 +179,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
const sendRequest = useSendRequest(activeRequest?.id ?? null);
|
||||
const sendRequest = useSendRequest(activeRequest.id ?? null);
|
||||
const handleSend = useCallback(
|
||||
async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -267,7 +266,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
placeholder="..."
|
||||
className="!bg-gray-50"
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest?.body?.text ?? ''}`}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
contentType="application/json"
|
||||
onChange={handleBodyTextChange}
|
||||
format={tryFormatJson}
|
||||
@@ -280,7 +279,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
placeholder="..."
|
||||
className="!bg-gray-50"
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest?.body?.text ?? ''}`}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
contentType="text/xml"
|
||||
onChange={handleBodyTextChange}
|
||||
/>
|
||||
@@ -289,7 +288,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
baseRequest={activeRequest}
|
||||
className="!bg-gray-50"
|
||||
defaultValue={`${activeRequest?.body?.text ?? ''}`}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
onChange={handleBodyTextChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? (
|
||||
|
||||
@@ -2,18 +2,16 @@ import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useLatestResponse } from '../hooks/useLatestResponse';
|
||||
import { useResponseContentType } from '../hooks/useResponseContentType';
|
||||
import { useHttpResponses } from '../hooks/useHttpResponses';
|
||||
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
|
||||
import { useResponseContentType } from '../hooks/useResponseContentType';
|
||||
import { useResponseViewMode } from '../hooks/useResponseViewMode';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import type { HttpRequest, HttpResponse } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import { Banner } from './core/Banner';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { DurationTag } from './core/DurationTag';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { JsonAttributeTree } from './core/JsonAttributeTree';
|
||||
import { SizeTag } from './core/SizeTag';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
@@ -24,22 +22,21 @@ import { RecentResponsesDropdown } from './RecentResponsesDropdown';
|
||||
import { ResponseHeaders } from './ResponseHeaders';
|
||||
import { CsvViewer } from './responseViewers/CsvViewer';
|
||||
import { ImageViewer } from './responseViewers/ImageViewer';
|
||||
import { JsonViewer } from './responseViewers/JsonViewer';
|
||||
import { TextViewer } from './responseViewers/TextViewer';
|
||||
import { WebPageViewer } from './responseViewers/WebPageViewer';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
activeRequest: HttpRequest;
|
||||
}
|
||||
|
||||
const useActiveTab = createGlobalState<string>('body');
|
||||
|
||||
export const ResponsePane = memo(function ResponsePane({ style, className }: Props) {
|
||||
export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) {
|
||||
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
|
||||
const activeRequest = useActiveRequest();
|
||||
const latestResponse = useLatestResponse(activeRequest?.id ?? null);
|
||||
const responses = useHttpResponses(activeRequest?.id ?? null);
|
||||
const latestResponse = useLatestHttpResponse(activeRequest.id);
|
||||
const responses = useHttpResponses(activeRequest.id);
|
||||
const activeResponse: HttpResponse | null = pinnedResponseId
|
||||
? responses.find((r) => r.id === pinnedResponseId) ?? null
|
||||
: latestResponse ?? null;
|
||||
@@ -87,10 +84,6 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
[activeResponse?.headers, contentType, setViewMode, viewMode],
|
||||
);
|
||||
|
||||
if (activeRequest === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
|
||||
@@ -21,7 +21,7 @@ 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 { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useSendManyRequests } from '../hooks/useSendFolder';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
@@ -558,7 +558,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
|
||||
const sendRequest = useSendRequest(itemId);
|
||||
const sendManyRequests = useSendManyRequests();
|
||||
const latestResponse = useLatestResponse(itemId);
|
||||
const latestResponse = useLatestHttpResponse(itemId);
|
||||
const updateHttpRequest = useUpdateHttpRequest(itemId);
|
||||
const updateGrpcRequest = useUpdateGrpcRequest(itemId);
|
||||
const updateAnyFolder = useUpdateAnyFolder();
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||
import { Button } from './core/Button';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
||||
import { HttpRequestLayout } from './HttpRequestLayout';
|
||||
@@ -165,10 +166,12 @@ export default function Workspace() {
|
||||
>
|
||||
<WorkspaceHeader className="pointer-events-none" />
|
||||
</HeaderSize>
|
||||
{activeRequest?.model === 'grpc_request' ? (
|
||||
{activeRequest == null ? (
|
||||
<HotKeyList hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']} />
|
||||
) : activeRequest.model === 'grpc_request' ? (
|
||||
<GrpcConnectionLayout style={body} />
|
||||
) : (
|
||||
<HttpRequestLayout style={body} />
|
||||
<HttpRequestLayout activeRequest={activeRequest} style={body} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
21
src-web/hooks/useDeleteGrpcConnection.ts
Normal file
21
src-web/hooks/useDeleteGrpcConnection.ts
Normal 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),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
19
src-web/hooks/useDeleteGrpcConnections.ts
Normal file
19
src-web/hooks/useDeleteGrpcConnections.ts
Normal 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 }), []);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import { trackEvent } from '../lib/analytics';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
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('cmd_delete_response', { id: id });
|
||||
return await invoke('cmd_delete_http_response', { id: id });
|
||||
},
|
||||
onSettled: () => trackEvent('HttpResponse', 'Delete'),
|
||||
onSuccess: ({ requestId, id: responseId }) => {
|
||||
@@ -3,12 +3,12 @@ import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
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('cmd_delete_all_responses', { requestId });
|
||||
await invoke('cmd_delete_all_http_responses', { requestId });
|
||||
},
|
||||
onSettled: () => trackEvent('HttpResponse', 'DeleteMany'),
|
||||
onSuccess: async () => {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { useHttpResponses } from './useHttpResponses';
|
||||
|
||||
export function useLatestResponse(requestId: string | null): HttpResponse | null {
|
||||
export function useLatestHttpResponse(requestId: string | null): HttpResponse | null {
|
||||
const responses = useHttpResponses(requestId);
|
||||
return responses[0] ?? null;
|
||||
}
|
||||
@@ -9,6 +9,8 @@ export function trackEvent(
|
||||
| 'Workspace'
|
||||
| 'Environment'
|
||||
| 'Folder'
|
||||
| 'GrpcMessage'
|
||||
| 'GrpcConnection'
|
||||
| 'GrpcRequest'
|
||||
| 'HttpRequest'
|
||||
| 'HttpResponse'
|
||||
|
||||
@@ -14,12 +14,17 @@ export function fallbackRequestName(r: HttpRequest | GrpcRequest | null): string
|
||||
|
||||
const fixedUrl = r.url.match(/^https?:\/\//) ? r.url : 'http://' + r.url;
|
||||
|
||||
try {
|
||||
const url = new URL(fixedUrl);
|
||||
const pathname = url.pathname === '/' ? '' : url.pathname;
|
||||
return `${url.host}${pathname}`;
|
||||
} catch (_) {
|
||||
// Nothing
|
||||
if (r.model === 'grpc_request' && r.service != null && r.method != null) {
|
||||
const shortService = r.service.split('.').pop();
|
||||
return `${shortService}/${r.method}`;
|
||||
} else {
|
||||
try {
|
||||
const url = new URL(fixedUrl);
|
||||
const pathname = url.pathname === '/' ? '' : url.pathname;
|
||||
return `${url.host}${pathname}`;
|
||||
} catch (_) {
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
|
||||
return r.url;
|
||||
|
||||
@@ -132,6 +132,7 @@ export interface GrpcConnection extends BaseModel {
|
||||
readonly model: 'grpc_connection';
|
||||
service: string;
|
||||
method: string;
|
||||
elapsed: number;
|
||||
}
|
||||
|
||||
export interface HttpRequest extends BaseModel {
|
||||
|
||||
Reference in New Issue
Block a user