Web console: Better hotkeys and library upgrades (#11365)

* improve hotkeys

* fix test name

* refactor explain dialog

* explain tests

* small fixes

* update to popover2

* use resize sensor 2

* misc cleanup
This commit is contained in:
Vadim Ogievetsky 2021-06-17 18:24:29 -07:00 committed by GitHub
parent 2e98d3c1ad
commit f56a5b9ba2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1356 additions and 1311 deletions

View File

@ -4931,6 +4931,15 @@ version: 3.26.1
---
name: "@blueprintjs/popover2"
license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Palantir Technologies
version: 0.10.1
---
name: "@hypnosphi/create-react-context"
license_category: binary
module: web-console
@ -4941,6 +4950,16 @@ license_file_path: licenses/bin/@hypnosphi-create-react-context.MIT
---
name: "@popperjs/core"
license_category: binary
module: web-console
license_name: MIT License
copyright: Federico Zivolo
version: 2.9.2
license_file_path: licenses/bin/@popperjs-core.MIT
---
name: "@types/dom4"
license_category: binary
module: web-console
@ -5498,6 +5517,16 @@ license_file_path: licenses/bin/react-dom.MIT
---
name: "react-fast-compare"
license_category: binary
module: web-console
license_name: MIT License
copyright: Chris Bolin
version: 3.2.0
license_file_path: licenses/bin/react-fast-compare.MIT
---
name: "react-is"
license_category: binary
module: web-console

View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2019 Federico Zivolo
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2018 Formidable Labs
Copyright (c) 2017 Evgeny Poberezkin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -3077,6 +3077,36 @@
}
}
},
"@blueprintjs/popover2": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@blueprintjs/popover2/-/popover2-0.10.1.tgz",
"integrity": "sha512-5zo4TsoGO79yA/CCsENE7cVuSk3BEMzS99iWDdesyh9I9AU0rkSYcaKbAmpIrWfO9Oe4s8lRPms0OrXF2fs94Q==",
"requires": {
"@blueprintjs/core": "^3.45.0",
"@popperjs/core": "^2.5.4",
"classnames": "^2.2",
"dom4": "^2.1.5",
"react-popper": "^2.2.4",
"resize-observer-polyfill": "^1.5.1",
"tslib": "~1.13.0"
},
"dependencies": {
"react-popper": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz",
"integrity": "sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==",
"requires": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
}
},
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
}
}
},
"@cnakazawa/watch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz",
@ -4294,6 +4324,11 @@
"integrity": "sha512-6RglhutqrGFMO1MNUXp95RBuYIuc8wTnMAV5MUhLmjTOy78ncwOw7RgeQ/HeymkKXRhZd0s2DNrM1rL7unk3MQ==",
"dev": true
},
"@popperjs/core": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz",
"integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q=="
},
"@sheerun/mutationobserver-shim": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz",
@ -18325,6 +18360,11 @@
}
}
},
"react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"react-is": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",

View File

@ -69,6 +69,7 @@
"@blueprintjs/core": "^3.45.0",
"@blueprintjs/datetime": "^3.23.4",
"@blueprintjs/icons": "^3.26.1",
"@blueprintjs/popover2": "^0.10.1",
"axios": "^0.21.1",
"brace": "^0.11.1",
"classnames": "^2.2.6",

View File

@ -16,8 +16,9 @@
* limitations under the License.
*/
import { Popover, Position } from '@blueprintjs/core';
import { Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import React from 'react';
import { BasicAction, basicActionsToMenu } from '../../utils/basic-action';
@ -42,9 +43,9 @@ export const ActionCell = React.memo(function ActionCell(props: ActionCellProps)
<div className="action-cell">
{onDetail && <ActionIcon icon={IconNames.SEARCH_TEMPLATE} onClick={onDetail} />}
{actionsMenu && (
<Popover content={actionsMenu} position={Position.BOTTOM_RIGHT}>
<Popover2 content={actionsMenu} position={Position.BOTTOM_RIGHT}>
<ActionIcon icon={IconNames.WRENCH} />
</Popover>
</Popover2>
)}
</div>
);

View File

@ -24,14 +24,6 @@
.bp3-text-muted {
position: absolute;
right: 0;
.bp3-popover-wrapper {
display: inline;
.bp3-popover-target {
display: inline;
}
}
}
}
}

View File

