mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-25 02:41:07 +01:00
More analytics, and cancel requests
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
|
||||
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
|
||||
|
||||
@@ -27,7 +27,7 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const actions = useMemo<Actions>(
|
||||
() => ({
|
||||
show({ id, ...props }: DialogEntry) {
|
||||
trackEvent('Dialog', 'Show', { id });
|
||||
trackEvent('dialog', 'show', { id });
|
||||
setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
|
||||
},
|
||||
toggle({ id, ...props }: DialogEntry) {
|
||||
|
||||
@@ -141,74 +141,56 @@ function EventRow({
|
||||
}) {
|
||||
const { eventType, status, createdAt, content, error } = event;
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left',
|
||||
'px-1 py-1 font-mono cursor-default group focus:outline-none',
|
||||
isActive && '!bg-highlight text-gray-900',
|
||||
'text-gray-800 hover:text-gray-900',
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={
|
||||
eventType === 'server_message'
|
||||
? 'text-blue-600'
|
||||
: eventType === 'client_message'
|
||||
? 'text-violet-600'
|
||||
: eventType === 'error' || (status != null && status > 0)
|
||||
? 'text-orange-600'
|
||||
: eventType === 'connection_end'
|
||||
? 'text-green-600'
|
||||
: 'text-gray-700'
|
||||
}
|
||||
title={
|
||||
eventType === 'server_message'
|
||||
? 'Server message'
|
||||
: eventType === 'client_message'
|
||||
? 'Client message'
|
||||
: eventType === 'error' || (status != null && status > 0)
|
||||
? 'Error'
|
||||
: eventType === 'connection_end'
|
||||
? 'Connection response'
|
||||
: undefined
|
||||
}
|
||||
icon={
|
||||
eventType === 'server_message'
|
||||
? 'arrowBigDownDash'
|
||||
: eventType === 'client_message'
|
||||
? 'arrowBigUpDash'
|
||||
: eventType === 'error' || (status != null && status > 0)
|
||||
? 'alert'
|
||||
: eventType === 'connection_end'
|
||||
? 'check'
|
||||
: 'info'
|
||||
}
|
||||
/>
|
||||
<div className={classNames('w-full truncate text-2xs')}>{error ?? content}</div>
|
||||
<div className={classNames('opacity-50 text-2xs')}>
|
||||
{format(createdAt + 'Z', 'HH:mm:ss.SSS')}
|
||||
</div>
|
||||
</button>
|
||||
<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-highlight text-gray-900',
|
||||
'text-gray-800 hover:text-gray-900',
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={
|
||||
eventType === 'server_message'
|
||||
? 'text-blue-600'
|
||||
: eventType === 'client_message'
|
||||
? 'text-violet-600'
|
||||
: eventType === 'error' || (status != null && status > 0)
|
||||
? 'text-orange-600'
|
||||
: eventType === 'connection_end'
|
||||
? 'text-green-600'
|
||||
: 'text-gray-700'
|
||||
}
|
||||
title={
|
||||
eventType === 'server_message'
|
||||
? 'Server message'
|
||||
: eventType === 'client_message'
|
||||
? 'Client message'
|
||||
: eventType === 'error' || (status != null && status > 0)
|
||||
? 'Error'
|
||||
: eventType === 'connection_end'
|
||||
? 'Connection response'
|
||||
: undefined
|
||||
}
|
||||
icon={
|
||||
eventType === 'server_message'
|
||||
? 'arrowBigDownDash'
|
||||
: eventType === 'client_message'
|
||||
? 'arrowBigUpDash'
|
||||
: eventType === 'error' || (status != null && status > 0)
|
||||
? 'alert'
|
||||
: eventType === 'connection_end'
|
||||
? 'check'
|
||||
: 'info'
|
||||
}
|
||||
/>
|
||||
<div className={classNames('w-full truncate text-2xs')}>{error ?? content}</div>
|
||||
<div className={classNames('opacity-50 text-2xs')}>
|
||||
{format(createdAt + 'Z', 'HH:mm:ss.SSS')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const GRPC_CODES: Record<number, string> = {
|
||||
0: 'Ok',
|
||||
1: 'Cancelled',
|
||||
2: 'Unknown',
|
||||
3: 'Invalid argument',
|
||||
4: 'Deadline exceeded',
|
||||
5: 'Not found',
|
||||
6: 'Already exists',
|
||||
7: 'Permission denied',
|
||||
8: 'Resource exhausted',
|
||||
9: 'Failed precondition',
|
||||
10: 'Aborted',
|
||||
11: 'Out of range',
|
||||
12: 'Unimplemented',
|
||||
13: 'Internal',
|
||||
14: 'Unavailable',
|
||||
15: 'Data loss',
|
||||
16: 'Unauthenticated',
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import useResizeObserver from '@react-hook/resize-observer';
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, FormEvent } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||
@@ -104,22 +104,18 @@ export function GrpcConnectionSetupPane({
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
const handleConnect = useCallback(
|
||||
async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (activeRequest == null) return;
|
||||
const handleConnect = useCallback(async () => {
|
||||
if (activeRequest == null) return;
|
||||
|
||||
if (activeRequest.service == null || activeRequest.method == null) {
|
||||
alert({
|
||||
id: 'grpc-invalid-service-method',
|
||||
title: 'Error',
|
||||
body: 'Service or method not selected',
|
||||
});
|
||||
}
|
||||
onGo();
|
||||
},
|
||||
[activeRequest, onGo],
|
||||
);
|
||||
if (activeRequest.service == null || activeRequest.method == null) {
|
||||
alert({
|
||||
id: 'grpc-invalid-service-method',
|
||||
title: 'Error',
|
||||
body: 'Service or method not selected',
|
||||
});
|
||||
}
|
||||
onGo();
|
||||
}, [activeRequest, onGo]);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
@@ -176,9 +172,10 @@ export function GrpcConnectionSetupPane({
|
||||
submitIcon={null}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
placeholder="localhost:50051"
|
||||
onSubmit={handleConnect}
|
||||
onSend={handleConnect}
|
||||
onUrlChange={handleChangeUrl}
|
||||
isLoading={false}
|
||||
onCancel={onCancel}
|
||||
isLoading={isStreaming}
|
||||
/>
|
||||
<HStack space={1.5}>
|
||||
<RadioDropdown
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, FormEvent } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
|
||||
@@ -183,13 +185,15 @@ export const RequestPane = memo(function RequestPane({
|
||||
);
|
||||
|
||||
const sendRequest = useSendRequest(activeRequest.id ?? null);
|
||||
const handleSend = useCallback(
|
||||
async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
await sendRequest.mutateAsync();
|
||||
},
|
||||
[sendRequest],
|
||||
);
|
||||
const { activeResponse } = usePinnedHttpResponse(activeRequest);
|
||||
const cancelResponse = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||
const handleSend = useCallback(async () => {
|
||||
await sendRequest.mutateAsync();
|
||||
}, [sendRequest]);
|
||||
|
||||
const handleCancel = useCallback(async () => {
|
||||
await cancelResponse.mutateAsync();
|
||||
}, [cancelResponse]);
|
||||
|
||||
const handleMethodChange = useCallback(
|
||||
(method: string) => updateRequest.mutate({ method }),
|
||||
@@ -214,7 +218,8 @@ export const RequestPane = memo(function RequestPane({
|
||||
url={activeRequest.url}
|
||||
method={activeRequest.method}
|
||||
placeholder="https://example.com"
|
||||
onSubmit={handleSend}
|
||||
onSend={handleSend}
|
||||
onCancel={handleCancel}
|
||||
onMethodChange={handleMethodChange}
|
||||
onUrlChange={handleUrlChange}
|
||||
forceUpdateKey={updateKey}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useHttpResponses } from '../hooks/useHttpResponses';
|
||||
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { useResponseContentType } from '../hooks/useResponseContentType';
|
||||
import { useResponseViewMode } from '../hooks/useResponseViewMode';
|
||||
import type { HttpRequest, HttpResponse } from '../lib/models';
|
||||
import type { HttpRequest } 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 { Icon } from './core/Icon';
|
||||
import { SizeTag } from './core/SizeTag';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
@@ -34,27 +34,11 @@ interface Props {
|
||||
const useActiveTab = createGlobalState<string>('body');
|
||||
|
||||
export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) {
|
||||
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(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;
|
||||
const { activeResponse, setPinnedResponse, responses } = usePinnedHttpResponse(activeRequest);
|
||||
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
|
||||
const [activeTab, setActiveTab] = useActiveTab();
|
||||
|
||||
// Unset pinned response when a new one comes in
|
||||
useEffect(() => setPinnedResponseId(null), [responses.length]);
|
||||
|
||||
const contentType = useResponseContentType(activeResponse);
|
||||
|
||||
const handlePinnedResponse = useCallback(
|
||||
(r: HttpResponse) => {
|
||||
setPinnedResponseId(r.id);
|
||||
},
|
||||
[setPinnedResponseId],
|
||||
);
|
||||
|
||||
const tabs = useMemo<TabItem[]>(
|
||||
() => [
|
||||
{
|
||||
@@ -89,21 +73,21 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
style={style}
|
||||
className={classNames(
|
||||
className,
|
||||
'max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1',
|
||||
'max-h-full h-full',
|
||||
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight',
|
||||
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
|
||||
)}
|
||||
>
|
||||
{!activeResponse && (
|
||||
<>
|
||||
<span />
|
||||
<HotKeyList
|
||||
hotkeys={['http_request.send', 'http_request.create', 'sidebar.toggle', 'urlBar.focus']}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{activeResponse && !isResponseLoading(activeResponse) && (
|
||||
<>
|
||||
{activeResponse == null ? (
|
||||
<HotKeyList
|
||||
hotkeys={['http_request.send', 'http_request.create', 'sidebar.toggle', 'urlBar.focus']}
|
||||
/>
|
||||
) : isResponseLoading(activeResponse) ? (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<Icon size="lg" className="opacity-disabled" spin icon="refresh" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1">
|
||||
<HStack
|
||||
alignItems="center"
|
||||
className={classNames(
|
||||
@@ -138,7 +122,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
<RecentResponsesDropdown
|
||||
responses={responses}
|
||||
activeResponse={activeResponse}
|
||||
onPinnedResponse={handlePinnedResponse}
|
||||
onPinnedResponse={setPinnedResponse}
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
@@ -179,7 +163,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppInfo } from '../hooks/useAppInfo';
|
||||
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { Heading } from './core/Heading';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { Input } from './core/Input';
|
||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||
import { Select } from './core/Select';
|
||||
import { Separator } from './core/Separator';
|
||||
import { VStack } from './core/Stacks';
|
||||
@@ -16,6 +20,7 @@ export const SettingsDialog = () => {
|
||||
const settings = useSettings();
|
||||
const updateSettings = useUpdateSettings();
|
||||
const appInfo = useAppInfo();
|
||||
const checkForUpdates = useCheckForUpdates();
|
||||
|
||||
if (settings == null || workspace == null) {
|
||||
return null;
|
||||
@@ -29,7 +34,10 @@ export const SettingsDialog = () => {
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={settings.appearance}
|
||||
onChange={(appearance) => updateSettings.mutateAsync({ ...settings, appearance })}
|
||||
onChange={async (appearance) => {
|
||||
await updateSettings.mutateAsync({ ...settings, appearance });
|
||||
trackEvent('setting', 'update', { appearance });
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: 'System',
|
||||
@@ -46,24 +54,37 @@ export const SettingsDialog = () => {
|
||||
]}
|
||||
/>
|
||||
|
||||
<Select
|
||||
name="updateChannel"
|
||||
label="Update Channel"
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={settings.updateChannel}
|
||||
onChange={(updateChannel) => updateSettings.mutateAsync({ ...settings, updateChannel })}
|
||||
options={[
|
||||
{
|
||||
label: 'Release',
|
||||
value: 'stable',
|
||||
},
|
||||
{
|
||||
label: 'Early Bird (Beta)',
|
||||
value: 'beta',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
|
||||
<Select
|
||||
name="updateChannel"
|
||||
label="Update Channel"
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={settings.updateChannel}
|
||||
onChange={async (updateChannel) => {
|
||||
trackEvent('setting', 'update', { update_channel: updateChannel });
|
||||
await updateSettings.mutateAsync({ ...settings, updateChannel });
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: 'Release',
|
||||
value: 'stable',
|
||||
},
|
||||
{
|
||||
label: 'Early Bird (Beta)',
|
||||
value: 'beta',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<IconButton
|
||||
variant="border"
|
||||
size="sm"
|
||||
title="Check for updates"
|
||||
icon="refresh"
|
||||
spin={checkForUpdates.isLoading}
|
||||
onClick={() => checkForUpdates.mutateAsync()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
@@ -88,41 +109,33 @@ export const SettingsDialog = () => {
|
||||
<Checkbox
|
||||
checked={workspace.settingValidateCertificates}
|
||||
title="Validate TLS Certificates"
|
||||
onChange={(settingValidateCertificates) =>
|
||||
updateWorkspace.mutateAsync({ settingValidateCertificates })
|
||||
}
|
||||
onChange={async (settingValidateCertificates) => {
|
||||
trackEvent('workspace', 'update', {
|
||||
validate_certificates: JSON.stringify(settingValidateCertificates),
|
||||
});
|
||||
await updateWorkspace.mutateAsync({ settingValidateCertificates });
|
||||
}}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={workspace.settingFollowRedirects}
|
||||
title="Follow Redirects"
|
||||
onChange={(settingFollowRedirects) =>
|
||||
updateWorkspace.mutateAsync({ settingFollowRedirects })
|
||||
}
|
||||
onChange={async (settingFollowRedirects) => {
|
||||
trackEvent('workspace', 'update', {
|
||||
follow_redirects: JSON.stringify(settingFollowRedirects),
|
||||
});
|
||||
await updateWorkspace.mutateAsync({ settingFollowRedirects });
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<Heading size={2}>App Info</Heading>
|
||||
<table className="text-sm w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="h-xs pr-3">Version</td>
|
||||
<td className="h-xs text-xs font-mono select-all cursor-text">
|
||||
{appInfo.data?.version}
|
||||
</td>
|
||||
</tr>
|
||||
{appInfo.data && (
|
||||
<tr>
|
||||
<td className="h-xs pr-3 whitespace-nowrap">Data Directory</td>
|
||||
<td className="h-xs text-xs font-mono select-all cursor-text break-all min-w-0">
|
||||
{appInfo.data.appDataDir}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<KeyValueRows>
|
||||
<KeyValueRow label="Version" value={appInfo.data?.version} />
|
||||
<KeyValueRow label="Data Directory" value={appInfo.data?.appDataDir} />
|
||||
</KeyValueRows>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,10 @@ import { useAppInfo } from '../hooks/useAppInfo';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||
import { SettingsDialog } from './SettingsDialog';
|
||||
|
||||
@@ -12,7 +12,7 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
<HStack className="h-full" alignItems="center">
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
trackEvent('Sidebar', 'Toggle');
|
||||
trackEvent('sidebar', 'toggle');
|
||||
|
||||
// NOTE: We're not using `toggle` because it may be out of sync
|
||||
// from changes in other windows
|
||||
|
||||
@@ -12,8 +12,9 @@ type Props = Pick<HttpRequest, 'url'> & {
|
||||
className?: string;
|
||||
method: HttpRequest['method'] | null;
|
||||
placeholder: string;
|
||||
onSubmit: (e: FormEvent) => void;
|
||||
onSend: () => void;
|
||||
onUrlChange: (url: string) => void;
|
||||
onCancel: () => void;
|
||||
submitIcon?: IconProps['icon'] | null;
|
||||
onMethodChange?: (method: string) => void;
|
||||
isLoading: boolean;
|
||||
@@ -27,7 +28,8 @@ export const UrlBar = memo(function UrlBar({
|
||||
method,
|
||||
placeholder,
|
||||
className,
|
||||
onSubmit,
|
||||
onSend,
|
||||
onCancel,
|
||||
onMethodChange,
|
||||
submitIcon = 'sendHorizontal',
|
||||
isLoading,
|
||||
@@ -43,8 +45,13 @@ export const UrlBar = memo(function UrlBar({
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
isLoading ? onCancel() : onSend();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className={className}>
|
||||
<form onSubmit={handleSubmit} className={className}>
|
||||
<Input
|
||||
autocompleteVariables
|
||||
ref={inputRef}
|
||||
@@ -81,8 +88,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
title="Send Request"
|
||||
type="submit"
|
||||
className="w-8 !h-auto my-0.5 mr-0.5"
|
||||
icon={isLoading ? 'update' : submitIcon}
|
||||
spin={isLoading}
|
||||
icon={isLoading ? 'x' : submitIcon}
|
||||
hotkeyAction="http_request.send"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { Compartment, EditorState, Transaction } from '@codemirror/state';
|
||||
import type { ViewUpdate } from '@codemirror/view';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||
import classNames from 'classnames';
|
||||
import { EditorView } from 'codemirror';
|
||||
@@ -148,14 +147,6 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||
}, [contentType, autocomplete, useTemplating, environment, workspace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cm.current === null) return;
|
||||
const { view } = cm.current;
|
||||
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: defaultValue ?? '' } });
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [forceUpdateKey]);
|
||||
|
||||
const classList = className?.split(/\s+/) ?? [];
|
||||
const bgClassList = classList
|
||||
.filter((c) => c.match(/(^|:)?bg-.+/)) // Find bg-* classes
|
||||
@@ -163,57 +154,59 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
.map((c) => c.replace(/^dark:bg-/, 'dark:!bg-')); // !important
|
||||
|
||||
// Initialize the editor when ref mounts
|
||||
const initEditorRef = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container === null) {
|
||||
cm.current?.view.destroy();
|
||||
cm.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let view: EditorView;
|
||||
try {
|
||||
const languageCompartment = new Compartment();
|
||||
const langExt = getLanguageExtension({
|
||||
contentType,
|
||||
useTemplating,
|
||||
autocomplete,
|
||||
environment,
|
||||
workspace,
|
||||
});
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: `${defaultValue ?? ''}`,
|
||||
extensions: [
|
||||
languageCompartment.of(langExt),
|
||||
placeholderCompartment.current.of([]),
|
||||
wrapLinesCompartment.current.of([]),
|
||||
...getExtensions({
|
||||
container,
|
||||
readOnly,
|
||||
singleLine,
|
||||
onChange: handleChange,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
onKeyDown: handleKeyDown,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
view = new EditorView({ state, parent: container });
|
||||
cm.current = { view, languageCompartment };
|
||||
syncGutterBg({ parent: container, bgClassList });
|
||||
if (autoFocus) {
|
||||
view.focus();
|
||||
const initEditorRef = useCallback(
|
||||
(container: HTMLDivElement | null) => {
|
||||
if (container === null) {
|
||||
cm.current?.view.destroy();
|
||||
cm.current = null;
|
||||
return;
|
||||
}
|
||||
if (autoSelect) {
|
||||
view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Failed to initialize Codemirror', e);
|
||||
}
|
||||
|
||||
let view: EditorView;
|
||||
try {
|
||||
const languageCompartment = new Compartment();
|
||||
const langExt = getLanguageExtension({
|
||||
contentType,
|
||||
useTemplating,
|
||||
autocomplete,
|
||||
environment,
|
||||
workspace,
|
||||
});
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: `${defaultValue ?? ''}`,
|
||||
extensions: [
|
||||
languageCompartment.of(langExt),
|
||||
placeholderCompartment.current.of([]),
|
||||
wrapLinesCompartment.current.of([]),
|
||||
...getExtensions({
|
||||
container,
|
||||
readOnly,
|
||||
singleLine,
|
||||
onChange: handleChange,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
onKeyDown: handleKeyDown,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
view = new EditorView({ state, parent: container });
|
||||
cm.current = { view, languageCompartment };
|
||||
syncGutterBg({ parent: container, bgClassList });
|
||||
if (autoFocus) {
|
||||
view.focus();
|
||||
}
|
||||
if (autoSelect) {
|
||||
view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Failed to initialize Codemirror', e);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
[forceUpdateKey],
|
||||
);
|
||||
|
||||
// Add bg classes to actions, so they appear over the text
|
||||
const decoratedActions = useMemo(() => {
|
||||
@@ -340,29 +333,13 @@ function getExtensions({
|
||||
|
||||
// Handle onChange
|
||||
EditorView.updateListener.of((update) => {
|
||||
// Only fire onChange if the document changed and the update was from user input. This prevents firing onChange when the document is updated when
|
||||
// changing pages (one request to another in header editor)
|
||||
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
|
||||
if (onChange && update.docChanged) {
|
||||
onChange.current?.(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
|
||||
// Make sure document has changed, ensuring user events like selections don't count.
|
||||
if (viewUpdate.docChanged) {
|
||||
// Check transactions for any that are direct user input, not changes from Y.js or another extension.
|
||||
for (const transaction of viewUpdate.transactions) {
|
||||
// Not using Transaction.isUserEvent because that only checks for a specific User event type ( "input", "delete", etc.). Checking the annotation directly allows for any type of user event.
|
||||
const userEventType = transaction.annotation(Transaction.userEvent);
|
||||
if (userEventType) return userEventType;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const syncGutterBg = ({
|
||||
parent,
|
||||
bgClassList,
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
|
||||
export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => {
|
||||
return (
|
||||
<div className="mx-auto h-full flex items-center text-gray-700 text-sm">
|
||||
<div className="h-full flex items-center justify-center text-gray-700 text-sm">
|
||||
<VStack space={2}>
|
||||
{hotkeys.map((hotkey) => (
|
||||
<HStack key={hotkey} className="grid grid-cols-2">
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { HStack } from './Stacks';
|
||||
import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
|
||||
|
||||
export function KeyValueRows({
|
||||
children,
|
||||
}: {
|
||||
children:
|
||||
| ReactElement<HTMLAttributes<HTMLTableColElement>>
|
||||
| ReactElement<HTMLAttributes<HTMLTableColElement>>[];
|
||||
}) {
|
||||
children = Array.isArray(children) ? children : [children];
|
||||
return (
|
||||
<table className="text-xs font-mono min-w-0 w-full mb-auto">
|
||||
<tbody className="divide-highlightSecondary">
|
||||
{children.map((child, i) => (
|
||||
<tr key={i}>{child}</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
label: ReactNode;
|
||||
@@ -8,17 +26,13 @@ interface Props {
|
||||
labelClassName?: string;
|
||||
}
|
||||
|
||||
export function KeyValueRows({ children }: { children: ReactNode }) {
|
||||
return <dl className="text-xs w-full font-mono divide-highlightSecondary">{children}</dl>;
|
||||
}
|
||||
|
||||
export function KeyValueRow({ label, value, labelClassName }: Props) {
|
||||
return (
|
||||
<HStack space={3} className="py-0.5">
|
||||
<dd className={classNames(labelClassName, 'w-1/3 text-gray-700 select-text cursor-text')}>
|
||||
<>
|
||||
<td className={classNames('py-1 pr-2 text-gray-700 select-text cursor-text', labelClassName)}>
|
||||
{label}
|
||||
</dd>
|
||||
<dt className="w-2/3 select-text cursor-text break-all">{value}</dt>
|
||||
</HStack>
|
||||
</td>
|
||||
<td className="py-1 cursor-text select-text break-all min-w-0">{value}</td>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user