mirror of https://github.com/apache/druid.git
Web console: Improve workbench view with resizable side panels (#17387)
This commit is contained in:
parent
d5bb7de5cf
commit
4b7902e74a
|
@ -6329,16 +6329,6 @@ license_file_path: licenses/bin/react-router.MIT
|
|||
|
||||
---
|
||||
|
||||
name: "react-splitter-layout"
|
||||
license_category: binary
|
||||
module: web-console
|
||||
license_name: MIT License
|
||||
copyright: Yang Liu
|
||||
version: 4.0.0
|
||||
license_file_path: licenses/bin/react-splitter-layout.MIT
|
||||
|
||||
---
|
||||
|
||||
name: "react-table"
|
||||
license_category: binary
|
||||
module: web-console
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
"react-dom": "^18.3.1",
|
||||
"react-router": "^5.3.4",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-splitter-layout": "^4.0.0",
|
||||
"react-table": "~6.11.5",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"tslib": "^2.8.0",
|
||||
|
@ -75,7 +74,6 @@
|
|||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-splitter-layout": "^3.0.5",
|
||||
"@types/react-table": "6.8.5",
|
||||
"@types/uuid": "^7.0.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
|
@ -4064,15 +4062,6 @@
|
|||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-splitter-layout": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-splitter-layout/-/react-splitter-layout-3.0.5.tgz",
|
||||
"integrity": "sha512-J3NKVdPguGcSSN41/EDV3BYdYfndxUil2SX4wDBJiVfcnyopCegdhIrC2kcaa4CxjpaPChe1bja2k8LLDxP0ZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-table": {
|
||||
"version": "6.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.5.tgz",
|
||||
|
@ -14964,15 +14953,6 @@
|
|||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/react-splitter-layout": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
|
||||
"integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA==",
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.5.0",
|
||||
"react": "^15.5.0 || ^16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-table": {
|
||||
"version": "6.11.5",
|
||||
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.11.5.tgz",
|
||||
|
@ -21157,15 +21137,6 @@
|
|||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-splitter-layout": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-splitter-layout/-/react-splitter-layout-3.0.5.tgz",
|
||||
"integrity": "sha512-J3NKVdPguGcSSN41/EDV3BYdYfndxUil2SX4wDBJiVfcnyopCegdhIrC2kcaa4CxjpaPChe1bja2k8LLDxP0ZQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "^18.3.11"
|
||||
}
|
||||
},
|
||||
"@types/react-table": {
|
||||
"version": "6.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.5.tgz",
|
||||
|
@ -28826,11 +28797,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-splitter-layout": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz",
|
||||
"integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA=="
|
||||
},
|
||||
"react-table": {
|
||||
"version": "6.11.5",
|
||||
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.11.5.tgz",
|
||||
|
|
|
@ -83,7 +83,6 @@
|
|||
"react-dom": "^18.3.1",
|
||||
"react-router": "^5.3.4",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-splitter-layout": "^4.0.0",
|
||||
"react-table": "~6.11.5",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"tslib": "^2.8.0",
|
||||
|
@ -116,7 +115,6 @@
|
|||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-splitter-layout": "^3.0.5",
|
||||
"@types/react-table": "6.8.5",
|
||||
"@types/uuid": "^7.0.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
|
|
|
@ -51,6 +51,7 @@ export * from './segment-timeline/segment-timeline';
|
|||
export * from './show-json/show-json';
|
||||
export * from './show-log/show-log';
|
||||
export * from './show-value/show-value';
|
||||
export * from './splitter-layout/splitter-layout';
|
||||
export * from './suggestion-menu/suggestion-menu';
|
||||
export * from './supervisor-history-panel/supervisor-history-panel';
|
||||
export * from './table-cell/table-cell';
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SplitterLayout matches snapshot one child 1`] = `
|
||||
<div
|
||||
class="splitter-layout splitter-layout-horizontal"
|
||||
>
|
||||
<div
|
||||
class="layout-pane layout-pane-primary"
|
||||
>
|
||||
<div
|
||||
class="child1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SplitterLayout matches snapshot two children 1`] = `
|
||||
<div
|
||||
class="splitter-layout splitter-layout-vertical"
|
||||
>
|
||||
<div
|
||||
class="layout-pane layout-pane-primary"
|
||||
>
|
||||
<div
|
||||
class="child1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="layout-splitter"
|
||||
role="separator"
|
||||
/>
|
||||
<div
|
||||
class="layout-pane"
|
||||
style="height: 50%;"
|
||||
>
|
||||
<div
|
||||
class="child2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface LayoutPaneProps {
|
||||
vertical?: boolean;
|
||||
size: number | undefined;
|
||||
percentage?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function LayoutPane(props: LayoutPaneProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames('layout-pane', {
|
||||
'layout-pane-primary': typeof props.size === 'undefined',
|
||||
})}
|
||||
style={
|
||||
typeof props.size === 'number'
|
||||
? {
|
||||
[props.vertical ? 'height' : 'width']: `${props.size}${
|
||||
props.percentage ? '%' : 'px'
|
||||
}`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Originally copied from https://github.com/zesik/react-splitter-layout/blob/master/src/stylesheets/index.css and heavily refactored
|
||||
|
||||
$default-splitter-size: 8px;
|
||||
|
||||
.splitter-layout {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
&.splitter-layout-horizontal {
|
||||
flex-direction: row;
|
||||
|
||||
&.layout-changing {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
& > .layout-splitter {
|
||||
width: $default-splitter-size;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
}
|
||||
}
|
||||
|
||||
&.splitter-layout-vertical {
|
||||
flex-direction: column;
|
||||
|
||||
&.layout-changing {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
& > .layout-splitter {
|
||||
width: 100%;
|
||||
height: $default-splitter-size;
|
||||
cursor: row-resize;
|
||||
}
|
||||
}
|
||||
|
||||
& > .layout-pane {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
overflow: auto;
|
||||
|
||||
&.layout-pane-primary {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
& > .layout-splitter {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { SplitterLayout } from './splitter-layout';
|
||||
|
||||
describe('SplitterLayout', () => {
|
||||
it('matches snapshot one child', () => {
|
||||
const splitterLayout = (
|
||||
<SplitterLayout primaryMinSize={100} secondaryInitialSize={50} secondaryMinSize={10}>
|
||||
<div className="child1" />
|
||||
</SplitterLayout>
|
||||
);
|
||||
|
||||
const { container } = render(splitterLayout);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot two children', () => {
|
||||
const splitterLayout = (
|
||||
<SplitterLayout
|
||||
vertical
|
||||
percentage
|
||||
primaryMinSize={100}
|
||||
secondaryInitialSize={50}
|
||||
secondaryMinSize={10}
|
||||
>
|
||||
<div className="child1" />
|
||||
<div className="child2" />
|
||||
</SplitterLayout>
|
||||
);
|
||||
|
||||
const { container } = render(splitterLayout);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Originally copied from https://github.com/zesik/react-splitter-layout/blob/master/src/components/SplitterLayout.jsx and heavily refactored
|
||||
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Children, Component } from 'react';
|
||||
|
||||
import { clamp } from '../../utils';
|
||||
|
||||
import { LayoutPane } from './layout-pane';
|
||||
|
||||
import './splitter-layout.scss';
|
||||
|
||||
function clearSelection() {
|
||||
if (window.getSelection) {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
if (selection.empty) {
|
||||
selection.empty();
|
||||
} else if (selection.removeAllRanges) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SplitterLayoutProps {
|
||||
className?: string;
|
||||
vertical?: boolean;
|
||||
percentage?: boolean;
|
||||
primaryIndex?: 0 | 1;
|
||||
primaryMinSize?: number;
|
||||
secondaryInitialSize: number;
|
||||
secondaryMinSize?: number;
|
||||
secondaryMaxSize?: number;
|
||||
splitterSize?: number;
|
||||
onSecondaryPaneSizeChange?: (size: number) => void;
|
||||
children: ReactNode | ReactNode[];
|
||||
}
|
||||
|
||||
interface SplitterLayoutState {
|
||||
secondaryPaneSize: number;
|
||||
resizing: boolean;
|
||||
}
|
||||
|
||||
export class SplitterLayout extends Component<SplitterLayoutProps, SplitterLayoutState> {
|
||||
private container: HTMLDivElement | null = null;
|
||||
private splitter: HTMLDivElement | null = null;
|
||||
|
||||
constructor(props: SplitterLayoutProps) {
|
||||
super(props);
|
||||
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||
this.handleTouchMove = this.handleTouchMove.bind(this);
|
||||
this.handleSplitterMouseDown = this.handleSplitterMouseDown.bind(this);
|
||||
|
||||
this.state = {
|
||||
secondaryPaneSize: props.secondaryInitialSize,
|
||||
resizing: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
document.addEventListener('mouseup', this.handleMouseUp);
|
||||
document.addEventListener('mousemove', this.handleMouseMove);
|
||||
document.addEventListener('touchend', this.handleMouseUp);
|
||||
document.addEventListener('touchmove', this.handleTouchMove);
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: SplitterLayoutProps, prevState: SplitterLayoutState) {
|
||||
if (prevState.secondaryPaneSize !== this.state.secondaryPaneSize) {
|
||||
this.props.onSecondaryPaneSizeChange?.(this.state.secondaryPaneSize);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||
document.removeEventListener('touchend', this.handleMouseUp);
|
||||
document.removeEventListener('touchmove', this.handleTouchMove);
|
||||
}
|
||||
|
||||
getSecondaryPaneSize(
|
||||
containerRect: DOMRect,
|
||||
splitterRect: DOMRect,
|
||||
clientPosition: { top: number; left: number },
|
||||
offsetMouse: boolean,
|
||||
) {
|
||||
const {
|
||||
vertical,
|
||||
percentage,
|
||||
primaryIndex,
|
||||
primaryMinSize = 0,
|
||||
secondaryMinSize = 0,
|
||||
secondaryMaxSize = Infinity,
|
||||
} = this.props;
|
||||
|
||||
let totalSize;
|
||||
let splitterSize;
|
||||
let offset;
|
||||
if (vertical) {
|
||||
totalSize = containerRect.height;
|
||||
splitterSize = splitterRect.height;
|
||||
offset = clientPosition.top - containerRect.top;
|
||||
} else {
|
||||
totalSize = containerRect.width;
|
||||
splitterSize = splitterRect.width;
|
||||
offset = clientPosition.left - containerRect.left;
|
||||
}
|
||||
if (offsetMouse) {
|
||||
offset -= splitterSize / 2;
|
||||
}
|
||||
offset = clamp(offset, 0, totalSize - splitterSize);
|
||||
|
||||
let secondaryPaneSize = primaryIndex === 1 ? offset : totalSize - splitterSize - offset;
|
||||
if (percentage) {
|
||||
secondaryPaneSize = (secondaryPaneSize * 100) / totalSize;
|
||||
splitterSize = (splitterSize * 100) / totalSize;
|
||||
totalSize = 100;
|
||||
}
|
||||
|
||||
return clamp(
|
||||
secondaryPaneSize,
|
||||
secondaryMinSize,
|
||||
Math.min(secondaryMaxSize, totalSize - splitterSize - primaryMinSize),
|
||||
);
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
if (this.container && this.splitter && !this.props.percentage) {
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
const splitterRect = this.splitter.getBoundingClientRect();
|
||||
const secondaryPaneSize = this.getSecondaryPaneSize(
|
||||
containerRect,
|
||||
splitterRect,
|
||||
{
|
||||
left: splitterRect.left,
|
||||
top: splitterRect.top,
|
||||
},
|
||||
false,
|
||||
);
|
||||
this.setState({ secondaryPaneSize });
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove(e: MouseEvent | Touch) {
|
||||
if (this.container && this.splitter && this.state.resizing) {
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
const splitterRect = this.splitter.getBoundingClientRect();
|
||||
const secondaryPaneSize = this.getSecondaryPaneSize(
|
||||
containerRect,
|
||||
splitterRect,
|
||||
{
|
||||
left: e.clientX,
|
||||
top: e.clientY,
|
||||
},
|
||||
true,
|
||||
);
|
||||
clearSelection();
|
||||
this.setState({ secondaryPaneSize });
|
||||
}
|
||||
}
|
||||
|
||||
handleTouchMove(e: TouchEvent) {
|
||||
this.handleMouseMove(e.changedTouches[0]);
|
||||
}
|
||||
|
||||
handleSplitterMouseDown() {
|
||||
clearSelection();
|
||||
this.setState({ resizing: true });
|
||||
}
|
||||
|
||||
handleMouseUp() {
|
||||
this.setState(prevState => (prevState.resizing ? { resizing: false } : null));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, vertical, percentage, primaryIndex, splitterSize, children } = this.props;
|
||||
const { resizing } = this.state;
|
||||
|
||||
const childrenArray = Children.toArray(children).slice(0, 2);
|
||||
if (childrenArray.length === 0) return null;
|
||||
|
||||
const effectivePrimaryIndex = primaryIndex === 1 ? 1 : 0;
|
||||
const wrappedChildren = childrenArray.map((child, i) => {
|
||||
const isSecondary = childrenArray.length > 1 && i !== effectivePrimaryIndex;
|
||||
return (
|
||||
<LayoutPane
|
||||
key={isSecondary ? 'secondary' : 'primary'}
|
||||
vertical={vertical}
|
||||
percentage={percentage}
|
||||
size={isSecondary ? this.state.secondaryPaneSize : undefined}
|
||||
>
|
||||
{child}
|
||||
</LayoutPane>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'splitter-layout',
|
||||
className,
|
||||
vertical ? 'splitter-layout-vertical' : 'splitter-layout-horizontal',
|
||||
{ 'layout-changing': resizing },
|
||||
)}
|
||||
ref={c => {
|
||||
this.container = c;
|
||||
}}
|
||||
>
|
||||
{wrappedChildren[0]}
|
||||
{wrappedChildren.length > 1 && (
|
||||
<div
|
||||
role="separator"
|
||||
className="layout-splitter"
|
||||
ref={c => {
|
||||
this.splitter = c;
|
||||
}}
|
||||
onMouseDown={this.handleSplitterMouseDown}
|
||||
onTouchStart={this.handleSplitterMouseDown}
|
||||
style={splitterSize ? { [vertical ? 'height' : 'width']: splitterSize } : undefined}
|
||||
/>
|
||||
)}
|
||||
{wrappedChildren[1]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -285,6 +285,10 @@ export class Execution {
|
|||
});
|
||||
}
|
||||
|
||||
static fromDartReport(dartReport: MsqTaskReportResponse): Execution {
|
||||
return Execution.fromTaskReport(dartReport).changeEngine('sql-msq-dart');
|
||||
}
|
||||
|
||||
static fromTaskReport(taskReport: MsqTaskReportResponse): Execution {
|
||||
// Must have status set for a valid report
|
||||
const id = deepGet(taskReport, 'multiStageQuery.taskId');
|
||||
|
@ -327,6 +331,7 @@ export class Execution {
|
|||
new Column({ name: sig.name, nativeType: sig.type, sqlType: sqlTypeNames?.[i] }),
|
||||
),
|
||||
rows: results,
|
||||
queryDuration: durationMs,
|
||||
}).inflateDatesFromSqlTypes();
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
@import '@blueprintjs/datetime/src/blueprint-datetime';
|
||||
@import '@blueprintjs/datetime2/src/blueprint-datetime2';
|
||||
@import '@blueprintjs/select/src/blueprint-select';
|
||||
@import 'react-splitter-layout/lib/index';
|
||||
@import './react-table/react-table-base-styles';
|
||||
@import './react-table/react-table-extra';
|
||||
|
||||
|
|
|
@ -51,6 +51,8 @@ export const LocalStorageKeys = {
|
|||
WORKBENCH_QUERIES: 'workbench-queries' as const,
|
||||
WORKBENCH_LAST_TAB: 'workbench-last-tab' as const,
|
||||
WORKBENCH_PANE_SIZE: 'workbench-pane-size' as const,
|
||||
WORKBENCH_LEFT_SIZE: 'workbench-left-size' as const,
|
||||
WORKBENCH_RIGHT_SIZE: 'workbench-right-size' as const,
|
||||
WORKBENCH_HISTORY: 'workbench-history' as const,
|
||||
WORKBENCH_TASK_PANEL: 'workbench-task-panel' as const,
|
||||
WORKBENCH_DART_PANEL: 'workbench-dart-panel' as const,
|
||||
|
|
|
@ -55,3 +55,11 @@ $table-cell-h-padding: 5px;
|
|||
box-shadow: $pt-dark-elevation-shadow-1;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin pin-full {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -108,6 +108,14 @@ exports[`DatasourcesView matches snapshot 1`] = `
|
|||
}
|
||||
/>
|
||||
</Memo(ViewControlBar)>
|
||||
<SplitterLayout
|
||||
percentage={true}
|
||||
primaryIndex={1}
|
||||
primaryMinSize={20}
|
||||
secondaryInitialSize={35}
|
||||
secondaryMinSize={10}
|
||||
vertical={true}
|
||||
>
|
||||
<ReactTable
|
||||
AggregatedComponent={[Function]}
|
||||
ExpanderComponent={[Function]}
|
||||
|
@ -438,5 +446,6 @@ exports[`DatasourcesView matches snapshot 1`] = `
|
|||
style={{}}
|
||||
subRowsKey="_subRows"
|
||||
/>
|
||||
</SplitterLayout>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -23,25 +23,30 @@
|
|||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.clickable-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ReactTable {
|
||||
.splitter-layout {
|
||||
position: absolute;
|
||||
top: $view-control-bar-height + $standard-padding;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
& > .layout-splitter:hover {
|
||||
background: black;
|
||||
opacity: 0.1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.show-segment-timeline {
|
||||
.segment-timeline {
|
||||
height: calc(50% - 55px);
|
||||
margin-top: 10px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ReactTable {
|
||||
top: 50%;
|
||||
@include pin-full;
|
||||
|
||||
.clickable-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
import { FormGroup, InputGroup, Intent, MenuItem, Switch, Tag } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { SqlQuery, T } from '@druid-toolkit/query';
|
||||
import classNames from 'classnames';
|
||||
import { sum } from 'd3-array';
|
||||
import React from 'react';
|
||||
import type { Filter } from 'react-table';
|
||||
|
@ -34,6 +33,7 @@ import {
|
|||
MoreButton,
|
||||
RefreshButton,
|
||||
SegmentTimeline,
|
||||
SplitterLayout,
|
||||
TableClickableCell,
|
||||
TableColumnSelector,
|
||||
type TableColumnSelectorColumn,
|
||||
|
@ -1678,11 +1678,7 @@ GROUP BY 1, 2`;
|
|||
} = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('datasources-view app-view', {
|
||||
'show-segment-timeline': showSegmentTimeline,
|
||||
})}
|
||||
>
|
||||
<div className="datasources-view app-view">
|
||||
<ViewControlBar label="Datasources">
|
||||
<RefreshButton
|
||||
onRefresh={auto => {
|
||||
|
@ -1717,8 +1713,17 @@ GROUP BY 1, 2`;
|
|||
tableColumnsHidden={visibleColumns.getHiddenColumns()}
|
||||
/>
|
||||
</ViewControlBar>
|
||||
<SplitterLayout
|
||||
vertical
|
||||
percentage
|
||||
secondaryInitialSize={35}
|
||||
primaryIndex={1}
|
||||
primaryMinSize={20}
|
||||
secondaryMinSize={10}
|
||||
>
|
||||
{showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
|
||||
{this.renderDatasourcesTable()}
|
||||
</SplitterLayout>
|
||||
{datasourceTableActionDialogId && (
|
||||
<DatasourceTableActionDialog
|
||||
datasource={datasourceTableActionDialogId}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.init-div {
|
||||
.init-pane {
|
||||
text-align: center;
|
||||
margin-top: 35vh;
|
||||
}
|
||||
|
|
|
@ -354,11 +354,13 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
|
|||
|
||||
if (isLookupsUninitialized(lookupEntriesAndTiersState.error)) {
|
||||
return (
|
||||
<div className="init-div">
|
||||
<div className="init-pane">
|
||||
<Button
|
||||
icon={IconNames.BUILD}
|
||||
text="Initialize lookups"
|
||||
onClick={() => void this.initializeLookup()}
|
||||
large
|
||||
intent={Intent.PRIMARY}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SegmentsView matches snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<div
|
||||
className="segments-view app-view"
|
||||
>
|
||||
|
@ -85,6 +84,14 @@ exports[`SegmentsView matches snapshot 1`] = `
|
|||
}
|
||||
/>
|
||||
</Memo(ViewControlBar)>
|
||||
<SplitterLayout
|
||||
percentage={true}
|
||||
primaryIndex={1}
|
||||
primaryMinSize={20}
|
||||
secondaryInitialSize={35}
|
||||
secondaryMinSize={10}
|
||||
vertical={true}
|
||||
>
|
||||
<ReactTable
|
||||
AggregatedComponent={[Function]}
|
||||
ExpanderComponent={[Function]}
|
||||
|
@ -453,6 +460,6 @@ exports[`SegmentsView matches snapshot 1`] = `
|
|||
style={{}}
|
||||
subRowsKey="_subRows"
|
||||
/>
|
||||
</SplitterLayout>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
|
|
@ -22,12 +22,28 @@
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.ReactTable {
|
||||
.splitter-layout {
|
||||
position: absolute;
|
||||
top: $view-control-bar-height + $standard-padding;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
& > .layout-splitter:hover {
|
||||
background: black;
|
||||
opacity: 0.1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.segment-timeline {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ReactTable {
|
||||
@include pin-full;
|
||||
|
||||
.-totalPages {
|
||||
display: none;
|
||||
}
|
||||
|
@ -60,15 +76,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.show-segment-timeline {
|
||||
.segment-timeline {
|
||||
height: calc(50% - 55px);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ReactTable {
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
import { Button, ButtonGroup, Intent, Label, MenuItem, Switch, Tag } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { C, L, SqlComparison, SqlExpression } from '@druid-toolkit/query';
|
||||
import classNames from 'classnames';
|
||||
import * as JSONBig from 'json-bigint-native';
|
||||
import React from 'react';
|
||||
import type { Filter } from 'react-table';
|
||||
|
@ -34,6 +33,7 @@ import {
|
|||
MoreButton,
|
||||
RefreshButton,
|
||||
SegmentTimeline,
|
||||
SplitterLayout,
|
||||
TableClickableCell,
|
||||
TableColumnSelector,
|
||||
type TableColumnSelectorColumn,
|
||||
|
@ -969,12 +969,7 @@ END AS "time_span"`,
|
|||
const { groupByInterval } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames('segments-view app-view', {
|
||||
'show-segment-timeline': showSegmentTimeline,
|
||||
})}
|
||||
>
|
||||
<div className="segments-view app-view">
|
||||
<ViewControlBar label="Segments">
|
||||
<RefreshButton
|
||||
onRefresh={auto => {
|
||||
|
@ -1025,9 +1020,17 @@ END AS "time_span"`,
|
|||
tableColumnsHidden={visibleColumns.getHiddenColumns()}
|
||||
/>
|
||||
</ViewControlBar>
|
||||
<SplitterLayout
|
||||
vertical
|
||||
percentage
|
||||
secondaryInitialSize={35}
|
||||
primaryIndex={1}
|
||||
primaryMinSize={20}
|
||||
secondaryMinSize={10}
|
||||
>
|
||||
{showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
|
||||
{this.renderSegmentsTable()}
|
||||
</div>
|
||||
</SplitterLayout>
|
||||
{this.renderTerminateSegmentAction()}
|
||||
{segmentTableActionDialogId && datasourceTableActionDialogId && (
|
||||
<SegmentTableActionDialog
|
||||
|
@ -1044,7 +1047,7 @@ END AS "time_span"`,
|
|||
onClose={() => this.setState({ showFullShardSpec: undefined })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,10 +135,11 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
|
|||
|
||||
return (
|
||||
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
||||
{aggregateMenuItem(F('SUM', column), `sum_${columnName}`)}
|
||||
{aggregateMenuItem(F('MIN', column), `min_${columnName}`)}
|
||||
{aggregateMenuItem(F('MAX', column), `max_${columnName}`)}
|
||||
{aggregateMenuItem(F('AVG', column), `avg_${columnName}`)}
|
||||
{aggregateMenuItem(F.sum(column), `sum_${columnName}`)}
|
||||
{aggregateMenuItem(F.min(column), `min_${columnName}`)}
|
||||
{aggregateMenuItem(F.max(column), `max_${columnName}`)}
|
||||
{aggregateMenuItem(F.avg(column), `avg_${columnName}`)}
|
||||
{aggregateMenuItem(F.countDistinct(column), `dist_${columnName}`)}
|
||||
{aggregateMenuItem(F('APPROX_QUANTILE', column, 0.98), `p98_${columnName}`)}
|
||||
{aggregateMenuItem(F('LATEST', column), `latest_${columnName}`)}
|
||||
</MenuItem>
|
||||
|
|
|
@ -104,7 +104,7 @@ type SearchMode = 'tables-and-columns' | 'tables-only' | 'columns-only';
|
|||
|
||||
const SEARCH_MODES: SearchMode[] = ['tables-and-columns', 'tables-only', 'columns-only'];
|
||||
|
||||
const SEARCH_MDOE_TITLE: Record<SearchMode, string> = {
|
||||
const SEARCH_MODE_TITLE: Record<SearchMode, string> = {
|
||||
'tables-and-columns': 'Tables and columns',
|
||||
'tables-only': 'Tables only',
|
||||
'columns-only': 'Columns only',
|
||||
|
@ -641,7 +641,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
|||
<MenuItem
|
||||
key={mode}
|
||||
icon={tickIcon(mode === searchMode)}
|
||||
text={SEARCH_MDOE_TITLE[mode]}
|
||||
text={SEARCH_MODE_TITLE[mode]}
|
||||
onClick={() => this.setState({ searchMode: mode })}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -142,7 +142,7 @@ export const ExecutionSummaryPanel = React.memo(function ExecutionSummaryPanel(
|
|||
<Button
|
||||
key="reset"
|
||||
icon={IconNames.CROSS}
|
||||
data-tooltip="Clear output pane"
|
||||
data-tooltip="Clear output"
|
||||
minimal
|
||||
onClick={onReset}
|
||||
/>,
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
@import '../../../variables';
|
||||
|
||||
$vertical-gap: 6px;
|
||||
$visible-splitter-size: 3px;
|
||||
|
||||
.query-tab {
|
||||
position: relative;
|
||||
|
@ -27,14 +28,26 @@ $vertical-gap: 6px;
|
|||
background: $dark-gray2;
|
||||
}
|
||||
|
||||
.splitter-layout {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
.splitter-layout.splitter-layout-vertical {
|
||||
height: 100%;
|
||||
|
||||
&.splitter-layout-vertical > .layout-splitter {
|
||||
height: 3px;
|
||||
& > .layout-splitter {
|
||||
height: 3px + $vertical-gap * 2;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: $gray1;
|
||||
position: absolute;
|
||||
top: $vertical-gap;
|
||||
width: 100%;
|
||||
height: $visible-splitter-size;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background-color: $gray2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +55,7 @@ $vertical-gap: 6px;
|
|||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
bottom: $vertical-gap;
|
||||
bottom: 0;
|
||||
|
||||
.query-section {
|
||||
position: absolute;
|
||||
|
@ -84,7 +97,7 @@ $vertical-gap: 6px;
|
|||
.output-section {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: $vertical-gap;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
@include card-like;
|
||||
|
||||
|
|
|
@ -22,10 +22,9 @@ import { QueryResult, QueryRunner, SqlQuery } from '@druid-toolkit/query';
|
|||
import axios from 'axios';
|
||||
import type { JSX } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import SplitterLayout from 'react-splitter-layout';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { Loader, QueryErrorPane } from '../../../components';
|
||||
import { Loader, QueryErrorPane, SplitterLayout } from '../../../components';
|
||||
import type { CapacityInfo, DruidEngine, LastExecution, QueryContext } from '../../../druid-models';
|
||||
import { DEFAULT_SERVER_QUERY_CONTEXT, Execution, WorkbenchQuery } from '../../../druid-models';
|
||||
import {
|
||||
|
@ -44,9 +43,9 @@ import {
|
|||
deepGet,
|
||||
DruidError,
|
||||
findAllSqlQueriesInText,
|
||||
localStorageGet,
|
||||
localStorageGetJson,
|
||||
LocalStorageKeys,
|
||||
localStorageSet,
|
||||
localStorageSetJson,
|
||||
QueryManager,
|
||||
} from '../../../utils';
|
||||
import { CapacityAlert } from '../capacity-alert/capacity-alert';
|
||||
|
@ -70,6 +69,10 @@ const queryRunner = new QueryRunner({
|
|||
inflateDateStrategy: 'none',
|
||||
});
|
||||
|
||||
function handleSecondaryPaneSizeChange(secondaryPaneSize: number) {
|
||||
localStorageSetJson(LocalStorageKeys.WORKBENCH_PANE_SIZE, secondaryPaneSize);
|
||||
}
|
||||
|
||||
export interface QueryTabProps
|
||||
extends Pick<
|
||||
RunPanelProps,
|
||||
|
@ -180,15 +183,12 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
);
|
||||
|
||||
function shouldAutoRun(): boolean {
|
||||
if (query.getEffectiveEngine() !== 'sql-native') return false;
|
||||
const effectiveEngine = query.getEffectiveEngine();
|
||||
if (effectiveEngine !== 'sql-native' && effectiveEngine !== 'sql-msq-dart') return false;
|
||||
const queryDuration = executionState.data?.result?.queryDuration;
|
||||
return Boolean(queryDuration && queryDuration < 10000);
|
||||
}
|
||||
|
||||
const handleSecondaryPaneSizeChange = useCallback((secondaryPaneSize: number) => {
|
||||
localStorageSet(LocalStorageKeys.WORKBENCH_PANE_SIZE, String(secondaryPaneSize));
|
||||
}, []);
|
||||
|
||||
const queryInputRef = useRef<FlexibleQueryInput | null>(null);
|
||||
|
||||
const cachedExecutionState = ExecutionStateCache.getState(id);
|
||||
|
@ -296,9 +296,10 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
if (deepGet(query, 'context.fullReport') && dartResponse[0][0] === 'fullReport') {
|
||||
const dartReport = dartResponse[dartResponse.length - 1][0];
|
||||
|
||||
return Execution.fromTaskReport(dartReport)
|
||||
.changeEngine('sql-msq-dart')
|
||||
.changeSqlQuery(query.query, query.context);
|
||||
return Execution.fromDartReport(dartReport).changeSqlQuery(
|
||||
query.query,
|
||||
query.context,
|
||||
);
|
||||
} else {
|
||||
return Execution.fromResult(
|
||||
engine,
|
||||
|
@ -308,7 +309,7 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
query.header,
|
||||
query.typesHeader,
|
||||
query.sqlTypesHeader,
|
||||
),
|
||||
).changeQueryDuration(Date.now() - startTime.valueOf()),
|
||||
).changeSqlQuery(query.query, query.context);
|
||||
}
|
||||
},
|
||||
|
@ -372,6 +373,16 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
|
||||
const execution = executionState.data;
|
||||
|
||||
// This is the execution that would be shown in the output pane, it is either the actual execution or a result
|
||||
// execution that will be shown under the loader
|
||||
const executionToShow =
|
||||
execution ||
|
||||
(() => {
|
||||
if (executionState.intermediate) return;
|
||||
const e = executionState.getSomeData();
|
||||
return e?.result ? e : undefined;
|
||||
})();
|
||||
|
||||
const incrementMetadataVersion = useStore(
|
||||
metadataStateStore,
|
||||
useCallback(state => state.increment, []),
|
||||
|
@ -456,7 +467,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
<SplitterLayout
|
||||
vertical
|
||||
percentage
|
||||
secondaryInitialSize={Number(localStorageGet(LocalStorageKeys.WORKBENCH_PANE_SIZE)!) || 40}
|
||||
secondaryInitialSize={
|
||||
Number(localStorageGetJson(LocalStorageKeys.WORKBENCH_PANE_SIZE)) || 40
|
||||
}
|
||||
primaryMinSize={20}
|
||||
secondaryMinSize={20}
|
||||
onSecondaryPaneSizeChange={handleSecondaryPaneSizeChange}
|
||||
|
@ -525,40 +538,40 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{execution &&
|
||||
(execution.error ? (
|
||||
{executionToShow &&
|
||||
(executionToShow.error ? (
|
||||
<div className="error-container">
|
||||
<ExecutionErrorPane execution={execution} />
|
||||
{execution.stages && (
|
||||
<ExecutionErrorPane execution={executionToShow} />
|
||||
{executionToShow.stages && (
|
||||
<ExecutionStagesPane
|
||||
execution={execution}
|
||||
onErrorClick={() => onDetails(execution, 'error')}
|
||||
onWarningClick={() => onDetails(execution, 'warnings')}
|
||||
execution={executionToShow}
|
||||
onErrorClick={() => onDetails(executionToShow, 'error')}
|
||||
onWarningClick={() => onDetails(executionToShow, 'warnings')}
|
||||
goToTask={goToTask}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : execution.result ? (
|
||||
) : executionToShow.result ? (
|
||||
<ResultTablePane
|
||||
runeMode={execution.engine === 'native'}
|
||||
queryResult={execution.result}
|
||||
runeMode={executionToShow.engine === 'native'}
|
||||
queryResult={executionToShow.result}
|
||||
onQueryAction={handleQueryAction}
|
||||
/>
|
||||
) : execution.isSuccessfulIngest() ? (
|
||||
) : executionToShow.isSuccessfulIngest() ? (
|
||||
<IngestSuccessPane
|
||||
execution={execution}
|
||||
execution={executionToShow}
|
||||
onDetails={onDetails}
|
||||
onQueryTab={onQueryTab}
|
||||
/>
|
||||
) : (
|
||||
<div className="generic-status-container">
|
||||
<div className="generic-status-container-info">
|
||||
{`Execution completed with status: ${execution.status}`}
|
||||
{`Execution completed with status: ${executionToShow.status}`}
|
||||
</div>
|
||||
<ExecutionStagesPane
|
||||
execution={execution}
|
||||
onErrorClick={() => onDetails(execution, 'error')}
|
||||
onWarningClick={() => onDetails(execution, 'warnings')}
|
||||
execution={executionToShow}
|
||||
onErrorClick={() => onDetails(executionToShow, 'error')}
|
||||
onWarningClick={() => onDetails(executionToShow, 'warnings')}
|
||||
goToTask={goToTask}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -37,6 +37,9 @@
|
|||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
padding: 8px 10px;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
|
|
|
@ -19,18 +19,27 @@
|
|||
@import '../../variables';
|
||||
|
||||
$column-tree-width: 250px;
|
||||
$recent-query-task-panel-width: 250px;
|
||||
|
||||
.workbench-view {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.workbench-splitter {
|
||||
height: 100%;
|
||||
|
||||
& > .layout-splitter:hover {
|
||||
background: black;
|
||||
opacity: 0.1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.column-tree {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: $column-tree-width;
|
||||
width: 100%;
|
||||
@include card-like;
|
||||
}
|
||||
|
||||
|
@ -39,7 +48,7 @@ $recent-query-task-panel-width: 250px;
|
|||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: $recent-query-task-panel-width;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
@ -54,8 +63,7 @@ $recent-query-task-panel-width: 250px;
|
|||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
left: $column-tree-width + $thin-padding;
|
||||
right: $recent-query-task-panel-width + $thin-padding;
|
||||
width: 100%;
|
||||
|
||||
.tab-and-tool-bar {
|
||||
position: absolute;
|
||||
|
|
|
@ -32,7 +32,7 @@ import classNames from 'classnames';
|
|||
import copy from 'copy-to-clipboard';
|
||||
import React from 'react';
|
||||
|
||||
import { MenuCheckbox } from '../../components';
|
||||
import { MenuCheckbox, SplitterLayout } from '../../components';
|
||||
import { SpecDialog, StringInputDialog } from '../../dialogs';
|
||||
import type {
|
||||
CapacityInfo,
|
||||
|
@ -87,6 +87,14 @@ import './workbench-view.scss';
|
|||
|
||||
const LAST_DAY = SqlExpression.parse(`__time >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`);
|
||||
|
||||
function handleLeftPaneSizeChange(paneSize: number) {
|
||||
localStorageSetJson(LocalStorageKeys.WORKBENCH_LEFT_SIZE, paneSize);
|
||||
}
|
||||
|
||||
function handleRightPaneSizeChange(paneSize: number) {
|
||||
localStorageSetJson(LocalStorageKeys.WORKBENCH_RIGHT_SIZE, paneSize);
|
||||
}
|
||||
|
||||
function cleanupTabEntry(tabEntry: TabEntry): void {
|
||||
const discardedId = tabEntry.id;
|
||||
WorkbenchRunningPromises.deletePromise(discardedId);
|
||||
|
@ -912,11 +920,28 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
|||
|
||||
const showRightPanel = showRecentQueryTaskPanel || showCurrentDartPanel;
|
||||
return (
|
||||
<div
|
||||
className={classNames('workbench-view app-view', {
|
||||
'hide-column-tree': columnMetadataState.isError(),
|
||||
'hide-right-panel': !showRightPanel,
|
||||
})}
|
||||
<div className="workbench-view app-view">
|
||||
<SplitterLayout
|
||||
className="workbench-splitter"
|
||||
primaryIndex={0}
|
||||
secondaryMinSize={250}
|
||||
secondaryMaxSize={500}
|
||||
secondaryInitialSize={
|
||||
Number(localStorageGetJson(LocalStorageKeys.WORKBENCH_RIGHT_SIZE)) || 250
|
||||
}
|
||||
onSecondaryPaneSizeChange={handleRightPaneSizeChange}
|
||||
primaryMinSize={600}
|
||||
>
|
||||
<SplitterLayout
|
||||
className="workbench-splitter"
|
||||
primaryIndex={1}
|
||||
secondaryMinSize={250}
|
||||
secondaryMaxSize={500}
|
||||
secondaryInitialSize={
|
||||
Number(localStorageGetJson(LocalStorageKeys.WORKBENCH_LEFT_SIZE)) || 250
|
||||
}
|
||||
onSecondaryPaneSizeChange={handleLeftPaneSizeChange}
|
||||
primaryMinSize={400}
|
||||
>
|
||||
{!columnMetadataState.isError() && (
|
||||
<ColumnTree
|
||||
|
@ -931,6 +956,7 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
|||
/>
|
||||
)}
|
||||
{this.renderCenterPanel()}
|
||||
</SplitterLayout>
|
||||
{showRightPanel && (
|
||||
<div className="recent-panel">
|
||||
{showRecentQueryTaskPanel && (
|
||||
|
@ -946,6 +972,8 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
</SplitterLayout>
|
||||
|
||||
{this.renderExecutionDetailsDialog()}
|
||||
{this.renderExplainDialog()}
|
||||
{this.renderHistoryDialog()}
|
||||
|
|
Loading…
Reference in New Issue