@ -13,30 +13,26 @@ exports[`form group with info matches snapshot 1`] = `
class="bp3-text-muted"
>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="bp3-icon bp3-icon-info-sign"
icon="info-sign"
>
<span
class="bp3-icon bp3-icon-info-sign"
icon="info-sign"
<svg
data-icon="info-sign"
height="14"
viewBox="0 0 16 16"
width="14"
>
<svg
data-icon="info-sign"
height="14"
viewBox="0 0 16 16"
width="14"
>
<desc>
info-sign
</desc>
<path
d="M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zM7 3h2v2H7V3zm3 10H6v-1h1V7H6V6h3v6h1v1z"
fill-rule="evenodd"
/>
</svg>
</span>
<desc>
info-sign
</desc>
<path
d="M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zM7 3h2v2H7V3zm3 10H6v-1h1V7H6V6h3v6h1v1z"
fill-rule="evenodd"
/>
</svg>
</span>
</span>
</span>

View File

@ -17,13 +17,17 @@
*/
.form-group-with-info {
.bp3-text-muted .bp3-popover2-target {
margin-top: 0;
}
.bp3-form-content {
position: relative;
& > .bp3-popover-wrapper {
& > .bp3-popover2-target {
position: absolute;
right: 0;
top: 7px;
top: 5px;
}
}
}

View File

@ -16,8 +16,9 @@
* limitations under the License.
*/
import { FormGroup, Icon, Popover } from '@blueprintjs/core';
import { FormGroup, Icon } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import React from 'react';
import './form-group-with-info.scss';
@ -35,9 +36,9 @@ export const FormGroupWithInfo = React.memo(function FormGroupWithInfo(
const { label, info, inlineInfo, children } = props;
const popover = (
<Popover content={info} position="left-bottom" boundary="viewport">
<Popover2 content={info} position="left-bottom">
<Icon icon={IconNames.INFO_SIGN} iconSize={14} />
</Popover>
</Popover2>
);
return (

View File

@ -77,8 +77,8 @@ exports[`header bar matches snapshot 1`] = `
}
}
/>
<Blueprint3.Popover
boundary="scrollParent"
<Blueprint3.Popover2
boundary="clippingParents"
captureDismiss={false}
content={
<Blueprint3.Menu>
@ -166,21 +166,20 @@ exports[`header bar matches snapshot 1`] = `
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="bottom-right"
positioningStrategy="absolute"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
<Blueprint3.Button
icon="cog"
minimal={true}
/>
</Blueprint3.Popover>
<Blueprint3.Popover
boundary="scrollParent"
</Blueprint3.Popover2>
<Blueprint3.Popover2
boundary="clippingParents"
captureDismiss={false}
content={
<Blueprint3.Menu>
@ -244,19 +243,18 @@ exports[`header bar matches snapshot 1`] = `
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="bottom-right"
positioningStrategy="absolute"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
<Blueprint3.Button
icon="help"
minimal={true}
/>
</Blueprint3.Popover>
</Blueprint3.Popover2>
</Blueprint3.NavbarGroup>
</Blueprint3.Navbar>
`;

View File

@ -27,10 +27,10 @@ import {
Navbar,
NavbarDivider,
NavbarGroup,
Popover,
Position,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import React, { useState } from 'react';
import {
@ -175,7 +175,7 @@ const RestrictedMode = React.memo(function RestrictedMode(props: RestrictedModeP
}
return (
<Popover
<Popover2
content={
<PopoverText>
<p>The console is running in restricted mode.</p>
@ -192,7 +192,7 @@ const RestrictedMode = React.memo(function RestrictedMode(props: RestrictedModeP
position={Position.BOTTOM_RIGHT}
>
<Button icon={IconNames.WARNING_SIGN} text={label} intent={Intent.WARNING} minimal />
</Popover>
</Popover2>
);
});
@ -367,12 +367,12 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<RestrictedMode capabilities={capabilities} />
<Popover content={configMenu} position={Position.BOTTOM_RIGHT}>
<Popover2 content={configMenu} position={Position.BOTTOM_RIGHT}>
<Button minimal icon={IconNames.COG} />
</Popover>
<Popover content={helpMenu} position={Position.BOTTOM_RIGHT}>
</Popover2>
<Popover2 content={helpMenu} position={Position.BOTTOM_RIGHT}>
<Button minimal icon={IconNames.HELP} />
</Popover>
</Popover2>
</NavbarGroup>
{aboutDialogOpen && <AboutDialog onClose={() => setAboutDialogOpen(false)} />}
{doctorDialogOpen && <DoctorDialog onClose={() => setDoctorDialogOpen(false)} />}

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interval calendar component matches snapshot 1`] = `
exports[`IntervalInput matches snapshot 1`] = `
<div
class="bp3-input-group bp3-intent-primary"
>
@ -16,36 +16,32 @@ exports[`interval calendar component matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button"
type="button"
>
<button
class="bp3-button"
type="button"
<span
class="bp3-icon bp3-icon-calendar"
icon="calendar"
>
<span
class="bp3-icon bp3-icon-calendar"
icon="calendar"
<svg
data-icon="calendar"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="calendar"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
calendar
</desc>
<path
d="M11 3c.6 0 1-.5 1-1V1c0-.6-.4-1-1-1s-1 .4-1 1v1c0 .5.4 1 1 1zm3-2h-1v1c0 1.1-.9 2-2 2s-2-.9-2-2V1H6v1c0 1.1-.9 2-2 2s-2-.9-2-2V1H1c-.6 0-1 .5-1 1v12c0 .6.4 1 1 1h13c.6 0 1-.4 1-1V2c0-.6-.5-1-1-1zM5 13H2v-3h3v3zm0-4H2V6h3v3zm4 4H6v-3h3v3zm0-4H6V6h3v3zm4 4h-3v-3h3v3zm0-4h-3V6h3v3zM4 3c.6 0 1-.5 1-1V1c0-.6-.4-1-1-1S3 .4 3 1v1c0 .5.4 1 1 1z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
calendar
</desc>
<path
d="M11 3c.6 0 1-.5 1-1V1c0-.6-.4-1-1-1s-1 .4-1 1v1c0 .5.4 1 1 1zm3-2h-1v1c0 1.1-.9 2-2 2s-2-.9-2-2V1H6v1c0 1.1-.9 2-2 2s-2-.9-2-2V1H1c-.6 0-1 .5-1 1v12c0 .6.4 1 1 1h13c.6 0 1-.4 1-1V2c0-.6-.5-1-1-1zM5 13H2v-3h3v3zm0-4H2V6h3v3zm4 4H6v-3h3v3zm0-4H6V6h3v3zm4 4h-3v-3h3v3zm0-4h-3V6h3v3zM4 3c.6 0 1-.5 1-1V1c0-.6-.4-1-1-1S3 .4 3 1v1c0 .5.4 1 1 1z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</div>
</span>

View File

@ -1,23 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.calendar {
.bp3-popover-content {
right: 310px;
}
}

View File

@ -22,7 +22,7 @@ import React from 'react';
import { IntervalInput } from './interval-input';
describe('interval calendar component', () => {
describe('IntervalInput', () => {
it('matches snapshot', () => {
const intervalInput = (
<IntervalInput

View File

@ -16,15 +16,14 @@
* limitations under the License.
*/
import { Button, InputGroup, Intent, Popover, Position } from '@blueprintjs/core';
import { Button, InputGroup, Intent, Position } from '@blueprintjs/core';
import { DateRange, DateRangePicker, TimePrecision } from '@blueprintjs/datetime';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import React from 'react';
import { intervalToLocalDateRange, localDateRangeToInterval } from '../../utils';
import './interval-input.scss';
export interface IntervalInputProps {
interval: string;
placeholder: string | undefined;
@ -41,7 +40,7 @@ export const IntervalInput = React.memo(function IntervalInput(props: IntervalIn
placeholder={placeholder}
rightElement={
<div>
<Popover
<Popover2
popoverClassName="calendar"
content={
<DateRangePicker
@ -57,7 +56,7 @@ export const IntervalInput = React.memo(function IntervalInput(props: IntervalIn
position={Position.BOTTOM_RIGHT}
>
<Button rightIcon={IconNames.CALENDAR} />
</Popover>
</Popover2>
</div>
}
onChange={(e: any) => {

View File

@ -2,72 +2,64 @@
exports[`more button matches snapshot (empty) 1`] = `
<span
class="bp3-popover-wrapper more-button"
class="more-button bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button bp3-disabled"
disabled=""
tabindex="-1"
type="button"
>
<button
class="bp3-button bp3-disabled"
disabled=""
tabindex="-1"
type="button"
<span
class="bp3-icon bp3-icon-more"
icon="more"
>
<span
class="bp3-icon bp3-icon-more"
icon="more"
<svg
data-icon="more"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="more"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
more
</desc>
<path
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
more
</desc>
<path
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
`;
exports[`more button matches snapshot (full) 1`] = `
<span
class="bp3-popover-wrapper more-button"
class="more-button bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button"
type="button"
>
<button
class="bp3-button"
type="button"
<span
class="bp3-icon bp3-icon-more"
icon="more"
>
<span
class="bp3-icon bp3-icon-more"
icon="more"
<svg
data-icon="more"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="more"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
more
</desc>
<path
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
more
</desc>
<path
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
`;

View File

@ -16,8 +16,9 @@
* limitations under the License.
*/
import { Button, Menu, Popover, Position } from '@blueprintjs/core';
import { Button, Menu, Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import React, { useState } from 'react';
type OpenState = 'open' | 'alt-open';
@ -39,7 +40,7 @@ export const MoreButton = React.memo(function MoreButton(props: MoreButtonProps)
});
return (
<Popover
<Popover2
className="more-button"
isOpen={Boolean(openState)}
content={
@ -55,6 +56,6 @@ export const MoreButton = React.memo(function MoreButton(props: MoreButtonProps)
}}
>
<Button icon={IconNames.MORE} disabled={!childCount} />
</Popover>
</Popover2>
);
});

View File

@ -16,10 +16,10 @@
* limitations under the License.
*/
import { HTMLInputProps, INumericInputProps, NumericInput } from '@blueprintjs/core';
import { HTMLInputProps, NumericInput, NumericInputProps } from '@blueprintjs/core';
import React, { useState } from 'react';
export type NumericInputWithDefaultProps = HTMLInputProps & INumericInputProps;
export type NumericInputWithDefaultProps = HTMLInputProps & NumericInputProps;
export const NumericInputWithDefault = React.memo(function NumericInputWithDefault(
props: NumericInputWithDefaultProps,

View File

@ -14,36 +14,32 @@ exports[`suggestible input matches snapshot 1`] = `
class="bp3-input-action"
>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button bp3-minimal"
type="button"
>
<button
class="bp3-button bp3-minimal"
type="button"
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</span>
</div>

View File

@ -23,10 +23,10 @@ import {
Intent,
Menu,
MenuItem,
Popover,
Position,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import classNames from 'classnames';
import React, { useRef } from 'react';
@ -82,8 +82,7 @@ export const SuggestibleInput = React.memo(function SuggestibleInput(props: Sugg
}}
rightElement={
suggestions && (
<Popover
boundary="window"
<Popover2
content={
<Menu>
{suggestions.map(suggestion => {
@ -123,7 +122,7 @@ export const SuggestibleInput = React.memo(function SuggestibleInput(props: Sugg
autoFocus={false}
>
<Button icon={IconNames.CARET_DOWN} minimal />
</Popover>
</Popover2>
)
}
{...rest}

View File

@ -2,45 +2,41 @@
exports[`table column matches snapshot 1`] = `
<span
class="bp3-popover-wrapper table-column-selector"
class="table-column-selector bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button"
type="button"
>
<button
class="bp3-button"
type="button"
<span
class="bp3-button-text"
>
Columns
<span
class="bp3-button-text"
class="counter"
>
Columns
<span
class="counter"
>
(2/3)
</span>
(2/3)
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
`;

View File

@ -16,8 +16,9 @@
* limitations under the License.
*/
import { Button, Menu, Popover, Position } from '@blueprintjs/core';
import { Button, Menu, Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import React, { useState } from 'react';
import { MenuCheckbox } from '../menu-checkbox/menu-checkbox';
@ -60,7 +61,7 @@ export const TableColumnSelector = React.memo(function TableColumnSelector(
const counterText = `(${columns.filter(isColumnShown).length}/${columns.length})`;
return (
<Popover
<Popover2
className="table-column-selector"
content={checkboxes}
position={Position.BOTTOM_RIGHT}
@ -73,6 +74,6 @@ export const TableColumnSelector = React.memo(function TableColumnSelector(
<Button rightIcon={IconNames.CARET_DOWN}>
Columns <span className="counter">{counterText}</span>
</Button>
</Popover>
</Popover2>
);
});

View File

@ -7,8 +7,8 @@ exports[`TimedButton matches snapshot 1`] = `
<Blueprint3.Button
onClick={[Function]}
/>
<Blueprint3.Popover
boundary="scrollParent"
<Blueprint3.Popover2
boundary="clippingParents"
captureDismiss={false}
content={
<Blueprint3.Menu>
@ -35,16 +35,15 @@ exports[`TimedButton matches snapshot 1`] = `
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
positioningStrategy="absolute"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
<Blueprint3.Button
rightIcon="caret-down"
/>
</Blueprint3.Popover>
</Blueprint3.Popover2>
</Blueprint3.ButtonGroup>
`;

View File

@ -16,16 +16,9 @@
* limitations under the License.
*/
import {
Button,
ButtonGroup,
IButtonProps,
Menu,
MenuDivider,
MenuItem,
Popover,
} from '@blueprintjs/core';
import { Button, ButtonGroup, ButtonProps, Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import React, { useState } from 'react';
import { useInterval } from '../../hooks';
@ -36,7 +29,7 @@ export interface DelayLabel {
delay: number;
}
export interface TimedButtonProps extends IButtonProps {
export interface TimedButtonProps extends ButtonProps {
delays: DelayLabel[];
onRefresh: (auto: boolean) => void;
localStorageKey?: LocalStorageKeys;
@ -77,7 +70,7 @@ export const TimedButton = React.memo(function TimedButton(props: TimedButtonPro
return (
<ButtonGroup className="timed-button">
<Button {...other} text={text} icon={icon} onClick={() => onRefresh(false)} />
<Popover
<Popover2
content={
<Menu>
<MenuDivider title={label} />
@ -93,7 +86,7 @@ export const TimedButton = React.memo(function TimedButton(props: TimedButtonPro
}
>
<Button {...other} rightIcon={IconNames.CARET_DOWN} />
</Popover>
</Popover2>
</ButtonGroup>
);
});

View File

@ -141,60 +141,56 @@ exports[`Datasource table action dialog matches snapshot 1`] = `
class="footer-actions-left"
>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button"
type="button"
>
<button
class="bp3-button"
type="button"
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
>
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</div>
<div

View File

@ -24,7 +24,6 @@ export * from './doctor-dialog/doctor-dialog';
export * from './history-dialog/history-dialog';
export * from './lookup-edit-dialog/lookup-edit-dialog';
export * from './overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
export * from './query-plan-dialog/query-plan-dialog';
export * from './retention-dialog/retention-dialog';
export * from './snitch-dialog/snitch-dialog';
export * from './spec-dialog/spec-dialog';

View File

@ -141,60 +141,56 @@ exports[`Lookup table action dialog matches snapshot 1`] = `
class="footer-actions-left"
>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button"
type="button"
>
<button
class="bp3-button"
type="button"
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
>
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</div>
<div

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`query plan dialog matches snapshot 1`] = `
exports[`QueryHistoryDialog matches snapshot 1`] = `
<div
class="bp3-portal"
>

View File

@ -21,7 +21,7 @@ import React from 'react';
import { QueryHistoryDialog } from './query-history-dialog';
describe('query plan dialog', () => {
describe('QueryHistoryDialog', () => {
it('matches snapshot', () => {
const queryPlanDialog = (
<QueryHistoryDialog setQueryString={() => null} queryRecords={[]} onClose={() => {}} />

View File

@ -1,86 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`query plan dialog matches snapshot 1`] = `
<div
class="bp3-portal"
>
<div
class="bp3-overlay bp3-overlay-open bp3-overlay-scroll-container"
>
<div
class="bp3-overlay-backdrop bp3-overlay-appear bp3-overlay-appear-active"
tabindex="0"
/>
<div
class="bp3-dialog-container bp3-overlay-content bp3-overlay-appear bp3-overlay-appear-active"
tabindex="0"
>
<div
class="bp3-dialog query-plan-dialog"
>
<div
class="bp3-dialog-header"
>
<h4
class="bp3-heading"
>
Query plan
</h4>
<button
aria-label="Close"
class="bp3-button bp3-minimal bp3-dialog-close-button"
type="button"
>
<span
class="bp3-icon bp3-icon-small-cross"
icon="small-cross"
>
<svg
data-icon="small-cross"
height="20"
viewBox="0 0 20 20"
width="20"
>
<desc>
small-cross
</desc>
<path
d="M11.41 10l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L10 8.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L8.59 10 5.3 13.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71l3.29-3.3 3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L11.41 10z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</div>
<div
class="bp3-dialog-body"
>
<div
class="generic-result"
>
test
</div>
</div>
<div
class="bp3-dialog-footer"
>
<div
class="bp3-dialog-footer-actions"
>
<button
class="bp3-button"
type="button"
>
<span
class="bp3-button-text"
>
Close
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -1,37 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { QueryPlanDialog } from './query-plan-dialog';
describe('query plan dialog', () => {
it('matches snapshot', () => {
const queryPlanDialog = (
<QueryPlanDialog
setQueryString={() => null}
explainResult="test"
explainError={undefined}
onClose={() => {}}
/>
);
render(queryPlanDialog);
expect(document.body.lastChild).toMatchSnapshot();
});
});

View File

@ -1,151 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Button,
Classes,
Dialog,
FormGroup,
InputGroup,
Intent,
TextArea,
} from '@blueprintjs/core';
import * as JSONBig from 'json-bigint-native';
import React from 'react';
import { BasicQueryExplanation, SemiJoinQueryExplanation } from '../../utils';
import './query-plan-dialog.scss';
export interface QueryPlanDialogProps {
explainResult?: BasicQueryExplanation | SemiJoinQueryExplanation | string;
explainError?: Error;
onClose: () => void;
setQueryString: (queryString: string) => void;
}
export const QueryPlanDialog = React.memo(function QueryPlanDialog(props: QueryPlanDialogProps) {
const { explainResult, explainError, onClose, setQueryString } = props;
let content: JSX.Element;
let queryString: string | undefined;
if (explainError) {
content = <div>{explainError.message}</div>;
} else if (!explainResult) {
content = <div />;
} else if ((explainResult as BasicQueryExplanation).query) {
let signature: JSX.Element | undefined;
if ((explainResult as BasicQueryExplanation).signature) {
const signatureContent = (explainResult as BasicQueryExplanation).signature || '';
signature = (
<FormGroup label="Signature">
<InputGroup defaultValue={signatureContent} readOnly />
</FormGroup>
);
}
queryString = JSONBig.stringify(
(explainResult as BasicQueryExplanation).query[0],
undefined,
2,
);
content = (
<div className="one-query">
<FormGroup label="Query">
<TextArea readOnly value={queryString} />
</FormGroup>
{signature}
</div>
);
} else if (
(explainResult as SemiJoinQueryExplanation).mainQuery &&
(explainResult as SemiJoinQueryExplanation).subQueryRight
) {
let mainSignature: JSX.Element | undefined;
let subSignature: JSX.Element | undefined;
if ((explainResult as SemiJoinQueryExplanation).mainQuery.signature) {
const signatureContent =
(explainResult as SemiJoinQueryExplanation).mainQuery.signature || '';
mainSignature = (
<FormGroup label="Signature">
<InputGroup defaultValue={signatureContent} readOnly />
</FormGroup>
);
}
if ((explainResult as SemiJoinQueryExplanation).subQueryRight.signature) {
const signatureContent =
(explainResult as SemiJoinQueryExplanation).subQueryRight.signature || '';
subSignature = (
<FormGroup label="Signature">
<InputGroup defaultValue={signatureContent} readOnly />
</FormGroup>
);
}
content = (
<div className="two-queries">
<FormGroup label="Main query">
<TextArea
readOnly
value={JSONBig.stringify(
(explainResult as SemiJoinQueryExplanation).mainQuery.query,
undefined,
2,
)}
/>
</FormGroup>
{mainSignature}
<FormGroup label="Sub query">
<TextArea
readOnly
value={JSONBig.stringify(
(explainResult as SemiJoinQueryExplanation).subQueryRight.query,
undefined,
2,
)}
/>
</FormGroup>
{subSignature}
</div>
);
} else {
content = <div className="generic-result">{explainResult}</div>;
}
return (
<Dialog className="query-plan-dialog" isOpen onClose={onClose} title="Query plan">
<div className={Classes.DIALOG_BODY}>{content}</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button text="Close" onClick={onClose} />
{queryString && (
<Button
text="Open query"
intent={Intent.PRIMARY}
onClick={() => {
if (queryString) setQueryString(queryString);
onClose();
}}
/>
)}
</div>
</div>
</Dialog>
);
});

View File

@ -245,36 +245,32 @@ exports[`retention dialog matches snapshot 1`] = `
class="bp3-input-action"
>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button bp3-minimal"
type="button"
>
<button
class="bp3-button bp3-minimal"
type="button"
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</span>
</div>

View File

@ -185,60 +185,56 @@ exports[`task table action dialog matches snapshot 1`] = `
class="footer-actions-left"
>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button"
type="button"
>
<button
class="bp3-button"
type="button"
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
>
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</div>
<div

View File

@ -272,60 +272,56 @@ exports[`supervisor table action dialog matches snapshot 1`] = `
class="footer-actions-left"
>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button"
type="button"
>
<button
class="bp3-button"
type="button"
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
>
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</div>
<div

View File

@ -16,8 +16,9 @@
* limitations under the License.
*/
import { Button, Classes, Dialog, Icon, IconName, Intent, Popover } from '@blueprintjs/core';
import { Button, Classes, Dialog, Icon, IconName, Intent } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import React, { ReactNode } from 'react';
import { BasicAction, basicActionsToMenu } from '../../utils/basic-action';
@ -66,9 +67,9 @@ export const TableActionDialog = React.memo(function TableActionDialog(
<div className={Classes.DIALOG_FOOTER}>
{actionsMenu && (
<div className="footer-actions-left">
<Popover content={actionsMenu}>
<Popover2 content={actionsMenu}>
<Button icon={IconNames.WRENCH} text="Actions" rightIcon={IconNames.CARET_DOWN} />
</Popover>
</Popover2>
</div>
)}
<div className={Classes.DIALOG_FOOTER_ACTIONS}>

View File

@ -272,60 +272,56 @@ exports[`task table action dialog matches snapshot 1`] = `
class="footer-actions-left"
>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button"
type="button"
>
<button
class="bp3-button"
type="button"
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
>
<span
class="bp3-icon bp3-icon-wrench"
icon="wrench"
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="wrench"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
<desc>
wrench
</desc>
<path
d="M15.83 3.7l-3.06 3.05-2.84-.7-.7-2.83L12.29.17a5.004 5.004 0 00-4.83 1.29 4.967 4.967 0 00-1.12 5.36L.58 12.58c-.36.36-.58.86-.58 1.41 0 1.1.9 2 2 2 .55 0 1.05-.22 1.41-.59l5.77-5.77c1.79.69 3.91.33 5.35-1.12 1.32-1.3 1.74-3.15 1.3-4.81z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp3-button-text"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
Actions
</span>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</div>
<div

View File

@ -77,6 +77,7 @@ export function getTimestampExpressionFields(transforms: Transform[]): Field<Tra
`timestamp_parse(concat("date", ' ', "time"))`,
`timestamp_parse(concat("date", ' ', "time"), 'M/d/yyyy H:mm:ss')`,
`timestamp_parse(concat("year", '-', "month", '-', "day"))`,
`timestamp_parse("west_coast_time", 'yyyy-MM-dd\\'T\\'HH:mm:ss.SSS', 'America/Los_Angeles')`,
`timestamp_parse("local_time", 'yyyy-MM-dd HH:mm:ss', "timezone")`,
],
info: (

View File

@ -21,6 +21,7 @@
@import './blueprint-overrides';
@import '~@blueprintjs/core/src/blueprint';
@import '~@blueprintjs/datetime/src/blueprint-datetime';
@import '~@blueprintjs/popover2/src/blueprint-popover2';
@import '~react-splitter-layout/lib/index';
@import '../lib/react-table';

View File

@ -18,7 +18,13 @@
import { sane } from 'druid-query-toolkit/build/test-utils';
import { DruidError, getDruidErrorMessage, parseHtmlError, parseQueryPlan } from './druid-query';
import {
DruidError,
getDruidErrorMessage,
parseHtmlError,
parseQueryPlan,
trimSemicolon,
} from './druid-query';
describe('DruidQuery', () => {
describe('DruidError.parsePosition', () => {
@ -226,4 +232,12 @@ describe('DruidQuery', () => {
expect(parseQueryPlan('start')).toMatchInlineSnapshot(`"start"`);
});
});
describe('.trimSemicolon', () => {
it('works', () => {
expect(trimSemicolon('SELECT * FROM tbl;')).toEqual('SELECT * FROM tbl');
expect(trimSemicolon('SELECT * FROM tbl; ')).toEqual('SELECT * FROM tbl ');
expect(trimSemicolon('SELECT * FROM tbl; --hello ')).toEqual('SELECT * FROM tbl --hello ');
});
});
});

View File

@ -21,10 +21,17 @@ import axios, { AxiosResponse } from 'axios';
import { Api } from '../singletons';
import { assemble } from './general';
import { QueryContext } from './query-context';
import { RowColumn } from './query-cursor';
const CANCELED_MESSAGE = 'Query canceled by user.';
export interface QueryWithContext {
queryString: string;
queryContext: QueryContext;
wrapQueryLimit: number | undefined;
}
export interface DruidErrorResponse {
error?: string;
errorMessage?: string;
@ -357,3 +364,8 @@ export function parseQueryPlan(
return parseQueryPlanResult(queryArgs);
}
export function trimSemicolon(query: string): string {
// Trims out a trailing semicolon while preserving space (https://bit.ly/1n1yfkJ)
return query.replace(/;+((?:\s*--[^\n]*)?\s*)$/, '$1');
}

View File

@ -34,11 +34,11 @@ import {
Intent,
Menu,
MenuItem,
Popover,
Switch,
TextArea,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import classNames from 'classnames';
import * as JSONBig from 'json-bigint-native';
import memoize from 'memoize-one';
@ -2696,25 +2696,25 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
/>
{reorderDimensionMenu && (
<FormGroup>
<Popover content={reorderDimensionMenu}>
<Popover2 content={reorderDimensionMenu}>
<Button
icon={IconNames.ARROWS_HORIZONTAL}
text="Reorder dimension"
rightIcon={IconNames.CARET_DOWN}
/>
</Popover>
</Popover2>
</FormGroup>
)}
{selectedDimensionSpecIndex !== -1 && deepGet(spec, 'spec.dataSchema.metricsSpec') && (
<FormGroup>
<Popover content={convertToMetricMenu}>
<Popover2 content={convertToMetricMenu}>
<Button
icon={IconNames.EXCHANGE}
text="Convert to metric"
rightIcon={IconNames.CARET_DOWN}
disabled={dimensions.length <= 1}
/>
</Popover>
</Popover2>
</FormGroup>
)}
<div className="control-buttons">
@ -2806,13 +2806,13 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
/>
{selectedMetricSpecIndex !== -1 && dimensionMode === 'specific' && (
<FormGroup>
<Popover content={convertToDimensionMenu}>
<Popover2 content={convertToDimensionMenu}>
<Button
icon={IconNames.EXCHANGE}
text="Convert to dimension"
rightIcon={IconNames.CARET_DOWN}
/>
</Popover>
</Popover2>
</FormGroup>
)}
<div className="control-buttons">

View File

@ -45,7 +45,7 @@ exports[`QueryView matches snapshot 1`] = `
queryContext={Object {}}
runeMode={false}
/>
<Blueprint3.Tooltip
<Blueprint3.Tooltip2
content="Automatically wrap the query with a limit to protect against queries with very large result sets."
hoverCloseDelay={0}
hoverOpenDelay={800}
@ -58,7 +58,7 @@ exports[`QueryView matches snapshot 1`] = `
label="Auto limit"
onChange={[Function]}
/>
</Blueprint3.Tooltip>
</Blueprint3.Tooltip2>
<Memo(LiveQueryModeSelector)
autoLiveQueryModeShouldRun={true}
liveQueryMode="auto"
@ -124,7 +124,7 @@ exports[`QueryView matches snapshot with query 1`] = `
queryContext={Object {}}
runeMode={false}
/>
<Blueprint3.Tooltip
<Blueprint3.Tooltip2
content="Automatically wrap the query with a limit to protect against queries with very large result sets."
hoverCloseDelay={0}
hoverOpenDelay={800}
@ -137,7 +137,7 @@ exports[`QueryView matches snapshot with query 1`] = `
label="Auto limit"
onChange={[Function]}
/>
</Blueprint3.Tooltip>
</Blueprint3.Tooltip2>
<Memo(LiveQueryModeSelector)
autoLiveQueryModeShouldRun={true}
liveQueryMode="auto"

View File

@ -36,9 +36,9 @@ exports[`ColumnTree matches snapshot 1`] = `
Object {
"icon": "time",
"id": "__time",
"label": <Blueprint3.Popover
"label": <Blueprint3.Popover2
autoFocus={false}
boundary="window"
boundary="clippingParents"
captureDismiss={false}
content={
<Memo(Deferred)
@ -54,24 +54,22 @@ exports[`ColumnTree matches snapshot 1`] = `
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="right"
targetClassName="bp3-popover-open"
positioningStrategy="absolute"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
__time
</Blueprint3.Popover>,
</Blueprint3.Popover2>,
},
Object {
"icon": "numerical",
"id": "added",
"label": <Blueprint3.Popover
"label": <Blueprint3.Popover2
autoFocus={false}
boundary="window"
boundary="clippingParents"
captureDismiss={false}
content={
<Memo(Deferred)
@ -87,24 +85,22 @@ exports[`ColumnTree matches snapshot 1`] = `
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="right"
targetClassName="bp3-popover-open"
positioningStrategy="absolute"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
added
</Blueprint3.Popover>,
</Blueprint3.Popover2>,
},
Object {
"icon": "numerical",
"id": "addedBy10",
"label": <Blueprint3.Popover
"label": <Blueprint3.Popover2
autoFocus={false}
boundary="window"
boundary="clippingParents"
captureDismiss={false}
content={
<Memo(Deferred)
@ -120,24 +116,22 @@ exports[`ColumnTree matches snapshot 1`] = `
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="right"
targetClassName="bp3-popover-open"
positioningStrategy="absolute"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
addedBy10
</Blueprint3.Popover>,
</Blueprint3.Popover2>,
},
],
"icon": "th",
"id": "wikipedia",
"isExpanded": true,
"label": <Blueprint3.Popover
boundary="window"
"label": <Blueprint3.Popover2
boundary="clippingParents"
captureDismiss={false}
content={
<Memo(Deferred)
@ -153,16 +147,15 @@ exports[`ColumnTree matches snapshot 1`] = `
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="right"
positioningStrategy="absolute"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
wikipedia
</Blueprint3.Popover>,
</Blueprint3.Popover2>,
},
]
}

View File

@ -45,14 +45,9 @@
}
}
.bp3-popover-wrapper {
.bp3-popover2-target {
width: 188px;
display: inline-block;
.bp3-popover-target {
width: 100%;
display: inline-block;
cursor: pointer;
}
cursor: pointer;
}
}

View File

@ -16,8 +16,9 @@
* limitations under the License.
*/
import { HTMLSelect, ITreeNode, Menu, MenuItem, Popover, Position, Tree } from '@blueprintjs/core';
import { HTMLSelect, Menu, MenuItem, Position, Tree, TreeNodeInfo } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import {
SqlComparison,
SqlExpression,
@ -115,8 +116,8 @@ export interface ColumnTreeProps {
export interface ColumnTreeState {
prevColumnMetadata?: readonly ColumnMetadata[];
columnTree?: ITreeNode[];
currentSchemaSubtree?: ITreeNode[];
columnTree?: TreeNodeInfo[];
currentSchemaSubtree?: TreeNodeInfo[];
selectedTreeIndex: number;
}
@ -150,18 +151,17 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
const columnTree = groupBy(
columnMetadata,
r => r.TABLE_SCHEMA,
(metadata, schemaName): ITreeNode => ({
(metadata, schemaName): TreeNodeInfo => ({
id: schemaName,
label: schemaName,
childNodes: groupBy(
metadata,
r => r.TABLE_NAME,
(metadata, tableName): ITreeNode => ({
(metadata, tableName): TreeNodeInfo => ({
id: tableName,
icon: IconNames.TH,
label: (
<Popover
boundary="window"
<Popover2
position={Position.RIGHT}
content={
<Deferred
@ -306,19 +306,17 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
}
>
{tableName}
</Popover>
</Popover2>
),
childNodes: metadata
.map(
(columnData): ITreeNode => ({
(columnData): TreeNodeInfo => ({
id: columnData.COLUMN_NAME,
icon: dataTypeToIcon(columnData.DATA_TYPE),
label: (
<Popover
boundary="window"
<Popover2
position={Position.RIGHT}
autoFocus={false}
targetClassName="bp3-popover-open"
content={
<Deferred
content={() => {
@ -384,7 +382,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
}
>
{columnData.COLUMN_NAME}
</Popover>
</Popover2>
),
}),
)
@ -441,7 +439,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
};
}
renderSchemaSelector() {
private renderSchemaSelector() {
const { columnTree, selectedTreeIndex } = this.state;
if (!columnTree) return null;
@ -465,11 +463,10 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
private readonly handleSchemaSelectorChange = (e: ChangeEvent<HTMLSelectElement>): void => {
const { columnTree } = this.state;
if (!columnTree) return;
const selectedTreeIndex = Number(e.target.value);
if (!columnTree) return;
const currentSchemaSubtree =
columnTree[selectedTreeIndex > -1 ? selectedTreeIndex : 0].childNodes;
@ -479,24 +476,16 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
});
};
private readonly handleNodeCollapse = (nodeData: ITreeNode) => {
private readonly handleNodeCollapse = (nodeData: TreeNodeInfo) => {
nodeData.isExpanded = false;
this.bounceState();
this.forceUpdate();
};
private readonly handleNodeExpand = (nodeData: ITreeNode) => {
private readonly handleNodeExpand = (nodeData: TreeNodeInfo) => {
nodeData.isExpanded = true;
this.bounceState();
this.forceUpdate();
};
bounceState() {
const { columnTree } = this.state;
if (!columnTree) return;
this.setState(prevState => ({
columnTree: prevState.columnTree ? prevState.columnTree.slice() : undefined,
}));
}
render(): JSX.Element | null {
const { columnMetadataLoading } = this.props;
const { currentSchemaSubtree } = this.state;

View File

@ -0,0 +1,179 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExplainDialog matches snapshot on error 1`] = `
<Blueprint3.Dialog
canOutsideClickClose={true}
className="explain-dialog"
isOpen={true}
onClose={[Function]}
title="Query plan"
>
<div
className="bp3-dialog-body"
>
<div>
test error
</div>
</div>
<div
className="bp3-dialog-footer"
>
<div
className="bp3-dialog-footer-actions"
>
<Blueprint3.Button
onClick={[Function]}
text="Close"
/>
</div>
</div>
</Blueprint3.Dialog>
`;
exports[`ExplainDialog matches snapshot on init 1`] = `
<Blueprint3.Dialog
canOutsideClickClose={true}
className="explain-dialog"
isOpen={true}
onClose={[Function]}
title="Query plan"
>
<div
className="bp3-dialog-body"
>
<div />
</div>
<div
className="bp3-dialog-footer"
>
<div
className="bp3-dialog-footer-actions"
>
<Blueprint3.Button
onClick={[Function]}
text="Close"
/>
</div>
</div>
</Blueprint3.Dialog>
`;
exports[`ExplainDialog matches snapshot on loading 1`] = `
<Blueprint3.Dialog
canOutsideClickClose={true}
className="explain-dialog"
isOpen={true}
onClose={[Function]}
title="Query plan"
>
<div
className="bp3-dialog-body"
>
<Memo(Loader) />
</div>
<div
className="bp3-dialog-footer"
>
<div
className="bp3-dialog-footer-actions"
>
<Blueprint3.Button
onClick={[Function]}
text="Close"
/>
</div>
</div>
</Blueprint3.Dialog>
`;
exports[`ExplainDialog matches snapshot on some data 1`] = `
<Blueprint3.Dialog
canOutsideClickClose={true}
className="explain-dialog"
isOpen={true}
onClose={[Function]}
title="Query plan"
>
<div
className="bp3-dialog-body"
>
<div
className="one-query"
>
<Blueprint3.FormGroup
label="Query"
>
<Blueprint3.TextArea
readOnly={true}
value="{
\\"queryType\\": \\"topN\\",
\\"dataSource\\": {
\\"type\\": \\"table\\",
\\"name\\": \\"kttm-multi-day\\"
},
\\"virtualColumns\\": [],
\\"dimension\\": {
\\"type\\": \\"default\\",
\\"dimension\\": \\"browser\\",
\\"outputName\\": \\"d0\\",
\\"outputType\\": \\"STRING\\"
},
\\"metric\\": {
\\"type\\": \\"numeric\\",
\\"metric\\": \\"a0\\"
},
\\"threshold\\": 101,
\\"intervals\\": {
\\"type\\": \\"intervals\\",
\\"intervals\\": [
\\"-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z\\"
]
},
\\"filter\\": null,
\\"granularity\\": {
\\"type\\": \\"all\\"
},
\\"aggregations\\": [
{
\\"type\\": \\"count\\",
\\"name\\": \\"a0\\"
}
],
\\"postAggregations\\": [],
\\"context\\": {
\\"sqlOuterLimit\\": 101,
\\"sqlQueryId\\": \\"5905fe8d-9a91-41e0-8f3a-d7a8ac21dce6\\"
},
\\"descending\\": false
}"
/>
</Blueprint3.FormGroup>
<Blueprint3.FormGroup
label="Signature"
>
<Blueprint3.InputGroup
defaultValue="[{d0:STRING, a0:LONG}]"
readOnly={true}
/>
</Blueprint3.FormGroup>
</div>
</div>
<div
className="bp3-dialog-footer"
>
<div
className="bp3-dialog-footer-actions"
>
<Blueprint3.Button
onClick={[Function]}
text="Close"
/>
<Blueprint3.Button
intent="primary"
onClick={[Function]}
text="Open query"
/>
</div>
</div>
</Blueprint3.Dialog>
`;

View File

@ -16,11 +16,15 @@
* limitations under the License.
*/
.query-plan-dialog {
.explain-dialog {
&.bp3-dialog {
width: 600px;
}
.bp3-dialog-body {
min-height: 70vh;
}
textarea {
width: 100%;
}

View File

@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { shallow } from 'enzyme';
import React from 'react';
import {
BasicQueryExplanation,
parseQueryPlan,
QueryState,
SemiJoinQueryExplanation,
} from '../../../utils';
import { ExplainDialog } from './explain-dialog';
let explainState: QueryState<BasicQueryExplanation | SemiJoinQueryExplanation | string> =
QueryState.INIT;
jest.mock('../../../hooks', () => {
return {
useQueryManager: () => [explainState],
};
});
describe('ExplainDialog', () => {
function makeExplainDialog() {
return (
<ExplainDialog
setQueryString={() => {}}
queryWithContext={{ queryString: 'test', queryContext: {}, wrapQueryLimit: undefined }}
onClose={() => {}}
/>
);
}
it('matches snapshot on init', () => {
expect(shallow(makeExplainDialog())).toMatchSnapshot();
});
it('matches snapshot on loading', () => {
explainState = QueryState.LOADING;
expect(shallow(makeExplainDialog())).toMatchSnapshot();
});
it('matches snapshot on error', () => {
explainState = new QueryState({ error: new Error('test error') });
expect(shallow(makeExplainDialog())).toMatchSnapshot();
});
it('matches snapshot on some data', () => {
explainState = new QueryState({
data: parseQueryPlan(
`DruidQueryRel(query=[{"queryType":"topN","dataSource":{"type":"table","name":"kttm-multi-day"},"virtualColumns":[],"dimension":{"type":"default","dimension":"browser","outputName":"d0","outputType":"STRING"},"metric":{"type":"numeric","metric":"a0"},"threshold":101,"intervals":{"type":"intervals","intervals":["-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z"]},"filter":null,"granularity":{"type":"all"},"aggregations":[{"type":"count","name":"a0"}],"postAggregations":[],"context":{"sqlOuterLimit":101,"sqlQueryId":"5905fe8d-9a91-41e0-8f3a-d7a8ac21dce6"},"descending":false}], signature=[{d0:STRING, a0:LONG}])`,
),
});
expect(shallow(makeExplainDialog())).toMatchSnapshot();
});
});

View File

@ -0,0 +1,197 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Button,
Classes,
Dialog,
FormGroup,
InputGroup,
Intent,
TextArea,
} from '@blueprintjs/core';
import * as JSONBig from 'json-bigint-native';
import React from 'react';
import { Loader } from '../../../components';
import { useQueryManager } from '../../../hooks';
import {
BasicQueryExplanation,
getDruidErrorMessage,
parseQueryPlan,
queryDruidSql,
QueryWithContext,
SemiJoinQueryExplanation,
trimSemicolon,
} from '../../../utils';
import { isEmptyContext } from '../../../utils/query-context';
import './explain-dialog.scss';
function isExplainQuery(query: string): boolean {
return /EXPLAIN\sPLAN\sFOR/i.test(query);
}
function wrapInExplainIfNeeded(query: string): string {
query = trimSemicolon(query);
if (isExplainQuery(query)) return query;
return `EXPLAIN PLAN FOR (${query}\n)`;
}
export interface ExplainDialogProps {
queryWithContext: QueryWithContext;
mandatoryQueryContext?: Record<string, any>;
onClose: () => void;
setQueryString: (queryString: string) => void;
}
export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDialogProps) {
const { queryWithContext, onClose, setQueryString, mandatoryQueryContext } = props;
const [explainState] = useQueryManager<
QueryWithContext,
BasicQueryExplanation | SemiJoinQueryExplanation | string
>({
processQuery: async (queryWithContext: QueryWithContext) => {
const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
let context: Record<string, any> | undefined;
if (!isEmptyContext(queryContext) || wrapQueryLimit || mandatoryQueryContext) {
context = { ...queryContext, ...(mandatoryQueryContext || {}) };
if (typeof wrapQueryLimit !== 'undefined') {
context.sqlOuterLimit = wrapQueryLimit + 1;
}
}
let result: any[] | undefined;
try {
result = await queryDruidSql({
query: wrapInExplainIfNeeded(queryString),
context,
});
} catch (e) {
throw new Error(getDruidErrorMessage(e));
}
return parseQueryPlan(result[0]['PLAN']);
},
initQuery: queryWithContext,
});
let content: JSX.Element;
let queryString: string | undefined;
const { loading, error: explainError, data: explainResult } = explainState;
if (loading) {
content = <Loader />;
} else if (explainError) {
content = <div>{explainError.message}</div>;
} else if (!explainResult) {
content = <div />;
} else if ((explainResult as BasicQueryExplanation).query) {
queryString = JSONBig.stringify(
(explainResult as BasicQueryExplanation).query[0],
undefined,
2,
);
content = (
<div className="one-query">
<FormGroup label="Query">
<TextArea readOnly value={queryString} />
</FormGroup>
{(explainResult as BasicQueryExplanation).signature && (
<FormGroup label="Signature">
<InputGroup
defaultValue={(explainResult as BasicQueryExplanation).signature || ''}
readOnly
/>
</FormGroup>
)}
</div>
);
} else if (
(explainResult as SemiJoinQueryExplanation).mainQuery &&
(explainResult as SemiJoinQueryExplanation).subQueryRight
) {
content = (
<div className="two-queries">
<FormGroup label="Main query">
<TextArea
readOnly
value={JSONBig.stringify(
(explainResult as SemiJoinQueryExplanation).mainQuery.query,
undefined,
2,
)}
/>
</FormGroup>
{(explainResult as SemiJoinQueryExplanation).mainQuery.signature && (
<FormGroup label="Signature">
<InputGroup
defaultValue={(explainResult as SemiJoinQueryExplanation).mainQuery.signature || ''}
readOnly
/>
</FormGroup>
)}
<FormGroup label="Sub query">
<TextArea
readOnly
value={JSONBig.stringify(
(explainResult as SemiJoinQueryExplanation).subQueryRight.query,
undefined,
2,
)}
/>
</FormGroup>
{(explainResult as SemiJoinQueryExplanation).subQueryRight.signature && (
<FormGroup label="Signature">
<InputGroup
defaultValue={
(explainResult as SemiJoinQueryExplanation).subQueryRight.signature || ''
}
readOnly
/>
</FormGroup>
)}
</div>
);
} else {
content = <div className="generic-result">{explainResult}</div>;
}
return (
<Dialog className="explain-dialog" isOpen onClose={onClose} title="Query plan">
<div className={Classes.DIALOG_BODY}>{content}</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button text="Close" onClick={onClose} />
{queryString && (
<Button
text="Open query"
intent={Intent.PRIMARY}
onClick={() => {
if (queryString) setQueryString(queryString);
onClose();
}}
/>
)}
</div>
</div>
</Dialog>
);
});

View File

@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LiveQueryModeSelector matches snapshot auto 1`] = `
<Blueprint3.Popover
boundary="scrollParent"
<Blueprint3.Popover2
boundary="clippingParents"
captureDismiss={false}
content={
<Blueprint3.Menu>
@ -47,14 +47,13 @@ exports[`LiveQueryModeSelector matches snapshot auto 1`] = `
inheritDarkTheme={true}
interactionKind="click"
minimal={true}
modifiers={Object {}}
openOnTargetFocus={true}
portalClassName="live-query-mode-selector-portal"
position="bottom-left"
positioningStrategy="absolute"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
<Blueprint3.Button
className="live-query-mode-selector"
@ -70,12 +69,12 @@ exports[`LiveQueryModeSelector matches snapshot auto 1`] = `
Auto
</span>
</Blueprint3.Button>
</Blueprint3.Popover>
</Blueprint3.Popover2>
`;
exports[`LiveQueryModeSelector matches snapshot on 1`] = `
<Blueprint3.Popover
boundary="scrollParent"
<Blueprint3.Popover2
boundary="clippingParents"
captureDismiss={false}
content={
<Blueprint3.Menu>
@ -120,14 +119,13 @@ exports[`LiveQueryModeSelector matches snapshot on 1`] = `
inheritDarkTheme={true}
interactionKind="click"
minimal={true}
modifiers={Object {}}
openOnTargetFocus={true}
portalClassName="live-query-mode-selector-portal"
position="bottom-left"
positioningStrategy="absolute"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
<Blueprint3.Button
className="live-query-mode-selector"
@ -143,5 +141,5 @@ exports[`LiveQueryModeSelector matches snapshot on 1`] = `
On
</span>
</Blueprint3.Button>
</Blueprint3.Popover>
</Blueprint3.Popover2>
`;

View File

@ -16,8 +16,9 @@
* limitations under the License.
*/
import { Button, Menu, MenuItem, Popover, PopoverPosition } from '@blueprintjs/core';
import { Button, Menu, MenuItem, PopoverPosition } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import classNames from 'classnames';
import React from 'react';
@ -43,7 +44,7 @@ export const LiveQueryModeSelector = React.memo(function LiveQueryModeSelector(
const { liveQueryMode, onLiveQueryModeChange, autoLiveQueryModeShouldRun } = props;
return (
<Popover
<Popover2
portalClassName="live-query-mode-selector-portal"
minimal
position={PopoverPosition.BOTTOM_LEFT}
@ -72,6 +73,6 @@ export const LiveQueryModeSelector = React.memo(function LiveQueryModeSelector(
{LIVE_QUERY_MODE_TITLE[liveQueryMode]}
</span>
</Button>
</Popover>
</Popover2>
);
});

View File

@ -8,51 +8,43 @@ exports[`QueryExtraInfo matches snapshot 1`] = `
class="query-info"
>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class=""
tabindex="0"
>
<span
class=""
tabindex="0"
>
0 results in 8.00s
</span>
0 results in 8.00s
</span>
</span>
</div>
<span
class="bp3-popover-wrapper download-button"
class="download-button bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button bp3-minimal"
type="button"
>
<button
class="bp3-button bp3-minimal"
type="button"
<span
class="bp3-icon bp3-icon-download"
icon="download"
>
<span
class="bp3-icon bp3-icon-download"
icon="download"
<svg
data-icon="download"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="download"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
download
</desc>
<path
d="M7.99-.01c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zM11.7 9.7l-3 3c-.18.18-.43.29-.71.29s-.53-.11-.71-.29l-3-3A1.003 1.003 0 015.7 8.28l1.29 1.29V3.99c0-.55.45-1 1-1s1 .45 1 1v5.59l1.29-1.29a1.003 1.003 0 011.71.71c0 .27-.11.52-.29.7z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
download
</desc>
<path
d="M7.99-.01c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zM11.7 9.7l-3 3c-.18.18-.43.29-.71.29s-.53-.11-.71-.29l-3-3A1.003 1.003 0 015.7 8.28l1.29 1.29V3.99c0-.55.45-1 1-1s1 .45 1 1v5.59l1.29-1.29a1.003 1.003 0 011.71.71c0 .27-.11.52-.29.7z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</div>
`;

View File

@ -16,17 +16,9 @@
* limitations under the License.
*/
import {
Button,
Intent,
Menu,
MenuDivider,
MenuItem,
Popover,
Position,
Tooltip,
} from '@blueprintjs/core';
import { Button, Intent, Menu, MenuDivider, MenuItem, Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2, Tooltip2 } from '@blueprintjs/popover2';
import copy from 'copy-to-clipboard';
import { QueryResult } from 'druid-query-toolkit';
import React, { MouseEvent } from 'react';
@ -103,14 +95,14 @@ export const QueryExtraInfo = React.memo(function QueryExtraInfo(props: QueryExt
<div className="query-extra-info">
{typeof queryResult.queryDuration !== 'undefined' && (
<div className="query-info" onClick={handleQueryInfoClick}>
<Tooltip content={tooltipContent} hoverOpenDelay={500}>
<Tooltip2 content={tooltipContent} hoverOpenDelay={500} placement="top-start">
{`${resultCount} in ${(queryResult.queryDuration / 1000).toFixed(2)}s`}
</Tooltip>
</Tooltip2>
</div>
)}
<Popover className="download-button" content={downloadMenu} position={Position.BOTTOM_RIGHT}>
<Popover2 className="download-button" content={downloadMenu} position={Position.BOTTOM_RIGHT}>
<Button icon={IconNames.DOWNLOAD} minimal />
</Popover>
</Popover2>
</div>
);
});

View File

@ -1,13 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`query input correctly formats helper HTML 1`] = `
exports[`QueryInput correctly formats helper HTML 1`] = `
"
<div class=\\"doc-name\\">COUNT</div>
<div class=\\"doc-syntax\\">COUNT(*)</div>
<div class=\\"doc-description\\">Counts the number of things</div>"
`;
exports[`query input matches snapshot 1`] = `
exports[`QueryInput matches snapshot 1`] = `
<div
class="query-input"
>

View File

@ -21,7 +21,7 @@ import React from 'react';
import { QueryInput } from './query-input';
describe('query input', () => {
describe('QueryInput', () => {
it('matches snapshot', () => {
const sqlControl = (
<QueryInput queryString="hello world" onQueryStringChange={() => {}} runeMode={false} />

View File

@ -16,7 +16,8 @@
* limitations under the License.
*/
import { IResizeEntry, ResizeSensor } from '@blueprintjs/core';
import { ResizeEntry } from '@blueprintjs/core';
import { ResizeSensor2 } from '@blueprintjs/popover2';
import ace, { Editor } from 'brace';
import escape from 'lodash.escape';
import React from 'react';
@ -198,7 +199,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
}
}
private readonly handleAceContainerResize = (entries: IResizeEntry[]) => {
private readonly handleAceContainerResize = (entries: ResizeEntry[]) => {
if (entries.length !== 1) return;
this.setState({ editorHeight: entries[0].contentRect.height });
};
@ -228,7 +229,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
// Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
return (
<div className="query-input">
<ResizeSensor onResize={this.handleAceContainerResize}>
<ResizeSensor2 onResize={this.handleAceContainerResize}>
<div className="ace-container">
<AceEditor
mode={runeMode ? 'hjson' : 'dsql'}
@ -259,7 +260,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
}}
/>
</div>
</ResizeSensor>
</ResizeSensor2>
</div>
);
}

View File

@ -29,36 +29,32 @@ exports[`query output matches snapshot 1`] = `
class="rt-resizable-header-content"
>
<span
class="bp3-popover-wrapper clickable-cell"
class="clickable-cell bp3-popover2-target"
>
<span
class="bp3-popover-target"
<div
class=""
>
<div
class=""
language
<span
class="bp3-icon bp3-icon-filter"
icon="filter"
>
language
<span
class="bp3-icon bp3-icon-filter"
icon="filter"
<svg
data-icon="filter"
height="14"
viewBox="0 0 16 16"
width="14"
>
<svg
data-icon="filter"
height="14"
viewBox="0 0 16 16"
width="14"
>
<desc>
filter
</desc>
<path
d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71 4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003 1.003 0 00-.71-1.71z"
fill-rule="evenodd"
/>
</svg>
</span>
</div>
</span>
<desc>
filter
</desc>
<path
d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71 4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003 1.003 0 00-.71-1.71z"
fill-rule="evenodd"
/>
</svg>
</span>
</div>
</span>
</div>
<div
@ -75,36 +71,32 @@ exports[`query output matches snapshot 1`] = `
class="rt-resizable-header-content"
>
<span
class="bp3-popover-wrapper clickable-cell"
class="clickable-cell bp3-popover2-target"
>
<span
class="bp3-popover-target"
<div
class=""
>
<div
class=""
Count
<span
class="bp3-icon bp3-icon-filter"
icon="filter"
>
Count
<span
class="bp3-icon bp3-icon-filter"
icon="filter"
<svg
data-icon="filter"
height="14"
viewBox="0 0 16 16"
width="14"
>
<svg
data-icon="filter"
height="14"
viewBox="0 0 16 16"
width="14"
>
<desc>
filter
</desc>
<path
d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71 4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003 1.003 0 00-.71-1.71z"
fill-rule="evenodd"
/>
</svg>
</span>
</div>
</span>
<desc>
filter
</desc>
<path
d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71 4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003 1.003 0 00-.71-1.71z"
fill-rule="evenodd"
/>
</svg>
</span>
</div>
</span>
</div>
<div
@ -121,17 +113,13 @@ exports[`query output matches snapshot 1`] = `
class="rt-resizable-header-content"
>
<span
class="bp3-popover-wrapper clickable-cell"
class="clickable-cell bp3-popover2-target"
>
<span
class="bp3-popover-target"
<div
class=""
>
<div
class=""
>
dist_language
</div>
</span>
dist_language
</div>
</span>
</div>
<div
@ -148,17 +136,13 @@ exports[`query output matches snapshot 1`] = `
class="rt-resizable-header-content"
>
<span
class="bp3-popover-wrapper clickable-cell"
class="clickable-cell bp3-popover2-target"
>
<span
class="bp3-popover-target"
<div
class=""
>
<div
class=""
>
language_filtered_count
</div>
</span>
language_filtered_count
</div>
</span>
</div>
<div
@ -186,16 +170,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell null"
>
<span
class="table-cell null"
>
null
</span>
null
</span>
</span>
</div>
@ -207,16 +187,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
6881
</span>
6881
</span>
</span>
</div>
@ -228,16 +204,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
1
</span>
1
</span>
</span>
</div>
@ -249,16 +221,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
0
</span>
0
</span>
</span>
</div>
@ -280,16 +248,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
JavaScript
</span>
JavaScript
</span>
</span>
</div>
@ -301,16 +265,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
166
</span>
166
</span>
</span>
</div>
@ -322,16 +282,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
1
</span>
1
</span>
</span>
</div>
@ -343,16 +299,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
0
</span>
0
</span>
</span>
</div>
@ -374,16 +326,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
Python
</span>
Python
</span>
</span>
</div>
@ -395,16 +343,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
62
</span>
62
</span>
</span>
</div>
@ -416,16 +360,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
1
</span>
1
</span>
</span>
</div>
@ -437,16 +377,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
0
</span>
0
</span>
</span>
</div>
@ -468,16 +404,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
HTML
</span>
HTML
</span>
</span>
</div>
@ -489,16 +421,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
46
</span>
46
</span>
</span>
</div>
@ -510,16 +438,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
1
</span>
1
</span>
</span>
</div>
@ -531,16 +455,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell plain"
>
<span
class="table-cell plain"
>
0
</span>
0
</span>
</span>
</div>
@ -562,16 +482,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell null"
>
<span
class="table-cell null"
>
null
</span>
null
</span>
</span>
</div>
@ -583,16 +499,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell null"
>
<span
class="table-cell null"
>
null
</span>
null
</span>
</span>
</div>
@ -604,16 +516,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell null"
>
<span
class="table-cell null"
>
null
</span>
null
</span>
</span>
</div>
@ -625,16 +533,12 @@ exports[`query output matches snapshot 1`] = `
>
<div>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
class="table-cell null"
>
<span
class="table-cell null"
>
null
</span>
null
</span>
</span>
</div>

View File

@ -69,7 +69,7 @@
width: 100%;
}
.bp3-popover-target {
.bp3-popover2-target {
width: 100%;
}

View File

@ -16,8 +16,9 @@
* limitations under the License.
*/
import { Icon, Menu, MenuItem, Popover } from '@blueprintjs/core';
import { Icon, Menu, MenuItem } from '@blueprintjs/core';
import { IconName, IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import classNames from 'classnames';
import {
QueryResult,
@ -396,14 +397,14 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
? () => <ColumnRenameInput initialName={h} onDone={renameColumnTo} />
: () => {
return (
<Popover className="clickable-cell" content={getHeaderMenu(h, i)}>
<Popover2 className="clickable-cell" content={getHeaderMenu(h, i)}>
<div>
{h}
{hasFilterOnHeader(h, i) && (
<Icon icon={IconNames.FILTER} iconSize={14} />
)}
</div>
</Popover>
</Popover2>
);
},
headerClassName: getHeaderClassName(h, i),
@ -412,7 +413,7 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
const value = row.value;
return (
<div>
<Popover content={getCellMenu(h, i, value)}>
<Popover2 content={getCellMenu(h, i, value)}>
{numericColumnBraces[i] ? (
<BracedText
text={String(value)}
@ -422,7 +423,7 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
) : (
<TableCell value={value} unlimited />
)}
</Popover>
</Popover2>
</div>
);
},

View File

@ -32,14 +32,6 @@ describe('QueryView', () => {
expect(sqlView).toMatchSnapshot();
});
it('.trimSemicolon', () => {
expect(QueryView.trimSemicolon('SELECT * FROM tbl;')).toEqual('SELECT * FROM tbl');
expect(QueryView.trimSemicolon('SELECT * FROM tbl; ')).toEqual('SELECT * FROM tbl ');
expect(QueryView.trimSemicolon('SELECT * FROM tbl; --hello ')).toEqual(
'SELECT * FROM tbl --hello ',
);
});
it('.formatStr', () => {
expect(QueryView.formatStr(null, 'csv')).toEqual('"null"');
expect(QueryView.formatStr('hello\nworld', 'csv')).toEqual('"hello world"');

View File

@ -16,7 +16,8 @@
* limitations under the License.
*/
import { Code, Intent, Switch, Tooltip } from '@blueprintjs/core';
import { Code, Intent, Switch } from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import classNames from 'classnames';
import { QueryResult, QueryRunner, SqlQuery } from 'druid-query-toolkit';
import Hjson from 'hjson';
@ -26,34 +27,31 @@ import React, { RefObject } from 'react';
import SplitterLayout from 'react-splitter-layout';
import { Loader } from '../../components';
import { QueryPlanDialog } from '../../dialogs';
import { EditContextDialog } from '../../dialogs/edit-context-dialog/edit-context-dialog';
import { QueryHistoryDialog } from '../../dialogs/query-history-dialog/query-history-dialog';
import { Api, AppToaster } from '../../singletons';
import {
BasicQueryExplanation,
ColumnMetadata,
downloadFile,
DruidError,
findEmptyLiteralPosition,
getDruidErrorMessage,
localStorageGet,
localStorageGetJson,
LocalStorageKeys,
localStorageSet,
localStorageSetJson,
parseQueryPlan,
queryDruidSql,
QueryManager,
QueryState,
QueryWithContext,
RowColumn,
SemiJoinQueryExplanation,
stringifyValue,
} from '../../utils';
import { isEmptyContext, QueryContext } from '../../utils/query-context';
import { QueryRecord, QueryRecordUtil } from '../../utils/query-history';
import { ColumnTree } from './column-tree/column-tree';
import { ExplainDialog } from './explain-dialog/explain-dialog';
import {
LIVE_QUERY_MODES,
LiveQueryMode,
@ -76,12 +74,6 @@ const parser = memoizeOne((sql: string): SqlQuery | undefined => {
}
});
interface QueryWithContext {
queryString: string;
queryContext: QueryContext;
wrapQueryLimit: number | undefined;
}
export interface QueryViewProps {
initQuery: string | undefined;
defaultQueryContext?: Record<string, any>;
@ -99,8 +91,7 @@ export interface QueryViewState {
queryResultState: QueryState<QueryResult, DruidError>;
explainDialogOpen: boolean;
explainResultState: QueryState<BasicQueryExplanation | SemiJoinQueryExplanation | string>;
explainDialogQuery?: QueryWithContext;
defaultSchema?: string;
defaultTable?: string;
@ -111,25 +102,10 @@ export interface QueryViewState {
}
export class QueryView extends React.PureComponent<QueryViewProps, QueryViewState> {
static trimSemicolon(query: string): string {
// Trims out a trailing semicolon while preserving space (https://bit.ly/1n1yfkJ)
return query.replace(/;+((?:\s*--[^\n]*)?\s*)$/, '$1');
}
static isEmptyQuery(query: string): boolean {
return query.trim() === '';
}
static isExplainQuery(query: string): boolean {
return /EXPLAIN\sPLAN\sFOR/i.test(query);
}
static wrapInExplainIfNeeded(query: string): string {
query = QueryView.trimSemicolon(query);
if (QueryView.isExplainQuery(query)) return query;
return `EXPLAIN PLAN FOR (${query}\n)`;
}
static isJsonLike(queryString: string): boolean {
return queryString.trim().startsWith('{');
}
@ -158,10 +134,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
private readonly metadataQueryManager: QueryManager<null, ColumnMetadata[]>;
private readonly queryManager: QueryManager<QueryWithContext, QueryResult>;
private readonly explainQueryManager: QueryManager<
QueryWithContext,
BasicQueryExplanation | SemiJoinQueryExplanation | string
>;
private readonly queryInputRef: RefObject<QueryInput>;
@ -196,9 +168,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
queryResultState: QueryState.INIT,
explainDialogOpen: false,
explainResultState: QueryState.INIT,
editContextDialogOpen: false,
historyDialogOpen: false,
queryHistory,
@ -260,37 +229,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
});
},
});
this.explainQueryManager = new QueryManager({
processQuery: async (queryWithContext: QueryWithContext) => {
const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
let context: Record<string, any> | undefined;
if (!isEmptyContext(queryContext) || wrapQueryLimit || mandatoryQueryContext) {
context = { ...queryContext, ...(mandatoryQueryContext || {}) };
if (typeof wrapQueryLimit !== 'undefined') {
context.sqlOuterLimit = wrapQueryLimit + 1;
}
}
let result: QueryResult | undefined;
try {
result = await queryRunner.runQuery({
query: QueryView.wrapInExplainIfNeeded(queryString),
extraQueryContext: context,
});
} catch (e) {
throw new Error(getDruidErrorMessage(e));
}
return parseQueryPlan(result.rows[0][0]);
},
onStateChange: explainResultState => {
this.setState({
explainResultState,
});
},
});
}
componentDidMount(): void {
@ -307,10 +245,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
componentWillUnmount(): void {
this.metadataQueryManager.terminate();
this.queryManager.terminate();
this.explainQueryManager.terminate();
}
prettyPrintJson(): void {
private prettyPrintJson(): void {
this.setState(prevState => {
let parsed: any;
try {
@ -367,21 +304,22 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
);
};
renderExplainDialog() {
const { explainDialogOpen, explainResultState } = this.state;
if (explainResultState.loading || !explainDialogOpen) return;
private renderExplainDialog() {
const { mandatoryQueryContext } = this.props;
const { explainDialogQuery } = this.state;
if (!explainDialogQuery) return;
return (
<QueryPlanDialog
explainResult={explainResultState.data}
explainError={explainResultState.error}
<ExplainDialog
queryWithContext={explainDialogQuery}
mandatoryQueryContext={mandatoryQueryContext}
setQueryString={this.handleQueryStringChange}
onClose={() => this.setState({ explainDialogOpen: false })}
onClose={() => this.setState({ explainDialogQuery: undefined })}
/>
);
}
renderHistoryDialog() {
private renderHistoryDialog() {
const { historyDialogOpen, queryHistory } = this.state;
if (!historyDialogOpen) return;
@ -397,7 +335,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
);
}
renderEditContextDialog() {
private renderEditContextDialog() {
const { editContextDialogOpen, queryContext } = this.state;
if (!editContextDialogOpen) return;
@ -412,7 +350,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
);
}
renderLiveQueryModeSelector() {
private renderLiveQueryModeSelector() {
const { liveQueryMode, queryString } = this.state;
if (QueryView.isJsonLike(queryString)) return;
@ -425,12 +363,12 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
);
}
renderWrapQueryLimitSelector() {
private renderWrapQueryLimitSelector() {
const { wrapQueryLimit, queryString } = this.state;
if (QueryView.isJsonLike(queryString)) return;
return (
<Tooltip
<Tooltip2
content="Automatically wrap the query with a limit to protect against queries with very large result sets."
hoverOpenDelay={800}
>
@ -440,11 +378,11 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
label="Auto limit"
onChange={() => this.handleWrapQueryLimitChange(wrapQueryLimit ? undefined : 100)}
/>
</Tooltip>
</Tooltip2>
);
}
renderMainArea() {
private renderMainArea() {
const { queryString, queryContext, queryResultState, columnMetadataState } = this.state;
const emptyQuery = QueryView.isEmptyQuery(queryString);
const queryResult = queryResultState.data;
@ -636,11 +574,12 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
private readonly handleExplain = () => {
const { queryString, queryContext, wrapQueryLimit } = this.state;
this.setState({ explainDialogOpen: true });
this.explainQueryManager.runQuery({
queryString,
queryContext,
wrapQueryLimit,
this.setState({
explainDialogQuery: {
queryString,
queryContext,
wrapQueryLimit,
},
});
};

View File

@ -36,36 +36,32 @@ exports[`run button matches snapshot loading state 1`] = `
</span>
</button>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button bp3-intent-primary"
type="button"
>
<button
class="bp3-button bp3-intent-primary"
type="button"
<span
class="bp3-icon bp3-icon-more"
icon="more"
>
<span
class="bp3-icon bp3-icon-more"
icon="more"
<svg
data-icon="more"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="more"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
more
</desc>
<path
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
more
</desc>
<path
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</div>
`;
@ -104,36 +100,32 @@ exports[`run button matches snapshot non-loading state 1`] = `
</span>
</button>
<span
class="bp3-popover-wrapper"
class="bp3-popover2-target"
>
<span
class="bp3-popover-target"
<button
class="bp3-button bp3-intent-primary"
type="button"
>
<button
class="bp3-button bp3-intent-primary"
type="button"
<span
class="bp3-icon bp3-icon-more"
icon="more"
>
<span
class="bp3-icon bp3-icon-more"
icon="more"
<svg
data-icon="more"
height="16"
viewBox="0 0 16 16"
width="16"
>
<svg
data-icon="more"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
more
</desc>
<path
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<desc>
more
</desc>
<path
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</div>
`;

View File

@ -23,11 +23,11 @@ import {
Menu,
MenuDivider,
MenuItem,
Popover,
Position,
useHotkeys,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
import React from 'react';
import { MenuCheckbox } from '../../../components';
@ -127,7 +127,7 @@ const RunButtonExtraMenu = (props: RunButtonProps) => {
};
export const RunButton = React.memo(function RunButton(props: RunButtonProps) {
const { runeMode, onRun, loading } = props;
const { runeMode, onRun, loading, onExplain } = props;
const handleRun = React.useCallback(() => {
if (!onRun) return;
@ -139,12 +139,29 @@ export const RunButton = React.memo(function RunButton(props: RunButtonProps) {
{
allowInInput: true,
global: true,
combo: 'ctrl + enter',
group: 'Query',
combo: 'mod + enter',
label: 'Runs the current query',
onKeyDown: handleRun,
},
{
allowInInput: true,
global: true,
group: 'Query',
combo: 'mod + e',
label: 'Explain the current query',
onKeyDown: onExplain,
},
{
allowInInput: true,
global: true,
group: 'X-Legacy', // This is prefixed with X so it appears in the bottom of the list
combo: 'ctrl + enter',
label: 'Runs the current query (old shortcut)',
onKeyDown: handleRun,
},
];
}, [handleRun]);
}, [handleRun, onExplain]);
useHotkeys(hotkeys);
@ -167,13 +184,13 @@ export const RunButton = React.memo(function RunButton(props: RunButtonProps) {
disabled
/>
)}
<Popover position={Position.BOTTOM_LEFT} content={<RunButtonExtraMenu {...props} />}>
<Popover2 position={Position.BOTTOM_LEFT} content={<RunButtonExtraMenu {...props} />}>
<Button
className={runeMode ? 'rune-button' : undefined}
icon={IconNames.MORE}
intent={onRun ? Intent.PRIMARY : undefined}
/>
</Popover>
</Popover2>
</ButtonGroup>
);
});