mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:13:51 +01:00
Websocket Support (#159)
This commit is contained in:
@@ -3,19 +3,21 @@ import classNames from 'classnames';
|
||||
import { format } from 'date-fns';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useCopy } from '../hooks/useCopy';
|
||||
import { useGrpcEvents } from '../hooks/useGrpcEvents';
|
||||
import { usePinnedGrpcConnection } from '../hooks/usePinnedGrpcConnection';
|
||||
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { JsonAttributeTree } from './core/JsonAttributeTree';
|
||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||
import { Separator } from './core/Separator';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
|
||||
import { RecentGrpcConnectionsDropdown } from './RecentGrpcConnectionsDropdown';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
@@ -37,6 +39,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
||||
const { activeConnection, connections, setPinnedConnectionId } =
|
||||
usePinnedGrpcConnection(activeRequest);
|
||||
const events = useGrpcEvents(activeConnection?.id ?? null);
|
||||
const copy = useCopy();
|
||||
|
||||
const activeEvent = useMemo(
|
||||
() => events.find((m) => m.id === activeEventId) ?? null,
|
||||
@@ -69,11 +72,13 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
||||
<Icon icon="refresh" size="sm" spin className="text-text-subtlest" />
|
||||
)}
|
||||
</HStack>
|
||||
<RecentConnectionsDropdown
|
||||
connections={connections}
|
||||
activeConnection={activeConnection}
|
||||
onPinnedConnectionId={setPinnedConnectionId}
|
||||
/>
|
||||
<div className="ml-auto">
|
||||
<RecentGrpcConnectionsDropdown
|
||||
connections={connections}
|
||||
activeConnection={activeConnection}
|
||||
onPinnedConnectionId={setPinnedConnectionId}
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
<div className="overflow-y-auto h-full">
|
||||
{activeConnection.error && (
|
||||
@@ -107,8 +112,16 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
||||
{activeEvent.eventType === 'client_message' ||
|
||||
activeEvent.eventType === 'server_message' ? (
|
||||
<>
|
||||
<div className="mb-2 select-text cursor-text font-semibold">
|
||||
Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'}
|
||||
<div className="mb-2 select-text cursor-text grid grid-cols-[minmax(0,1fr)_auto] items-center">
|
||||
<div className="font-semibold">
|
||||
Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'}
|
||||
</div>
|
||||
<IconButton
|
||||
title="Copy message"
|
||||
icon="copy"
|
||||
size="xs"
|
||||
onClick={() => copy(activeEvent.content)}
|
||||
/>
|
||||
</div>
|
||||
{!showLarge && activeEvent.content.length > 1000 * 1000 ? (
|
||||
<VStack space={2} className="italic text-text-subtlest">
|
||||
|
||||
@@ -312,6 +312,7 @@ export function GrpcConnectionSetupPane({
|
||||
<TabContent value="message">
|
||||
<GrpcEditor
|
||||
onChange={handleChangeMessage}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
services={services}
|
||||
reflectionError={reflectionError}
|
||||
reflectionLoading={reflectionLoading}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { InlineCode } from './core/InlineCode';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { GrpcProtoSelection } from './GrpcProtoSelection';
|
||||
|
||||
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className'> & {
|
||||
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className' | 'forceUpdateKey'> & {
|
||||
services: ReflectResponseService[] | null;
|
||||
reflectionError?: string;
|
||||
reflectionLoading?: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import type { HttpRequestHeader } from '@yaakapp-internal/models';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import { charsets } from '../lib/data/charsets';
|
||||
import { connections } from '../lib/data/connections';
|
||||
@@ -11,18 +11,19 @@ import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||
|
||||
type Props = {
|
||||
forceUpdateKey: string;
|
||||
request: HttpRequest;
|
||||
onChange: (headers: HttpRequest['headers']) => void;
|
||||
headers: HttpRequestHeader[];
|
||||
stateKey: string;
|
||||
onChange: (headers: HttpRequestHeader[]) => void;
|
||||
};
|
||||
|
||||
export function HeadersEditor({ request, onChange, forceUpdateKey }: Props) {
|
||||
export function HeadersEditor({ stateKey, headers, onChange, forceUpdateKey }: Props) {
|
||||
return (
|
||||
<PairOrBulkEditor
|
||||
preferenceName="headers"
|
||||
stateKey={`headers.${request.id}`}
|
||||
stateKey={stateKey}
|
||||
valueAutocompleteVariables
|
||||
nameAutocompleteVariables
|
||||
pairs={request.headers}
|
||||
pairs={headers}
|
||||
onChange={onChange}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
nameValidate={validateHttpHeader}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
||||
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import React, { useCallback } from 'react';
|
||||
import { upsertWebsocketRequest } from '../commands/upsertWebsocketRequest';
|
||||
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
|
||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
@@ -13,7 +14,7 @@ import { DynamicForm } from './DynamicForm';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
|
||||
interface Props {
|
||||
request: HttpRequest | GrpcRequest;
|
||||
request: HttpRequest | GrpcRequest | WebsocketRequest;
|
||||
}
|
||||
|
||||
export function HttpAuthenticationEditor({ request }: Props) {
|
||||
@@ -32,6 +33,8 @@ export function HttpAuthenticationEditor({ request }: Props) {
|
||||
id: request.id,
|
||||
update: (r) => ({ ...r, authentication }),
|
||||
});
|
||||
} else if (request.model === 'websocket_request') {
|
||||
upsertWebsocketRequest.mutate({ ...request, authentication });
|
||||
} else {
|
||||
updateGrpcRequest.mutate({
|
||||
id: request.id,
|
||||
@@ -39,7 +42,7 @@ export function HttpAuthenticationEditor({ request }: Props) {
|
||||
});
|
||||
}
|
||||
},
|
||||
[request.id, request.model, updateGrpcRequest, updateHttpRequest],
|
||||
[request, updateGrpcRequest, updateHttpRequest],
|
||||
);
|
||||
|
||||
if (authConfig.data == null) {
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { CSSProperties } from 'react';
|
||||
import React from 'react';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { RequestPane } from './RequestPane';
|
||||
import { ResponsePane } from './ResponsePane';
|
||||
import { HttpRequestPane } from './HttpRequestPane';
|
||||
import { HttpResponsePane } from './HttpResponsePane';
|
||||
|
||||
interface Props {
|
||||
activeRequest: HttpRequest;
|
||||
@@ -17,13 +17,13 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
|
||||
className="p-3 gap-1.5"
|
||||
style={style}
|
||||
firstSlot={({ orientation, style }) => (
|
||||
<RequestPane
|
||||
<HttpRequestPane
|
||||
style={style}
|
||||
activeRequest={activeRequest}
|
||||
fullHeight={orientation === 'horizontal'}
|
||||
/>
|
||||
)}
|
||||
secondSlot={({ style }) => <ResponsePane activeRequestId={activeRequest.id} style={style} />}
|
||||
secondSlot={({ style }) => <HttpResponsePane activeRequestId={activeRequest.id} style={style} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import classNames from 'classnames';
|
||||
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||
@@ -77,12 +77,7 @@ const nonActiveRequestUrlsAtom = atom((get) => {
|
||||
|
||||
const memoNotActiveRequestUrlsAtom = deepEqualAtom(nonActiveRequestUrlsAtom);
|
||||
|
||||
export const RequestPane = memo(function RequestPane({
|
||||
style,
|
||||
fullHeight,
|
||||
className,
|
||||
activeRequest,
|
||||
}: Props) {
|
||||
export function HttpRequestPane({ style, fullHeight, className, activeRequest }: Props) {
|
||||
const activeRequestId = activeRequest.id;
|
||||
const { mutateAsync: updateRequestAsync, mutate: updateRequest } = useUpdateAnyHttpRequest();
|
||||
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
|
||||
@@ -94,7 +89,7 @@ export const RequestPane = memo(function RequestPane({
|
||||
|
||||
const handleContentTypeChange = useCallback(
|
||||
async (contentType: string | null) => {
|
||||
if (activeRequest == null || activeRequest.model !== 'http_request') {
|
||||
if (activeRequest == null) {
|
||||
console.error('Failed to get active request to update', activeRequest);
|
||||
return;
|
||||
}
|
||||
@@ -381,7 +376,8 @@ export const RequestPane = memo(function RequestPane({
|
||||
<TabContent value={TAB_HEADERS}>
|
||||
<HeadersEditor
|
||||
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
||||
request={activeRequest}
|
||||
headers={activeRequest.headers}
|
||||
stateKey={`headers.${activeRequest.id}`}
|
||||
onChange={(headers) => updateRequest({ id: activeRequestId, update: { headers } })}
|
||||
/>
|
||||
</TabContent>
|
||||
@@ -492,4 +488,4 @@ export const RequestPane = memo(function RequestPane({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
@@ -18,7 +18,7 @@ import { StatusTag } from './core/StatusTag';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
|
||||
import { RecentHttpResponsesDropdown } from './RecentHttpResponsesDropdown';
|
||||
import { ResponseHeaders } from './ResponseHeaders';
|
||||
import { ResponseInfo } from './ResponseInfo';
|
||||
import { AudioViewer } from './responseViewers/AudioViewer';
|
||||
@@ -40,11 +40,7 @@ const TAB_BODY = 'body';
|
||||
const TAB_HEADERS = 'headers';
|
||||
const TAB_INFO = 'info';
|
||||
|
||||
export const ResponsePane = memo(function ResponsePane({
|
||||
style,
|
||||
className,
|
||||
activeRequestId,
|
||||
}: Props) {
|
||||
export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequestId);
|
||||
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
|
||||
const [activeTabs, setActiveTabs] = useLocalStorage<Record<string, string>>(
|
||||
@@ -135,7 +131,7 @@ export const ResponsePane = memo(function ResponsePane({
|
||||
<SizeTag contentLength={activeResponse.contentLength ?? 0} />
|
||||
|
||||
<div className="ml-auto">
|
||||
<RecentResponsesDropdown
|
||||
<RecentHttpResponsesDropdown
|
||||
responses={responses}
|
||||
activeResponse={activeResponse}
|
||||
onPinnedResponseId={setPinnedResponseId}
|
||||
@@ -206,7 +202,7 @@ export const ResponsePane = memo(function ResponsePane({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function EnsureCompleteResponse({
|
||||
response,
|
||||
@@ -47,7 +47,7 @@ export function LicenseBadge() {
|
||||
className="!rounded-full mx-1"
|
||||
onClick={async () => {
|
||||
if (checkType === 'beta') {
|
||||
await openUrl('https://feedback.yaak.app/p/yaak-20-feedback');
|
||||
await openUrl('https://feedback.yaak.app');
|
||||
} else {
|
||||
openSettings.mutate();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
||||
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import React, { useState } from 'react';
|
||||
import { upsertWebsocketRequest } from '../commands/upsertWebsocketRequest';
|
||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
@@ -13,7 +14,7 @@ import { VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
activeWorkspaceId: string;
|
||||
request: HttpRequest | GrpcRequest;
|
||||
request: HttpRequest | GrpcRequest | WebsocketRequest;
|
||||
onDone: () => void;
|
||||
}
|
||||
|
||||
@@ -39,15 +40,17 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
|
||||
color="primary"
|
||||
disabled={selectedWorkspaceId === activeWorkspaceId}
|
||||
onClick={async () => {
|
||||
const args = {
|
||||
id: request.id,
|
||||
update: { workspaceId: selectedWorkspaceId, folderId: null },
|
||||
const update = {
|
||||
workspaceId: selectedWorkspaceId,
|
||||
folderId: null,
|
||||
};
|
||||
|
||||
if (request.model === 'http_request') {
|
||||
await updateHttpRequest.mutateAsync(args);
|
||||
await updateHttpRequest.mutateAsync({ id: request.id, update });
|
||||
} else if (request.model === 'grpc_request') {
|
||||
await updateGrpcRequest.mutateAsync(args);
|
||||
await updateGrpcRequest.mutateAsync({ id: request.id, update });
|
||||
} else if (request.model === 'websocket_request') {
|
||||
await upsertWebsocketRequest.mutateAsync({ ...request, ...update });
|
||||
}
|
||||
|
||||
// Hide after a moment, to give time for request to disappear
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
onPinnedConnectionId: (id: string) => void;
|
||||
}
|
||||
|
||||
export function RecentConnectionsDropdown({
|
||||
export function RecentGrpcConnectionsDropdown({
|
||||
activeConnection,
|
||||
connections,
|
||||
onPinnedConnectionId,
|
||||
@@ -38,7 +38,7 @@ export function RecentConnectionsDropdown({
|
||||
disabled: connections.length === 0,
|
||||
},
|
||||
{ type: 'separator', label: 'History' },
|
||||
...connections.slice(0, 20).map((c) => ({
|
||||
...connections.map((c) => ({
|
||||
label: (
|
||||
<HStack space={2}>
|
||||
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago •{' '}
|
||||
@@ -53,7 +53,7 @@ export function RecentConnectionsDropdown({
|
||||
<IconButton
|
||||
title="Show connection history"
|
||||
icon={activeConnection?.id === latestConnectionId ? 'chevron_down' : 'pin'}
|
||||
className="ml-auto"
|
||||
className="m-0.5"
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
/>
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RecentResponsesDropdown = function ResponsePane({
|
||||
export const RecentHttpResponsesDropdown = function ResponsePane({
|
||||
activeResponse,
|
||||
responses,
|
||||
onPinnedResponseId,
|
||||
@@ -65,7 +65,7 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
disabled: responses.length === 0,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
...responses.slice(0, 20).map((r: HttpResponse) => ({
|
||||
...responses.map((r: HttpResponse) => ({
|
||||
label: (
|
||||
<HStack space={2}>
|
||||
<StatusTag className="text-sm" response={r} />
|
||||
@@ -2,11 +2,10 @@ import classNames from 'classnames';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
||||
import { useKeyboardEvent } from '../hooks/useKeyboardEvent';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { requestsAtom } from '../hooks/useRequests';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { router } from '../lib/router';
|
||||
@@ -51,7 +50,7 @@ export function RecentRequestsDropdown({ className }: Props) {
|
||||
const activeWorkspaceId = getActiveWorkspaceId();
|
||||
if (activeWorkspaceId === null) return [];
|
||||
|
||||
const requests = [...jotaiStore.get(httpRequestsAtom), ...jotaiStore.get(grpcRequestsAtom)];
|
||||
const requests = jotaiStore.get(requestsAtom);
|
||||
const recentRequestItems: DropdownItem[] = [];
|
||||
for (const id of recentRequestIds) {
|
||||
const request = requests.find((r) => r.id === id);
|
||||
|
||||
69
src-web/components/RecentWebsocketConnectionsDropdown.tsx
Normal file
69
src-web/components/RecentWebsocketConnectionsDropdown.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { WebsocketConnection } from '@yaakapp-internal/models';
|
||||
import { formatDistanceToNowStrict } from 'date-fns';
|
||||
import { deleteWebsocketConnection } from '../commands/deleteWebsocketConnection';
|
||||
import { deleteWebsocketConnections } from '../commands/deleteWebsocketConnections';
|
||||
import { websocketRequestsAtom } from '../hooks/useWebsocketRequests';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { pluralizeCount } 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: WebsocketConnection[];
|
||||
activeConnection: WebsocketConnection;
|
||||
onPinnedConnectionId: (id: string) => void;
|
||||
}
|
||||
|
||||
export function RecentWebsocketConnectionsDropdown({
|
||||
activeConnection,
|
||||
connections,
|
||||
onPinnedConnectionId,
|
||||
}: Props) {
|
||||
const latestConnectionId = connections[0]?.id ?? 'n/a';
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: 'Clear Connection',
|
||||
onSelect: () => deleteWebsocketConnection.mutate(activeConnection),
|
||||
disabled: connections.length === 0,
|
||||
},
|
||||
{
|
||||
label: `Clear ${pluralizeCount('Connection', connections.length)}`,
|
||||
onSelect: () => {
|
||||
const request = jotaiStore
|
||||
.get(websocketRequestsAtom)
|
||||
.find((r) => r.id === activeConnection.requestId);
|
||||
if (request != null) {
|
||||
deleteWebsocketConnections.mutate(request);
|
||||
}
|
||||
},
|
||||
hidden: connections.length <= 1,
|
||||
disabled: connections.length === 0,
|
||||
},
|
||||
{ type: 'separator', label: 'History' },
|
||||
...connections.map((c) => ({
|
||||
label: (
|
||||
<HStack space={2}>
|
||||
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago •{' '}
|
||||
<span className="font-mono text-sm">{c.elapsed}ms</span>
|
||||
</HStack>
|
||||
),
|
||||
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
||||
onSelect: () => onPinnedConnectionId(c.id),
|
||||
})),
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
title="Show connection history"
|
||||
icon={activeConnection?.id === latestConnectionId ? 'chevron_down' : 'pin'}
|
||||
className="m-0.5"
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type { IconProps } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import type { InputProps } from './core/Input';
|
||||
import { Input } from './core/Input';
|
||||
import {HStack} from "./core/Stacks";
|
||||
import { RequestMethodDropdown } from './RequestMethodDropdown';
|
||||
|
||||
type Props = Pick<HttpRequest, 'url'> & {
|
||||
@@ -69,7 +70,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
ref={inputRef}
|
||||
autocompleteVariables
|
||||
stateKey={stateKey}
|
||||
size="md"
|
||||
size="sm"
|
||||
wrapLines={isFocused}
|
||||
hideLabel
|
||||
useTemplating
|
||||
@@ -99,10 +100,10 @@ export const UrlBar = memo(function UrlBar({
|
||||
)
|
||||
}
|
||||
rightSlot={
|
||||
<>
|
||||
{rightSlot}
|
||||
<HStack space={0.5}>
|
||||
{rightSlot && <div className="py-0.5 h-full">{rightSlot}</div>}
|
||||
{submitIcon !== null && (
|
||||
<div className="py-0.5">
|
||||
<div className="py-0.5 h-full">
|
||||
<IconButton
|
||||
size="xs"
|
||||
iconSize="md"
|
||||
@@ -114,7 +115,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</HStack>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
|
||||
42
src-web/components/WebsocketRequestLayout.tsx
Normal file
42
src-web/components/WebsocketRequestLayout.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React from 'react';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { WebsocketRequestPane } from './WebsocketRequestPane';
|
||||
import { WebsocketResponsePane } from './WebsocketResponsePane';
|
||||
|
||||
interface Props {
|
||||
activeRequest: WebsocketRequest;
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
export function WebsocketRequestLayout({ activeRequest, style }: Props) {
|
||||
return (
|
||||
<SplitLayout
|
||||
name="websocket_layout"
|
||||
className="p-3 gap-1.5"
|
||||
style={style}
|
||||
firstSlot={({ orientation, style }) => (
|
||||
<WebsocketRequestPane
|
||||
style={style}
|
||||
activeRequest={activeRequest}
|
||||
fullHeight={orientation === 'horizontal'}
|
||||
/>
|
||||
)}
|
||||
secondSlot={({ style }) => (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
'x-theme-responsePane',
|
||||
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1',
|
||||
'bg-surface rounded-md border border-border-subtle',
|
||||
'shadow relative',
|
||||
)}
|
||||
>
|
||||
<WebsocketResponsePane activeRequest={activeRequest} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
325
src-web/components/WebsocketRequestPane.tsx
Normal file
325
src-web/components/WebsocketRequestPane.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
import type { HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import { closeWebsocket, connectWebsocket, sendWebsocket } from '@yaakapp-internal/ws';
|
||||
import classNames from 'classnames';
|
||||
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { upsertWebsocketRequest } from '../commands/upsertWebsocketRequest';
|
||||
import { getActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||
import { getActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||
import { useImportQuerystring } from '../hooks/useImportQuerystring';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||
import { requestsAtom } from '../hooks/useRequests';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useLatestWebsocketConnection } from '../hooks/useWebsocketConnections';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { deepEqualAtom } from '../lib/atoms';
|
||||
import { languageFromContentType } from '../lib/contentType';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { generateId } from '../lib/generateId';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import type { Pair } from './core/PairEditor';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { UrlBar } from './UrlBar';
|
||||
import { UrlParametersEditor } from './UrlParameterEditor';
|
||||
|
||||
interface Props {
|
||||
style: CSSProperties;
|
||||
fullHeight: boolean;
|
||||
className?: string;
|
||||
activeRequest: WebsocketRequest;
|
||||
}
|
||||
|
||||
const TAB_MESSAGE = 'message';
|
||||
const TAB_PARAMS = 'params';
|
||||
const TAB_HEADERS = 'headers';
|
||||
const TAB_AUTH = 'auth';
|
||||
const TAB_DESCRIPTION = 'description';
|
||||
|
||||
const tabsAtom = atomWithStorage<Record<string, string>>('requestPaneActiveTabs', {});
|
||||
|
||||
const nonActiveRequestUrlsAtom = atom((get) => {
|
||||
const activeRequestId = get(activeRequestIdAtom);
|
||||
const requests = get(requestsAtom);
|
||||
return requests
|
||||
.filter((r) => r.id !== activeRequestId)
|
||||
.map((r): GenericCompletionOption => ({ type: 'constant', label: r.url }));
|
||||
});
|
||||
|
||||
const memoNotActiveRequestUrlsAtom = deepEqualAtom(nonActiveRequestUrlsAtom);
|
||||
|
||||
export function WebsocketRequestPane({ style, fullHeight, className, activeRequest }: Props) {
|
||||
const activeRequestId = activeRequest.id;
|
||||
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
const [{ urlKey }] = useRequestEditor();
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
|
||||
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
||||
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
|
||||
(m) => m[1] ?? '',
|
||||
);
|
||||
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
|
||||
const items: Pair[] = [...nonEmptyParameters];
|
||||
for (const name of placeholderNames) {
|
||||
const index = items.findIndex((p) => p.name === name);
|
||||
if (index >= 0) {
|
||||
items[index]!.readOnlyName = true;
|
||||
} else {
|
||||
items.push({ name, value: '', enabled: true, readOnlyName: true, id: generateId() });
|
||||
}
|
||||
}
|
||||
return { urlParameterPairs: items, urlParametersKey: placeholderNames.join(',') };
|
||||
}, [activeRequest.url, activeRequest.urlParameters]);
|
||||
|
||||
const tabs = useMemo<TabItem[]>(() => {
|
||||
// const options: Omit<RadioDropdownProps<WebsocketMessageType>, 'children'> = {
|
||||
// value: activeRequest.messageType ?? 'text',
|
||||
// items: [
|
||||
// { label: 'Text', value: 'text' },
|
||||
// { label: 'Binary', value: 'binary' },
|
||||
// ],
|
||||
// onChange: async (messageType) => {
|
||||
// if (messageType === activeRequest.messageType) return;
|
||||
// upsertWebsocketRequest.mutate({ ...activeRequest, messageType });
|
||||
// },
|
||||
// };
|
||||
return [
|
||||
{
|
||||
value: TAB_MESSAGE,
|
||||
label: 'Message',
|
||||
} as TabItem,
|
||||
{
|
||||
value: TAB_PARAMS,
|
||||
rightSlot: <CountBadge count={urlParameterPairs.length} />,
|
||||
label: 'Params',
|
||||
},
|
||||
{
|
||||
value: TAB_HEADERS,
|
||||
label: 'Headers',
|
||||
rightSlot: <CountBadge count={activeRequest.headers.filter((h) => h.name).length} />,
|
||||
},
|
||||
{
|
||||
value: TAB_AUTH,
|
||||
label: 'Auth',
|
||||
options: {
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
...authentication.map((a) => ({
|
||||
label: a.label || 'UNKNOWN',
|
||||
shortLabel: a.shortLabel,
|
||||
value: a.name,
|
||||
})),
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
|
||||
],
|
||||
onChange: async (authenticationType) => {
|
||||
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
|
||||
if (activeRequest.authenticationType !== authenticationType) {
|
||||
authentication = {
|
||||
// Reset auth if changing types
|
||||
};
|
||||
}
|
||||
upsertWebsocketRequest.mutate({
|
||||
...activeRequest,
|
||||
authenticationType,
|
||||
authentication,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
value: TAB_DESCRIPTION,
|
||||
label: 'Info',
|
||||
},
|
||||
];
|
||||
}, [activeRequest, authentication, urlParameterPairs.length]);
|
||||
|
||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
||||
const { mutate: importQuerystring } = useImportQuerystring(activeRequestId);
|
||||
const connection = useLatestWebsocketConnection(activeRequestId);
|
||||
|
||||
const activeTab = activeTabs?.[activeRequestId];
|
||||
const setActiveTab = useCallback(
|
||||
(tab: string) => {
|
||||
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
||||
},
|
||||
[activeRequest.id, setActiveTabs],
|
||||
);
|
||||
|
||||
useRequestEditorEvent('request_pane.focus_tab', () => {
|
||||
setActiveTab(TAB_PARAMS);
|
||||
});
|
||||
|
||||
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
||||
|
||||
const autocomplete: GenericCompletionConfig = useMemo(
|
||||
() => ({
|
||||
minMatch: 3,
|
||||
options:
|
||||
autocompleteUrls.length > 0
|
||||
? autocompleteUrls
|
||||
: [
|
||||
{ label: 'http://', type: 'constant' },
|
||||
{ label: 'https://', type: 'constant' },
|
||||
],
|
||||
}),
|
||||
[autocompleteUrls],
|
||||
);
|
||||
|
||||
const handleConnect = useCallback(async () => {
|
||||
await connectWebsocket({
|
||||
requestId: activeRequest.id,
|
||||
environmentId: getActiveEnvironment()?.id ?? null,
|
||||
cookieJarId: getActiveCookieJar()?.id ?? null,
|
||||
});
|
||||
trackEvent('websocket_request', 'send');
|
||||
}, [activeRequest.id]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (connection == null) return;
|
||||
await sendWebsocket({
|
||||
connectionId: connection?.id,
|
||||
environmentId: getActiveEnvironment()?.id ?? null,
|
||||
});
|
||||
trackEvent('websocket_connection', 'send');
|
||||
}, [connection]);
|
||||
|
||||
const handleCancel = useCallback(async () => {
|
||||
if (connection == null) return;
|
||||
await closeWebsocket({ connectionId: connection?.id });
|
||||
trackEvent('websocket_connection', 'cancel');
|
||||
}, [connection]);
|
||||
|
||||
const handleUrlChange = useCallback(
|
||||
(url: string) => upsertWebsocketRequest.mutate({ ...activeRequest, url }),
|
||||
[activeRequest],
|
||||
);
|
||||
|
||||
const messageLanguage = languageFromContentType(null, activeRequest.message);
|
||||
|
||||
const isLoading = connection !== null && connection.state !== 'closed';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}
|
||||
>
|
||||
{activeRequest && (
|
||||
<>
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto]">
|
||||
<UrlBar
|
||||
stateKey={`url.${activeRequest.id}`}
|
||||
key={forceUpdateKey + urlKey}
|
||||
url={activeRequest.url}
|
||||
submitIcon={isLoading ? 'send_horizontal' : 'arrow_up_down'}
|
||||
rightSlot={
|
||||
isLoading && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
title="Close connection"
|
||||
icon="x"
|
||||
className="w-8 mr-0.5 !h-full"
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
placeholder="wss://example.com"
|
||||
onPasteOverwrite={importQuerystring}
|
||||
autocomplete={autocomplete}
|
||||
onSend={isLoading ? handleSend : handleConnect}
|
||||
onCancel={cancelResponse}
|
||||
onUrlChange={handleUrlChange}
|
||||
forceUpdateKey={updateKey}
|
||||
isLoading={activeResponse != null && activeResponse.state !== 'closed'}
|
||||
method={null}
|
||||
/>
|
||||
</div>
|
||||
<Tabs
|
||||
key={activeRequest.id} // Freshen tabs on request change
|
||||
value={activeTab}
|
||||
label="Request"
|
||||
onChangeValue={setActiveTab}
|
||||
tabs={tabs}
|
||||
tabListClassName="mt-2 !mb-1.5"
|
||||
>
|
||||
<TabContent value={TAB_AUTH}>
|
||||
<HttpAuthenticationEditor request={activeRequest} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_HEADERS}>
|
||||
<HeadersEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
headers={activeRequest.headers}
|
||||
stateKey={`headers.${activeRequest.id}`}
|
||||
onChange={(headers) => upsertWebsocketRequest.mutate({ ...activeRequest, headers })}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_PARAMS}>
|
||||
<UrlParametersEditor
|
||||
stateKey={`params.${activeRequest.id}`}
|
||||
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
||||
pairs={urlParameterPairs}
|
||||
onChange={(urlParameters) =>
|
||||
upsertWebsocketRequest.mutate({ ...activeRequest, urlParameters })
|
||||
}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_MESSAGE}>
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
useTemplating
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={activeRequest.message}
|
||||
language={messageLanguage}
|
||||
onChange={(message) => upsertWebsocketRequest.mutate({ ...activeRequest, message })}
|
||||
stateKey={`json.${activeRequest.id}`}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_DESCRIPTION}>
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
||||
<PlainInput
|
||||
label="Request Name"
|
||||
hideLabel
|
||||
forceUpdateKey={updateKey}
|
||||
defaultValue={activeRequest.name}
|
||||
className="font-sans !text-xl !px-0"
|
||||
containerClassName="border-0"
|
||||
placeholder={fallbackRequestName(activeRequest)}
|
||||
onChange={(name) => upsertWebsocketRequest.mutate({ ...activeRequest, name })}
|
||||
/>
|
||||
<MarkdownEditor
|
||||
name="request-description"
|
||||
placeholder="Request description"
|
||||
defaultValue={activeRequest.description}
|
||||
stateKey={`description.${activeRequest.id}`}
|
||||
forceUpdateKey={updateKey}
|
||||
onChange={(description) =>
|
||||
upsertWebsocketRequest.mutate({ ...activeRequest, description })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
229
src-web/components/WebsocketResponsePane.tsx
Normal file
229
src-web/components/WebsocketResponsePane.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import type { WebsocketEvent, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { format } from 'date-fns';
|
||||
import { hexy } from 'hexy';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useCopy } from '../hooks/useCopy';
|
||||
import { useFormatText } from '../hooks/useFormatText';
|
||||
import { usePinnedWebsocketConnection } from '../hooks/usePinnedWebsocketConnection';
|
||||
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||
import { useWebsocketEvents } from '../hooks/useWebsocketEvents';
|
||||
import { languageFromContentType } from '../lib/contentType';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { Separator } from './core/Separator';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { RecentWebsocketConnectionsDropdown } from './RecentWebsocketConnectionsDropdown';
|
||||
|
||||
interface Props {
|
||||
activeRequest: WebsocketRequest;
|
||||
}
|
||||
|
||||
export function WebsocketResponsePane({ activeRequest }: Props) {
|
||||
const [activeEventId, setActiveEventId] = useState<string | null>(null);
|
||||
const [showLarge, setShowLarge] = useStateWithDeps<boolean>(false, [activeRequest.id]);
|
||||
const [showingLarge, setShowingLarge] = useState<boolean>(false);
|
||||
const [hexDumps, setHexDumps] = useState<Record<string, boolean>>({});
|
||||
|
||||
const { activeConnection, connections, setPinnedConnectionId } =
|
||||
usePinnedWebsocketConnection(activeRequest);
|
||||
|
||||
// const isLoading = activeConnection !== null && activeConnection.state !== 'closed';
|
||||
const events = useWebsocketEvents(activeConnection?.id ?? null);
|
||||
|
||||
const activeEvent = useMemo(
|
||||
() => events.find((m) => m.id === activeEventId) ?? null,
|
||||
[activeEventId, events],
|
||||
);
|
||||
|
||||
const hexDump = hexDumps[activeEventId ?? 'n/a'] ?? activeEvent?.messageType === 'binary';
|
||||
|
||||
const message = useMemo(() => {
|
||||
if (hexDump) {
|
||||
return activeEvent?.message ? hexy(activeEvent?.message) : '';
|
||||
}
|
||||
const text = activeEvent?.message
|
||||
? new TextDecoder('utf-8').decode(Uint8Array.from(activeEvent.message))
|
||||
: '';
|
||||
return text;
|
||||
}, [activeEvent?.message, hexDump]);
|
||||
|
||||
const language = languageFromContentType(null, message);
|
||||
const formattedContent = useFormatText({ language, text: message, pretty: true });
|
||||
const copy = useCopy();
|
||||
|
||||
return (
|
||||
<SplitLayout
|
||||
layout="vertical"
|
||||
name="grpc_events"
|
||||
defaultRatio={0.4}
|
||||
minHeightPx={20}
|
||||
firstSlot={() =>
|
||||
activeConnection && (
|
||||
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
|
||||
<HStack className="pl-3 mb-1 font-mono text-sm">
|
||||
<HStack space={2}>
|
||||
{activeConnection.state !== 'closed' && (
|
||||
<Icon icon="refresh" size="sm" spin className="text-text-subtlest" />
|
||||
)}
|
||||
<StatusTag showReason response={activeConnection} />
|
||||
<span>•</span>
|
||||
<span>{events.length} Messages</span>
|
||||
</HStack>
|
||||
<div className="ml-auto">
|
||||
<RecentWebsocketConnectionsDropdown
|
||||
connections={connections}
|
||||
activeConnection={activeConnection}
|
||||
onPinnedConnectionId={setPinnedConnectionId}
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
<div className="overflow-y-auto h-full">
|
||||
{activeConnection.error && (
|
||||
<Banner color="danger" className="m-3">
|
||||
{activeConnection.error}
|
||||
</Banner>
|
||||
)}
|
||||
{...events.map((e) => (
|
||||
<EventRow
|
||||
key={e.id}
|
||||
event={e}
|
||||
isActive={e.id === activeEventId}
|
||||
onClick={() => {
|
||||
if (e.id === activeEventId) setActiveEventId(null);
|
||||
else setActiveEventId(e.id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
secondSlot={
|
||||
activeEvent &&
|
||||
(() => (
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="pb-3 px-2">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="mx-2 overflow-y-auto grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="mb-2 select-text cursor-text grid grid-cols-[minmax(0,1fr)_auto] items-center">
|
||||
<div className="font-semibold">
|
||||
{activeEvent.messageType === 'close'
|
||||
? 'Connection Closed'
|
||||
: `Message ${activeEvent.isServer ? 'Received' : 'Sent'}`}
|
||||
</div>
|
||||
<HStack space={1}>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
if (activeEventId == null) return;
|
||||
setHexDumps({ ...hexDumps, [activeEventId]: !hexDump });
|
||||
}}
|
||||
>
|
||||
{hexDump ? 'Show Message' : 'Show Hexdump'}
|
||||
</Button>
|
||||
<IconButton
|
||||
title="Copy message"
|
||||
icon="copy"
|
||||
size="xs"
|
||||
onClick={() => copy(message)}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
{!showLarge && activeEvent.message.length > 1000 * 1000 ? (
|
||||
<VStack space={2} className="italic text-text-subtlest">
|
||||
Message previews larger than 1MB are hidden
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowingLarge(true);
|
||||
setTimeout(() => {
|
||||
setShowLarge(true);
|
||||
setShowingLarge(false);
|
||||
}, 500);
|
||||
}}
|
||||
isLoading={showingLarge}
|
||||
color="secondary"
|
||||
variant="border"
|
||||
size="xs"
|
||||
>
|
||||
Try Showing
|
||||
</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
) : activeEvent.message.length === 0 ? (
|
||||
<EmptyStateText>No Content</EmptyStateText>
|
||||
) : (
|
||||
<Editor
|
||||
language={language}
|
||||
defaultValue={formattedContent.data ?? ''}
|
||||
wrapLines={false}
|
||||
readOnly={true}
|
||||
stateKey={null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EventRow({
|
||||
onClick,
|
||||
isActive,
|
||||
event,
|
||||
}: {
|
||||
onClick?: () => void;
|
||||
isActive?: boolean;
|
||||
event: WebsocketEvent;
|
||||
}) {
|
||||
const { createdAt, message: messageBytes, isServer, messageType } = event;
|
||||
const message = messageBytes
|
||||
? new TextDecoder('utf-8').decode(Uint8Array.from(messageBytes))
|
||||
: '';
|
||||
return (
|
||||
<div className="px-1">
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left',
|
||||
'px-1.5 py-1 font-mono cursor-default group focus:outline-none rounded',
|
||||
isActive && '!bg-surface-highlight !text-text',
|
||||
'text-text-subtle hover:text',
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={classNames(
|
||||
messageType === 'close' ? 'text-secondary' : isServer ? 'text-info' : 'text-primary',
|
||||
)}
|
||||
icon={
|
||||
messageType === 'close'
|
||||
? 'info'
|
||||
: isServer
|
||||
? 'arrow_big_down_dash'
|
||||
: 'arrow_big_up_dash'
|
||||
}
|
||||
/>
|
||||
<div className={classNames('w-full truncate text-xs')}>
|
||||
{messageType === 'close'
|
||||
? 'Connection closed by ' + (isServer ? 'server' : 'client')
|
||||
: message.slice(0, 1000)}
|
||||
{/*{error && <span className="text-warning"> ({error})</span>}*/}
|
||||
</div>
|
||||
<div className={classNames('opacity-50 text-xs')}>
|
||||
{format(createdAt + 'Z', 'HH:mm:ss.SSS')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,25 +2,28 @@ import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {useEnsureActiveCookieJar, useSubscribeActiveCookieJarId} from "../hooks/useActiveCookieJar";
|
||||
import {useSubscribeActiveEnvironmentId} from "../hooks/useActiveEnvironment";
|
||||
import {getActiveRequest, useActiveRequest} from '../hooks/useActiveRequest';
|
||||
import {useSubscribeActiveRequestId} from "../hooks/useActiveRequestId";
|
||||
import {
|
||||
useEnsureActiveCookieJar,
|
||||
useSubscribeActiveCookieJarId,
|
||||
} from '../hooks/useActiveCookieJar';
|
||||
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
|
||||
import { getActiveRequest, useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import {useDuplicateGrpcRequest} from "../hooks/useDuplicateGrpcRequest";
|
||||
import {useDuplicateHttpRequest} from "../hooks/useDuplicateHttpRequest";
|
||||
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
|
||||
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
|
||||
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
|
||||
import {useHotKey} from "../hooks/useHotKey";
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import {useSubscribeRecentCookieJars} from "../hooks/useRecentCookieJars";
|
||||
import {useSubscribeRecentEnvironments} from "../hooks/useRecentEnvironments";
|
||||
import {useSubscribeRecentRequests} from "../hooks/useRecentRequests";
|
||||
import {useSubscribeRecentWorkspaces} from "../hooks/useRecentWorkspaces";
|
||||
import { useSubscribeRecentCookieJars } from '../hooks/useRecentCookieJars';
|
||||
import { useSubscribeRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useSubscribeRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useSubscribeRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||
import {useSyncWorkspaceRequestTitle} from "../hooks/useSyncWorkspaceRequestTitle";
|
||||
import {useToggleCommandPalette} from "../hooks/useToggleCommandPalette";
|
||||
import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
|
||||
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
@@ -33,8 +36,9 @@ import { HeaderSize } from './HeaderSize';
|
||||
import { HttpRequestLayout } from './HttpRequestLayout';
|
||||
import { Overlay } from './Overlay';
|
||||
import { ResizeHandle } from './ResizeHandle';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { Sidebar } from './sidebar/Sidebar';
|
||||
import { SidebarActions } from './sidebar/SidebarActions';
|
||||
import { WebsocketRequestLayout } from './WebsocketRequestLayout';
|
||||
import { WorkspaceHeader } from './WorkspaceHeader';
|
||||
|
||||
const side = { gridArea: 'side' };
|
||||
@@ -213,9 +217,11 @@ function WorkspaceBody() {
|
||||
|
||||
if (activeRequest.model === 'grpc_request') {
|
||||
return <GrpcConnectionLayout style={body} />;
|
||||
} else if (activeRequest.model === 'websocket_request') {
|
||||
return <WebsocketRequestLayout style={body} activeRequest={activeRequest} />;
|
||||
} else {
|
||||
return <HttpRequestLayout activeRequest={activeRequest} style={body} />;
|
||||
}
|
||||
|
||||
return <HttpRequestLayout activeRequest={activeRequest} style={body} />;
|
||||
}
|
||||
|
||||
function useGlobalWorkspaceHooks() {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ImportCurlButton } from './ImportCurlButton';
|
||||
import { LicenseBadge } from './LicenseBadge';
|
||||
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||
import { SettingsDropdown } from './SettingsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { SidebarActions } from './sidebar/SidebarActions';
|
||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
||||
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
request: HttpRequest | GrpcRequest;
|
||||
request: HttpRequest | GrpcRequest | WebsocketRequest;
|
||||
className?: string;
|
||||
shortNames?: boolean;
|
||||
}
|
||||
|
||||
const methodNames: Record<string, string> = {
|
||||
get: ' GET',
|
||||
put: ' PUT',
|
||||
get: 'GET',
|
||||
put: 'PUT',
|
||||
post: 'POST',
|
||||
patch: 'PTCH',
|
||||
delete: 'DELE',
|
||||
@@ -24,7 +24,11 @@ export function HttpMethodTag({ request, className }: Props) {
|
||||
? 'GQL'
|
||||
: request.model === 'grpc_request'
|
||||
? 'GRPC'
|
||||
: request.method;
|
||||
: request.model === 'websocket_request'
|
||||
? 'WS'
|
||||
: (methodNames[request.method.toLowerCase()] ?? request.method.slice(0, 4));
|
||||
|
||||
const paddedMethod = method.padStart(4, ' ').toUpperCase();
|
||||
|
||||
return (
|
||||
<span
|
||||
@@ -34,7 +38,7 @@ export function HttpMethodTag({ request, className }: Props) {
|
||||
'pt-[0.25em]', // Fix for monospace font not vertically centering
|
||||
)}
|
||||
>
|
||||
{(methodNames[method.toLowerCase()] ?? method.slice(0, 4)).toUpperCase()}
|
||||
{paddedMethod}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||
import type {HttpResponse, WebsocketConnection} from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
response: HttpResponse | WebsocketConnection;
|
||||
className?: string;
|
||||
showReason?: boolean;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function StatusTag({ response, className, showReason }: Props) {
|
||||
)}
|
||||
>
|
||||
{isInitializing ? 'CONNECTING' : label}{' '}
|
||||
{showReason && response.statusReason && response.statusReason}
|
||||
{showReason && 'statusReason' in response ? response.statusReason : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp-internal/models';
|
||||
import type {
|
||||
Folder,
|
||||
GrpcRequest,
|
||||
HttpRequest,
|
||||
WebsocketRequest,
|
||||
Workspace,
|
||||
} from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import { getActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
||||
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
|
||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useHttpResponses } from '../hooks/useHttpResponses';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { getSidebarCollapsedMap } from '../hooks/useSidebarItemCollapsed';
|
||||
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
|
||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import { router } from '../lib/router';
|
||||
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
|
||||
import { ContextMenu } from './core/Dropdown';
|
||||
import { upsertWebsocketRequest } from '../../commands/upsertWebsocketRequest';
|
||||
import { getActiveRequest } from '../../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
|
||||
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
|
||||
import { useDeleteAnyRequest } from '../../hooks/useDeleteAnyRequest';
|
||||
import { useGrpcConnections } from '../../hooks/useGrpcConnections';
|
||||
import { useHotKey } from '../../hooks/useHotKey';
|
||||
import { useHttpResponses } from '../../hooks/useHttpResponses';
|
||||
import { useSidebarHidden } from '../../hooks/useSidebarHidden';
|
||||
import { getSidebarCollapsedMap } from '../../hooks/useSidebarItemCollapsed';
|
||||
import { useUpdateAnyFolder } from '../../hooks/useUpdateAnyFolder';
|
||||
import { useUpdateAnyGrpcRequest } from '../../hooks/useUpdateAnyGrpcRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../../hooks/useUpdateAnyHttpRequest';
|
||||
import { getWebsocketRequest } from '../../hooks/useWebsocketRequests';
|
||||
import { router } from '../../lib/router';
|
||||
import { setWorkspaceSearchParams } from '../../lib/setWorkspaceSearchParams';
|
||||
import { ContextMenu } from '../core/Dropdown';
|
||||
import { sidebarSelectedIdAtom, sidebarTreeAtom } from './SidebarAtoms';
|
||||
import type { SidebarItemProps } from './SidebarItem';
|
||||
import { SidebarItems } from './SidebarItems';
|
||||
@@ -26,7 +34,7 @@ interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type SidebarModel = Folder | GrpcRequest | HttpRequest | Workspace;
|
||||
export type SidebarModel = Folder | GrpcRequest | HttpRequest | WebsocketRequest | Workspace;
|
||||
|
||||
export interface SidebarTreeNode {
|
||||
id: string;
|
||||
@@ -97,7 +105,7 @@ export function Sidebar({ className }: Props) {
|
||||
}
|
||||
|
||||
// NOTE: I'm not sure why, but TS thinks workspaceId is (string | undefined) here
|
||||
if ((node.model === 'http_request' || node.model === 'grpc_request') && node.workspaceId) {
|
||||
if (node.model !== 'folder' && node.workspaceId) {
|
||||
const workspaceId = node.workspaceId;
|
||||
await router.navigate({
|
||||
to: '/workspaces/$workspaceId',
|
||||
@@ -281,6 +289,11 @@ export function Sidebar({ className }: Props) {
|
||||
} else if (child.model === 'http_request') {
|
||||
const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId });
|
||||
return updateAnyHttpRequest({ id: child.id, update: updateRequest });
|
||||
} else if (child.model === 'websocket_request') {
|
||||
const request = getWebsocketRequest(child.id);
|
||||
return upsertWebsocketRequest.mutateAsync({ ...request, sortPriority, folderId });
|
||||
} else {
|
||||
throw new Error('Invalid model to update: ' + child.model);
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -295,6 +308,11 @@ export function Sidebar({ className }: Props) {
|
||||
} else if (child.model === 'http_request') {
|
||||
const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId });
|
||||
await updateAnyHttpRequest({ id: child.id, update: updateRequest });
|
||||
} else if (child.model === 'websocket_request') {
|
||||
const request = getWebsocketRequest(child.id);
|
||||
return upsertWebsocketRequest.mutateAsync({ ...request, sortPriority, folderId });
|
||||
} else {
|
||||
throw new Error('Invalid model to update: ' + child.model);
|
||||
}
|
||||
}
|
||||
setDraggingId(null);
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
|
||||
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { CreateDropdown } from './CreateDropdown';
|
||||
import { useFloatingSidebarHidden } from '../../hooks/useFloatingSidebarHidden';
|
||||
import { useShouldFloatSidebar } from '../../hooks/useShouldFloatSidebar';
|
||||
import { useSidebarHidden } from '../../hooks/useSidebarHidden';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { HStack } from '../core/Stacks';
|
||||
import { CreateDropdown } from '../CreateDropdown';
|
||||
|
||||
export function SidebarActions() {
|
||||
const floating = useShouldFloatSidebar();
|
||||
@@ -1,22 +1,20 @@
|
||||
import type { Folder, GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
||||
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
|
||||
// This is an atom so we can use it in the child items to avoid re-rendering the entire list
|
||||
import { atom } from 'jotai';
|
||||
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
|
||||
import { foldersAtom } from '../hooks/useFolders';
|
||||
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
||||
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
||||
import { deepEqualAtom } from '../lib/atoms';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||
import { foldersAtom } from '../../hooks/useFolders';
|
||||
import { requestsAtom } from '../../hooks/useRequests';
|
||||
import { deepEqualAtom } from '../../lib/atoms';
|
||||
import { fallbackRequestName } from '../../lib/fallbackRequestName';
|
||||
import type { SidebarTreeNode } from './Sidebar';
|
||||
|
||||
export const sidebarSelectedIdAtom = atom<string | null>(null);
|
||||
|
||||
const allPotentialChildrenAtom = atom((get) => {
|
||||
const httpRequests = get(httpRequestsAtom);
|
||||
const grpcRequests = get(grpcRequestsAtom);
|
||||
const requests = get(requestsAtom);
|
||||
const folders = get(foldersAtom);
|
||||
return [...httpRequests, ...folders, ...grpcRequests].map((v) => ({
|
||||
return [...requests, ...folders].map((v) => ({
|
||||
id: v.id,
|
||||
model: v.model,
|
||||
folderId: v.folderId,
|
||||
@@ -62,7 +60,7 @@ export const sidebarTreeAtom = atom<{
|
||||
return { tree: null, treeParentMap, selectableRequests };
|
||||
}
|
||||
|
||||
const selectedRequest: HttpRequest | GrpcRequest | null = null;
|
||||
const selectedRequest: HttpRequest | GrpcRequest | WebsocketRequest | null = null;
|
||||
let selectableRequestIndex = 0;
|
||||
|
||||
// Put requests and folders into a tree structure
|
||||
@@ -102,7 +100,7 @@ export const sidebarTreeAtom = atom<{
|
||||
|
||||
function itemFromModel(
|
||||
item: Pick<
|
||||
Folder | HttpRequest | GrpcRequest,
|
||||
Folder | HttpRequest | GrpcRequest | WebsocketRequest,
|
||||
'folderId' | 'model' | 'workspaceId' | 'id' | 'name' | 'sortPriority'
|
||||
>,
|
||||
depth = 0,
|
||||
@@ -5,18 +5,19 @@ import type { ReactElement } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { XYCoord } from 'react-dnd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { activeRequestAtom } from '../hooks/useActiveRequest';
|
||||
import { foldersAtom } from '../hooks/useFolders';
|
||||
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
||||
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
||||
import { useScrollIntoView } from '../hooks/useScrollIntoView';
|
||||
import { useSidebarItemCollapsed } from '../hooks/useSidebarItemCollapsed';
|
||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { HttpMethodTag } from './core/HttpMethodTag';
|
||||
import { Icon } from './core/Icon';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import { upsertWebsocketRequest } from '../../commands/upsertWebsocketRequest';
|
||||
import { activeRequestAtom } from '../../hooks/useActiveRequest';
|
||||
import { foldersAtom } from '../../hooks/useFolders';
|
||||
import { requestsAtom } from '../../hooks/useRequests';
|
||||
import { useScrollIntoView } from '../../hooks/useScrollIntoView';
|
||||
import { useSidebarItemCollapsed } from '../../hooks/useSidebarItemCollapsed';
|
||||
import { useUpdateAnyGrpcRequest } from '../../hooks/useUpdateAnyGrpcRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../../hooks/useUpdateAnyHttpRequest';
|
||||
import { getWebsocketRequest } from '../../hooks/useWebsocketRequests';
|
||||
import { jotaiStore } from '../../lib/jotai';
|
||||
import { HttpMethodTag } from '../core/HttpMethodTag';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { StatusTag } from '../core/StatusTag';
|
||||
import type { SidebarTreeNode } from './Sidebar';
|
||||
import { sidebarSelectedIdAtom } from './SidebarAtoms';
|
||||
import { SidebarItemContextMenu } from './SidebarItemContextMenu';
|
||||
@@ -138,6 +139,10 @@ export const SidebarItem = memo(function SidebarItem({
|
||||
id: itemId,
|
||||
update: (r) => ({ ...r, name: el.value }),
|
||||
});
|
||||
} else if (itemModel === 'websocket_request') {
|
||||
const request = getWebsocketRequest(itemId);
|
||||
if (request == null) return;
|
||||
await upsertWebsocketRequest.mutateAsync({ ...request, name: el.value });
|
||||
}
|
||||
setEditing(false);
|
||||
},
|
||||
@@ -167,7 +172,12 @@ export const SidebarItem = memo(function SidebarItem({
|
||||
);
|
||||
|
||||
const handleStartEditing = useCallback(() => {
|
||||
if (itemModel !== 'http_request' && itemModel !== 'grpc_request') return;
|
||||
if (
|
||||
itemModel !== 'http_request' &&
|
||||
itemModel !== 'grpc_request' &&
|
||||
itemModel !== 'websocket_request'
|
||||
)
|
||||
return;
|
||||
setEditing(true);
|
||||
}, [setEditing, itemModel]);
|
||||
|
||||
@@ -197,14 +207,10 @@ export const SidebarItem = memo(function SidebarItem({
|
||||
|
||||
const itemAtom = useMemo(() => {
|
||||
return atom((get) => {
|
||||
if (itemModel === 'http_request') {
|
||||
return get(httpRequestsAtom).find((v) => v.id === itemId);
|
||||
} else if (itemModel === 'grpc_request') {
|
||||
return get(grpcRequestsAtom).find((v) => v.id === itemId);
|
||||
} else if (itemModel === 'folder') {
|
||||
if (itemModel === 'folder') {
|
||||
return get(foldersAtom).find((v) => v.id === itemId);
|
||||
} else {
|
||||
return null;
|
||||
return get(requestsAtom).find((v) => v.id === itemId);
|
||||
}
|
||||
});
|
||||
}, [itemId, itemModel]);
|
||||
@@ -215,7 +221,7 @@ export const SidebarItem = memo(function SidebarItem({
|
||||
return null;
|
||||
}
|
||||
|
||||
const itemPrefix = (item.model === 'http_request' || item.model === 'grpc_request') && (
|
||||
const itemPrefix = item.model !== 'folder' && (
|
||||
<HttpMethodTag
|
||||
request={item}
|
||||
className={classNames(!(active || selected) && 'text-text-subtlest')}
|
||||
@@ -1,24 +1,24 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
||||
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
||||
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
|
||||
import { useDuplicateFolder } from '../hooks/useDuplicateFolder';
|
||||
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
|
||||
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
|
||||
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
|
||||
import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace';
|
||||
import { useRenameRequest } from '../hooks/useRenameRequest';
|
||||
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
||||
import { useSendManyRequests } from '../hooks/useSendManyRequests';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
|
||||
import { useDeleteFolder } from '../../hooks/useDeleteFolder';
|
||||
import { useDeleteAnyRequest } from '../../hooks/useDeleteAnyRequest';
|
||||
import { useDuplicateFolder } from '../../hooks/useDuplicateFolder';
|
||||
import { useDuplicateGrpcRequest } from '../../hooks/useDuplicateGrpcRequest';
|
||||
import { useDuplicateHttpRequest } from '../../hooks/useDuplicateHttpRequest';
|
||||
import { useHttpRequestActions } from '../../hooks/useHttpRequestActions';
|
||||
import { useMoveToWorkspace } from '../../hooks/useMoveToWorkspace';
|
||||
import { useRenameRequest } from '../../hooks/useRenameRequest';
|
||||
import { useSendAnyHttpRequest } from '../../hooks/useSendAnyHttpRequest';
|
||||
import { useSendManyRequests } from '../../hooks/useSendManyRequests';
|
||||
import { useWorkspaces } from '../../hooks/useWorkspaces';
|
||||
|
||||
import { showDialog } from '../lib/dialog';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { ContextMenu } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { FolderSettingsDialog } from './FolderSettingsDialog';
|
||||
import { showDialog } from '../../lib/dialog';
|
||||
import type { DropdownItem } from '../core/Dropdown';
|
||||
import { ContextMenu } from '../core/Dropdown';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { FolderSettingsDialog } from '../FolderSettingsDialog';
|
||||
import type { SidebarTreeNode } from './Sidebar';
|
||||
import { getHttpRequest } from '../hooks/useHttpRequests';
|
||||
import { getHttpRequest } from '../../hooks/useHttpRequests';
|
||||
|
||||
interface Props {
|
||||
child: SidebarTreeNode;
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GrpcConnection, HttpResponse } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import React, { Fragment, memo } from 'react';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { DropMarker } from './DropMarker';
|
||||
import { VStack } from '../core/Stacks';
|
||||
import { DropMarker } from '../DropMarker';
|
||||
import type { SidebarTreeNode } from './Sidebar';
|
||||
import { SidebarItem } from './SidebarItem';
|
||||
|
||||
Reference in New Issue
Block a user