You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by cw...@apache.org on 2019/05/06 10:20:20 UTC
[incubator-druid] branch master updated: Improved UI for actions in
task/supervisor table (#7528)
This is an automated email from the ASF dual-hosted git repository.
cwylie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git
The following commit(s) were added to refs/heads/master by this push:
new 83b38c0 Improved UI for actions in task/supervisor table (#7528)
83b38c0 is described below
commit 83b38c0911c6a040308c29dc88a3051d60c68b93
Author: Qi Shu <sh...@gmail.com>
AuthorDate: Mon May 6 06:20:12 2019 -0400
Improved UI for actions in task/supervisor table (#7528)
* Grouped actions in task/supervisor table to a dialog
* Design change
* fix divider
* Removed await for onclick event on buttons
* fix package.json
* Better error handling
* remove log
* name change; better code organization
* Code refactor; extrac table action dialog
* minor change
* tidy up actions
* drop reload actions
---
web-console/package-lock.json | 42 +++++++
web-console/package.json | 4 +
.../bootstrap/react-table-custom-pagination.tsx | 2 +-
web-console/src/components/action-cell.scss | 34 +++++
web-console/src/components/action-cell.tsx | 36 ++++++
web-console/src/components/show-json.scss | 41 ++++++
web-console/src/components/show-json.tsx | 106 ++++++++++++++++
web-console/src/components/show-log.scss | 42 +++++++
web-console/src/components/show-log.tsx | 116 +++++++++++++++++
web-console/src/components/sql-control.tsx | 2 +-
web-console/src/console-application.tsx | 4 +-
web-console/src/dialogs/async-action-dialog.tsx | 5 +-
web-console/src/dialogs/history-dialog.tsx | 2 +-
web-console/src/dialogs/snitch-dialog.tsx | 2 +-
.../src/dialogs/supervisor-table-action-dialog.tsx | 89 +++++++++++++
web-console/src/dialogs/table-action-dialog.scss | 94 ++++++++++++++
web-console/src/dialogs/table-action-dialog.tsx | 86 +++++++++++++
.../src/dialogs/task-table-action-dialog.tsx | 90 +++++++++++++
web-console/src/utils/basic-action.tsx | 61 +++++++++
web-console/src/utils/general.tsx | 10 ++
web-console/src/views/datasource-view.tsx | 116 ++++++++++++++---
web-console/src/views/tasks-view.tsx | 140 ++++++++++++++++-----
22 files changed, 1068 insertions(+), 56 deletions(-)
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 8a6638c..e3ee018 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -498,6 +498,12 @@
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
+ "@types/file-saver": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.0.tgz",
+ "integrity": "sha512-dxdRrUov2HVTbSRFX+7xwUPlbGYVEZK6PrSqClg2QPos3PNe0bCajkDDkDeeC1znjSH03KOEqVbXpnJuWa2wgQ==",
+ "dev": true
+ },
"@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
@@ -591,6 +597,15 @@
"csstype": "^2.2.0"
}
},
+ "@types/react-copy-to-clipboard": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.6.tgz",
+ "integrity": "sha512-v4/yLsuPf8GSFuTy9fA1ABpL5uuy04vwW7qs+cfxSe1UU/M/KK95rF3N3GRseismoK9tA28SvpwVsAg/GWoF3A==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
"@types/react-dom": {
"version": "16.8.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.4.tgz",
@@ -2278,6 +2293,14 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
"dev": true
},
+ "copy-to-clipboard": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz",
+ "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==",
+ "requires": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
"core-js": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
@@ -3538,6 +3561,11 @@
"flat-cache": "^2.0.1"
}
},
+ "file-saver": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.1.tgz",
+ "integrity": "sha512-dCB3K7/BvAcUmtmh1DzFdv0eXSVJ9IAFt1mw3XZfAexodNRoE29l3xB2EX4wH2q8m/UTzwzEPq/ArYk98kUkBQ=="
+ },
"fileset": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz",
@@ -8595,6 +8623,15 @@
"prop-types": "^15.6.2"
}
},
+ "react-copy-to-clipboard": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz",
+ "integrity": "sha512-ELKq31/E3zjFs5rDWNCfFL4NvNFQvGRoJdAKReD/rUPA+xxiLPQmZBZBvy2vgH7V0GE9isIQpT9WXbwIVErYdA==",
+ "requires": {
+ "copy-to-clipboard": "^3",
+ "prop-types": "^15.5.8"
+ }
+ },
"react-dom": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
@@ -10858,6 +10895,11 @@
"repeat-string": "^1.6.1"
}
},
+ "toggle-selection": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+ "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
+ },
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
diff --git a/web-console/package.json b/web-console/package.json
index ad9c0f8..93beecb 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -42,11 +42,13 @@
"druid-console": "^0.0.2",
"es6-shim": "^0.35.5",
"es7-shim": "^6.0.0",
+ "file-saver": "^2.0.1",
"hjson": "^3.1.2",
"lodash.debounce": "^4.0.8",
"numeral": "^2.0.6",
"react": "^16.8.6",
"react-ace": "^6.4.0",
+ "react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.8.6",
"react-router": "^5.0.0",
"react-router-dom": "^5.0.0",
@@ -56,11 +58,13 @@
"devDependencies": {
"@types/classnames": "^2.2.7",
"@types/d3-array": "^2.0.0",
+ "@types/file-saver": "^2.0.0",
"@types/hjson": "^2.4.1",
"@types/jest": "^24.0.11",
"@types/lodash.debounce": "^4.0.6",
"@types/node": "^11.13.4",
"@types/numeral": "^0.0.25",
+ "@types/react-copy-to-clipboard": "^4.2.6",
"@types/react-dom": "^16.8.4",
"@types/react-router-dom": "^4.3.2",
"@types/react-table": "^6.8.1",
diff --git a/web-console/src/bootstrap/react-table-custom-pagination.tsx b/web-console/src/bootstrap/react-table-custom-pagination.tsx
index a55cf51..7225fb9 100644
--- a/web-console/src/bootstrap/react-table-custom-pagination.tsx
+++ b/web-console/src/bootstrap/react-table-custom-pagination.tsx
@@ -100,7 +100,7 @@ export class ReactTableCustomPagination extends React.Component<ReactTableCustom
} = this.props;
return (
- <div className={'-pagination'} style={style}>
+ <div className="-pagination" style={style}>
<div className="-previous">
<Button
fill
diff --git a/web-console/src/components/action-cell.scss b/web-console/src/components/action-cell.scss
new file mode 100644
index 0000000..db722e0
--- /dev/null
+++ b/web-console/src/components/action-cell.scss
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+.action-cell {
+ & > * {
+ margin-right: 10px;
+ color: #48aff0;
+ cursor: pointer;
+
+ &:hover {
+ color: #4891d2;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+}
+
diff --git a/web-console/src/components/action-cell.tsx b/web-console/src/components/action-cell.tsx
new file mode 100644
index 0000000..e37b2c9
--- /dev/null
+++ b/web-console/src/components/action-cell.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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 * as React from 'react';
+
+import './action-cell.scss';
+
+export interface ActionCellProps extends React.Props<any> {
+}
+
+export class ActionCell extends React.Component<ActionCellProps, {}> {
+ constructor(props: ActionCellProps, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ return <div className="action-cell">
+ {this.props.children}
+ </div>;
+ }
+}
diff --git a/web-console/src/components/show-json.scss b/web-console/src/components/show-json.scss
new file mode 100644
index 0000000..c3937a5
--- /dev/null
+++ b/web-console/src/components/show-json.scss
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+.show-json {
+ position: relative;
+ height: 100%;
+
+ .top-actions {
+ text-align: right;
+ padding-bottom: 10px;
+
+ & > * {
+ display: inline-block;
+ }
+ }
+
+ .main-area {
+ height: calc(100% - 40px);
+
+ textarea {
+ height: 100%;
+ width: 100%;
+ resize: none;
+ }
+ }
+}
diff --git a/web-console/src/components/show-json.tsx b/web-console/src/components/show-json.tsx
new file mode 100644
index 0000000..ebc4bfc
--- /dev/null
+++ b/web-console/src/components/show-json.tsx
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Button, ButtonGroup, InputGroup, Intent, TextArea } from '@blueprintjs/core';
+import axios from 'axios';
+import * as React from 'react';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
+
+import { AppToaster } from '../singletons/toaster';
+import { UrlBaser } from '../singletons/url-baser';
+import { downloadFile } from '../utils';
+
+import './show-json.scss';
+
+export interface ShowJsonProps extends React.Props<any> {
+ endpoint: string;
+ downloadFilename?: string;
+}
+
+export interface ShowJsonState {
+ jsonValue: string;
+}
+
+export class ShowJson extends React.Component<ShowJsonProps, ShowJsonState> {
+ constructor(props: ShowJsonProps, context: any) {
+ super(props, context);
+ this.state = {
+ jsonValue: ''
+ };
+
+ this.getJsonInfo();
+ }
+
+ private getJsonInfo = async (): Promise<void> => {
+ const { endpoint } = this.props;
+ try {
+ const resp = await axios.get(endpoint);
+ const data = resp.data;
+ this.setState({
+ jsonValue: typeof (data) === 'string' ? data : JSON.stringify(data, undefined, 2)
+ });
+ } catch (e) {
+ this.setState({
+ jsonValue: `Error: ` + e.response.data
+ });
+ }
+ }
+
+ render() {
+ const { endpoint, downloadFilename } = this.props;
+ const { jsonValue } = this.state;
+
+ return <div className="show-json">
+ <div className="top-actions">
+ <ButtonGroup className="right-buttons">
+ {
+ downloadFilename &&
+ <Button
+ text="Save"
+ minimal
+ onClick={() => downloadFile(jsonValue, 'json', downloadFilename)}
+ />
+ }
+ <CopyToClipboard text={jsonValue}>
+ <Button
+ text="Copy"
+ minimal
+ onClick={() => {
+ AppToaster.show({
+ message: 'Copied JSON to clipboard',
+ intent: Intent.SUCCESS
+ });
+ }}
+ />
+ </CopyToClipboard>
+ <Button
+ text="View raw"
+ minimal
+ onClick={() => window.open(UrlBaser.base(endpoint), '_blank')}
+ />
+ </ButtonGroup>
+ </div>
+ <div className="main-area">
+ <TextArea
+ readOnly
+ value={jsonValue}
+ />
+ </div>
+ </div>;
+ }
+}
diff --git a/web-console/src/components/show-log.scss b/web-console/src/components/show-log.scss
new file mode 100644
index 0000000..58e7acf
--- /dev/null
+++ b/web-console/src/components/show-log.scss
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+.show-log {
+ position: relative;
+ height: 100%;
+
+ .top-actions {
+ text-align: right;
+ padding-bottom: 10px;
+
+ & > * {
+ display: inline-block;
+ }
+ }
+
+ .main-area {
+ height: calc(100% - 40px);
+
+ textarea {
+ height: 100%;
+ width: 100%;
+ resize: none;
+ white-space: pre;
+ }
+ }
+}
diff --git a/web-console/src/components/show-log.tsx b/web-console/src/components/show-log.tsx
new file mode 100644
index 0000000..c89e406
--- /dev/null
+++ b/web-console/src/components/show-log.tsx
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Button, ButtonGroup, InputGroup, Intent, TextArea } from '@blueprintjs/core';
+import axios from 'axios';
+import * as React from 'react';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
+
+import { AppToaster } from '../singletons/toaster';
+import { UrlBaser } from '../singletons/url-baser';
+import { downloadFile } from '../utils';
+
+import './show-log.scss';
+
+function removeFirstPartialLine(log: string): string {
+ const lines = log.split('\n');
+ if (lines.length > 1) {
+ lines.shift();
+ }
+ return lines.join('\n');
+}
+
+export interface ShowLogProps extends React.Props<any> {
+ endpoint: string;
+ downloadFilename?: string;
+ tailOffset?: number;
+}
+
+export interface ShowLogState {
+ logValue: string;
+}
+
+export class ShowLog extends React.Component<ShowLogProps, ShowLogState> {
+ constructor(props: ShowLogProps, context: any) {
+ super(props, context);
+ this.state = {
+ logValue: ''
+ };
+
+ this.getLogInfo();
+ }
+
+ private getLogInfo = async (): Promise<void> => {
+ const { endpoint, tailOffset } = this.props;
+ try {
+ const resp = await axios.get(endpoint + (tailOffset ? `?offset=-${tailOffset}` : ''));
+ const data = resp.data;
+
+ let logValue = typeof (data) === 'string' ? data : JSON.stringify(data, undefined, 2);
+ if (tailOffset) logValue = removeFirstPartialLine(logValue);
+ this.setState({ logValue });
+ } catch (e) {
+ this.setState({
+ logValue: `Error: ` + e.response.data
+ });
+ }
+ }
+
+ render() {
+ const { endpoint, downloadFilename } = this.props;
+ const { logValue } = this.state;
+
+ return <div className="show-log">
+ <div className="top-actions">
+ <ButtonGroup className="right-buttons">
+ {
+ downloadFilename &&
+ <Button
+ text="Save"
+ minimal
+ onClick={() => downloadFile(logValue, 'plain', downloadFilename)}
+ />
+ }
+ <CopyToClipboard text={logValue}>
+ <Button
+ text="Copy"
+ minimal
+ onClick={() => {
+ AppToaster.show({
+ message: 'Copied log to clipboard',
+ intent: Intent.SUCCESS
+ });
+ }}
+ />
+ </CopyToClipboard>
+ <Button
+ text="View full log"
+ minimal
+ onClick={() => window.open(UrlBaser.base(endpoint), '_blank')}
+ />
+ </ButtonGroup>
+ </div>
+ <div className="main-area">
+ <TextArea
+ readOnly
+ value={logValue}
+ />
+ </div>
+ </div>;
+ }
+}
diff --git a/web-console/src/components/sql-control.tsx b/web-console/src/components/sql-control.tsx
index d33f281..e34a3f3 100644
--- a/web-console/src/components/sql-control.tsx
+++ b/web-console/src/components/sql-control.tsx
@@ -302,7 +302,7 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
</ButtonGroup>
{
queryElapsed &&
- <span className={'query-elapsed'}> Last query took {(queryElapsed / 1000).toFixed(2)} seconds</span>
+ <span className="query-elapsed"> Last query took {(queryElapsed / 1000).toFixed(2)} seconds</span>
}
</div>
</div>;
diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx
index 8691939..4f10977 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -240,9 +240,9 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
const { capabilitiesLoading } = this.state;
if (capabilitiesLoading) {
- return <div className={'loading-capabilities'}>
+ return <div className="loading-capabilities">
<Loader
- loadingText={''}
+ loadingText=""
loading={capabilitiesLoading}
/>
</div>;
diff --git a/web-console/src/dialogs/async-action-dialog.tsx b/web-console/src/dialogs/async-action-dialog.tsx
index c29d058..1cfe144 100644
--- a/web-console/src/dialogs/async-action-dialog.tsx
+++ b/web-console/src/dialogs/async-action-dialog.tsx
@@ -34,6 +34,7 @@ export interface AsyncAlertDialogProps extends React.Props<any> {
action: null | (() => Promise<void>);
onClose: (success: boolean) => void;
confirmButtonText: string;
+ confirmButtonDisabled?: boolean;
cancelButtonText?: string;
className?: string;
icon?: IconName;
@@ -79,7 +80,7 @@ export class AsyncActionDialog extends React.Component<AsyncAlertDialogProps, As
}
render() {
- const { action, onClose, className, icon, intent, confirmButtonText, cancelButtonText, children } = this.props;
+ const { action, onClose, className, icon, intent, confirmButtonText, cancelButtonText, confirmButtonDisabled, children } = this.props;
const { working } = this.state;
if (!action) return null;
@@ -99,7 +100,7 @@ export class AsyncActionDialog extends React.Component<AsyncAlertDialogProps, As
working ?
<ProgressBar/> :
<div className={Classes.ALERT_FOOTER}>
- <Button intent={intent} text={confirmButtonText} onClick={this.handleConfirm}/>
+ <Button intent={intent} text={confirmButtonText} onClick={this.handleConfirm} disabled={confirmButtonDisabled}/>
<Button text={cancelButtonText || 'Cancel'} onClick={handleClose}/>
</div>
}
diff --git a/web-console/src/dialogs/history-dialog.tsx b/web-console/src/dialogs/history-dialog.tsx
index 9a860e6..5704176 100644
--- a/web-console/src/dialogs/history-dialog.tsx
+++ b/web-console/src/dialogs/history-dialog.tsx
@@ -46,7 +46,7 @@ export class HistoryDialog extends React.Component<HistoryDialogProps, HistoryDi
content = <div className="no-record">No history records available</div>;
} else {
content = <>
- <span className={'history-dialog-title'}>History</span>
+ <span className="history-dialog-title">History</span>
<div className="history-record-entries">
{
historyRecords.map((record: any) => {
diff --git a/web-console/src/dialogs/snitch-dialog.tsx b/web-console/src/dialogs/snitch-dialog.tsx
index 91b94e7..bfa5a76 100644
--- a/web-console/src/dialogs/snitch-dialog.tsx
+++ b/web-console/src/dialogs/snitch-dialog.tsx
@@ -127,7 +127,7 @@ export class SnitchDialog extends React.Component<SnitchDialogProps, SnitchDialo
const { historyRecords } = this.props;
return <HistoryDialog
{...this.props}
- className={'history-dialog'}
+ className="history-dialog"
historyRecords={historyRecords}
>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
diff --git a/web-console/src/dialogs/supervisor-table-action-dialog.tsx b/web-console/src/dialogs/supervisor-table-action-dialog.tsx
new file mode 100644
index 0000000..3c40f5d
--- /dev/null
+++ b/web-console/src/dialogs/supervisor-table-action-dialog.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 { IDialogProps } from '@blueprintjs/core';
+import * as React from 'react';
+
+import { ShowJson } from '../components/show-json';
+import { BasicAction, basicActionsToButtons } from '../utils/basic-action';
+
+import { SideButtonMetaData, TableActionDialog } from './table-action-dialog';
+
+interface SupervisorTableActionDialogProps extends IDialogProps {
+ supervisorId: string;
+ actions: BasicAction[];
+ onClose: () => void;
+}
+
+interface SupervisorTableActionDialogState {
+ activeTab: 'payload' | 'status' | 'stats' | 'history';
+}
+
+export class SupervisorTableActionDialog extends React.Component<SupervisorTableActionDialogProps, SupervisorTableActionDialogState> {
+ constructor(props: SupervisorTableActionDialogProps) {
+ super(props);
+ this.state = {
+ activeTab: 'payload'
+ };
+ }
+
+ render(): React.ReactNode {
+ const { supervisorId, actions, onClose } = this.props;
+ const { activeTab } = this.state;
+
+ const supervisorTableSideButtonMetadata: SideButtonMetaData[] = [
+ {
+ icon: 'align-left',
+ text: 'Payload',
+ active: activeTab === 'payload',
+ onClick: () => this.setState({ activeTab: 'payload' })
+ },
+ {
+ icon: 'dashboard',
+ text: 'Status',
+ active: activeTab === 'status',
+ onClick: () => this.setState({ activeTab: 'status' })
+ },
+ {
+ icon: 'chart',
+ text: 'Statistics',
+ active: activeTab === 'stats',
+ onClick: () => this.setState({ activeTab: 'stats' })
+ },
+ {
+ icon: 'history',
+ text: 'History',
+ active: activeTab === 'history',
+ onClick: () => this.setState({ activeTab: 'history' })
+ }
+ ];
+
+ return <TableActionDialog
+ isOpen
+ sideButtonMetadata={supervisorTableSideButtonMetadata}
+ onClose={onClose}
+ title={`Supervisor: ${supervisorId}`}
+ bottomButtons={basicActionsToButtons(actions)}
+ >
+ {activeTab === 'payload' && <ShowJson endpoint={`/druid/indexer/v1/supervisor/${supervisorId}`} downloadFilename={`supervisor-payload-${supervisorId}.json`}/>}
+ {activeTab === 'status' && <ShowJson endpoint={`/druid/indexer/v1/supervisor/${supervisorId}/status`} downloadFilename={`supervisor-status-${supervisorId}.json`}/>}
+ {activeTab === 'stats' && <ShowJson endpoint={`/druid/indexer/v1/supervisor/${supervisorId}/stats`} downloadFilename={`supervisor-stats-${supervisorId}.json`}/>}
+ {activeTab === 'history' && <ShowJson endpoint={`/druid/indexer/v1/supervisor/${supervisorId}/history`} downloadFilename={`supervisor-history-${supervisorId}.json`}/>}
+ </TableActionDialog>;
+ }
+}
diff --git a/web-console/src/dialogs/table-action-dialog.scss b/web-console/src/dialogs/table-action-dialog.scss
new file mode 100644
index 0000000..a1e5d49
--- /dev/null
+++ b/web-console/src/dialogs/table-action-dialog.scss
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+$side-bar-width: 120px;
+
+.table-action-dialog {
+ &.bp3-dialog {
+ position: relative;
+ width: 700px;
+ top: 5%;
+ height: 70vh;
+
+ &::after {
+ content: "";
+ position: absolute;
+ width: 0;
+ top: 40px;
+ bottom: 0;
+ left: $side-bar-width;
+ border-right: 1px solid #1f2832;
+ }
+ }
+
+ .bp3-dialog-body {
+ position: relative;
+ margin: 0;
+
+ .side-bar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 120px;
+ height: 100%;
+
+ .tab-button {
+ width: 100%;
+ height: 10vh;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ border-radius: 0;
+
+ &.active {
+ background-color: #2C74A8;
+ }
+
+ .bp3-icon {
+ margin-right: 0;
+ }
+
+ .bp3-button-text {
+ margin-top: 5px;
+ }
+ }
+ }
+
+ .main-section {
+ position: absolute;
+ top: 0;
+ left: $side-bar-width;
+ right: 0;
+ height: 100%;
+ padding: 10px 20px;
+ }
+ }
+
+ .bp3-dialog-footer {
+ position: relative;
+
+ .footer-actions-left {
+ position: absolute;
+ left: $side-bar-width;
+
+ & > * {
+ margin-right: 10px;
+ }
+ }
+ }
+}
diff --git a/web-console/src/dialogs/table-action-dialog.tsx b/web-console/src/dialogs/table-action-dialog.tsx
new file mode 100644
index 0000000..5870cea
--- /dev/null
+++ b/web-console/src/dialogs/table-action-dialog.tsx
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Button, Classes, Dialog, Icon, IconName, IDialogProps, Intent } from '@blueprintjs/core';
+import * as React from 'react';
+
+import './table-action-dialog.scss';
+
+export interface SideButtonMetaData {
+ icon: IconName;
+ text: string;
+ active?: boolean;
+ onClick?: () => void;
+}
+
+interface TableActionDialogProps extends IDialogProps {
+ sideButtonMetadata: SideButtonMetaData[];
+ onClose: () => void;
+ bottomButtons?: React.ReactNode;
+}
+
+export class TableActionDialog extends React.Component<TableActionDialogProps, {}> {
+ constructor(props: TableActionDialogProps) {
+ super(props);
+ this.state = {};
+ }
+
+ render() {
+ const { sideButtonMetadata, isOpen, onClose, title, bottomButtons } = this.props;
+
+ return <Dialog
+ className="table-action-dialog"
+ isOpen={isOpen}
+ onClose={onClose}
+ title={title}
+ >
+ <div className={Classes.DIALOG_BODY}>
+ <div className="side-bar">
+ {
+ sideButtonMetadata.map((d: SideButtonMetaData) => (
+ <Button
+ className="tab-button"
+ icon={<Icon icon={d.icon} iconSize={20}/>}
+ key={d.text}
+ text={d.text}
+ intent={d.active ? Intent.PRIMARY : Intent.NONE}
+ minimal={!d.active}
+ onClick={d.onClick}
+ />
+ ))
+ }
+ </div>
+ <div className="main-section">
+ {this.props.children}
+ </div>
+ </div>
+ <div className={Classes.DIALOG_FOOTER}>
+ <div className="footer-actions-left">
+ {bottomButtons}
+ </div>
+ <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+ <Button
+ text="Close"
+ intent={Intent.PRIMARY}
+ onClick={onClose}
+ />
+ </div>
+ </div>
+ </Dialog>;
+ }
+}
diff --git a/web-console/src/dialogs/task-table-action-dialog.tsx b/web-console/src/dialogs/task-table-action-dialog.tsx
new file mode 100644
index 0000000..b5518e6
--- /dev/null
+++ b/web-console/src/dialogs/task-table-action-dialog.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 { IDialogProps } from '@blueprintjs/core';
+import * as React from 'react';
+
+import { ShowJson } from '../components/show-json';
+import { ShowLog } from '../components/show-log';
+import { BasicAction, basicActionsToButtons } from '../utils/basic-action';
+
+import { SideButtonMetaData, TableActionDialog } from './table-action-dialog';
+
+interface TaskTableActionDialogProps extends IDialogProps {
+ taskId: string;
+ actions: BasicAction[];
+ onClose: () => void;
+}
+
+interface TaskTableActionDialogState {
+ activeTab: 'payload' | 'status' | 'reports' | 'log';
+}
+
+export class TaskTableActionDialog extends React.Component<TaskTableActionDialogProps, TaskTableActionDialogState> {
+ constructor(props: TaskTableActionDialogProps) {
+ super(props);
+ this.state = {
+ activeTab: 'payload'
+ };
+ }
+
+ render(): React.ReactNode {
+ const { taskId, actions, onClose } = this.props;
+ const { activeTab } = this.state;
+
+ const taskTableSideButtonMetadata: SideButtonMetaData[] = [
+ {
+ icon: 'align-left',
+ text: 'Payload',
+ active: activeTab === 'payload',
+ onClick: () => this.setState({ activeTab: 'payload' })
+ },
+ {
+ icon: 'dashboard',
+ text: 'Status',
+ active: activeTab === 'status',
+ onClick: () => this.setState({ activeTab: 'status' })
+ },
+ {
+ icon: 'comparison',
+ text: 'Reports',
+ active: activeTab === 'reports',
+ onClick: () => this.setState({ activeTab: 'reports' })
+ },
+ {
+ icon: 'align-justify',
+ text: 'Logs',
+ active: activeTab === 'log',
+ onClick: () => this.setState({ activeTab: 'log' })
+ }
+ ];
+
+ return <TableActionDialog
+ isOpen
+ sideButtonMetadata={taskTableSideButtonMetadata}
+ onClose={onClose}
+ title={`Task: ${taskId}`}
+ bottomButtons={basicActionsToButtons(actions)}
+ >
+ {activeTab === 'payload' && <ShowJson endpoint={`/druid/indexer/v1/task/${taskId}`} downloadFilename={`task-payload-${taskId}.json`}/>}
+ {activeTab === 'status' && <ShowJson endpoint={`/druid/indexer/v1/task/${taskId}/status`} downloadFilename={`task-status-${taskId}.json`}/>}
+ {activeTab === 'reports' && <ShowJson endpoint={`/druid/indexer/v1/task/${taskId}/reports`} downloadFilename={`task-reports-${taskId}.json`}/>}
+ {activeTab === 'log' && <ShowLog endpoint={`/druid/indexer/v1/task/${taskId}/log`} downloadFilename={`task-log-${taskId}.json`} tailOffset={16000}/>}
+ </TableActionDialog>;
+ }
+}
diff --git a/web-console/src/utils/basic-action.tsx b/web-console/src/utils/basic-action.tsx
new file mode 100644
index 0000000..c5293d9
--- /dev/null
+++ b/web-console/src/utils/basic-action.tsx
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Button, IconName, Intent, Menu, MenuItem } from '@blueprintjs/core';
+import * as React from 'react';
+
+export interface BasicAction {
+ icon?: IconName;
+ title: string;
+ intent?: Intent;
+ onAction: () => void;
+}
+
+export function basicActionsToMenu(basicActions: BasicAction[]) {
+ if (!basicActions.length) return null;
+ return <Menu>
+ {
+ basicActions.map((action) => (
+ <MenuItem
+ key={action.title}
+ icon={action.icon}
+ text={action.title}
+ intent={action.intent}
+ onClick={action.onAction}
+ />
+ ))
+ }
+ </Menu>;
+}
+
+export function basicActionsToButtons(basicActions: BasicAction[]) {
+ if (!basicActions.length) return null;
+ return <>
+ {
+ basicActions.map((action) => (
+ <Button
+ key={action.title}
+ icon={action.icon}
+ text={action.title}
+ intent={action.intent}
+ onClick={action.onAction}
+ />
+ ))
+ }
+ </>;
+}
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index c69ef32..599ce1c 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -18,6 +18,7 @@
import { Button, HTMLSelect, InputGroup, Intent } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
+import * as FileSaver from 'file-saver';
import * as numeral from 'numeral';
import * as React from 'react';
import { Filter, FilterRender } from 'react-table';
@@ -216,3 +217,12 @@ export function sortWithPrefixSuffix(things: string[], prefix: string[], suffix:
const post = things.filter((x) => suffix.includes(x)).sort();
return pre.concat(mid, post);
}
+
+// ----------------------------
+
+export function downloadFile(text: string, type: string, fileName: string): void {
+ const blob = new Blob([text], {
+ type: `text/${type}`
+ });
+ FileSaver.saveAs(blob, fileName);
+}
diff --git a/web-console/src/views/datasource-view.tsx b/web-console/src/views/datasource-view.tsx
index e08cd45..1b4b1e3 100644
--- a/web-console/src/views/datasource-view.tsx
+++ b/web-console/src/views/datasource-view.tsx
@@ -16,12 +16,14 @@
* limitations under the License.
*/
-import { Button, Intent, Switch } from '@blueprintjs/core';
+import { Button, Icon, InputGroup, Intent, Popover, Position, Switch } from '@blueprintjs/core';
+import { FormGroup } from '@blueprintjs/core/lib/esnext';
import { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import * as React from 'react';
import ReactTable, { Filter } from 'react-table';
+import { ActionCell } from '../components/action-cell';
import { RuleEditor } from '../components/rule-editor';
import { TableColumnSelection } from '../components/table-column-selection';
import { ViewControlBar } from '../components/view-control-bar';
@@ -40,6 +42,7 @@ import {
queryDruidSql,
QueryManager, TableColumnSelectionHandler
} from '../utils';
+import { BasicAction, basicActionsToMenu } from '../utils/basic-action';
import './datasource-view.scss';
@@ -80,7 +83,9 @@ export interface DatasourcesViewState {
dropDataDatasource: string | null;
enableDatasource: string | null;
killDatasource: string | null;
-
+ dropReloadDatasource: string | null;
+ dropReloadAction: 'drop' | 'reload';
+ dropReloadInterval: string;
}
export class DatasourcesView extends React.Component<DatasourcesViewProps, DatasourcesViewState> {
@@ -116,8 +121,10 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas
compactionDialogOpenOn: null,
dropDataDatasource: null,
enableDatasource: null,
- killDatasource: null
-
+ killDatasource: null,
+ dropReloadDatasource: null,
+ dropReloadAction: 'drop',
+ dropReloadInterval: ''
};
this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
@@ -250,6 +257,46 @@ GROUP BY 1`);
</AsyncActionDialog>;
}
+ renderDropReloadAction() {
+ const { dropReloadDatasource, dropReloadAction, dropReloadInterval } = this.state;
+ const isDrop = dropReloadAction === 'drop';
+
+ return <AsyncActionDialog
+ action={
+ dropReloadDatasource ? async () => {
+ if (!dropReloadInterval) return;
+ const resp = await axios.post(`/druid/coordinator/v1/datasources/${dropReloadDatasource}/${isDrop ? 'markUnused' : 'markUsed'}`, {
+ interval: dropReloadInterval
+ });
+ return resp.data;
+ } : null
+ }
+ confirmButtonText={`${isDrop ? 'Drop' : 'Reload'} selected data`}
+ confirmButtonDisabled={!/.\/./.test(dropReloadInterval)}
+ successText={`${isDrop ? 'Drop' : 'Reload'} request submitted`}
+ failText={`Could not ${isDrop ? 'drop' : 'reload'} data`}
+ intent={Intent.PRIMARY}
+ onClose={(success) => {
+ this.setState({ dropReloadDatasource: null });
+ if (success) this.datasourceQueryManager.rerunLastQuery();
+ }}
+ >
+ <p>
+ {`Please select the interval that you want to ${isDrop ? 'drop' : 'reload'}?`}
+ </p>
+ <FormGroup>
+ <InputGroup
+ value={dropReloadInterval}
+ onChange={(e: any) => {
+ const v = e.target.value;
+ this.setState({ dropReloadInterval: v.toUpperCase() });
+ }}
+ placeholder="2018-01-01T00:00:00/2018-01-03T00:00:00"
+ />
+ </FormGroup>
+ </AsyncActionDialog>;
+ }
+
renderKillAction() {
const { killDatasource } = this.state;
@@ -354,6 +401,43 @@ GROUP BY 1`);
});
}
+ getDatasourceActions(datasource: string, disabled: boolean): BasicAction[] {
+ if (disabled) {
+ return [
+ {
+ icon: IconNames.EXPORT,
+ title: 'Enable',
+ onAction: () => this.setState({ enableDatasource: datasource })
+ },
+ {
+ icon: IconNames.TRASH,
+ title: 'Permanently delete (kill task)',
+ intent: Intent.DANGER,
+ onAction: () => this.setState({ killDatasource: datasource })
+ }
+ ];
+ } else {
+ return [
+ {
+ icon: IconNames.EXPORT,
+ title: 'Reload data by interval',
+ onAction: () => this.setState({ dropReloadDatasource: datasource, dropReloadAction: 'reload' })
+ },
+ {
+ icon: IconNames.IMPORT,
+ title: 'Drop data by interval',
+ onAction: () => this.setState({ dropReloadDatasource: datasource, dropReloadAction: 'drop' })
+ },
+ {
+ icon: IconNames.IMPORT,
+ title: 'Drop datasource (disable)',
+ intent: Intent.DANGER,
+ onAction: () => this.setState({ dropDataDatasource: datasource })
+ }
+ ];
+ }
+ }
+
renderRetentionDialog() {
const { retentionDialogOpenOn, tiers } = this.state;
if (!retentionDialogOpenOn) return null;
@@ -527,21 +611,22 @@ GROUP BY 1`);
Header: 'Actions',
accessor: 'datasource',
id: 'actions',
- width: 160,
+ width: 70,
filterable: false,
Cell: row => {
const datasource = row.value;
const { disabled } = row.original;
- if (disabled) {
- return <div>
- <a onClick={() => this.setState({ enableDatasource: datasource })}>Enable</a>
- <a onClick={() => this.setState({ killDatasource: datasource })}>Permanently delete</a>
- </div>;
- } else {
- return <div>
- <a onClick={() => this.setState({ dropDataDatasource: datasource })}>Drop data</a>
- </div>;
- }
+ const datasourceActions = this.getDatasourceActions(datasource, disabled);
+ const datasourceMenu = basicActionsToMenu(datasourceActions);
+
+ return <ActionCell>
+ {
+ datasourceMenu &&
+ <Popover content={datasourceMenu} position={Position.BOTTOM_RIGHT}>
+ <Icon icon={IconNames.WRENCH}/>
+ </Popover>
+ }
+ </ActionCell>;
},
show: tableColumnSelectionHandler.showColumn('Actions')
}
@@ -551,6 +636,7 @@ GROUP BY 1`);
/>
{this.renderDropDataAction()}
{this.renderEnableAction()}
+ {this.renderDropReloadAction()}
{this.renderKillAction()}
{this.renderRetentionDialog()}
{this.renderCompactionDialog()}
diff --git a/web-console/src/views/tasks-view.tsx b/web-console/src/views/tasks-view.tsx
index b5a412a..8acee10 100644
--- a/web-console/src/views/tasks-view.tsx
+++ b/web-console/src/views/tasks-view.tsx
@@ -16,31 +16,31 @@
* limitations under the License.
*/
-import { Alert, Button, ButtonGroup, Intent, Label, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
+import { Alert, Button, ButtonGroup, Icon, Intent, Label, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import * as React from 'react';
import ReactTable from 'react-table';
import { Filter } from 'react-table';
+import { ActionCell } from '../components/action-cell';
import { TableColumnSelection } from '../components/table-column-selection';
import { ViewControlBar } from '../components/view-control-bar';
import { AsyncActionDialog } from '../dialogs/async-action-dialog';
import { SpecDialog } from '../dialogs/spec-dialog';
+import { SupervisorTableActionDialog } from '../dialogs/supervisor-table-action-dialog';
+import { TaskTableActionDialog } from '../dialogs/task-table-action-dialog';
import { AppToaster } from '../singletons/toaster';
-import { UrlBaser } from '../singletons/url-baser';
import {
addFilter,
booleanCustomTableFilter,
countBy,
formatDuration,
- getDruidErrorMessage, localStorageGet, LocalStorageKeys,
+ getDruidErrorMessage, LocalStorageKeys,
queryDruidSql,
QueryManager, TableColumnSelectionHandler
} from '../utils';
-import { IngestionType } from '../utils/ingestion-spec';
-
-import { LoadDataViewSeed } from './load-data-view';
+import { BasicAction, basicActionsToMenu } from '../utils/basic-action';
import './tasks-view.scss';
@@ -77,6 +77,11 @@ export interface TasksViewState {
taskSpecDialogOpen: boolean;
initSpec: any;
alertErrorMsg: string | null;
+
+ taskTableActionDialogId: string | null;
+ taskTableActionDialogActions: BasicAction[];
+ supervisorTableActionDialogId: string | null;
+ supervisorTableActionDialogActions: BasicAction[];
}
interface TaskQueryResultRow {
@@ -137,8 +142,12 @@ export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
supervisorSpecDialogOpen: false,
taskSpecDialogOpen: false,
initSpec: null,
- alertErrorMsg: null
+ alertErrorMsg: null,
+ taskTableActionDialogId: null,
+ taskTableActionDialogActions: [],
+ supervisorTableActionDialogId: null,
+ supervisorTableActionDialogActions: []
};
this.supervisorTableColumnSelectionHandler = new TableColumnSelectionHandler(
@@ -274,6 +283,27 @@ ORDER BY "rank" DESC, "created_time" DESC`);
this.taskQueryManager.rerunLastQuery();
}
+ private getSupervisorActions(id: string, supervisorSuspended: boolean): BasicAction[] {
+ return [
+ {
+ icon: IconNames.STEP_BACKWARD,
+ title: 'Reset',
+ onAction: () => this.setState({ resetSupervisorId: id })
+ },
+ {
+ icon: supervisorSuspended ? IconNames.PLAY : IconNames.PAUSE,
+ title: supervisorSuspended ? 'Resume' : 'Suspend',
+ onAction: () => supervisorSuspended ? this.setState({ resumeSupervisorId: id }) : this.setState({ suspendSupervisorId: id })
+ },
+ {
+ icon: IconNames.CROSS,
+ title: 'Terminate',
+ intent: Intent.DANGER,
+ onAction: () => this.setState({ terminateSupervisorId: id })
+ }
+ ];
+ }
+
renderResumeSupervisorAction() {
const { resumeSupervisorId } = this.state;
@@ -440,23 +470,29 @@ ORDER BY "rank" DESC, "created_time" DESC`);
Header: 'Actions',
id: 'actions',
accessor: 'id',
- width: 420,
+ width: 70,
filterable: false,
Cell: row => {
const id = row.value;
- const suspendResume = row.original.spec.suspended ?
- <a onClick={() => this.setState({ resumeSupervisorId: id })}>Resume</a> :
- <a onClick={() => this.setState({ suspendSupervisorId: id })}>Suspend</a>;
-
- return <div>
- <a href={UrlBaser.base(`/druid/indexer/v1/supervisor/${id}`)} target="_blank">Payload</a>
- <a href={UrlBaser.base(`/druid/indexer/v1/supervisor/${id}/status`)} target="_blank">Status</a>
- <a href={UrlBaser.base(`/druid/indexer/v1/supervisor/${id}/stats`)} target="_blank">Stats</a>
- <a href={UrlBaser.base(`/druid/indexer/v1/supervisor/${id}/history`)} target="_blank">History</a>
- {suspendResume}
- <a onClick={() => this.setState({ resetSupervisorId: id })}>Reset</a>
- <a onClick={() => this.setState({ terminateSupervisorId: id })}>Terminate</a>
- </div>;
+ const supervisorSuspended = row.original.spec.suspended;
+ const supervisorActions = this.getSupervisorActions(id, supervisorSuspended);
+ const supervisorMenu = basicActionsToMenu(supervisorActions);
+
+ return <ActionCell>
+ <Icon
+ icon={IconNames.SEARCH_TEMPLATE}
+ onClick={() => this.setState({
+ supervisorTableActionDialogId: id,
+ supervisorTableActionDialogActions: supervisorActions
+ })}
+ />
+ {
+ supervisorMenu &&
+ <Popover content={supervisorMenu} position={Position.BOTTOM_RIGHT}>
+ <Icon icon={IconNames.WRENCH}/>
+ </Popover>
+ }
+ </ActionCell>;
},
show: supervisorTableColumnSelectionHandler.showColumn('Actions')
}
@@ -473,6 +509,18 @@ ORDER BY "rank" DESC, "created_time" DESC`);
// --------------------------------------
+ private getTaskActions(id: string, status: string): BasicAction[] {
+ if (status !== 'RUNNING' && status !== 'WAITING' && status !== 'PENDING') return [];
+ return [
+ {
+ icon: IconNames.CROSS,
+ title: 'Kill',
+ intent: Intent.DANGER,
+ onAction: () => this.setState({ killTaskId: id })
+ }
+ ];
+ }
+
renderKillTaskAction() {
const { killTaskId } = this.state;
@@ -601,20 +649,30 @@ ORDER BY "rank" DESC, "created_time" DESC`);
Header: 'Actions',
id: 'actions',
accessor: 'task_id',
- width: 360,
+ width: 70,
filterable: false,
Cell: row => {
if (row.aggregated) return '';
const id = row.value;
const { status } = row.original;
- return <div>
- <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}`)} target="_blank">Payload</a>
- <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}/status`)} target="_blank">Status</a>
- <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}/reports`)} target="_blank">Reports</a>
- <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}/log`)} target="_blank">Log (all)</a>
- <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}/log?offset=-8192`)} target="_blank">Log (last 8kb)</a>
- {(status === 'RUNNING' || status === 'WAITING' || status === 'PENDING') && <a onClick={() => this.setState({ killTaskId: id })}>Kill</a>}
- </div>;
+ const taskActions = this.getTaskActions(id, status);
+ const taskMenu = basicActionsToMenu(taskActions);
+
+ return <ActionCell>
+ <Icon
+ icon={IconNames.SEARCH_TEMPLATE}
+ onClick={() => this.setState({
+ taskTableActionDialogId: id,
+ taskTableActionDialogActions: taskActions
+ })}
+ />
+ {
+ taskMenu &&
+ <Popover content={taskMenu} position={Position.BOTTOM_RIGHT}>
+ <Icon icon={IconNames.WRENCH}/>
+ </Popover>
+ }
+ </ActionCell>;
},
Aggregated: row => '',
show: taskTableColumnSelectionHandler.showColumn('Actions')
@@ -629,7 +687,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
render() {
const { goToSql, goToLoadDataView, noSqlMode } = this.props;
- const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, initSpec, alertErrorMsg } = this.state;
+ const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg, taskTableActionDialogId, taskTableActionDialogActions, supervisorTableActionDialogId, supervisorTableActionDialogActions } = this.state;
const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
const submitTaskMenu = <Menu>
@@ -702,7 +760,6 @@ ORDER BY "rank" DESC, "created_time" DESC`);
onClose={this.closeSpecDialogs}
onSubmit={this.submitSupervisor}
title="Submit supervisor"
- initSpec={initSpec}
/>
}
{
@@ -711,7 +768,6 @@ ORDER BY "rank" DESC, "created_time" DESC`);
onClose={this.closeSpecDialogs}
onSubmit={this.submitTask}
title="Submit task"
- initSpec={initSpec}
/>
}
<Alert
@@ -723,6 +779,24 @@ ORDER BY "rank" DESC, "created_time" DESC`);
>
<p>{alertErrorMsg}</p>
</Alert>
+ {
+ supervisorTableActionDialogId &&
+ <SupervisorTableActionDialog
+ isOpen
+ supervisorId={supervisorTableActionDialogId}
+ actions={supervisorTableActionDialogActions}
+ onClose={() => this.setState({supervisorTableActionDialogId: null})}
+ />
+ }
+ {
+ taskTableActionDialogId &&
+ <TaskTableActionDialog
+ isOpen
+ taskId={taskTableActionDialogId}
+ actions={taskTableActionDialogActions}
+ onClose={() => this.setState({taskTableActionDialogId: null})}
+ />
+ }
</div>;
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org