mirror of https://github.com/apache/druid.git
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:
parent
4eced9b3c9
commit
51c73b5a4e
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -57,18 +57,102 @@ exports[`EditContextDialog matches snapshot 1`] = `
|
|||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
class="bp4-input"
|
||||
<div
|
||||
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"
|
||||
>
|
||||
{
|
||||
|
||||
}
|
||||
</textarea>
|
||||
<div
|
||||
class="bp4-dialog-footer-actions"
|
||||
>
|
||||
<div
|
||||
class="edit-context-dialog-buttons"
|
||||
>
|
||||
<button
|
||||
class="bp4-button"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 />
|
||||
<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}>
|
||||
{error && (
|
||||
<Callout intent={Intent.DANGER} className="edit-context-dialog-error">
|
||||
{error}
|
||||
</Callout>
|
||||
)}
|
||||
<div className="edit-context-dialog-buttons">
|
||||
<Button text="Close" onClick={onClose} />
|
||||
<Button
|
||||
text="Save"
|
||||
intent={Intent.PRIMARY}
|
||||
disabled={Boolean(error)}
|
||||
onClick={
|
||||
queryContext
|
||||
? () => {
|
||||
onClick={() => {
|
||||
let queryContext: QueryContext;
|
||||
try {
|
||||
queryContext = Hjson.parse(queryContextString);
|
||||
} catch (e) {
|
||||
AppToaster.show({
|
||||
message: e.message,
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onQueryContextChange(queryContext);
|
||||
onClose();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -57,12 +57,19 @@ exports[`ShowValueDialog matches snapshot 1`] = `
|
|||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="bp4-dialog-body"
|
||||
>
|
||||
<textarea
|
||||
class="bp4-input"
|
||||
spellcheck="false"
|
||||
>
|
||||
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"
|
||||
>
|
||||
|
@ -107,6 +114,7 @@ exports[`ShowValueDialog matches snapshot 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bp4-overlay-end-focus-trap bp4-overlay-appear bp4-overlay-appear-active"
|
||||
tabindex="0"
|
||||
|
|
|
@ -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;
|
||||
.#{$bp-ns}-dialog-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ace-editor {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.#{$bp-ns}-dialog-footer-actions {
|
||||
padding-right: 10px;
|
||||
.#{$bp-ns}-input {
|
||||
flex: 1;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$bp-ns}-dialog-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,11 +70,42 @@ export const ShowValueDialog = React.memo(function ShowValueDialog(props: ShowVa
|
|||
onClose={onClose}
|
||||
title={title || 'Full value'}
|
||||
>
|
||||
<TextArea value={str} spellCheck={false} />
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
)}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -535,7 +535,7 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
|
|||
)}
|
||||
{editContextDialogOpen && (
|
||||
<EditContextDialog
|
||||
queryContext={queryContext}
|
||||
initQueryContext={queryContext}
|
||||
onQueryContextChange={changeQueryContext}
|
||||
onClose={() => {
|
||||
setEditContextDialogOpen(false);
|
||||
|
|
Loading…
Reference in New Issue