This is an automated email from the ASF dual-hosted git repository.
fjy 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 b9c68a5 Web console: refactor home view, add tests (#8247)
b9c68a5 is described below
commit b9c68a5b7bcca5ddff772c2d7409303dee3e88fb
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Tue Aug 6 12:41:07 2019 -0700
Web console: refactor home view, add tests (#8247)
* refactor home view
* updated mode button placement
---
web-console/script/mkcomp | 3 +-
.../__snapshots__/joda-to-regexp.spec.ts.snap | 15 -
web-console/src/utils/joda-to-regexp.spec.ts | 34 +-
.../__snapshots__/datasource-view.spec.tsx.snap | 2 +-
.../src/views/datasource-view/datasource-view.tsx | 2 +-
.../__snapshots__/home-view.spec.tsx.snap | 162 +------
.../__snapshots__/datasources-card.spec.tsx.snap | 42 ++
.../datasources-card.spec.tsx} | 23 +-
.../datasources-card/datasources-card.tsx | 97 ++++
.../__snapshots__/home-view-card.spec.tsx.snap | 40 ++
.../home-view-card.scss} | 15 +-
.../home-view-card.spec.tsx} | 35 +-
.../home-view/home-view-card/home-view-card.tsx | 56 +++
web-console/src/views/home-view/home-view.scss | 4 -
web-console/src/views/home-view/home-view.tsx | 527 +--------------------
.../__snapshots__/lookups-card.spec.tsx.snap | 42 ++
.../lookups-card.spec.tsx} | 23 +-
.../views/home-view/lookups-card/lookups-card.tsx | 98 ++++
.../__snapshots__/segments-card.spec.tsx.snap | 42 ++
.../segments-card.spec.tsx} | 23 +-
.../home-view/segments-card/segments-card.tsx | 122 +++++
.../__snapshots__/servers-card.spec.tsx.snap | 42 ++
.../servers-card.spec.tsx} | 23 +-
.../views/home-view/servers-card/servers-card.tsx | 160 +++++++
.../__snapshots__/status-card.spec.tsx.snap | 41 ++
.../status-card.spec.tsx} | 23 +-
.../views/home-view/status-card/status-card.tsx | 103 ++++
.../__snapshots__/supervisors-card.spec.tsx.snap | 42 ++
.../supervisors-card.spec.tsx} | 23 +-
.../supervisors-card/supervisors-card.tsx | 106 +++++
.../__snapshots__/tasks-card.spec.tsx.snap | 42 ++
.../tasks-card.spec.tsx} | 23 +-
.../src/views/home-view/tasks-card/tasks-card.tsx | 138 ++++++
.../__snapshots__/segments-view.spec.tsx.snap | 36 +-
.../src/views/segments-view/segments-view.tsx | 37 +-
.../__snapshots__/servers-view.spec.tsx.snap | 2 +-
.../src/views/servers-view/servers-view.tsx | 2 +-
.../__snapshots__/tasks-view.spec.tsx.snap | 32 +-
web-console/src/views/task-view/tasks-view.tsx | 4 +-
39 files changed, 1427 insertions(+), 859 deletions(-)
diff --git a/web-console/script/mkcomp b/web-console/script/mkcomp
index 2b226c9..01a1509 100755
--- a/web-console/script/mkcomp
+++ b/web-console/script/mkcomp
@@ -79,7 +79,6 @@ writeFile(
* limitations under the License.
*/
-import { Button, InputGroup } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import React from 'react';
@@ -156,8 +155,8 @@ writeFile(
* limitations under the License.
*/
+import { render } from '@testing-library/react';
import React from 'react';
-import { render } from 'react-testing-library';
import { ${camelName} } from './${name}';
diff --git a/web-console/src/utils/__snapshots__/joda-to-regexp.spec.ts.snap
b/web-console/src/utils/__snapshots__/joda-to-regexp.spec.ts.snap
deleted file mode 100644
index 7ee4beb..0000000
--- a/web-console/src/utils/__snapshots__/joda-to-regexp.spec.ts.snap
+++ /dev/null
@@ -1,15 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`jodaFormatToRegExp works for common formats 1`] =
`"/^(?:3[0-1]|[12][0-9]|[1-9])\\\\/(?:1[0-2]|[1-9])\\\\/[0-9]{4}$/i"`;
-
-exports[`jodaFormatToRegExp works for common formats 2`] =
`"/^(?:1[0-2]|0[1-9])\\\\/(?:3[0-1]|[12][0-9]|0[1-9])\\\\/[0-9]{4}$/i"`;
-
-exports[`jodaFormatToRegExp works for common formats 3`] =
`"/^(?:1[0-2]|[1-9])\\\\/(?:3[0-1]|[12][0-9]|[1-9])\\\\/[0-9]{2}$/i"`;
-
-exports[`jodaFormatToRegExp works for common formats 4`] =
`"/^(?:3[0-1]|[12][0-9]|[1-9])-(?:1[0-2]|[1-9])-[0-9]{4}
(?:1[0-2]|0[1-9]):[0-5][0-9]:[0-5][0-9] [ap]m$/i"`;
-
-exports[`jodaFormatToRegExp works for common formats 5`] =
`"/^(?:1[0-2]|0[1-9])\\\\/(?:3[0-1]|[12][0-9]|0[1-9])\\\\/[0-9]{4}
(?:1[0-2]|0[1-9]):[0-5][0-9]:[0-5][0-9] [ap]m$/i"`;
-
-exports[`jodaFormatToRegExp works for common formats 6`] =
`"/^[0-9]{4}-(?:1[0-2]|0[1-9])-(?:3[0-1]|[12][0-9]|0[1-9])
(?:2[0-3]|1[0-9]|0[0-9]):[0-5][0-9]:[0-5][0-9]$/i"`;
-
-exports[`jodaFormatToRegExp works for common formats 7`] =
`"/^[0-9]{4}-(?:1[0-2]|0[1-9])-(?:3[0-1]|[12][0-9]|0[1-9])
(?:2[0-3]|1[0-9]|0[0-9]):[0-5][0-9]:[0-5][0-9].[0-9]{1,3}$/i"`;
diff --git a/web-console/src/utils/joda-to-regexp.spec.ts
b/web-console/src/utils/joda-to-regexp.spec.ts
index c88da0d..54ce9ae 100644
--- a/web-console/src/utils/joda-to-regexp.spec.ts
+++ b/web-console/src/utils/joda-to-regexp.spec.ts
@@ -20,13 +20,33 @@ import { jodaFormatToRegExp } from './joda-to-regexp';
describe('jodaFormatToRegExp', () => {
it('works for common formats', () => {
- expect(jodaFormatToRegExp('d/M/yyyy').toString()).toMatchSnapshot();
- expect(jodaFormatToRegExp('MM/dd/YYYY').toString()).toMatchSnapshot();
- expect(jodaFormatToRegExp('M/d/YY').toString()).toMatchSnapshot();
- expect(jodaFormatToRegExp('d-M-yyyy hh:mm:ss
a').toString()).toMatchSnapshot();
- expect(jodaFormatToRegExp('MM/dd/YYYY hh:mm:ss
a').toString()).toMatchSnapshot();
- expect(jodaFormatToRegExp('YYYY-MM-dd
HH:mm:ss').toString()).toMatchSnapshot();
- expect(jodaFormatToRegExp('YYYY-MM-dd
HH:mm:ss.S').toString()).toMatchSnapshot();
+ expect(jodaFormatToRegExp('d/M/yyyy').toString()).toMatchInlineSnapshot(
+ `"/^(?:3[0-1]|[12][0-9]|[1-9])\\\\/(?:1[0-2]|[1-9])\\\\/[0-9]{4}$/i"`,
+ );
+
+ expect(jodaFormatToRegExp('MM/dd/YYYY').toString()).toMatchInlineSnapshot(
+ `"/^(?:1[0-2]|0[1-9])\\\\/(?:3[0-1]|[12][0-9]|0[1-9])\\\\/[0-9]{4}$/i"`,
+ );
+
+ expect(jodaFormatToRegExp('M/d/YY').toString()).toMatchInlineSnapshot(
+ `"/^(?:1[0-2]|[1-9])\\\\/(?:3[0-1]|[12][0-9]|[1-9])\\\\/[0-9]{2}$/i"`,
+ );
+
+ expect(jodaFormatToRegExp('d-M-yyyy hh:mm:ss
a').toString()).toMatchInlineSnapshot(
+ `"/^(?:3[0-1]|[12][0-9]|[1-9])-(?:1[0-2]|[1-9])-[0-9]{4}
(?:1[0-2]|0[1-9]):[0-5][0-9]:[0-5][0-9] [ap]m$/i"`,
+ );
+
+ expect(jodaFormatToRegExp('MM/dd/YYYY hh:mm:ss
a').toString()).toMatchInlineSnapshot(
+ `"/^(?:1[0-2]|0[1-9])\\\\/(?:3[0-1]|[12][0-9]|0[1-9])\\\\/[0-9]{4}
(?:1[0-2]|0[1-9]):[0-5][0-9]:[0-5][0-9] [ap]m$/i"`,
+ );
+
+ expect(jodaFormatToRegExp('YYYY-MM-dd
HH:mm:ss').toString()).toMatchInlineSnapshot(
+ `"/^[0-9]{4}-(?:1[0-2]|0[1-9])-(?:3[0-1]|[12][0-9]|0[1-9])
(?:2[0-3]|1[0-9]|0[0-9]):[0-5][0-9]:[0-5][0-9]$/i"`,
+ );
+
+ expect(jodaFormatToRegExp('YYYY-MM-dd
HH:mm:ss.S').toString()).toMatchInlineSnapshot(
+ `"/^[0-9]{4}-(?:1[0-2]|0[1-9])-(?:3[0-1]|[12][0-9]|0[1-9])
(?:2[0-3]|1[0-9]|0[0-9]):[0-5][0-9]:[0-5][0-9].[0-9]{1,3}$/i"`,
+ );
});
it('matches dates when needed', () => {
diff --git
a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
index 0e4bc80..84c1416 100755
---
a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
+++
b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
@@ -23,7 +23,7 @@ exports[`data source view matches snapshot 1`] = `
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
- text="See in SQL view"
+ text="View SQL query for table"
/>
</Blueprint3.Menu>
}
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx
b/web-console/src/views/datasource-view/datasource-view.tsx
index 13fcf96..386d5d2 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -471,7 +471,7 @@ GROUP BY 1`;
{!noSqlMode && (
<MenuItem
icon={IconNames.APPLICATION}
- text="See in SQL view"
+ text="View SQL query for table"
onClick={() => goToQuery(DatasourcesView.DATASOURCE_SQL)}
/>
)}
diff --git
a/web-console/src/views/home-view/__snapshots__/home-view.spec.tsx.snap
b/web-console/src/views/home-view/__snapshots__/home-view.spec.tsx.snap
index 7ee75ff..6303a2e 100644
--- a/web-console/src/views/home-view/__snapshots__/home-view.spec.tsx.snap
+++ b/web-console/src/views/home-view/__snapshots__/home-view.spec.tsx.snap
@@ -4,152 +4,20 @@ exports[`home view matches snapshot 1`] = `
<div
className="home-view app-view"
>
- <a
- onClick={[Function]}
- >
- <Blueprint3.Card
- className="home-view-card"
- elevation={0}
- interactive={true}
- >
- <Component>
- <Blueprint3.Icon
- color="#bfccd5"
- icon="graph"
- />
-
- Status
- </Component>
- <p>
- Loading...
- </p>
- </Blueprint3.Card>
- </a>
- <a
- href="#datasources"
- >
- <Blueprint3.Card
- className="home-view-card"
- elevation={0}
- interactive={true}
- >
- <Component>
- <Blueprint3.Icon
- color="#bfccd5"
- icon="multi-select"
- />
-
- Datasources
- </Component>
- <p>
- Loading...
- </p>
- </Blueprint3.Card>
- </a>
- <a
- href="#segments"
- >
- <Blueprint3.Card
- className="home-view-card"
- elevation={0}
- interactive={true}
- >
- <Component>
- <Blueprint3.Icon
- color="#bfccd5"
- icon="stacked-chart"
- />
-
- Segments
- </Component>
- <p>
- Loading...
- </p>
- </Blueprint3.Card>
- </a>
- <a
- href="#tasks"
- >
- <Blueprint3.Card
- className="home-view-card"
- elevation={0}
- interactive={true}
- >
- <Component>
- <Blueprint3.Icon
- color="#bfccd5"
- icon="list-columns"
- />
-
- Supervisors
- </Component>
- <p>
- Loading...
- </p>
- </Blueprint3.Card>
- </a>
- <a
- href="#tasks"
- >
- <Blueprint3.Card
- className="home-view-card"
- elevation={0}
- interactive={true}
- >
- <Component>
- <Blueprint3.Icon
- color="#bfccd5"
- icon="gantt-chart"
- />
-
- Tasks
- </Component>
- <p>
- Loading...
- </p>
- </Blueprint3.Card>
- </a>
- <a
- href="#servers"
- >
- <Blueprint3.Card
- className="home-view-card"
- elevation={0}
- interactive={true}
- >
- <Component>
- <Blueprint3.Icon
- color="#bfccd5"
- icon="database"
- />
-
- Servers
- </Component>
- <p>
- Loading...
- </p>
- </Blueprint3.Card>
- </a>
- <a
- href="#lookups"
- >
- <Blueprint3.Card
- className="home-view-card"
- elevation={0}
- interactive={true}
- >
- <Component>
- <Blueprint3.Icon
- color="#bfccd5"
- icon="properties"
- />
-
- Lookups
- </Component>
- <p>
- Loading...
- </p>
- </Blueprint3.Card>
- </a>
+ <StatusCard />
+ <DatasourcesCard
+ noSqlMode={false}
+ />
+ <SegmentsCard
+ noSqlMode={false}
+ />
+ <SupervisorsCard />
+ <TasksCard
+ noSqlMode={false}
+ />
+ <ServersCard
+ noSqlMode={false}
+ />
+ <LookupsCard />
</div>
`;
diff --git
a/web-console/src/views/home-view/datasources-card/__snapshots__/datasources-card.spec.tsx.snap
b/web-console/src/views/home-view/datasources-card/__snapshots__/datasources-card.spec.tsx.snap
new file mode 100644
index 0000000..8adf764
--- /dev/null
+++
b/web-console/src/views/home-view/datasources-card/__snapshots__/datasources-card.spec.tsx.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`datasources card matches snapshot 1`] = `
+<a
+ class="home-view-card datasources-card"
+ href="#datasources"
+>
+ <div
+ class="bp3-card bp3-interactive bp3-elevation-0"
+ >
+ <h5
+ class="bp3-heading"
+ >
+ <span
+ class="bp3-icon bp3-icon-multi-select"
+ icon="multi-select"
+ >
+ <svg
+ data-icon="multi-select"
+ fill="#bfccd5"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ multi-select
+ </desc>
+ <path
+ d="M12 3.98H4c-.55 0-1 .45-1 1v1h8v5h1c.55 0 1-.45
1-1v-5c0-.55-.45-1-1-1zm3-3H7c-.55 0-1 .45-1 1v1h8v5h1c.55 0 1-.45
1-1v-5c0-.55-.45-1-1-1zm-6 6H1c-.55 0-1 .45-1 1v5c0 .55.45 1 1 1h8c.55 0 1-.45
1-1v-5c0-.55-.45-1-1-1zm-1 5H2v-3h6v3z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+
+ Datasources
+ </h5>
+ <p>
+ Loading...
+ </p>
+ </div>
+</a>
+`;
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/datasources-card/datasources-card.spec.tsx
similarity index 68%
copy from web-console/src/views/home-view/home-view.scss
copy to
web-console/src/views/home-view/datasources-card/datasources-card.spec.tsx
index c2f5bb1..edc4b9e 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/datasources-card/datasources-card.spec.tsx
@@ -16,19 +16,16 @@
* limitations under the License.
*/
-@import '../../variables';
+import { render } from '@testing-library/react';
+import React from 'react';
-.home-view {
- display: grid;
- grid-gap: $standard-padding;
- grid-template-columns: 1fr 1fr 1fr 1fr;
+import { DatasourcesCard } from './datasources-card';
- & > a {
- text-decoration: inherit;
- color: inherit;
- }
+describe('datasources card', () => {
+ it('matches snapshot', () => {
+ const datasourcesCard = <DatasourcesCard noSqlMode={false} />;
- .home-view-card {
- height: 170px;
- }
-}
+ const { container } = render(datasourcesCard);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git
a/web-console/src/views/home-view/datasources-card/datasources-card.tsx
b/web-console/src/views/home-view/datasources-card/datasources-card.tsx
new file mode 100644
index 0000000..34530c6
--- /dev/null
+++ b/web-console/src/views/home-view/datasources-card/datasources-card.tsx
@@ -0,0 +1,97 @@
+/*
+ * 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 { IconNames } from '@blueprintjs/icons';
+import axios from 'axios';
+import React from 'react';
+
+import { pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
+import { HomeViewCard } from '../home-view-card/home-view-card';
+
+export interface DatasourcesCardProps {
+ noSqlMode: boolean;
+}
+
+export interface DatasourcesCardState {
+ datasourceCountLoading: boolean;
+ datasourceCount: number;
+ datasourceCountError?: string;
+}
+
+export class DatasourcesCard extends React.PureComponent<
+ DatasourcesCardProps,
+ DatasourcesCardState
+> {
+ private datasourceQueryManager: QueryManager<boolean, any>;
+
+ constructor(props: DatasourcesCardProps, context: any) {
+ super(props, context);
+ this.state = {
+ datasourceCountLoading: false,
+ datasourceCount: 0,
+ };
+
+ this.datasourceQueryManager = new QueryManager({
+ processQuery: async noSqlMode => {
+ let datasources: string[];
+ if (!noSqlMode) {
+ datasources = await queryDruidSql({
+ query: `SELECT datasource FROM sys.segments GROUP BY 1`,
+ });
+ } else {
+ const datasourcesResp = await
axios.get('/druid/coordinator/v1/datasources');
+ datasources = datasourcesResp.data;
+ }
+ return datasources.length;
+ },
+ onStateChange: ({ result, loading, error }) => {
+ this.setState({
+ datasourceCountLoading: loading,
+ datasourceCount: result,
+ datasourceCountError: error || undefined,
+ });
+ },
+ });
+ }
+
+ componentDidMount(): void {
+ const { noSqlMode } = this.props;
+
+ this.datasourceQueryManager.runQuery(noSqlMode);
+ }
+
+ componentWillUnmount(): void {
+ this.datasourceQueryManager.terminate();
+ }
+
+ render(): JSX.Element {
+ const { datasourceCountLoading, datasourceCountError, datasourceCount } =
this.state;
+ return (
+ <HomeViewCard
+ className="datasources-card"
+ href={'#datasources'}
+ icon={IconNames.MULTI_SELECT}
+ title={'Datasources'}
+ loading={datasourceCountLoading}
+ error={datasourceCountError}
+ >
+ {pluralIfNeeded(datasourceCount, 'datasource')}
+ </HomeViewCard>
+ );
+ }
+}
diff --git
a/web-console/src/views/home-view/home-view-card/__snapshots__/home-view-card.spec.tsx.snap
b/web-console/src/views/home-view/home-view-card/__snapshots__/home-view-card.spec.tsx.snap
new file mode 100644
index 0000000..6643f8d
--- /dev/null
+++
b/web-console/src/views/home-view/home-view-card/__snapshots__/home-view-card.spec.tsx.snap
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`home view card matches snapshot 1`] = `
+<a
+ class="home-view-card some-card"
+ href="#somewhere"
+>
+ <div
+ class="bp3-card bp3-interactive bp3-elevation-0"
+ >
+ <h5
+ class="bp3-heading"
+ >
+ <span
+ class="bp3-icon bp3-icon-database"
+ icon="database"
+ >
+ <svg
+ data-icon="database"
+ fill="#bfccd5"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ database
+ </desc>
+ <path
+ d="M8 4c3.31 0 6-.9 6-2s-2.69-2-6-2C4.68 0 2 .9 2 2s2.68 2 6
2zm-6-.48V8c0 1.1 2.69 2 6 2s6-.9 6-2V3.52C12.78 4.4 10.56 5 8
5s-4.78-.6-6-1.48zm0 6V14c0 1.1 2.69 2 6 2s6-.9 6-2V9.52C12.78 10.4 10.56 11 8
11s-4.78-.6-6-1.48z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+
+ Something
+ </h5>
+ Thigns
+ </div>
+</a>
+`;
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/home-view-card/home-view-card.scss
similarity index 79%
copy from web-console/src/views/home-view/home-view.scss
copy to web-console/src/views/home-view/home-view-card/home-view-card.scss
index c2f5bb1..82b4b93 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/home-view-card/home-view-card.scss
@@ -16,19 +16,8 @@
* limitations under the License.
*/
-@import '../../variables';
-
-.home-view {
- display: grid;
- grid-gap: $standard-padding;
- grid-template-columns: 1fr 1fr 1fr 1fr;
-
- & > a {
- text-decoration: inherit;
- color: inherit;
- }
-
- .home-view-card {
+.home-view-card {
+ .bp3-card {
height: 170px;
}
}
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/home-view-card/home-view-card.spec.tsx
similarity index 56%
copy from web-console/src/views/home-view/home-view.scss
copy to web-console/src/views/home-view/home-view-card/home-view-card.spec.tsx
index c2f5bb1..cd1768f 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/home-view-card/home-view-card.spec.tsx
@@ -16,19 +16,28 @@
* limitations under the License.
*/
-@import '../../variables';
+import { IconNames } from '@blueprintjs/icons';
+import { render } from '@testing-library/react';
+import React from 'react';
-.home-view {
- display: grid;
- grid-gap: $standard-padding;
- grid-template-columns: 1fr 1fr 1fr 1fr;
+import { HomeViewCard } from './home-view-card';
- & > a {
- text-decoration: inherit;
- color: inherit;
- }
+describe('home view card', () => {
+ it('matches snapshot', () => {
+ const homeViewCard = (
+ <HomeViewCard
+ className="some-card"
+ href={'#somewhere'}
+ icon={IconNames.DATABASE}
+ title={'Something'}
+ loading={false}
+ error={undefined}
+ >
+ Thigns
+ </HomeViewCard>
+ );
- .home-view-card {
- height: 170px;
- }
-}
+ const { container } = render(homeViewCard);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/views/home-view/home-view-card/home-view-card.tsx
b/web-console/src/views/home-view/home-view-card/home-view-card.tsx
new file mode 100644
index 0000000..52d1a8e
--- /dev/null
+++ b/web-console/src/views/home-view/home-view-card/home-view-card.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 { Card, H5, Icon, IconName } from '@blueprintjs/core';
+import classNames from 'classnames';
+import React from 'react';
+
+import './home-view-card.scss';
+
+export interface HomeViewCardProps {
+ className: string;
+ onClick?: () => void;
+ href?: string;
+ icon: IconName;
+ title: string;
+ loading: boolean;
+ error: string | undefined;
+}
+
+export class HomeViewCard extends React.PureComponent<HomeViewCardProps> {
+ render(): JSX.Element {
+ const { className, onClick, href, icon, title, loading, error, children }
= this.props;
+
+ return (
+ <a
+ className={classNames('home-view-card', className)}
+ onClick={onClick}
+ href={href}
+ target={href && href[0] === '/' ? '_blank' : undefined}
+ >
+ <Card interactive>
+ <H5>
+ <Icon color="#bfccd5" icon={icon} />
+ {title}
+ </H5>
+ {loading ? <p>Loading...</p> : error ? `Error: ${error}` : children}
+ </Card>
+ </a>
+ );
+ }
+}
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/home-view.scss
index c2f5bb1..0a2d82d 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/home-view.scss
@@ -27,8 +27,4 @@
text-decoration: inherit;
color: inherit;
}
-
- .home-view-card {
- height: 170px;
- }
}
diff --git a/web-console/src/views/home-view/home-view.tsx
b/web-console/src/views/home-view/home-view.tsx
index 5cb7d94..87c7adf 100644
--- a/web-console/src/views/home-view/home-view.tsx
+++ b/web-console/src/views/home-view/home-view.tsx
@@ -16,530 +16,35 @@
* limitations under the License.
*/
-import { Card, H5, Icon } from '@blueprintjs/core';
-import { IconName, IconNames } from '@blueprintjs/icons';
-import axios from 'axios';
-import { sum } from 'd3-array';
import React from 'react';
-import { StatusDialog } from '../../dialogs/status-dialog/status-dialog';
-import { compact, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from
'../../utils';
-import { deepGet } from '../../utils/object-change';
+import { DatasourcesCard } from './datasources-card/datasources-card';
+import { LookupsCard } from './lookups-card/lookups-card';
+import { SegmentsCard } from './segments-card/segments-card';
+import { ServersCard } from './servers-card/servers-card';
+import { StatusCard } from './status-card/status-card';
+import { SupervisorsCard } from './supervisors-card/supervisors-card';
+import { TasksCard } from './tasks-card/tasks-card';
import './home-view.scss';
-export interface CardOptions {
- onClick?: () => void;
- href?: string;
- icon: IconName;
- title: string;
- loading?: boolean;
- content: JSX.Element | string;
- error?: string;
-}
-
export interface HomeViewProps {
noSqlMode: boolean;
}
-export interface HomeViewState {
- versionLoading: boolean;
- version: string;
- versionError?: string;
-
- datasourceCountLoading: boolean;
- datasourceCount: number;
- datasourceCountError?: string;
-
- segmentCountLoading: boolean;
- segmentCount: number;
- unavailableSegmentCount: number;
- segmentCountError?: string;
-
- supervisorCountLoading: boolean;
- runningSupervisorCount: number;
- suspendedSupervisorCount: number;
- supervisorCountError?: string;
-
- taskCountLoading: boolean;
- runningTaskCount: number;
- pendingTaskCount: number;
- successTaskCount: number;
- failedTaskCount: number;
- waitingTaskCount: number;
- taskCountError?: string;
-
- serverCountLoading: boolean;
- coordinatorCount: number;
- overlordCount: number;
- routerCount: number;
- brokerCount: number;
- historicalCount: number;
- middleManagerCount: number;
- peonCount: number;
- indexerCount: number;
- serverCountError?: string;
-
- showStatusDialog: boolean;
-
- lookupsCountLoading: boolean;
- lookupsCount: number;
- lookupsUninitialized: boolean;
- lookupsCountError?: string;
-}
-
-export class HomeView extends React.PureComponent<HomeViewProps,
HomeViewState> {
- private versionQueryManager: QueryManager<null, string>;
- private datasourceQueryManager: QueryManager<boolean, any>;
- private segmentQueryManager: QueryManager<boolean, any>;
- private supervisorQueryManager: QueryManager<null, any>;
- private taskQueryManager: QueryManager<boolean, any>;
- private serverQueryManager: QueryManager<boolean, any>;
- private lookupsQueryManager: QueryManager<null, any>;
-
- constructor(props: HomeViewProps, context: any) {
- super(props, context);
- this.state = {
- versionLoading: true,
- version: '',
-
- datasourceCountLoading: false,
- datasourceCount: 0,
-
- segmentCountLoading: false,
- segmentCount: 0,
- unavailableSegmentCount: 0,
-
- supervisorCountLoading: false,
- runningSupervisorCount: 0,
- suspendedSupervisorCount: 0,
-
- taskCountLoading: false,
- runningTaskCount: 0,
- pendingTaskCount: 0,
- successTaskCount: 0,
- failedTaskCount: 0,
- waitingTaskCount: 0,
-
- serverCountLoading: false,
- coordinatorCount: 0,
- overlordCount: 0,
- routerCount: 0,
- brokerCount: 0,
- historicalCount: 0,
- middleManagerCount: 0,
- peonCount: 0,
- indexerCount: 0,
-
- showStatusDialog: false,
-
- lookupsCountLoading: false,
- lookupsCount: 0,
- lookupsUninitialized: false,
- };
-
- this.versionQueryManager = new QueryManager({
- processQuery: async () => {
- const statusResp = await axios.get('/status');
- return statusResp.data.version;
- },
- onStateChange: ({ result, loading, error }) => {
- this.setState({
- versionLoading: loading,
- version: result,
- versionError: error,
- });
- },
- });
-
- this.datasourceQueryManager = new QueryManager({
- processQuery: async noSqlMode => {
- let datasources: string[];
- if (!noSqlMode) {
- datasources = await queryDruidSql({
- query: `SELECT datasource FROM sys.segments GROUP BY 1`,
- });
- } else {
- const datasourcesResp = await
axios.get('/druid/coordinator/v1/datasources');
- datasources = datasourcesResp.data;
- }
- return datasources.length;
- },
- onStateChange: ({ result, loading, error }) => {
- this.setState({
- datasourceCountLoading: loading,
- datasourceCount: result,
- datasourceCountError: error || undefined,
- });
- },
- });
-
- this.segmentQueryManager = new QueryManager({
- processQuery: async noSqlMode => {
- if (noSqlMode) {
- const loadstatusResp = await
axios.get('/druid/coordinator/v1/loadstatus?simple');
- const loadstatus = loadstatusResp.data;
- const unavailableSegmentNum = sum(Object.keys(loadstatus), key =>
loadstatus[key]);
-
- const datasourcesMetaResp = await
axios.get('/druid/coordinator/v1/datasources?simple');
- const datasourcesMeta = datasourcesMetaResp.data;
- const availableSegmentNum = sum(datasourcesMeta, (curr: any) =>
- deepGet(curr, 'properties.segments.count'),
- );
-
- return {
- count: availableSegmentNum + unavailableSegmentNum,
- unavailable: unavailableSegmentNum,
- };
- } else {
- const segments = await queryDruidSql({
- query: `SELECT
- COUNT(*) as "count",
- COUNT(*) FILTER (WHERE is_available = 0) as "unavailable"
-FROM sys.segments`,
- });
- return segments.length === 1 ? segments[0] : null;
- }
- },
- onStateChange: ({ result, loading, error }) => {
- this.setState({
- segmentCountLoading: loading,
- segmentCount: result ? result.count : 0,
- unavailableSegmentCount: result ? result.unavailable : 0,
- segmentCountError: error,
- });
- },
- });
-
- this.supervisorQueryManager = new QueryManager({
- processQuery: async () => {
- const resp = await axios.get('/druid/indexer/v1/supervisor?full');
- const data = resp.data;
- const runningSupervisorCount = data.filter((d: any) =>
d.spec.suspended === false).length;
- const suspendedSupervisorCount = data.filter((d: any) =>
d.spec.suspended === true).length;
- return {
- runningSupervisorCount,
- suspendedSupervisorCount,
- };
- },
- onStateChange: ({ result, loading, error }) => {
- this.setState({
- runningSupervisorCount: result ? result.runningSupervisorCount : 0,
- suspendedSupervisorCount: result ? result.suspendedSupervisorCount :
0,
- supervisorCountLoading: loading,
- supervisorCountError: error,
- });
- },
- });
-
- this.taskQueryManager = new QueryManager({
- processQuery: async noSqlMode => {
- if (noSqlMode) {
- const completeTasksResp = await
axios.get('/druid/indexer/v1/completeTasks');
- const runningTasksResp = await
axios.get('/druid/indexer/v1/runningTasks');
- const pendingTasksResp = await
axios.get('/druid/indexer/v1/pendingTasks');
- const waitingTasksResp = await
axios.get('/druid/indexer/v1/waitingTasks');
- return {
- SUCCESS: completeTasksResp.data.filter((d: any) => d.status ===
'SUCCESS').length,
- FAILED: completeTasksResp.data.filter((d: any) => d.status ===
'FAILED').length,
- RUNNING: runningTasksResp.data.length,
- PENDING: pendingTasksResp.data.length,
- WAITING: waitingTasksResp.data.length,
- };
- } else {
- const taskCountsFromQuery: { status: string; count: number }[] =
await queryDruidSql({
- query: `SELECT
- CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS
"status",
- COUNT (*) AS "count"
-FROM sys.tasks
-GROUP BY 1`,
- });
- return lookupBy(taskCountsFromQuery, x => x.status, x => x.count);
- }
- },
- onStateChange: ({ result, loading, error }) => {
- this.setState({
- taskCountLoading: loading,
- successTaskCount: result ? result.SUCCESS : 0,
- failedTaskCount: result ? result.FAILED : 0,
- runningTaskCount: result ? result.RUNNING : 0,
- pendingTaskCount: result ? result.PENDING : 0,
- waitingTaskCount: result ? result.WAITING : 0,
- taskCountError: error,
- });
- },
- });
-
- this.serverQueryManager = new QueryManager({
- processQuery: async noSqlMode => {
- if (noSqlMode) {
- const serversResp = await
axios.get('/druid/coordinator/v1/servers?simple');
- const middleManagerResp = await
axios.get('/druid/indexer/v1/workers');
- return {
- historical: serversResp.data.filter((s: any) => s.type ===
'historical').length,
- middle_manager: middleManagerResp.data.length,
- peon: serversResp.data.filter((s: any) => s.type ===
'indexer-executor').length,
- };
- } else {
- const serverCountsFromQuery: {
- server_type: string;
- count: number;
- }[] = await queryDruidSql({
- query: `SELECT server_type, COUNT(*) as "count" FROM sys.servers
GROUP BY 1`,
- });
- return lookupBy(serverCountsFromQuery, x => x.server_type, x =>
x.count);
- }
- },
- onStateChange: ({ result, loading, error }) => {
- this.setState({
- serverCountLoading: loading,
- coordinatorCount: result ? result.coordinator : 0,
- overlordCount: result ? result.overlord : 0,
- routerCount: result ? result.router : 0,
- brokerCount: result ? result.broker : 0,
- historicalCount: result ? result.historical : 0,
- middleManagerCount: result ? result.middle_manager : 0,
- peonCount: result ? result.peon : 0,
- indexerCount: result ? result.indexer : 0,
- serverCountError: error,
- });
- },
- });
-
- this.lookupsQueryManager = new QueryManager({
- processQuery: async () => {
- const resp = await axios.get('/druid/coordinator/v1/lookups/status');
- const data = resp.data;
- const lookupsCount = sum(Object.keys(data).map(k =>
Object.keys(data[k]).length));
- return {
- lookupsCount,
- };
- },
- onStateChange: ({ result, loading, error }) => {
- this.setState({
- lookupsCount: result ? result.lookupsCount : 0,
- lookupsUninitialized: error === 'Request failed with status code
404',
- lookupsCountLoading: loading,
- lookupsCountError: error,
- });
- },
- });
- }
-
- componentDidMount(): void {
- const { noSqlMode } = this.props;
-
- this.versionQueryManager.runQuery(null);
- this.datasourceQueryManager.runQuery(noSqlMode);
- this.segmentQueryManager.runQuery(noSqlMode);
- this.supervisorQueryManager.runQuery(null);
- this.taskQueryManager.runQuery(noSqlMode);
- this.serverQueryManager.runQuery(noSqlMode);
- this.lookupsQueryManager.runQuery(null);
- }
-
- componentWillUnmount(): void {
- this.versionQueryManager.terminate();
- this.datasourceQueryManager.terminate();
- this.segmentQueryManager.terminate();
- this.supervisorQueryManager.terminate();
- this.taskQueryManager.terminate();
- this.serverQueryManager.terminate();
- }
-
- renderStatusDialog() {
- const { showStatusDialog } = this.state;
- if (!showStatusDialog) {
- return null;
- }
- return (
- <StatusDialog
- onClose={() => this.setState({ showStatusDialog: false })}
- title={'Status'}
- isOpen
- />
- );
- }
-
- renderCard(cardOptions: CardOptions): JSX.Element {
- return (
- <a
- onClick={cardOptions.onClick}
- href={cardOptions.href}
- target={cardOptions.href && cardOptions.href[0] === '/' ? '_blank' :
undefined}
- >
- <Card className="home-view-card" interactive>
- <H5>
- <Icon color="#bfccd5" icon={cardOptions.icon} />
- {cardOptions.title}
- </H5>
- {cardOptions.loading ? (
- <p>Loading...</p>
- ) : cardOptions.error ? (
- `Error: ${cardOptions.error}`
- ) : (
- cardOptions.content
- )}
- </Card>
- </a>
- );
- }
-
- renderPluralIfNeededPair(
- count1: number,
- singular1: string,
- count2: number,
- singular2: string,
- ): JSX.Element | undefined {
- const text = compact([
- count1 ? pluralIfNeeded(count1, singular1) : undefined,
- count2 ? pluralIfNeeded(count2, singular2) : undefined,
- ]).join(', ');
- if (!text) return;
- return <p>{text}</p>;
- }
-
+export class HomeView extends React.PureComponent<HomeViewProps> {
render(): JSX.Element {
- const state = this.state;
+ const { noSqlMode } = this.props;
return (
<div className="home-view app-view">
- {this.renderCard({
- onClick: () => this.setState({ showStatusDialog: true }),
- icon: IconNames.GRAPH,
- title: 'Status',
- loading: state.versionLoading,
- content: state.version ? `Apache Druid is running version
${state.version}` : '',
- error: state.versionError,
- })}
- {this.renderCard({
- href: '#datasources',
- icon: IconNames.MULTI_SELECT,
- title: 'Datasources',
- loading: state.datasourceCountLoading,
- content: pluralIfNeeded(state.datasourceCount, 'datasource'),
- error: state.datasourceCountError,
- })}
- {this.renderCard({
- href: '#segments',
- icon: IconNames.STACKED_CHART,
- title: 'Segments',
- loading: state.segmentCountLoading,
- content: (
- <>
- <p>{pluralIfNeeded(state.segmentCount, 'segment')}</p>
- {Boolean(state.unavailableSegmentCount) && (
- <p>{pluralIfNeeded(state.unavailableSegmentCount, 'unavailable
segment')}</p>
- )}
- </>
- ),
- error: state.datasourceCountError,
- })}
- {this.renderCard({
- href: '#tasks',
- icon: IconNames.LIST_COLUMNS,
- title: 'Supervisors',
- loading: state.supervisorCountLoading,
- content: (
- <>
- {!Boolean(state.runningSupervisorCount +
state.suspendedSupervisorCount) && (
- <p>0 supervisors</p>
- )}
- {Boolean(state.runningSupervisorCount) && (
- <p>{pluralIfNeeded(state.runningSupervisorCount, 'running
supervisor')}</p>
- )}
- {Boolean(state.suspendedSupervisorCount) && (
- <p>{pluralIfNeeded(state.suspendedSupervisorCount, 'suspended
supervisor')}</p>
- )}
- </>
- ),
- error: state.supervisorCountError,
- })}
- {this.renderCard({
- href: '#tasks',
- icon: IconNames.GANTT_CHART,
- title: 'Tasks',
- loading: state.taskCountLoading,
- content: (
- <>
- {Boolean(state.runningTaskCount) && (
- <p>{pluralIfNeeded(state.runningTaskCount, 'running task')}</p>
- )}
- {Boolean(state.pendingTaskCount) && (
- <p>{pluralIfNeeded(state.pendingTaskCount, 'pending task')}</p>
- )}
- {Boolean(state.successTaskCount) && (
- <p>{pluralIfNeeded(state.successTaskCount, 'successful
task')}</p>
- )}
- {Boolean(state.waitingTaskCount) && (
- <p>{pluralIfNeeded(state.waitingTaskCount, 'waiting task')}</p>
- )}
- {Boolean(state.failedTaskCount) && (
- <p>{pluralIfNeeded(state.failedTaskCount, 'failed task')}</p>
- )}
- {!(
- Boolean(state.runningTaskCount) ||
- Boolean(state.pendingTaskCount) ||
- Boolean(state.successTaskCount) ||
- Boolean(state.waitingTaskCount) ||
- Boolean(state.failedTaskCount)
- ) && <p>There are no tasks</p>}
- </>
- ),
- error: state.taskCountError,
- })}
- {this.renderCard({
- href: '#servers',
- icon: IconNames.DATABASE,
- title: 'Servers',
- loading: state.serverCountLoading,
- content: (
- <>
- {this.renderPluralIfNeededPair(
- state.overlordCount,
- 'overlord',
- state.coordinatorCount,
- 'coordinator',
- )}
- {this.renderPluralIfNeededPair(
- state.routerCount,
- 'router',
- state.brokerCount,
- 'broker',
- )}
- {this.renderPluralIfNeededPair(
- state.historicalCount,
- 'historical',
- state.middleManagerCount,
- 'middle manager',
- )}
- {this.renderPluralIfNeededPair(
- state.peonCount,
- 'peon',
- state.indexerCount,
- 'indexer',
- )}
- </>
- ),
- error: state.serverCountError ? state.serverCountError : undefined,
- })}
- {this.renderCard({
- href: '#lookups',
- icon: IconNames.PROPERTIES,
- title: 'Lookups',
- loading: state.lookupsCountLoading,
- content: (
- <>
- <p>
- {!state.lookupsUninitialized
- ? pluralIfNeeded(state.lookupsCount, 'lookup')
- : 'Lookups uninitialized'}
- </p>
- </>
- ),
- error: !state.lookupsUninitialized ? state.lookupsCountError :
undefined,
- })}
- {!state.versionLoading && this.renderStatusDialog()}
+ <StatusCard />
+ <DatasourcesCard noSqlMode={noSqlMode} />
+ <SegmentsCard noSqlMode={noSqlMode} />
+ <SupervisorsCard />
+ <TasksCard noSqlMode={noSqlMode} />
+ <ServersCard noSqlMode={noSqlMode} />
+ <LookupsCard />
</div>
);
}
diff --git
a/web-console/src/views/home-view/lookups-card/__snapshots__/lookups-card.spec.tsx.snap
b/web-console/src/views/home-view/lookups-card/__snapshots__/lookups-card.spec.tsx.snap
new file mode 100644
index 0000000..9996d92
--- /dev/null
+++
b/web-console/src/views/home-view/lookups-card/__snapshots__/lookups-card.spec.tsx.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lookups card matches snapshot 1`] = `
+<a
+ class="home-view-card lookups-card"
+ href="#lookups"
+>
+ <div
+ class="bp3-card bp3-interactive bp3-elevation-0"
+ >
+ <h5
+ class="bp3-heading"
+ >
+ <span
+ class="bp3-icon bp3-icon-properties"
+ icon="properties"
+ >
+ <svg
+ data-icon="properties"
+ fill="#bfccd5"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ properties
+ </desc>
+ <path
+ d="M2 6C.9 6 0 6.9 0 8s.9 2 2 2 2-.9 2-2-.9-2-2-2zm4-3h9c.55 0
1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1s.45 1 1 1zm-4 9c-1.1 0-2 .9-2 2s.9 2 2
2 2-.9 2-2-.9-2-2-2zm13-5H6c-.55 0-1 .45-1 1s.45 1 1 1h9c.55 0 1-.45
1-1s-.45-1-1-1zm0 6H6c-.55 0-1 .45-1 1s.45 1 1 1h9c.55 0 1-.45
1-1s-.45-1-1-1zM2 0C.9 0 0 .9 0 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+
+ Lookups
+ </h5>
+ <p>
+ Loading...
+ </p>
+ </div>
+</a>
+`;
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/lookups-card/lookups-card.spec.tsx
similarity index 70%
copy from web-console/src/views/home-view/home-view.scss
copy to web-console/src/views/home-view/lookups-card/lookups-card.spec.tsx
index c2f5bb1..1ebbd1c 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/lookups-card/lookups-card.spec.tsx
@@ -16,19 +16,16 @@
* limitations under the License.
*/
-@import '../../variables';
+import { render } from '@testing-library/react';
+import React from 'react';
-.home-view {
- display: grid;
- grid-gap: $standard-padding;
- grid-template-columns: 1fr 1fr 1fr 1fr;
+import { LookupsCard } from './lookups-card';
- & > a {
- text-decoration: inherit;
- color: inherit;
- }
+describe('lookups card', () => {
+ it('matches snapshot', () => {
+ const lookupsCard = <LookupsCard />;
- .home-view-card {
- height: 170px;
- }
-}
+ const { container } = render(lookupsCard);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/views/home-view/lookups-card/lookups-card.tsx
b/web-console/src/views/home-view/lookups-card/lookups-card.tsx
new file mode 100644
index 0000000..e8fe533
--- /dev/null
+++ b/web-console/src/views/home-view/lookups-card/lookups-card.tsx
@@ -0,0 +1,98 @@
+/*
+ * 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 { IconNames } from '@blueprintjs/icons';
+import axios from 'axios';
+import { sum } from 'd3-array';
+import React from 'react';
+
+import { pluralIfNeeded, QueryManager } from '../../../utils';
+import { HomeViewCard } from '../home-view-card/home-view-card';
+
+export interface LookupsCardProps {}
+
+export interface LookupsCardState {
+ lookupsCountLoading: boolean;
+ lookupsCount: number;
+ lookupsUninitialized: boolean;
+ lookupsCountError?: string;
+}
+
+export class LookupsCard extends React.PureComponent<LookupsCardProps,
LookupsCardState> {
+ private lookupsQueryManager: QueryManager<null, any>;
+
+ constructor(props: LookupsCardProps, context: any) {
+ super(props, context);
+ this.state = {
+ lookupsCountLoading: false,
+ lookupsCount: 0,
+ lookupsUninitialized: false,
+ };
+
+ this.lookupsQueryManager = new QueryManager({
+ processQuery: async () => {
+ const resp = await axios.get('/druid/coordinator/v1/lookups/status');
+ const data = resp.data;
+ const lookupsCount = sum(Object.keys(data).map(k =>
Object.keys(data[k]).length));
+ return {
+ lookupsCount,
+ };
+ },
+ onStateChange: ({ result, loading, error }) => {
+ this.setState({
+ lookupsCount: result ? result.lookupsCount : 0,
+ lookupsUninitialized: error === 'Request failed with status code
404',
+ lookupsCountLoading: loading,
+ lookupsCountError: error,
+ });
+ },
+ });
+ }
+
+ componentDidMount(): void {
+ this.lookupsQueryManager.runQuery(null);
+ }
+
+ componentWillUnmount(): void {
+ this.lookupsQueryManager.terminate();
+ }
+
+ render(): JSX.Element {
+ const {
+ lookupsCountLoading,
+ lookupsCount,
+ lookupsUninitialized,
+ lookupsCountError,
+ } = this.state;
+
+ return (
+ <HomeViewCard
+ className="lookups-card"
+ href={'#lookups'}
+ icon={IconNames.PROPERTIES}
+ title={'Lookups'}
+ loading={lookupsCountLoading}
+ error={!lookupsUninitialized ? lookupsCountError : undefined}
+ >
+ <p>
+ {!lookupsUninitialized ? pluralIfNeeded(lookupsCount, 'lookup') :
'Lookups uninitialized'}
+ </p>
+ </HomeViewCard>
+ );
+ }
+}
diff --git
a/web-console/src/views/home-view/segments-card/__snapshots__/segments-card.spec.tsx.snap
b/web-console/src/views/home-view/segments-card/__snapshots__/segments-card.spec.tsx.snap
new file mode 100644
index 0000000..dcd6ae6
--- /dev/null
+++
b/web-console/src/views/home-view/segments-card/__snapshots__/segments-card.spec.tsx.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`segments card matches snapshot 1`] = `
+<a
+ class="home-view-card segments-card"
+ href="#segments"
+>
+ <div
+ class="bp3-card bp3-interactive bp3-elevation-0"
+ >
+ <h5
+ class="bp3-heading"
+ >
+ <span
+ class="bp3-icon bp3-icon-stacked-chart"
+ icon="stacked-chart"
+ >
+ <svg
+ data-icon="stacked-chart"
+ fill="#bfccd5"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ stacked-chart
+ </desc>
+ <path
+ d="M10 2c0-.55-.45-1-1-1H8c-.55 0-1 .45-1 1v3h3V2zm3 10h1c.55 0
1-.45 1-1V8h-3v3c0 .55.45 1 1 1zm2-7c0-.55-.45-1-1-1h-1c-.55 0-1 .45-1
1v2h3V5zm-5 1H7v3h3V6zM5 7c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1v1h3V7zm3 5h1c.55
0 1-.45 1-1v-1H7v1c0 .55.45 1 1 1zm7 1H2c-.55 0-1 .45-1 1s.45 1 1 1h13c.55 0
1-.45 1-1s-.45-1-1-1zM3 12h1c.55 0 1-.45 1-1V9H2v2c0 .55.45 1 1 1z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+
+ Segments
+ </h5>
+ <p>
+ Loading...
+ </p>
+ </div>
+</a>
+`;
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/segments-card/segments-card.spec.tsx
similarity index 69%
copy from web-console/src/views/home-view/home-view.scss
copy to web-console/src/views/home-view/segments-card/segments-card.spec.tsx
index c2f5bb1..7bf98c6 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/segments-card/segments-card.spec.tsx
@@ -16,19 +16,16 @@
* limitations under the License.
*/
-@import '../../variables';
+import { render } from '@testing-library/react';
+import React from 'react';
-.home-view {
- display: grid;
- grid-gap: $standard-padding;
- grid-template-columns: 1fr 1fr 1fr 1fr;
+import { SegmentsCard } from './segments-card';
- & > a {
- text-decoration: inherit;
- color: inherit;
- }
+describe('segments card', () => {
+ it('matches snapshot', () => {
+ const segmentsCard = <SegmentsCard noSqlMode={false} />;
- .home-view-card {
- height: 170px;
- }
-}
+ const { container } = render(segmentsCard);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/views/home-view/segments-card/segments-card.tsx
b/web-console/src/views/home-view/segments-card/segments-card.tsx
new file mode 100644
index 0000000..379d4ce
--- /dev/null
+++ b/web-console/src/views/home-view/segments-card/segments-card.tsx
@@ -0,0 +1,122 @@
+/*
+ * 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 { IconNames } from '@blueprintjs/icons';
+import axios from 'axios';
+import { sum } from 'd3-array';
+import React from 'react';
+
+import { pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
+import { deepGet } from '../../../utils/object-change';
+import { HomeViewCard } from '../home-view-card/home-view-card';
+
+export interface SegmentsCardProps {
+ noSqlMode: boolean;
+}
+
+export interface SegmentsCardState {
+ segmentCountLoading: boolean;
+ segmentCount: number;
+ unavailableSegmentCount: number;
+ segmentCountError?: string;
+}
+
+export class SegmentsCard extends React.PureComponent<SegmentsCardProps,
SegmentsCardState> {
+ private segmentQueryManager: QueryManager<boolean, any>;
+
+ constructor(props: SegmentsCardProps, context: any) {
+ super(props, context);
+ this.state = {
+ segmentCountLoading: false,
+ segmentCount: 0,
+ unavailableSegmentCount: 0,
+ };
+
+ this.segmentQueryManager = new QueryManager({
+ processQuery: async noSqlMode => {
+ if (noSqlMode) {
+ const loadstatusResp = await
axios.get('/druid/coordinator/v1/loadstatus?simple');
+ const loadstatus = loadstatusResp.data;
+ const unavailableSegmentNum = sum(Object.keys(loadstatus), key =>
loadstatus[key]);
+
+ const datasourcesMetaResp = await
axios.get('/druid/coordinator/v1/datasources?simple');
+ const datasourcesMeta = datasourcesMetaResp.data;
+ const availableSegmentNum = sum(datasourcesMeta, (curr: any) =>
+ deepGet(curr, 'properties.segments.count'),
+ );
+
+ return {
+ count: availableSegmentNum + unavailableSegmentNum,
+ unavailable: unavailableSegmentNum,
+ };
+ } else {
+ const segments = await queryDruidSql({
+ query: `SELECT
+ COUNT(*) as "count",
+ COUNT(*) FILTER (WHERE is_available = 0) as "unavailable"
+FROM sys.segments`,
+ });
+ return segments.length === 1 ? segments[0] : null;
+ }
+ },
+ onStateChange: ({ result, loading, error }) => {
+ this.setState({
+ segmentCountLoading: loading,
+ segmentCount: result ? result.count : 0,
+ unavailableSegmentCount: result ? result.unavailable : 0,
+ segmentCountError: error,
+ });
+ },
+ });
+ }
+
+ componentDidMount(): void {
+ const { noSqlMode } = this.props;
+
+ this.segmentQueryManager.runQuery(noSqlMode);
+ }
+
+ componentWillUnmount(): void {
+ this.segmentQueryManager.terminate();
+ }
+
+ render(): JSX.Element {
+ const {
+ segmentCountLoading,
+ segmentCountError,
+ segmentCount,
+ unavailableSegmentCount,
+ } = this.state;
+
+ return (
+ <HomeViewCard
+ className="segments-card"
+ href={'#segments'}
+ icon={IconNames.STACKED_CHART}
+ title={'Segments'}
+ loading={segmentCountLoading}
+ error={segmentCountError}
+ >
+ <p>{pluralIfNeeded(segmentCount, 'segment')}</p>
+ {Boolean(unavailableSegmentCount) && (
+ <p>{pluralIfNeeded(unavailableSegmentCount, 'unavailable
segment')}</p>
+ )}
+ </HomeViewCard>
+ );
+ }
+}
diff --git
a/web-console/src/views/home-view/servers-card/__snapshots__/servers-card.spec.tsx.snap
b/web-console/src/views/home-view/servers-card/__snapshots__/servers-card.spec.tsx.snap
new file mode 100644
index 0000000..770bab4
--- /dev/null
+++
b/web-console/src/views/home-view/servers-card/__snapshots__/servers-card.spec.tsx.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`servers card matches snapshot 1`] = `
+<a
+ class="home-view-card servers-card"
+ href="#servers"
+>
+ <div
+ class="bp3-card bp3-interactive bp3-elevation-0"
+ >
+ <h5
+ class="bp3-heading"
+ >
+ <span
+ class="bp3-icon bp3-icon-database"
+ icon="database"
+ >
+ <svg
+ data-icon="database"
+ fill="#bfccd5"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ database
+ </desc>
+ <path
+ d="M8 4c3.31 0 6-.9 6-2s-2.69-2-6-2C4.68 0 2 .9 2 2s2.68 2 6
2zm-6-.48V8c0 1.1 2.69 2 6 2s6-.9 6-2V3.52C12.78 4.4 10.56 5 8
5s-4.78-.6-6-1.48zm0 6V14c0 1.1 2.69 2 6 2s6-.9 6-2V9.52C12.78 10.4 10.56 11 8
11s-4.78-.6-6-1.48z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+
+ Servers
+ </h5>
+ <p>
+ Loading...
+ </p>
+ </div>
+</a>
+`;
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/servers-card/servers-card.spec.tsx
similarity index 69%
copy from web-console/src/views/home-view/home-view.scss
copy to web-console/src/views/home-view/servers-card/servers-card.spec.tsx
index c2f5bb1..b92fc2b 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/servers-card/servers-card.spec.tsx
@@ -16,19 +16,16 @@
* limitations under the License.
*/
-@import '../../variables';
+import { render } from '@testing-library/react';
+import React from 'react';
-.home-view {
- display: grid;
- grid-gap: $standard-padding;
- grid-template-columns: 1fr 1fr 1fr 1fr;
+import { ServersCard } from './servers-card';
- & > a {
- text-decoration: inherit;
- color: inherit;
- }
+describe('servers card', () => {
+ it('matches snapshot', () => {
+ const serversCard = <ServersCard noSqlMode={false} />;
- .home-view-card {
- height: 170px;
- }
-}
+ const { container } = render(serversCard);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/views/home-view/servers-card/servers-card.tsx
b/web-console/src/views/home-view/servers-card/servers-card.tsx
new file mode 100644
index 0000000..ee30b32
--- /dev/null
+++ b/web-console/src/views/home-view/servers-card/servers-card.tsx
@@ -0,0 +1,160 @@
+/*
+ * 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 { IconNames } from '@blueprintjs/icons';
+import axios from 'axios';
+import React from 'react';
+
+import { compact, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from
'../../../utils';
+import { HomeViewCard } from '../home-view-card/home-view-card';
+
+export interface ServersCardProps {
+ noSqlMode: boolean;
+}
+
+export interface ServersCardState {
+ serverCountLoading: boolean;
+ coordinatorCount: number;
+ overlordCount: number;
+ routerCount: number;
+ brokerCount: number;
+ historicalCount: number;
+ middleManagerCount: number;
+ peonCount: number;
+ indexerCount: number;
+ serverCountError?: string;
+}
+
+export class ServersCard extends React.PureComponent<ServersCardProps,
ServersCardState> {
+ static renderPluralIfNeededPair(
+ count1: number,
+ singular1: string,
+ count2: number,
+ singular2: string,
+ ): JSX.Element | undefined {
+ const text = compact([
+ count1 ? pluralIfNeeded(count1, singular1) : undefined,
+ count2 ? pluralIfNeeded(count2, singular2) : undefined,
+ ]).join(', ');
+ if (!text) return;
+ return <p>{text}</p>;
+ }
+
+ private serverQueryManager: QueryManager<boolean, any>;
+
+ constructor(props: ServersCardProps, context: any) {
+ super(props, context);
+ this.state = {
+ serverCountLoading: false,
+ coordinatorCount: 0,
+ overlordCount: 0,
+ routerCount: 0,
+ brokerCount: 0,
+ historicalCount: 0,
+ middleManagerCount: 0,
+ peonCount: 0,
+ indexerCount: 0,
+ };
+
+ this.serverQueryManager = new QueryManager({
+ processQuery: async noSqlMode => {
+ if (noSqlMode) {
+ const serversResp = await
axios.get('/druid/coordinator/v1/servers?simple');
+ const middleManagerResp = await
axios.get('/druid/indexer/v1/workers');
+ return {
+ historical: serversResp.data.filter((s: any) => s.type ===
'historical').length,
+ middle_manager: middleManagerResp.data.length,
+ peon: serversResp.data.filter((s: any) => s.type ===
'indexer-executor').length,
+ };
+ } else {
+ const serverCountsFromQuery: {
+ server_type: string;
+ count: number;
+ }[] = await queryDruidSql({
+ query: `SELECT server_type, COUNT(*) as "count" FROM sys.servers
GROUP BY 1`,
+ });
+ return lookupBy(serverCountsFromQuery, x => x.server_type, x =>
x.count);
+ }
+ },
+ onStateChange: ({ result, loading, error }) => {
+ this.setState({
+ serverCountLoading: loading,
+ coordinatorCount: result ? result.coordinator : 0,
+ overlordCount: result ? result.overlord : 0,
+ routerCount: result ? result.router : 0,
+ brokerCount: result ? result.broker : 0,
+ historicalCount: result ? result.historical : 0,
+ middleManagerCount: result ? result.middle_manager : 0,
+ peonCount: result ? result.peon : 0,
+ indexerCount: result ? result.indexer : 0,
+ serverCountError: error,
+ });
+ },
+ });
+ }
+
+ componentDidMount(): void {
+ const { noSqlMode } = this.props;
+
+ this.serverQueryManager.runQuery(noSqlMode);
+ }
+
+ componentWillUnmount(): void {
+ this.serverQueryManager.terminate();
+ }
+
+ render(): JSX.Element {
+ const {
+ serverCountLoading,
+ coordinatorCount,
+ overlordCount,
+ routerCount,
+ brokerCount,
+ historicalCount,
+ middleManagerCount,
+ peonCount,
+ indexerCount,
+ serverCountError,
+ } = this.state;
+ return (
+ <HomeViewCard
+ className="servers-card"
+ href={'#servers'}
+ icon={IconNames.DATABASE}
+ title={'Servers'}
+ loading={serverCountLoading}
+ error={serverCountError}
+ >
+ {ServersCard.renderPluralIfNeededPair(
+ overlordCount,
+ 'overlord',
+ coordinatorCount,
+ 'coordinator',
+ )}
+ {ServersCard.renderPluralIfNeededPair(routerCount, 'router',
brokerCount, 'broker')}
+ {ServersCard.renderPluralIfNeededPair(
+ historicalCount,
+ 'historical',
+ middleManagerCount,
+ 'middle manager',
+ )}
+ {ServersCard.renderPluralIfNeededPair(peonCount, 'peon', indexerCount,
'indexer')}
+ </HomeViewCard>
+ );
+ }
+}
diff --git
a/web-console/src/views/home-view/status-card/__snapshots__/status-card.spec.tsx.snap
b/web-console/src/views/home-view/status-card/__snapshots__/status-card.spec.tsx.snap
new file mode 100644
index 0000000..81789a8
--- /dev/null
+++
b/web-console/src/views/home-view/status-card/__snapshots__/status-card.spec.tsx.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`status card matches snapshot 1`] = `
+<a
+ class="home-view-card status-card"
+>
+ <div
+ class="bp3-card bp3-interactive bp3-elevation-0"
+ >
+ <h5
+ class="bp3-heading"
+ >
+ <span
+ class="bp3-icon bp3-icon-graph"
+ icon="graph"
+ >
+ <svg
+ data-icon="graph"
+ fill="#bfccd5"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ graph
+ </desc>
+ <path
+ d="M14 3c-1.06 0-1.92.83-1.99 1.88l-1.93.97A2.95 2.95 0 008 5c-.56
0-1.08.16-1.52.43L3.97 3.34C3.98 3.23 4 3.12 4 3c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2
2 2c.24 0 .47-.05.68-.13l2.51 2.09C5.08 7.29 5 7.63 5 8c0 .96.46 1.81 1.16
2.35l-.56 1.69c-.91.19-1.6.99-1.6 1.96 0 1.1.9 2 2 2s2-.9
2-2c0-.51-.2-.97-.51-1.32l.56-1.69A2.99 2.99 0 0011
8c0-.12-.02-.24-.04-.36l1.94-.97c.32.21.69.33 1.1.33 1.1 0 2-.9 2-2s-.9-2-2-2z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+
+ Status
+ </h5>
+ <p>
+ Loading...
+ </p>
+ </div>
+</a>
+`;
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/status-card/status-card.spec.tsx
similarity index 70%
copy from web-console/src/views/home-view/home-view.scss
copy to web-console/src/views/home-view/status-card/status-card.spec.tsx
index c2f5bb1..a602e7d 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/status-card/status-card.spec.tsx
@@ -16,19 +16,16 @@
* limitations under the License.
*/
-@import '../../variables';
+import { render } from '@testing-library/react';
+import React from 'react';
-.home-view {
- display: grid;
- grid-gap: $standard-padding;
- grid-template-columns: 1fr 1fr 1fr 1fr;
+import { StatusCard } from './status-card';
- & > a {
- text-decoration: inherit;
- color: inherit;
- }
+describe('status card', () => {
+ it('matches snapshot', () => {
+ const statusCard = <StatusCard />;
- .home-view-card {
- height: 170px;
- }
-}
+ const { container } = render(statusCard);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/views/home-view/status-card/status-card.tsx
b/web-console/src/views/home-view/status-card/status-card.tsx
new file mode 100644
index 0000000..a55247d
--- /dev/null
+++ b/web-console/src/views/home-view/status-card/status-card.tsx
@@ -0,0 +1,103 @@
+/*
+ * 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 { IconNames } from '@blueprintjs/icons';
+import axios from 'axios';
+import React from 'react';
+
+import { StatusDialog } from '../../../dialogs/status-dialog/status-dialog';
+import { QueryManager } from '../../../utils';
+import { HomeViewCard } from '../home-view-card/home-view-card';
+
+export interface StatusCardProps {}
+
+export interface StatusCardState {
+ versionLoading: boolean;
+ version: string;
+ versionError?: string;
+
+ showStatusDialog: boolean;
+}
+
+export class StatusCard extends React.PureComponent<StatusCardProps,
StatusCardState> {
+ private versionQueryManager: QueryManager<null, string>;
+
+ constructor(props: StatusCardProps, context: any) {
+ super(props, context);
+ this.state = {
+ versionLoading: true,
+ version: '',
+
+ showStatusDialog: false,
+ };
+
+ this.versionQueryManager = new QueryManager({
+ processQuery: async () => {
+ const statusResp = await axios.get('/status');
+ return statusResp.data.version;
+ },
+ onStateChange: ({ result, loading, error }) => {
+ this.setState({
+ versionLoading: loading,
+ version: result,
+ versionError: error,
+ });
+ },
+ });
+ }
+
+ componentDidMount(): void {
+ this.versionQueryManager.runQuery(null);
+ }
+
+ componentWillUnmount(): void {
+ this.versionQueryManager.terminate();
+ }
+
+ renderStatusDialog() {
+ const { showStatusDialog } = this.state;
+ if (!showStatusDialog) {
+ return null;
+ }
+ return (
+ <StatusDialog
+ onClose={() => this.setState({ showStatusDialog: false })}
+ title={'Status'}
+ isOpen
+ />
+ );
+ }
+
+ render(): JSX.Element {
+ const { version, versionLoading, versionError } = this.state;
+
+ return (
+ <HomeViewCard
+ className="status-card"
+ onClick={() => this.setState({ showStatusDialog: true })}
+ icon={IconNames.GRAPH}
+ title="Status"
+ loading={versionLoading}
+ error={versionError}
+ >
+ {version ? `Apache Druid is running version ${version}` : ''}
+ {this.renderStatusDialog()}
+ </HomeViewCard>
+ );
+ }
+}
diff --git
a/web-console/src/views/home-view/supervisors-card/__snapshots__/supervisors-card.spec.tsx.snap
b/web-console/src/views/home-view/supervisors-card/__snapshots__/supervisors-card.spec.tsx.snap
new file mode 100644
index 0000000..434726f
--- /dev/null
+++
b/web-console/src/views/home-view/supervisors-card/__snapshots__/supervisors-card.spec.tsx.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`supervisors card matches snapshot 1`] = `
+<a
+ class="home-view-card supervisors-card"
+ href="#tasks"
+>
+ <div
+ class="bp3-card bp3-interactive bp3-elevation-0"
+ >
+ <h5
+ class="bp3-heading"
+ >
+ <span
+ class="bp3-icon bp3-icon-list-columns"
+ icon="list-columns"
+ >
+ <svg
+ data-icon="list-columns"
+ fill="#bfccd5"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ list-columns
+ </desc>
+ <path
+ d="M6 1c.55 0 1 .45 1 1s-.45 1-1 1H1c-.55 0-1-.45-1-1s.45-1
1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1H1c-.55 0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0
1 .45 1 1s-.45 1-1 1H1c-.55 0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45
1-1 1H1c-.55 0-1-.45-1-1s.45-1 1-1h5zm9-12c.55 0 1 .45 1 1s-.45 1-1 1h-5c-.55
0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1h-5c-.55
0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1h-5c-.55
0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1h [...]
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+
+ Supervisors
+ </h5>
+ <p>
+ Loading...
+ </p>
+ </div>
+</a>
+`;
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/supervisors-card/supervisors-card.spec.tsx
similarity index 69%
copy from web-console/src/views/home-view/home-view.scss
copy to
web-console/src/views/home-view/supervisors-card/supervisors-card.spec.tsx
index c2f5bb1..d2b0adf 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/supervisors-card/supervisors-card.spec.tsx
@@ -16,19 +16,16 @@
* limitations under the License.
*/
-@import '../../variables';
+import { render } from '@testing-library/react';
+import React from 'react';
-.home-view {
- display: grid;
- grid-gap: $standard-padding;
- grid-template-columns: 1fr 1fr 1fr 1fr;
+import { SupervisorsCard } from './supervisors-card';
- & > a {
- text-decoration: inherit;
- color: inherit;
- }
+describe('supervisors card', () => {
+ it('matches snapshot', () => {
+ const supervisorsCard = <SupervisorsCard />;
- .home-view-card {
- height: 170px;
- }
-}
+ const { container } = render(supervisorsCard);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git
a/web-console/src/views/home-view/supervisors-card/supervisors-card.tsx
b/web-console/src/views/home-view/supervisors-card/supervisors-card.tsx
new file mode 100644
index 0000000..7bbe1d0
--- /dev/null
+++ b/web-console/src/views/home-view/supervisors-card/supervisors-card.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 { IconNames } from '@blueprintjs/icons';
+import axios from 'axios';
+import React from 'react';
+
+import { pluralIfNeeded, QueryManager } from '../../../utils';
+import { HomeViewCard } from '../home-view-card/home-view-card';
+
+export interface SupervisorsCardProps {}
+
+export interface SupervisorsCardState {
+ supervisorCountLoading: boolean;
+ runningSupervisorCount: number;
+ suspendedSupervisorCount: number;
+ supervisorCountError?: string;
+}
+
+export class SupervisorsCard extends React.PureComponent<
+ SupervisorsCardProps,
+ SupervisorsCardState
+> {
+ private supervisorQueryManager: QueryManager<null, any>;
+
+ constructor(props: SupervisorsCardProps, context: any) {
+ super(props, context);
+ this.state = {
+ supervisorCountLoading: false,
+ runningSupervisorCount: 0,
+ suspendedSupervisorCount: 0,
+ };
+
+ this.supervisorQueryManager = new QueryManager({
+ processQuery: async () => {
+ const resp = await axios.get('/druid/indexer/v1/supervisor?full');
+ const data = resp.data;
+ const runningSupervisorCount = data.filter((d: any) =>
d.spec.suspended === false).length;
+ const suspendedSupervisorCount = data.filter((d: any) =>
d.spec.suspended === true).length;
+ return {
+ runningSupervisorCount,
+ suspendedSupervisorCount,
+ };
+ },
+ onStateChange: ({ result, loading, error }) => {
+ this.setState({
+ runningSupervisorCount: result ? result.runningSupervisorCount : 0,
+ suspendedSupervisorCount: result ? result.suspendedSupervisorCount :
0,
+ supervisorCountLoading: loading,
+ supervisorCountError: error,
+ });
+ },
+ });
+ }
+
+ componentDidMount(): void {
+ this.supervisorQueryManager.runQuery(null);
+ }
+
+ componentWillUnmount(): void {
+ this.supervisorQueryManager.terminate();
+ }
+
+ render(): JSX.Element {
+ const {
+ supervisorCountLoading,
+ supervisorCountError,
+ runningSupervisorCount,
+ suspendedSupervisorCount,
+ } = this.state;
+
+ return (
+ <HomeViewCard
+ className="supervisors-card"
+ href={'#tasks'}
+ icon={IconNames.LIST_COLUMNS}
+ title={'Supervisors'}
+ loading={supervisorCountLoading}
+ error={supervisorCountError}
+ >
+ {!Boolean(runningSupervisorCount + suspendedSupervisorCount) && <p>No
supervisors</p>}
+ {Boolean(runningSupervisorCount) && (
+ <p>{pluralIfNeeded(runningSupervisorCount, 'running supervisor')}</p>
+ )}
+ {Boolean(suspendedSupervisorCount) && (
+ <p>{pluralIfNeeded(suspendedSupervisorCount, 'suspended
supervisor')}</p>
+ )}
+ </HomeViewCard>
+ );
+ }
+}
diff --git
a/web-console/src/views/home-view/tasks-card/__snapshots__/tasks-card.spec.tsx.snap
b/web-console/src/views/home-view/tasks-card/__snapshots__/tasks-card.spec.tsx.snap
new file mode 100644
index 0000000..04afaf6
--- /dev/null
+++
b/web-console/src/views/home-view/tasks-card/__snapshots__/tasks-card.spec.tsx.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`tasks card matches snapshot 1`] = `
+<a
+ class="home-view-card tasks-card"
+ href="#tasks"
+>
+ <div
+ class="bp3-card bp3-interactive bp3-elevation-0"
+ >
+ <h5
+ class="bp3-heading"
+ >
+ <span
+ class="bp3-icon bp3-icon-gantt-chart"
+ icon="gantt-chart"
+ >
+ <svg
+ data-icon="gantt-chart"
+ fill="#bfccd5"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ gantt-chart
+ </desc>
+ <path
+ d="M10 10c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1h-4c-.55 0-1
.45-1 1zM6 7c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1zm9
5H2V3c0-.55-.45-1-1-1s-1 .45-1 1v10c0 .55.45 1 1 1h14c.55 0 1-.45
1-1s-.45-1-1-1zM4 5h3c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+
+ Tasks
+ </h5>
+ <p>
+ Loading...
+ </p>
+ </div>
+</a>
+`;
diff --git a/web-console/src/views/home-view/home-view.scss
b/web-console/src/views/home-view/tasks-card/tasks-card.spec.tsx
similarity index 70%
copy from web-console/src/views/home-view/home-view.scss
copy to web-console/src/views/home-view/tasks-card/tasks-card.spec.tsx
index c2f5bb1..14f1ea5 100644
--- a/web-console/src/views/home-view/home-view.scss
+++ b/web-console/src/views/home-view/tasks-card/tasks-card.spec.tsx
@@ -16,19 +16,16 @@
* limitations under the License.
*/
-@import '../../variables';
+import { render } from '@testing-library/react';
+import React from 'react';
-.home-view {
- display: grid;
- grid-gap: $standard-padding;
- grid-template-columns: 1fr 1fr 1fr 1fr;
+import { TasksCard } from './tasks-card';
- & > a {
- text-decoration: inherit;
- color: inherit;
- }
+describe('tasks card', () => {
+ it('matches snapshot', () => {
+ const tasksCard = <TasksCard noSqlMode={false} />;
- .home-view-card {
- height: 170px;
- }
-}
+ const { container } = render(tasksCard);
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/views/home-view/tasks-card/tasks-card.tsx
b/web-console/src/views/home-view/tasks-card/tasks-card.tsx
new file mode 100644
index 0000000..375f106
--- /dev/null
+++ b/web-console/src/views/home-view/tasks-card/tasks-card.tsx
@@ -0,0 +1,138 @@
+/*
+ * 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 { IconNames } from '@blueprintjs/icons';
+import axios from 'axios';
+import React from 'react';
+
+import { lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from
'../../../utils';
+import { HomeViewCard } from '../home-view-card/home-view-card';
+
+export interface TasksCardProps {
+ noSqlMode: boolean;
+}
+
+export interface TasksCardState {
+ taskCountLoading: boolean;
+ runningTaskCount: number;
+ pendingTaskCount: number;
+ successTaskCount: number;
+ failedTaskCount: number;
+ waitingTaskCount: number;
+ taskCountError?: string;
+}
+
+export class TasksCard extends React.PureComponent<TasksCardProps,
TasksCardState> {
+ private taskQueryManager: QueryManager<boolean, any>;
+
+ constructor(props: TasksCardProps, context: any) {
+ super(props, context);
+ this.state = {
+ taskCountLoading: false,
+ runningTaskCount: 0,
+ pendingTaskCount: 0,
+ successTaskCount: 0,
+ failedTaskCount: 0,
+ waitingTaskCount: 0,
+ };
+
+ this.taskQueryManager = new QueryManager({
+ processQuery: async noSqlMode => {
+ if (noSqlMode) {
+ const completeTasksResp = await
axios.get('/druid/indexer/v1/completeTasks');
+ const runningTasksResp = await
axios.get('/druid/indexer/v1/runningTasks');
+ const pendingTasksResp = await
axios.get('/druid/indexer/v1/pendingTasks');
+ const waitingTasksResp = await
axios.get('/druid/indexer/v1/waitingTasks');
+ return {
+ SUCCESS: completeTasksResp.data.filter((d: any) => d.status ===
'SUCCESS').length,
+ FAILED: completeTasksResp.data.filter((d: any) => d.status ===
'FAILED').length,
+ RUNNING: runningTasksResp.data.length,
+ PENDING: pendingTasksResp.data.length,
+ WAITING: waitingTasksResp.data.length,
+ };
+ } else {
+ const taskCountsFromQuery: { status: string; count: number }[] =
await queryDruidSql({
+ query: `SELECT
+ CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS
"status",
+ COUNT (*) AS "count"
+FROM sys.tasks
+GROUP BY 1`,
+ });
+ return lookupBy(taskCountsFromQuery, x => x.status, x => x.count);
+ }
+ },
+ onStateChange: ({ result, loading, error }) => {
+ this.setState({
+ taskCountLoading: loading,
+ successTaskCount: result ? result.SUCCESS : 0,
+ failedTaskCount: result ? result.FAILED : 0,
+ runningTaskCount: result ? result.RUNNING : 0,
+ pendingTaskCount: result ? result.PENDING : 0,
+ waitingTaskCount: result ? result.WAITING : 0,
+ taskCountError: error,
+ });
+ },
+ });
+ }
+
+ componentDidMount(): void {
+ const { noSqlMode } = this.props;
+
+ this.taskQueryManager.runQuery(noSqlMode);
+ }
+
+ componentWillUnmount(): void {
+ this.taskQueryManager.terminate();
+ }
+
+ render(): JSX.Element {
+ const {
+ taskCountError,
+ taskCountLoading,
+ runningTaskCount,
+ pendingTaskCount,
+ successTaskCount,
+ failedTaskCount,
+ waitingTaskCount,
+ } = this.state;
+
+ return (
+ <HomeViewCard
+ className="tasks-card"
+ href={'#tasks'}
+ icon={IconNames.GANTT_CHART}
+ title={'Tasks'}
+ loading={taskCountLoading}
+ error={taskCountError}
+ >
+ {Boolean(runningTaskCount) && <p>{pluralIfNeeded(runningTaskCount,
'running task')}</p>}
+ {Boolean(pendingTaskCount) && <p>{pluralIfNeeded(pendingTaskCount,
'pending task')}</p>}
+ {Boolean(successTaskCount) && <p>{pluralIfNeeded(successTaskCount,
'successful task')}</p>}
+ {Boolean(waitingTaskCount) && <p>{pluralIfNeeded(waitingTaskCount,
'waiting task')}</p>}
+ {Boolean(failedTaskCount) && <p>{pluralIfNeeded(failedTaskCount,
'failed task')}</p>}
+ {!(
+ Boolean(runningTaskCount) ||
+ Boolean(pendingTaskCount) ||
+ Boolean(successTaskCount) ||
+ Boolean(waitingTaskCount) ||
+ Boolean(failedTaskCount)
+ ) && <p>There are no tasks</p>}
+ </HomeViewCard>
+ );
+ }
+}
diff --git
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index 077e7dd..cb49c4e 100755
---
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -12,6 +12,23 @@ exports[`segments-view matches snapshot 1`] = `
localStorageKey="segments-refresh-rate"
onRefresh={[Function]}
/>
+ <Component>
+ Group by
+ </Component>
+ <Blueprint3.ButtonGroup>
+ <Blueprint3.Button
+ active={true}
+ onClick={[Function]}
+ >
+ None
+ </Blueprint3.Button>
+ <Blueprint3.Button
+ active={false}
+ onClick={[Function]}
+ >
+ Interval
+ </Blueprint3.Button>
+ </Blueprint3.ButtonGroup>
<Blueprint3.Popover
boundary="scrollParent"
captureDismiss={false}
@@ -24,7 +41,7 @@ exports[`segments-view matches snapshot 1`] = `
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
- text="See in SQL view"
+ text="View SQL query for table"
/>
</Blueprint3.Menu>
}
@@ -49,23 +66,6 @@ exports[`segments-view matches snapshot 1`] = `
icon="more"
/>
</Blueprint3.Popover>
- <Component>
- Group by
- </Component>
- <Blueprint3.ButtonGroup>
- <Blueprint3.Button
- active={true}
- onClick={[Function]}
- >
- None
- </Blueprint3.Button>
- <Blueprint3.Button
- active={false}
- onClick={[Function]}
- >
- Interval
- </Blueprint3.Button>
- </Blueprint3.ButtonGroup>
<TableColumnSelector
columns={
Array [
diff --git a/web-console/src/views/segments-view/segments-view.tsx
b/web-console/src/views/segments-view/segments-view.tsx
index 6e10b68..1e822d6 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -37,6 +37,7 @@ import { AsyncActionDialog } from '../../dialogs';
import { SegmentTableActionDialog } from
'../../dialogs/segments-table-action-dialog/segment-table-action-dialog';
import {
addFilter,
+ compact,
filterMap,
formatBytes,
formatNumber,
@@ -180,24 +181,28 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
});
let queryParts: string[];
+
+ let whereClause = '';
+ if (whereParts.length) {
+ whereClause = whereParts.join(' AND ');
+ }
+
if (query.groupByInterval) {
- queryParts = [
+ queryParts = compact([
`SELECT`,
` ("start" || '/' || "end") AS "interval",`,
` "segment_id", "datasource", "start", "end", "size", "version",
"partition_num", "num_replicas", "num_rows", "is_published", "is_available",
"is_realtime", "is_overshadowed", "payload"`,
`FROM sys.segments`,
`WHERE`,
- ];
- if (whereParts.length) {
- queryParts.push(whereParts.join(' AND ') + 'AND');
- }
- queryParts.push(
- ` ("start" || '/' || "end") IN (SELECT "start" || '/' || "end"
FROM sys.segments GROUP BY 1 LIMIT ${totalQuerySize})`,
- );
-
- if (whereParts.length) {
- queryParts.push('AND ' + whereParts.join(' AND '));
- }
+ ` ("start" || '/' || "end") IN (`,
+ ` SELECT "start" || '/' || "end"`,
+ ` FROM sys.segments`,
+ whereClause ? ` WHERE ${whereClause}` : '',
+ ` GROUP BY 1`,
+ ` LIMIT ${totalQuerySize}`,
+ ` )`,
+ whereClause ? ` AND ${whereClause}` : '',
+ ]);
if (query.sorted.length) {
queryParts.push(
@@ -215,8 +220,8 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
`FROM sys.segments`,
];
- if (whereParts.length) {
- queryParts.push('WHERE ' + whereParts.join(' AND '));
+ if (whereClause) {
+ queryParts.push(`WHERE ${whereClause}`);
}
if (query.sorted.length) {
@@ -625,7 +630,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
{!noSqlMode && (
<MenuItem
icon={IconNames.APPLICATION}
- text="See in SQL view"
+ text="View SQL query for table"
disabled={!lastSegmentsQuery}
onClick={() => {
if (!lastSegmentsQuery) return;
@@ -667,7 +672,6 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
}
localStorageKey={LocalStorageKeys.SEGMENTS_REFRESH_RATE}
/>
- {this.renderBulkSegmentsActions()}
<Label>Group by</Label>
<ButtonGroup>
<Button
@@ -689,6 +693,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
Interval
</Button>
</ButtonGroup>
+ {this.renderBulkSegmentsActions()}
<TableColumnSelector
columns={noSqlMode ? tableColumnsNoSql : tableColumns}
onChange={column => this.setState({ hiddenColumns:
hiddenColumns.toggle(column) })}
diff --git
a/web-console/src/views/servers-view/__snapshots__/servers-view.spec.tsx.snap
b/web-console/src/views/servers-view/__snapshots__/servers-view.spec.tsx.snap
index c64a85e..4e927cb 100755
---
a/web-console/src/views/servers-view/__snapshots__/servers-view.spec.tsx.snap
+++
b/web-console/src/views/servers-view/__snapshots__/servers-view.spec.tsx.snap
@@ -46,7 +46,7 @@ exports[`servers view action servers view 1`] = `
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
- text="See in SQL view"
+ text="View SQL query for table"
/>
</Blueprint3.Menu>
}
diff --git a/web-console/src/views/servers-view/servers-view.tsx
b/web-console/src/views/servers-view/servers-view.tsx
index 9478bb4..a53da2e 100644
--- a/web-console/src/views/servers-view/servers-view.tsx
+++ b/web-console/src/views/servers-view/servers-view.tsx
@@ -627,7 +627,7 @@ ORDER BY "rank" DESC, "server" DESC`;
{!noSqlMode && (
<MenuItem
icon={IconNames.APPLICATION}
- text="See in SQL view"
+ text="View SQL query for table"
onClick={() => goToQuery(ServersView.SERVER_SQL)}
/>
)}
diff --git
a/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
b/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
index 397bf16..a64c9b7 100644
--- a/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
+++ b/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
@@ -374,12 +374,21 @@ exports[`tasks view matches snapshot 1`] = `
<Blueprint3.Menu>
<Blueprint3.MenuItem
disabled={false}
- icon="application"
+ icon="cloud-upload"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
- text="See in SQL view"
+ text="Go to data loader"
+ />
+ <Blueprint3.MenuItem
+ disabled={false}
+ icon="manually-entered-data"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={Object {}}
+ shouldDismissPopover={true}
+ text="Submit JSON task"
/>
</Blueprint3.Menu>
}
@@ -401,7 +410,8 @@ exports[`tasks view matches snapshot 1`] = `
wrapperTagName="span"
>
<Blueprint3.Button
- icon="more"
+ icon="plus"
+ text="Submit task"
/>
</Blueprint3.Popover>
<Blueprint3.Popover
@@ -411,21 +421,12 @@ exports[`tasks view matches snapshot 1`] = `
<Blueprint3.Menu>
<Blueprint3.MenuItem
disabled={false}
- icon="cloud-upload"
- multiline={false}
- onClick={[Function]}
- popoverProps={Object {}}
- shouldDismissPopover={true}
- text="Go to data loader"
- />
- <Blueprint3.MenuItem
- disabled={false}
- icon="manually-entered-data"
+ icon="application"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
- text="Submit JSON task"
+ text="View SQL query for table"
/>
</Blueprint3.Menu>
}
@@ -447,8 +448,7 @@ exports[`tasks view matches snapshot 1`] = `
wrapperTagName="span"
>
<Blueprint3.Button
- icon="plus"
- text="Submit task"
+ icon="more"
/>
</Blueprint3.Popover>
<TableColumnSelector
diff --git a/web-console/src/views/task-view/tasks-view.tsx
b/web-console/src/views/task-view/tasks-view.tsx
index 248eee7..af4e7eb 100644
--- a/web-console/src/views/task-view/tasks-view.tsx
+++ b/web-console/src/views/task-view/tasks-view.tsx
@@ -1018,7 +1018,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
{!noSqlMode && (
<MenuItem
icon={IconNames.APPLICATION}
- text="See in SQL view"
+ text="View SQL query for table"
onClick={() => goToQuery(TasksView.TASK_SQL)}
/>
)}
@@ -1146,10 +1146,10 @@ ORDER BY "rank" DESC, "created_time" DESC`;
localStorageKey={LocalStorageKeys.TASKS_REFRESH_RATE}
onRefresh={auto => this.taskQueryManager.rerunLastQuery(auto)}
/>
- {this.renderBulkTasksActions()}
<Popover content={submitTaskMenu}
position={Position.BOTTOM_LEFT}>
<Button icon={IconNames.PLUS} text="Submit task" />
</Popover>
+ {this.renderBulkTasksActions()}
<TableColumnSelector
columns={taskTableColumns}
onChange={column =>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]