Explorar el Código

[frontend] Display Impala query history details (#3226)

* [frontend] Display Impala query history details

* [frontend] Display Impala query history details

* [frontend] Display Impala query history details

* [frontend] Added ImpalaQueryProfile type

* [frontend] Added UTs for ImpalaQueries

* [frontend] Pruning query table components

* [frontend] Added UTs for LabeledInfo

* [frontend] Added UTs for QueryDetails

* [frontend] Added UTs for QueryDetailsDiff

* [frontend] Added UTs for Queryinfo

* [frontend] Added UTs for QueryInfoTop

* [frontend] Added UTs for QueryTable

* [frontend] QueryDetailsDiff - Merics were not displayed

* [frontend] QueryTable - Changing Impala status to display

* [frontend] QueryTable - Adding defaultDb & requestPool columns

* [frontend] Queries search UI polish - Filter UX is improper

* [frontend] Fixing UTs

* [frontend] Query store config verbiage change - ENABLE to IS_ENABLED

* [frontend] Hive Queries tab verbiage change

* [frontend] Fixing internationalization

* [frontend] Addressing review comments

* [frontend] Addressing review comments

---------

Co-authored-by: Johan Åhlén <johan@johanahlen.com>
Co-authored-by: Ying Chen <106372+wing2fly@users.noreply.github.com>
Sreenath Somarajapuram hace 2 años
padre
commit
0319ab57e4
Se han modificado 45 ficheros con 3205 adiciones y 52 borrados
  1. 13 10
      apps/jobbrowser/src/jobbrowser/conf.py
  2. 24 11
      apps/jobbrowser/src/jobbrowser/templates/job_browser.mako
  3. 3 4
      desktop/conf.dist/hue.ini
  4. 5 5
      desktop/conf/pseudo-distributed.ini.tmpl
  5. 1 0
      desktop/core/src/desktop/js/apps/jobBrowser/app.js
  6. 65 0
      desktop/core/src/desktop/js/apps/jobBrowser/commons/api-utils/api.test.ts
  7. 41 0
      desktop/core/src/desktop/js/apps/jobBrowser/commons/api-utils/api.ts
  8. 44 0
      desktop/core/src/desktop/js/apps/jobBrowser/commons/api-utils/search.ts
  9. 36 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/ImpalaQueries.test.ts
  10. 189 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/ImpalaQueries.vue
  11. 14 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/__snapshots__/ImpalaQueries.test.ts.snap
  12. 43 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/api/query.ts
  13. 84 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/index.d.ts
  14. 32 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/LabeledInfo.test.ts
  15. 83 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/LabeledInfo.vue
  16. 41 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryDetails.test.ts
  17. 193 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryDetails.vue
  18. 48 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryDetailsDiff.test.ts
  19. 206 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryDetailsDiff.vue
  20. 34 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryInfo.test.ts
  21. 154 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryInfo.vue
  22. 36 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryInfoTop.test.ts
  23. 87 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryInfoTop.vue
  24. 22 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/LabeledInfo.test.ts.snap
  25. 36 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/QueryDetails.test.ts.snap
  26. 30 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/QueryDetailsDiff.test.ts.snap
  27. 53 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/QueryInfo.test.ts.snap
  28. 20 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/QueryInfoTop.test.ts.snap
  29. 85 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__test_data__/impala_query_1.json
  30. 85 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__test_data__/impala_query_2.json
  31. 29 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/CounterSet.ts
  32. 33 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/CountersTable.test.ts
  33. 143 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/CountersTable.vue
  34. 90 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/VarianceCell.vue
  35. 235 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/__snapshots__/CountersTable.test.ts.snap
  36. 57 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/__test_data__/counters.json
  37. 31 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-table/QueryTable.test.ts
  38. 421 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-table/QueryTable.vue
  39. 202 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-table/SortSelector.vue
  40. 104 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-table/__snapshots__/QueryTable.test.ts.snap
  41. 24 0
      desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/webcomp.ts
  42. 18 20
      desktop/core/src/desktop/js/components/FacetSelector.vue
  43. 2 0
      desktop/core/src/desktop/js/components/StatusIndicator.vue
  44. 7 0
      desktop/core/src/desktop/js/components/styles/mixins.scss
  45. 2 2
      desktop/core/src/desktop/models.py

+ 13 - 10
apps/jobbrowser/src/jobbrowser/conf.py

@@ -77,13 +77,6 @@ ENABLE_HIVE_QUERY_BROWSER = Config(
   default=False
   default=False
 )
 )
 
 
