mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 20:00:29 +01:00
Split layouts and things
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'} />
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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() },
|
||||
|
||||
Reference in New Issue
Block a user