mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 01:08:28 +02:00
Better status tags and delete request on key
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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 && <> • {activeResponse.elapsed}ms</>}
|
||||||
{activeResponse.statusReason && ` ${activeResponse.statusReason}`}
|
{activeResponse.body.length > 0 && (
|
||||||
</StatusColor>
|
<> • {(activeResponse.body.length / 1000).toFixed(1)} KB</>
|
||||||
•
|
)}
|
||||||
{activeResponse.elapsed}ms •
|
|
||||||
{(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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
47
src-web/components/core/StatusTag.tsx
Normal file
47
src-web/components/core/StatusTag.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
7
src-web/hooks/useLatestResponse.ts
Normal file
7
src-web/hooks/useLatestResponse.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user