-ENABLE_QUERIES_LIST = Config(
-  key="enable_queries_list",
-  help=_("Show the Queries section for listing Hive/Impala query history and providing more troubleshooting information."),
-  type=coerce_bool,
-  default=False
-)
-
 ENABLE_HISTORY_V2 = Config(
 ENABLE_HISTORY_V2 = Config(
   key="enable_history_v2",
   key="enable_history_v2",
   help=_("Show the version 2 of job/query History which unifies the all into one."),
   help=_("Show the version 2 of job/query History which unifies the all into one."),
@@ -91,15 +84,25 @@ ENABLE_HISTORY_V2 = Config(
   default=False
   default=False
 )
 )
 
 
+def is_query_store_url_set():
+  """Check if query store url is configured"""
+  return QUERY_STORE.SERVER_URL.get() != ''
+
 QUERY_STORE = ConfigSection(
 QUERY_STORE = ConfigSection(
   key="query_store",
   key="query_store",
-  help=_("""Credentials for query store API."""),
+  help=_("Configs for managing query store interface."),
   members=dict(
   members=dict(
     SERVER_URL=Config(
     SERVER_URL=Config(
       key="server_url",
       key="server_url",
-      default='http://localhost:8080/',
+      default='',
       help=_("URL of Query Store API server.")
       help=_("URL of Query Store API server.")
-    )
+    ),
+    IS_ENABLED=Config(
+      key="is_enabled",
+      type=coerce_bool,
+      dynamic_default=is_query_store_url_set,
+      help=_("Visualize Hive/Impala query history data from Query Store.")
+    ),
   )
   )
 )
 )
 
 

+ 24 - 11
apps/jobbrowser/src/jobbrowser/templates/job_browser.mako

@@ -23,7 +23,7 @@ from impala.conf import COORDINATOR_URL as IMPALA_COORDINATOR_URL
 from metadata.conf import PROMETHEUS
 from metadata.conf import PROMETHEUS
 from notebook.conf import ENABLE_QUERY_SCHEDULING
 from notebook.conf import ENABLE_QUERY_SCHEDULING
 
 
-from jobbrowser.conf import DISABLE_KILLING_JOBS, MAX_JOB_FETCH, ENABLE_QUERY_BROWSER, ENABLE_HIVE_QUERY_BROWSER, ENABLE_QUERIES_LIST, ENABLE_HISTORY_V2
+from jobbrowser.conf import DISABLE_KILLING_JOBS, MAX_JOB_FETCH, ENABLE_QUERY_BROWSER, ENABLE_HIVE_QUERY_BROWSER, QUERY_STORE, ENABLE_HISTORY_V2
 
 
 from webpack_loader.templatetags.webpack_loader import render_bundle
 from webpack_loader.templatetags.webpack_loader import render_bundle
 
 
@@ -334,7 +334,7 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
               <div data-bind="template: { name: 'breadcrumbs${ SUFFIX }' }"></div>
               <div data-bind="template: { name: 'breadcrumbs${ SUFFIX }' }"></div>
               <!-- /ko -->
               <!-- /ko -->
 
 
-              <!-- ko if: interface() !== 'slas' && interface() !== 'oozie-info' && interface() !== 'queries'-->
+              <!-- ko if: interface() !== 'slas' && interface() !== 'oozie-info' && interface() !== 'hive-queries' && interface() !== 'impala-queries'-->
               <!-- ko if: !$root.job() -->
               <!-- ko if: !$root.job() -->
               <form class="form-inline">
               <form class="form-inline">
                 <!-- ko if: !$root.isMini() && interface() == 'queries-impala' -->
                 <!-- ko if: !$root.isMini() && interface() == 'queries-impala' -->
@@ -486,10 +486,14 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
               <!-- /ko -->
               <!-- /ko -->
               <div id="slas" data-bind="visible: interface() === 'slas'"></div>
               <div id="slas" data-bind="visible: interface() === 'slas'"></div>
 
 
-              <!-- ko if: interface() === 'queries' -->
+              <!-- ko if: interface() === 'hive-queries' -->
                 <queries-list></queries-list>
                 <queries-list></queries-list>
               <!-- /ko -->
               <!-- /ko -->
 
 
+              <!-- ko if: interface() === 'impala-queries' -->
+                <impala-queries></impala-queries>
+              <!-- /ko -->
+
               <!-- ko if: interface() === 'oozie-info' -->
               <!-- ko if: interface() === 'oozie-info' -->
                 <!-- ko hueSpinner: { spin: oozieInfoLoading(), center: true, size: 'xlarge' } --><!-- /ko -->
                 <!-- ko hueSpinner: { spin: oozieInfoLoading(), center: true, size: 'xlarge' } --><!-- /ko -->
               <!-- /ko -->
               <!-- /ko -->
@@ -3707,7 +3711,7 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
       });
       });
 
 
       self.fetchJobs = function () {
       self.fetchJobs = function () {
-        if(vm.interface() === 'queries') {
+        if(vm.interface() === 'hive-queries' || vm.interface() === 'impala-queries') {
           return;
           return;
         }
         }
 
 
@@ -3731,7 +3735,7 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
       }
       }
 
 
       self.updateJobs = function () {
       self.updateJobs = function () {
-        if(vm.interface() === 'queries') {
+        if(vm.interface() === 'hive-queries' || vm.interface() === 'impala-queries') {
           return;
           return;
         }
         }
 
 
@@ -3914,6 +3918,10 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
       });
       });
 
 
       self.availableInterfaces = ko.pureComputed(function () {
       self.availableInterfaces = ko.pureComputed(function () {
+        var isDialectEnabled = function (dialect) {
+          return self.appConfig()?.editor?.interpreter_names?.indexOf(dialect) >= 0;
+        };
+
         var historyInterfaceCondition = function () {
         var historyInterfaceCondition = function () {
           return '${ ENABLE_HISTORY_V2.get() }' == 'True';
           return '${ ENABLE_HISTORY_V2.get() }' == 'True';
         };
         };
@@ -3953,8 +3961,11 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
         var scheduleHiveInterfaceCondition = function () {
         var scheduleHiveInterfaceCondition = function () {
           return '${ ENABLE_QUERY_SCHEDULING.get() }' == 'True';
           return '${ ENABLE_QUERY_SCHEDULING.get() }' == 'True';
         };
         };
-        var queriesInterfaceCondition = function () {
-          return '${ ENABLE_QUERIES_LIST.get() }' == 'True';
+        var hiveQueriesInterfaceCondition = function () {
+          return '${ QUERY_STORE.IS_ENABLED.get() }' == 'True' && isDialectEnabled('hive');
+        };
+        var impalaQueriesInterfaceCondition = function () {
+          return '${ QUERY_STORE.IS_ENABLED.get() }' == 'True' && isDialectEnabled('impala');
         };
         };
 
 
         var interfaces = [
         var interfaces = [
@@ -3974,7 +3985,8 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
           {'interface': 'bundles', 'label': '${ _ko('Bundles') }', 'condition': schedulerExtraInterfaceCondition},
           {'interface': 'bundles', 'label': '${ _ko('Bundles') }', 'condition': schedulerExtraInterfaceCondition},
           {'interface': 'slas', 'label': '${ _ko('SLAs') }', 'condition': schedulerExtraInterfaceCondition},
           {'interface': 'slas', 'label': '${ _ko('SLAs') }', 'condition': schedulerExtraInterfaceCondition},
           {'interface': 'livy-sessions', 'label': '${ _ko('Livy') }', 'condition': livyInterfaceCondition},
           {'interface': 'livy-sessions', 'label': '${ _ko('Livy') }', 'condition': livyInterfaceCondition},
-          {'interface': 'queries', 'label': '${ _ko('Queries') }', 'condition': queriesInterfaceCondition},
+          {'interface': 'hive-queries', 'label': '${ _ko('Hive Queries') }', 'condition': hiveQueriesInterfaceCondition},
+          {'interface': 'impala-queries', 'label': '${ _ko('Impala Queries') }', 'condition': impalaQueriesInterfaceCondition},
         ];
         ];
 
 
         return interfaces.filter(function (i) {
         return interfaces.filter(function (i) {
@@ -4062,7 +4074,7 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
             self.loadOozieInfoPage();
             self.loadOozieInfoPage();
           % endif
           % endif
         }
         }
-        else if(interface !== 'queries'){
+        else if(interface !== 'hive-queries' || interface !== 'impala-queries'){
           self.jobs.fetchJobs();
           self.jobs.fetchJobs();
         }
         }
       };
       };
@@ -4093,7 +4105,7 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
         window.clearTimeout(updateJobsTimeout);
         window.clearTimeout(updateJobsTimeout);
         jobUpdateCounter = 0;
         jobUpdateCounter = 0;
         exponentialFactor = 1;
         exponentialFactor = 1;
-        if (self.interface() && self.interface() !== 'slas' && self.interface() !== 'oozie-info' && self.interface !== 'queries'){
+        if (self.interface() && self.interface() !== 'slas' && self.interface() !== 'oozie-info' && self.interface !== 'hive-queries' && self.interface !== 'impala-queries'){
           if (job) {
           if (job) {
             if (job.apiStatus() === 'RUNNING') {
             if (job.apiStatus() === 'RUNNING') {
               var _updateJob = function () {
               var _updateJob = function () {
@@ -4180,7 +4192,8 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
           case 'engines':
           case 'engines':
           case 'dataeng-jobs':
           case 'dataeng-jobs':
           case 'livy-sessions':
           case 'livy-sessions':
-          case 'queries':
+          case 'hive-queries':
+          case 'impala-queries':
             self.selectInterface(h);
             self.selectInterface(h);
             break;
             break;
           default:
           default:

+ 3 - 4
desktop/conf.dist/hue.ini

@@ -1797,16 +1797,15 @@ submit_to=True
 # Show the Hive section for listing the query history and providing more troubleshooting information.
 # Show the Hive section for listing the query history and providing more troubleshooting information.
 ## enable_hive_query_browser=false
 ## enable_hive_query_browser=false
 
 
-# Show the Queries section for listing Hive/Impala query history and providing more troubleshooting information.
-## enable_queries_list=false
-
 # Use the proxy API instead of the ORM to access the query_store.
 # Use the proxy API instead of the ORM to access the query_store.
 ## use_proxy=true
 ## use_proxy=true
 
 
 [[query_store]]
 [[query_store]]
 # URL of Query Store API server.
 # URL of Query Store API server.
-##server_url=http://localhost:8080/
+##server_url=
 
 
+# Show the Hive/Impala queries UI. The value is automatically set to false if server_url is empty, else true.
+##is_enabled=false
 
 
 ###########################################################################
 ###########################################################################
 # Settings to configure Sentry / Security App.
 # Settings to configure Sentry / Security App.

+ 5 - 5
desktop/conf/pseudo-distributed.ini.tmpl

@@ -1240,7 +1240,7 @@
 
 
   # Size, in bytes, of the chunks Django should store into memory and feed into the handler. Default is 64MB.
   # Size, in bytes, of the chunks Django should store into memory and feed into the handler. Default is 64MB.
   ## upload_chunk_size=64*1024*1024
   ## upload_chunk_size=64*1024*1024
-  
+
   # Configuration for YARN (MR2)
   # Configuration for YARN (MR2)
   # ------------------------------------------------------------------------
   # ------------------------------------------------------------------------
   [[yarn_clusters]]
   [[yarn_clusters]]
@@ -1781,15 +1781,15 @@
   # Show the Hive section for listing the query history and providing more troubleshooting information.
   # Show the Hive section for listing the query history and providing more troubleshooting information.
   ## enable_hive_query_browser=false
   ## enable_hive_query_browser=false
 
 
-  # Show the Queries section for listing Hive/Impala query history and providing more troubleshooting information.
-  ## enable_queries_list=false
-
   # Use the proxy API instead of the ORM to access the query_store.
   # Use the proxy API instead of the ORM to access the query_store.
   ## use_proxy=true
   ## use_proxy=true
 
 
   [[query_store]]
   [[query_store]]
     # URL of Query Store API server.
     # URL of Query Store API server.
-    ##server_url=http://localhost:8080/
+    ##server_url=
+
+    # Show the Hive/Impala queries UI. The value is automatically set to false if server_url is empty, else true.
+    ##is_enabled=false
 
 
 ###########################################################################
 ###########################################################################
 # Settings to configure Sentry / Security App.
 # Settings to configure Sentry / Security App.

+ 1 - 0
desktop/core/src/desktop/js/apps/jobBrowser/app.js

@@ -17,6 +17,7 @@
 import huePubSub from 'utils/huePubSub';
 import huePubSub from 'utils/huePubSub';
 import './components/hiveQueryPlan/webcomp';
 import './components/hiveQueryPlan/webcomp';
 import './components/queriesList/webcomp';
 import './components/queriesList/webcomp';
+import './components/impalaQueries/webcomp';
 
 
 huePubSub.subscribe('app.dom.loaded', app => {
 huePubSub.subscribe('app.dom.loaded', app => {
   if (app !== 'jobbrowser') {
   if (app !== 'jobbrowser') {

+ 65 - 0
desktop/core/src/desktop/js/apps/jobBrowser/commons/api-utils/api.test.ts

@@ -0,0 +1,65 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 api from './api';
+import { ApiError } from './api';
+import { AxiosAdapter, AxiosPromise, AxiosRequestConfig } from 'axios';
+
+const TEST_URL = 'http://test-url';
+
+const TEST_MSG = 'Test msg';
+const TEST_DETAILS = 'Test details';
+
+describe('api.ts', () => {
+  it('Interceptor should throw ApiError if response contain invalid status, even if HTTP status is 200', async () => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    api.defaults.adapter = <AxiosAdapter>((config: AxiosRequestConfig): AxiosPromise<any> => {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      return <AxiosPromise<any>>new Promise((resolve: any) => {
+        const response = {
+          data: {
+            status: -1,
+            message: TEST_MSG,
+            content: TEST_DETAILS
+          },
+          status: 200,
+          statusText: '',
+          headers: [],
+          config
+        };
+        resolve(response);
+      });
+    });
+
+    let response!: ApiError;
+
+    try {
+      await api.get(TEST_URL);
+    } catch (e) {
+      response = e;
+    }
+
+    expect(response).toBeTruthy();
+    expect(response.message).toBe(TEST_MSG);
+    expect(response.details).toBe(TEST_DETAILS);
+  });
+
+  it('ApiError', async () => {
+    expect(ApiError).toBeTruthy();
+  });
+});

+ 41 - 0
desktop/core/src/desktop/js/apps/jobBrowser/commons/api-utils/api.ts

@@ -0,0 +1,41 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 axios, { AxiosInstance, AxiosResponse } from 'axios';
+
+const api: AxiosInstance = axios.create();
+export default api;
+
+api.interceptors.response.use(
+  (response: AxiosResponse): AxiosResponse => {
+    if (response.data.status === -1) {
+      throw new ApiError(response);
+    }
+    return response;
+  },
+  error => Promise.reject(error)
+);
+
+export class ApiError extends Error {
+  details!: string;
+
+  constructor(response: AxiosResponse) {
+    super(response.data.message);
+    this.details = response.data.content;
+  }
+}

+ 44 - 0
desktop/core/src/desktop/js/apps/jobBrowser/commons/api-utils/search.ts

@@ -0,0 +1,44 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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.
+ */
+
+export interface SearchMeta {
+  limit: number;
+  offset: number;
+  size: number;
+  updateTime: number;
+}
+
+export interface SearchFacet {
+  field: string;
+  values: string[];
+}
+
+export interface SearchRequest {
+  endTime: number;
+  limit: number;
+  offset: number;
+  text?: string;
+  facets?: SearchFacet[];
+  sortText: string;
+  startTime: number;
+}
+
+export interface SearchResponse<Q> {
+  meta: SearchMeta;
+  queries: Q[];
+}

+ 36 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/ImpalaQueries.test.ts

@@ -0,0 +1,36 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { SearchResponse } from '../../commons/api-utils/search';
+import { ImpalaQuery } from './index.d';
+
+import * as searchApis from './api/query';
+import { shallowMount } from '@vue/test-utils';
+import ImpalaQueries from './ImpalaQueries.vue';
+
+describe('ImpalaQueries.vue', () => {
+  it('should render', () => {
+    jest.spyOn(searchApis, 'searchQueries').mockImplementation(
+      async (): Promise<SearchResponse<ImpalaQuery>> =>
+        Promise.resolve({
+          meta: { limit: 25, offset: 0, size: 20, updateTime: 1678557946198 },
+          queries: []
+        })
+    );
+    const wrapper = shallowMount(ImpalaQueries);
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 189 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/ImpalaQueries.vue

@@ -0,0 +1,189 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div class="impala-queries">
+    <Spinner v-if="loading" size="xlarge" :center="true" :overlay="true" />
+    <InlineAlert
+      v-if="error"
+      :type="AlertType.Error"
+      :message="error.message"
+      :details="error.details"
+      :show-close="true"
+      @close="showQueries"
+    />
+
+    <query-details
+      v-else-if="selectedQuery"
+      :query="selectedQuery"
+      @reload="selectedQuery && querySelected(selectedQuery)"
+    />
+    <query-details-diff v-else-if="queriesToDiff" :queries="queriesToDiff" />
+    <query-table
+      v-else
+      :queries="queries"
+      :total-queries="(searchMeta && searchMeta.size) || 0"
+      :update-time="(searchMeta && searchMeta.updateTime) || 0"
+      @diff-queries="diffQueries"
+      @query-selected="querySelected"
+      @reload="lastFetchOptions && fetch(lastFetchOptions)"
+      @search="fetch"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  import hueUtils from 'utils/hueUtils';
+  import { SearchFacet, SearchMeta } from '../../commons/api-utils/search';
+  import QueryDetailsDiff from './query-details/QueryDetailsDiff.vue';
+  import QueryDetails from './query-details/QueryDetails.vue';
+  import QueryTable from './query-table/QueryTable.vue';
+  import { ImpalaQuery } from './index';
+  import InlineAlert, { AlertType } from 'components/InlineAlert.vue';
+  import { Page } from 'components/Paginator';
+  import Spinner from 'components/Spinner.vue';
+  import { ApiError } from '../../commons/api-utils/api';
+  import { loadQuery, searchQueries } from './api/query';
+
+  const QUERY_ID_PARAM = 'queryId';
+  const DEFAULT_TIME_WINDOW = 1000 * 60 * 60 * 24 * 7; // 7 days
+
+  export type SortInfo = { column: string; order: 'ASC' | 'DESC' };
+
+  interface FetchOptions {
+    page: Page;
+    text?: string;
+    timeRange?: { from: number; to: number };
+    facets: SearchFacet[];
+    sort: SortInfo;
+  }
+
+  // TODO: Have one single implimentation for Hive & Impala
+  export default defineComponent({
+    name: 'ImpalaQueries',
+    components: {
+      InlineAlert,
+      QueryDetails,
+      QueryDetailsDiff,
+      QueryTable,
+      Spinner
+    },
+    provide() {
+      return {
+        showQueries: () => this.showQueries()
+      };
+    },
+    data() {
+      return {
+        AlertType: AlertType,
+        error: null as ApiError | null,
+        lastFetchOptions: null as FetchOptions | null,
+        loading: false,
+        queries: [] as ImpalaQuery[],
+        queriesToDiff: null as ImpalaQuery[] | null,
+        searchMeta: null as SearchMeta | null,
+        selectedQuery: null as ImpalaQuery | null
+      };
+    },
+    async created(): Promise<void> {
+      const urlParams = new URLSearchParams(window.location.search);
+
+      const queryIdValues = [];
+      let queryIndex = 0;
+      while (urlParams.get(QUERY_ID_PARAM + queryIndex)) {
+        queryIdValues.push(urlParams.get(QUERY_ID_PARAM + queryIndex));
+        queryIndex++;
+      }
+
+      if (queryIdValues.length === 1) {
+        await this.querySelected(<ImpalaQuery>{ queryId: queryIdValues[0] });
+      } else if (queryIdValues.length > 1) {
+        await this.diffQueries(<ImpalaQuery[]>queryIdValues.map(queryId => ({ queryId })));
+      }
+    },
+    methods: {
+      showQueries(): void {
+        this.selectedQuery = null;
+        this.queriesToDiff = null;
+        this.error = null;
+
+        const urlParams = new URLSearchParams(window.location.search);
+        let index = 0;
+        while (urlParams.get(QUERY_ID_PARAM + index)) {
+          hueUtils.removeURLParameter(QUERY_ID_PARAM + index);
+          index++;
+        }
+      },
+
+      async fetch(options: FetchOptions): Promise<void> {
+        // Initial fetch triggered by the paginator
+        const now = Date.now();
+
+        this.lastFetchOptions = options;
+        this.loading = true;
+        try {
+          const searchResponse = await searchQueries<ImpalaQuery>({
+            endTime: (options.timeRange && options.timeRange.to) || now,
+            limit: options.page.limit,
+            offset: options.page.offset,
+            facets: options.facets,
+            text: options.text,
+            sortText: `${options.sort.column}:${options.sort.order}`,
+            startTime: (options.timeRange && options.timeRange.from) || now - DEFAULT_TIME_WINDOW
+          });
+          this.searchMeta = searchResponse.meta;
+          this.queries = searchResponse.queries;
+        } catch (error) {
+          this.error = error as ApiError;
+        }
+        this.loading = false;
+      },
+      async diffQueries(queriesToDiff: ImpalaQuery[]): Promise<void> {
+        queriesToDiff.forEach((query, index) => {
+          hueUtils.changeURLParameter(QUERY_ID_PARAM + index, query.queryId);
+        });
+        this.loading = true;
+        try {
+          const fetchPromises = queriesToDiff.map(query => loadQuery(query.queryId));
+          this.queriesToDiff = await Promise.all(fetchPromises);
+        } catch (error) {
+          this.error = error as ApiError;
+        }
+        this.loading = false;
+      },
+      async querySelected(query: ImpalaQuery): Promise<void> {
+        hueUtils.changeURLParameter(QUERY_ID_PARAM + 0, query.queryId);
+        this.loading = true;
+        try {
+          this.selectedQuery = await loadQuery(query.queryId);
+        } catch (error) {
+          this.error = error as ApiError;
+        }
+        this.loading = false;
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .impala-queries {
+    margin: 11px;
+  }
+</style>

+ 14 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/__snapshots__/ImpalaQueries.test.ts.snap

@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ImpalaQueries.vue should render 1`] = `
+<div
+  class="impala-queries"
+>
+  <!--v-if-->
+  <query-table-stub
+    queries=""
+    totalqueries="0"
+    updatetime="0"
+  />
+</div>
+`;

+ 43 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/api/query.ts

@@ -0,0 +1,43 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 api from '../../../commons/api-utils/api';
+import { AxiosResponse } from 'axios';
+
+import { ImpalaQuery, ImpalaQueryProfile } from '../index.d';
+import { SearchRequest, SearchResponse } from '../../../commons/api-utils/search';
+
+const QUERIES_URL = '/jobbrowser/query-store/api/impala/queries';
+
+export const searchQueries = async <Q>(options: SearchRequest): Promise<SearchResponse<Q>> => {
+  const response = await api.post<SearchRequest, AxiosResponse<SearchResponse<Q>>>(
+    QUERIES_URL,
+    options
+  );
+  return response.data;
+};
+
+type QueryData = { query: ImpalaQuery; profile: ImpalaQueryProfile };
+export const loadQuery = async (queryId: string): Promise<ImpalaQuery> => {
+  const url = `${QUERIES_URL}/${encodeURIComponent(queryId)}`;
+  const response = await api.get<ImpalaQuery, AxiosResponse<QueryData>>(url);
+  return {
+    ...response.data.query,
+    profile: response.data.profile
+  };
+};

+ 84 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/index.d.ts

@@ -0,0 +1,84 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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.
+ */
+
+export enum ImpalaQueryStatus {
+  SUCCESS = 'FINISHED',
+  ERROR = 'ERROR'
+}
+
+export enum ImpalaQueryTypes {
+  QUERY = 'QUERY',
+  DDL = 'DDL'
+}
+
+type Map = { [key: string]: string };
+export interface ImpalaQueryProfile {
+  summaryMap: Map;
+
+  cpuMetrics: Map;
+  hdfsMetrics: Map;
+  insertMetrics: Map;
+  memoryMetrics: Map;
+  threadTimeMetrics: Map;
+}
+
+export interface ImpalaQuery {
+  queryId: string;
+  queryText: string;
+  status: ImpalaQueryStatus;
+  queryType: ImpalaQueryTypes;
+
+  startTime: number;
+  endTime: number;
+  duration: number;
+
+  userName: string;
+  coordinator: string;
+
+  cpuTime: number;
+  rowsProduced: number;
+  peakMemory: number;
+  hdfsBytesRead: number;
+
+  profile: ImpalaQueryProfile;
+}
+
+// TODO: Remove if not used
+interface TableDefinition {
+  rangeData: {
+    title: string;
+  };
+  columnPreferences: { id: string }[];
+}
+
+// TODO: Remove if not used
+interface DataProcessor {
+  facets: {
+    fieldCount?: number;
+  };
+}
+
+export interface CounterGroup {
+  counterGroupName: string;
+  counters: CounterDetails[];
+}
+
+export interface CounterDetails {
+  counterName: string;
+  counterValue: string;
+}

+ 32 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/LabeledInfo.test.ts

@@ -0,0 +1,32 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { mount } from '@vue/test-utils';
+import LabeledInfo from './LabeledInfo.vue';
+
+describe('LabeledInfo.vue', () => {
+  it('should render', () => {
+    const wrapper = mount(LabeledInfo, {
+      propsData: {
+        label: 'TestLable'
+      },
+      slots: {
+        default: '<div>Some item</div>'
+      }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 83 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/LabeledInfo.vue

@@ -0,0 +1,83 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div>
+    <div class="labeled-info-title">{{ I18n(label) }}</div>
+    <div class="labled-info-value">
+      <slot v-if="showSlot" />
+      <span v-else>-</span>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, VNodeNormalizedChildren } from 'vue';
+  import I18n from 'utils/i18n';
+
+  export default defineComponent({
+    props: {
+      label: {
+        type: String,
+        required: true
+      }
+    },
+
+    computed: {
+      showSlot(): boolean {
+        if (this.$slots.default) {
+          for (const node of this.$slots.default()) {
+            if (node && (node.type || this.validateTextChildren(node.children))) {
+              return true;
+            }
+          }
+        }
+        return false;
+      }
+    },
+
+    methods: {
+      I18n,
+
+      validateTextChildren(text: VNodeNormalizedChildren): boolean {
+        if (typeof text === 'string') {
+          text = text.replace(/(\r\n|\n|\r)/gm, '');
+          return !!text.trim();
+        }
+        return false;
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import '../../../../../components/styles/colors.scss';
+
+  .labeled-info-title {
+    text-transform: uppercase;
+    color: $fluid-gray-500;
+    font-weight: normal;
+    font-size: 12px;
+    margin: 0;
+  }
+
+  .labled-info-value {
+    color: $fluid-gray-700;
+    margin-bottom: 5px;
+  }
+</style>

+ 41 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryDetails.test.ts

@@ -0,0 +1,41 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { shallowMount } from '@vue/test-utils';
+import QueryDetails from './QueryDetails.vue';
+
+import queryMockResponse from './__test_data__/impala_query_1.json';
+
+describe('QueryDetails.vue', () => {
+  it('should render', () => {
+    const wrapper = shallowMount(QueryDetails, {
+      propsData: {
+        query: {
+          ...queryMockResponse.query,
+          profile: queryMockResponse.profile
+        }
+      },
+      global: {
+        provide: {
+          showQueries: () => {
+            // Empty
+          }
+        }
+      }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 193 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryDetails.vue

@@ -0,0 +1,193 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div>
+    <div>
+      <div class="buttons-container">
+        <hue-button borderless @click="() => showQueries && showQueries()">
+          <em class="fa fa-chevron-left" />
+          {{ I18n('Queries') }}
+        </hue-button>
+        <div class="buttons-right">
+          <hue-button @click="$emit('reload')">
+            <em class="fa fa-refresh" />
+            {{ I18n('Refresh') }}
+          </hue-button>
+        </div>
+      </div>
+
+      <QueryInfoTop :query="query" />
+
+      <tabs>
+        <tab :title="I18n('Query Info')">
+          <QueryInfo :query="query" />
+        </tab>
+        <tab :title="I18n('Plan')">
+          <pre class="hue-box">
+            {{ query.profile.summaryMap['Plan'] }}
+          </pre>
+        </tab>
+        <tab :title="I18n('Exec Summary')">
+          <pre class="hue-box">
+            {{ query.profile.summaryMap['ExecSummary'] }}
+          </pre>
+        </tab>
+        <tab :title="I18n('Metrics')">
+          <CountersTable
+            :counters="[
+              {
+                title: `${I18n('Metrics')} : ${query.queryId}`,
+                counters: getCounters(query.profile),
+                cssClass: ''
+              }
+            ]"
+          />
+        </tab>
+      </tabs>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType, inject } from 'vue';
+
+  import HueButton from '../../../../../components/HueButton.vue';
+  import Tab from '../../../../../components/Tab.vue';
+  import Tabs from '../../../../../components/Tabs.vue';
+  import QueryInfoTop from './QueryInfoTop.vue';
+  import QueryInfo from './QueryInfo.vue';
+
+  import CountersTable from './counters-table/CountersTable.vue';
+
+  import { CounterDetails, CounterGroup, ImpalaQuery, ImpalaQueryProfile } from '../index';
+  import { hueWindow } from 'types/types';
+  import huePubSub from 'utils/huePubSub';
+
+  import I18n from 'utils/i18n';
+
+  declare const ENABLE_NOTEBOOK_2: boolean;
+
+  export default defineComponent({
+    components: {
+      Tab,
+      Tabs,
+      HueButton,
+      QueryInfo,
+      QueryInfoTop,
+      CountersTable
+    },
+
+    props: {
+      query: {
+        type: Object as PropType<ImpalaQuery>,
+        required: true
+      }
+    },
+
+    emits: ['reload'],
+
+    setup(): {
+      showQueries?: () => void;
+    } {
+      return {
+        showQueries: inject('showQueries')
+      };
+    },
+
+    methods: {
+      I18n,
+      async reExecute() {
+        huePubSub.publish('open.editor.new.query', {
+          sourceType: 'impala'
+        });
+
+        huePubSub.subscribeOnce(
+          ENABLE_NOTEBOOK_2 ? 'ace.editor.focused' : 'set.current.app.view.model',
+          () =>
+            setTimeout(() => {
+              huePubSub.publish('editor.insert.at.cursor', {
+                text: this.query.queryText,
+                cursorEndAdjust: 0
+              });
+            }, 100),
+          ''
+        );
+      },
+      getCounters: function (profile: ImpalaQueryProfile): CounterGroup[] {
+        const kvToCounters = (kv: ImpalaQueryProfile['cpuMetrics']): CounterDetails[] =>
+          Object.keys(kv).map(key => ({
+            counterName: key,
+            counterValue: kv[key]
+          }));
+        return [
+          {
+            counterGroupName: I18n('CPU Metrics'),
+            counters: kvToCounters(profile.cpuMetrics)
+          },
+          {
+            counterGroupName: I18n('HDFS Metrics'),
+            counters: kvToCounters(profile.hdfsMetrics)
+          },
+          {
+            counterGroupName: I18n('Memory Metrics'),
+            counters: kvToCounters(profile.memoryMetrics)
+          },
+          {
+            counterGroupName: I18n('Thread Time Metrics'),
+            counters: kvToCounters(profile.threadTimeMetrics)
+          }
+        ];
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import '../../../../../components/styles/colors.scss';
+  @import '../../../../../components/styles/mixins';
+
+  .buttons-container {
+    margin-bottom: 20px;
+    position: relative;
+
+    .buttons-right {
+      position: absolute;
+      right: 0px;
+      top: 0px;
+
+      & > * {
+        margin-left: 5px;
+      }
+    }
+  }
+
+  .download-link {
+    color: $hue-action-primary;
+    border-color: $hue-border-color;
+    border-radius: $hue-panel-border-radius;
+
+    &:hover {
+      border-color: $fluid-blue-700;
+    }
+  }
+
+  .hue-layout-column {
+    @include hue-flex-layout(column);
+  }
+</style>

+ 48 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryDetailsDiff.test.ts

@@ -0,0 +1,48 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { shallowMount } from '@vue/test-utils';
+import QueryDetailsDiff from './QueryDetailsDiff.vue';
+
+import queryMockResponseA from './__test_data__/impala_query_1.json';
+import queryMockResponseB from './__test_data__/impala_query_2.json';
+
+describe('QueryDetailsDiff.vue', () => {
+  it('should render', () => {
+    const wrapper = shallowMount(QueryDetailsDiff, {
+      props: {
+        queries: [
+          {
+            ...queryMockResponseA.query,
+            profile: queryMockResponseA.profile
+          },
+          {
+            ...queryMockResponseB.query,
+            profile: queryMockResponseB.profile
+          }
+        ]
+      },
+      global: {
+        provide: {
+          showQueries: () => {
+            // Empty
+          }
+        }
+      }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 206 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryDetailsDiff.vue

@@ -0,0 +1,206 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div>
+    <div class="buttons-container">
+      <hue-button borderless @click="() => showQueries && showQueries()">
+        <em class="fa fa-chevron-left" />
+        {{ I18n('Queries') }}
+      </hue-button>
+    </div>
+
+    <div class="hue-layout-row">
+      <QueryInfoTop :query="queries[0]" />
+      <QueryInfoTop :query="queries[1]" class="query-b" />
+    </div>
+
+    <tabs>
+      <!-- Query Tabs -->
+      <tab :title="I18n('Query Info')" class="query-info">
+        <QueryInfo :query="queries[0]" layout="column" />
+        <QueryInfo :query="queries[1]" layout="column" class="query-b" />
+      </tab>
+      <tab :title="I18n('Plan')">
+        <pre class="hue-box">
+          {{ queries[0].profile.summaryMap['Plan'] }}
+        </pre>
+        <pre class="hue-box query-b">
+          {{ queries[1].profile.summaryMap['Plan'] }}
+        </pre>
+      </tab>
+      <tab :title="I18n('Exec Summary')">
+        <pre class="hue-box">
+          {{ queries[0].profile.summaryMap['ExecSummary'] }}
+        </pre>
+        <pre class="hue-box query-b">
+          {{ queries[1].profile.summaryMap['ExecSummary'] }}
+        </pre>
+      </tab>
+      <tab :title="I18n('Metrics')">
+        <CountersTable
+          :counters="[
+            {
+              title: `${I18n('Metrics')} : ${queries[0].queryId}`,
+              counters: getCounters(queries[0].profile),
+              cssClass: ''
+            },
+            {
+              title: `${I18n('Metrics')} : ${queries[1].queryId}`,
+              counters: getCounters(queries[1].profile),
+              cssClass: ''
+            }
+          ]"
+        />
+      </tab>
+    </tabs>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType, inject } from 'vue';
+
+  import HueButton from '../../../../../components/HueButton.vue';
+  import Tab from '../../../../../components/Tab.vue';
+  import Tabs from '../../../../../components/Tabs.vue';
+  import QueryInfoTop from './QueryInfoTop.vue';
+  import QueryInfo from './QueryInfo.vue';
+  import CountersTable from './counters-table/CountersTable.vue';
+
+  import I18n from 'utils/i18n';
+
+  import { CounterDetails, CounterGroup, ImpalaQuery } from '../index';
+
+  import { get } from 'lodash';
+
+  export default defineComponent({
+    components: {
+      HueButton,
+      Tab,
+      Tabs,
+      QueryInfoTop,
+      QueryInfo,
+      CountersTable
+    },
+
+    props: {
+      queries: {
+        type: Array as PropType<ImpalaQuery[]>,
+        required: true
+      }
+    },
+
+    setup() {
+      const showQueries: (() => void | undefined) | undefined = inject('showQueries');
+      return {
+        showQueries
+      };
+    },
+
+    data() {
+      return {
+        hideSimilarValues: false,
+        useSameTimeScale: true
+      };
+    },
+
+    methods: {
+      get,
+      I18n,
+      getCounters: function (profile: any): CounterGroup[] {
+        const kvToCounters = (kv: any): CounterDetails[] => {
+          return Object.keys(kv).map(key => ({
+            counterName: key,
+            counterValue: kv[key]
+          }));
+        };
+        return [
+          {
+            counterGroupName: I18n('CPU Metrics'),
+            counters: kvToCounters(profile.cpuMetrics)
+          },
+          {
+            counterGroupName: I18n('HDFS Metrics'),
+            counters: kvToCounters(profile.hdfsMetrics)
+          },
+          {
+            counterGroupName: I18n('Memory Metrics'),
+            counters: kvToCounters(profile.memoryMetrics)
+          },
+          {
+            counterGroupName: I18n('Thread Time Metrics'),
+            counters: kvToCounters(profile.threadTimeMetrics)
+          }
+        ];
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import '../../../../../components/styles/colors.scss';
+  @import '../../../../../components/styles/mixins';
+
+  .hue-info-box {
+    padding: 10px;
+
+    border: 1px solid $fluid-gray-300;
+    border-radius: $hue-panel-border-radius;
+  }
+
+  .buttons-container {
+    margin-bottom: 20px;
+  }
+
+  .hue-layout-row {
+    @include hue-flex-layout(row);
+  }
+
+  .hue-layout-column {
+    vertical-align: top;
+    @include hue-flex-layout(column);
+  }
+
+  .query-info {
+    white-space: nowrap;
+  }
+
+  .query-b,
+  ::v-deep(.query-b-deep .query-b),
+  ::v-deep(.query-b-deep .hue-info-box),
+  ::v-deep(.query-b-deep .dag-view-container) {
+    background-color: $fluid-blue-050;
+    border-radius: $hue-panel-border-radius;
+  }
+
+  .hide-similar {
+    text-align: right;
+    input {
+      vertical-align: top;
+    }
+  }
+
+  .scale-controls {
+    label {
+      display: inline-block;
+      input {
+        vertical-align: top;
+      }
+    }
+  }
+</style>

+ 34 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryInfo.test.ts

@@ -0,0 +1,34 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { shallowMount } from '@vue/test-utils';
+import QueryInfo from './QueryInfo.vue';
+
+import queryMockResponse from './__test_data__/impala_query_1.json';
+
+describe('QueryInfo.vue', () => {
+  it('should render', () => {
+    const wrapper = shallowMount(QueryInfo, {
+      propsData: {
+        query: {
+          ...queryMockResponse.query,
+          profile: queryMockResponse.profile
+        }
+      }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 154 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryInfo.vue

@@ -0,0 +1,154 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div :class="`query-info layout-${layout}`">
+    <div>
+      <div class="hue-info-box">
+        <LabeledInfo label="Query">
+          <sql-text :enable-overflow="true" :format="true" :value="query.queryText" />
+        </LabeledInfo>
+      </div>
+    </div>
+    <div>
+      <div class="hue-info-box">
+        <LabeledInfo label="Start Time">
+          <time-ago v-if="query.startTime" :value="query.startTime" />
+        </LabeledInfo>
+
+        <LabeledInfo label="End Time">
+          <time-ago v-if="query.endTime" :value="query.endTime" />
+        </LabeledInfo>
+
+        <LabeledInfo label="Duration">
+          <duration v-if="query.duration" :value="query.duration" :short="true" />
+        </LabeledInfo>
+
+        <LabeledInfo label="Query Type">
+          {{ query.queryType }}
+        </LabeledInfo>
+
+        <LabeledInfo label="User Name">
+          {{ query.userName }}
+        </LabeledInfo>
+
+        <LabeledInfo label="Coordinator">
+          {{ query.coordinator }}
+        </LabeledInfo>
+
+        <LabeledInfo label="CPU Time">
+          <duration v-if="query.cpuTime" :value="query.cpuTime" :short="true" />
+        </LabeledInfo>
+
+        <LabeledInfo label="Rows Produced">
+          <span v-if="query.rowsProduced">{{ query.rowsProduced }}</span>
+          <span v-else>None</span>
+        </LabeledInfo>
+
+        <LabeledInfo label="Peak Memory">
+          <HumanByteSize v-if="query.peakMemory" :value="query.peakMemory" />
+        </LabeledInfo>
+
+        <LabeledInfo label="HDFS Bytes Read">
+          <HumanByteSize v-if="query.hdfsBytesRead" :value="query.hdfsBytesRead" />
+        </LabeledInfo>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
+  import Duration from '../../../../../components/Duration.vue';
+  import SqlText from '../../../../../components/SqlText.vue';
+  import TimeAgo from '../../../../../components/TimeAgo.vue';
+  import HumanByteSize from '../../../../../components/HumanByteSize.vue';
+  import LabeledInfo from './LabeledInfo.vue';
+
+  import { ImpalaQuery } from '../index';
+
+  export default defineComponent({
+    components: {
+      Duration,
+      TimeAgo,
+      SqlText,
+      HumanByteSize,
+      LabeledInfo
+    },
+
+    props: {
+      query: {
+        type: Object as PropType<ImpalaQuery>,
+        required: true
+      },
+      layout: {
+        type: String as PropType<'row' | 'column'>,
+        default: 'row'
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import '../../../../../components/styles/colors.scss';
+  @import '../../../../../components/styles/mixins';
+
+  .query-info {
+    &.layout-row {
+      white-space: nowrap;
+
+      > div {
+        width: 50%;
+        display: inline-block;
+        vertical-align: top;
+
+        &:first-child {
+          width: calc(50% - 14px);
+          margin-right: 10px;
+        }
+      }
+    }
+    &.layout-column {
+      display: inline-block;
+      width: calc(50% - 7px);
+      vertical-align: top;
+      margin-right: 10px;
+
+      > div {
+        width: 100%;
+
+        &:first-child {
+          margin-bottom: 10px;
+        }
+      }
+    }
+
+    .hue-info-box {
+      @include hue-flex-layout(column);
+
+      padding: 10px;
+
+      overflow: hidden;
+      white-space: normal;
+
+      border: 1px solid $fluid-gray-300;
+      border-radius: $hue-panel-border-radius;
+    }
+  }
+</style>

+ 36 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryInfoTop.test.ts

@@ -0,0 +1,36 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 { shallowMount } from '@vue/test-utils';
+import QueryInfoTop from './QueryInfoTop.vue';
+
+import queryMockResponse from './__test_data__/impala_query_1.json';
+
+describe('QueryInfoTop.vue', () => {
+  it('should render', () => {
+    const wrapper = shallowMount(QueryInfoTop, {
+      props: {
+        query: {
+          ...queryMockResponse.query,
+          profile: queryMockResponse.profile
+        }
+      }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 87 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/QueryInfoTop.vue

@@ -0,0 +1,87 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div class="hue-info-box">
+    <LabeledInfo label="Query ID" class="inline-info">
+      {{ query.queryId }}
+      <a href="javascript:void(0);" class="fa fa-copy" @click="copyQueryId(query.queryId)" />
+    </LabeledInfo>
+
+    <LabeledInfo label="User" class="inline-info">
+      {{ query.userName }}
+    </LabeledInfo>
+
+    <LabeledInfo label="Status" class="inline-info">
+      <StatusIndicator :value="query.status" />
+      {{ query.status }}
+    </LabeledInfo>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
+  import StatusIndicator from '../../../../../components/StatusIndicator.vue';
+  import LabeledInfo from './LabeledInfo.vue';
+
+  import { ImpalaQuery } from '..';
+
+  export default defineComponent({
+    components: {
+      StatusIndicator,
+      LabeledInfo
+    },
+
+    props: {
+      query: {
+        type: Object as PropType<ImpalaQuery>,
+        required: true
+      }
+    },
+
+    methods: {
+      copyQueryId(queryId: string): void {
+        navigator.clipboard.writeText(queryId);
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import '../../../../../components/styles/colors.scss';
+  @import '../../../../../components/styles/mixins';
+
+  .hue-info-box {
+    padding: 10px;
+
+    border: 1px solid $fluid-gray-300;
+    border-radius: $hue-panel-border-radius;
+
+    @include nowrap-ellipsis;
+
+    margin-bottom: 10px;
+
+    .inline-info {
+      display: inline-block;
+      margin-right: 20px;
+
+      vertical-align: baseline;
+    }
+  }
+</style>

+ 22 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/LabeledInfo.test.ts.snap

@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LabeledInfo.vue should render 1`] = `
+<div>
+  <div
+    class="labeled-info-title"
+  >
+    TestLable
+  </div>
+  <div
+    class="labled-info-value"
+  >
+    
+    
+    <div>
+      Some item
+    </div>
+    
+    
+  </div>
+</div>
+`;

+ 36 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/QueryDetails.test.ts.snap

@@ -0,0 +1,36 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QueryDetails.vue should render 1`] = `
+<div>
+  <div>
+    <div
+      class="buttons-container"
+    >
+      <hue-button-stub
+        alert="false"
+        borderless="true"
+        disabled="false"
+        large="false"
+        primary="false"
+        small="false"
+      />
+      <div
+        class="buttons-right"
+      >
+        <hue-button-stub
+          alert="false"
+          borderless="false"
+          disabled="false"
+          large="false"
+          primary="false"
+          small="false"
+        />
+      </div>
+    </div>
+    <query-info-top-stub
+      query="[object Object]"
+    />
+    <tabs-stub />
+  </div>
+</div>
+`;

+ 30 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/QueryDetailsDiff.test.ts.snap

@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QueryDetailsDiff.vue should render 1`] = `
+<div>
+  <div
+    class="buttons-container"
+  >
+    <hue-button-stub
+      alert="false"
+      borderless="true"
+      disabled="false"
+      large="false"
+      primary="false"
+      small="false"
+    />
+  </div>
+  <div
+    class="hue-layout-row"
+  >
+    <query-info-top-stub
+      query="[object Object]"
+    />
+    <query-info-top-stub
+      class="query-b"
+      query="[object Object]"
+    />
+  </div>
+  <tabs-stub />
+</div>
+`;

+ 53 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/QueryInfo.test.ts.snap

@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QueryInfo.vue should render 1`] = `
+<div
+  class="query-info layout-row"
+>
+  <div>
+    <div
+      class="hue-info-box"
+    >
+      <labeled-info-stub
+        label="Query"
+      />
+    </div>
+  </div>
+  <div>
+    <div
+      class="hue-info-box"
+    >
+      <labeled-info-stub
+        label="Start Time"
+      />
+      <labeled-info-stub
+        label="End Time"
+      />
+      <labeled-info-stub
+        label="Duration"
+      />
+      <labeled-info-stub
+        label="Query Type"
+      />
+      <labeled-info-stub
+        label="User Name"
+      />
+      <labeled-info-stub
+        label="Coordinator"
+      />
+      <labeled-info-stub
+        label="CPU Time"
+      />
+      <labeled-info-stub
+        label="Rows Produced"
+      />
+      <labeled-info-stub
+        label="Peak Memory"
+      />
+      <labeled-info-stub
+        label="HDFS Bytes Read"
+      />
+    </div>
+  </div>
+</div>
+`;

+ 20 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__snapshots__/QueryInfoTop.test.ts.snap

@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QueryInfoTop.vue should render 1`] = `
+<div
+  class="hue-info-box"
+>
+  <labeled-info-stub
+    class="inline-info"
+    label="Query ID"
+  />
+  <labeled-info-stub
+    class="inline-info"
+    label="User"
+  />
+  <labeled-info-stub
+    class="inline-info"
+    label="Status"
+  />
+</div>
+`;

+ 85 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__test_data__/impala_query_1.json

@@ -0,0 +1,85 @@
+{
+  "query": {
+      "queryId": "e140b4f4dff9c2d5:cf78c8f400000000",
+      "queryText": "SELECT *\nFROM `default`.sample_08\nLIMIT 105",
+      "status": "FINISHED",
+      "queryType": "QUERY",
+      "startTime": 1678521664204,
+      "endTime": 1678521667266,
+      "duration": 3062,
+      "userName": "admin",
+      "coordinator": "sree-test2-1.sree-test2.root.hwx.site:27000",
+      "cpuTime": 1,
+      "rowsProduced": 105,
+      "peakMemory": 4225761,
+      "hdfsBytesRead": 96126
+  },
+  "profile": {
+      "summaryMap": {
+          "Estimated Per-Host Mem": "71319552",
+          "Per Host Number of Fragment Instances": "sree-test2-2.sree-test2.root.hwx.site:27000(1) sree-test2-1.sree-test2.root.hwx.site:27000(1)",
+          "User": "admin",
+          "Start Time": "2023-03-11 08:01:04.204660000",
+          "Connected User": "hue",
+          "ExecSummary": "\nOperator              #Hosts  #Inst   Avg Time   Max Time  #Rows  Est. #Rows   Peak Mem  Est. Peak Mem  Detail            \n--------------------------------------------------------------------------------------------------------------------------\nF01:ROOT                   1      1  105.431us  105.431us                       4.01 MB        4.00 MB                    \n01:EXCHANGE                1      1   32.077us   32.077us    105         105   16.00 KB       16.00 KB  UNPARTITIONED     \nF00:EXCHANGE SENDER        1      1  165.330us  165.330us                       3.88 KB              0                    \n00:SCAN HDFS               1      1    3.883ms    3.883ms    105         105  172.96 KB       64.00 MB  default.sample_08",
+          "Query Options (set by configuration and planner)": "QUERY_TIMEOUT_S=300,MT_DOP=0,IDLE_SESSION_TIMEOUT=900,TIMEZONE=UTC,DEFAULT_FILE_FORMAT=PARQUET,DEFAULT_TRANSACTIONAL_TYPE=INSERT_ONLY,MINMAX_FILTER_THRESHOLD=0.5,MINMAX_FILTERING_LEVEL=PAGE",
+          "Tables Missing Stats": "default.sample_08",
+          "Query Type": "QUERY",
+          "Session Type": "HIVESERVER2",
+          "Per Host Min Memory Reservation": "sree-test2-2.sree-test2.root.hwx.site:27000(64.00 KB) sree-test2-1.sree-test2.root.hwx.site:27000(4.00 MB)",
+          "Session ID": "434af3e40c14f427:a836c6f0ced92583",
+          "Executor Group": "default",
+          "Query Status": "OK",
+          "Query State": "FINISHED",
+          "End Time": "2023-03-11 08:01:07.266477000",
+          "HiveServer2 Protocol Version": "V6",
+          "Admission result": "Admitted immediately",
+          "Delegated User": "admin",
+          "Impala Version": "impalad version 4.0.0.7.2.17.0-140 RELEASE (build d0609b8f4cca258c229637691977fc48b3e5e028)",
+          "Sql Statement": "SELECT *\nFROM `default`.sample_08\nLIMIT 105",
+          "Coordinator": "sree-test2-1.sree-test2.root.hwx.site:27000",
+          "Plan": "\n----------------\nMax Per-Host Resource Reservation: Memory=4.06MB Threads=3\nPer-Host Resource Estimates: Memory=68MB\nCodegen disabled by planner\nWARNING: The following tables are missing relevant table and/or column statistics.\ndefault.sample_08\nAnalyzed query: SELECT * FROM `default`.sample_08 LIMIT CAST(105 AS TINYINT)\n\nF01:PLAN FRAGMENT [UNPARTITIONED] hosts=1 instances=1\n|  Per-Host Resources: mem-estimate=4.02MB mem-reservation=4.00MB thread-reservation=1\nPLAN-ROOT SINK\n|  output exprs: default.sample_08.code, default.sample_08.description, default.sample_08.total_emp, default.sample_08.salary\n|  mem-estimate=4.00MB mem-reservation=4.00MB spill-buffer=2.00MB thread-reservation=0\n|\n01:EXCHANGE [UNPARTITIONED]\n|  limit: 105\n|  mem-estimate=16.00KB mem-reservation=0B thread-reservation=0\n|  tuple-ids=0 row-size=32B cardinality=105\n|  in pipelines: 00(GETNEXT)\n|\nF00:PLAN FRAGMENT [RANDOM] hosts=1 instances=1\nPer-Host Resources: mem-estimate=64.00MB mem-reservation=64.00KB thread-reservation=2\n00:SCAN HDFS [default.sample_08, RANDOM]\n   HDFS partitions=1/1 files=1 size=47.40KB\n   stored statistics:\n     table: rows=823 size=47.40KB\n     columns: unavailable\n   extrapolated-rows=disabled max-scan-range-rows=823\n   limit: 105\n   mem-estimate=64.00MB mem-reservation=64.00KB thread-reservation=1\n   tuple-ids=0 row-size=32B cardinality=105\n   in pipelines: 00(GETNEXT)\n----------------",
+          "Errors": "",
+          "Impala Query State": "FINISHED",
+          "Default Db": "default",
+          "Network Address": "172.27.38.15:41232",
+          "Request Pool": "root.admin",
+          "Cluster Memory Admitted": "136.03 MB",
+          "Query Options (set by configuration)": "QUERY_TIMEOUT_S=300,IDLE_SESSION_TIMEOUT=900,TIMEZONE=UTC,DEFAULT_FILE_FORMAT=PARQUET,DEFAULT_TRANSACTIONAL_TYPE=INSERT_ONLY"
+      },
+      "hdfsMetrics": {
+          "hdfs_bytes_read": "96126",
+          "hdfs_bytes_read_local": "96126",
+          "hdfs_bytes_read_local_percentage": "100",
+          "hdfs_bytes_read_remote": "0",
+          "hdfs_bytes_read_remote_percentage": "0",
+          "hdfs_bytes_read_short_circuit": "96126",
+          "hdfs_bytes_read_short_circuit_percentage": "100",
+          "hdfs_bytes_read_from_cache": "0",
+          "hdfs_bytes_read_from_cache_percentage": "0",
+          "hdfs_average_scan_range": "96126.0"
+      },
+      "memoryMetrics": {
+          "memory_per_node_peak": "4225761.28",
+          "memory_per_node_peak_node": "sree-test2-1.sree-test2.root.hwx.site:27000"
+      },
+      "cpuMetrics": {},
+      "threadTimeMetrics": {
+          "thread_total_time": "9",
+          "thread_cpu_time": "1",
+          "thread_cpu_time_percentage": "19",
+          "thread_storage_wait_time": "2",
+          "thread_storage_wait_time_percentage": "30",
+          "thread_network_send_wait_time": "0",
+          "thread_network_send_wait_time_percentage": "6",
+          "thread_network_receive_wait_time": "4",
+          "thread_network_receive_wait_time_percentage": "45",
+          "planning_wait_time": "11",
+          "planning_wait_time_percentage": "0",
+          "client_fetch_wait_time": "6269513",
+          "client_fetch_wait_time_percentage": "204752"
+      },
+      "insertMetrics": {},
+      "queryId": "e140b4f4dff9c2d5:cf78c8f400000000"
+  }
+}

+ 85 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/__test_data__/impala_query_2.json

@@ -0,0 +1,85 @@
+{
+  "query": {
+      "queryId": "cb4137379d658f7a:b9c3488400000000",
+      "queryText": "SELECT *\nFROM `default`.sample_08\nLIMIT 104",
+      "status": "FINISHED",
+      "queryType": "QUERY",
+      "startTime": 1678521646997,
+      "endTime": 1678521664117,
+      "duration": 17120,
+      "userName": "admin",
+      "coordinator": "sree-test2-1.sree-test2.root.hwx.site:27000",
+      "cpuTime": 1,
+      "rowsProduced": 104,
+      "peakMemory": 4225761,
+      "hdfsBytesRead": 96126
+  },
+  "profile": {
+      "summaryMap": {
+          "Estimated Per-Host Mem": "71319552",
+          "Per Host Number of Fragment Instances": "sree-test2-2.sree-test2.root.hwx.site:27000(1) sree-test2-1.sree-test2.root.hwx.site:27000(1)",
+          "User": "admin",
+          "Start Time": "2023-03-11 08:00:46.997228000",
+          "Connected User": "hue",
+          "ExecSummary": "\nOperator              #Hosts  #Inst   Avg Time   Max Time  #Rows  Est. #Rows   Peak Mem  Est. Peak Mem  Detail            \n--------------------------------------------------------------------------------------------------------------------------\nF01:ROOT                   1      1   34.699us   34.699us                       4.01 MB        4.00 MB                    \n01:EXCHANGE                1      1   16.200us   16.200us    104         104   16.00 KB       16.00 KB  UNPARTITIONED     \nF00:EXCHANGE SENDER        1      1  156.173us  156.173us                       3.88 KB              0                    \n00:SCAN HDFS               1      1   15.739ms   15.739ms    104         104  172.96 KB       64.00 MB  default.sample_08",
+          "Query Options (set by configuration and planner)": "QUERY_TIMEOUT_S=300,MT_DOP=0,IDLE_SESSION_TIMEOUT=900,TIMEZONE=UTC,DEFAULT_FILE_FORMAT=PARQUET,DEFAULT_TRANSACTIONAL_TYPE=INSERT_ONLY,MINMAX_FILTER_THRESHOLD=0.5,MINMAX_FILTERING_LEVEL=PAGE",
+          "Tables Missing Stats": "default.sample_08",
+          "Query Type": "QUERY",
+          "Session Type": "HIVESERVER2",
+          "Per Host Min Memory Reservation": "sree-test2-2.sree-test2.root.hwx.site:27000(64.00 KB) sree-test2-1.sree-test2.root.hwx.site:27000(4.00 MB)",
+          "Session ID": "434af3e40c14f427:a836c6f0ced92583",
+          "Executor Group": "default",
+          "Query Status": "OK",
+          "Query State": "FINISHED",
+          "End Time": "2023-03-11 08:01:04.117774000",
+          "HiveServer2 Protocol Version": "V6",
+          "Admission result": "Admitted immediately",
+          "Delegated User": "admin",
+          "Impala Version": "impalad version 4.0.0.7.2.17.0-140 RELEASE (build d0609b8f4cca258c229637691977fc48b3e5e028)",
+          "Sql Statement": "SELECT *\nFROM `default`.sample_08\nLIMIT 104",
+          "Coordinator": "sree-test2-1.sree-test2.root.hwx.site:27000",
+          "Plan": "\n----------------\nMax Per-Host Resource Reservation: Memory=4.06MB Threads=3\nPer-Host Resource Estimates: Memory=68MB\nCodegen disabled by planner\nWARNING: The following tables are missing relevant table and/or column statistics.\ndefault.sample_08\nAnalyzed query: SELECT * FROM `default`.sample_08 LIMIT CAST(104 AS TINYINT)\n\nF01:PLAN FRAGMENT [UNPARTITIONED] hosts=1 instances=1\n|  Per-Host Resources: mem-estimate=4.02MB mem-reservation=4.00MB thread-reservation=1\nPLAN-ROOT SINK\n|  output exprs: default.sample_08.code, default.sample_08.description, default.sample_08.total_emp, default.sample_08.salary\n|  mem-estimate=4.00MB mem-reservation=4.00MB spill-buffer=2.00MB thread-reservation=0\n|\n01:EXCHANGE [UNPARTITIONED]\n|  limit: 104\n|  mem-estimate=16.00KB mem-reservation=0B thread-reservation=0\n|  tuple-ids=0 row-size=32B cardinality=104\n|  in pipelines: 00(GETNEXT)\n|\nF00:PLAN FRAGMENT [RANDOM] hosts=1 instances=1\nPer-Host Resources: mem-estimate=64.00MB mem-reservation=64.00KB thread-reservation=2\n00:SCAN HDFS [default.sample_08, RANDOM]\n   HDFS partitions=1/1 files=1 size=47.40KB\n   stored statistics:\n     table: rows=823 size=47.40KB\n     columns: unavailable\n   extrapolated-rows=disabled max-scan-range-rows=823\n   limit: 104\n   mem-estimate=64.00MB mem-reservation=64.00KB thread-reservation=1\n   tuple-ids=0 row-size=32B cardinality=104\n   in pipelines: 00(GETNEXT)\n----------------",
+          "Errors": "",
+          "Impala Query State": "FINISHED",
+          "Default Db": "default",
+          "Network Address": "172.27.38.15:41232",
+          "Request Pool": "root.admin",
+          "Cluster Memory Admitted": "136.03 MB",
+          "Query Options (set by configuration)": "QUERY_TIMEOUT_S=300,IDLE_SESSION_TIMEOUT=900,TIMEZONE=UTC,DEFAULT_FILE_FORMAT=PARQUET,DEFAULT_TRANSACTIONAL_TYPE=INSERT_ONLY"
+      },
+      "hdfsMetrics": {
+          "hdfs_bytes_read": "96126",
+          "hdfs_bytes_read_local": "96126",
+          "hdfs_bytes_read_local_percentage": "100",
+          "hdfs_bytes_read_remote": "0",
+          "hdfs_bytes_read_remote_percentage": "0",
+          "hdfs_bytes_read_short_circuit": "96126",
+          "hdfs_bytes_read_short_circuit_percentage": "100",
+          "hdfs_bytes_read_from_cache": "0",
+          "hdfs_bytes_read_from_cache_percentage": "0",
+          "hdfs_average_scan_range": "96126.0"
+      },
+      "memoryMetrics": {
+          "memory_per_node_peak": "4225761.28",
+          "memory_per_node_peak_node": "sree-test2-1.sree-test2.root.hwx.site:27000"
+      },
+      "cpuMetrics": {},
+      "threadTimeMetrics": {
+          "thread_total_time": "33",
+          "thread_cpu_time": "1",
+          "thread_cpu_time_percentage": "5",
+          "thread_storage_wait_time": "14",
+          "thread_storage_wait_time_percentage": "44",
+          "thread_network_send_wait_time": "0",
+          "thread_network_send_wait_time_percentage": "2",
+          "thread_network_receive_wait_time": "16",
+          "thread_network_receive_wait_time_percentage": "50",
+          "planning_wait_time": "34",
+          "planning_wait_time_percentage": "0",
+          "client_fetch_wait_time": "17053",
+          "client_fetch_wait_time_percentage": "100"
+      },
+      "insertMetrics": {},
+      "queryId": "cb4137379d658f7a:b9c3488400000000"
+  }
+}

+ 29 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/CounterSet.ts

@@ -0,0 +1,29 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 { CounterGroup } from '../../index';
+
+export default interface CounterSet {
+  title: string;
+  counters: CounterGroup[];
+  cssClass: string;
+}
+
+export type Row = { [key: string]: unknown };
+
+export const generateValueColumnKey = (index: number): string => `counterSet_${index}`;

+ 33 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/CountersTable.test.ts

@@ -0,0 +1,33 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 { mount } from '@vue/test-utils';
+import CountersTable from './CountersTable.vue';
+
+import counters from './__test_data__/counters.json';
+
+describe('CountersTable.vue', () => {
+  it('should render', () => {
+    const wrapper = mount(CountersTable, {
+      propsData: {
+        counters
+      }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 143 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/CountersTable.vue

@@ -0,0 +1,143 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div>
+    <HueTable v-if="rows.length" :columns="columns" :rows="rows">
+      <template #cell-variance="row">
+        <VarianceCell :data="row" />
+      </template>
+    </HueTable>
+    <h2 v-else>{{ I18n('No counters available!') }}</h2>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
+  import I18n from 'utils/i18n';
+
+  import { Column } from '../../../../../../components/HueTable';
+  import HueTable from '../../../../../../components/HueTable.vue';
+
+  import { CounterGroup, CounterDetails } from '../../index';
+
+  import CounterSet, { Row, generateValueColumnKey } from './CounterSet';
+  import VarianceCell from './VarianceCell.vue';
+
+  const DEFAULT_VALUE_COLUMN_TITLE = 'Counter Value';
+  const NAME_COLUMNS: Column<Row>[] = [
+    {
+      label: I18n('Group Name'),
+      key: 'groupName'
+    },
+    {
+      label: I18n('Counter Name'),
+      key: 'counterName'
+    }
+  ];
+
+  export default defineComponent({
+    components: {
+      HueTable,
+      VarianceCell
+    },
+
+    props: {
+      counters: {
+        type: Object as PropType<CounterSet[]>,
+        required: true
+      },
+      hideSimilarValues: {
+        type: Boolean,
+        default: false
+      }
+    },
+
+    computed: {
+      columns(): Column<Row>[] {
+        const valueColumns: Column<Row>[] = [];
+
+        this.counters.forEach((counterSet: CounterSet, index: number) => {
+          valueColumns.push({
+            label: counterSet.title || DEFAULT_VALUE_COLUMN_TITLE,
+            key: generateValueColumnKey(index),
+            headerCssClass: counterSet.cssClass,
+            cssClass: counterSet.cssClass
+          });
+        });
+
+        if (this.counters.length > 1) {
+          valueColumns.push({
+            label: I18n('Variance'),
+            key: 'variance'
+          });
+        }
+
+        return NAME_COLUMNS.concat(valueColumns);
+      },
+      rows(): Row[] {
+        const rowHash: Map<string, Row> = new Map();
+        let rows: Row[];
+
+        const counters = this.counters.filter((counterSet: CounterSet) => counterSet.counters);
+        counters.forEach((counterSet: CounterSet, index: number) => {
+          const counterKey: string = generateValueColumnKey(index);
+          counterSet.counters.forEach((counterGroup: CounterGroup) => {
+            counterGroup.counters.forEach((counter: CounterDetails) => {
+              const counterId: string = counterGroup.counterGroupName + counter.counterName;
+              let row: Row | undefined = rowHash.get(counterId);
+              if (!row) {
+                row = {
+                  groupName: counterGroup.counterGroupName,
+                  counterName: counter.counterName
+                };
+                rowHash.set(counterId, row);
+              }
+              row[counterKey] = counter.counterValue;
+            });
+          });
+        });
+
+        rows = Array.from(rowHash.values());
+
+        if (this.hideSimilarValues) {
+          rows = rows.filter((row: Row) => this.isValuesDifferent(row, this.counters.length));
+        }
+
+        return rows;
+      }
+    },
+
+    methods: {
+      I18n,
+      isValuesDifferent(row: Row, valueCount: number): boolean {
+        if (valueCount > 1) {
+          const firstVal = row[generateValueColumnKey(0)];
+          for (let i = 1; i < valueCount; i++) {
+            if (row[generateValueColumnKey(i)] !== firstVal) {
+              return true;
+            }
+          }
+          return false;
+        }
+        return true;
+      }
+    }
+  });
+</script>

+ 90 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/VarianceCell.vue

@@ -0,0 +1,90 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div v-if="varianceTxt">
+    <div class="variance-bar">
+      <div class="value2-bar" :style="{ width: variancePercent + '%' }" />
+    </div>
+    {{ varianceTxt }}
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
+  import { Row, generateValueColumnKey } from './CounterSet';
+
+  export default defineComponent({
+    props: {
+      data: {
+        type: Object as PropType<Row>,
+        required: true
+      }
+    },
+
+    data() {
+      return {
+        varianceTxt: '',
+        variancePercent: 0
+      };
+    },
+
+    watch: {
+      data: function () {
+        const value1 = Number(this.data[generateValueColumnKey(0)]);
+        const value2 = Number(this.data[generateValueColumnKey(1)]);
+
+        if (isNaN(value1) || isNaN(value2) || value1 <= 0 || value2 <= 0) {
+          this.varianceTxt = '';
+        } else {
+          this.variancePercent = (value2 / (value1 + value2)) * 100;
+          const variance = value1 >= value2 ? value1 / value2 : value2 / value1;
+          this.varianceTxt = `${Number(variance.toFixed(2))}x`;
+        }
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import '../../../../../../components/styles/colors.scss';
+  @import '../../../../../../components/styles/mixins';
+  .variance-bar {
+    display: inline-block;
+
+    border: 1px solid $fluid-gray-400;
+    border-radius: $hue-panel-border-radius;
+
+    width: 150px;
+    height: 10px;
+
+    overflow: hidden;
+
+    .value2-bar {
+      background-color: $fluid-blue-100;
+      height: 10px;
+      width: 50%;
+
+      border-left: 1px solid $fluid-gray-400;
+
+      margin-left: auto;
+      margin-right: 0;
+    }
+  }
+</style>

+ 235 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/__snapshots__/CountersTable.test.ts.snap

@@ -0,0 +1,235 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CountersTable.vue should render 1`] = `
+<div>
+  <div
+    class="hue-table-container"
+  >
+    <table
+      class="hue-table"
+    >
+      <caption>
+        <!-- Because of Web:TableWithoutCaptionCheck -->
+        
+      </caption>
+      <thead>
+        <tr
+          class="header-row"
+        >
+          
+          <th
+            class=""
+            scope="col"
+          >
+            Group Name
+          </th>
+          <th
+            class=""
+            scope="col"
+          >
+            Counter Name
+          </th>
+          <th
+            class="test-class1"
+            scope="col"
+          >
+            Counter set 1
+          </th>
+          <th
+            class="test-class2"
+            scope="col"
+          >
+            Counter set 2
+          </th>
+          <th
+            class=""
+            scope="col"
+          >
+            Variance
+          </th>
+          
+          <!-- To fill the blank space to the right when table width is smaller than available horizontal space -->
+          <th
+            class="column-flush"
+            scope="col"
+          />
+        </tr>
+      </thead>
+      <tbody
+        class=""
+      >
+        
+        <tr>
+          
+          <td
+            class=""
+          >
+            
+            test-group1
+            
+          </td>
+          <td
+            class=""
+          >
+            
+            test-counter1
+            
+          </td>
+          <td
+            class="test-class1"
+          >
+            
+            str-val1
+            
+          </td>
+          <td
+            class="test-class2"
+          >
+            
+            str-val3
+            
+          </td>
+          <td
+            class=""
+          >
+            
+            <!--v-if-->
+            
+          </td>
+          
+          <td
+            class="column-flush"
+          />
+        </tr>
+        <tr>
+          
+          <td
+            class=""
+          >
+            
+            test-group1
+            
+          </td>
+          <td
+            class=""
+          >
+            
+            test-counter2
+            
+          </td>
+          <td
+            class="test-class1"
+          >
+            
+            str-val2
+            
+          </td>
+          <td
+            class="test-class2"
+          >
+            
+            str-val4
+            
+          </td>
+          <td
+            class=""
+          >
+            
+            <!--v-if-->
+            
+          </td>
+          
+          <td
+            class="column-flush"
+          />
+        </tr>
+        <tr>
+          
+          <td
+            class=""
+          >
+            
+            test-group1
+            
+          </td>
+          <td
+            class=""
+          >
+            
+            test-counter3
+            
+          </td>
+          <td
+            class="test-class1"
+          >
+            
+            1
+            
+          </td>
+          <td
+            class="test-class2"
+          >
+            
+            2
+            
+          </td>
+          <td
+            class=""
+          >
+            
+            <!--v-if-->
+            
+          </td>
+          
+          <td
+            class="column-flush"
+          />
+        </tr>
+        <tr>
+          
+          <td
+            class=""
+          >
+            
+            test-group2
+            
+          </td>
+          <td
+            class=""
+          >
+            
+            test-counter1
+            
+          </td>
+          <td
+            class="test-class1"
+          >
+            
+            
+            
+          </td>
+          <td
+            class="test-class2"
+          >
+            
+            str-val5
+            
+          </td>
+          <td
+            class=""
+          >
+            
+            <!--v-if-->
+            
+          </td>
+          
+          <td
+            class="column-flush"
+          />
+        </tr>
+        
+      </tbody>
+    </table>
+  </div>
+</div>
+`;

+ 57 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-details/counters-table/__test_data__/counters.json

@@ -0,0 +1,57 @@
+[
+  {
+    "title": "Counter set 1",
+    "counters": [
+      {
+        "counterGroupName": "test-group1",
+        "counters": [
+          {
+            "counterName": "test-counter1",
+            "counterValue": "str-val1"
+          },
+          {
+            "counterName": "test-counter2",
+            "counterValue": "str-val2"
+          },
+          {
+            "counterName": "test-counter3",
+            "counterValue": "1"
+          }
+        ]
+      }
+    ],
+    "cssClass": "test-class1"
+  },
+  {
+    "title": "Counter set 2",
+    "counters": [
+      {
+        "counterGroupName": "test-group1",
+        "counters": [
+          {
+            "counterName": "test-counter1",
+            "counterValue": "str-val3"
+          },
+          {
+            "counterName": "test-counter2",
+            "counterValue": "str-val4"
+          },
+          {
+            "counterName": "test-counter3",
+            "counterValue": "2"
+          }
+        ]
+      },
+      {
+        "counterGroupName": "test-group2",
+        "counters": [
+          {
+            "counterName": "test-counter1",
+            "counterValue": "str-val5"
+          }
+        ]
+      }
+    ],
+    "cssClass": "test-class2"
+  }
+]

+ 31 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-table/QueryTable.test.ts

@@ -0,0 +1,31 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { shallowMount } from '@vue/test-utils';
+import QueryTable from './QueryTable.vue';
+
+describe('QueryTable.vue', () => {
+  it('should render', () => {
+    const wrapper = shallowMount(QueryTable, {
+      propsData: {
+        queries: [],
+        totalQueries: 0,
+        updateTime: 10
+      }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 421 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-table/QueryTable.vue

@@ -0,0 +1,421 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<!-- from table-component.hbs -->
+
+<template>
+  <div class="impala-query-table">
+    <div class="query-table-actions">
+      <search-input
+        v-model="searchText"
+        :placeholder="I18n('Query text or Query ID')"
+        :show-magnify="false"
+        @search="searchQueries"
+      />
+      <hue-button @click="searchQueries">
+        <i class="fa fa-search" />
+      </hue-button>
+
+      <div class="query-table-filters">
+        <hue-icon type="hi-filter" /> Filter by:
+        <facet-selector
+          ref="statusFacetSelector"
+          :facet="statusFacet"
+          :value-labels="statusValueLabels"
+          field-label="Status"
+          @facet-removed="facetRemoved"
+          @facet-changed="facetChanged"
+        />
+        <date-range-picker ref="rangePicker" @date-range-changed="timeRangeChanged" />
+      </div>
+
+      <div class="query-table-sort">
+        <hue-icon type="hi-sort" /> {{ I18n('Sort by') }}:
+        <sort-selector :sort="sort" :columns="columns" @sort-changed="sortChanged" />
+        <hue-button class="clear-link" borderless @click="clearSearch">
+          {{ I18n('Clear All') }}
+        </hue-button>
+      </div>
+
+      <div class="query-table-actions-right">
+        <hue-button :disabled="selectedQueries.length !== 2" @click="diffQueries(selectedQueries)">
+          {{ I18n('Compare') }}
+        </hue-button>
+        <hue-button @click="searchQueries">
+          <em class="fa fa-refresh" />
+          {{ I18n('Refresh') }}
+          <span class="update-time">
+            {{ getUpdateTimeText(updateTime) }}
+          </span>
+        </hue-button>
+      </div>
+    </div>
+    <div class="query-table-container">
+      <div class="query-table-right-panel">
+        <hue-table :columns="columns" :rows="queries">
+          <template #cell-status="query">
+            <status-indicator
+              :title="query.status"
+              class="status-indicator"
+              :value="query.status"
+            />
+          </template>
+          <template #cell-select="query">
+            <input v-model="selectedQueries" type="checkbox" :value="query" />
+          </template>
+          <template #cell-query="query">
+            <hue-link class="query-link" @click="querySelected(query)">
+              {{ query.queryText }}
+              <div class="query-popup">{{ query.queryText }}</div>
+            </hue-link>
+          </template>
+        </hue-table>
+      </div>
+    </div>
+    <paginator
+      ref="paginator"
+      :total-entries="totalQueries"
+      :current-page="currentPage"
+      @page-changed="pageChanged"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
+  import { DataProcessor, ImpalaQuery, TableDefinition } from '../index';
+  import { Range } from 'components/DateRangePicker';
+  import DateRangePicker from 'components/DateRangePicker.vue';
+  import { duration } from 'components/Duration.vue';
+  import { Facet, FacetValue, FacetValueLabels, SearchFacet } from 'components/FacetSelector';
+  import FacetSelector from 'components/FacetSelector.vue';
+  import SortSelector from './SortSelector.vue';
+  import HueButton from 'components/HueButton.vue';
+  import HueIcon from 'components/HueIcon.vue';
+  import HueLink from 'components/HueLink.vue';
+  import HueTable from 'components/HueTable.vue';
+  import { Column } from 'components/HueTable';
+  import { humanSize } from 'components/HumanByteSize.vue';
+  import Paginator, { Page } from 'components/Paginator';
+  import SearchInput from 'components/SearchInput.vue';
+  import StatusIndicator from 'components/StatusIndicator.vue';
+  import { timeAgo } from 'components/TimeAgo.vue';
+  import I18n from 'utils/i18n';
+  import { SortInfo } from '../ImpalaQueries.vue';
+
+  const STATUS_FACET_VALUES: FacetValue[] = [
+    { key: 'FINISHED', value: 0 },
+    { key: 'EXCEPTION', value: 0 }
+  ];
+  const STATUS_LABELS: FacetValueLabels = {
+    FINISHED: I18n('Finished'),
+    EXCEPTION: I18n('Exception'),
+    COMPILED: I18n('Compiled')
+  };
+
+  const DEFAULT_SORT: SortInfo = {
+    column: 'startTime',
+    order: 'DESC'
+  };
+
+  export default defineComponent({
+    name: 'QueryTable',
+    components: {
+      FacetSelector,
+      SortSelector,
+      HueIcon,
+      DateRangePicker,
+      StatusIndicator,
+      SearchInput,
+      HueButton,
+      HueLink,
+      HueTable,
+      Paginator
+    },
+    props: {
+      queries: {
+        type: Array as PropType<ImpalaQuery[]>,
+        required: true
+      },
+      totalQueries: {
+        type: Number,
+        required: true
+      },
+      updateTime: {
+        type: Number,
+        required: true
+      }
+    },
+    emits: ['diff-queries', 'kill-queries', 'query-selected', 'reload', 'search'],
+    setup() {
+      const columns: Column<ImpalaQuery>[] = [
+        { key: 'select', label: '' },
+        { key: 'status', label: 'Status' },
+        { key: 'query', label: 'Query' },
+        { key: 'queryType', label: 'Query Type' },
+        {
+          key: 'startTime',
+          label: 'Start Time',
+          adapter: (_, query) => timeAgo(query.startTime)
+        },
+        {
+          key: 'endTime',
+          label: 'End Time',
+          adapter: (_, query) => timeAgo(query.endTime)
+        },
+        {
+          key: 'duration',
+          label: 'Duration',
+          adapter: (_, query) => duration(query.duration)
+        },
+
+        { key: 'userName', label: 'User' },
+        { key: 'defaultDb', label: 'Default DB' },
+        { key: 'requestPool', label: 'Request Pool' },
+
+        {
+          key: 'cpuTime',
+          label: 'CPU Time',
+          adapter: (_, query) => duration(query.cpuTime)
+        },
+        { key: 'rowsProduced', label: 'Rows Produced' },
+        {
+          key: 'peakMemory',
+          label: 'Peak Memory',
+          adapter: (_, query) => humanSize(query.peakMemory)
+        },
+        {
+          key: 'hdfsBytesRead',
+          label: 'HDFS Bytes Read',
+          adapter: (_, query) => humanSize(query.hdfsBytesRead)
+        },
+        { key: 'coordinator', label: 'Coordinator' }
+      ];
+
+      return { columns, statusValueLabels: STATUS_LABELS, notifyThrottle: -1 };
+    },
+    data() {
+      return {
+        selectedQueries: [] as ImpalaQuery[],
+        currentPage: null as Page | null,
+        searchText: '',
+        sort: DEFAULT_SORT,
+        timeRange: null as Range | null,
+        facets: {} as { [field: string]: SearchFacet },
+        statusFacet: {
+          facetField: 'status',
+          values: STATUS_FACET_VALUES
+        } as Facet,
+        userFacet: {
+          facetField: 'userId',
+          values: []
+        } as Facet,
+        // TODO: Properly initiate TableDefinition
+        tableDefinition: {
+          rangeData: {
+            title: 'Some title'
+          },
+          columnPreferences: [{ id: 'some id' }]
+        } as TableDefinition,
+        // TODO: Properly initiate DataProcessor
+        dataProcessor: {
+          facets: { fieldCount: 0 }
+        } as DataProcessor
+      };
+    },
+    methods: {
+      getUpdateTimeText(time: number): string {
+        return time ? `${I18n('Data updated')} ${timeAgo(time)}` : '';
+      },
+      clearSearch(): void {
+        this.searchText = '';
+        this.sort = DEFAULT_SORT;
+
+        (this.$refs.rangePicker as typeof DateRangePicker).clear();
+        (this.$refs.statusFacetSelector as typeof FacetSelector).clear();
+      },
+      diffQueries(queries: ImpalaQuery[]): void {
+        this.$emit('diff-queries', queries);
+      },
+      sortChanged(sort: SortInfo): void {
+        this.sort = sort;
+        this.notifySearch();
+      },
+      facetChanged(searchFacet: SearchFacet): void {
+        this.facets[searchFacet.field] = searchFacet;
+        this.notifySearch();
+      },
+      facetRemoved(field: string): void {
+        if (this.facets[field]) {
+          delete this.facets[field];
+          this.notifySearch();
+        }
+      },
+      killQueries(queries: ImpalaQuery[]): void {
+        this.$emit('kill-queries', queries);
+      },
+      notifySearch(): void {
+        window.clearTimeout(this.notifyThrottle);
+        this.notifyThrottle = window.setTimeout(async () => {
+          this.$emit('search', {
+            page: this.currentPage,
+            text: this.searchText,
+            timeRange: this.timeRange,
+            facets: Object.values(this.facets),
+            sort: this.sort
+          });
+        }, 0);
+      },
+      pageChanged(page: Page): void {
+        this.currentPage = page;
+        this.notifySearch();
+      },
+      querySelected(query: ImpalaQuery): void {
+        this.$emit('query-selected', query);
+      },
+      reload(): void {
+        this.selectedQueries = [];
+        this.$emit('reload');
+      },
+      searchQueries(searchText: string): void {
+        if (searchText !== undefined) {
+          this.searchText = searchText;
+        }
+
+        if (this.currentPage && this.currentPage.offset !== 0) {
+          // pageChanged will notify
+          const limit = this.currentPage.limit;
+          this.currentPage = { offset: 0, pageNumber: 0, limit };
+        } else {
+          this.notifySearch();
+        }
+      },
+      timeRangeChanged(timeRange: Range): void {
+        this.timeRange = timeRange;
+        this.notifySearch();
+      },
+      I18n
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import '../../../../../components/styles/colors.scss';
+  @import '../../../../../components/styles/mixins';
+
+  .impala-query-table {
+    .query-table-actions {
+      margin-bottom: 20px;
+      width: 100%;
+
+      ::v-deep(.hue-dropdown-panel button) {
+        margin-left: 10px;
+      }
+
+      .hue-btn {
+        margin-left: 5px;
+      }
+
+      .query-table-sort,
+      .query-table-filters {
+        display: inline-block;
+        margin-left: 30px;
+
+        .columns-link,
+        .clear-link {
+          margin-left: 10px;
+        }
+      }
+
+      .query-table-actions-right {
+        display: inline-block;
+        position: absolute;
+        right: 44px;
+
+        .update-time {
+          display: inline-block;
+          position: absolute;
+          white-space: nowrap;
+
+          pointer-events: none;
+
+          font-size: 0.7em;
+          color: #5a656d;
+
+          top: 30px;
+          right: 0px;
+        }
+      }
+    }
+
+    .query-table-container {
+      display: flex;
+      flex-direction: row;
+
+      .query-table-left-panel {
+        flex: 0 0 100px;
+      }
+
+      .query-table-right-panel {
+        flex: 1 1 100%;
+        max-width: 100%;
+      }
+    }
+
+    .status-indicator {
+      font-size: 24px;
+      margin: 4px;
+
+      ::v-deep(i) {
+        font-size: 20px;
+        padding: 2px;
+        color: $fluid-gray-500;
+      }
+    }
+
+    .query-link {
+      .query-popup {
+        position: absolute;
+
+        margin-top: 5px;
+
+        white-space: pre-wrap;
+        word-break: normal;
+        overflow-wrap: break-word;
+
+        pointer-events: none;
+
+        max-width: 1000px;
+        background-color: #fff;
+        border: 1px solid $hue-border-color;
+        border-radius: $hue-panel-border-radius;
+
+        padding: 10px;
+
+        visibility: hidden;
+      }
+
+      &:hover {
+        .query-popup {
+          visibility: visible;
+        }
+      }
+    }
+  }
+</style>

+ 202 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-table/SortSelector.vue

@@ -0,0 +1,202 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <dropdown-panel :text="label" :close-on-click="false">
+    <template #default="slotProps">
+      <div class="sort-selector">
+        <div class="sort-selection">
+          <div v-for="column in sortableColumns" :key="column.key" class="sort-list-entry">
+            <label>
+              <input
+                name="sort-column"
+                type="radio"
+                :checked="column.key === currentSort.column"
+                @change="setColumn(column)"
+              />
+              {{ column.label }}
+            </label>
+          </div>
+        </div>
+
+        <label class="order-selector">
+          <input
+            type="checkbox"
+            :checked="currentSort.order === 'ASC'"
+            @change="setOrder($event.target.checked)"
+          />
+          Ascending
+        </label>
+
+        <div class="sort-selector-actions">
+          <hue-link @click="cancel(slotProps.closePanel)">{{ I18n('Cancel') }}</hue-link>
+          <hue-button
+            :small="true"
+            :primary="true"
+            :disabled="applyDisabled"
+            @click="apply(slotProps.closePanel)"
+          >
+            {{ I18n('Apply') }}
+          </hue-button>
+        </div>
+      </div>
+    </template>
+  </dropdown-panel>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
+  import I18n from 'utils/i18n';
+  import DropdownPanel from 'components/dropdown/DropdownPanel.vue';
+  import HueButton from 'components/HueButton.vue';
+  import HueLink from 'components/HueLink.vue';
+  import { SortInfo } from '../ImpalaQueries.vue';
+  import { Column } from 'components/HueTable';
+  import { ImpalaQuery } from '../index';
+
+  const SORTABLE_COLUMNS = new Set<keyof ImpalaQuery>([
+    'startTime',
+    'endTime',
+    'duration',
+    'cpuTime',
+    'rowsProduced',
+    'peakMemory',
+    'hdfsBytesRead'
+  ]);
+
+  export default defineComponent({
+    name: 'SortSelector',
+    components: {
+      DropdownPanel,
+      HueButton,
+      HueLink
+    },
+
+    props: {
+      sort: {
+        type: Object as PropType<SortInfo>,
+        required: true
+      },
+      columns: {
+        type: Object as PropType<Column<ImpalaQuery>[]>,
+        required: true
+      }
+    },
+
+    emits: ['sort-changed'],
+
+    data(): {
+      sortableColumns: Column<ImpalaQuery>[];
+      currentSort: SortInfo;
+    } {
+      return {
+        sortableColumns: this.columns.filter(column => SORTABLE_COLUMNS.has(String(column.key))),
+        currentSort: this.sort
+      };
+    },
+
+    computed: {
+      applyDisabled(): boolean {
+        return this.currentSort === this.sort;
+      },
+
+      label(): string {
+        const column = this.columns.find(col => col.key === this.sort.column);
+        return column ? `${column.label}: ${I18n(this.sort.order === 'ASC' ? 'Asc' : 'Desc')}` : '';
+      }
+    },
+
+    watch: {
+      sort: function (sort: SortInfo): void {
+        this.currentSort = sort;
+      }
+    },
+
+    methods: {
+      I18n,
+
+      setColumn(column: Column<ImpalaQuery>): void {
+        this.currentSort = { ...this.currentSort, column: String(column.key) };
+      },
+
+      setOrder(asc: boolean): void {
+        this.currentSort = { ...this.currentSort, order: asc ? 'ASC' : 'DESC' };
+      },
+
+      cancel(closePanel: () => void): void {
+        closePanel();
+      },
+
+      apply(closePanel: () => void): void {
+        this.$emit('sort-changed', this.currentSort);
+        closePanel();
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import 'components/styles/colors';
+  @import 'components/styles/mixins';
+
+  .sort-selector {
+    width: 170px;
+    padding: 3px 0;
+
+    .sort-list-entry {
+      width: 100%;
+      padding: 3px 16px;
+      margin: 6px 0;
+    }
+
+    .sort-selector-actions {
+      line-height: 24px;
+      padding: 5px 10px;
+      margin-top: 3px;
+
+      a {
+        font-size: 12px;
+      }
+
+      button {
+        float: right;
+      }
+    }
+
+    ::v-deep(label) {
+      margin: 0;
+    }
+
+    ::v-deep(input) {
+      margin: 0 10px 0 0;
+    }
+
+    .sort-selection {
+      border-bottom: 1px dotted $hue-border-color;
+      overflow-x: hidden;
+      overflow-y: auto;
+      max-height: 350px;
+    }
+
+    .order-selector {
+      padding: 5px 16px;
+      border-bottom: 1px solid $hue-border-color;
+    }
+  }
+</style>

+ 104 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/query-table/__snapshots__/QueryTable.test.ts.snap

@@ -0,0 +1,104 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QueryTable.vue should render 1`] = `
+<div
+  class="impala-query-table"
+>
+  <div
+    class="query-table-actions"
+  >
+    <search-input-stub
+      modelvalue=""
+      placeholder="Query text or Query ID"
+      showmagnify="false"
+      small="false"
+    />
+    <hue-button-stub
+      alert="false"
+      borderless="false"
+      disabled="false"
+      large="false"
+      primary="false"
+      small="false"
+    />
+    <div
+      class="query-table-filters"
+    >
+      <hue-icon-stub
+        type="hi-filter"
+      />
+       Filter by: 
+      <facet-selector-stub
+        disabled="false"
+        facet="[object Object]"
+        fieldlabel="Status"
+        filterenabled="false"
+        valuelabels="[object Object]"
+      />
+      <date-range-picker-stub
+        inline="false"
+      />
+    </div>
+    <div
+      class="query-table-sort"
+    >
+      <hue-icon-stub
+        type="hi-sort"
+      />
+       Sort by: 
+      <sort-selector-stub
+        columns="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
+        sort="[object Object]"
+      />
+      <hue-button-stub
+        alert="false"
+        borderless="true"
+        class="clear-link"
+        disabled="false"
+        large="false"
+        primary="false"
+        small="false"
+      />
+    </div>
+    <div
+      class="query-table-actions-right"
+    >
+      <hue-button-stub
+        alert="false"
+        borderless="false"
+        disabled="true"
+        large="false"
+        primary="false"
+        small="false"
+      />
+      <hue-button-stub
+        alert="false"
+        borderless="false"
+        disabled="false"
+        large="false"
+        primary="false"
+        small="false"
+      />
+    </div>
+  </div>
+  <div
+    class="query-table-container"
+  >
+    <div
+      class="query-table-right-panel"
+    >
+      <hue-table-stub
+        clickablerows="false"
+        columns="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
+        rows=""
+        showheader="true"
+        stickyfirstcolumn="false"
+        stickyheader="false"
+      />
+    </div>
+  </div>
+  <paginator-stub
+    totalentries="0"
+  />
+</div>
+`;

+ 24 - 0
desktop/core/src/desktop/js/apps/jobBrowser/components/impalaQueries/webcomp.ts

@@ -0,0 +1,24 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 { wrap } from 'vue/webComponentWrap';
+import ImpalaQueries from './ImpalaQueries.vue';
+
+export const COMPONENT_NAME = 'impala-queries';
+
+wrap(COMPONENT_NAME, ImpalaQueries);

+ 18 - 20
desktop/core/src/desktop/js/components/FacetSelector.vue

@@ -106,8 +106,9 @@
 
 
       lastKnownValues?: string[];
       lastKnownValues?: string[];
     } {
     } {
+      const facetKeys = this.facet.values.map(val => val.key);
       return {
       return {
-        selectedValues: [],
+        selectedValues: facetKeys,
         previousSelection: []
         previousSelection: []
       };
       };
     },
     },
@@ -147,28 +148,25 @@
     },
     },
 
 
     created() {
     created() {
-      this.$watch(
-        () => this.facet.values,
-        (): void => {
-          const newValues = this.facet.values.map(val => val.key);
-          if (!this.lastKnownValues) {
-            // Select all initially
-            this.selectedValues = newValues;
-            this.previousSelection = newValues;
-          } else {
-            // Keep previous selection on change
-            const selected = new Set(this.selectedValues);
+      this.$watch('facet.values.length', (): void => {
+        const newValues = this.facet.values.map(val => val.key);
+        if (!this.lastKnownValues) {
+          // Select all initially
+          this.selectedValues = newValues;
+          this.previousSelection = newValues;
+        } else {
+          // Keep previous selection on change
+          const selected = new Set(this.selectedValues);
 
 
-            // Select any new values that might have appeared
-            const oldValues = new Set(this.lastKnownValues);
+          // Select any new values that might have appeared
+          const oldValues = new Set(this.lastKnownValues);
 
 
-            this.selectedValues = newValues.filter(
-              newValue => selected.has(newValue) || !oldValues.has(newValue)
-            );
-          }
-          this.lastKnownValues = newValues;
+          this.selectedValues = newValues.filter(
+            newValue => selected.has(newValue) || !oldValues.has(newValue)
+          );
         }
         }
-      );
+        this.lastKnownValues = newValues;
+      });
     },
     },
 
 
     methods: {
     methods: {

+ 2 - 0
desktop/core/src/desktop/js/components/StatusIndicator.vue

@@ -32,7 +32,9 @@
   // TODO: Support spinner
   // TODO: Support spinner
   const ICON_MAPPING: { [value: string]: string } = {
   const ICON_MAPPING: { [value: string]: string } = {
     success: 'hi-status-success',
     success: 'hi-status-success',
+    finished: 'hi-status-success',
     error: 'hi-status-error',
     error: 'hi-status-error',
+    exception: 'hi-status-error',
     warning: 'hi-status-warning',
     warning: 'hi-status-warning',
     stopped: 'hi-status-stopped'
     stopped: 'hi-status-stopped'
   };
   };

+ 7 - 0
desktop/core/src/desktop/js/components/styles/mixins.scss

@@ -38,6 +38,13 @@ $hue-panel-border-radius: 3px;
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
+.hue-box {
+  padding: 10px;
+  overflow: hidden;
+  border: 1px solid $fluid-gray-300;
+  border-radius: $hue-panel-border-radius;
+}
+
 .no-select {
 .no-select {
   // Every browser except IE 9
   // Every browser except IE 9
   -webkit-touch-callout: none;
   -webkit-touch-callout: none;

+ 2 - 2
desktop/core/src/desktop/models.py

@@ -2084,9 +2084,9 @@ class ClusterConfig(object):
 
 
     if 'jobbrowser' in self.apps:
     if 'jobbrowser' in self.apps:
       from hadoop.cluster import get_default_yarncluster  # Circular loop
       from hadoop.cluster import get_default_yarncluster  # Circular loop
-      from jobbrowser.conf import ENABLE_HIVE_QUERY_BROWSER, ENABLE_QUERIES_LIST
+      from jobbrowser.conf import ENABLE_HIVE_QUERY_BROWSER, QUERY_STORE
 
 
-      if get_default_yarncluster() or ENABLE_HIVE_QUERY_BROWSER.get() or ENABLE_QUERIES_LIST.get():
+      if get_default_yarncluster() or ENABLE_HIVE_QUERY_BROWSER.get() or QUERY_STORE.IS_ENABLED.get():
         interpreters.append({
         interpreters.append({
           'type': 'yarn',
           'type': 'yarn',
           'displayName': _('Jobs'),
           'displayName': _('Jobs'),