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"
|
name: "react-table"
|
||||||
license_category: binary
|
license_category: binary
|
||||||
module: web-console
|
module: web-console
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router": "^5.3.4",
|
"react-router": "^5.3.4",
|
||||||
"react-router-dom": "^5.3.4",
|
"react-router-dom": "^5.3.4",
|
||||||
"react-splitter-layout": "^4.0.0",
|
|
||||||
"react-table": "~6.11.5",
|
"react-table": "~6.11.5",
|
||||||
"regenerator-runtime": "^0.13.7",
|
"regenerator-runtime": "^0.13.7",
|
||||||
"tslib": "^2.8.0",
|
"tslib": "^2.8.0",
|
||||||
|
@ -75,7 +74,6 @@
|
||||||
"@types/react": "^18.3.11",
|
"@types/react": "^18.3.11",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-splitter-layout": "^3.0.5",
|
|
||||||
"@types/react-table": "6.8.5",
|
"@types/react-table": "6.8.5",
|
||||||
"@types/uuid": "^7.0.2",
|
"@types/uuid": "^7.0.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
@ -4064,15 +4062,6 @@
|
||||||
"@types/react-router": "*"
|
"@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": {
|
"node_modules/@types/react-table": {
|
||||||
"version": "6.8.5",
|
"version": "6.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.5.tgz",
|
||||||
|
@ -14964,15 +14953,6 @@
|
||||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/react-table": {
|
||||||
"version": "6.11.5",
|
"version": "6.11.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.11.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.11.5.tgz",
|
||||||
|
@ -21157,15 +21137,6 @@
|
||||||
"@types/react-router": "*"
|
"@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": {
|
"@types/react-table": {
|
||||||
"version": "6.8.5",
|
"version": "6.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.5.tgz",
|
"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": {
|
"react-table": {
|
||||||
"version": "6.11.5",
|
"version": "6.11.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.11.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.11.5.tgz",
|
||||||
|
|
|
@ -83,7 +83,6 @@
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router": "^5.3.4",
|
"react-router": "^5.3.4",
|
||||||
"react-router-dom": "^5.3.4",
|
"react-router-dom": "^5.3.4",
|
||||||
"react-splitter-layout": "^4.0.0",
|
|
||||||
"react-table": "~6.11.5",
|
"react-table": "~6.11.5",
|
||||||
"regenerator-runtime": "^0.13.7",
|
"regenerator-runtime": "^0.13.7",
|
||||||
"tslib": "^2.8.0",
|
"tslib": "^2.8.0",
|
||||||
|
@ -116,7 +115,6 @@
|
||||||
"@types/react": "^18.3.11",
|
"@types/react": "^18.3.11",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-splitter-layout": "^3.0.5",
|
|
||||||
"@types/react-table": "6.8.5",
|
"@types/react-table": "6.8.5",
|
||||||
"@types/uuid": "^7.0.2",
|
"@types/uuid": "^7.0.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
|
|
@ -51,6 +51,7 @@ export * from './segment-timeline/segment-timeline';
|
||||||
export * from './show-json/show-json';
|
export * from './show-json/show-json';
|
||||||
export * from './show-log/show-log';
|
export * from './show-log/show-log';
|
||||||
export * from './show-value/show-value';
|
export * from './show-value/show-value';
|
||||||
|
export * from './splitter-layout/splitter-layout';
|
||||||
export * from './suggestion-menu/suggestion-menu';
|
export * from './suggestion-menu/suggestion-menu';
|
||||||
export * from './supervisor-history-panel/supervisor-history-panel';
|
export * from './supervisor-history-panel/supervisor-history-panel';
|
||||||
export * from './table-cell/table-cell';
|
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 {
|
static fromTaskReport(taskReport: MsqTaskReportResponse): Execution {
|
||||||
// Must have status set for a valid report
|
// Must have status set for a valid report
|
||||||
const id = deepGet(taskReport, 'multiStageQuery.taskId');
|
const id = deepGet(taskReport, 'multiStageQuery.taskId');
|
||||||
|
@ -327,6 +331,7 @@ export class Execution {
|
||||||
new Column({ name: sig.name, nativeType: sig.type, sqlType: sqlTypeNames?.[i] }),
|
new Column({ name: sig.name, nativeType: sig.type, sqlType: sqlTypeNames?.[i] }),
|
||||||
),
|
),
|
||||||
rows: results,
|
rows: results,
|
||||||
|
queryDuration: durationMs,
|
||||||
}).inflateDatesFromSqlTypes();
|
}).inflateDatesFromSqlTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
@import '@blueprintjs/datetime/src/blueprint-datetime';
|
@import '@blueprintjs/datetime/src/blueprint-datetime';
|
||||||
@import '@blueprintjs/datetime2/src/blueprint-datetime2';
|
@import '@blueprintjs/datetime2/src/blueprint-datetime2';
|
||||||
@import '@blueprintjs/select/src/blueprint-select';
|
@import '@blueprintjs/select/src/blueprint-select';
|
||||||
@import 'react-splitter-layout/lib/index';
|
|
||||||
@import './react-table/react-table-base-styles';
|
@import './react-table/react-table-base-styles';
|
||||||
@import './react-table/react-table-extra';
|
@import './react-table/react-table-extra';
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,8 @@ export const LocalStorageKeys = {
|
||||||
WORKBENCH_QUERIES: 'workbench-queries' as const,
|
WORKBENCH_QUERIES: 'workbench-queries' as const,
|
||||||
WORKBENCH_LAST_TAB: 'workbench-last-tab' as const,
|
WORKBENCH_LAST_TAB: 'workbench-last-tab' as const,
|
||||||
WORKBENCH_PANE_SIZE: 'workbench-pane-size' 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_HISTORY: 'workbench-history' as const,
|
||||||
WORKBENCH_TASK_PANEL: 'workbench-task-panel' as const,
|
WORKBENCH_TASK_PANEL: 'workbench-task-panel' as const,
|
||||||
WORKBENCH_DART_PANEL: 'workbench-dart-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;
|
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)>
|
</Memo(ViewControlBar)>
|
||||||
|
<SplitterLayout
|
||||||
|
percentage={true}
|
||||||
|
primaryIndex={1}
|
||||||
|
primaryMinSize={20}
|
||||||
|
secondaryInitialSize={35}
|
||||||
|
secondaryMinSize={10}
|
||||||
|
vertical={true}
|
||||||
|
>
|
||||||
<ReactTable
|
<ReactTable
|
||||||
AggregatedComponent={[Function]}
|
AggregatedComponent={[Function]}
|
||||||
ExpanderComponent={[Function]}
|
ExpanderComponent={[Function]}
|
||||||
|
@ -438,5 +446,6 @@ exports[`DatasourcesView matches snapshot 1`] = `
|
||||||
style={{}}
|
style={{}}
|
||||||
subRowsKey="_subRows"
|
subRowsKey="_subRows"
|
||||||
/>
|
/>
|
||||||
|
</SplitterLayout>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -23,25 +23,30 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.clickable-cell {
|
.splitter-layout {
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ReactTable {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $view-control-bar-height + $standard-padding;
|
top: $view-control-bar-height + $standard-padding;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
& > .layout-splitter:hover {
|
||||||
|
background: black;
|
||||||
|
opacity: 0.1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.show-segment-timeline {
|
|
||||||
.segment-timeline {
|
.segment-timeline {
|
||||||
height: calc(50% - 55px);
|
position: absolute;
|
||||||
margin-top: 10px;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactTable {
|
.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 { FormGroup, InputGroup, Intent, MenuItem, Switch, Tag } from '@blueprintjs/core';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import { SqlQuery, T } from '@druid-toolkit/query';
|
import { SqlQuery, T } from '@druid-toolkit/query';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { sum } from 'd3-array';
|
import { sum } from 'd3-array';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Filter } from 'react-table';
|
import type { Filter } from 'react-table';
|
||||||
|
@ -34,6 +33,7 @@ import {
|
||||||
MoreButton,
|
MoreButton,
|
||||||
RefreshButton,
|
RefreshButton,
|
||||||
SegmentTimeline,
|
SegmentTimeline,
|
||||||
|
SplitterLayout,
|
||||||
TableClickableCell,
|
TableClickableCell,
|
||||||
TableColumnSelector,
|
TableColumnSelector,
|
||||||
type TableColumnSelectorColumn,
|
type TableColumnSelectorColumn,
|
||||||
|
@ -1678,11 +1678,7 @@ GROUP BY 1, 2`;
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="datasources-view app-view">
|
||||||
className={classNames('datasources-view app-view', {
|
|
||||||
'show-segment-timeline': showSegmentTimeline,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ViewControlBar label="Datasources">
|
<ViewControlBar label="Datasources">
|
||||||
<RefreshButton
|
<RefreshButton
|
||||||
onRefresh={auto => {
|
onRefresh={auto => {
|
||||||
|
@ -1717,8 +1713,17 @@ GROUP BY 1, 2`;
|
||||||
tableColumnsHidden={visibleColumns.getHiddenColumns()}
|
tableColumnsHidden={visibleColumns.getHiddenColumns()}
|
||||||
/>
|
/>
|
||||||
</ViewControlBar>
|
</ViewControlBar>
|
||||||
|
<SplitterLayout
|
||||||
|
vertical
|
||||||
|
percentage
|
||||||
|
secondaryInitialSize={35}
|
||||||
|
primaryIndex={1}
|
||||||
|
primaryMinSize={20}
|
||||||
|
secondaryMinSize={10}
|
||||||
|
>
|
||||||
{showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
|
{showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
|
||||||
{this.renderDatasourcesTable()}
|
{this.renderDatasourcesTable()}
|
||||||
|
</SplitterLayout>
|
||||||
{datasourceTableActionDialogId && (
|
{datasourceTableActionDialogId && (
|
||||||
<DatasourceTableActionDialog
|
<DatasourceTableActionDialog
|
||||||
datasource={datasourceTableActionDialogId}
|
datasource={datasourceTableActionDialogId}
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.init-div {
|
.init-pane {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 35vh;
|
margin-top: 35vh;
|
||||||
}
|
}
|
||||||
|
|
|
@ -354,11 +354,13 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
|
||||||
|
|
||||||
if (isLookupsUninitialized(lookupEntriesAndTiersState.error)) {
|
if (isLookupsUninitialized(lookupEntriesAndTiersState.error)) {
|
||||||
return (
|
return (
|
||||||
<div className="init-div">
|
<div className="init-pane">
|
||||||
<Button
|
<Button
|
||||||
icon={IconNames.BUILD}
|
icon={IconNames.BUILD}
|
||||||
text="Initialize lookups"
|
text="Initialize lookups"
|
||||||
onClick={() => void this.initializeLookup()}
|
onClick={() => void this.initializeLookup()}
|
||||||
|
large
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SegmentsView matches snapshot 1`] = `
|
exports[`SegmentsView matches snapshot 1`] = `
|
||||||
<React.Fragment>
|
<div
|
||||||
<div
|
|
||||||
className="segments-view app-view"
|
className="segments-view app-view"
|
||||||
>
|
>
|
||||||
<Memo(ViewControlBar)
|
<Memo(ViewControlBar)
|
||||||
label="Segments"
|
label="Segments"
|
||||||
>
|
>
|
||||||
|
@ -85,6 +84,14 @@ exports[`SegmentsView matches snapshot 1`] = `
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Memo(ViewControlBar)>
|
</Memo(ViewControlBar)>
|
||||||
|
<SplitterLayout
|
||||||
|
percentage={true}
|
||||||
|
primaryIndex={1}
|
||||||
|
primaryMinSize={20}
|
||||||
|
secondaryInitialSize={35}
|
||||||
|
secondaryMinSize={10}
|
||||||
|
vertical={true}
|
||||||
|
>
|
||||||
<ReactTable
|
<ReactTable
|
||||||
AggregatedComponent={[Function]}
|
AggregatedComponent={[Function]}
|
||||||
ExpanderComponent={[Function]}
|
ExpanderComponent={[Function]}
|
||||||
|
@ -453,6 +460,6 @@ exports[`SegmentsView matches snapshot 1`] = `
|
||||||
style={{}}
|
style={{}}
|
||||||
subRowsKey="_subRows"
|
subRowsKey="_subRows"
|
||||||
/>
|
/>
|
||||||
</div>
|
</SplitterLayout>
|
||||||
</React.Fragment>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -22,12 +22,28 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.ReactTable {
|
.splitter-layout {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $view-control-bar-height + $standard-padding;
|
top: $view-control-bar-height + $standard-padding;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
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 {
|
.-totalPages {
|
||||||
display: none;
|
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 { Button, ButtonGroup, Intent, Label, MenuItem, Switch, Tag } from '@blueprintjs/core';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import { C, L, SqlComparison, SqlExpression } from '@druid-toolkit/query';
|
import { C, L, SqlComparison, SqlExpression } from '@druid-toolkit/query';
|
||||||
import classNames from 'classnames';
|
|
||||||
import * as JSONBig from 'json-bigint-native';
|
import * as JSONBig from 'json-bigint-native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Filter } from 'react-table';
|
import type { Filter } from 'react-table';
|
||||||
|
@ -34,6 +33,7 @@ import {
|
||||||
MoreButton,
|
MoreButton,
|
||||||
RefreshButton,
|
RefreshButton,
|
||||||
SegmentTimeline,
|
SegmentTimeline,
|
||||||
|
SplitterLayout,
|
||||||
TableClickableCell,
|
TableClickableCell,
|
||||||
TableColumnSelector,
|
TableColumnSelector,
|
||||||
type TableColumnSelectorColumn,
|
type TableColumnSelectorColumn,
|
||||||
|
@ -969,12 +969,7 @@ END AS "time_span"`,
|
||||||
const { groupByInterval } = this.state;
|
const { groupByInterval } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="segments-view app-view">
|
||||||
<div
|
|
||||||
className={classNames('segments-view app-view', {
|
|
||||||
'show-segment-timeline': showSegmentTimeline,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ViewControlBar label="Segments">
|
<ViewControlBar label="Segments">
|
||||||
<RefreshButton
|
<RefreshButton
|
||||||
onRefresh={auto => {
|
onRefresh={auto => {
|
||||||
|
@ -1025,9 +1020,17 @@ END AS "time_span"`,
|
||||||
tableColumnsHidden={visibleColumns.getHiddenColumns()}
|
tableColumnsHidden={visibleColumns.getHiddenColumns()}
|
||||||
/>
|
/>
|
||||||
</ViewControlBar>
|
</ViewControlBar>
|
||||||
|
<SplitterLayout
|
||||||
|
vertical
|
||||||
|
percentage
|
||||||
|
secondaryInitialSize={35}
|
||||||
|
primaryIndex={1}
|
||||||
|
primaryMinSize={20}
|
||||||
|
secondaryMinSize={10}
|
||||||
|
>
|
||||||
{showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
|
{showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
|
||||||
{this.renderSegmentsTable()}
|
{this.renderSegmentsTable()}
|
||||||
</div>
|
</SplitterLayout>
|
||||||
{this.renderTerminateSegmentAction()}
|
{this.renderTerminateSegmentAction()}
|
||||||
{segmentTableActionDialogId && datasourceTableActionDialogId && (
|
{segmentTableActionDialogId && datasourceTableActionDialogId && (
|
||||||
<SegmentTableActionDialog
|
<SegmentTableActionDialog
|
||||||
|
@ -1044,7 +1047,7 @@ END AS "time_span"`,
|
||||||
onClose={() => this.setState({ showFullShardSpec: undefined })}
|
onClose={() => this.setState({ showFullShardSpec: undefined })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,10 +135,11 @@ export const NumberMenuItems = React.memo(function NumberMenuItems(props: Number
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
<MenuItem icon={IconNames.FUNCTION} text="Aggregate">
|
||||||
{aggregateMenuItem(F('SUM', column), `sum_${columnName}`)}
|
{aggregateMenuItem(F.sum(column), `sum_${columnName}`)}
|
||||||
{aggregateMenuItem(F('MIN', column), `min_${columnName}`)}
|
{aggregateMenuItem(F.min(column), `min_${columnName}`)}
|
||||||
{aggregateMenuItem(F('MAX', column), `max_${columnName}`)}
|
{aggregateMenuItem(F.max(column), `max_${columnName}`)}
|
||||||
{aggregateMenuItem(F('AVG', column), `avg_${columnName}`)}
|
{aggregateMenuItem(F.avg(column), `avg_${columnName}`)}
|
||||||
|
{aggregateMenuItem(F.countDistinct(column), `dist_${columnName}`)}
|
||||||
{aggregateMenuItem(F('APPROX_QUANTILE', column, 0.98), `p98_${columnName}`)}
|
{aggregateMenuItem(F('APPROX_QUANTILE', column, 0.98), `p98_${columnName}`)}
|
||||||
{aggregateMenuItem(F('LATEST', column), `latest_${columnName}`)}
|
{aggregateMenuItem(F('LATEST', column), `latest_${columnName}`)}
|
||||||
</MenuItem>
|
</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_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-and-columns': 'Tables and columns',
|
||||||
'tables-only': 'Tables only',
|
'tables-only': 'Tables only',
|
||||||
'columns-only': 'Columns only',
|
'columns-only': 'Columns only',
|
||||||
|
@ -641,7 +641,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={mode}
|
key={mode}
|
||||||
icon={tickIcon(mode === searchMode)}
|
icon={tickIcon(mode === searchMode)}
|
||||||
text={SEARCH_MDOE_TITLE[mode]}
|
text={SEARCH_MODE_TITLE[mode]}
|
||||||
onClick={() => this.setState({ searchMode: mode })}
|
onClick={() => this.setState({ searchMode: mode })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -142,7 +142,7 @@ export const ExecutionSummaryPanel = React.memo(function ExecutionSummaryPanel(
|
||||||
<Button
|
<Button
|
||||||
key="reset"
|
key="reset"
|
||||||
icon={IconNames.CROSS}
|
icon={IconNames.CROSS}
|
||||||
data-tooltip="Clear output pane"
|
data-tooltip="Clear output"
|
||||||
minimal
|
minimal
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
/>,
|
/>,
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
@import '../../../variables';
|
@import '../../../variables';
|
||||||
|
|
||||||
$vertical-gap: 6px;
|
$vertical-gap: 6px;
|
||||||
|
$visible-splitter-size: 3px;
|
||||||
|
|
||||||
.query-tab {
|
.query-tab {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -27,14 +28,26 @@ $vertical-gap: 6px;
|
||||||
background: $dark-gray2;
|
background: $dark-gray2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitter-layout {
|
.splitter-layout.splitter-layout-vertical {
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.splitter-layout-vertical > .layout-splitter {
|
& > .layout-splitter {
|
||||||
height: 3px;
|
height: 3px + $vertical-gap * 2;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
background-color: $gray1;
|
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;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: $vertical-gap;
|
bottom: 0;
|
||||||
|
|
||||||
.query-section {
|
.query-section {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -84,7 +97,7 @@ $vertical-gap: 6px;
|
||||||
.output-section {
|
.output-section {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: $vertical-gap;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@include card-like;
|
@include card-like;
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,9 @@ import { QueryResult, QueryRunner, SqlQuery } from '@druid-toolkit/query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import SplitterLayout from 'react-splitter-layout';
|
|
||||||
import { useStore } from 'zustand';
|
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 type { CapacityInfo, DruidEngine, LastExecution, QueryContext } from '../../../druid-models';
|
||||||
import { DEFAULT_SERVER_QUERY_CONTEXT, Execution, WorkbenchQuery } from '../../../druid-models';
|
import { DEFAULT_SERVER_QUERY_CONTEXT, Execution, WorkbenchQuery } from '../../../druid-models';
|
||||||
import {
|
import {
|
||||||
|
@ -44,9 +43,9 @@ import {
|
||||||
deepGet,
|
deepGet,
|
||||||
DruidError,
|
DruidError,
|
||||||
findAllSqlQueriesInText,
|
findAllSqlQueriesInText,
|
||||||
localStorageGet,
|
localStorageGetJson,
|
||||||
LocalStorageKeys,
|
LocalStorageKeys,
|
||||||
localStorageSet,
|
localStorageSetJson,
|
||||||
QueryManager,
|
QueryManager,
|
||||||
} from '../../../utils';
|
} from '../../../utils';
|
||||||
import { CapacityAlert } from '../capacity-alert/capacity-alert';
|
import { CapacityAlert } from '../capacity-alert/capacity-alert';
|
||||||
|
@ -70,6 +69,10 @@ const queryRunner = new QueryRunner({
|
||||||
inflateDateStrategy: 'none',
|
inflateDateStrategy: 'none',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleSecondaryPaneSizeChange(secondaryPaneSize: number) {
|
||||||
|
localStorageSetJson(LocalStorageKeys.WORKBENCH_PANE_SIZE, secondaryPaneSize);
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueryTabProps
|
export interface QueryTabProps
|
||||||
extends Pick<
|
extends Pick<
|
||||||
RunPanelProps,
|
RunPanelProps,
|
||||||
|
@ -180,15 +183,12 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
function shouldAutoRun(): boolean {
|
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;
|
const queryDuration = executionState.data?.result?.queryDuration;
|
||||||
return Boolean(queryDuration && queryDuration < 10000);
|
return Boolean(queryDuration && queryDuration < 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSecondaryPaneSizeChange = useCallback((secondaryPaneSize: number) => {
|
|
||||||
localStorageSet(LocalStorageKeys.WORKBENCH_PANE_SIZE, String(secondaryPaneSize));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const queryInputRef = useRef<FlexibleQueryInput | null>(null);
|
const queryInputRef = useRef<FlexibleQueryInput | null>(null);
|
||||||
|
|
||||||
const cachedExecutionState = ExecutionStateCache.getState(id);
|
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') {
|
if (deepGet(query, 'context.fullReport') && dartResponse[0][0] === 'fullReport') {
|
||||||
const dartReport = dartResponse[dartResponse.length - 1][0];
|
const dartReport = dartResponse[dartResponse.length - 1][0];
|
||||||
|
|
||||||
return Execution.fromTaskReport(dartReport)
|
return Execution.fromDartReport(dartReport).changeSqlQuery(
|
||||||
.changeEngine('sql-msq-dart')
|
query.query,
|
||||||
.changeSqlQuery(query.query, query.context);
|
query.context,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return Execution.fromResult(
|
return Execution.fromResult(
|
||||||
engine,
|
engine,
|
||||||
|
@ -308,7 +309,7 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
||||||
query.header,
|
query.header,
|
||||||
query.typesHeader,
|
query.typesHeader,
|
||||||
query.sqlTypesHeader,
|
query.sqlTypesHeader,
|
||||||
),
|
).changeQueryDuration(Date.now() - startTime.valueOf()),
|
||||||
).changeSqlQuery(query.query, query.context);
|
).changeSqlQuery(query.query, query.context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -372,6 +373,16 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
||||||
|
|
||||||
const execution = executionState.data;
|
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(
|
const incrementMetadataVersion = useStore(
|
||||||
metadataStateStore,
|
metadataStateStore,
|
||||||
useCallback(state => state.increment, []),
|
useCallback(state => state.increment, []),
|
||||||
|
@ -456,7 +467,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
||||||
<SplitterLayout
|
<SplitterLayout
|
||||||
vertical
|
vertical
|
||||||
percentage
|
percentage
|
||||||
secondaryInitialSize={Number(localStorageGet(LocalStorageKeys.WORKBENCH_PANE_SIZE)!) || 40}
|
secondaryInitialSize={
|
||||||
|
Number(localStorageGetJson(LocalStorageKeys.WORKBENCH_PANE_SIZE)) || 40
|
||||||
|
}
|
||||||
primaryMinSize={20}
|
primaryMinSize={20}
|
||||||
secondaryMinSize={20}
|
secondaryMinSize={20}
|
||||||
onSecondaryPaneSizeChange={handleSecondaryPaneSizeChange}
|
onSecondaryPaneSizeChange={handleSecondaryPaneSizeChange}
|
||||||
|
@ -525,40 +538,40 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{execution &&
|
{executionToShow &&
|
||||||
(execution.error ? (
|
(executionToShow.error ? (
|
||||||
<div className="error-container">
|
<div className="error-container">
|
||||||
<ExecutionErrorPane execution={execution} />
|
<ExecutionErrorPane execution={executionToShow} />
|
||||||
{execution.stages && (
|
{executionToShow.stages && (
|
||||||
<ExecutionStagesPane
|
<ExecutionStagesPane
|
||||||
execution={execution}
|
execution={executionToShow}
|
||||||
onErrorClick={() => onDetails(execution, 'error')}
|
onErrorClick={() => onDetails(executionToShow, 'error')}
|
||||||
onWarningClick={() => onDetails(execution, 'warnings')}
|
onWarningClick={() => onDetails(executionToShow, 'warnings')}
|
||||||
goToTask={goToTask}
|
goToTask={goToTask}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : execution.result ? (
|
) : executionToShow.result ? (
|
||||||
<ResultTablePane
|
<ResultTablePane
|
||||||
runeMode={execution.engine === 'native'}
|
runeMode={executionToShow.engine === 'native'}
|
||||||
queryResult={execution.result}
|
queryResult={executionToShow.result}
|
||||||
onQueryAction={handleQueryAction}
|
onQueryAction={handleQueryAction}
|
||||||
/>
|
/>
|
||||||
) : execution.isSuccessfulIngest() ? (
|
) : executionToShow.isSuccessfulIngest() ? (
|
||||||
<IngestSuccessPane
|
<IngestSuccessPane
|
||||||
execution={execution}
|
execution={executionToShow}
|
||||||
onDetails={onDetails}
|
onDetails={onDetails}
|
||||||
onQueryTab={onQueryTab}
|
onQueryTab={onQueryTab}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="generic-status-container">
|
<div className="generic-status-container">
|
||||||
<div className="generic-status-container-info">
|
<div className="generic-status-container-info">
|
||||||
{`Execution completed with status: ${execution.status}`}
|
{`Execution completed with status: ${executionToShow.status}`}
|
||||||
</div>
|
</div>
|
||||||
<ExecutionStagesPane
|
<ExecutionStagesPane
|
||||||
execution={execution}
|
execution={executionToShow}
|
||||||
onErrorClick={() => onDetails(execution, 'error')}
|
onErrorClick={() => onDetails(executionToShow, 'error')}
|
||||||
onWarningClick={() => onDetails(execution, 'warnings')}
|
onWarningClick={() => onDetails(executionToShow, 'warnings')}
|
||||||
goToTask={goToTask}
|
goToTask={goToTask}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,6 +37,9 @@
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.close-button {
|
.close-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -19,18 +19,27 @@
|
||||||
@import '../../variables';
|
@import '../../variables';
|
||||||
|
|
||||||
$column-tree-width: 250px;
|
$column-tree-width: 250px;
|
||||||
$recent-query-task-panel-width: 250px;
|
|
||||||
|
|
||||||
.workbench-view {
|
.workbench-view {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
.workbench-splitter {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
& > .layout-splitter:hover {
|
||||||
|
background: black;
|
||||||
|
opacity: 0.1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.column-tree {
|
.column-tree {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: $column-tree-width;
|
width: 100%;
|
||||||
@include card-like;
|
@include card-like;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +48,7 @@ $recent-query-task-panel-width: 250px;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: $recent-query-task-panel-width;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
@ -54,8 +63,7 @@ $recent-query-task-panel-width: 250px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: $column-tree-width + $thin-padding;
|
width: 100%;
|
||||||
right: $recent-query-task-panel-width + $thin-padding;
|
|
||||||
|
|
||||||
.tab-and-tool-bar {
|
.tab-and-tool-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -32,7 +32,7 @@ import classNames from 'classnames';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { MenuCheckbox } from '../../components';
|
import { MenuCheckbox, SplitterLayout } from '../../components';
|
||||||
import { SpecDialog, StringInputDialog } from '../../dialogs';
|
import { SpecDialog, StringInputDialog } from '../../dialogs';
|
||||||
import type {
|
import type {
|
||||||
CapacityInfo,
|
CapacityInfo,
|
||||||
|
@ -87,6 +87,14 @@ import './workbench-view.scss';
|
||||||
|
|
||||||
const LAST_DAY = SqlExpression.parse(`__time >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`);
|
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 {
|
function cleanupTabEntry(tabEntry: TabEntry): void {
|
||||||
const discardedId = tabEntry.id;
|
const discardedId = tabEntry.id;
|
||||||
WorkbenchRunningPromises.deletePromise(discardedId);
|
WorkbenchRunningPromises.deletePromise(discardedId);
|
||||||
|
@ -912,11 +920,28 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
||||||
|
|
||||||
const showRightPanel = showRecentQueryTaskPanel || showCurrentDartPanel;
|
const showRightPanel = showRecentQueryTaskPanel || showCurrentDartPanel;
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="workbench-view app-view">
|
||||||
className={classNames('workbench-view app-view', {
|
<SplitterLayout
|
||||||
'hide-column-tree': columnMetadataState.isError(),
|
className="workbench-splitter"
|
||||||
'hide-right-panel': !showRightPanel,
|
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() && (
|
{!columnMetadataState.isError() && (
|
||||||
<ColumnTree
|
<ColumnTree
|
||||||
|
@ -931,6 +956,7 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.renderCenterPanel()}
|
{this.renderCenterPanel()}
|
||||||
|
</SplitterLayout>
|
||||||
{showRightPanel && (
|
{showRightPanel && (
|
||||||
<div className="recent-panel">
|
<div className="recent-panel">
|
||||||
{showRecentQueryTaskPanel && (
|
{showRecentQueryTaskPanel && (
|
||||||
|
@ -946,6 +972,8 @@ export class WorkbenchView extends React.PureComponent<WorkbenchViewProps, Workb
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</SplitterLayout>
|
||||||
|
|
||||||
{this.renderExecutionDetailsDialog()}
|
{this.renderExecutionDetailsDialog()}
|
||||||
{this.renderExplainDialog()}
|
{this.renderExplainDialog()}
|
||||||
{this.renderHistoryDialog()}
|
{this.renderHistoryDialog()}
|
||||||
|
|
Loading…
Reference in New Issue