Web console: show formatted JSON value (#16632)

* show formatted json value

* update snapshot

* window functions

* count star can also have a window

* better edit query context
This commit is contained in:
Vadim Ogievetsky 2024-06-21 18:33:15 -07:00 committed by GitHub
parent 4eced9b3c9
commit 51c73b5a4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 282 additions and 157 deletions

View File

@ -5094,7 +5094,7 @@ license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Imply Data
version: 0.22.15
version: 0.22.20
---

View File

@ -47,6 +47,7 @@ exports.SQL_KEYWORDS = [
'FULL',
'CROSS',
'USING',
'NATURAL',
'FETCH',
'FIRST',
'NEXT',
@ -67,6 +68,8 @@ exports.SQL_KEYWORDS = [
'RANGE',
'PRECEDING',
'FOLLOWING',
'CURRENT',
'UNBOUNDED',
'EXTEND',
'PIVOT',
'UNPIVOT',

View File

@ -15,7 +15,7 @@
"@blueprintjs/icons": "^4.16.0",
"@blueprintjs/popover2": "^1.14.9",
"@blueprintjs/select": "^4.9.24",
"@druid-toolkit/query": "^0.22.15",
"@druid-toolkit/query": "^0.22.20",
"@druid-toolkit/visuals-core": "^0.3.3",
"@druid-toolkit/visuals-react": "^0.3.3",
"ace-builds": "~1.4.14",
@ -1005,9 +1005,9 @@
}
},
"node_modules/@druid-toolkit/query": {
"version": "0.22.15",
"resolved": "https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.15.tgz",
"integrity": "sha512-LyQVIVkVNhduscf2wnBO/oGBvj353tS5ElIws20xQzApvEIwNNxmlkA+8npqwy77BkJj3nRQvlenbSEDHQdqow==",
"version": "0.22.20",
"resolved": "https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.20.tgz",
"integrity": "sha512-GmmSd27y7zLVTjgTBQy+XoGeSSGhSDNmwyiwWtSua7I5LX8XqHV7Chi8HIH25YQoVgTK1pLK4RS8eRXxthRAzg==",
"dependencies": {
"tslib": "^2.5.2"
}
@ -19147,9 +19147,9 @@
"dev": true
},
"@druid-toolkit/query": {
"version": "0.22.15",
"resolved": "https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.15.tgz",
"integrity": "sha512-LyQVIVkVNhduscf2wnBO/oGBvj353tS5ElIws20xQzApvEIwNNxmlkA+8npqwy77BkJj3nRQvlenbSEDHQdqow==",
"version": "0.22.20",
"resolved": "https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.20.tgz",
"integrity": "sha512-GmmSd27y7zLVTjgTBQy+XoGeSSGhSDNmwyiwWtSua7I5LX8XqHV7Chi8HIH25YQoVgTK1pLK4RS8eRXxthRAzg==",
"requires": {
"tslib": "^2.5.2"
}

View File

@ -69,7 +69,7 @@
"@blueprintjs/icons": "^4.16.0",
"@blueprintjs/popover2": "^1.14.9",
"@blueprintjs/select": "^4.9.24",
"@druid-toolkit/query": "^0.22.15",
"@druid-toolkit/query": "^0.22.20",
"@druid-toolkit/visuals-core": "^0.3.3",
"@druid-toolkit/visuals-react": "^0.3.3",
"ace-builds": "~1.4.14",

View File

@ -174,7 +174,9 @@ export const RecordTablePane = React.memo(function RecordTablePane(props: Record
})}
/>
)}
{showValue && <ShowValueDialog onClose={() => setShowValue(undefined)} str={showValue} />}
{showValue && (
<ShowValueDialog onClose={() => setShowValue(undefined)} str={showValue} size="large" />
)}
</div>
);
});

View File

