Split layouts and things

This commit is contained in:
Gregory Schier
2024-02-02 12:41:37 -08:00
parent fdedb9bd28
commit d8948bb061
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();
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
contentType="application/graphql"
defaultValue={query ?? ''}
@@ -124,19 +124,22 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
}
{...extraEditorProps}
/>
<Separator variant="primary" />
<p className="py-1 text-gray-500 text-sm">Variables</p>
<Editor
format={tryFormatJson}
contentType="application/json"
defaultValue={JSON.stringify(variables, null, 2)}
heightMode="auto"
onChange={handleChangeVariables}
placeholder="{}"
useTemplating
autocompleteVariables
{...extraEditorProps}
/>
<div className="grid min-h-[5rem]">
<Separator variant="primary" className="pb-1">
Variables
</Separator>
<Editor
format={tryFormatJson}
contentType="application/json"
defaultValue={JSON.stringify(variables, null, 2)}
heightMode="auto"
onChange={handleChangeVariables}
placeholder="{}"
useTemplating
autocompleteVariables
{...extraEditorProps}
/>
</div>
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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