Split layouts and things

This commit is contained in:
Gregory Schier
2024-02-02 12:41:37 -08:00
parent 50866abda4
commit 7adb0cbb50
8 changed files with 126 additions and 98 deletions

View File

@@ -72,7 +72,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
const dialog = useDialog(); const dialog = useDialog();
return ( return (
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]"> <div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto]">
<Editor <Editor
contentType="application/graphql" contentType="application/graphql"
defaultValue={query ?? ''} defaultValue={query ?? ''}
@@ -124,19 +124,22 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
} }
{...extraEditorProps} {...extraEditorProps}
/> />
<Separator variant="primary" /> <div className="grid min-h-[5rem]">
<p className="py-1 text-gray-500 text-sm">Variables</p> <Separator variant="primary" className="pb-1">
<Editor Variables
format={tryFormatJson} </Separator>
contentType="application/json" <Editor
defaultValue={JSON.stringify(variables, null, 2)} format={tryFormatJson}
heightMode="auto" contentType="application/json"
onChange={handleChangeVariables} defaultValue={JSON.stringify(variables, null, 2)}
placeholder="{}" heightMode="auto"
useTemplating onChange={handleChangeVariables}
autocompleteVariables placeholder="{}"
{...extraEditorProps} useTemplating
/> autocompleteVariables
{...extraEditorProps}
/>
</div>
</div> </div>
); );
} }

View File