@ -57,18 +57,102 @@ exports[`EditContextDialog matches snapshot 1`] = `
</span>
</button>
</div>
<textarea
class="bp4-input"
>
{
}
</textarea>
<div
class="bp4-dialog-footer-actions"
class=" ace_editor ace_hidpi ace-solarized-dark ace_dark query-string"
id="ace-editor"
style="width: 100%; height: 100%; font-size: 12px;"
>
<textarea
autocapitalize="off"
autocorrect="off"
class="ace_text-input"
spellcheck="false"
style="opacity: 0; font-size: 1px;"
wrap="off"
/>
<div
aria-hidden="true"
class="ace_gutter"
>
<div
class="ace_layer ace_gutter-layer ace_folding-enabled"
style="height: 1000000px;"
/>
</div>
<div
class="ace_scroller"
style="line-height: 0px;"
>
<div
class="ace_content"
>
<div
class="ace_layer ace_print-margin-layer"
>
<div
class="ace_print-margin"
style="left: 4px; visibility: hidden;"
/>
</div>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_text-layer"
style="height: 1000000px; margin: 0px 4px;"
/>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_cursor-layer ace_hidden-cursors"
>
<div
class="ace_cursor"
/>
</div>
</div>
</div>
<div
class="ace_scrollbar ace_scrollbar-v"
style="display: none; width: 20px;"
>
<div
class="ace_scrollbar-inner"
style="width: 20px;"
>
 
</div>
</div>
<div
class="ace_scrollbar ace_scrollbar-h"
style="display: none; height: 20px;"
>
<div
class="ace_scrollbar-inner"
style="height: 20px;"
>
 
</div>
</div>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
/>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
>
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
</div>
</div>
</div>
<div
class="bp4-dialog-footer"
>
<div
class="edit-context-dialog-buttons"
class="bp4-dialog-footer-actions"
>
<button
class="bp4-button"

View File

@ -20,25 +20,6 @@
.edit-context-dialog {
&.#{$bp-ns}-dialog {
padding-bottom: 10px;
}
.#{$bp-ns}-input {
margin: 10px;
height: 400px;
}
.#{$bp-ns}-dialog-footer-actions {
padding: 0px 10px 0px 10px;
display: grid;
grid-template-columns: 340px 1fr;
grid-template-areas: 'error buttons';
}
.edit-context-dialog-error {
grid-area: error;
}
.edit-context-dialog-buttons {
grid-area: buttons;
height: 60vh;
}
}

View File

@ -24,7 +24,11 @@ import { EditContextDialog } from './edit-context-dialog';
describe('EditContextDialog', () => {
it('matches snapshot', () => {
const compactionDialog = (
<EditContextDialog queryContext={{}} onQueryContextChange={() => null} onClose={() => {}} />
<EditContextDialog
initQueryContext={{}}
onQueryContextChange={() => null}
onClose={() => {}}
/>
);
render(compactionDialog);
expect(document.body.lastChild).toMatchSnapshot();

View File

@ -16,86 +16,72 @@
* limitations under the License.
*/
import { Button, Callout, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
import { Button, Classes, Dialog, Intent } from '@blueprintjs/core';
import Hjson from 'hjson';
import * as JSONBig from 'json-bigint-native';
import React, { useState } from 'react';
import AceEditor from 'react-ace';
import type { QueryContext } from '../../druid-models';
import { AppToaster } from '../../singletons';
import './edit-context-dialog.scss';
export interface EditContextDialogProps {
queryContext: QueryContext;
onQueryContextChange: (queryContext: QueryContext) => void;
onClose: () => void;
function formatContext(context: QueryContext | undefined): string {
const str = JSONBig.stringify(context || {}, undefined, 2);
return str === '{}' ? '{\n\n}' : str;
}
export interface EditContextDialogState {
queryContextString: string;
queryContext?: QueryContext;
error?: string;
export interface EditContextDialogProps {
initQueryContext: QueryContext | undefined;
onQueryContextChange(queryContext: QueryContext): void;
onClose(): void;
}
export const EditContextDialog = React.memo(function EditContextDialog(
props: EditContextDialogProps,
) {
const { onQueryContextChange, onClose } = props;
const [state, setState] = useState<EditContextDialogState>(() => ({
queryContext: props.queryContext,
queryContextString: Object.keys(props.queryContext).length
? JSONBig.stringify(props.queryContext, undefined, 2)
: '{\n\n}',
}));
const { queryContext, queryContextString, error } = state;
function handleTextChange(e: any) {
const queryContextString = (e.target as HTMLInputElement).value;
let error: string | undefined;
let queryContext: QueryContext | undefined;
try {
queryContext = Hjson.parse(queryContextString);
} catch (e) {
error = e.message;
}
if (!error && (!queryContext || typeof queryContext !== 'object')) {
error = 'Input is not a valid object';
queryContext = undefined;
}
setState({
queryContextString,
queryContext,
error,
});
}
const { initQueryContext, onQueryContextChange, onClose } = props;
const [queryContextString, setQueryContextString] = useState<string>(
formatContext(initQueryContext),
);
return (
<Dialog className="edit-context-dialog" isOpen onClose={onClose} title="Edit query context">
<TextArea value={queryContextString} onChange={handleTextChange} autoFocus />
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
{error && (
<Callout intent={Intent.DANGER} className="edit-context-dialog-error">
{error}
</Callout>
)}
<div className="edit-context-dialog-buttons">
<AceEditor
mode="hjson"
theme="solarized_dark"
className="query-string"
name="ace-editor"
fontSize={12}
width="100%"
height="100%"
showGutter
showPrintMargin={false}
value={queryContextString}
onChange={v => setQueryContextString(v)}
/>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button text="Close" onClick={onClose} />
<Button
text="Save"
intent={Intent.PRIMARY}
disabled={Boolean(error)}
onClick={
queryContext
? () => {
onQueryContextChange(queryContext);
onClose();
}
: undefined
}
onClick={() => {
let queryContext: QueryContext;
try {
queryContext = Hjson.parse(queryContextString);
} catch (e) {
AppToaster.show({
message: e.message,
intent: Intent.DANGER,
});
return;
}
onQueryContextChange(queryContext);
onClose();
}}
/>
</div>
</div>

View File

@ -57,53 +57,61 @@ exports[`ShowValueDialog matches snapshot 1`] = `
</span>
</button>
</div>
<textarea
class="bp4-input"
spellcheck="false"
>
Bot: Automatska zamjena teksta (-[[Administrativna podjela Meksika|Admin]] +[[Administrativna podjela Meksika|Admi]])
</textarea>
<div
class="bp4-dialog-footer-actions"
class="bp4-dialog-body"
>
<button
class="bp4-button"
type="button"
<textarea
class="bp4-input"
spellcheck="false"
>
<span
aria-hidden="true"
class="bp4-icon bp4-icon-duplicate"
icon="duplicate"
Bot: Automatska zamjena teksta (-[[Administrativna podjela Meksika|Admin]] +[[Administrativna podjela Meksika|Admi]])
</textarea>
</div>
<div
class="bp4-dialog-footer"
>
<div
class="bp4-dialog-footer-actions"
>
<button
class="bp4-button"
type="button"
>
<svg
data-icon="duplicate"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
<span
aria-hidden="true"
class="bp4-icon bp4-icon-duplicate"
icon="duplicate"
>
<path
d="M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp4-button-text"
<svg
data-icon="duplicate"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
>
<path
d="M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp4-button-text"
>
Copy
</span>
</button>
<button
class="bp4-button bp4-intent-primary"
type="button"
>
Copy
</span>
</button>
<button
class="bp4-button bp4-intent-primary"
type="button"
>
<span
class="bp4-button-text"
>
Close
</span>
</button>
<span
class="bp4-button-text"
>
Close
</span>
</button>
</div>
</div>
</div>
</div>

