Better status tags and delete request on key

This commit is contained in:
Gregory Schier
2023-04-04 12:36:30 -07:00
parent 7d154800a0
commit b3c461afdd
12 changed files with 125 additions and 74 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Yaak App</title> <title>Yaak App</title>
<script src="http://localhost:8097"></script> <!-- <script src="http://localhost:8097"></script>-->
<style> <style>
body { body {
background-color: white; background-color: white;

View File

@@ -2,6 +2,7 @@ interface Props {
data: string; data: string;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function ImageView({ data }: Props) { export function ImageView({ data }: Props) {
// const dataUri = `data:image/png;base64,${window.btoa(data)}`; // const dataUri = `data:image/png;base64,${window.btoa(data)}`;
return <div>Image preview not supported until binary response support is added</div>; return <div>Image preview not supported until binary response support is added</div>;

View File

@@ -15,7 +15,7 @@ interface Props {
export function RequestActionsDropdown({ requestId, children }: Props) { export function RequestActionsDropdown({ requestId, children }: Props) {
const request = useRequest(requestId ?? null); const request = useRequest(requestId ?? null);
const deleteRequest = useDeleteRequest(requestId ?? null); const deleteRequest = useDeleteRequest(request);
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true }); const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
const confirm = useConfirm(); const confirm = useConfirm();

View File

@@ -9,6 +9,7 @@ import { useResponses } from '../hooks/useResponses';
import { useResponseViewMode } from '../hooks/useResponseViewMode'; import { useResponseViewMode } from '../hooks/useResponseViewMode';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import { pluralize } from '../lib/pluralize'; import { pluralize } from '../lib/pluralize';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { CountBadge } from './core/CountBadge'; import { CountBadge } from './core/CountBadge';
@@ -17,7 +18,7 @@ import { Editor } from './core/Editor';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
import { StatusColor } from './core/StatusColor'; import { StatusTag } from './core/StatusTag';
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
import { Webview } from './core/Webview'; import { Webview } from './core/Webview';
@@ -93,7 +94,8 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
'shadow shadow-gray-100 dark:shadow-gray-0 relative', 'shadow shadow-gray-100 dark:shadow-gray-0 relative',
)} )}
> >
{activeResponse && ( {activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
{activeResponse && !activeResponse.error && !isResponseLoading(activeResponse) && (
<> <>
<HStack <HStack
alignItems="center" alignItems="center"
@@ -106,22 +108,15 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
{activeResponse && ( {activeResponse && (
<HStack alignItems="center" className="w-full"> <HStack alignItems="center" className="w-full">
<div className="whitespace-nowrap px-3"> <div className="whitespace-nowrap px-3">
<StatusColor statusCode={activeResponse.status}> <StatusTag response={activeResponse} />
{activeResponse.status} {activeResponse.elapsed > 0 && <>&nbsp;&bull;&nbsp;{activeResponse.elapsed}ms</>}
{activeResponse.statusReason && ` ${activeResponse.statusReason}`} {activeResponse.body.length > 0 && (
</StatusColor> <>&nbsp;&bull;&nbsp;{(activeResponse.body.length / 1000).toFixed(1)} KB</>
&nbsp;&bull;&nbsp; )}
{activeResponse.elapsed}ms &nbsp;&bull;&nbsp;
{(activeResponse.body.length / 1000).toFixed(1)} KB
</div> </div>
<Dropdown <Dropdown
items={[ items={[
{
label: viewMode === 'pretty' ? 'View Raw' : 'View Prettified',
onSelect: toggleViewMode,
},
{ type: 'separator', label: 'Actions' },
{ {
label: 'Clear Response', label: 'Clear Response',
onSelect: deleteResponse.mutate, onSelect: deleteResponse.mutate,
@@ -153,9 +148,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
)} )}
</HStack> </HStack>
{activeResponse?.error ? ( {
<Banner className="m-2">{activeResponse.error}</Banner>
) : (
<Tabs <Tabs
value={activeTab} value={activeTab}
onChangeValue={setActiveTab} onChangeValue={setActiveTab}
@@ -196,7 +189,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
) : null} ) : null}
</TabContent> </TabContent>
</Tabs> </Tabs>
)} }
</> </>
)} )}
</div> </div>

View File

@@ -4,15 +4,18 @@ import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useSta
import type { XYCoord } from 'react-dnd'; import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useLatestResponse } from '../hooks/useLatestResponse';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest'; import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { IconButton } from './core/IconButton'; import { Icon } from './core/Icon';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag';
import { DropMarker } from './DropMarker'; import { DropMarker } from './DropMarker';
import { RequestActionsDropdown } from './RequestActionsDropdown';
import { ToggleThemeButton } from './ToggleThemeButton'; import { ToggleThemeButton } from './ToggleThemeButton';
interface Props { interface Props {
@@ -136,7 +139,9 @@ const _SidebarItem = forwardRef(function SidebarItem(
{ className, requestName, requestId, workspaceId, active }: SidebarItemProps, { className, requestName, requestId, workspaceId, active }: SidebarItemProps,
ref: ForwardedRef<HTMLLIElement>, ref: ForwardedRef<HTMLLIElement>,
) { ) {
const latestResponse = useLatestResponse(requestId);
const updateRequest = useUpdateRequest(requestId); const updateRequest = useUpdateRequest(requestId);
const deleteRequest = useDeleteRequest(requestId);
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const handleSubmitNameEdit = useCallback( const handleSubmitNameEdit = useCallback(
@@ -159,12 +164,17 @@ const _SidebarItem = forwardRef(function SidebarItem(
e.preventDefault(); e.preventDefault();
setEditing(true); setEditing(true);
} }
if (active && (e.key === 'Backspace' || e.key === 'Delete')) {
e.preventDefault();
deleteRequest.mutate();
}
}, },
[active], [active, deleteRequest],
); );
const handleInputKeyDown = useCallback( const handleInputKeyDown = useCallback(
async (e: KeyboardEvent<HTMLInputElement>) => { async (e: KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
e.preventDefault(); e.preventDefault();
@@ -183,12 +193,12 @@ const _SidebarItem = forwardRef(function SidebarItem(
<li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}> <li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}>
<div className="relative"> <div className="relative">
<Button <Button
tabIndex={0}
color="custom" color="custom"
size="sm" size="sm"
to={`/workspaces/${workspaceId}/requests/${requestId}`} to={`/workspaces/${workspaceId}/requests/${requestId}`}
draggable={false} // Item should drag, not the link draggable={false} // Item should drag, not the link
onDoubleClick={() => setEditing(true)} onDoubleClick={() => setEditing(true)}
onClick={active ? () => setEditing(true) : undefined}
justify="start" justify="start"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={classnames( className={classnames(
@@ -196,8 +206,6 @@ const _SidebarItem = forwardRef(function SidebarItem(
active active
? 'bg-highlight text-gray-900' ? 'bg-highlight text-gray-900'
: 'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary', : 'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary',
// Move out of the way when trash is shown
'group-hover/item:pr-7',
)} )}
> >
{editing ? ( {editing ? (
@@ -213,19 +221,20 @@ const _SidebarItem = forwardRef(function SidebarItem(
{requestName || 'New Request'} {requestName || 'New Request'}
</span> </span>
)} )}
{latestResponse && (
<div className="ml-auto">
{isResponseLoading(latestResponse) ? (
<Icon spin size="sm" icon="update" />
) : (
<StatusTag
asBackground
className="px-0.5 rounded-sm font-mono text-2xs"
response={latestResponse}
/>
)}
</div>
)}
</Button> </Button>
<RequestActionsDropdown requestId={requestId}>
<IconButton
color="custom"
size="sm"
title="Request Options"
icon="dotsH"
className={classnames(
'absolute right-0 top-0 transition-opacity !opacity-0',
'group-hover/item:!opacity-100 focus-visible:!opacity-100',
)}
/>
</RequestActionsDropdown>
</div> </div>
</li> </li>
); );

View File

@@ -183,19 +183,16 @@
@apply bg-highlight text-gray-900; @apply bg-highlight text-gray-900;
} }
& > ul > li:hover {
@apply text-gray-800;
}
.cm-completionIcon { .cm-completionIcon {
@apply text-sm flex items-center pb-0.5 flex-shrink-0; @apply text-sm flex items-center pb-0.5 flex-shrink-0;
} }
.cm-completionLabel { .cm-completionLabel {
@apply text-gray-700;
} }
.cm-completionDetail { .cm-completionDetail {
@apply ml-auto pl-4; @apply ml-auto pl-6;
} }
} }
} }

View File

@@ -1,23 +0,0 @@
import classnames from 'classnames';
import type { ReactNode } from 'react';
interface Props {
statusCode: number;
children: ReactNode;
}
export function StatusColor({ statusCode, children }: Props) {
return (
<span
className={classnames(
statusCode >= 100 && statusCode < 200 && 'text-green-600',
statusCode >= 200 && statusCode < 300 && 'text-green-600',
statusCode >= 300 && statusCode < 400 && 'text-pink-600',
statusCode >= 400 && statusCode < 500 && 'text-orange-600',
statusCode >= 500 && statusCode < 600 && 'text-red-600',
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,47 @@
import classnames from 'classnames';
import type { HttpResponse } from '../../lib/models';
interface Props {
response: Pick<HttpResponse, 'status' | 'error'>;
className?: string;
asBackground?: boolean;
}
export function StatusTag({ asBackground, response, className }: Props) {
const { status, error } = response;
const label = error ? 'ERR' : status;
if (asBackground) {
return (
<span
className={classnames(
className,
'text-white bg-opacity-90',
status >= 0 && status < 100 && 'bg-red-600',
status >= 100 && status < 200 && 'bg-yellow-600',
status >= 200 && status < 300 && 'bg-green-600',
status >= 300 && status < 400 && 'bg-pink-600',
status >= 400 && status < 500 && 'bg-orange-600',
status >= 500 && 'bg-red-600',
)}
>
{label}
</span>
);
} else {
return (
<span
className={classnames(
className,
status >= 0 && status < 100 && 'text-red-600',
status >= 100 && status < 200 && 'text-green-600',
status >= 200 && status < 300 && 'text-green-600',
status >= 300 && status < 400 && 'text-pink-600',
status >= 400 && status < 500 && 'text-orange-600',
status >= 500 && 'text-red-600',
)}
>
{label}
</span>
);
}
}

View File

@@ -1,7 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { getRequest } from '../lib/store';
import { useActiveRequestId } from './useActiveRequestId'; import { useActiveRequestId } from './useActiveRequestId';
import { useConfirm } from './useConfirm';
import { requestsQueryKey } from './useRequests'; import { requestsQueryKey } from './useRequests';
import { responsesQueryKey } from './useResponses'; import { responsesQueryKey } from './useResponses';
import { useRoutes } from './useRoutes'; import { useRoutes } from './useRoutes';
@@ -10,9 +12,23 @@ export function useDeleteRequest(id: string | null) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const activeRequestId = useActiveRequestId(); const activeRequestId = useActiveRequestId();
const routes = useRoutes(); const routes = useRoutes();
return useMutation<HttpRequest, string>({ const confirm = useConfirm();
mutationFn: async () => invoke('delete_request', { requestId: id }), return useMutation<HttpRequest | null, string>({
onSuccess: async ({ workspaceId, id: requestId }) => { mutationFn: async () => {
const request = await getRequest(id);
const confirmed = await confirm({
title: 'Delete Request',
variant: 'delete',
description: `Are you sure you want to delete ${request?.name}?`,
});
if (!confirmed) return null;
return invoke('delete_request', { requestId: id });
},
onSuccess: async (request) => {
// Was it cancelled?
if (request === null) return;
const { workspaceId, id: requestId } = request;
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) => queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId), (requests ?? []).filter((r) => r.id !== requestId),

View File

@@ -1,8 +1,8 @@
import { useResponses } from './useResponses'; import { isResponseLoading } from '../lib/models';
import { useLatestResponse } from './useLatestResponse';
export function useIsResponseLoading(requestId: string | null): boolean { export function useIsResponseLoading(requestId: string | null): boolean {
const responses = useResponses(requestId); const response = useLatestResponse(requestId);
const response = responses[responses.length - 1]; if (response === null) return false;
if (!response) return false; return isResponseLoading(response);
return !(response.body || response.status || response.error);
} }

View File

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

View File

@@ -72,3 +72,7 @@ export interface HttpResponse extends BaseModel {
readonly url: string; readonly url: string;
readonly headers: HttpHeader[]; readonly headers: HttpHeader[];
} }
export function isResponseLoading(response: HttpResponse): boolean {
return !(response.body || response.status || response.error);
}