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} />
+            &nbsp;{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} />
-            &nbsp;{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]

Reply via email to