New loading icon

This commit is contained in:
Gregory Schier
2025-02-04 06:52:25 -08:00
parent 4d80c8d993
commit 25c1b04043
11 changed files with 82 additions and 33 deletions

12
package-lock.json generated
View File

@@ -8873,12 +8873,12 @@
} }
}, },
"node_modules/lucide-react": { "node_modules/lucide-react": {
"version": "0.439.0", "version": "0.474.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.439.0.tgz", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz",
"integrity": "sha512-PafSWvDTpxdtNEndS2HIHxcNAbd54OaqSYJO90/b63rab2HWYqDbH194j0i82ZFdWOAcf0AHinRykXRRK2PJbw==", "integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==",
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/make-cancellable-promise": { "node_modules/make-cancellable-promise": {
@@ -15491,7 +15491,7 @@
}, },
"packages/plugin-runtime-types": { "packages/plugin-runtime-types": {
"name": "@yaakapp/api", "name": "@yaakapp/api",
"version": "0.4.0", "version": "0.4.1",
"dependencies": { "dependencies": {
"@types/node": "^22.5.4" "@types/node": "^22.5.4"
}, },
@@ -15657,7 +15657,7 @@
"history": "^5.3.0", "history": "^5.3.0",
"jotai": "^2.9.3", "jotai": "^2.9.3",
"js-md5": "^0.8.3", "js-md5": "^0.8.3",
"lucide-react": "^0.439.0", "lucide-react": "^0.474.0",
"mime": "^4.0.4", "mime": "^4.0.4",
"nanoid": "^5.0.9", "nanoid": "^5.0.9",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",

View File

@@ -14,6 +14,7 @@ import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { JsonAttributeTree } from './core/JsonAttributeTree'; import { JsonAttributeTree } from './core/JsonAttributeTree';
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
import { LoadingIcon } from './core/LoadingIcon';
import { Separator } from './core/Separator'; import { Separator } from './core/Separator';
import { SplitLayout } from './core/SplitLayout'; import { SplitLayout } from './core/SplitLayout';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
@@ -70,7 +71,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
<HStack space={2}> <HStack space={2}>
<span>{events.length} Messages</span> <span>{events.length} Messages</span>
{activeConnection.state !== 'closed' && ( {activeConnection.state !== 'closed' && (
<Icon icon="refresh" size="sm" spin className="text-text-subtlest" /> <LoadingIcon size="sm" className="text-text-subtlest" />
)} )}
</HStack> </HStack>
<div className="ml-auto"> <div className="ml-auto">

View File

@@ -11,7 +11,7 @@ import { Banner } from './core/Banner';
import { CountBadge } from './core/CountBadge'; import { CountBadge } from './core/CountBadge';
import { DurationTag } from './core/DurationTag'; import { DurationTag } from './core/DurationTag';
import { HotKeyList } from './core/HotKeyList'; import { HotKeyList } from './core/HotKeyList';
import { Icon } from './core/Icon'; import { LoadingIcon } from './core/LoadingIcon';
import { SizeTag } from './core/SizeTag'; import { SizeTag } from './core/SizeTag';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag'; import { StatusTag } from './core/StatusTag';
@@ -120,7 +120,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
'whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm', 'whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm',
)} )}
> >
{activeResponse.state !== 'closed' && <Icon size="sm" icon="refresh" spin />} {activeResponse.state !== 'closed' && <LoadingIcon size="sm" />}
<StatusTag showReason response={activeResponse} /> <StatusTag showReason response={activeResponse} />
<span>&bull;</span> <span>&bull;</span>
<DurationTag <DurationTag
@@ -159,7 +159,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<ConfirmLargeResponse response={activeResponse}> <ConfirmLargeResponse response={activeResponse}>
{activeResponse.state === 'initialized' ? ( {activeResponse.state === 'initialized' ? (
<EmptyStateText> <EmptyStateText>
<Icon size="xl" spin icon="refresh" className="text-text-subtlest" /> <LoadingIcon size="xl" className="text-text-subtlest" />
</EmptyStateText> </EmptyStateText>
) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? ( ) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? (
<EmptyStateText>Empty </EmptyStateText> <EmptyStateText>Empty </EmptyStateText>
@@ -217,7 +217,7 @@ function EnsureCompleteResponse({
if (response.state !== 'closed') { if (response.state !== 'closed') {
return ( return (
<EmptyStateText> <EmptyStateText>
<Icon icon="refresh" spin /> <LoadingIcon />
</EmptyStateText> </EmptyStateText>
); );
} }

View File

@@ -15,6 +15,7 @@ import { Button } from './core/Button';
import { Editor } from './core/Editor/Editor'; import { Editor } from './core/Editor/Editor';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { LoadingIcon } from './core/LoadingIcon';
import { Separator } from './core/Separator'; import { Separator } from './core/Separator';
import { SplitLayout } from './core/SplitLayout'; import { SplitLayout } from './core/SplitLayout';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
@@ -71,7 +72,7 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
<HStack className="pl-3 mb-1 font-mono text-sm"> <HStack className="pl-3 mb-1 font-mono text-sm">
<HStack space={2}> <HStack space={2}>
{activeConnection.state !== 'closed' && ( {activeConnection.state !== 'closed' && (
<Icon icon="refresh" size="sm" spin className="text-text-subtlest" /> <LoadingIcon size="sm" className="text-text-subtlest" />
)} )}
<StatusTag showReason response={activeConnection} /> <StatusTag showReason response={activeConnection} />
<span>&bull;</span> <span>&bull;</span>

View File

@@ -5,6 +5,7 @@ import type { HotkeyAction } from '../../hooks/useHotKey';
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey'; import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
import { trackEvent } from '../../lib/analytics'; import { trackEvent } from '../../lib/analytics';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LoadingIcon } from './LoadingIcon';
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onChange'> & { export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onChange'> & {
innerClassName?: string; innerClassName?: string;
@@ -125,7 +126,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
{...props} {...props}
> >
{isLoading ? ( {isLoading ? (
<Icon icon="refresh" size={size} className="animate-spin mr-1" /> <LoadingIcon size={size} className="mr-1" />
) : leftSlot ? ( ) : leftSlot ? (
<div className="mr-2">{leftSlot}</div> <div className="mr-2">{leftSlot}</div>
) : null} ) : null}

View File

@@ -0,0 +1,35 @@
import classNames from 'classnames';
interface Props {
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
className?: string;
}
export function LoadingIcon({ size = 'md', className }: Props) {
const classes = classNames(
className,
'text-inherit flex-shrink-0',
size === 'xl' && 'h-6 w-6',
size === 'lg' && 'h-5 w-5',
size === 'md' && 'h-4 w-4',
size === 'sm' && 'h-3.5 w-3.5',
size === 'xs' && 'h-3 w-3',
size === '2xs' && 'h-2.5 w-2.5',
'animate-spin',
);
return (
<div
className={classNames(
classes,
'border-[currentColor] border-b-transparent rounded-full',
size === 'xl' && 'border-[0.2rem]',
size === 'lg' && 'border-[0.16rem]',
size === 'md' && 'border-[0.13rem]',
size === 'sm' && 'border-[0.1rem]',
size === 'xs' && 'border-[0.08rem]',
size === '2xs' && 'border-[0.06rem]',
)}
/>
);
}

View File

@@ -1,10 +1,10 @@
import { useSaveResponse } from '../../hooks/useSaveResponse';
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from '@yaakapp-internal/models';
import { useSaveResponse } from '../../hooks/useSaveResponse';
import { getContentTypeHeader } from '../../lib/model_util'; import { getContentTypeHeader } from '../../lib/model_util';
import { Banner } from '../core/Banner'; import { Banner } from '../core/Banner';
import { Button } from '../core/Button'; import { Button } from '../core/Button';
import { Icon } from '../core/Icon';
import { InlineCode } from '../core/InlineCode'; import { InlineCode } from '../core/InlineCode';
import { LoadingIcon } from '../core/LoadingIcon';
import { EmptyStateText } from '../EmptyStateText'; import { EmptyStateText } from '../EmptyStateText';
interface Props { interface Props {
@@ -19,7 +19,7 @@ export function BinaryViewer({ response }: Props) {
if (response.state === 'closed') { if (response.state === 'closed') {
return ( return (
<EmptyStateText> <EmptyStateText>
<Icon icon="refresh" spin /> <LoadingIcon size="sm" />
</EmptyStateText> </EmptyStateText>
); );
} }

View File

@@ -14,9 +14,7 @@ import { getActiveRequest } from '../../hooks/useActiveRequest';
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems'; import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
import { useDeleteAnyRequest } from '../../hooks/useDeleteAnyRequest'; import { useDeleteAnyRequest } from '../../hooks/useDeleteAnyRequest';
import { useGrpcConnections } from '../../hooks/useGrpcConnections';
import { useHotKey } from '../../hooks/useHotKey'; import { useHotKey } from '../../hooks/useHotKey';
import { useHttpResponses } from '../../hooks/useHttpResponses';
import { useSidebarHidden } from '../../hooks/useSidebarHidden'; import { useSidebarHidden } from '../../hooks/useSidebarHidden';
import { getSidebarCollapsedMap } from '../../hooks/useSidebarItemCollapsed'; import { getSidebarCollapsedMap } from '../../hooks/useSidebarItemCollapsed';
import { useUpdateAnyFolder } from '../../hooks/useUpdateAnyFolder'; import { useUpdateAnyFolder } from '../../hooks/useUpdateAnyFolder';
@@ -51,8 +49,6 @@ export function Sidebar({ className }: Props) {
const [hidden, setHidden] = useSidebarHidden(); const [hidden, setHidden] = useSidebarHidden();
const sidebarRef = useRef<HTMLElement>(null); const sidebarRef = useRef<HTMLElement>(null);
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const httpResponses = useHttpResponses();
const grpcConnections = useGrpcConnections();
const [hasFocus, setHasFocus] = useState<boolean>(false); const [hasFocus, setHasFocus] = useState<boolean>(false);
const [selectedId, setSelectedId] = useAtom(sidebarSelectedIdAtom); const [selectedId, setSelectedId] = useAtom(sidebarSelectedIdAtom);
const [selectedTree, setSelectedTree] = useState<SidebarTreeNode | null>(null); const [selectedTree, setSelectedTree] = useState<SidebarTreeNode | null>(null);
@@ -372,8 +368,6 @@ export function Sidebar({ className }: Props) {
<SidebarItems <SidebarItems
treeParentMap={treeParentMap} treeParentMap={treeParentMap}
selectedTree={selectedTree} selectedTree={selectedTree}
httpResponses={httpResponses}
grpcConnections={grpcConnections}
tree={tree} tree={tree}
draggingId={draggingId} draggingId={draggingId}
onSelect={handleSelect} onSelect={handleSelect}

View File

@@ -1,4 +1,9 @@
import type { AnyModel, GrpcConnection, HttpResponse } from '@yaakapp-internal/models'; import type {
AnyModel,
GrpcConnection,
HttpResponse,
WebsocketConnection,
} from '@yaakapp-internal/models';
import classNames from 'classnames'; import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
@@ -17,6 +22,7 @@ import { getWebsocketRequest } from '../../hooks/useWebsocketRequests';
import { jotaiStore } from '../../lib/jotai'; import { jotaiStore } from '../../lib/jotai';
import { HttpMethodTag } from '../core/HttpMethodTag'; import { HttpMethodTag } from '../core/HttpMethodTag';
import { Icon } from '../core/Icon'; import { Icon } from '../core/Icon';
import { LoadingIcon } from '../core/LoadingIcon';
import { StatusTag } from '../core/StatusTag'; import { StatusTag } from '../core/StatusTag';
import type { SidebarTreeNode } from './Sidebar'; import type { SidebarTreeNode } from './Sidebar';
import { sidebarSelectedIdAtom } from './SidebarAtoms'; import { sidebarSelectedIdAtom } from './SidebarAtoms';
@@ -39,6 +45,7 @@ export type SidebarItemProps = {
child: SidebarTreeNode; child: SidebarTreeNode;
latestHttpResponse: HttpResponse | null; latestHttpResponse: HttpResponse | null;
latestGrpcConnection: GrpcConnection | null; latestGrpcConnection: GrpcConnection | null;
latestWebsocketConnection: WebsocketConnection | null;
} & Pick<SidebarItemsProps, 'onSelect'>; } & Pick<SidebarItemsProps, 'onSelect'>;
type DragItem = { type DragItem = {
@@ -58,6 +65,7 @@ export const SidebarItem = memo(function SidebarItem({
className, className,
latestHttpResponse, latestHttpResponse,
latestGrpcConnection, latestGrpcConnection,
latestWebsocketConnection,
children, children,
}: SidebarItemProps) { }: SidebarItemProps) {
const ref = useRef<HTMLLIElement>(null); const ref = useRef<HTMLLIElement>(null);
@@ -283,13 +291,19 @@ export const SidebarItem = memo(function SidebarItem({
{latestGrpcConnection ? ( {latestGrpcConnection ? (
<div className="ml-auto"> <div className="ml-auto">
{latestGrpcConnection.state !== 'closed' && ( {latestGrpcConnection.state !== 'closed' && (
<Icon spin size="sm" icon="update" className="text-text-subtlest" /> <LoadingIcon size="sm" className="text-text-subtlest" />
)}
</div>
) : latestWebsocketConnection ? (
<div className="ml-auto">
{latestWebsocketConnection.state !== 'closed' && (
<LoadingIcon size="sm" className="text-text-subtlest" />
)} )}
</div> </div>
) : latestHttpResponse ? ( ) : latestHttpResponse ? (
<div className="ml-auto"> <div className="ml-auto">
{latestHttpResponse.state !== 'closed' ? ( {latestHttpResponse.state !== 'closed' ? (
<Icon spin size="sm" icon="refresh" className="text-text-subtlest" /> <LoadingIcon size="sm" className="text-text-subtlest" />
) : ( ) : (
<StatusTag className="text-xs" response={latestHttpResponse} /> <StatusTag className="text-xs" response={latestHttpResponse} />
)} )}

View File

@@ -1,6 +1,8 @@
import type { GrpcConnection, HttpResponse } from '@yaakapp-internal/models';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { Fragment, memo } from 'react'; import React, { Fragment, memo } from 'react';
import { useGrpcConnections } from '../../hooks/useGrpcConnections';
import { useHttpResponses } from '../../hooks/useHttpResponses';
import { useWebsocketConnections } from '../../hooks/useWebsocketConnections';
import { VStack } from '../core/Stacks'; import { VStack } from '../core/Stacks';
import { DropMarker } from '../DropMarker'; import { DropMarker } from '../DropMarker';
import type { SidebarTreeNode } from './Sidebar'; import type { SidebarTreeNode } from './Sidebar';
@@ -17,8 +19,6 @@ export interface SidebarItemsProps {
handleEnd: (id: string) => void; handleEnd: (id: string) => void;
handleDragStart: (id: string) => void; handleDragStart: (id: string) => void;
onSelect: (requestId: string) => void; onSelect: (requestId: string) => void;
httpResponses: HttpResponse[];
grpcConnections: GrpcConnection[];
} }
export const SidebarItems = memo(function SidebarItems({ export const SidebarItems = memo(function SidebarItems({
@@ -32,9 +32,11 @@ export const SidebarItems = memo(function SidebarItems({
handleEnd, handleEnd,
handleMove, handleMove,
handleDragStart, handleDragStart,
httpResponses,
grpcConnections,
}: SidebarItemsProps) { }: SidebarItemsProps) {
const httpResponses = useHttpResponses();
const grpcConnections = useGrpcConnections();
const websocketConnections = useWebsocketConnections();
return ( return (
<VStack <VStack
as="ul" as="ul"
@@ -57,6 +59,9 @@ export const SidebarItems = memo(function SidebarItems({
itemModel={child.model} itemModel={child.model}
latestHttpResponse={httpResponses.find((r) => r.requestId === child.id) ?? null} latestHttpResponse={httpResponses.find((r) => r.requestId === child.id) ?? null}
latestGrpcConnection={grpcConnections.find((c) => c.requestId === child.id) ?? null} latestGrpcConnection={grpcConnections.find((c) => c.requestId === child.id) ?? null}
latestWebsocketConnection={
websocketConnections.find((c) => c.requestId === child.id) ?? null
}
onMove={handleMove} onMove={handleMove}
onEnd={handleEnd} onEnd={handleEnd}
onSelect={onSelect} onSelect={onSelect}
@@ -71,8 +76,6 @@ export const SidebarItems = memo(function SidebarItems({
handleMove={handleMove} handleMove={handleMove}
hoveredIndex={hoveredIndex} hoveredIndex={hoveredIndex}
hoveredTree={hoveredTree} hoveredTree={hoveredTree}
httpResponses={httpResponses}
grpcConnections={grpcConnections}
onSelect={onSelect} onSelect={onSelect}
selectedTree={selectedTree} selectedTree={selectedTree}
tree={child} tree={child}

View File

@@ -49,7 +49,7 @@
"history": "^5.3.0", "history": "^5.3.0",
"jotai": "^2.9.3", "jotai": "^2.9.3",
"js-md5": "^0.8.3", "js-md5": "^0.8.3",
"lucide-react": "^0.439.0", "lucide-react": "^0.474.0",
"mime": "^4.0.4", "mime": "^4.0.4",
"nanoid": "^5.0.9", "nanoid": "^5.0.9",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",