View File

@ -19,10 +19,6 @@
@import '../../variables';
.show-value-dialog {
&.#{$bp-ns}-dialog {
padding-bottom: 10px;
}
&.normal.#{$bp-ns}-dialog {
height: 600px;
}
@ -32,12 +28,21 @@
height: 90vh;
}
.#{$bp-ns}-input {
margin: 10px;
flex: 1;
.#{$bp-ns}-dialog-body {
display: flex;
flex-direction: column;
.ace-editor {
flex: 1;
}
.#{$bp-ns}-input {
flex: 1;
resize: none;
}
}
.#{$bp-ns}-dialog-footer-actions {
padding-right: 10px;
.#{$bp-ns}-dialog-footer {
margin-top: 0;
}
}

View File

@ -16,11 +16,21 @@
* limitations under the License.
*/
import { Button, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
import {
Button,
ButtonGroup,
Classes,
Dialog,
FormGroup,
Intent,
TextArea,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import copy from 'copy-to-clipboard';
import React from 'react';
import * as JSONBig from 'json-bigint-native';
import React, { useMemo, useState } from 'react';
import AceEditor from 'react-ace';
import { AppToaster } from '../../singletons';
@ -35,6 +45,15 @@ export interface ShowValueDialogProps {
export const ShowValueDialog = React.memo(function ShowValueDialog(props: ShowValueDialogProps) {
const { title, onClose, str, size } = props;
const [tab, setTab] = useState<'formatted' | 'raw'>('formatted');
const parsed = useMemo(() => {
try {
return JSONBig.parse(str);
} catch {}
}, [str]);
const hasParsed = typeof parsed !== 'undefined';
function handleCopy() {
copy(str, { format: 'text/plain' });
@ -51,10 +70,41 @@ export const ShowValueDialog = React.memo(function ShowValueDialog(props: ShowVa
onClose={onClose}
title={title || 'Full value'}
>
<TextArea value={str} spellCheck={false} />
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button icon={IconNames.DUPLICATE} text="Copy" onClick={handleCopy} />
<Button text="Close" intent={Intent.PRIMARY} onClick={onClose} />
<div className={Classes.DIALOG_BODY}>
{hasParsed && (
<FormGroup>
<ButtonGroup fill>
<Button
text="Formatted"
active={tab === 'formatted'}
onClick={() => setTab('formatted')}
/>
<Button text="Raw" active={tab === 'raw'} onClick={() => setTab('raw')} />
</ButtonGroup>
</FormGroup>
)}
{hasParsed && tab === 'formatted' && (
<AceEditor
mode="hjson"
theme="solarized_dark"
className="query-string"
name="ace-editor"
fontSize={12}
width="100%"
height="100%"
showGutter
showPrintMargin={false}
value={JSONBig.stringify(parsed, undefined, 2)}
readOnly
/>
)}
{(!hasParsed || tab === 'raw') && <TextArea value={str} spellCheck={false} />}
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button icon={IconNames.DUPLICATE} text="Copy" onClick={handleCopy} />
<Button text="Close" intent={Intent.PRIMARY} onClick={onClose} />
</div>
</div>
</Dialog>
);

View File

@ -136,7 +136,7 @@ export const StringMenuItems = React.memo(function StringMenuItems(props: String
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
{aggregateMenuItem(F.countDistinct(column), `dist_${columnName}`)}
{aggregateMenuItem(
F.count().addWhereExpression(column.equal(SqlPlaceholder.PLACEHOLDER)),
F.count().addWhere(column.equal(SqlPlaceholder.PLACEHOLDER)),
`filtered_dist_${columnName}`,
false,
)}

View File

@ -657,7 +657,9 @@ export const ResultTablePane = React.memo(function ResultTablePane(props: Result
})}
/>
)}
{showValue && <ShowValueDialog onClose={() => setShowValue(undefined)} str={showValue} />}
{showValue && (
<ShowValueDialog onClose={() => setShowValue(undefined)} str={showValue} size="large" />
)}
{editingExpression && (
<ExpressionEditorDialog
includeOutputName

View File

@ -535,7 +535,7 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
)}
{editContextDialogOpen && (
<EditContextDialog
queryContext={queryContext}
initQueryContext={queryContext}
onQueryContextChange={changeQueryContext}
onClose={() => {
setEditContextDialogOpen(false);