From 9ff071854d3a703a5d0a89b5697af6b71f857a9c Mon Sep 17 00:00:00 2001 From: James Clarke Date: Fri, 25 Oct 2024 18:13:47 +0100 Subject: [PATCH] Various updates to editor and repl views - Updates to styling of editor and repl - Add result table output mode to editor and repl - Improve inspector perf in repl - Update to use mobx-react-lite everywhere --- .../components/dataGrid/dataGrid.module.scss | 21 +- shared/common/components/dataGrid/index.tsx | 24 +- .../components/dataGrid/renderUtils.tsx | 97 ++++ shared/common/components/resultGrid/index.tsx | 138 +++-- .../resultGrid/resultGrid.module.scss | 25 +- shared/common/components/resultGrid/state.ts | 69 ++- shared/common/newui/button/button.module.scss | 6 + shared/common/newui/button/index.tsx | 17 +- shared/common/newui/theme.module.scss | 2 + shared/common/ui/customScrollbar/index.tsx | 3 + shared/common/ui/splitView/index.tsx | 5 +- .../common/ui/splitView/splitView.module.scss | 25 +- shared/inspector/index.tsx | 5 +- .../components/schemaGraph/SchemaLink.tsx | 2 +- .../components/schemaGraph/SchemaNode.tsx | 14 +- .../schemaGraph/SchemaNodeLinkProps.tsx | 2 +- .../schemaGraph/SchemaNodeObject.tsx | 2 +- .../components/schemaGraph/index.tsx | 2 +- .../components/schemaMinimap/index.tsx | 2 +- shared/schemaGraph/package.json | 4 +- shared/studio/components/explainVis/index.tsx | 62 +- shared/studio/components/explainVis/state.ts | 22 +- .../components/explainVis/treemapLayout.tsx | 3 +- .../extendedViewers/hexViewer/index.tsx | 2 +- .../components/visualQuerybuilder/index.tsx | 2 +- shared/studio/package.json | 2 +- shared/studio/state/explainGraphSettings.ts | 2 +- .../tabs/dataview/dataInspector.module.scss | 17 - shared/studio/tabs/dataview/dataInspector.tsx | 99 +--- .../studio/tabs/dataview/dataview.module.scss | 2 +- shared/studio/tabs/dataview/fieldConfig.tsx | 4 +- shared/studio/tabs/dataview/index.tsx | 2 +- shared/studio/tabs/graphql/index.tsx | 2 +- shared/studio/tabs/queryEditor/index.tsx | 148 ++++- .../tabs/queryEditor/paramEditor/index.tsx | 2 +- .../studio/tabs/queryEditor/repl.module.scss | 163 +++++- shared/studio/tabs/queryEditor/state/index.ts | 118 ++-- shared/studio/tabs/repl/index.tsx | 536 +++++++++++++----- shared/studio/tabs/repl/repl.module.scss | 253 +++++---- shared/studio/tabs/repl/state/index.ts | 169 +++--- shared/studio/tabs/schema/index.tsx | 2 +- shared/studio/utils/completer.ts | 25 + web/package.json | 2 +- web/src/app.module.scss | 2 +- web/src/app.tsx | 2 +- web/src/components/databasePage/index.tsx | 2 +- web/src/components/header/index.tsx | 2 +- web/src/components/instancePage/index.tsx | 2 +- yarn.lock | 42 +- 49 files changed, 1431 insertions(+), 725 deletions(-) create mode 100644 shared/common/components/dataGrid/renderUtils.tsx create mode 100644 shared/studio/utils/completer.ts diff --git a/shared/common/components/dataGrid/dataGrid.module.scss b/shared/common/components/dataGrid/dataGrid.module.scss index 5a7b29d9..dc0f60c5 100644 --- a/shared/common/components/dataGrid/dataGrid.module.scss +++ b/shared/common/components/dataGrid/dataGrid.module.scss @@ -10,10 +10,14 @@ width: 100%; height: 100%; overflow: auto; - overscroll-behavior: contain; + overscroll-behavior: contain auto; font-family: "Roboto Mono Variable", monospace; @include hideScrollbar; + + &.noVerticalScroll { + overflow-y: hidden; + } } .innerWrapper { @@ -28,7 +32,7 @@ top: 0; min-width: max-content; display: grid; - background: var(--panel_background); + background: var(--header_background); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06); color: var(--main_text_color); font-weight: 450; @@ -86,7 +90,7 @@ left: 0; grid-row: 1 / -1; display: grid; - background: var(--panel_background); + background: var(--header_background); z-index: 2; border-right: 1px solid var(--panel_border); @@ -153,3 +157,14 @@ background: var(--Grey12) !important; } } + +.scalar_str, +.scalar_uuid { + &:before, + &:after { + content: ""; + } +} +.scalar_uuid { + color: #6f6f6f; +} diff --git a/shared/common/components/dataGrid/index.tsx b/shared/common/components/dataGrid/index.tsx index b7e5061a..b4f069b0 100644 --- a/shared/common/components/dataGrid/index.tsx +++ b/shared/common/components/dataGrid/index.tsx @@ -13,9 +13,18 @@ import {useGlobalDragCursor} from "../../hooks/globalDragCursor"; export interface DataGridProps { state: DataGridState; + className?: string; + style?: React.CSSProperties; + noVerticalScroll?: boolean; } -export function DataGrid({state, children}: PropsWithChildren) { +export function DataGrid({ + state, + className, + style, + noVerticalScroll, + children, +}: PropsWithChildren) { const ref = useRef(null); useResize(ref, ({width, height}) => @@ -40,9 +49,11 @@ export function DataGrid({state, children}: PropsWithChildren) { return (
{ @@ -53,7 +64,9 @@ export function DataGrid({state, children}: PropsWithChildren) { el.scrollLeft = state.scrollPos.left; } }} - className={styles.dataGrid} + className={cn(styles.dataGrid, { + [styles.noVerticalScroll]: !!noVerticalScroll, + })} >
{children}
@@ -66,11 +79,13 @@ export const GridHeaders = observer(function GridHeaders({ state, pinnedHeaders, headers, + style, }: { className?: string; state: DataGridState; pinnedHeaders: React.ReactNode; headers: React.ReactNode; + style?: React.CSSProperties; }) { const ref = useRef(null); @@ -81,6 +96,7 @@ export const GridHeaders = observer(function GridHeaders({ ref={ref} className={cn(styles.headers, className)} style={{ + ...style, gridTemplateColumns: `${ state.pinnedColsWidth ? `${state.pinnedColsWidth}px ` : "" }${state.colWidths.join("px ")}px minmax(100px, 1fr)`, diff --git a/shared/common/components/dataGrid/renderUtils.tsx b/shared/common/components/dataGrid/renderUtils.tsx new file mode 100644 index 00000000..a0258235 --- /dev/null +++ b/shared/common/components/dataGrid/renderUtils.tsx @@ -0,0 +1,97 @@ +import {Fragment} from "react"; + +import {ICodec} from "edgedb/dist/codecs/ifaces"; +import {EnumCodec} from "edgedb/dist/codecs/enum"; +import {NamedTupleCodec} from "edgedb/dist/codecs/namedtuple"; +import {MultiRangeCodec, RangeCodec} from "edgedb/dist/codecs/range"; + +import {renderValue} from "@edgedb/inspector/buildScalar"; + +import styles from "./dataGrid.module.scss"; + +const inspectorOverrideStyles = { + uuid: styles.scalar_uuid, + str: styles.scalar_str, +}; + +export function renderCellValue( + value: any, + codec: ICodec, + nested = false +): JSX.Element { + switch (codec.getKind()) { + case "scalar": + case "range": + case "multirange": + return renderValue( + value, + codec.getKnownTypeName(), + codec instanceof EnumCodec, + codec instanceof RangeCodec || codec instanceof MultiRangeCodec + ? codec.getSubcodecs()[0].getKnownTypeName() + : undefined, + false, + !nested ? inspectorOverrideStyles : undefined, + 100 + ).body; + case "set": + return ( + <> + {"{"} + {(value as any[]).map((item, i) => ( + + {i !== 0 ? ", " : null} + {renderCellValue(item, codec.getSubcodecs()[0], true)} + + ))} + {"}"} + + ); + case "array": + return ( + <> + [ + {(value as any[]).map((item, i) => ( + + {i !== 0 ? ", " : null} + {renderCellValue(item, codec.getSubcodecs()[0], true)} + + ))} + ] + + ); + case "tuple": + return ( + <> + ( + {(value as any[]).map((item, i) => ( + + {i !== 0 ? ", " : null} + {renderCellValue(item, codec.getSubcodecs()[i], true)} + + ))} + ) + + ); + case "namedtuple": { + const fieldNames = (codec as NamedTupleCodec).getNames(); + const subCodecs = codec.getSubcodecs(); + return ( + <> + ( + {fieldNames.map((name, i) => ( + + {i !== 0 ? ", " : null} + {name} + {" := "} + {renderCellValue(value[name], subCodecs[i], true)} + + ))} + ) + + ); + } + default: + return <>; + } +} diff --git a/shared/common/components/resultGrid/index.tsx b/shared/common/components/resultGrid/index.tsx index 64e37ff1..a30dafbe 100644 --- a/shared/common/components/resultGrid/index.tsx +++ b/shared/common/components/resultGrid/index.tsx @@ -6,73 +6,130 @@ import {GridHeader, ResultGridState, RowHeight} from "./state"; import { DataGrid, + DataGridProps, GridContent, GridHeaders, HeaderResizeHandle, } from "../dataGrid"; +import {renderCellValue} from "../dataGrid/renderUtils"; import gridStyles from "../dataGrid/dataGrid.module.scss"; +import inspectorStyles from "@edgedb/inspector/inspector.module.scss"; + import styles from "./resultGrid.module.scss"; +import {useEffect} from "react"; +import {calculateInitialColWidths} from "../dataGrid/utils"; export {createResultGridState, ResultGridState} from "./state"; -export interface ResultGridProps { +export interface ResultGridProps extends Omit { state: ResultGridState; } -export function ResultGrid({state}: ResultGridProps) { +export const ResultGrid = observer(function ResultGrid({ + state, + ...props +}: ResultGridProps) { + useEffect(() => { + if ( + state.grid.gridContainerSize.width > 0 && + state.grid._colWidths.size == 0 + ) { + state.grid.setColWidths( + calculateInitialColWidths( + state.flatHeaders.map(({id, typename}) => ({ + id, + typename, + isLink: false, + })), + state.grid.gridContainerSize.width + ) + ); + } + }, [state.grid.gridContainerSize.width]); + return ( - + ); -} +}); -const ResultGridHeaders = observer(function ResultGridHeaders({ +export const ResultGridHeaders = observer(function ResultGridHeaders({ state, }: { state: ResultGridState; }) { + const headers = state.allHeaders; + const lastHeader = headers[headers.length - 1]; return ( [ -
{ + if (header.name == null) { + return []; + } + const hasSubheaders = header.subHeaders?.[0].name != null; + return header.name != null + ? [ +
+
{header.name}
+ {!hasSubheaders ? ( +
{header.typename}
+ ) : null} +
, + ( + header.parent == null + ? header.startIndex != 0 + : header.parent.subHeaders![0] != header + ) ? ( + + ) : null, + ] + : []; + }), + - {header.name} -
, - ( - header.parent == null - ? header.startIndex != 0 - : header.parent.subHeaders![0] != header - ) ? ( - - ) : null, - ])} + />, + ]} /> ); }); -const ResultGridContent = observer(function ResultGridContent({ +export const ResultGridContent = observer(function ResultGridContent({ state, }: ResultGridProps) { const ranges = state.grid.visibleRanges; @@ -104,7 +161,7 @@ const ResultGridContent = observer(function ResultGridContent({ indexOffset - rowIndex } - data={data[dataIndex][header.name]} + data={header.name ? data[dataIndex][header.name] : data[dataIndex]} /> ); dataIndex += 1; @@ -128,7 +185,14 @@ const ResultGridContent = observer(function ResultGridContent({ } } - return ; + return ( + + ); }); const GridCell = observer(function GridCell({ @@ -160,7 +224,7 @@ const GridCell = observer(function GridCell({
1})} > - {data?.toString() ?? "{}"} + {renderCellValue(data, header.codec)}
) : null} diff --git a/shared/common/components/resultGrid/resultGrid.module.scss b/shared/common/components/resultGrid/resultGrid.module.scss index e79ac2c7..3646f30f 100644 --- a/shared/common/components/resultGrid/resultGrid.module.scss +++ b/shared/common/components/resultGrid/resultGrid.module.scss @@ -3,18 +3,39 @@ .header { position: relative; padding: 8px 12px; - overflow: hidden; + overflow: clip; + font-size: 13px; + white-space: nowrap; + + &.hasSubheaders { + color: var(--secondary_text_color); + border-bottom: 1px solid var(--panel_border); + margin: 0 8px; + padding: 8px 4px; + + .headerName { + position: sticky; + left: 12px; + width: max-content; + } + } + + .typename { + font-size: 11px; + color: var(--tertiary_text_color); + margin-top: 2px; + } } .cellContent { height: 39px; line-height: 38px; padding: 0 12px; - top: 64px; white-space: nowrap; overflow: hidden; &.stickyCell { position: sticky; + top: var(--gridHeaderHeight); } } diff --git a/shared/common/components/resultGrid/state.ts b/shared/common/components/resultGrid/state.ts index e54871aa..07693acf 100644 --- a/shared/common/components/resultGrid/state.ts +++ b/shared/common/components/resultGrid/state.ts @@ -2,6 +2,8 @@ import {ICodec} from "edgedb/dist/codecs/ifaces"; import {ObjectCodec} from "edgedb/dist/codecs/object"; import {SetCodec} from "edgedb/dist/codecs/set"; import {DataGridState} from "../dataGrid/state"; +import {assertNever} from "../../utils/assertNever"; +import {NamedTupleCodec} from "edgedb/dist/codecs/namedtuple"; export function createResultGridState(codec: ICodec, data: any[]) { return new ResultGridState(codec, data); @@ -12,9 +14,10 @@ export const RowHeight = 40; export interface GridHeader { id: string; parent: GridHeader | null; - name: string; + name: string | null; multi: boolean; codec: ICodec; + typename: string; depth: number; startIndex: number; span: number; @@ -50,9 +53,6 @@ export class ResultGridState { () => [], () => this.rowCount ); - - console.log(this.data); - console.log(this.rowTops); } getData( @@ -76,7 +76,7 @@ export class ResultGridState { ? tops.findIndex((top) => top > offsetRowIndex) - 1 : offsetRowIndex; return { - data: parentData[dataIndex][header.parent.name], + data: parentData[dataIndex][header.parent.name!], indexOffset: indexOffset + (tops ? tops[dataIndex] : dataIndex), endIndex: indexOffset + (tops ? tops[dataIndex + 1] : dataIndex + 1), }; @@ -95,9 +95,10 @@ function _getRowTops( let height = 1; for (const header of headers) { if (!header.multi) continue; - const colHeight = header.subHeaders - ? _getRowTops(topsMap, item[header.name], header.subHeaders) - : item[header.name].length; + const colHeight = + header.subHeaders && header.subHeaders[0].name !== null + ? _getRowTops(topsMap, item[header.name!], header.subHeaders) + : item[header.name!].length; if (colHeight > height) { height = colHeight; } @@ -123,9 +124,11 @@ function _getHeaders( const headers: GridHeader[] = []; let colCount = 0; let i = 0; - for (const field of codec.getFields()) { + const fields = codec.getFields(); + const showImplicitId = fields.every((field) => field.implicit); + for (const field of fields) { let subcodec = subcodecs[i++]; - if (!field.implicit) { + if (!field.implicit || (showImplicitId && field.name === "id")) { let multi = false; if (subcodec instanceof SetCodec) { multi = true; @@ -138,6 +141,7 @@ function _getHeaders( name: field.name, multi, codec: subcodec, + typename: getTypename(subcodec), depth, startIndex, span: 1, @@ -155,6 +159,22 @@ function _getHeaders( header.subHeaders = subheaders.headers; colCount += subheaders.colCount; } else { + if (multi) { + header.subHeaders = [ + { + id: `${header.id}.__multi__`, + parent: header, + name: null, + multi: false, + codec: subcodec, + typename: getTypename(subcodec), + depth, + startIndex, + span: 1, + subHeaders: null, + }, + ]; + } colCount++; } } @@ -170,3 +190,32 @@ function _flattenHeaders(headers: GridHeader[]): GridHeader[] { ...(header.subHeaders ? _flattenHeaders(header.subHeaders) : []), ]); } + +function getTypename(codec: ICodec): string { + const kind = codec.getKind(); + switch (kind) { + case "scalar": + case "object": + case "sparse_object": + return codec.getKnownTypeName(); + case "array": + case "range": + case "multirange": + return `${kind}<${getTypename(codec.getSubcodecs()[0])}>`; + case "tuple": + return `tuple<${codec + .getSubcodecs() + .map((subcodec) => getTypename(subcodec)) + .join(", ")}>`; + case "namedtuple": + const subcodecs = codec.getSubcodecs(); + return `tuple<${(codec as NamedTupleCodec) + .getNames() + .map((name, i) => `${name}: ${getTypename(subcodecs[i])}`) + .join(", ")}>`; + case "set": + return getTypename(codec.getSubcodecs()[0]); + default: + assertNever(kind); + } +} diff --git a/shared/common/newui/button/button.module.scss b/shared/common/newui/button/button.module.scss index 50390aad..46b165c3 100644 --- a/shared/common/newui/button/button.module.scss +++ b/shared/common/newui/button/button.module.scss @@ -32,6 +32,12 @@ flex-shrink: 0; } + .shortcut { + opacity: 0.85; + font-size: 13px; + margin-left: 8px; + } + &.primary { // background: linear-gradient(180deg, #a565cd 0%, #9c56b4 100%); background: var(--buttonPrimaryBackground, #a565cd); diff --git a/shared/common/newui/button/index.tsx b/shared/common/newui/button/index.tsx index 6adc35cb..779ef045 100644 --- a/shared/common/newui/button/index.tsx +++ b/shared/common/newui/button/index.tsx @@ -4,6 +4,8 @@ import styles from "./button.module.scss"; import Spinner from "../../ui/spinner"; import {CSSProperties, PropsWithChildren, useEffect, useState} from "react"; +const isMac = navigator.platform.toLowerCase().includes("mac"); + interface _BaseButtonProps { className?: string; kind?: "primary" | "secondary" | "outline"; @@ -12,6 +14,7 @@ interface _BaseButtonProps { rightIcon?: JSX.Element; disabled?: boolean; loading?: boolean; + shortcut?: string | {default: string; macos?: string}; style?: CSSProperties; } @@ -28,6 +31,7 @@ function _Button({ rightIcon, disabled, loading, + shortcut, ...props }: ButtonProps & {type: "button" | "submit"}) { return ( @@ -45,7 +49,18 @@ function _Button({ {...props} > {loading ? : leftIcon} - {children} + + {children} + {shortcut ? ( + + {typeof shortcut === "string" + ? shortcut + : isMac && shortcut.macos + ? shortcut.macos + : shortcut.default} + + ) : null} + {rightIcon} ); diff --git a/shared/common/newui/theme.module.scss b/shared/common/newui/theme.module.scss index b12d4f1d..7ff2f817 100644 --- a/shared/common/newui/theme.module.scss +++ b/shared/common/newui/theme.module.scss @@ -34,6 +34,7 @@ --app_panel_background: var(--Grey97); --panel_background: var(--Grey99); --panel_border: var(--Grey90); + --header_background: var(--Grey99); --main_text_color: var(--Grey30); --secondary_text_color: var(--Grey40); @@ -44,6 +45,7 @@ --app_panel_background: var(--Grey14); --panel_background: var(--Grey22); --panel_border: var(--Grey30); + --header_background: var(--Grey20); --main_text_color: var(--Grey80); --secondary_text_color: var(--Grey70); diff --git a/shared/common/ui/customScrollbar/index.tsx b/shared/common/ui/customScrollbar/index.tsx index c0173004..18900d68 100644 --- a/shared/common/ui/customScrollbar/index.tsx +++ b/shared/common/ui/customScrollbar/index.tsx @@ -22,6 +22,7 @@ export interface CustomScrollbarsProps { reverse?: boolean; hideVertical?: boolean; hideHorizontal?: boolean; + style?: React.CSSProperties; } const defaultThumbSizes: [number, number] = [-1, -1]; @@ -38,6 +39,7 @@ export function CustomScrollbars({ reverse, hideVertical, hideHorizontal, + style }: PropsWithChildren) { const ref = useRef(null); @@ -252,6 +254,7 @@ export function CustomScrollbars({ className={cn(styles.customScrollbars, className, { [styles.dragging]: dragging, })} + style={style} onMouseDownCapture={onMouseDown} > {children} diff --git a/shared/common/ui/splitView/index.tsx b/shared/common/ui/splitView/index.tsx index a9c9931a..e3662ff9 100644 --- a/shared/common/ui/splitView/index.tsx +++ b/shared/common/ui/splitView/index.tsx @@ -1,5 +1,5 @@ import React, {Fragment, useRef} from "react"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import cn from "@edgedb/common/utils/classNames"; @@ -98,7 +98,8 @@ export default observer(function SplitView({ > {views.map((view, viewIndex) => { const lastView = viewIndex === views.length - 1; - const margins = 4 + (viewIndex === 0 ? -2 : 0) + (lastView ? -2 : 0); + const margins = + 3 + (viewIndex === 0 ? -1.5 : 0) + (lastView ? -1.5 : 0); const size = { [childSizeKey]: `calc(${state.sizes[viewIndex]}%${ margins ? ` - ${margins}px` : "" diff --git a/shared/common/ui/splitView/splitView.module.scss b/shared/common/ui/splitView/splitView.module.scss index d71a95f8..4ad1e1ec 100644 --- a/shared/common/ui/splitView/splitView.module.scss +++ b/shared/common/ui/splitView/splitView.module.scss @@ -59,18 +59,23 @@ } .resizer { - min-width: 4px; - min-height: 4px; + min-width: 3px; + min-height: 3px; position: relative; z-index: 100; display: flex; align-items: center; justify-content: center; + background: #e2e2e2; @include breakpoint(mobile) { display: none; } + @include darkTheme { + background: #0f0f0f; + } + .grabHandle { position: absolute; top: -3px; @@ -81,20 +86,20 @@ } .resizerIndicator { - width: 10px; - height: 40px; + width: 9px; + height: 38px; flex-shrink: 0; - background: var(--app-bg); + background: #e2e2e2; border-radius: 5px; pointer-events: none; &:after { content: ""; display: block; - width: 2px; + width: 3px; height: 32px; - background: var(--app-card-bg); - margin: 4px; + background: var(--panel_background); + margin: 3px; border-radius: 2px; } @@ -107,6 +112,10 @@ width: 32px; } } + + @include darkTheme { + background: #0f0f0f; + } } .resizerFlip { diff --git a/shared/inspector/index.tsx b/shared/inspector/index.tsx index 9f475311..fadb56a3 100644 --- a/shared/inspector/index.tsx +++ b/shared/inspector/index.tsx @@ -1,4 +1,4 @@ -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {VariableSizeList as List, ListChildComponentProps} from "react-window"; import {_ICodec} from "edgedb"; @@ -315,6 +315,7 @@ interface InspectorRowProps { isExpanded: boolean; toggleExpanded: () => void; disableCopy?: boolean; + style?: React.CSSProperties; } export const InspectorRow = observer(function InspectorRow({ @@ -325,6 +326,7 @@ export const InspectorRow = observer(function InspectorRow({ isExpanded, toggleExpanded, disableCopy = false, + style, }: InspectorRowProps) { const expandableItem = item.type !== ItemType.Scalar && item.type !== ItemType.Other; @@ -341,6 +343,7 @@ export const InspectorRow = observer(function InspectorRow({ state.hoverId !== item.id && item.id.startsWith(state.hoverId!), })} style={{ + ...style, paddingLeft: `${(item.level + 1) * 2 + 1}ch`, }} onClick={index != null ? () => state.setSelectedIndex(index) : undefined} diff --git a/shared/schemaGraph/components/schemaGraph/SchemaLink.tsx b/shared/schemaGraph/components/schemaGraph/SchemaLink.tsx index deb74d73..a2e5dc04 100644 --- a/shared/schemaGraph/components/schemaGraph/SchemaLink.tsx +++ b/shared/schemaGraph/components/schemaGraph/SchemaLink.tsx @@ -1,5 +1,5 @@ import {Fragment} from "react"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import styles from "./schemaGraph.module.scss"; import {useSchemaState} from "../../state/provider"; diff --git a/shared/schemaGraph/components/schemaGraph/SchemaNode.tsx b/shared/schemaGraph/components/schemaGraph/SchemaNode.tsx index d649a366..c3209ad9 100644 --- a/shared/schemaGraph/components/schemaGraph/SchemaNode.tsx +++ b/shared/schemaGraph/components/schemaGraph/SchemaNode.tsx @@ -1,5 +1,5 @@ import React, {useState, useRef} from "react"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {CSSTransition} from "react-transition-group"; import styles from "./schemaGraph.module.scss"; @@ -38,7 +38,7 @@ export default observer(function SchemaNode({node}: SchemaNodeProps) { > @@ -123,9 +123,8 @@ const DraggableNode = observer( let dragging = false; const updateNodePosition = () => { - const mouseGraphPos = viewport.getClientPosInGraphSpace( - currentMousePos - ); + const mouseGraphPos = + viewport.getClientPosInGraphSpace(currentMousePos); nodeState.updatePosition( initialNodePos.x + (mouseGraphPos.x - initialMouseGraphPos.x), initialNodePos.y + (mouseGraphPos.y - initialMouseGraphPos.y) @@ -174,9 +173,8 @@ const DraggableNode = observer( setDragging(true); viewport.updateViewportRect(); - initialMouseGraphPos = viewport.getClientPosInGraphSpace( - initialMousePos - ); + initialMouseGraphPos = + viewport.getClientPosInGraphSpace(initialMousePos); initialNodePos = {x: nodeState.x, y: nodeState.y}; currentMousePos = initialMousePos; diff --git a/shared/schemaGraph/components/schemaGraph/SchemaNodeLinkProps.tsx b/shared/schemaGraph/components/schemaGraph/SchemaNodeLinkProps.tsx index 3ddc2a11..28af4dc2 100644 --- a/shared/schemaGraph/components/schemaGraph/SchemaNodeLinkProps.tsx +++ b/shared/schemaGraph/components/schemaGraph/SchemaNodeLinkProps.tsx @@ -1,4 +1,4 @@ -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import styles from "./schemaGraph.module.scss"; import {useSchemaState} from "../../state/provider"; diff --git a/shared/schemaGraph/components/schemaGraph/SchemaNodeObject.tsx b/shared/schemaGraph/components/schemaGraph/SchemaNodeObject.tsx index 86de9f2c..304843b8 100644 --- a/shared/schemaGraph/components/schemaGraph/SchemaNodeObject.tsx +++ b/shared/schemaGraph/components/schemaGraph/SchemaNodeObject.tsx @@ -1,4 +1,4 @@ -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {useDBRouter} from "@edgedb/studio/hooks/dbRoute"; import styles from "./schemaGraph.module.scss"; diff --git a/shared/schemaGraph/components/schemaGraph/index.tsx b/shared/schemaGraph/components/schemaGraph/index.tsx index 73e3f63a..5598064b 100644 --- a/shared/schemaGraph/components/schemaGraph/index.tsx +++ b/shared/schemaGraph/components/schemaGraph/index.tsx @@ -1,5 +1,5 @@ import React, {useRef, useEffect, useCallback, Fragment} from "react"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import styles from "./schemaGraph.module.scss"; import {useSchemaState} from "../../state/provider"; diff --git a/shared/schemaGraph/components/schemaMinimap/index.tsx b/shared/schemaGraph/components/schemaMinimap/index.tsx index 83e061d7..92427a9f 100644 --- a/shared/schemaGraph/components/schemaMinimap/index.tsx +++ b/shared/schemaGraph/components/schemaMinimap/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import styles from "./schemaMinimap.module.scss"; import {useSchemaState} from "../../state/provider"; diff --git a/shared/schemaGraph/package.json b/shared/schemaGraph/package.json index faed1d2d..ac42a8bd 100644 --- a/shared/schemaGraph/package.json +++ b/shared/schemaGraph/package.json @@ -13,8 +13,8 @@ "@types/react-transition-group": "^4.4.0", "fastpriorityqueue": "^0.6.3", "fuse.js": "^6.4.1", - "mobx": "^6.3.0", - "mobx-react": "^7.1.0", + "mobx": "^6.5.0", + "mobx-react-lite": "^4.0.0", "mobx-state-tree": "^5.0.1", "react-transition-group": "^4.4.1", "webcola": "^3.4.0" diff --git a/shared/studio/components/explainVis/index.tsx b/shared/studio/components/explainVis/index.tsx index 1d275667..8576cc1f 100644 --- a/shared/studio/components/explainVis/index.tsx +++ b/shared/studio/components/explainVis/index.tsx @@ -61,7 +61,7 @@ export const ExplainVis = observer(function ExplainVis({ const ExplainHeader = observer(function ExplainHeader() { const state = useExplainState(); const plan = state.focusedPlan ?? state.planTree.data; - const queryTimeCost = explainGraphSettings.isTimeGraph + const queryTimeCost = state.isTimeGraph ? `${plan.totalTime}ms` : plan.totalCost; @@ -88,13 +88,9 @@ const ExplainHeader = observer(function ExplainHeader() { { - explainGraphSettings.isTimeGraph + state.isTimeGraph ? explainGraphSettings.setGraphUnit(graphUnit.cost) : explainGraphSettings.setGraphUnit(graphUnit.time); @@ -127,12 +123,12 @@ const Flamegraph = observer(function Flamegraph({ const range = isLight ? (state.planTree.data.totalTime ?? state.planTree.data.totalCost) / zoom - : ((explainGraphSettings.isTimeGraph && state.planTree.data.totalTime) || + : ((state.isTimeGraph && state.planTree.data.totalTime) || state.planTree.data.totalCost) / zoom; const isTimeGraph = isLight ? !!state.planTree.data.totalTime - : explainGraphSettings.isTimeGraph; + : state.isTimeGraph; return (
{plan.name ?? "Query"}: - Self {explainGraphSettings.isTimeGraph ? "Time:" : "Cost:"} + Self {state.isTimeGraph ? "Time:" : "Cost:"} - {explainGraphSettings.isTimeGraph - ? plan.selfTime!.toPrecision(5).replace(/\.?0+$/, "") + "ms" - : plan.selfCost.toPrecision(5).replace(/\.?0+$/, "")}{" "} + {state.isTimeGraph + ? plan.selfTime!.toPrecision(5).replace(/\.0+$/, "") + "ms" + : plan.selfCost.toPrecision(5).replace(/\.0+$/, "")}{" "}     {( - (explainGraphSettings.isTimeGraph + (state.isTimeGraph ? plan.selfTimePercent! : plan.selfCostPercent) * 100 ) - .toPrecision(2) - .replace(/\.?0+$/, "")} + .toPrecision(3) + .replace(/\.0+$/, "")} % @@ -224,17 +220,19 @@ const PlanDetails = observer(function PlanDetails() {
))} -
- {[ - ["Startup Time", "actual_startup_time"], - ["Total Time", "actual_total_time"], - ].map(([name, key]) => ( -
- {name}: - {plan.raw[key]}ms -
- ))} -
+ {plan.totalTime != null ? ( +
+ {[ + ["Startup Time", "actual_startup_time"], + ["Total Time", "actual_total_time"], + ].map(([name, key]) => ( +
+ {name}: + {plan.raw[key]}ms +
+ ))} +
+ ) : null} ) : ( @@ -280,7 +278,7 @@ const FlamegraphNode = observer(function _FlamegraphNode({ } const childWidth = - (explainGraphSettings.isTimeGraph + (state.isTimeGraph ? subplan.totalTime! / plan.totalTime! : subplan.totalCost / plan.totalCost) * (width - 8); @@ -395,11 +393,11 @@ const FlamegraphNode = observer(function _FlamegraphNode({ {flamegraphNode}

- {explainGraphSettings.isTimeGraph ? "Self time: " : "Self cost: "} + {state.isTimeGraph ? "Self time: " : "Self cost: "} - {explainGraphSettings.isTimeGraph - ? plan.selfTime!.toPrecision(5).replace(/\.?0+$/, "") + "ms" - : plan.selfCost.toPrecision(5).replace(/\.?0+$/, "")} + {state.isTimeGraph + ? plan.selfTime!.toPrecision(5).replace(/\.0+$/, "") + "ms" + : plan.selfCost.toPrecision(5).replace(/\.0+$/, "")}

diff --git a/shared/studio/components/explainVis/state.ts b/shared/studio/components/explainVis/state.ts index a3c9c0a1..4c61e95a 100644 --- a/shared/studio/components/explainVis/state.ts +++ b/shared/studio/components/explainVis/state.ts @@ -9,10 +9,7 @@ import { modelAction, prop, } from "mobx-keystone"; -import { - explainGraphSettings, - graphUnit, -} from "../../state/explainGraphSettings"; +import {explainGraphSettings} from "../../state/explainGraphSettings"; export function createExplainState(rawExplainOutput: string) { const rawData = JSON.parse(rawExplainOutput); @@ -27,13 +24,6 @@ export function createExplainState(rawExplainOutput: string) { contexts ); - explainGraphSettings.setGraphUnit( - planTree.totalTime == null || - explainGraphSettings.userUnitChoice === graphUnit.cost - ? graphUnit.cost - : graphUnit.time - ); - return new ExplainState({ rawData: rawExplainOutput, planTree: frozen(planTree, FrozenCheckMode.Off), @@ -59,7 +49,7 @@ export class ExplainState extends Model({ get maxFlamegraphZoom() { return Math.max( 1, - explainGraphSettings.isTimeGraph + this.isTimeGraph ? this.planTree.data.totalTime! * 10 : this.planTree.data.totalCost ); @@ -80,6 +70,14 @@ export class ExplainState extends Model({ return this.getCtxId(this.selectedPlan?.parent); } + @computed + get isTimeGraph() { + return ( + this.planTree.data.totalTime !== null && + explainGraphSettings._isTimeGraph + ); + } + getCtxId(plan: Plan | null | undefined) { return plan?.contextId; } diff --git a/shared/studio/components/explainVis/treemapLayout.tsx b/shared/studio/components/explainVis/treemapLayout.tsx index 6ac08df2..e447d22c 100644 --- a/shared/studio/components/explainVis/treemapLayout.tsx +++ b/shared/studio/components/explainVis/treemapLayout.tsx @@ -16,7 +16,6 @@ import cn from "@edgedb/common/utils/classNames"; import CodeBlock from "@edgedb/common/ui/codeBlock"; import {observer} from "mobx-react-lite"; import {Theme, useTheme} from "@edgedb/common/hooks/useTheme"; -import {explainGraphSettings} from "../../state/explainGraphSettings"; import {useIsMobile} from "@edgedb/common/hooks/useMobile"; export const lightPalette = ["#D5D8EF", "#FDF5E2", "#DAE9FB", "#E6FFF8"]; @@ -277,7 +276,7 @@ export const TreemapNode = observer( const parentArea = parentSize[0] * parentSize[1]; - const isTimeGraph = explainGraphSettings.isTimeGraph; + const isTimeGraph = state.isTimeGraph; const ctxId = plan.contextId; diff --git a/shared/studio/components/extendedViewers/hexViewer/index.tsx b/shared/studio/components/extendedViewers/hexViewer/index.tsx index ed36cc79..9943652f 100644 --- a/shared/studio/components/extendedViewers/hexViewer/index.tsx +++ b/shared/studio/components/extendedViewers/hexViewer/index.tsx @@ -6,7 +6,7 @@ import styles from "./hexViewer.module.scss"; import cn from "@edgedb/common/utils/classNames"; import {useResize} from "@edgedb/common/hooks/useResize"; import {createHexViewerState, HexViewer as HexViewerState} from "./state"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {computed} from "mobx"; import {ActionButton, ActionsBar} from "../shared"; import {CustomScrollbars} from "@edgedb/common/ui/customScrollbar"; diff --git a/shared/studio/components/visualQuerybuilder/index.tsx b/shared/studio/components/visualQuerybuilder/index.tsx index 3bebcf8e..1e9542fd 100644 --- a/shared/studio/components/visualQuerybuilder/index.tsx +++ b/shared/studio/components/visualQuerybuilder/index.tsx @@ -1,4 +1,4 @@ -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import cn from "@edgedb/common/utils/classNames"; import {useDatabaseState, useTabState} from "../../state"; import {SchemaData} from "../../state/database"; diff --git a/shared/studio/package.json b/shared/studio/package.json index df8a69b1..096773d0 100644 --- a/shared/studio/package.json +++ b/shared/studio/package.json @@ -20,7 +20,7 @@ "idb": "^7.0.1", "mobx": "^6.5.0", "mobx-keystone": "^1.11.0", - "mobx-react-lite": "^3.3.0", + "mobx-react-lite": "^4.0.0", "react-colorful": "^5.6.1", "react-error-boundary": "^3.1.4", "react-hook-form": "^7.32.2", diff --git a/shared/studio/state/explainGraphSettings.ts b/shared/studio/state/explainGraphSettings.ts index 9a4291c5..5ead1980 100644 --- a/shared/studio/state/explainGraphSettings.ts +++ b/shared/studio/state/explainGraphSettings.ts @@ -20,7 +20,7 @@ export class ExplainGraphSettings extends Model({ userUnitChoice: prop(null).withSetter(), }) { @computed - get isTimeGraph() { + get _isTimeGraph() { return this.graphUnit === graphUnit.time; } diff --git a/shared/studio/tabs/dataview/dataInspector.module.scss b/shared/studio/tabs/dataview/dataInspector.module.scss index 7d23c469..1d157fcd 100644 --- a/shared/studio/tabs/dataview/dataInspector.module.scss +++ b/shared/studio/tabs/dataview/dataInspector.module.scss @@ -480,23 +480,6 @@ } } -.scalar_str, -.scalar_uuid { - &:before, - &:after { - content: ""; - } -} -.scalar_uuid { - color: #6f6f6f; - - @include breakpoint(mobile) { - @include darkTheme { - color: #adadad; - } - } -} - .rowIndex { display: flex; padding-left: 4px; diff --git a/shared/studio/tabs/dataview/dataInspector.tsx b/shared/studio/tabs/dataview/dataInspector.tsx index 2b80be0d..843413a2 100644 --- a/shared/studio/tabs/dataview/dataInspector.tsx +++ b/shared/studio/tabs/dataview/dataInspector.tsx @@ -1,15 +1,9 @@ -import {createContext, Fragment, useContext, useEffect} from "react"; -import {observer} from "mobx-react"; - -import {ICodec} from "edgedb/dist/codecs/ifaces"; -import {EnumCodec} from "edgedb/dist/codecs/enum"; -import {NamedTupleCodec} from "edgedb/dist/codecs/namedtuple"; -import {MultiRangeCodec, RangeCodec} from "edgedb/dist/codecs/range"; +import {createContext, useContext, useEffect} from "react"; +import {observer} from "mobx-react-lite"; import cn from "@edgedb/common/utils/classNames"; import {InspectorRow} from "@edgedb/inspector"; -import {renderValue} from "@edgedb/inspector/buildScalar"; import inspectorStyles from "@edgedb/inspector/inspector.module.scss"; import styles from "./dataInspector.module.scss"; @@ -50,6 +44,8 @@ import { HeaderResizeHandle, } from "@edgedb/common/components/dataGrid"; import gridStyles from "@edgedb/common/components/dataGrid/dataGrid.module.scss"; +import {renderCellValue} from "@edgedb/common/components/dataGrid/renderUtils"; + import {DefaultColumnWidth} from "@edgedb/common/components/dataGrid/state"; import {calculateInitialColWidths} from "@edgedb/common/components/dataGrid/utils"; import {FieldConfigButton} from "./fieldConfig"; @@ -452,93 +448,6 @@ const ExpandedEndCell = observer(function ExpandedEndCell({ ); }); -const inspectorOverrideStyles = { - uuid: styles.scalar_uuid, - str: styles.scalar_str, -}; - -function renderCellValue( - value: any, - codec: ICodec, - nested = false -): JSX.Element { - switch (codec.getKind()) { - case "scalar": - case "range": - case "multirange": - return renderValue( - value, - codec.getKnownTypeName(), - codec instanceof EnumCodec, - codec instanceof RangeCodec || codec instanceof MultiRangeCodec - ? codec.getSubcodecs()[0].getKnownTypeName() - : undefined, - false, - !nested ? inspectorOverrideStyles : undefined, - 100 - ).body; - case "set": - return ( - <> - {"{"} - {(value as any[]).map((item, i) => ( - - {i !== 0 ? ", " : null} - {renderCellValue(item, codec.getSubcodecs()[0], true)} - - ))} - {"}"} - - ); - case "array": - return ( - <> - [ - {(value as any[]).map((item, i) => ( - - {i !== 0 ? ", " : null} - {renderCellValue(item, codec.getSubcodecs()[0], true)} - - ))} - ] - - ); - case "tuple": - return ( - <> - ( - {(value as any[]).map((item, i) => ( - - {i !== 0 ? ", " : null} - {renderCellValue(item, codec.getSubcodecs()[i], true)} - - ))} - ) - - ); - case "namedtuple": { - const fieldNames = (codec as NamedTupleCodec).getNames(); - const subCodecs = codec.getSubcodecs(); - return ( - <> - ( - {fieldNames.map((name, i) => ( - - {i !== 0 ? ", " : null} - {name} - {" := "} - {renderCellValue(value[name], subCodecs[i], true)} - - ))} - ) - - ); - } - default: - return <>; - } -} - const GridCell = observer(function GridCell({ field, data, diff --git a/shared/studio/tabs/dataview/dataview.module.scss b/shared/studio/tabs/dataview/dataview.module.scss index c870462b..bb33e50f 100644 --- a/shared/studio/tabs/dataview/dataview.module.scss +++ b/shared/studio/tabs/dataview/dataview.module.scss @@ -63,7 +63,7 @@ align-items: center; flex-shrink: 0; height: 40px; - background: var(--panel_background); + background: var(--header_background); padding-left: 4px; border-bottom: 1px solid var(--panel_border); diff --git a/shared/studio/tabs/dataview/fieldConfig.tsx b/shared/studio/tabs/dataview/fieldConfig.tsx index e561737e..bd04cba3 100644 --- a/shared/studio/tabs/dataview/fieldConfig.tsx +++ b/shared/studio/tabs/dataview/fieldConfig.tsx @@ -345,7 +345,7 @@ const FieldConfigPopup = observer(function FieldConfigPopup({
{draftState.pinned.size ? (
@@ -368,7 +368,7 @@ const FieldConfigPopup = observer(function FieldConfigPopup({
) : null}
diff --git a/shared/studio/tabs/dataview/index.tsx b/shared/studio/tabs/dataview/index.tsx index 24f43dcb..0ef6aff8 100644 --- a/shared/studio/tabs/dataview/index.tsx +++ b/shared/studio/tabs/dataview/index.tsx @@ -1,5 +1,5 @@ import {useEffect, useMemo} from "react"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import cn from "@edgedb/common/utils/classNames"; diff --git a/shared/studio/tabs/graphql/index.tsx b/shared/studio/tabs/graphql/index.tsx index 511ccf67..683c44cf 100644 --- a/shared/studio/tabs/graphql/index.tsx +++ b/shared/studio/tabs/graphql/index.tsx @@ -1,5 +1,5 @@ import {useRef, useEffect} from "react"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {jsonLanguage, json} from "@codemirror/lang-json"; diff --git a/shared/studio/tabs/queryEditor/index.tsx b/shared/studio/tabs/queryEditor/index.tsx index e6749b5d..c87cecf4 100644 --- a/shared/studio/tabs/queryEditor/index.tsx +++ b/shared/studio/tabs/queryEditor/index.tsx @@ -1,5 +1,5 @@ import {useCallback, useEffect, useMemo, useRef, useState} from "react"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {Text} from "@codemirror/state"; import cn from "@edgedb/common/utils/classNames"; @@ -15,6 +15,9 @@ import { QueryHistoryResultItem, QueryHistoryErrorItem, EditorKind, + QueryHistoryItem, + OutputMode, + explainStateCache, } from "./state"; import {DatabaseTabSpec} from "../../components/databasePage"; @@ -22,15 +25,17 @@ import {DatabaseTabSpec} from "../../components/databasePage"; import {Theme, useTheme} from "@edgedb/common/hooks/useTheme"; import SplitView from "@edgedb/common/ui/splitView"; -import Button from "@edgedb/common/ui/button"; import {CustomScrollbars} from "@edgedb/common/ui/customScrollbar"; +import {Button} from "@edgedb/common/newui"; + import {HistoryPanel} from "./history"; import ParamEditorPanel from "./paramEditor"; import {TabEditorIcon, MobileHistoryIcon} from "../../icons"; import {useResize} from "@edgedb/common/hooks/useResize"; import {VisualQuerybuilder} from "../../components/visualQuerybuilder"; import Inspector from "@edgedb/inspector"; +import {ResultGrid} from "@edgedb/common/components/resultGrid"; import { ExtendedViewerContext, ExtendedViewerRenderer, @@ -43,6 +48,9 @@ import {CodeEditorExplainContexts} from "../../components/explainVis/codeEditorC import {ExplainStateType} from "../../components/explainVis/state"; import {LabelsSwitch, switchState} from "@edgedb/common/ui/switch"; import {useIsMobile} from "@edgedb/common/hooks/useMobile"; +import {EdgeDBSet} from "@edgedb/common/decodeRawBuffer"; +import {ObjectCodec} from "edgedb/dist/codecs/object"; +import {ICodec} from "edgedb/dist/codecs/ifaces"; export const QueryEditorView = observer(function QueryEditorView() { const editorState = useTabState(QueryEditor); @@ -119,7 +127,6 @@ export const QueryEditorView = observer(function QueryEditorView() {
- {!editorState.queryRunning ? ( ) : ( )} @@ -161,7 +171,15 @@ export const QueryEditorView = observer(function QueryEditorView() { /> )} , - , + editorState.currentResult ? ( + + ) : ( + <> + ), ]} state={editorState.splitView} minViewSize={20} @@ -203,7 +221,6 @@ export const QueryEditorView = observer(function QueryEditorView() { )} - {editorState.extendedViewerItem ? (
) : null} - {/* {editorState.showExplain && (editorState.currentResult as QueryHistoryResultItem).explainState ? ( (null); - const result = editorState.currentResult; + useEffect(() => { + if ( + !data && + result instanceof QueryHistoryResultItem && + result.hasResult + ) { + state.getResultData(result.$modelId).then((data) => setData(data)); + } + }, [data]); let content: JSX.Element | null = null; if (result instanceof QueryHistoryResultItem) { if (result.hasResult) { - if (result.status === "EXPLAIN" || result.status === "ANALYZE QUERY") { - content = ; - } else if (result.inspectorState) { - content = ( - - ); - } else { + if (data === null) { content = (
); + } else if ( + result.status === "EXPLAIN" || + result.status === "ANALYZE QUERY" + ) { + content = ; + } else { + const {mode, toggleEl} = outputModeToggle( + data._codec, + state.outputMode, + state.setOutputMode.bind(state) + ); + + content = ( + <> +
+ {toggleEl} +
+ {mode === OutputMode.Grid ? ( + + ) : ( + + )} + + ); } } else { content = ( @@ -398,6 +447,53 @@ const QueryResult = observer(function QueryResult() { return
{content}
; }); +export function outputModeToggle( + codec: ICodec, + outputMode: OutputMode, + setOutputMode: (mode: OutputMode) => void +) { + const tableOutputAvailable = codec instanceof ObjectCodec; + const mode = tableOutputAvailable ? outputMode : OutputMode.Tree; + + return { + mode, + toggleEl: ( +
+
setOutputMode(OutputMode.Tree)} + > + Tree +
+
+ setOutputMode( + outputMode === OutputMode.Grid + ? OutputMode.Tree + : OutputMode.Grid + ) + } + /> +
setOutputMode(OutputMode.Grid)} + > + Table +
+
+ ), + }; +} + export const editorTabSpec: DatabaseTabSpec = { path: "editor", label: "Editor", diff --git a/shared/studio/tabs/queryEditor/paramEditor/index.tsx b/shared/studio/tabs/queryEditor/paramEditor/index.tsx index 420ca31e..c9a60c00 100644 --- a/shared/studio/tabs/queryEditor/paramEditor/index.tsx +++ b/shared/studio/tabs/queryEditor/paramEditor/index.tsx @@ -1,4 +1,4 @@ -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import cn from "@edgedb/common/utils/classNames"; diff --git a/shared/studio/tabs/queryEditor/repl.module.scss b/shared/studio/tabs/queryEditor/repl.module.scss index 9ca06a74..b14dfcf9 100644 --- a/shared/studio/tabs/queryEditor/repl.module.scss +++ b/shared/studio/tabs/queryEditor/repl.module.scss @@ -11,11 +11,18 @@ $historyTransitionTime: 0.15s; .main { height: auto; transition: margin $historyTransitionTime; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.04), 0 0 4px rgba(0, 0, 0, 0.06); + border-radius: 0 12px 12px 0; & > div:first-child { border-top-left-radius: 0; border-bottom-left-radius: 0; } + + @include darkTheme { + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.2), + 0px 0px 4px 0px rgba(0, 0, 0, 0.3); + } } &.showHistory .main { @@ -41,29 +48,31 @@ $historyTransitionTime: 0.15s; left: 0; right: 0; bottom: 0; - background: var(--app-panel-bg); - border-radius: 8px; + background: var(--app_panel_background); + border-radius: 12px; z-index: 1; overflow: hidden; } .sidebar { position: relative; - width: 32px; + width: 36px; flex-shrink: 0; - background: #e9e9e9; - border-radius: 8px 0 0 8px; + background: var(--header_background); + border-radius: 12px 0 0 12px; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.04), 0 0 4px rgba(0, 0, 0, 0.06); z-index: 1; overflow: hidden; transition: width $historyTransitionTime; .showHistory & { width: 194px; - border-radius: 8px; + border-radius: 12px; } @include darkTheme { - background: #2e2e2e; + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.2), + 0px 0px 4px 0px rgba(0, 0, 0, 0.3); } @include breakpoint(mobile) { @@ -74,7 +83,10 @@ $historyTransitionTime: 0.15s; .queryResult { flex-grow: 1; min-width: 0; - font-family: "Roboto Mono", monospace; + font-family: "Roboto Mono Variable", monospace; + display: flex; + flex-direction: column; + background: var(--app_panel_background); } .inspector { @@ -101,6 +113,89 @@ $historyTransitionTime: 0.15s; padding: 16px; } +.resultHeader { + background: var(--header_background); + border-bottom: 1px solid var(--panel_border); + display: flex; + align-items: center; + justify-content: end; + height: 36px; + flex-shrink: 0; + padding: 0 8px; + + &.noBorder { + border-bottom: 0; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06); + + @include darkTheme { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + } + } +} + +.outputModeToggle { + display: flex; + + .label { + font-family: "Roboto Flex Variable", sans-serif; + font-size: 13px; + font-weight: 500; + line-height: 24px; + padding: 0 12px; + color: var(--tertiary_text_color); + cursor: pointer; + + &.selected { + color: var(--main_text_color); + } + + &.disabled { + pointer-events: none; + opacity: 0.5; + } + } + + .toggle { + position: relative; + width: 38px; + height: 24px; + border-radius: 12px; + background: var(--Grey90); + cursor: pointer; + + &:after { + position: absolute; + content: ""; + width: 20px; + height: 20px; + top: 2px; + left: 2px; + border-radius: 10px; + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); + transition: transform 0.25s; + } + + &.rightSelected:after { + transform: translateX(14px); // 36 - 20 - 2 + } + + &.disabled { + pointer-events: none; + opacity: 0.5; + } + + @include darkTheme { + background: var(--Grey14); + + &:after { + background: var(--Grey45); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); + } + } + } +} + .inspectorLoading { height: 100%; display: flex; @@ -111,7 +206,7 @@ $historyTransitionTime: 0.15s; .toolbar { position: absolute; - width: 32px; + width: 36px; height: 100%; display: grid; grid-template-rows: 1fr auto 1fr; @@ -264,8 +359,9 @@ $historyTransitionTime: 0.15s; .historyItem { position: absolute; - background-color: #fff; - border-radius: 6px; + background-color: var(--Grey97); + border: 1px solid var(--Grey90); + border-radius: 8px; cursor: pointer; height: 105px; width: calc(100% - 32px); @@ -273,7 +369,7 @@ $historyTransitionTime: 0.15s; transition: opacity 0.1s ease-in-out; &:hover { - background-color: #fafafa; + background-color: var(--Grey95); .loadButton { opacity: 1; @@ -281,10 +377,11 @@ $historyTransitionTime: 0.15s; } @include darkTheme { - background-color: #4c4c4c; + background-color: var(--Grey25); + border-color: var(--Grey30); &:hover { - background-color: #4c4c4c; + background-color: var(--Grey30); } } @@ -302,6 +399,7 @@ $historyTransitionTime: 0.15s; } &.selected { + border-color: transparent; outline: 2px solid var(--accentGreen); } @@ -325,7 +423,7 @@ $historyTransitionTime: 0.15s; position: absolute; bottom: 8px; right: 8px; - background-color: #f2f2f2; + background-color: #fff; border-radius: 16px; font-size: 10px; font-weight: 500; @@ -346,18 +444,34 @@ $historyTransitionTime: 0.15s; } &.draft { - background: none !important; + background-color: transparent; + border-width: 0; + background-image: dashedBorderBg(#aaa); display: flex; align-items: center; justify-content: center; font-style: italic; - color: #666; - border: 2px dashed #aaa; - box-sizing: border-box; + color: var(--Grey40); + + &:hover { + background-color: var(--Grey97); + } &.selected { outline: none; - border-color: var(--accentGreen); + background-image: dashedBorderBg(#2cb88e, 2px); + } + + @include darkTheme { + background-image: dashedBorderBg(#666); + + &:hover { + background-color: var(--Grey22); + } + + &.selected { + background-image: dashedBorderBg(#279474, 2px); + } } } @@ -379,7 +493,7 @@ $historyTransitionTime: 0.15s; content: ""; height: 8px; flex-grow: 1; - border-bottom: 2px solid #d7d7d7; + border-bottom: 1px solid #d7d7d7; } &:before { margin-right: 8px; @@ -439,6 +553,7 @@ $historyTransitionTime: 0.15s; display: flex; min-width: 0; flex-direction: column; + --code-editor-bg: var(--app_panel_background); .editorBlockInner { position: relative; @@ -482,7 +597,7 @@ $historyTransitionTime: 0.15s; } .controls { - margin: 8px; + margin: 12px; display: flex; align-items: center; justify-content: flex-end; @@ -534,11 +649,11 @@ $historyTransitionTime: 0.15s; } .runBtn { - --buttonBg: var(--app-accent-green); - --buttonTextColour: #0a4634; + --buttonPrimaryBackground: var(--app-accent-green); align-self: flex-start; flex-shrink: 0; pointer-events: auto; + border-radius: 12px; @include breakpoint(mobile) { display: none; diff --git a/shared/studio/tabs/queryEditor/state/index.ts b/shared/studio/tabs/queryEditor/state/index.ts index 8f841315..8f76b0a1 100644 --- a/shared/studio/tabs/queryEditor/state/index.ts +++ b/shared/studio/tabs/queryEditor/state/index.ts @@ -50,15 +50,24 @@ import {sessionStateCtx} from "../../../state/sessionState"; import { createExplainState, ExplainState, - ExplainStateType, } from "../../../components/explainVis/state"; import {ProtocolVersion} from "edgedb/dist/ifaces"; +import { + createResultGridState, + ResultGridState, +} from "@edgedb/common/components/resultGrid"; +import LRU from "edgedb/dist/primitives/lru"; export enum EditorKind { EdgeQL, VisualBuilder, } +export enum OutputMode { + Tree, + Grid, +} + type HistoryItemQueryData = | { kind: EditorKind.EdgeQL; @@ -85,7 +94,9 @@ export class QueryHistoryDraftItem extends ExtendedModel( {} ) {} -const explainStateCache = new ObservableLRU(10); +const resultInspectorCache = new LRU({capacity: 10}); +const resultGridStateCache = new LRU({capacity: 10}); +export const explainStateCache = new ObservableLRU(10); @model("QueryEditor/HistoryResultItem") export class QueryHistoryResultItem extends ExtendedModel(QueryHistoryItem, { @@ -93,33 +104,42 @@ export class QueryHistoryResultItem extends ExtendedModel(QueryHistoryItem, { hasResult: prop(), implicitLimit: prop(), }) { - get inspectorState() { - const queryEditor = queryEditorCtx.get(this)!; - const state = queryEditor.resultInspectorCache.get(this.$modelId); + get queryEditor() { + return queryEditorCtx.get(this)!; + } + + getInspectorState(data: EdgeDBSet) { + const queryEditor = this.queryEditor; + + let state = resultInspectorCache.get(this.$modelId); if (!state) { - queryEditor.fetchResultData(this.$modelId, this.implicitLimit); + state = createInspector(data, this.implicitLimit, (item) => + queryEditor.setExtendedViewerItem(item) + ); + resultInspectorCache.set(this.$modelId, state); } - return state ?? null; + + return state; } - get explainState() { - const state = explainStateCache.get(this.$modelId); + getResultGridState(data: EdgeDBSet) { + let state = resultGridStateCache.get(this.$modelId); + if (!state) { + state = createResultGridState(data._codec, data); + resultGridStateCache.set(this.$modelId, state); + } + + return state; + } + getExplainState(data: EdgeDBSet) { + let state = explainStateCache.get(this.$modelId); if (!state) { - fetchResultData(this.$modelId).then((resultData) => { - if (resultData) { - const explainState = createExplainState( - decode( - resultData.outCodecBuf, - resultData.resultBuf, - resultData.protoVer ?? [1, 0] - )![0] - ); - explainStateCache.set(this.$modelId, explainState); - } - }); + state = createExplainState(data[0]); + explainStateCache.set(this.$modelId, state); } - return state ?? null; + + return state; } } @@ -155,6 +175,8 @@ export class QueryEditor extends Model({ selectedEditor: prop(EditorKind.EdgeQL).withSetter(), + outputMode: prop(OutputMode.Tree).withSetter(), + showHistory: prop(false), queryHistory: prop(() => [null as any]), @@ -284,7 +306,7 @@ export class QueryEditor extends Model({ this._fetchingHistory = false; }); - resultInspectorCache = new ObservableLRU(20); + resultDataCache = new LRU>({capacity: 20}); @observable.ref extendedViewerItem: Item | null = null; @@ -294,20 +316,21 @@ export class QueryEditor extends Model({ this.extendedViewerItem = item; } - async fetchResultData(itemId: string, implicitLimit: number) { - const resultData = await fetchResultData(itemId); - if (resultData) { - const inspector = createInspector( - decode( - resultData.outCodecBuf, - resultData.resultBuf, - resultData.protoVer ?? [1, 0] - )!, - implicitLimit, - (item) => this.setExtendedViewerItem(item) + async getResultData(itemId: string): Promise { + let data = this.resultDataCache.get(itemId); + if (!data) { + data = fetchResultData(itemId).then((resultData) => + resultData + ? decode( + resultData.outCodecBuf, + resultData.resultBuf, + resultData.protoVer ?? [1, 0] + ) + : null ); - this.resultInspectorCache.set(itemId, inspector); + this.resultDataCache.set(itemId, data); } + return data; } @modelAction @@ -402,8 +425,9 @@ export class QueryEditor extends Model({ }), }; - if (item.explainState) - explainStateCache.set(item.$modelId, item.explainState); + if (item._explainState) { + explainStateCache.set(item.$modelId, item._explainState); + } const historyItem = new QueryHistoryResultItem({ ...historyItemData, @@ -530,22 +554,10 @@ export class QueryEditor extends Model({ implicitLimit: data.implicitLimit, }); if (data.result) { - if ( - data.status === ExplainStateType.explain || - data.status === ExplainStateType.analyzeQuery - ) { - explainStateCache.set( - historyItem.$modelId, - createExplainState(data.result[0]) - ); - } else { - this.resultInspectorCache.set( - historyItem.$modelId, - createInspector(data.result, data.implicitLimit, (item) => - this.setExtendedViewerItem(item) - ) - ); - } + this.resultDataCache.set( + historyItem.$modelId, + Promise.resolve(data.result) + ); resultData = { outCodecBuf: data.outCodecBuf, resultBuf: data.resultBuf, diff --git a/shared/studio/tabs/repl/index.tsx b/shared/studio/tabs/repl/index.tsx index 5c9abc10..04b4a27e 100644 --- a/shared/studio/tabs/repl/index.tsx +++ b/shared/studio/tabs/repl/index.tsx @@ -8,7 +8,7 @@ import { useState, } from "react"; -import {reaction} from "mobx"; +import {reaction, runInAction} from "mobx"; import {observer} from "mobx-react-lite"; import {useInitialValue} from "@edgedb/common/hooks/useInitialValue"; @@ -18,7 +18,7 @@ import {Theme, useTheme} from "@edgedb/common/hooks/useTheme"; import CodeBlock from "@edgedb/common/ui/codeBlock"; import {CustomScrollbars} from "@edgedb/common/ui/customScrollbar"; import Spinner from "@edgedb/common/ui/spinner"; -import Button from "@edgedb/common/ui/button"; +import {Button} from "@edgedb/common/newui"; import cn from "@edgedb/common/utils/classNames"; import { @@ -26,7 +26,14 @@ import { CodeEditorRef, createCodeEditor, } from "@edgedb/code-editor"; -import Inspector, {DEFAULT_ROW_HEIGHT} from "@edgedb/inspector"; + +import { + DEFAULT_LINE_HEIGHT, + DEFAULT_ROW_HEIGHT, + InspectorRow, + useInspectorKeybindings, +} from "@edgedb/inspector"; +import inspectorStyles from "@edgedb/inspector/inspector.module.scss"; import {DatabaseTabSpec} from "../../components/databasePage"; import {ExplainType, ExplainVis} from "../../components/explainVis"; @@ -35,12 +42,11 @@ import { ExplainHighlightsRef, ExplainHighlightsRenderer, } from "../../components/explainVis/codeEditorContexts"; -import {ExplainStateType} from "../../components/explainVis/state"; import { ExtendedViewerContext, ExtendedViewerRenderer, } from "../../components/extendedViewers"; -import {ArrowDown, TabReplIcon} from "../../icons"; +import {TabReplIcon} from "../../icons"; import {useDatabaseState, useTabState} from "../../state"; import { @@ -48,7 +54,7 @@ import { Repl, ReplHistoryItem as ReplHistoryItemState, } from "./state"; -import {QueryEditor} from "../queryEditor/state"; +import {OutputMode, QueryEditor} from "../queryEditor/state"; import {renderCommandResult} from "./commands"; import {useDBRouter} from "../../hooks/dbRoute"; @@ -57,6 +63,13 @@ import styles from "./repl.module.scss"; import {isEndOfStatement} from "./state/utils"; import {useIsMobile} from "@edgedb/common/hooks/useMobile"; import {RunButton} from "@edgedb/common/ui/mobile"; +import {InspectorState} from "@edgedb/inspector/state"; +import {InspectorContext} from "@edgedb/inspector/context"; +import {outputModeToggle} from "../queryEditor"; +import { + ResultGrid, + ResultGridState, +} from "@edgedb/common/components/resultGrid"; const ReplView = observer(function ReplView() { const replState = useTabState(Repl); @@ -378,6 +391,190 @@ const ReplInput = observer(function ReplInput() { ); }); +const InspectorRenderer = observer(function InspectorRenderer({ + replState, + inspectorState, + maxLines, + offsetTop, +}: { + replState: Repl; + inspectorState: InspectorState; + maxLines?: number; + offsetTop: number; +}) { + const vPad = DEFAULT_ROW_HEIGHT - DEFAULT_LINE_HEIGHT; + const items = inspectorState.getItems(); + const noVirt = (items[1]?.height ?? 0) > 1; + const itemsLength = maxLines + ? Math.min(items.length, maxLines) + : items.length; + + const [visibleItems, setVisibleItems] = useState([0, maxLines ?? 1]); + + useLayoutEffect(() => { + const el = replState.scrollRef; + if (!el || noVirt) return; + + const listener = () => { + const scrollTop = el.scrollHeight + el.scrollTop - el.clientHeight; + setVisibleItems([ + Math.min( + itemsLength - 1, + Math.max( + 0, + Math.floor((scrollTop - offsetTop) / DEFAULT_ROW_HEIGHT) - 5 + ) + ), + Math.min( + itemsLength - 1, + Math.max( + maxLines ?? 0, + Math.ceil( + (scrollTop - offsetTop + el.clientHeight) / DEFAULT_ROW_HEIGHT + ) + 5 + ) + ), + ]); + }; + listener(); + + el.addEventListener("scroll", listener); + + return () => { + el.removeEventListener("scroll", listener); + }; + }, [itemsLength, offsetTop, replState.scrollRef, noVirt]); + + const onKeyDown = useInspectorKeybindings(inspectorState); + + const inspectorStyle = { + "--lineHeight": `${DEFAULT_LINE_HEIGHT}px`, + "--rowPad": `${vPad / 2}px`, + gridAutoRows: DEFAULT_ROW_HEIGHT, + ...(noVirt + ? { + height: + DEFAULT_ROW_HEIGHT * 2 + + items[1].height! * DEFAULT_LINE_HEIGHT + + vPad, + gridTemplateRows: `${DEFAULT_ROW_HEIGHT}px ${ + items[1].height! * DEFAULT_LINE_HEIGHT + vPad + }px ${DEFAULT_ROW_HEIGHT}px`, + } + : {height: itemsLength * DEFAULT_ROW_HEIGHT}), + } as any; + + return ( + +
+ +
+
+ ); +}); + +const InspectorRendererInner = observer(function InspectorRendererInner({ + inspectorState, + startIndex, + endIndex, +}: { + inspectorState: InspectorState; + startIndex: number; + endIndex: number; +}) { + return ( + <> + {inspectorState + .getItems() + .slice(startIndex, endIndex + 1) + .map((item, i) => { + const isExpanded = inspectorState.expanded!.has(item.id); + const index = i + startIndex; + return ( + { + isExpanded + ? inspectorState.collapseItem(index) + : inspectorState.expandItem(index); + }} + /> + ); + })} + + ); +}); + +const ResultGridWrapper = observer(function ResultGridWrapper({ + replState, + gridState, + offsetTop, + truncated, +}: { + replState: Repl; + gridState: ResultGridState; + offsetTop: number; + truncated: boolean; +}) { + const [containerHeight, setContainerHeight] = useState( + replState.scrollRef?.clientHeight ?? 0 + ); + useResize(replState.scrollRef, ({height}) => setContainerHeight(height)); + + const contentHeight = + gridState.grid.headerHeight + gridState.grid.gridContentHeight; + + useLayoutEffect(() => { + const el = replState.scrollRef; + const gridEl = gridState.grid.gridElRef; + if (truncated || !el || !gridEl) return; + + const listener = () => { + const scrollTop = el.scrollHeight + el.scrollTop - el.clientHeight; + gridEl.scrollTop = scrollTop - offsetTop; + }; + listener(); + + el.addEventListener("scroll", listener); + + return () => { + el.removeEventListener("scroll", listener); + }; + }, [offsetTop, truncated, replState.scrollRef, gridState.grid.gridElRef]); + + return ( +
+ +
+ ); +}); + const ReplHistoryItem = observer(function ReplHistoryItem({ state, index, @@ -388,29 +585,45 @@ const ReplHistoryItem = observer(function ReplHistoryItem({ state: Repl; index: number; item: ReplHistoryItemState; - styleTop: any; + styleTop: number; dbName: string; }) { const ref = useRef(null); - const replState = useTabState(Repl); - const editorState = useTabState(QueryEditor); - const isMobile = useIsMobile(); - let showExpandBtn = false; + const headerRef = useRef(null); + const editorState = useTabState(QueryEditor); const {navigate, currentPath} = useDBRouter(); - const containerRef = useRef(null); - - const updateScroll = useRef(false); - const runInEditor = () => { editorState.loadFromRepl(item); navigate(`${currentPath[0]}/editor`); }; + const replState = useTabState(Repl); + const isMobile = useIsMobile(); + + const containerRef = useRef(null); + + const updateScroll = useRef(false); + + const [data, setData] = useState(() => + item.hasResult + ? state.resultDataCache.get(item.$modelId)?.result + : undefined + ); + + useEffect(() => { + if (item.hasResult && data === undefined) { + state.getResultData(item.$modelId).promise.then((data) => setData(data)); + } + }, [item.hasResult]); + + const [headerHeight, setHeaderHeight] = useState(0); + useResize(headerRef, ({height}) => setHeaderHeight(height)); + useEffect(() => { const disposer = reaction( - () => item.inspectorState?._items.length, + () => item._inspectorState?._items.length, (curr, prev) => { if (prev && curr !== prev) { updateScroll.current = true; @@ -420,7 +633,10 @@ const ReplHistoryItem = observer(function ReplHistoryItem({ return () => { disposer(); - item._inspector = null; + runInAction(() => { + item._inspectorState = null; + item._explainState = null; + }); }; }, []); @@ -441,11 +657,11 @@ const ReplHistoryItem = observer(function ReplHistoryItem({ [item.showDateHeader] ); - let output: JSX.Element | null; - - const isExplain = - item.status === ExplainStateType.explain || - item.status === ExplainStateType.analyzeQuery; + let output: JSX.Element | null = null; + let headerExtra: JSX.Element | null = null; + let showExpandBtn = false; + let showHeaderShadow = true; + let hasScroll = false; if (item.error) { output = ( @@ -464,40 +680,70 @@ const ReplHistoryItem = observer(function ReplHistoryItem({ ); } else if (item.status) { if (item.hasResult) { - if (isExplain) { + if (!data) { + output = <>loading ...; + } else if (item.isExplain) { output = (
-
); } else { - const inspectorState = item.inspectorState; + const maxLines = item.showMore ? undefined : 16; + + const {mode, toggleEl} = outputModeToggle( + data._codec, + item.outputMode, + (mode) => { + updateScroll.current = true; + item.setOutputMode(mode); + } + ); - if (inspectorState) { - const maxLines = item.showMore ? undefined : 16; + headerExtra = ( +
{toggleEl}
+ ); + + if (mode === OutputMode.Tree) { + const inspectorState = item.getInspectorState(data); showExpandBtn = !!maxLines && inspectorState.totalItemsLines > maxLines; output = ( - ); } else { - output = <>loading ...; + const gridState = item.getResultGridState(data); + + showExpandBtn = !!maxLines && gridState.rowCount > maxLines; + showHeaderShadow = false; + hasScroll = true; + + output = ( + + ); } } } else { @@ -532,16 +778,29 @@ const ReplHistoryItem = observer(function ReplHistoryItem({ const marginLeftRepl = isMobile ? "0px" - : isExplain + : item.isExplain ? "16px" : `${dbName.length + 2}ch`; + const expandButton = showExpandBtn ? ( +
+ +
+ ) : null; + return (
@@ -551,115 +810,124 @@ const ReplHistoryItem = observer(function ReplHistoryItem({
) : null}
-
- {[ - `${dbName}>`, - ...Array((truncateQuery ? 20 : queryLines) - 1).fill( - ".".repeat(dbName.length + 1) - ), - ].join("\n")} -
+
+
+ {[ + `${dbName}>`, + ...Array((truncateQuery ? 20 : queryLines) - 1).fill( + ".".repeat(dbName.length + 1) + ), + ].join("\n")} +
- -
-
- - {item.error?.data.range ? ( -
+ +
+
+ + {item.error?.data.range ? ( +
+ ) : null} +
+ {truncateQuery ? ( +
+ +
) : null}
- {truncateQuery ? ( -
-
- ) : null} + +
+ {new Date(item.timestamp).toLocaleTimeString()}
- -
- {new Date(item.timestamp).toLocaleTimeString()}
+ {headerExtra}
{output ? ( - -
- {output} -
- {showExpandBtn ? ( -
- +
+ {output}
- ) : null} + {expandButton} +
+ + ) : ( +
+ {output} + {expandButton}
- + ) ) : null}
); }); -function QueryCodeBlock({ +const QueryCodeBlock = observer(function QueryCodeBlock({ item, containerRef, }: { @@ -667,8 +935,6 @@ function QueryCodeBlock({ containerRef: RefObject; }) { const [ref, setRef] = useState(null); - const isExplain = - item.status === "EXPLAIN" || item.status === "ANALYZE QUERY"; const explainHighlightsRef = useCallback((node: any) => { if (node) { @@ -682,17 +948,17 @@ function QueryCodeBlock({ } }, [ref]); - return isExplain ? ( + return item.isExplain ? ( <> - {item.explainState ? ( + {item._explainState ? ( ) : null} @@ -713,7 +979,7 @@ function QueryCodeBlock({ } /> ); -} +}); const headerASCII = ` /$$ diff --git a/shared/studio/tabs/repl/repl.module.scss b/shared/studio/tabs/repl/repl.module.scss index bcc5da2f..6aaa9258 100644 --- a/shared/studio/tabs/repl/repl.module.scss +++ b/shared/studio/tabs/repl/repl.module.scss @@ -17,10 +17,11 @@ flex-grow: 1; min-height: 0; min-width: 0; - background-color: #f7f7f7; - --code-editor-bg: #f7f7f7; - border-radius: 8px; - font-family: "Roboto Mono", monospace; + background-color: var(--app_panel_background); + --code-editor-bg: var(--app_panel_background); + border-radius: 12px; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.04), 0 0 4px rgba(0, 0, 0, 0.06); + font-family: "Roboto Mono Variable", monospace; :global { .cm-scroller { @@ -40,8 +41,8 @@ } @include darkTheme { - background-color: #242424; - --code-editor-bg: #242424; + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.2), + 0px 0px 4px 0px rgba(0, 0, 0, 0.3); } @include breakpoint(mobile) { @@ -195,23 +196,23 @@ .replHistoryItem { position: absolute; - background: #ededed; - --inspectorBg: #ededed; + background: var(--Grey95); + --inspectorBg: var(--Grey95); + border: 1px solid var(--Grey90); box-sizing: border-box; margin: 12px 16px; width: calc(100% - 32px); - padding-left: 16px; border-radius: 8px; &.explain { display: flex; .explainVis { - background: #ededed; + background: var(--Grey95); width: 100%; > div { - background: #ededed; + background: var(--Grey95); } @include darkTheme { @@ -246,28 +247,29 @@ } @include darkTheme { - background: #1c1c1c; - --inspectorBg: #1c1c1c; + background: var(--Grey12); + --inspectorBg: var(--Grey12); + border-color: var(--Grey20); } } .historyDateHeader { position: absolute; - top: -26px; + top: -28px; left: 0; right: 0; display: flex; - font-size: 10px; - line-height: 15px; - font-family: Roboto, sans-serif; - font-weight: 500; - color: #a6a6a6; + font-size: 12px; + line-height: 16px; + font-family: "Roboto Mono Variable", monospace; + font-weight: 450; + color: var(--Grey60); &:before, &:after { content: ""; height: 8px; - border-bottom: 2px solid #e3e3e3; + border-bottom: 1px solid var(--Grey85); flex-grow: 1; margin: 0 8px; } @@ -282,11 +284,47 @@ } } +.historyHeader { + position: relative; + display: flex; + flex-direction: column; + background: var(--header_background); + border-radius: 7px 7px 0 0; + + &:after { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 4px; + pointer-events: none; + background: linear-gradient(rgba(0, 0, 0, 0.04), transparent); + } + + &.noOutput { + border-radius: 7px; + } + + &.noOutput, + &.noShadow { + &:after { + display: none; + } + } +} + +.resultModeHeader { + display: flex; + justify-content: end; + padding-bottom: 8px; +} + .historyQuery { display: flex; user-select: text; line-height: 19px; - width: 100%; + padding-left: 16px; .historyPrompt { white-space: pre; @@ -336,90 +374,45 @@ z-index: 1; } - .showFullQuery { - position: absolute; - bottom: 0px; - width: 100%; - display: flex; - justify-content: center; - padding-bottom: 12px; - padding-top: 82px; - z-index: 1; - background: linear-gradient( - 180deg, - rgba(237, 237, 237, 0) 0%, - #ededed 70.31% - ); - - @include darkTheme { - background: linear-gradient( - 180deg, - rgba(28, 28, 28, 0) 0%, - #1c1c1c 70.31% - ); - } - - .showFullQueryBtn { - background: #d9d9d9; - border-radius: 32px; - padding: 4px 10px; - padding: 0; - color: #4c4c4c; - font-size: 11px; - line-height: 16px; - - > div { - padding: 0 16px 0 11px; - } - - svg { - width: 7px; - height: 6px; - fill: #4c4c4c; - } - - @include darkTheme { - background: #595959; - color: #141414; - - svg { - fill: #141414; - } - } - } - } - .historyTime { - font-size: 10px; + font-size: 12px; + font-weight: 450; line-height: 15px; - color: #a6a6a6; + color: var(--Grey60); pointer-events: none; user-select: none; - margin: 8px 8px 0 16px; + margin: 14px 12px 0 16px; } } -.historyQueryExplain { +.explain .historyHeader { width: 50%; - border-right: 4px solid #f7f7f7; - @include breakpoint(mobile) { - width: 100%; + @media (min-width: 768px) { + border-radius: 7px 0 0 7px; + + &:after { + top: 0; + left: 100%; + right: auto; + height: 100%; + width: 4px; + pointer-events: none; + background: linear-gradient(90deg, rgba(0, 0, 0, 0.04), transparent); + } } - @include darkTheme { - border-color: #242424; + @include breakpoint(mobile) { + width: 100%; } } .outputOuterWrapper { - margin: 0 0 0 -16px; flex-grow: 1; - padding-bottom: 12px; } .historyOutput { - padding: 4px 8px 0px 18px; + padding: 12px 8px 12px 18px; min-width: max-content; height: max-content; height: fit-content; @@ -436,23 +429,7 @@ padding-right: 16px; .runInEditorBtn { - color: #4c4c4c; - border-radius: 24px; - background: #d9d9d9; - font-size: 11px; - line-height: 16px; - padding: 1px 10px; - margin-right: 6px; - margin-top: 12px; - - > div { - padding: 0; - } - - @include darkTheme { - background: #595959; - color: #141414; - } + font-family: "Roboto Flex Variable", sans-serif; } @include breakpoint(mobile) { @@ -493,9 +470,10 @@ } .inspector { + display: grid; min-width: max-content; - --inspectorRowHoverBg: #f5f5f5; + --inspectorRowHoverBg: var(--Grey90); --inspectorRowSelectedBg: #d6e2e9; @include darkTheme { @@ -564,7 +542,11 @@ td, th, tbody { - border: 2px solid #7d7d7d; + border: 1px solid var(--Grey70); + + @include darkTheme { + border-color: var(--Grey30); + } } td, @@ -648,43 +630,62 @@ } } -.showMore { +.showMore, +.showFullQuery { position: absolute; bottom: 0; width: 100%; text-align: center; - padding-bottom: 24px; - background: linear-gradient( - 0deg, - var(--inspectorBg), - var(--inspectorBg) 12px, - transparent - ); + padding: 8px 0 16px 0; button { - font-family: Roboto, "sans-serif"; + font-family: "Roboto Flex Variable", sans-serif; text-transform: uppercase; color: var(--app-text-colour); font-size: 12px; - font-weight: 500; - background: #f8f8f8; - border-radius: 14px; - padding: 4px 10px; - opacity: 0.8; + font-weight: 550; + background: #fff; + border-radius: 16px; + padding: 6px 12px; border: none; cursor: pointer; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.06); :global(.dark-theme) & { background: #272727; } } +} +.showMore { + border-radius: 0 0 7px 7px; + background: linear-gradient( + 0deg, + var(--inspectorBg), + var(--inspectorBg) 12px, + transparent + ); - @include breakpoint(mobile) { - text-align: unset; - - button { - position: sticky; - left: calc(50% - 51px); - } + button { + opacity: 0.8; } } +.showFullQuery { + z-index: 1; + background: linear-gradient( + 0deg, + var(--header_background), + var(--header_background) 12px, + transparent + ); +} + +.replResultGridWrapper { + overflow: clip; + border-radius: 0 0 7px 7px; + border-top: 1px solid var(--panel_border); +} + +.replResultGrid { + position: sticky; + top: 0; +} diff --git a/shared/studio/tabs/repl/state/index.ts b/shared/studio/tabs/repl/state/index.ts index cc3456a2..29a757a8 100644 --- a/shared/studio/tabs/repl/state/index.ts +++ b/shared/studio/tabs/repl/state/index.ts @@ -22,7 +22,6 @@ import { extractErrorDetails, } from "../../../utils/extractErrorDetails"; import {InspectorState, Item} from "@edgedb/inspector/state"; -import {ObservableLRU} from "../../../state/utils/lru"; import {decode, EdgeDBSet} from "../../../utils/decodeRawBuffer"; import {CommandResult, handleSlashCommand} from "./commands"; import { @@ -43,6 +42,13 @@ import { ExplainStateType, } from "../../../components/explainVis/state"; import {NavigateFunction} from "../../../hooks/dbRoute"; +import { + createResultGridState, + ResultGridState, +} from "@edgedb/common/components/resultGrid"; +import LRU from "edgedb/dist/primitives/lru"; +import {Completer} from "../../../utils/completer"; +import {OutputMode} from "../../queryEditor/state"; export const defaultItemHeight = 85; @@ -72,6 +78,14 @@ export class ReplHistoryItem extends Model({ renderHeight: number | null = null; showDateHeader: boolean = false; + @computed + get isExplain() { + return ( + this.status === ExplainStateType.explain || + this.status === ExplainStateType.analyzeQuery + ); + } + @observable showMore = false; @@ -88,6 +102,20 @@ export class ReplHistoryItem extends Model({ this.showFullQuery = val; } + @observable + outputMode: OutputMode = OutputMode.Tree; + + @action + setOutputMode(mode: OutputMode) { + this.outputMode = mode; + } + + @action + toggleOutputMode() { + this.outputMode = + this.outputMode === OutputMode.Tree ? OutputMode.Grid : OutputMode.Tree; + } + @modelAction setResult(status: string, hasResult: boolean, implicitLimit: number | null) { this.hasResult = hasResult; @@ -105,72 +133,50 @@ export class ReplHistoryItem extends Model({ this.commandResult = frozen(result); } - _inspector: InspectorState | null = null; + @observable.ref + _inspectorState: InspectorState | null = null; - get inspectorState() { - if (!this.hasResult) { - return null; - } - if (this._inspector) { - return this._inspector; - } - const cache = inspectorCacheCtx.get(this)!; - const state = cache.get(this.$modelId); + @action + getInspectorState(data: EdgeDBSet) { + const cache = cachesCtx.get(this)!.inspector; + let state = cache.get(this.$modelId); if (!state) { - fetchResultData(this.$modelId).then((resultData) => { - if (resultData) { - const inspector = createInspector( - decode( - resultData.outCodecBuf, - resultData.resultBuf, - resultData.protoVer ?? [1, 0] - )!, - this.implicitLimit, - (item) => - findParent( - this, - (p) => p instanceof Repl - )!.setExtendedViewerItem(item) - ); - cache.set(this.$modelId, inspector); - } - }); - } else { - this._inspector = state; + state = createInspector(data, this.implicitLimit, (item) => + findParent( + this, + (p) => p instanceof Repl + )!.setExtendedViewerItem(item) + ); + cache.set(this.$modelId, state); } - return state ?? null; + this._inspectorState = state; + return state; } - _explain: ExplainState | null = null; - - get explainState() { - if (!this.hasResult) { - return null; - } - - if (this._explain) { - return this._explain; + @action + getResultGridState(data: EdgeDBSet) { + const cache = cachesCtx.get(this)!.grid; + let state = cache.get(this.$modelId); + if (!state) { + state = createResultGridState(data._codec, data); + cache.set(this.$modelId, state); } + return state; + } - const explainCache = replExplainCacheCtx.get(this)!; - const state = explainCache.get(this.$modelId); + @observable.ref + _explainState: ExplainState | null = null; + @action + getExplainState(data: EdgeDBSet) { + const cache = cachesCtx.get(this)!.explain; + let state = cache.get(this.$modelId); if (!state) { - fetchResultData(this.$modelId).then((resultData) => { - if (resultData) { - const explainState = createExplainState( - decode( - resultData.outCodecBuf, - resultData.resultBuf, - resultData.protoVer ?? [1, 0] - )![0] - ); - explainCache.set(this.$modelId, explainState); - } - }); + state = createExplainState(data[0]); + cache.set(this.$modelId, state); } - - return state ?? null; + this._explainState = state; + return state; } } @@ -178,19 +184,22 @@ export interface ReplSettings { retroMode: boolean; } -const inspectorCacheCtx = - createMobxContext>(); - -const replExplainCacheCtx = - createMobxContext>(); +const cachesCtx = createMobxContext<{ + inspector: LRU; + grid: LRU; + explain: LRU; +}>(); @model("Repl") export class Repl extends Model({ queryHistory: prop(() => []), }) { onInit() { - inspectorCacheCtx.set(this, this.resultInspectorCache); - replExplainCacheCtx.set(this, this.resultExplainCache); + cachesCtx.set(this, { + inspector: this.resultInspectorCache, + grid: this.resultGridCache, + explain: this.resultExplainCache, + }); } navigation: NavigateFunction | null = null; @@ -274,8 +283,32 @@ export class Repl extends Model({ this.extendedViewerItem = item; } - resultInspectorCache = new ObservableLRU(20); - resultExplainCache = new ObservableLRU(20); + resultDataCache = new LRU>({ + capacity: 20, + }); + + resultInspectorCache = new LRU({capacity: 20}); + resultExplainCache = new LRU({capacity: 20}); + resultGridCache = new LRU({capacity: 20}); + + getResultData(itemId: string): Completer { + let data = this.resultDataCache.get(itemId); + if (!data) { + data = new Completer( + fetchResultData(itemId).then((resultData) => + resultData + ? decode( + resultData.outCodecBuf, + resultData.resultBuf, + resultData.protoVer ?? [1, 0] + ) + : null + ) + ); + this.resultDataCache.set(itemId, data); + } + return data; + } @observable _hasUnfetchedHistory = true; @@ -437,6 +470,10 @@ export class Repl extends Model({ implicitLimitConfig != null ? Number(implicitLimitConfig) : null; historyItem.setResult(status, !!result, implicitLimit); if (result) { + this.resultDataCache.set( + historyItem.$modelId, + new Completer(result) + ); if ( status === ExplainStateType.explain || status === ExplainStateType.analyzeQuery diff --git a/shared/studio/tabs/schema/index.tsx b/shared/studio/tabs/schema/index.tsx index 8e548501..3f5ee8d4 100644 --- a/shared/studio/tabs/schema/index.tsx +++ b/shared/studio/tabs/schema/index.tsx @@ -1,4 +1,4 @@ -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import cn from "@edgedb/common/utils/classNames"; diff --git a/shared/studio/utils/completer.ts b/shared/studio/utils/completer.ts new file mode 100644 index 00000000..b035bfc4 --- /dev/null +++ b/shared/studio/utils/completer.ts @@ -0,0 +1,25 @@ +export class Completer { + public result: T | undefined; + private _promise: Promise | null = null; + + constructor(data: T | Promise) { + if (data instanceof Promise) { + this._promise = data; + } else { + this.result = data; + } + } + + get completed() { + return this.result !== undefined; + } + + get promise(): Promise { + return (async () => { + if (this._promise) { + this.result = await this._promise; + } + return this.result!; + })(); + } +} diff --git a/web/package.json b/web/package.json index 363c7799..4a2793fb 100644 --- a/web/package.json +++ b/web/package.json @@ -23,7 +23,7 @@ "hash.js": "^1.1.7", "mobx": "^6.5.0", "mobx-keystone": "^1.11.0", - "mobx-react-lite": "^3.3.0", + "mobx-react-lite": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.3.0", diff --git a/web/src/app.module.scss b/web/src/app.module.scss index 1f1570e8..4ca52977 100644 --- a/web/src/app.module.scss +++ b/web/src/app.module.scss @@ -24,7 +24,7 @@ --app-card-bg: #e6e6e6; --app-card-outline: #c3c3c3; --app-text-colour: #4d4d4d; - --app-accent-green: #0ccb93; + --app-accent-green: #13b686; --app-modal-overlay: rgba(0, 0, 0, 0.2); --app-modal-bg: #fff; --app-err-color: #cd9089; diff --git a/web/src/app.tsx b/web/src/app.tsx index 211c2843..0cb58f70 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -1,4 +1,4 @@ -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {BrowserRouter, Route, Routes} from "react-router-dom"; import "./fonts/include.scss"; diff --git a/web/src/components/databasePage/index.tsx b/web/src/components/databasePage/index.tsx index e87a2b2e..b1832a3b 100644 --- a/web/src/components/databasePage/index.tsx +++ b/web/src/components/databasePage/index.tsx @@ -1,5 +1,5 @@ import {PropsWithChildren, useState} from "react"; -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {useLocation, useNavigate, useParams, Link} from "react-router-dom"; import { diff --git a/web/src/components/header/index.tsx b/web/src/components/header/index.tsx index 2b4a853f..1cdee105 100644 --- a/web/src/components/header/index.tsx +++ b/web/src/components/header/index.tsx @@ -1,4 +1,4 @@ -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {HeaderTabs} from "@edgedb/studio/components/headerNav"; import {ThemeSwitcher} from "@edgedb/common/ui/themeSwitcher"; import cn from "@edgedb/common/utils/classNames"; diff --git a/web/src/components/instancePage/index.tsx b/web/src/components/instancePage/index.tsx index f7f2815f..1ff10aa5 100644 --- a/web/src/components/instancePage/index.tsx +++ b/web/src/components/instancePage/index.tsx @@ -1,4 +1,4 @@ -import {observer} from "mobx-react"; +import {observer} from "mobx-react-lite"; import {Link} from "react-router-dom"; import {BranchGraph} from "@edgedb/common/branchGraph"; diff --git a/yarn.lock b/yarn.lock index 638c042c..5fd2f7ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1033,8 +1033,8 @@ __metadata: "@types/react-transition-group": ^4.4.0 fastpriorityqueue: ^0.6.3 fuse.js: ^6.4.1 - mobx: ^6.3.0 - mobx-react: ^7.1.0 + mobx: ^6.5.0 + mobx-react-lite: ^4.0.0 mobx-state-tree: ^5.0.1 react-transition-group: ^4.4.1 webcola: ^3.4.0 @@ -1066,7 +1066,7 @@ __metadata: idb: ^7.0.1 mobx: ^6.5.0 mobx-keystone: ^1.11.0 - mobx-react-lite: ^3.3.0 + mobx-react-lite: ^4.0.0 react-colorful: ^5.6.1 react-error-boundary: ^3.1.4 react-hook-form: ^7.32.2 @@ -6599,21 +6599,6 @@ __metadata: languageName: node linkType: hard -"mobx-react-lite@npm:^3.3.0": - version: 3.3.0 - resolution: "mobx-react-lite@npm:3.3.0" - peerDependencies: - mobx: ^6.1.0 - react: ^16.8.0 || ^17 - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - checksum: 0f55bd2009a9cedc6b81d70b88b57dc4161362a16ba6ae0af341e673ca1c627bc3c4088c0cb13133e57e6fa6748b09b4c26aff7fab26c60ed95d27e939846fa3 - languageName: node - linkType: hard - "mobx-react-lite@npm:^4.0.0": version: 4.0.7 resolution: "mobx-react-lite@npm:4.0.7" @@ -6631,23 +6616,6 @@ __metadata: languageName: node linkType: hard -"mobx-react@npm:^7.1.0": - version: 7.3.0 - resolution: "mobx-react@npm:7.3.0" - dependencies: - mobx-react-lite: ^3.3.0 - peerDependencies: - mobx: ^6.1.0 - react: ^16.8.0 || ^17 - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - checksum: 4a53fd45994bfc6d6bcf24072a651f09c15b7afa0f1d0040b92058e6aa55ef0c3a5176986e616a7e791ea0c24561f8c75a964cb08cfe3a1025c550b8a890799a - languageName: node - linkType: hard - "mobx-state-tree@npm:^5.0.1": version: 5.1.3 resolution: "mobx-state-tree@npm:5.1.3" @@ -6657,7 +6625,7 @@ __metadata: languageName: node linkType: hard -"mobx@npm:^6.3.0, mobx@npm:^6.5.0": +"mobx@npm:^6.5.0": version: 6.5.0 resolution: "mobx@npm:6.5.0" checksum: 1210fb0b1c515b5f0ec2916296c32ca19b733e03b34f180af382d44b90668a15b4143c69bb06ca8785ebc3da3e761c6c60d0e72c945c199efc823088af1941ab @@ -8482,7 +8450,7 @@ __metadata: jest: ^29.7.0 mobx: ^6.5.0 mobx-keystone: ^1.11.0 - mobx-react-lite: ^3.3.0 + mobx-react-lite: ^4.0.0 react: ^18.3.1 react-dom: ^18.3.1 react-router-dom: ^6.3.0