@@ -154,6 +154,8 @@ export function GrpcConnectionLayout({ style }: Props) {
return ( return (
<SplitLayout <SplitLayout
name="grpc_layout"
className="p-3"
style={style} style={style}
leftSlot={() => ( leftSlot={() => (
<VStack space={2}> <VStack space={2}>
@@ -188,7 +190,10 @@ export function GrpcConnectionLayout({ style }: Props) {
> >
<Button <Button
size="sm" size="sm"
className="border border-highlight font-mono text-xs text-gray-800" className={classNames(
'border border-highlight font-mono text-xs text-gray-800',
paneSize < 400 && 'flex-1',
)}
rightSlot={<Icon className="text-gray-600" size="sm" icon="chevronDown" />} rightSlot={<Icon className="text-gray-600" size="sm" icon="chevronDown" />}
> >
{select.options.find((o) => o.value === select.value)?.label} {select.options.find((o) => o.value === select.value)?.label}
@@ -198,7 +203,7 @@ export function GrpcConnectionLayout({ style }: Props) {
className="border border-highlight" className="border border-highlight"
size="sm" size="sm"
title="to-do" title="to-do"
hotkeyAction="request.send" hotkeyAction={grpc.isStreaming ? undefined : 'request.send'}
onClick={grpc.isStreaming ? handleCancel : handleConnect} onClick={grpc.isStreaming ? handleCancel : handleConnect}
icon={ icon={
grpc.isStreaming grpc.isStreaming
@@ -212,7 +217,7 @@ export function GrpcConnectionLayout({ style }: Props) {
: 'sendHorizontal' : 'sendHorizontal'
} }
/> />
{activeMethod?.clientStreaming && ( {activeMethod?.clientStreaming && grpc.isStreaming && (
<IconButton <IconButton
className="border border-highlight" className="border border-highlight"
size="sm" size="sm"
@@ -251,62 +256,63 @@ export function GrpcConnectionLayout({ style }: Props) {
{grpc.unary.error} {grpc.unary.error}
</Banner> </Banner>
) : grpc.messages.length > 0 ? ( ) : grpc.messages.length > 0 ? (
<div className="grid grid-rows-[minmax(0,1fr)_auto] overflow-hidden"> <SplitLayout
<div className="overflow-y-auto"> name="grpc_messages2"
{...grpc.messages.map((m) => ( minHeightPx={20}
<HStack defaultRatio={0.25}
key={m.time.getTime()} leftSlot={() => (
space={2} <div className="overflow-y-auto">
onClick={() => { {...grpc.messages.map((m, i) => (
if (m === activeMessage) setActiveMessage(null); <HStack
else setActiveMessage(m); key={`${m.time.getTime()}::${m.message}::${i}`}
}} space={2}
alignItems="center" onClick={() => {
className={classNames( if (m === activeMessage) setActiveMessage(null);
'px-2 py-1 font-mono', else setActiveMessage(m);
m === activeMessage && 'bg-highlight', }}
)} alignItems="center"
> className={classNames(
<Icon 'px-2 py-1 font-mono',
className={ m === activeMessage && 'bg-highlight',
m.type === 'server' )}
? 'text-blue-600' >
: m.type === 'client' <Icon
? 'text-green-600' className={
: 'text-gray-600' m.type === 'server'
} ? 'text-blue-600'
icon={ : m.type === 'client'
m.type === 'server' ? 'text-green-600'
? 'arrowBigDownDash' : 'text-gray-600'
: m.type === 'client' }
? 'arrowBigUpDash' icon={
: 'info' m.type === 'server'
} ? 'arrowBigDownDash'
/> : m.type === 'client'
<div className="w-full truncate text-gray-800 text-xs">{m.message}</div> ? 'arrowBigUpDash'
<div className="text-gray-600 text-2xs" title={m.time.toISOString()}> : 'info'
{format(m.time, 'HH:mm:ss')} }
/>
<div className="w-full truncate text-gray-800 text-xs">{m.message}</div>
<div className="text-gray-600 text-2xs" title={m.time.toISOString()}>
{format(m.time, 'HH:mm:ss')}
</div>
</HStack>
))}
</div>
)}
rightSlot={() =>
activeMessage && (
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
<div className="pb-3 px-2">
<Separator />
</div> </div>
</HStack> <div className="pl-2 overflow-y-auto">
))} <JsonAttributeTree attrValue={JSON.parse(activeMessage?.message ?? '{}')} />
</div> </div>
<div className={classNames(activeMessage ? 'block' : 'hidden')}> </div>
<div className="pb-1 px-2"> )
<Separator /> }
</div> />
<div className="pl-2 pb-1 h-[6rem]">
<JsonAttributeTree attrValue={JSON.parse(activeMessage?.message ?? '{}')} />
</div>
{/*<Editor*/}
{/* className="bg-gray-50 dark:bg-gray-100 max-h-30"*/}
{/* contentType="application/json"*/}
{/* heightMode="auto"*/}
{/* defaultValue={tryFormatJson(activeMessage?.message ?? '')}*/}
{/* forceUpdateKey={activeMessage?.time.getTime()}*/}
{/* readOnly*/}
{/*/>*/}
</div>
</div>
) : resp ? ( ) : resp ? (
<Editor <Editor
className="bg-gray-50 dark:bg-gray-100" className="bg-gray-50 dark:bg-gray-100"

View File

@@ -11,6 +11,8 @@ interface Props {
export function HttpRequestLayout({ style }: Props) { export function HttpRequestLayout({ style }: Props) {
return ( return (
<SplitLayout <SplitLayout
name="http_layout"
className="p-3"
style={style} style={style}
leftSlot={({ orientation, style }) => ( leftSlot={({ orientation, style }) => (
<RequestPane style={style} fullHeight={orientation === 'horizontal'} /> <RequestPane style={style} fullHeight={orientation === 'horizontal'} />

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import React from 'react'; import React from 'react';
import { Separator } from './core/Separator';
interface ResizeBarProps { interface ResizeBarProps {
style?: CSSProperties; style?: CSSProperties;
@@ -17,6 +18,7 @@ export function ResizeHandle({
style, style,
justify, justify,
className, className,
barClassName,
onResizeStart, onResizeStart,
onReset, onReset,
isResizing, isResizing,
@@ -28,6 +30,8 @@ export function ResizeHandle({
aria-hidden aria-hidden
draggable draggable
style={style} style={style}
onDragStart={onResizeStart}
onDoubleClick={onReset}
className={classNames( className={classNames(
className, className,
'group z-10 flex', 'group z-10 flex',
@@ -39,8 +43,6 @@ export function ResizeHandle({
side === 'left' && 'left-0', side === 'left' && 'left-0',
side === 'top' && 'top-0', side === 'top' && 'top-0',
)} )}
onDragStart={onResizeStart}
onDoubleClick={onReset}
> >
{/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */} {/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */}
{isResizing && ( {isResizing && (

View File

@@ -100,7 +100,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
) : null} ) : null}
<div <div
className={classNames( className={classNames(
'max-w-[15em] truncate w-full', 'truncate w-full',
justify === 'start' ? 'text-left' : 'text-center', justify === 'start' ? 'text-left' : 'text-center',
innerClassName, innerClassName,
)} )}

View File

@@ -1,10 +1,11 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react';
interface Props { interface Props {
orientation?: 'horizontal' | 'vertical'; orientation?: 'horizontal' | 'vertical';
variant?: 'primary' | 'secondary'; variant?: 'primary' | 'secondary';
className?: string; className?: string;
children?: string; children?: ReactNode;
} }
export function Separator({ export function Separator({

View File

@@ -15,27 +15,40 @@ interface SlotProps {
} }
interface Props { interface Props {
style: CSSProperties; name: string;
leftSlot: (props: SlotProps) => ReactNode; leftSlot: (props: SlotProps) => ReactNode;
rightSlot: (props: SlotProps) => ReactNode; rightSlot: (props: SlotProps) => ReactNode;
style?: CSSProperties;
className?: string;
defaultRatio?: number;
minHeightPx?: number;
minWidthPx?: number;
} }
const areaL = { gridArea: 'left' }; const areaL = { gridArea: 'left' };
const areaR = { gridArea: 'right' }; const areaR = { gridArea: 'right' };
const areaD = { gridArea: 'drag' }; const areaD = { gridArea: 'drag' };
const DEFAULT = 0.5;
const MIN_WIDTH_PX = 10;
const MIN_HEIGHT_PX = 30;
const STACK_VERTICAL_WIDTH = 700; const STACK_VERTICAL_WIDTH = 700;
export function SplitLayout({ style, leftSlot, rightSlot }: Props) { export function SplitLayout({
style,
leftSlot,
rightSlot,
className,
name,
defaultRatio = 0.5,
minHeightPx = 10,
minWidthPx = 10,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [vertical, setVertical] = useState<boolean>(false); const [vertical, setVertical] = useState<boolean>(false);
const [widthRaw, setWidth] = useLocalStorage<number>(`body_width::${useActiveWorkspaceId()}`); const [widthRaw, setWidth] = useLocalStorage<number>(`${name}_width::${useActiveWorkspaceId()}`);
const [heightRaw, setHeight] = useLocalStorage<number>(`body_height::${useActiveWorkspaceId()}`); const [heightRaw, setHeight] = useLocalStorage<number>(
const width = widthRaw ?? DEFAULT; `${name}_height::${useActiveWorkspaceId()}`,
const height = heightRaw ?? DEFAULT; );
const width = widthRaw ?? defaultRatio;
const height = heightRaw ?? defaultRatio;
const [isResizing, setIsResizing] = useState<boolean>(false); const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
null, null,
@@ -52,7 +65,7 @@ export function SplitLayout({ style, leftSlot, rightSlot }: Props) {
? ` ? `
' ${areaL.gridArea}' minmax(0,${1 - height}fr) ' ${areaL.gridArea}' minmax(0,${1 - height}fr)
' ${areaD.gridArea}' 0 ' ${areaD.gridArea}' 0
' ${areaR.gridArea}' minmax(0,${height}fr) ' ${areaR.gridArea}' minmax(${minHeightPx}px,${height}fr)
/ 1fr / 1fr
` `
: ` : `
@@ -71,8 +84,8 @@ export function SplitLayout({ style, leftSlot, rightSlot }: Props) {
}; };
const handleReset = useCallback( const handleReset = useCallback(
() => (vertical ? setHeight(DEFAULT) : setWidth(DEFAULT)), () => (vertical ? setHeight(defaultRatio) : setWidth(defaultRatio)),
[setHeight, vertical, setWidth], [vertical, setHeight, defaultRatio, setWidth],
); );
const handleResizeStart = useCallback( const handleResizeStart = useCallback(
@@ -91,18 +104,18 @@ export function SplitLayout({ style, leftSlot, rightSlot }: Props) {
move: (e: MouseEvent) => { move: (e: MouseEvent) => {
e.preventDefault(); // Prevent text selection and things e.preventDefault(); // Prevent text selection and things
if (vertical) { if (vertical) {
const maxHeightPx = containerRect.height - MIN_HEIGHT_PX; const maxHeightPx = containerRect.height - minHeightPx;
const newHeightPx = clamp( const newHeightPx = clamp(
startHeight - (e.clientY - mouseStartY), startHeight - (e.clientY - mouseStartY),
MIN_HEIGHT_PX, minHeightPx,
maxHeightPx, maxHeightPx,
); );
setHeight(newHeightPx / containerRect.height); setHeight(newHeightPx / containerRect.height);
} else { } else {
const maxWidthPx = containerRect.width - MIN_WIDTH_PX; const maxWidthPx = containerRect.width - minWidthPx;
const newWidthPx = clamp( const newWidthPx = clamp(
startWidth - (e.clientX - mouseStartX), startWidth - (e.clientX - mouseStartX),
MIN_WIDTH_PX, minWidthPx,
maxWidthPx, maxWidthPx,
); );
setWidth(newWidthPx / containerRect.width); setWidth(newWidthPx / containerRect.width);
@@ -118,7 +131,7 @@ export function SplitLayout({ style, leftSlot, rightSlot }: Props) {
document.documentElement.addEventListener('mouseup', moveState.current.up); document.documentElement.addEventListener('mouseup', moveState.current.up);
setIsResizing(true); setIsResizing(true);
}, },
[width, height, vertical, setHeight, setWidth], [width, height, vertical, minHeightPx, setHeight, minWidthPx, setWidth],
); );
const activeRequestId = useActiveRequestId(); const activeRequestId = useActiveRequestId();
@@ -127,11 +140,12 @@ export function SplitLayout({ style, leftSlot, rightSlot }: Props) {
} }
return ( return (
<div ref={containerRef} className="grid gap-1.5 w-full h-full p-3" style={styles}> <div ref={containerRef} className={classNames(className, 'grid w-full h-full')} style={styles}>
{leftSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })} {leftSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
<ResizeHandle <ResizeHandle
style={areaD} style={areaD}
isResizing={isResizing} isResizing={isResizing}
barClassName={'bg-red-300'}
className={classNames(vertical ? 'translate-y-0.5' : 'translate-x-0.5')} className={classNames(vertical ? 'translate-y-0.5' : 'translate-x-0.5')}
onResizeStart={handleResizeStart} onResizeStart={handleResizeStart}
onReset={handleReset} onReset={handleReset}

View File

@@ -1,9 +1,7 @@
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { message } from '@tauri-apps/api/dialog';
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import { useState } from 'react'; import { useState } from 'react';
import { send } from 'vite';
import { useListenToTauriEvent } from './useListenToTauriEvent'; import { useListenToTauriEvent } from './useListenToTauriEvent';
interface ReflectResponseService { interface ReflectResponseService {
@@ -20,6 +18,7 @@ export interface GrpcMessage {
export function useGrpc(url: string | null) { export function useGrpc(url: string | null) {
const [messages, setMessages] = useState<GrpcMessage[]>([]); const [messages, setMessages] = useState<GrpcMessage[]>([]);
const [activeConnectionId, setActiveConnectionId] = useState<string | null>(null); const [activeConnectionId, setActiveConnectionId] = useState<string | null>(null);
useListenToTauriEvent<string>( useListenToTauriEvent<string>(
'grpc_message', 'grpc_message',
(event) => { (event) => {
@@ -28,8 +27,9 @@ export function useGrpc(url: string | null) {
{ message: event.payload, time: new Date(), type: 'server' }, { message: event.payload, time: new Date(), type: 'server' },
]); ]);
}, },
[], [setMessages],
); );
const unary = useMutation<string, string, { service: string; method: string; message: string }>({ const unary = useMutation<string, string, { service: string; method: string; message: string }>({
mutationKey: ['grpc_unary', url], mutationKey: ['grpc_unary', url],
mutationFn: async ({ service, method, message }) => { mutationFn: async ({ service, method, message }) => {
@@ -94,8 +94,8 @@ export function useGrpc(url: string | null) {
const cancel = useMutation({ const cancel = useMutation({
mutationKey: ['grpc_cancel', url], mutationKey: ['grpc_cancel', url],
mutationFn: async () => { mutationFn: async () => {
await emit('grpc_message_in', 'Cancel');
setActiveConnectionId(null); setActiveConnectionId(null);
await emit('grpc_message_in', 'Cancel');
setMessages((m) => [ setMessages((m) => [
...m, ...m,
{ type: 'info', message: 'Cancelled by client', time: new Date() }, { type: 'info', message: 'Cancelled by client', time: new Date() },