mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-21 17:09:09 +01:00
Refactor into grpc events
This commit is contained in:
@@ -6,7 +6,7 @@ interface Props {
|
||||
|
||||
export function EmptyStateText({ children }: Props) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-highlight h-full text-gray-400 flex items-center justify-center">
|
||||
<div className="rounded-lg border border-dashed border-highlight h-full py-2 text-gray-400 flex items-center justify-center">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
||||
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
|
||||
import { grpcMessagesQueryKey } from '../hooks/useGrpcMessages';
|
||||
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
|
||||
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
|
||||
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
|
||||
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
|
||||
@@ -53,8 +53,8 @@ export function GlobalHooks() {
|
||||
? httpResponsesQueryKey(payload)
|
||||
: payload.model === 'grpc_connection'
|
||||
? grpcConnectionsQueryKey(payload)
|
||||
: payload.model === 'grpc_message'
|
||||
? grpcMessagesQueryKey(payload)
|
||||
: payload.model === 'grpc_event'
|
||||
? grpcEventsQueryKey(payload)
|
||||
: payload.model === 'grpc_request'
|
||||
? grpcRequestsQueryKey(payload)
|
||||
: payload.model === 'workspace'
|
||||
@@ -107,8 +107,8 @@ export function GlobalHooks() {
|
||||
queryClient.setQueryData(grpcRequestsQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'grpc_connection') {
|
||||
queryClient.setQueryData(grpcConnectionsQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'grpc_message') {
|
||||
queryClient.setQueryData(grpcMessagesQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'grpc_event') {
|
||||
queryClient.setQueryData(grpcEventsQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'key_value') {
|
||||
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
|
||||
} else if (payload.model === 'cookie_jar') {
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useEffect, useMemo } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useGrpc } from '../hooks/useGrpc';
|
||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||
import { useGrpcMessages } from '../hooks/useGrpcMessages';
|
||||
import { useGrpcEvents } from '../hooks/useGrpcEvents';
|
||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||
import { Banner } from './core/Banner';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
@@ -21,7 +21,7 @@ export function GrpcConnectionLayout({ style }: Props) {
|
||||
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
|
||||
const connections = useGrpcConnections(activeRequest?.id ?? null);
|
||||
const activeConnection = connections[0] ?? null;
|
||||
const messages = useGrpcMessages(activeConnection?.id ?? null);
|
||||
const messages = useGrpcEvents(activeConnection?.id ?? null);
|
||||
const grpc = useGrpc(activeRequest, activeConnection);
|
||||
|
||||
const services = grpc.reflect.data ?? null;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import classNames from 'classnames';
|
||||
import { format } from 'date-fns';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { format, addMilliseconds } from 'date-fns';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||
import { useGrpcMessages } from '../hooks/useGrpcMessages';
|
||||
import type { GrpcRequest } from '../lib/models';
|
||||
import { useGrpcEvents } from '../hooks/useGrpcEvents';
|
||||
import type { GrpcEvent, GrpcRequest } from '../lib/models';
|
||||
import { Icon } from './core/Icon';
|
||||
import { JsonAttributeTree } from './core/JsonAttributeTree';
|
||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||
import { Separator } from './core/Separator';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
|
||||
|
||||
interface Props {
|
||||
@@ -25,34 +27,80 @@ interface Props {
|
||||
| 'no-method';
|
||||
}
|
||||
|
||||
const CONNECTION_RESPONSE_EVENT_ID = 'connection_response';
|
||||
|
||||
export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: Props) {
|
||||
const [activeMessageId, setActiveMessageId] = useState<string | null>(null);
|
||||
const [activeEventId, setActiveEventId] = useState<string | null>(null);
|
||||
const connections = useGrpcConnections(activeRequest.id ?? null);
|
||||
const activeConnection = connections[0] ?? null;
|
||||
const messages = useGrpcMessages(activeConnection?.id ?? null);
|
||||
const ogEvents = useGrpcEvents(activeConnection?.id ?? null);
|
||||
|
||||
const activeMessage = useMemo(
|
||||
() => messages.find((m) => m.id === activeMessageId) ?? null,
|
||||
[activeMessageId, messages],
|
||||
const events = useMemo(() => {
|
||||
const createdAt =
|
||||
activeConnection != null &&
|
||||
addMilliseconds(activeConnection.createdAt, activeConnection.elapsed)
|
||||
.toISOString()
|
||||
.replace('Z', '');
|
||||
if (activeConnection == null || activeConnection.elapsed === 0) {
|
||||
return ogEvents;
|
||||
} else if (activeConnection.error != null) {
|
||||
return [
|
||||
...ogEvents,
|
||||
{
|
||||
id: CONNECTION_RESPONSE_EVENT_ID,
|
||||
eventType: 'error',
|
||||
content: activeConnection.error,
|
||||
metadata: activeConnection.trailers,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
} as GrpcEvent,
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
...ogEvents,
|
||||
{
|
||||
id: CONNECTION_RESPONSE_EVENT_ID,
|
||||
eventType: activeConnection.status === 0 ? 'connection_response' : 'error',
|
||||
content: `Connection ${GRPC_CODES[activeConnection.status] ?? 'closed'}`,
|
||||
metadata: activeConnection.trailers,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
} as GrpcEvent,
|
||||
];
|
||||
}
|
||||
}, [activeConnection, ogEvents]);
|
||||
|
||||
const activeEvent = useMemo(
|
||||
() => events.find((m) => m.id === activeEventId) ?? null,
|
||||
[activeEventId, events],
|
||||
);
|
||||
|
||||
// Set active message to the first message received if unary
|
||||
useEffect(() => {
|
||||
if (events.length === 0 || activeEvent != null || methodType !== 'unary') {
|
||||
return;
|
||||
}
|
||||
setActiveEventId(events.find((m) => m.eventType === 'server_message')?.id ?? null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [events.length]);
|
||||
|
||||
return (
|
||||
<SplitLayout
|
||||
layout="vertical"
|
||||
style={style}
|
||||
name={methodType === 'unary' ? 'grpc_messages_unary' : 'grpc_messages_streaming'}
|
||||
defaultRatio={methodType === 'unary' ? 0.75 : 0.3}
|
||||
name="grpc_events"
|
||||
defaultRatio={0.4}
|
||||
minHeightPx={20}
|
||||
firstSlot={() => (
|
||||
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
|
||||
<HStack className="pl-3 mb-1 font-mono" alignItems="center">
|
||||
<HStack alignItems="center" space={2}>
|
||||
<span>{messages.filter((m) => !m.isInfo).length} messages</span>
|
||||
{activeConnection?.elapsed === 0 && (
|
||||
<Icon icon="refresh" size="sm" spin className="text-gray-600" />
|
||||
)}
|
||||
</HStack>
|
||||
{activeConnection && (
|
||||
firstSlot={() =>
|
||||
activeConnection && (
|
||||
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
|
||||
<HStack className="pl-3 mb-1 font-mono" alignItems="center">
|
||||
<HStack alignItems="center" space={2}>
|
||||
<span>{events.length} messages</span>
|
||||
{activeConnection.elapsed === 0 && (
|
||||
<Icon icon="refresh" size="sm" spin className="text-gray-600" />
|
||||
)}
|
||||
</HStack>
|
||||
<RecentConnectionsDropdown
|
||||
connections={connections}
|
||||
activeConnection={activeConnection}
|
||||
@@ -60,63 +108,59 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
||||
// todo
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
<div className="overflow-y-auto h-full">
|
||||
{...messages.map((m) => (
|
||||
<HStack
|
||||
role="button"
|
||||
key={m.id}
|
||||
space={2}
|
||||
onClick={() => {
|
||||
if (m.id === activeMessageId) setActiveMessageId(null);
|
||||
else setActiveMessageId(m.id);
|
||||
}}
|
||||
alignItems="center"
|
||||
className={classNames(
|
||||
'px-2 py-1 font-mono cursor-default group',
|
||||
m === activeMessage && '!bg-highlight',
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={
|
||||
m.isInfo ? 'text-gray-600' : m.isServer ? 'text-blue-600' : 'text-green-600'
|
||||
}
|
||||
icon={m.isInfo ? 'info' : m.isServer ? 'arrowBigDownDash' : 'arrowBigUpDash'}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'w-full truncate text-gray-800 text-2xs group-hover:text-gray-900',
|
||||
m.id === activeMessageId && 'text-gray-900',
|
||||
)}
|
||||
</HStack>
|
||||
<div className="overflow-y-auto h-full">
|
||||
{...events.map((m) => (
|
||||
<MessageRow
|
||||
key={m.id}
|
||||
isActive={m.id === activeEventId}
|
||||
eventType={m.eventType}
|
||||
timestamp={m.createdAt}
|
||||
onClick={() => {
|
||||
if (m.id === activeEventId) setActiveEventId(null);
|
||||
else setActiveEventId(m.id);
|
||||
}}
|
||||
>
|
||||
{m.message}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'text-gray-600 text-2xs group-hover:text-gray-700',
|
||||
m.id === activeMessageId && 'text-gray-700',
|
||||
)}
|
||||
>
|
||||
{format(m.createdAt, 'HH:mm:ss')}
|
||||
</div>
|
||||
</HStack>
|
||||
))}
|
||||
{m.content}
|
||||
</MessageRow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
secondSlot={
|
||||
activeMessage &&
|
||||
activeEvent &&
|
||||
(() => (
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="pb-3 px-2">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="pl-2 overflow-y-auto">
|
||||
{activeMessage.isInfo ? (
|
||||
<span>{activeMessage.message}</span>
|
||||
{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>
|
||||
<JsonAttributeTree attrValue={JSON.parse(activeEvent?.content ?? '{}')} />
|
||||
</>
|
||||
) : (
|
||||
<JsonAttributeTree attrValue={JSON.parse(activeMessage?.message ?? '{}')} />
|
||||
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="mb-2 select-text cursor-text font-semibold">
|
||||
{activeEvent.content}
|
||||
</div>
|
||||
{Object.keys(activeEvent.metadata).length === 0 ? (
|
||||
<EmptyStateText>
|
||||
No {activeEvent.eventType === 'connection_response' ? 'trailers' : 'metadata'}
|
||||
</EmptyStateText>
|
||||
) : (
|
||||
<KeyValueRows>
|
||||
{Object.entries(activeEvent.metadata).map(([key, value]) => (
|
||||
<KeyValueRow key={key} label={key} value={value} />
|
||||
))}
|
||||
</KeyValueRows>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,3 +169,89 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MessageRow({
|
||||
onClick,
|
||||
isActive,
|
||||
eventType,
|
||||
children,
|
||||
timestamp,
|
||||
}: {
|
||||
onClick?: () => void;
|
||||
isActive?: boolean;
|
||||
eventType: GrpcEvent['eventType'];
|
||||
children: ReactNode;
|
||||
timestamp: string;
|
||||
}) {
|
||||
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'
|
||||
? 'text-orange-600'
|
||||
: eventType === 'connection_response'
|
||||
? 'text-green-600'
|
||||
: 'text-gray-700'
|
||||
}
|
||||
title={
|
||||
eventType === 'server_message'
|
||||
? 'Server message'
|
||||
: eventType === 'client_message'
|
||||
? 'Client message'
|
||||
: eventType === 'error'
|
||||
? 'Error'
|
||||
: eventType === 'connection_response'
|
||||
? 'Connection response'
|
||||
: undefined
|
||||
}
|
||||
icon={
|
||||
eventType === 'server_message'
|
||||
? 'arrowBigDownDash'
|
||||
: eventType === 'client_message'
|
||||
? 'arrowBigUpDash'
|
||||
: eventType === 'error'
|
||||
? 'alert'
|
||||
: eventType === 'connection_response'
|
||||
? 'check'
|
||||
: 'info'
|
||||
}
|
||||
/>
|
||||
<div className={classNames('w-full truncate text-2xs')}>{children}</div>
|
||||
<div className={classNames('opacity-50 text-2xs')}>
|
||||
{format(timestamp + 'Z', 'HH:mm:ss.SSS')}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
@@ -218,7 +218,7 @@ export function GrpcConnectionSetupPane({
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title={methodType === 'unary' ? 'Send' : 'Connect'}
|
||||
hotkeyAction={isStreaming ? undefined : 'http_request.send'}
|
||||
hotkeyAction="grpc_request.send"
|
||||
onClick={handleConnect}
|
||||
disabled={methodType === 'no-schema' || methodType === 'no-method'}
|
||||
icon={
|
||||
@@ -240,24 +240,24 @@ export function GrpcConnectionSetupPane({
|
||||
disabled={!isStreaming}
|
||||
/>
|
||||
)}
|
||||
{methodType === 'client_streaming' && isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
onClick={onCommit}
|
||||
icon="check"
|
||||
/>
|
||||
)}
|
||||
{(methodType === 'client_streaming' || methodType === 'streaming') && isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
hotkeyAction="grpc_request.send"
|
||||
onClick={() => onSend({ message: activeRequest.message ?? '' })}
|
||||
icon="sendHorizontal"
|
||||
/>
|
||||
<>
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
onClick={onCommit}
|
||||
icon="check"
|
||||
/>
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
hotkeyAction="grpc_request.send"
|
||||
onClick={() => onSend({ message: activeRequest.message ?? '' })}
|
||||
icon="sendHorizontal"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { shell } from '@tauri-apps/api';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||
import { Separator } from './core/Separator';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
@@ -13,16 +11,16 @@ interface Props {
|
||||
export function ResponseHeaders({ response }: Props) {
|
||||
return (
|
||||
<div className="overflow-auto h-full pb-4">
|
||||
<dl className="text-xs w-full font-mono flex flex-col">
|
||||
<KeyValueRows>
|
||||
{response.headers.map((h, i) => (
|
||||
<Row key={i} label={h.name} value={h.value} labelClassName="!text-violet-600" />
|
||||
<KeyValueRow key={i} label={h.name} value={h.value} labelClassName="!text-violet-600" />
|
||||
))}
|
||||
</dl>
|
||||
</KeyValueRows>
|
||||
<Separator className="my-4">Other Info</Separator>
|
||||
<dl className="text-xs w-full font-mono divide-highlightSecondary">
|
||||
<Row label="Version" value={response.version} />
|
||||
<Row label="Remote Address" value={response.remoteAddr} />
|
||||
<Row
|
||||
<KeyValueRows>
|
||||
<KeyValueRow label="Version" value={response.version} />
|
||||
<KeyValueRow label="Remote Address" value={response.remoteAddr} />
|
||||
<KeyValueRow
|
||||
label={
|
||||
<div className="flex items-center">
|
||||
URL
|
||||
@@ -41,26 +39,7 @@ export function ResponseHeaders({ response }: Props) {
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</dl>
|
||||
</KeyValueRows>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({
|
||||
label,
|
||||
value,
|
||||
labelClassName,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
value: ReactNode;
|
||||
labelClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<HStack space={3} className="py-0.5">
|
||||
<dd className={classNames(labelClassName, 'w-1/3 text-gray-700 select-text cursor-text')}>
|
||||
{label}
|
||||
</dd>
|
||||
<dt className="w-2/3 select-text cursor-text break-all">{value}</dt>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
},
|
||||
{
|
||||
key: 'create-grpc-request',
|
||||
label: 'GRPC Call',
|
||||
label: 'gRPC Call',
|
||||
onSelect: () => createGrpcRequest.mutate({}),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
<RequestMethodDropdown
|
||||
method={method}
|
||||
onChange={onMethodChange}
|
||||
className="!h-auto my-0.5 mr-0.5"
|
||||
className="!h-auto my-0.5 ml-0.5"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||
import { Button } from './core/Button';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
||||
import { HttpRequestLayout } from './HttpRequestLayout';
|
||||
import { Overlay } from './Overlay';
|
||||
@@ -160,7 +161,19 @@ export default function Workspace() {
|
||||
<WorkspaceHeader className="pointer-events-none" />
|
||||
</HeaderSize>
|
||||
{activeRequest == null ? (
|
||||
<HotKeyList hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']} />
|
||||
<HotKeyList
|
||||
hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']}
|
||||
bottomSlot={
|
||||
<HStack space={1} justifyContent="center" className="mt-3">
|
||||
<Button size="sm" color="gray">
|
||||
Import
|
||||
</Button>
|
||||
<Button size="sm" color="gray">
|
||||
New Request
|
||||
</Button>
|
||||
</HStack>
|
||||
}
|
||||
/>
|
||||
) : activeRequest.model === 'grpc_request' ? (
|
||||
<GrpcConnectionLayout style={body} />
|
||||
) : (
|
||||
|
||||
@@ -6,9 +6,10 @@ import { HStack, VStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
hotkeys: HotkeyAction[];
|
||||
bottomSlot?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const HotKeyList = ({ hotkeys }: Props) => {
|
||||
export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => {
|
||||
return (
|
||||
<div className="mx-auto h-full flex items-center text-gray-700 text-sm">
|
||||
<VStack space={2}>
|
||||
@@ -18,6 +19,7 @@ export const HotKeyList = ({ hotkeys }: Props) => {
|
||||
<HotKey className="ml-auto" action={hotkey} />
|
||||
</HStack>
|
||||
))}
|
||||
{bottomSlot}
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,11 @@ import type { HTMLAttributes } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const icons = {
|
||||
alert: lucide.AlertTriangleIcon,
|
||||
archive: lucide.ArchiveIcon,
|
||||
arrowBigDownDash: lucide.ArrowBigDownDashIcon,
|
||||
arrowBigLeftDash: lucide.ArrowBigLeftDashIcon,
|
||||
arrowBigRightDash: lucide.ArrowBigRightDashIcon,
|
||||
arrowBigUpDash: lucide.ArrowBigUpDashIcon,
|
||||
arrowDown: lucide.ArrowDownIcon,
|
||||
arrowDownToDot: lucide.ArrowDownToDotIcon,
|
||||
@@ -60,12 +63,14 @@ export interface IconProps {
|
||||
className?: string;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
spin?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const Icon = memo(function Icon({ icon, spin, size = 'md', className }: IconProps) {
|
||||
export const Icon = memo(function Icon({ icon, spin, size = 'md', className, title }: IconProps) {
|
||||
const Component = icons[icon] ?? icons.question;
|
||||
return (
|
||||
<Component
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
'text-inherit flex-shrink-0',
|
||||
|
||||
24
src-web/components/core/KeyValueRow.tsx
Normal file
24
src-web/components/core/KeyValueRow.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
label: ReactNode;
|
||||
value: ReactNode;
|
||||
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')}>
|
||||
{label}
|
||||
</dd>
|
||||
<dt className="w-2/3 select-text cursor-text break-all">{value}</dt>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user