You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by ji...@apache.org on 2021/01/08 18:35:13 UTC

[incubator-pinot] branch master updated: [TE]frontend - Add support for Group Constituents and Entity Metric components (#6421)

This is an automated email from the ASF dual-hosted git repository.

jihao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 3d4c98d  [TE]frontend - Add support for Group Constituents and Entity Metric components (#6421)
3d4c98d is described below

commit 3d4c98d0b669b2abb6a9e47bd5be98098e56ef75
Author: Tejas Ajmera <33...@users.noreply.github.com>
AuthorDate: Fri Jan 8 10:34:56 2021 -0800

    [TE]frontend - Add support for Group Constituents and Entity Metric components (#6421)
    
    Additionally, this PR also does the integration for being able to perform multi-level drilldown for Composite Anomalies. The Breadcrumb navigation is activated as well.
---
 .../app/mocks/compositeAnomalies.js                | 188 ++++++++++-----------
 .../components/composite-anomalies/component.js    |  52 +++++-
 .../composite-anomalies/data-table/template.hbs    |  24 +++
 .../entity-metrics-anomalies/component.js          |  86 ++++++++++
 .../entity-metrics-anomalies/template.hbs          |   1 +
 .../group-constituents-anomalies/component.js      |  81 +++++++++
 .../group-constituents-anomalies/template.hbs      |   1 +
 .../parent-anomalies/component.js                  |  64 ++-----
 .../parent-anomalies/template.hbs                  |  21 +--
 .../components/composite-anomalies/template.hbs    |  11 +-
 .../anomalies-list/template.hbs                    |   2 +-
 .../criticality/template.hbs                       |   3 +
 .../current-predicted/template.hbs                 |   6 +
 .../dimensions/template.hbs                        |   3 +
 .../group-name/component.js                        |  10 ++
 .../group-name/template.hbs                        |   3 +
 .../composite-anomalies-table/metric/template.hbs  |   3 +
 .../start-duration/component.js                    |  10 ++
 .../start-duration/template.hbs                    |   6 +-
 .../explore/composite-anomalies/controller.js      |  10 ++
 .../explore/composite-anomalies/template.hbs       |   9 +-
 .../app/styles/components/breadcrumb-list.scss     |   2 +
 .../app/styles/components/te-anomaly-table.scss    |   1 +
 .../styles/pods/custom/parent-anomalies-table.scss |  11 +-
 .../app/styles/shared/_styles.scss                 |   4 +
 .../app/utils/anomalies-tree-parser.js             |  43 +++--
 .../app/utils/composite-anomalies.js               |  48 ++++++
 .../composite-anomalies/component-test.js          |   2 +-
 .../entity-metrics-anomalies/component-test.js     |  72 ++++++++
 .../group-constituents-anomalies/component-test.js |  81 +++++++++
 .../parent-anomalies/component-test.js             |  12 +-
 .../tests/unit/utils/anomalies-tree-parser-test.js | 181 ++++++++++++--------
 32 files changed, 778 insertions(+), 273 deletions(-)

diff --git a/thirdeye/thirdeye-frontend/app/mocks/compositeAnomalies.js b/thirdeye/thirdeye-frontend/app/mocks/compositeAnomalies.js
index e81c0cf..b02f94f 100644
--- a/thirdeye/thirdeye-frontend/app/mocks/compositeAnomalies.js
+++ b/thirdeye/thirdeye-frontend/app/mocks/compositeAnomalies.js
@@ -5,8 +5,8 @@ export const mockData = [
     endTime: 1599721200000,
     feedback: null,
     properties: {
-      detectorComponentName: "grouper_one:GROUPER",
-      subEntityName: "entity_one"
+      detectorComponentName: 'grouper_one:GROUPER',
+      subEntityName: 'entity_one'
     },
     children: [
       {
@@ -14,44 +14,44 @@ export const mockData = [
         startTime: 1599462000000,
         endTime: 1599721200000,
         feedback: null,
-        avgCurrentVal: 32,
-        avgBaselineVal: 33,
+        avgCurrentVal: 4,
+        avgBaselineVal: 2,
         properties: {
-          groupScore: "6.189942819613212",
-          detectorComponentName: "detection_grouper:ANOMALY_SUMMARIZE",
-          subEntityName: "group_entity_one",
-          groupKey: "groupConstituentOne"
+          groupScore: '6.189942819613212',
+          detectorComponentName: 'detection_grouper:ANOMALY_SUMMARIZE',
+          subEntityName: 'group_entity_one',
+          groupKey: 'groupConstituentOne'
         },
         children: [
           {
             id: 3,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentOne#",
-              feature_section: "groupConstituentOne",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentOne#',
+              feature_section: 'groupConstituentOne',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
             startTime: 1599462000000,
             endTime: 1599548400000,
-            avgCurrentVal: 32,
-            avgBaselineVal: 33,
+            avgCurrentVal: 4,
+            avgBaselineVal: 2,
             feedback: null,
             children: []
           },
           {
             id: 4,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentOne#",
-              feature_section: "groupConstituentOne",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentOne#',
+              feature_section: 'groupConstituentOne',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
             startTime: 1599462000000,
             endTime: 1599548400000,
-            avgCurrentVal: 32,
-            avgBaselineVal: 33,
+            avgCurrentVal: 4,
+            avgBaselineVal: 2,
             feedback: null,
             children: []
           }
@@ -62,44 +62,44 @@ export const mockData = [
         startTime: 1599462000000,
         endTime: 1599721200000,
         feedback: null,
-        avgCurrentVal: 32,
-        avgBaselineVal: 33,
+        avgCurrentVal: 4,
+        avgBaselineVal: 2,
         properties: {
-          groupScore: "6.189942819613212",
-          detectorComponentName: "detection_grouper:ANOMALY_SUMMARIZE",
-          subEntityName: "group_entity_one",
-          groupKey: "groupConstituentTwo"
+          groupScore: '6.189942819613212',
+          detectorComponentName: 'detection_grouper:ANOMALY_SUMMARIZE',
+          subEntityName: 'group_entity_one',
+          groupKey: 'groupConstituentTwo'
         },
         children: [
           {
             id: 6,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentTwo#",
-              feature_section: "groupConstituentTwo",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentTwo#',
+              feature_section: 'groupConstituentTwo',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
             startTime: 1599462000000,
             endTime: 1599548400000,
-            avgCurrentVal: 32,
-            avgBaselineVal: 33,
+            avgCurrentVal: 4,
+            avgBaselineVal: 2,
             feedback: null,
             children: []
           },
           {
             id: 7,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentTwo#",
-              feature_section: "groupConstituentTwo",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentTwo#',
+              feature_section: 'groupConstituentTwo',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
             startTime: 1599462000000,
             endTime: 1599548400000,
-            avgCurrentVal: 32,
-            avgBaselineVal: 33,
+            avgCurrentVal: 4,
+            avgBaselineVal: 2,
             feedback: null,
             children: []
           }
@@ -109,45 +109,45 @@ export const mockData = [
         id: 8,
         startTime: 1599462000000,
         endTime: 1599721200000,
-        avgCurrentVal: 32,
-        avgBaselineVal: 33,
+        avgCurrentVal: 4,
+        avgBaselineVal: 2,
         feedback: null,
         properties: {
-          groupScore: "6.189942819613212",
-          detectorComponentName: "detection_grouper:ANOMALY_SUMMARIZE",
-          subEntityName: "group_entity_two",
-          groupKey: "groupConstituentOne"
+          groupScore: '6.189942819613212',
+          detectorComponentName: 'detection_grouper:ANOMALY_SUMMARIZE',
+          subEntityName: 'group_entity_two',
+          groupKey: 'groupConstituentOne'
         },
         children: [
           {
             id: 9,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentOne#",
-              feature_section: "groupConstituentOne",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentOne#',
+              feature_section: 'groupConstituentOne',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
             startTime: 1599462000000,
             endTime: 1599548400000,
-            avgCurrentVal: 32,
-            avgBaselineVal: 33,
+            avgCurrentVal: 4,
+            avgBaselineVal: 2,
             feedback: null,
             children: []
           },
           {
             id: 10,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentOne#",
-              feature_section: "groupConstituentOne",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentOne#',
+              feature_section: 'groupConstituentOne',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
             startTime: 1599462000000,
             endTime: 1599548400000,
-            avgCurrentVal: 32,
-            avgBaselineVal: 33,
+            avgCurrentVal: 4,
+            avgBaselineVal: 2,
             feedback: null,
             children: []
           }
@@ -157,45 +157,45 @@ export const mockData = [
         id: 11,
         startTime: 1599462000000,
         endTime: 1599721200000,
-        avgCurrentVal: 32,
-        avgBaselineVal: 33,
+        avgCurrentVal: 4,
+        avgBaselineVal: 2,
         feedback: null,
         properties: {
-          groupScore: "6.189942819613212",
-          detectorComponentName: "detection_grouper:ANOMALY_SUMMARIZE",
-          subEntityName: "group_entity_two",
-          groupKey: "groupConstituentTwo"
+          groupScore: '6.189942819613212',
+          detectorComponentName: 'detection_grouper:ANOMALY_SUMMARIZE',
+          subEntityName: 'group_entity_two',
+          groupKey: 'groupConstituentTwo'
         },
         children: [
           {
             id: 12,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentTwo#",
-              feature_section: "groupConstituentTwo",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentTwo#',
+              feature_section: 'groupConstituentTwo',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
             startTime: 1599462000000,
             endTime: 1599548400000,
-            avgCurrentVal: 32,
-            avgBaselineVal: 33,
+            avgCurrentVal: 4,
+            avgBaselineVal: 2,
             feedback: null,
             children: []
           },
           {
             id: 13,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentTwo#",
-              feature_section: "groupConstituentTwo",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentTwo#',
+              feature_section: 'groupConstituentTwo',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
             startTime: 1599462000000,
             endTime: 1599548400000,
-            avgCurrentVal: 32,
-            avgBaselineVal: 33,
+            avgCurrentVal: 4,
+            avgBaselineVal: 2,
             feedback: null,
             children: []
           }
@@ -205,31 +205,31 @@ export const mockData = [
         id: 15,
         startTime: 1599462000000,
         endTime: 1599462000000,
-        avgCurrentVal: 32,
-        avgBaselineVal: 33,
+        avgCurrentVal: 4,
+        avgBaselineVal: 2,
         feedback: null,
         children: [],
-        metric: "metric_one"
+        metric: 'metric_one'
       },
       {
         id: 16,
         startTime: 1599462000000,
         endTime: 1599462000000,
-        avgCurrentVal: 32,
-        avgBaselineVal: 33,
+        avgCurrentVal: 4,
+        avgBaselineVal: 2,
         feedback: null,
         children: [],
-        metric: "metric_one"
+        metric: 'metric_one'
       },
       {
         id: 17,
         startTime: 1599462000000,
         endTime: 1599462000000,
-        avgCurrentVal: 32,
-        avgBaselineVal: 33,
+        avgCurrentVal: 4,
+        avgBaselineVal: 2,
         feedback: null,
         children: [],
-        metric: "metric_two"
+        metric: 'metric_two'
       },
       {
         id: 18,
@@ -243,11 +243,11 @@ export const mockData = [
             id: 19,
             startTime: 1599462000000,
             endTime: 1599462000000,
-            avgCurrentVal: 32,
-            avgBaselineVal: 33,
+            avgCurrentVal: 4,
+            avgBaselineVal: 2,
             feedback: null,
             children: [],
-            metric: "metric_three"
+            metric: 'metric_three'
           }
         ]
       }
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/component.js b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/component.js
index 461754f..b7e43f0 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/component.js
+++ b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/component.js
@@ -1,9 +1,11 @@
 import Component from '@ember/component';
 import { A as EmberArray } from '@ember/array';
 
-import { parseRoot } from 'thirdeye-frontend/utils/anomalies-tree-parser';
+import { parseRoot, parseSubtree } from 'thirdeye-frontend/utils/anomalies-tree-parser';
+import pubSub from 'thirdeye-frontend/utils/pub-sub';
 
 export default Component.extend({
+  data: EmberArray(),
   /**
    * @public
    * @templateProps
@@ -32,6 +34,14 @@ export default Component.extend({
    */
   onBreadcrumbClick: () => {},
 
+  /**
+   * The callback function to invoke when anomaly is drilled down
+   *
+   * @public
+   * @type {Function}
+   */
+  onDrilldown: () => {},
+
   /** Internal states */
 
   /**
@@ -42,6 +52,10 @@ export default Component.extend({
    */
   breadcrumbList: EmberArray(),
 
+  onAnomalyDrilldown(anomalyId) {
+    this.set('data', parseSubtree(anomalyId, this.anomalies));
+  },
+
   /**
    * Ember component life hook.
    *
@@ -53,17 +67,44 @@ export default Component.extend({
 
   /**
    * Ember component life hook.
-   * Call the tree parser to fetch the component information for the root level
+   * Call the tree parser to fetch the component information for the root level and
+   * subscribe to drilldown updates.
    *
    * @override
    */
   didReceiveAttrs() {
     this._super(...arguments);
 
-    const { breadcrumbInfo } = parseRoot(this.alertId, this.anomalies);
+    //Initial Processing
+    const { output, breadcrumbInfo } = parseRoot(this.alertId, this.anomalies);
 
     this.breadcrumbList.push(breadcrumbInfo);
+    this.set('data', output);
+
+    //Processing on each drilldown
+    const subscription = pubSub.subscribe('onAnomalyDrilldown', (anomalyId) => {
+      const { output, breadcrumbInfo } = parseSubtree(anomalyId, this.anomalies);
+
+      this.set('breadcrumbList', [...this.breadcrumbList, breadcrumbInfo]);
+      this.set('data', output);
+
+      this.onDrilldown();
+    });
+
+    this.set('subscription', subscription);
   },
+
+  /**
+   * Ember component life hook.
+   * Delete the callback subscribed to for anomaly drilldowns
+   *
+   * @override
+   */
+  willDestroyElement() {
+    this.subscription.unSubscribe();
+    this._super(...arguments);
+  },
+
   /**
    * Event handlers
    */
@@ -78,7 +119,10 @@ export default Component.extend({
      * @param {Number} index
      *   The index of the breadcrumb that was clicked
      */
-    onBreadcrumbClick({ isRoot = false }, index) {
+    onBreadcrumbClick({ id, isRoot = false }, index) {
+      const { output } = isRoot ? parseRoot(id, this.anomalies) : parseSubtree(id, this.anomalies);
+
+      this.set('data', output);
       this.set('breadcrumbList', this.breadcrumbList.splice(0, index + 1));
 
       this.onBreadcrumbClick(isRoot);
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/data-table/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/data-table/template.hbs
new file mode 100644
index 0000000..3031f27
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/data-table/template.hbs
@@ -0,0 +1,24 @@
+<div class="panel-group">
+  <div class="panel panel-default">
+    <div class="panel-heading">
+      <h4 class="panel-title">
+        {{title}}
+      </h4>
+    </div>
+    {{#if tableData.length}}
+      <div class="panel-body">
+        {{models-table
+          data=tableData
+          columns=tableColumns
+          showGlobalFilter=false
+          showColumnsDropdown=false
+          customClasses=customClasses
+        }}
+      </div>
+    {{else}}
+      <p class="composite-anomalies-no-records">
+        {{noRecords}}
+      </p>
+    {{/if}}
+  </div>
+</div>
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/entity-metrics-anomalies/component.js b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/entity-metrics-anomalies/component.js
new file mode 100644
index 0000000..29402a7
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/entity-metrics-anomalies/component.js
@@ -0,0 +1,86 @@
+/*
+ * Entity Metric Anomalies Component
+ *
+ * Display a table containing entity metric anomalies
+ * @module composite-anomalies/entity-metric-anomalies
+ * @property {string} title - Heading to use on the table
+ * @property {object[]} data - [required] array of composite anomalies objects
+ *
+ * @example
+ * {{composite-anomalies/entity-metric-anomalies title=<title> data=<data>}}
+ *
+ * @exports composite-anomalies/entity-metric-anomalies
+ */
+
+import Component from '@ember/component';
+import { computed } from '@ember/object';
+import { A as EmberArray } from '@ember/array';
+
+import moment from 'moment';
+import config from 'thirdeye-frontend/config/environment';
+import { getAnomaliesStartDuration, getFeedback } from 'thirdeye-frontend/utils/composite-anomalies';
+
+const TABLE_COLUMNS = [
+  {
+    component: 'custom/composite-anomalies-table/start-duration',
+    propertyName: 'startDuration',
+    title: `Start / Duration (${moment.tz([2012, 5], config.timeZone).format('z')})`
+  },
+  {
+    component: 'custom/composite-anomalies-table/metric',
+    propertyName: 'metric',
+    title: 'Metric'
+  },
+  {
+    component: 'custom/composite-anomalies-table/dimensions',
+    propertyName: 'dimensions',
+    title: 'Dimension'
+  },
+  {
+    component: 'custom/composite-anomalies-table/current-predicted',
+    propertyName: 'currentPredicted',
+    title: 'Current / Predicted'
+  },
+  {
+    component: 'custom/composite-anomalies-table/resolution',
+    propertyName: 'feedback',
+    title: 'Feedback'
+  },
+  {
+    component: 'custom/anomalies-table/investigation-link',
+    propertyName: 'id',
+    title: 'Investigate'
+  }
+];
+
+const CUSTOM_TABLE_CLASSES = {
+  table: 'composite-anomalies-table'
+};
+
+export default Component.extend({
+  data: EmberArray(),
+  tagName: 'section',
+  customClasses: CUSTOM_TABLE_CLASSES,
+  title: 'Metrics Anomalies',
+  noRecords: 'No Entity Metrics found',
+  tableData: computed('data', function () {
+    const computedTableData = [];
+
+    if (this.data && this.data.length > 0) {
+      this.data.map((d) => {
+        const row = {
+          id: d.id,
+          startDuration: getAnomaliesStartDuration(d.startTime, d.endTime, false),
+          metric: d.metric,
+          dimensions: d.dimensions,
+          currentPredicted: d.currentPredicted,
+          feedback: getFeedback(d.feedback)
+        };
+        computedTableData.push(row);
+      });
+    }
+
+    return computedTableData;
+  }),
+  tableColumns: TABLE_COLUMNS
+});
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/entity-metrics-anomalies/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/entity-metrics-anomalies/template.hbs
new file mode 100644
index 0000000..4c67aa6
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/entity-metrics-anomalies/template.hbs
@@ -0,0 +1 @@
+{{composite-anomalies/data-table title=title tableData=tableData tableColumns=tableColumns noRecords=noRecords}}
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/group-constituents-anomalies/component.js b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/group-constituents-anomalies/component.js
new file mode 100644
index 0000000..2a868d4
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/group-constituents-anomalies/component.js
@@ -0,0 +1,81 @@
+/*
+ * Group Constituents Anomalies Component
+ *
+ * Display a table containing group constituents anomalies
+ * @module composite-anomalies/group-constituents-anomalies
+ * @property {string} title - Heading to use on the table
+ * @property {object[]} data - [required] array of composite anomalies objects
+ *
+ * @example
+ * {{composite-anomalies/group-constituents-anomalies title=<title> data=<data>}}
+ *
+ * @exports composite-anomalies/group-constituents-anomalies
+ */
+
+import Component from '@ember/component';
+import { computed } from '@ember/object';
+import { A as EmberArray } from '@ember/array';
+
+import moment from 'moment';
+import config from 'thirdeye-frontend/config/environment';
+import { getAnomaliesStartDuration, getFeedback } from 'thirdeye-frontend/utils/composite-anomalies';
+
+const TABLE_COLUMNS = [
+  {
+    component: 'custom/composite-anomalies-table/start-duration',
+    propertyName: 'startDuration',
+    title: `Start / Duration (${moment.tz([2012, 5], config.timeZone).format('z')})`
+  },
+  {
+    component: 'custom/composite-anomalies-table/group-name',
+    propertyName: 'groupName',
+    title: 'Group Name'
+  },
+  {
+    component: 'custom/composite-anomalies-table/criticality',
+    propertyName: 'criticalityScore',
+    title: 'Criticality Score'
+  },
+  {
+    component: 'custom/composite-anomalies-table/current-predicted',
+    propertyName: 'currentPredicted',
+    title: 'Current / Predicted'
+  },
+  {
+    component: 'custom/composite-anomalies-table/resolution',
+    propertyName: 'feedback',
+    title: 'Feedback'
+  }
+];
+
+const CUSTOM_TABLE_CLASSES = {
+  table: 'composite-anomalies-table'
+};
+
+export default Component.extend({
+  data: EmberArray(),
+  tagName: 'section',
+  customClasses: CUSTOM_TABLE_CLASSES,
+  title: 'Entity', // Default Header if no title is passed in.
+  noRecords: 'No Groups found',
+  tableData: computed('data', function () {
+    const computedTableData = [];
+
+    if (this.data && this.data.length > 0) {
+      this.data.map((d) => {
+        const row = {
+          anomalyId: d.id,
+          startDuration: getAnomaliesStartDuration(d.startTime, d.endTime, false),
+          groupName: d.groupName,
+          criticalityScore: d.criticality,
+          currentPredicted: d.currentPredicted,
+          feedback: getFeedback(d.feedback)
+        };
+        computedTableData.push(row);
+      });
+    }
+
+    return computedTableData;
+  }),
+  tableColumns: TABLE_COLUMNS
+});
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/group-constituents-anomalies/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/group-constituents-anomalies/template.hbs
new file mode 100644
index 0000000..4c67aa6
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/group-constituents-anomalies/template.hbs
@@ -0,0 +1 @@
+{{composite-anomalies/data-table title=title tableData=tableData tableColumns=tableColumns noRecords=noRecords}}
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/parent-anomalies/component.js b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/parent-anomalies/component.js
index 2bbf542..de9049e 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/parent-anomalies/component.js
+++ b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/parent-anomalies/component.js
@@ -18,32 +18,34 @@ import { A as EmberArray } from '@ember/array';
 
 import moment from 'moment';
 import config from 'thirdeye-frontend/config/environment';
-import * as anomalyUtil from 'thirdeye-frontend/utils/anomaly';
+import { getAnomaliesStartDuration, getFeedback } from 'thirdeye-frontend/utils/composite-anomalies';
 
 const TABLE_COLUMNS = [
   {
-    template: 'custom/composite-anomalies-table/start-duration',
+    component: 'custom/composite-anomalies-table/start-duration',
     propertyName: 'startDuration',
     title: `Start / Duration (${moment.tz([2012, 5], config.timeZone).format('z')})`
   },
   {
     template: 'custom/composite-anomalies-table/anomalies-list',
-    propertyName: 'details',
+    propertyName: 'anomaliesDetails',
     title: 'Anomalies details'
   },
   {
     component: 'custom/composite-anomalies-table/resolution',
     propertyName: 'feedback',
-    title: 'Feedback '
+    title: 'Feedback'
   }
 ];
 
+const CUSTOM_TABLE_CLASSES = {
+  table: 'composite-anomalies-table'
+};
+
 export default Component.extend({
   data: EmberArray(),
   tagName: 'section',
-  customClasses: {
-    table: 'composite-anomalies-table'
-  },
+  customClasses: CUSTOM_TABLE_CLASSES,
   title: 'Composite Anomalies', // Default Header if no title is passed in.
   noRecords: 'No Composite Anomalies found',
   tableData: computed('data', function () {
@@ -52,9 +54,10 @@ export default Component.extend({
     if (this.data && this.data.length > 0) {
       this.data.map((d) => {
         const row = {
-          startDuration: this.getAnomaliesStartDuration(d.startTime, d.endTime),
+          anomalyId: d.id,
+          startDuration: getAnomaliesStartDuration(d.startTime, d.endTime, true),
           anomaliesDetails: this.getAnomaliesDetails(d.details),
-          feedback: this.getFeedback(d.feedback)
+          feedback: getFeedback(d.feedback)
         };
         computedTableData.push(row);
       });
@@ -63,55 +66,18 @@ export default Component.extend({
   }),
   tableColumns: TABLE_COLUMNS,
   /*
-   *  convert anomaly 'start' and 'end' into an object to be used by template: 'custom/composite-animalies-table/start-duration'
-   *
-   * @param {Number} start
-   * The start time in milliseconds.
-   * @param {Number} end
-   * The end time in milliseconds
-   *
-   * @returns {Object}
-   * Description of the object.
-   */
-  getAnomaliesStartDuration: (start, end) => {
-    return {
-      startTime: start,
-      endTime: end,
-      duration: moment.duration(end - start).asHours() + ' hours'
-    };
-  },
-  /*
-   *  convert list of anonalies object in to an array of objects to be used by template: 'custom/composite-animalies-table/anomalies-list'
+   * Convert list of anonalies object in to an array of objects to be used by template: 'custom/composite-animalies-table/anomalies-list'
    *
    * @param {Object} details
-   * The object containing the list of anomalies and their count.
+   *   The object containing the list of anomalies and their count.
    *
    * @returns {Array}
-   * Description of the object.
+   *   Description of the object.
    */
   getAnomaliesDetails: (anomalies) => {
     return Object.entries(anomalies).reduce((anomalyList, anomalyDetails) => {
       anomalyList.push({ name: anomalyDetails[0], count: anomalyDetails[1] });
       return anomalyList;
     }, []);
-  },
-  /*
-   * return feedbackObject with pre-selected 'feedback' to be use in the feedback dropdown
-   *
-   * @param {String} feedback
-   * The object containing the list of anomalies and their count.
-   *
-   * @returns {Object} feedbackObject
-   * Description of the object.
-   */
-  getFeedback: (feedback) => {
-    const selectedFeedback = feedback
-      ? anomalyUtil.anomalyResponseObj.find((f) => f.value === feedback)
-      : anomalyUtil.anomalyResponseObj[0];
-
-    return {
-      options: anomalyUtil.anomalyResponseObj.mapBy('name'),
-      selected: selectedFeedback ? selectedFeedback.name : anomalyUtil.anomalyResponseObj[0].name
-    };
   }
 });
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/parent-anomalies/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/parent-anomalies/template.hbs
index ea66b01..4c67aa6 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/parent-anomalies/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/parent-anomalies/template.hbs
@@ -1,20 +1 @@
-<div class="panel-group">
-    <div class="panel panel-default">
-        <div class="panel-heading">
-            <h4 class="panel-title">{{title}}</h4>
-        </div>
-    {{#if tableData.length}}
-        <div class="panel-body"> 
-            {{models-table
-            data=tableData
-            columns=tableColumns
-            showGlobalFilter=false
-            showColumnsDropdown=false
-            customClasses=customClasses
-            }}
-        </div> 
-    {{else}}
-        <p class="composite-anomalies-no-records">{{noRecords}}</p>
-    {{/if}}
-    </div>
-</div>
\ No newline at end of file
+{{composite-anomalies/data-table title=title tableData=tableData tableColumns=tableColumns noRecords=noRecords}}
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/template.hbs
index a37c87f..9b64c90 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/components/composite-anomalies/template.hbs
@@ -1,8 +1,9 @@
 {{yield}}
-
 <section>
-  {{breadcrumb-list
-    items=breadcrumbList
-    onBreadcrumbClick=(action "onBreadcrumbClick")
-  }}
+  {{breadcrumb-list items=breadcrumbList onBreadcrumbClick=(action "onBreadcrumbClick")}}
 </section>
+<section>
+  {{#each data as |cmpt|}}
+    {{component cmpt.componentPath title=cmpt.title data=cmpt.data}}
+  {{/each}}
+</section>
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/anomalies-list/template.hbs b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/anomalies-list/template.hbs
index 0f64c45..26c6c47 100644
--- a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/anomalies-list/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/anomalies-list/template.hbs
@@ -1,3 +1,3 @@
 {{#each record.anomaliesDetails as |anomalyDetails| }}
-    <p class="details">{{anomalyDetails.name}} ({{anomalyDetails.count}})</p>
+    <p class="te-anomaly-table__details te-label te-label--small">{{anomalyDetails.name}} ({{anomalyDetails.count}})</p>
 {{/each}}
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/criticality/template.hbs b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/criticality/template.hbs
new file mode 100644
index 0000000..7a3d230
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/criticality/template.hbs
@@ -0,0 +1,3 @@
+<p class="te-label te-label--small te-anomaly-table__criticality">
+    {{record.criticalityScore}}
+</p>
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/current-predicted/template.hbs b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/current-predicted/template.hbs
new file mode 100644
index 0000000..e0bd32f
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/current-predicted/template.hbs
@@ -0,0 +1,6 @@
+<p class="te-anomaly-table__current-baseline">
+  {{record.currentPredicted.current}}/{{record.currentPredicted.predicted}}
+</p>
+<p class="te-anomaly-table__value-label te-anomaly-table__value-label--dash te-anomaly-table__deviation-percent te-anomaly-table__value-label--{{calculate-direction record.currentPredicted.deviation}}">
+  ({{record.currentPredicted.deviationPercent}})
+</p>
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/dimensions/template.hbs b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/dimensions/template.hbs
new file mode 100644
index 0000000..a2142c1
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/dimensions/template.hbs
@@ -0,0 +1,3 @@
+{{#each-in record.dimensions as |key value|}}
+  <p class="te-label te-label--small te-anomaly-table__dimension">{{key}}:{{value}}</p>
+{{/each-in}}
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/group-name/component.js b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/group-name/component.js
new file mode 100644
index 0000000..adbb6d4
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/group-name/component.js
@@ -0,0 +1,10 @@
+import Component from '@ember/component';
+import pubSub from 'thirdeye-frontend/utils/pub-sub';
+
+export default Component.extend({
+  actions: {
+    drilldownGroupConstituent(anomalyId) {
+      pubSub.publish('onAnomalyDrilldown', anomalyId);
+    }
+  }
+});
diff --git a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/group-name/template.hbs b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/group-name/template.hbs
new file mode 100644
index 0000000..323da0c
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/group-name/template.hbs
@@ -0,0 +1,3 @@
+<p class="te-label te-label--small te-anomaly-table__link te-anomaly-table__group" onclick={{action "drilldownGroupConstituent" record.anomalyId}}>
+    {{record.groupName}}
+</p>
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/metric/template.hbs b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/metric/template.hbs
new file mode 100644
index 0000000..f24d5d8
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/metric/template.hbs
@@ -0,0 +1,3 @@
+<p class="te-label te-label--small te-anomaly-table__metric">
+    {{record.metric}}
+</p>
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/start-duration/component.js b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/start-duration/component.js
new file mode 100644
index 0000000..1a2713a
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/start-duration/component.js
@@ -0,0 +1,10 @@
+import Component from '@ember/component';
+import pubSub from 'thirdeye-frontend/utils/pub-sub';
+
+export default Component.extend({
+  actions: {
+    drilldownAnomaly(anomalyId) {
+      pubSub.publish('onAnomalyDrilldown', anomalyId);
+    }
+  }
+});
diff --git a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/start-duration/template.hbs b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/start-duration/template.hbs
index 51fce82..0d2f4b1 100644
--- a/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/start-duration/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/custom/composite-anomalies-table/start-duration/template.hbs
@@ -1,8 +1,8 @@
-<label class="te-label te-label--small te-anomaly-table__link">
-    <span class="start-time">{{moment-format record.startDuration.startTime "MMM Do, h:mm z"}}</span>
+<label class="te-label te-label--small {{if record.startDuration.showLink "te-anomaly-table__link" "te-label--disable-click"}}">
+    <span class="start-time" onclick={{action "drilldownAnomaly" record.anomalyId}}>{{moment-format record.startDuration.startTime "MMM Do, h:mm z"}}</span>
     {{#tooltip-on-element}}
       Start: {{moment-format record.startDuration.startTime "MMM Do, h:mm z"}}<br />
       End: {{moment-format record.startDuration.endTime "MMM Do, h:mm z"}}
     {{/tooltip-on-element}}
 </label>
-<p class="duration">{{record.startDuration.duration}}</p>
\ No newline at end of file
+<p class="te-anomaly-table__duration">{{record.startDuration.duration}}</p>
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/explore/composite-anomalies/controller.js b/thirdeye/thirdeye-frontend/app/pods/manage/explore/composite-anomalies/controller.js
index 02d2c19..4251b84 100644
--- a/thirdeye/thirdeye-frontend/app/pods/manage/explore/composite-anomalies/controller.js
+++ b/thirdeye/thirdeye-frontend/app/pods/manage/explore/composite-anomalies/controller.js
@@ -149,6 +149,16 @@ export default Controller.extend({
      */
     onBreadcrumbClick(isRoot = false) {
       this.set('rootLevel', isRoot);
+    },
+
+    /**
+     * Function to disable the ability to select dates since user is not on root level
+     *
+     * @param {Boolean} isRoot
+     *   Tracks if we are working on root level of the tree
+     */
+    onDrilldown() {
+      this.set('rootLevel', false);
     }
   }
 });
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/explore/composite-anomalies/template.hbs b/thirdeye/thirdeye-frontend/app/pods/manage/explore/composite-anomalies/template.hbs
index e38db1f..06969f5 100644
--- a/thirdeye/thirdeye-frontend/app/pods/manage/explore/composite-anomalies/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/manage/explore/composite-anomalies/template.hbs
@@ -53,8 +53,13 @@
       <div class="spinner-wrapper spinner-wrapper--card">
         {{ember-spinner lines=30 radius=20 length=0 width=10 opacity=0 trail=75 color="blue"}}
       </div>
-    {{else}}
-      {{composite-anomalies anomalies=anomalies alertId=model.alertId onBreadcrumbClick=(action "onBreadcrumbClick")}}
+    {{else}}  
+      {{composite-anomalies
+        anomalies=anomalies
+        alertId=model.alertId
+        onBreadcrumbClick=(action "onBreadcrumbClick")
+        onDrilldown=(action "onDrilldown")
+      }}
     {{/if}}
   </section>
 </section>
diff --git a/thirdeye/thirdeye-frontend/app/styles/components/breadcrumb-list.scss b/thirdeye/thirdeye-frontend/app/styles/components/breadcrumb-list.scss
index 34d9f76..e87b29b 100644
--- a/thirdeye/thirdeye-frontend/app/styles/components/breadcrumb-list.scss
+++ b/thirdeye/thirdeye-frontend/app/styles/components/breadcrumb-list.scss
@@ -11,9 +11,11 @@
     padding-left: 10px;
     font-size: 14px;
     font-weight: 400;
+    cursor:pointer;
   }
 
   &__title:last-of-type {
     font-weight: 700;
+    cursor: auto;
   }
 }
diff --git a/thirdeye/thirdeye-frontend/app/styles/components/te-anomaly-table.scss b/thirdeye/thirdeye-frontend/app/styles/components/te-anomaly-table.scss
index d2d0ff5..d00e0fa 100644
--- a/thirdeye/thirdeye-frontend/app/styles/components/te-anomaly-table.scss
+++ b/thirdeye/thirdeye-frontend/app/styles/components/te-anomaly-table.scss
@@ -176,6 +176,7 @@
     font-weight: 600;
     letter-spacing: .03em;
     font-size: 15px;
+    cursor: pointer;
 
     &--glyph {
       margin-left: 10px;
diff --git a/thirdeye/thirdeye-frontend/app/styles/pods/custom/parent-anomalies-table.scss b/thirdeye/thirdeye-frontend/app/styles/pods/custom/parent-anomalies-table.scss
index 258a86a..ff2ee95 100644
--- a/thirdeye/thirdeye-frontend/app/styles/pods/custom/parent-anomalies-table.scss
+++ b/thirdeye/thirdeye-frontend/app/styles/pods/custom/parent-anomalies-table.scss
@@ -6,10 +6,13 @@
       }
       td, th {
           padding: 10px 10px 10px 20px;
-  
-          .details, .duration {
-              margin: 0;
-          } 
+      }
+      &__multi-line-column {
+          margin: 0;
+          color: black;
+      }
+      &__duration {
+          margin: 0;
       }
       margin-bottom: 20px;
   }
diff --git a/thirdeye/thirdeye-frontend/app/styles/shared/_styles.scss b/thirdeye/thirdeye-frontend/app/styles/shared/_styles.scss
index 2041630..58f98bc 100644
--- a/thirdeye/thirdeye-frontend/app/styles/shared/_styles.scss
+++ b/thirdeye/thirdeye-frontend/app/styles/shared/_styles.scss
@@ -334,6 +334,10 @@ body {
   &--dark {
     color: app-shade(black, 0.85);
   }
+
+  &--disable-click {
+    pointer-events: none;
+  }
 }
 
 .te-input {
diff --git a/thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js b/thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
index af02b31..72d3128 100644
--- a/thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
+++ b/thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
@@ -2,21 +2,22 @@ import { isEmpty } from '@ember/utils';
 import { set } from '@ember/object';
 import moment from 'moment';
 import { BREADCRUMB_TIME_DISPLAY_FORMAT } from 'thirdeye-frontend/utils/constants';
+import { humanizeFloat, humanizeChange } from 'thirdeye-frontend/utils/utils';
 
 const CLASSIFICATIONS = {
   METRICS: {
     KEY: 'metrics',
-    COMPONENT_PATH: 'entity-metrics',
+    COMPONENT_PATH: 'composite-anomalies/entity-metrics-anomalies',
     DEFAULT_TITLE: 'Metric Anomalies'
   },
   GROUPS: {
     KEY: 'groups',
-    COMPONENT_PATH: 'entity-groups',
+    COMPONENT_PATH: 'composite-anomalies/group-constituents-anomalies',
     DEFAULT_TITLE: 'ENTITY:'
   },
   ENTITIES: {
     KEY: 'entities',
-    COMPONENT_PATH: 'parent-anomalies',
+    COMPONENT_PATH: 'composite-anomalies/parent-anomalies',
     DEFAULT_TITLE: 'Entity'
   }
 };
@@ -162,8 +163,14 @@ const setMetricsBucket = (buckets, anomaly, metric) => {
     endTime,
     metric,
     feedback,
-    current,
-    predicted
+    currentPredicted: {
+      current: humanizeFloat(current),
+      predicted: humanizeFloat(predicted),
+      deviation: Number((current - predicted) / predicted),
+      get deviationPercent() {
+        return humanizeChange(this.deviation);
+      }
+    }
   };
 
   if (isEmpty(data)) {
@@ -211,8 +218,14 @@ const setGroupsBucket = (buckets, anomaly, subEntityName, groupName) => {
     endTime,
     feedback,
     criticality,
-    current,
-    predicted
+    currentPredicted: {
+      current: humanizeFloat(current),
+      predicted: humanizeFloat(predicted),
+      deviation: Number((current - predicted) / predicted),
+      get deviationPercent() {
+        return humanizeChange(this.deviation);
+      }
+    }
   };
 
   if ([groupKey] in buckets) {
@@ -362,7 +375,7 @@ const parseGroupAnomaly = (input) => {
   const output = [];
   const data = [];
   const {
-    GROUPS: { DEFAULT_TITLE, COMPONENT_PATH }
+    METRICS: { DEFAULT_TITLE, COMPONENT_PATH }
   } = CLASSIFICATIONS;
   const {
     id,
@@ -393,8 +406,14 @@ const parseGroupAnomaly = (input) => {
       feedback,
       metric,
       dimensions,
-      current,
-      predicted
+      currentPredicted: {
+        current: humanizeFloat(current),
+        predicted: humanizeFloat(predicted),
+        deviation: Number((current - predicted) / predicted),
+        get deviationPercent() {
+          return humanizeChange(this.deviation);
+        }
+      }
     });
   }
 
@@ -515,8 +534,8 @@ export const parseRoot = (explorationId, input) => {
  *   The anomaly id
  * @param {Array<Object> or Object} input
  *   The tree structure hosting the anomaly referenced by the id.
- *      -If the entire tree is being passed, it would in array form
- *      -If a subtree is being passed, it would be in object form
+ *      -If the entire tree is being passed, it would be in an array form
+ *      -If a subtree is being passed, it would be in an object form
  *
  * @return {Object}
  *   The breadcrumb info and data for instantiating tables at any level in tree
diff --git a/thirdeye/thirdeye-frontend/app/utils/composite-anomalies.js b/thirdeye/thirdeye-frontend/app/utils/composite-anomalies.js
new file mode 100644
index 0000000..b08d764
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/utils/composite-anomalies.js
@@ -0,0 +1,48 @@
+/*
+ * Util functions to structure the data to be passed into individual columns
+ */
+
+import moment from 'moment';
+import * as anomalyUtil from 'thirdeye-frontend/utils/anomaly';
+
+/*
+ * Convert anomaly 'start' and 'end' into an object
+ *
+ * @param {Number} start
+ *   The start time in milliseconds.
+ * @param {Number} end
+ *   The end time in milliseconds
+ * @param {Boolean} showLink
+ *   Indicates whether to hyperlink the start duration
+ *
+ * @returns {Object}
+ *   Description of the object.
+ */
+export const getAnomaliesStartDuration = (start, end, showLink = false) => {
+  return {
+    startTime: start,
+    endTime: end,
+    duration: moment.duration(end - start).asHours() + ' hours',
+    showLink
+  };
+};
+
+/*
+ * Return feedbackObject with pre-selected 'feedback' to be use in the feedback dropdown
+ *
+ * @param {String} feedback
+ *   The object containing the list of anomalies and their count.
+ *
+ * @returns {Object} feedbackObject
+ *   Description of the object.
+ */
+export const getFeedback = (feedback) => {
+  const selectedFeedback = feedback
+    ? anomalyUtil.anomalyResponseObj.find((f) => f.value === feedback)
+    : anomalyUtil.anomalyResponseObj[0];
+
+  return {
+    options: anomalyUtil.anomalyResponseObj.mapBy('name'),
+    selected: selectedFeedback ? selectedFeedback.name : anomalyUtil.anomalyResponseObj[0].name
+  };
+};
diff --git a/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/component-test.js b/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/component-test.js
index 9dae98b..53637a7 100644
--- a/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/component-test.js
+++ b/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/component-test.js
@@ -16,5 +16,5 @@ test('it renders', function (assert) {
 
   this.render(hbs`{{composite-anomalies alertId=alertId anomalies=anomalies}}`);
 
-  assert.equal(this.$().text().trim(), 'Alert Anomalies');
+  assert.ok(this.$().text().trim().includes('Alert Anomalies'));
 });
diff --git a/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/entity-metrics-anomalies/component-test.js b/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/entity-metrics-anomalies/component-test.js
new file mode 100644
index 0000000..21b4da5
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/entity-metrics-anomalies/component-test.js
@@ -0,0 +1,72 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import * as anomalyUtil from 'thirdeye-frontend/utils/anomaly';
+
+moduleForComponent(
+  'composite-anomalies/entity-metrics-anomalies',
+  'Integration | Component | composite anomalies/entity metrics anomalies',
+  {
+    integration: true
+  }
+);
+
+test('it renders', function (assert) {
+  this.setProperties({
+    tableTitle: 'ENTITY',
+    tableData: [
+      {
+        id: 9,
+        startTime: 1599462000000,
+        endTime: 1599721200000,
+        feedback: null,
+        metric: 'metric_four',
+        dimensions: {
+          feature_name: 'groupConstituentOne#',
+          feature_section: 'groupConstituentOne',
+          dimension_three: 'True',
+          use_case: 'DESKTOP'
+        },
+        currentPredicted: {
+          current: '4.00',
+          predicted: '2.00',
+          deviation: 1,
+          deviationPercent: '+100.0%'
+        }
+      }
+    ],
+    feedbackOptionNames: anomalyUtil.anomalyResponseObj.mapBy('name'),
+    feedbackOptionValues: anomalyUtil.anomalyResponseObj.mapBy('value')
+  });
+
+  this.render(hbs`
+    {{composite-anomalies/entity-metrics-anomalies title=tableTitle data=tableData}}
+  `);
+
+  assert.equal(this.$('.panel-title').html().trim(), this.tableTitle);
+
+  assert.equal(this.$('.te-anomaly-table__duration')[0].innerHTML, '72 hours');
+
+  assert.equal(this.$('.te-anomaly-table__metric')[0].innerHTML.trim(), 'metric_four');
+
+  assert.equal(this.$('.te-anomaly-table__dimension')[0].innerHTML.trim(), 'feature_name:groupConstituentOne#');
+  assert.equal(this.$('.te-anomaly-table__dimension')[1].innerHTML.trim(), 'feature_section:groupConstituentOne');
+  assert.equal(this.$('.te-anomaly-table__dimension')[2].innerHTML.trim(), 'dimension_three:True');
+  assert.equal(this.$('.te-anomaly-table__dimension')[3].innerHTML.trim(), 'use_case:DESKTOP');
+
+  assert.equal(this.$('.te-anomaly-table__current-baseline')[0].innerHTML.trim(), '4.00/2.00');
+  assert.equal(this.$('.te-anomaly-table__deviation-percent')[0].innerHTML.trim(), '(+100.0%)');
+
+  assert.equal(this.$('.ember-power-select-selected-item').html().trim(), this.feedbackOptionNames[0]);
+
+  // Check other values based on feedback
+
+  this.feedbackOptionNames.forEach((option, index) => {
+    this.tableData[0].feedback = this.feedbackOptionValues[index];
+
+    this.render(hbs`
+      {{composite-anomalies/entity-metrics-anomalies data=tableData}}
+    `);
+
+    assert.equal(this.$('.ember-power-select-selected-item').html().trim(), option);
+  });
+});
diff --git a/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/group-constituents-anomalies/component-test.js b/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/group-constituents-anomalies/component-test.js
new file mode 100644
index 0000000..ddd8279
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/group-constituents-anomalies/component-test.js
@@ -0,0 +1,81 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import * as anomalyUtil from 'thirdeye-frontend/utils/anomaly';
+
+moduleForComponent(
+  'composite-anomalies/group-constituent-anomalies',
+  'Integration | Component | composite anomalies/group constituent anomalies',
+  {
+    integration: true
+  }
+);
+
+test('it renders', function (assert) {
+  this.setProperties({
+    tableTitle: 'ENTITY:group_entity_one',
+    tableData: [
+      {
+        id: 2,
+        groupName: 'groupConstituentOne',
+        startTime: 1599462000000,
+        endTime: 1599721200000,
+        feedback: null,
+        criticality: '6.189942819613212',
+        currentPredicted: {
+          current: '4.00',
+          predicted: '2.00',
+          deviation: 1,
+          deviationPercent: '+100.0%'
+        }
+      },
+      {
+        id: 5,
+        groupName: 'groupConstituentTwo',
+        startTime: 1599462000000,
+        endTime: 1599721200000,
+        feedback: null,
+        criticality: '1.213451',
+        currentPredicted: {
+          current: '4.00',
+          predicted: '2.00',
+          deviation: 1,
+          deviationPercent: '+100.0%'
+        }
+      }
+    ],
+    feedbackOptionNames: anomalyUtil.anomalyResponseObj.mapBy('name'),
+    feedbackOptionValues: anomalyUtil.anomalyResponseObj.mapBy('value')
+  });
+
+  this.render(hbs`
+    {{composite-anomalies/group-constituents-anomalies title=tableTitle data=tableData}}
+  `);
+
+  assert.equal(this.$('.panel-title').html().trim(), this.tableTitle);
+
+  assert.equal(this.$('.te-anomaly-table__duration')[0].innerHTML, '72 hours');
+  assert.equal(this.$('.te-anomaly-table__duration')[1].innerHTML, '72 hours');
+
+  assert.equal(this.$('.te-anomaly-table__group')[0].innerHTML.trim(), 'groupConstituentOne');
+  assert.equal(this.$('.te-anomaly-table__group')[1].innerHTML.trim(), 'groupConstituentTwo');
+
+  assert.equal(this.$('.te-anomaly-table__criticality')[0].innerHTML.trim(), '6.189942819613212');
+  assert.equal(this.$('.te-anomaly-table__criticality')[1].innerHTML.trim(), '1.213451');
+
+  assert.equal(this.$('.te-anomaly-table__current-baseline')[0].innerHTML.trim(), '4.00/2.00');
+  assert.equal(this.$('.te-anomaly-table__deviation-percent')[0].innerHTML.trim(), '(+100.0%)');
+
+  assert.equal(this.$('.ember-power-select-selected-item').html().trim(), this.feedbackOptionNames[0]);
+
+  // Check other values based on feedback
+
+  this.feedbackOptionNames.forEach((option, index) => {
+    this.tableData[0].feedback = this.feedbackOptionValues[index];
+
+    this.render(hbs`
+      {{composite-anomalies/group-constituents-anomalies data=tableData}}
+    `);
+
+    assert.equal(this.$('.ember-power-select-selected-item').html().trim(), option);
+  });
+});
diff --git a/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/parent-anomalies/component-test.js b/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/parent-anomalies/component-test.js
index ebb08d8..1c8dbc9 100644
--- a/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/parent-anomalies/component-test.js
+++ b/thirdeye/thirdeye-frontend/tests/integration/pods/components/composite-anomalies/parent-anomalies/component-test.js
@@ -36,23 +36,23 @@ test('it renders', function (assert) {
 
   this.render(hbs`{{composite-anomalies/parent-anomalies}}`);
 
-  assert.equal(this.$('h4.panel-title').html(), this.tableTitle);
-  assert.equal(this.$('p.composite-anomalies-no-records').html(), this.noAnmalies);
+  assert.equal(this.$('h4.panel-title').html().trim(), this.tableTitle);
+  assert.equal(this.$('p.composite-anomalies-no-records').html().trim(), this.noAnmalies);
 
   this.render(hbs`
     {{composite-anomalies/parent-anomalies title=tableTitle}}
   `);
 
-  assert.equal(this.$('.panel-title').html(), this.tableTitle);
-  assert.equal(this.$('.composite-anomalies-no-records').html(), this.noAnmalies);
+  assert.equal(this.$('.panel-title').html().trim(), this.tableTitle);
+  assert.equal(this.$('.composite-anomalies-no-records').html().trim(), this.noAnmalies);
 
   this.render(hbs`
     {{composite-anomalies/parent-anomalies data=tableData}}
   `);
 
   // assert.equal($('.start-time').html(), 'Sep 7th, 12:00 ');
-  assert.equal(this.$('.duration').html(), '72 hours');
-  assert.equal(this.$('.details').html(), 'oe_viral_detection (2)');
+  assert.equal(this.$('.te-anomaly-table__duration').html(), '72 hours');
+  assert.equal(this.$('.te-anomaly-table__details').html(), 'oe_viral_detection (2)');
   assert.equal(this.$('.ember-power-select-selected-item').html().trim(), this.feedbackOptionNames[0]);
 
   // Check other values based on feedback
diff --git a/thirdeye/thirdeye-frontend/tests/unit/utils/anomalies-tree-parser-test.js b/thirdeye/thirdeye-frontend/tests/unit/utils/anomalies-tree-parser-test.js
index bc6ad54..61fc716 100644
--- a/thirdeye/thirdeye-frontend/tests/unit/utils/anomalies-tree-parser-test.js
+++ b/thirdeye/thirdeye-frontend/tests/unit/utils/anomalies-tree-parser-test.js
@@ -1,24 +1,21 @@
-import { mockData } from "thirdeye-frontend/mocks/compositeAnomalies";
-import { module, test } from "qunit";
-import {
-  parseRoot,
-  parseSubtree
-} from "thirdeye-frontend/utils/anomalies-tree-parser";
-
-module("Unit | Utility | Anomalies tree parser utils", function() {
-  test("it parses root level parent anomalies correctly", function(assert) {
+import { mockData } from 'thirdeye-frontend/mocks/compositeAnomalies';
+import { module, test } from 'qunit';
+import { parseRoot, parseSubtree } from 'thirdeye-frontend/utils/anomalies-tree-parser';
+
+module('Unit | Utility | Anomalies tree parser utils', function () {
+  test('it parses root level parent anomalies correctly', function (assert) {
     const explorationId = 121;
     const { breadcrumbInfo, output } = parseRoot(explorationId, mockData);
 
     const expectedBreadcrumbInfo = {
-      title: "Alert Anomalies",
+      title: 'Alert Anomalies',
       isRoot: true,
       id: 121
     };
 
     const expectedOutput = [
       {
-        componentPath: "parent-anomalies",
+        componentPath: 'composite-anomalies/parent-anomalies',
         data: [
           {
             id: 1,
@@ -34,7 +31,7 @@ module("Unit | Utility | Anomalies tree parser utils", function() {
             }
           }
         ],
-        title: "Entity"
+        title: 'Entity'
       }
     ];
 
@@ -42,7 +39,7 @@ module("Unit | Utility | Anomalies tree parser utils", function() {
     assert.deepEqual(output, expectedOutput);
   });
 
-  test("it drills down a composite anomaly correctly - level 1 drilldown example", function(assert) {
+  test('it drills down a composite anomaly correctly - level 1 drilldown example', function (assert) {
     const anomalyId = 1;
     const {
       breadcrumbInfo: { id, isRoot },
@@ -51,93 +48,121 @@ module("Unit | Utility | Anomalies tree parser utils", function() {
 
     const expectedOutput = [
       {
-        componentPath: "entity-groups",
-        title: "ENTITY:group_entity_one",
+        componentPath: 'composite-anomalies/group-constituents-anomalies',
+        title: 'ENTITY:group_entity_one',
         data: [
           {
             id: 2,
-            groupName: "groupConstituentOne",
+            groupName: 'groupConstituentOne',
             startTime: 1599462000000,
             endTime: 1599721200000,
             feedback: null,
-            criticality: "6.189942819613212",
-            current: 32,
-            predicted: 33
+            criticality: '6.189942819613212',
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           },
           {
             id: 5,
-            groupName: "groupConstituentTwo",
+            groupName: 'groupConstituentTwo',
             startTime: 1599462000000,
             endTime: 1599721200000,
             feedback: null,
-            criticality: "6.189942819613212",
-            current: 32,
-            predicted: 33
+            criticality: '6.189942819613212',
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           }
         ]
       },
       {
-        componentPath: "entity-groups",
-        title: "ENTITY:group_entity_two",
+        componentPath: 'composite-anomalies/group-constituents-anomalies',
+        title: 'ENTITY:group_entity_two',
         data: [
           {
             id: 8,
-            groupName: "groupConstituentOne",
+            groupName: 'groupConstituentOne',
             startTime: 1599462000000,
             endTime: 1599721200000,
             feedback: null,
-            criticality: "6.189942819613212",
-            current: 32,
-            predicted: 33
+            criticality: '6.189942819613212',
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           },
           {
             id: 11,
-            groupName: "groupConstituentTwo",
+            groupName: 'groupConstituentTwo',
             startTime: 1599462000000,
             endTime: 1599721200000,
             feedback: null,
-            criticality: "6.189942819613212",
-            current: 32,
-            predicted: 33
+            criticality: '6.189942819613212',
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           }
         ]
       },
       {
-        componentPath: "entity-metrics",
-        title: "Metric Anomalies",
+        componentPath: 'composite-anomalies/entity-metrics-anomalies',
+        title: 'Metric Anomalies',
         data: [
           {
             id: 15,
             startTime: 1599462000000,
             endTime: 1599462000000,
-            metric: "metric_one",
+            metric: 'metric_one',
             feedback: null,
-            current: 32,
-            predicted: 33
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           },
           {
             id: 16,
             startTime: 1599462000000,
             endTime: 1599462000000,
-            metric: "metric_one",
+            metric: 'metric_one',
             feedback: null,
-            current: 32,
-            predicted: 33
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           },
           {
             id: 17,
             startTime: 1599462000000,
             endTime: 1599462000000,
-            metric: "metric_two",
+            metric: 'metric_two',
             feedback: null,
-            current: 32,
-            predicted: 33
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           }
         ]
       },
       {
-        componentPath: "parent-anomalies",
-        title: "Entity",
+        componentPath: 'composite-anomalies/parent-anomalies',
+        title: 'Entity',
         data: [
           {
             id: 18,
@@ -157,56 +182,64 @@ module("Unit | Utility | Anomalies tree parser utils", function() {
     assert.notEqual(
       isRoot,
       true,
-      "Breadcrumb state should be indicating we are not doing root level parsing of the tree"
+      'Breadcrumb state should be indicating we are not doing root level parsing of the tree'
     );
 
     //output tests
     assert.deepEqual(output, expectedOutput);
   });
 
-  test("it drills down an ananomaly grouped by anomaly summarize grouper correctly - level 2 drilldown example", function(assert) {
+  test('it drills down an ananomaly grouped by anomaly summarize grouper correctly - level 2 drilldown example', function (assert) {
     const anomalyId = 8;
     const { breadcrumbInfo, output } = parseSubtree(anomalyId, mockData);
 
     const expectedBreadcrumbInfo = {
-      title: "group_entity_two/groupConstituentOne",
+      title: 'group_entity_two/groupConstituentOne',
       id: 8
     };
 
     const expectedOutput = [
       {
-        componentPath: "entity-groups",
-        title: "ENTITY:",
+        componentPath: 'composite-anomalies/entity-metrics-anomalies',
+        title: 'Metric Anomalies',
         data: [
           {
             id: 9,
             startTime: 1599462000000,
             endTime: 1599548400000,
             feedback: null,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentOne#",
-              feature_section: "groupConstituentOne",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentOne#',
+              feature_section: 'groupConstituentOne',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
-            current: 32,
-            predicted: 33
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           },
           {
             id: 10,
             startTime: 1599462000000,
             endTime: 1599548400000,
             feedback: null,
-            metric: "metric_four",
+            metric: 'metric_four',
             dimensions: {
-              feature_name: "groupConstituentOne#",
-              feature_section: "groupConstituentOne",
-              dimension_three: "True",
-              use_case: "DESKTOP"
+              feature_name: 'groupConstituentOne#',
+              feature_section: 'groupConstituentOne',
+              dimension_three: 'True',
+              use_case: 'DESKTOP'
             },
-            current: 32,
-            predicted: 33
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           }
         ]
       }
@@ -216,7 +249,7 @@ module("Unit | Utility | Anomalies tree parser utils", function() {
     assert.deepEqual(output, expectedOutput);
   });
 
-  test("it drills down a composite anomaly correctly - level 2 drilldown example (composite anomaly within a composite anomaly)", function(assert) {
+  test('it drills down a composite anomaly correctly - level 2 drilldown example (composite anomaly within a composite anomaly)', function (assert) {
     const anomalyId = 18;
     const {
       breadcrumbInfo: { id, isRoot },
@@ -225,17 +258,21 @@ module("Unit | Utility | Anomalies tree parser utils", function() {
 
     const expectedOutput = [
       {
-        componentPath: "entity-metrics",
-        title: "Metric Anomalies",
+        componentPath: 'composite-anomalies/entity-metrics-anomalies',
+        title: 'Metric Anomalies',
         data: [
           {
             id: 19,
             startTime: 1599462000000,
             endTime: 1599462000000,
-            metric: "metric_three",
+            metric: 'metric_three',
             feedback: null,
-            current: 32,
-            predicted: 33
+            currentPredicted: {
+              current: '4.00',
+              predicted: '2.00',
+              deviation: 1,
+              deviationPercent: '+100.0%'
+            }
           }
         ]
       }
@@ -246,7 +283,7 @@ module("Unit | Utility | Anomalies tree parser utils", function() {
     assert.notEqual(
       isRoot,
       true,
-      "Breadcrumb state should be indicating we are not doing root level parsing of the tree"
+      'Breadcrumb state should be indicating we are not doing root level parsing of the tree'
     );
 
     //output tests


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org