You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by GitBox <gi...@apache.org> on 2020/11/25 10:48:40 UTC

[GitHub] [incubator-pinot] tejasajmera opened a new pull request #6290: [TE]frontend - Build the tree parser for composite anomalies

tejasajmera opened a new pull request #6290:
URL: https://github.com/apache/incubator-pinot/pull/6290


   The parser contains the logic to extract data in order to build child components at any level of the tree when performing the drill-down analysis for Entity Monitoring.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [incubator-pinot] tejasajmera commented on a change in pull request #6290: [TE]frontend - Build the tree parser for composite anomalies

Posted by GitBox <gi...@apache.org>.
tejasajmera commented on a change in pull request #6290:
URL: https://github.com/apache/incubator-pinot/pull/6290#discussion_r532978875



##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";
+
+/**
+ * Format the timestamp into the form to be shown in the breadcrumb
+ *
+ * @param {Number} timestamp
+ *   The timestamp of anomaly creation time in milliseconds
+ *
+ * @returns {String}
+ *   Formatted timestamp. Example of the required format - "Sep 15 16:49 EST"
+ */
+const getFormattedBreadcrumbTime = timestamp => {
+  const zoneName = moment.tz.guess();
+  const timeZoneAbbreviation = moment.tz(zoneName).zoneAbbr();
+
+  return `${moment(timestamp).format(
+    BREADCRUMB_TIME_DISPLAY_FORMAT
+  )} ${timeZoneAbbreviation}`;
+};
+
+/**
+ * Parse the anomalies generated by the composite alert to populate parent-anomalies table with relevent details about
+ * children for each anomaly.
+ *
+ * @param {Array<Object>} input
+ *   The anomalies for composite alert.
+ *
+ * @returns {Array<Object>}
+ *   Parsed out contents to populate parent-anomalies table
+ */
+const populateParentAnomaliesTable = input => {
+  const output = [];
+
+  for (const entry of input) {
+    const { id, startTime, endTime, feedback, children } = entry;
+    const entryOutput = {
+      id,
+      startTime,
+      endTime,
+      feedback
+    };
+
+    const details = {};
+    let item;
+    if (children.length > 0) {
+      for (const child of children) {
+        const { metric, properties: { subEntityName } = {} } = child;
+
+        if (!isEmpty(metric)) {
+          item = metric;
+        } else {
+          item = subEntityName;
+        }
+
+        if (item in details) {
+          details[item]++;
+        } else {
+          details[item] = 1;
+        }
+      }
+      entryOutput.details = details;
+      output.push(entryOutput);
+    }
+  }
+
+  return output;
+};
+
+/**
+ * Parse the generated bucket for metric anomalies into the schema for the entity-metrics component
+ *
+ * @param {Object} input
+ *   The metric anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ *   The content to be passed into the the leaf level entity-metrics component. Each item in the array represents
+ *   contents for the row in the table.
+ */
+const parseMetricsBucket = input => {
+  return [input];
+};
+
+/**
+ * Parse the generated bucket for parent anomalies into the schema for the entity-groups component
+ *
+ * @param {Object} input
+ *   The group anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ *   The content to be passed into the the entity-groups component. Each item in the array represents
+ *   contents for the row in the table.
+ */
+const parseGroupsBucket = input => {
+  const output = [];
+
+  for (const group in input) {
+    output.push(input[group]);
+  }
+
+  return output;
+};
+
+/**
+ * Parse the generated bucket for parent anomalies into the schema for the parent-anomalies component
+ *
+ * @param {Object} input
+ *   The parent anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ *   The content to be passed into the parent-anomalies component. Each item in the array represents
+ *   contents for the row in the table.
+ */
+const parseEntitiesBucket = input => {
+  const output = [];
+
+  for (const entity in input) {
+    const { componentPath, title, data } = input[entity];
+
+    output.push({
+      componentPath,
+      title,
+      data: populateParentAnomaliesTable(data)
+    });
+  }
+
+  return output;
+};
+
+/**
+ * Add the anomaly referencing a metric to the metric bucket
+ *
+ * @param {Object} buckets
+ *   The reference to buckets object within which the anomaly needs to be classified
+ * @param {Object} anomaly
+ *   The metric anomaly that needs be classified added to the metric bucket
+ * @param {String} metric
+ *   The metric for which this anomaly was generated
+ */
+const setMetricsBucket = (buckets, anomaly, metric) => {
+  const {
+    METRICS: { KEY: metricKey, DEFAULT_TITLE, COMPONENT_PATH }
+  } = CLASSIFICATIONS;
+  const { [metricKey]: { data } = {} } = buckets;
+  const {
+    id,
+    startTime,
+    endTime,
+    feedback,
+    avgCurrentVal: current,
+    avgBaselineVal: predicted
+  } = anomaly;
+
+  const metricTableRow = {
+    id,
+    startTime,
+    endTime,
+    metric,
+    feedback,
+    current,
+    predicted
+  };
+
+  if (isEmpty(data)) {
+    const metricBucketObj = {
+      componentPath: COMPONENT_PATH,
+      title: DEFAULT_TITLE,
+      data: [metricTableRow]
+    };
+
+    set(buckets, `${metricKey}`, metricBucketObj);
+  } else {
+    data.push(metricTableRow);
+  }
+};
+
+/**
+ * Add the anomaly referencing a group constitient to the right group characterized by the subEntityName
+ *
+ * @param {Object} buckets
+ *   The reference to buckets object within which the anomaly needs to be classified
+ * @param {Object} anomaly
+ *   The anomaly produced due to the anomaly summarize grouper that needs be classified into the appropriate bucket
+ * @param {String} subEntityName
+ *   The entity name under which certain set of metrics would be grouped
+ * @param {String} groupName
+ *   The group constituent name. Each group constituent hosts anomalies from one metric.
+ */
+const setGroupsBucket = (buckets, anomaly, subEntityName, groupName) => {
+  const {
+    GROUPS: { KEY: groupKey, COMPONENT_PATH, DEFAULT_TITLE }
+  } = CLASSIFICATIONS;
+  const {
+    id,
+    startTime,
+    endTime,
+    feedback,
+    avgCurrentVal: current,
+    avgBaselineVal: predicted,
+    properties: { groupScore: criticality }
+  } = anomaly;
+  const groupTableRow = {
+    id,
+    groupName,
+    startTime,
+    endTime,
+    feedback,
+    criticality,
+    current,
+    predicted
+  };
+
+  if ([groupKey] in buckets) {
+    if (subEntityName in buckets[groupKey]) {
+      const {
+        [subEntityName]: { data }
+      } = buckets[groupKey];
+
+      data.push(groupTableRow);
+    } else {
+      set(buckets, `${groupKey}.${subEntityName}`, {
+        componentPath: COMPONENT_PATH,
+        title: `${DEFAULT_TITLE}${subEntityName}`,
+        data: [groupTableRow]
+      });
+    }
+  } else {
+    set(buckets, `${groupKey}`, {
+      [subEntityName]: {
+        componentPath: COMPONENT_PATH,
+        title: `${DEFAULT_TITLE}${subEntityName}`,
+        data: [groupTableRow]
+      }
+    });
+  }
+};
+
+/**
+ * Add the composite anomaly to the right bucket characterized by the subEntityName
+ *
+ * @param {Object} buckets
+ *   The reference to buckets object within which the anomaly needs to be classified
+ * @param {Object} anomaly
+ *   The composite anomaly that needs be classified into the appropriate bucket
+ * @param {String} subEntityName
+ *   The entity name under which this anomaly falls
+ */
+const setEntitiesBucket = (buckets, anomaly, subEntityName) => {
+  const {
+    ENTITIES: { KEY: entityKey, COMPONENT_PATH }
+  } = CLASSIFICATIONS;
+  let title;
+
+  if (isEmpty(subEntityName)) {
+    const {
+      ENTITIES: { DEFAULT_TITLE }
+    } = CLASSIFICATIONS;
+
+    title = DEFAULT_TITLE;
+  } else {
+    title = subEntityName;
+  }
+
+  if ([entityKey] in buckets) {
+    if (subEntityName in buckets[entityKey]) {
+      const {
+        [subEntityName]: { data }
+      } = buckets[entityKey];
+
+      data.push(anomaly);
+    } else {
+      set(buckets, `${entityKey}.${subEntityName}`, {
+        componentPath: COMPONENT_PATH,
+        title: title,
+        data: [anomaly]
+      });
+    }
+  } else {
+    set(buckets, `${entityKey}`, {
+      [subEntityName]: {
+        componentPath: COMPONENT_PATH,
+        title: title,
+        data: [anomaly]
+      }
+    });
+  }
+};
+
+/**
+ * Classify the child anomalies of particular anomaly into metrics, groups and parent-anomalies
+ *   -Anomalies of the yaml type METRIC_ALERT classify into "metrics"
+ *   -Anomalies of the yaml type METRIC_ALERT and grouper as ANOMALY_SUMMARIZE classify into "groups"
+ *   -Anomalies of the yaml type COMPOSITE_ALERT classify into "parent-anomalies"
+ *
+ * @param {Object} input
+ *   The subtree structure that needs to be parsed
+ *
+ * @return {Object}
+ *   The classification of children anomalies into the buckets of "metrics", "groups" and "entities".
+ *   The structure will take the form as below
+ *   {
+ *     metrics: {
+ *        componentPath: '',
+ *        title: '',
+ *        data:[{},{}] //anomaly entries
+ *      },
+ *     groups: {
+ *         groupEntity1: {
+ *            componentPath: '',
+ *            title:'',
+ *            data:[{},{}]  //each entry in array corresponds to information for 1 group constituent
+ *         },
+ *         groupEntity2: {
+ *         }
+ *      },
+ *     entities: {
+ *         entity1: {
+ *           componentPath: '',
+ *            title:'',
+ *            data:[{},{}]
+ *         },
+ *         entity2: {
+ *         }
+ *     }
+ *   }
+ */
+const generateBuckets = input => {
+  const buckets = {};
+  const { children } = input;
+
+  for (const child of children) {
+    const {
+      metric,
+      properties: { detectorComponentName = "", subEntityName, groupKey } = {}
+    } = child;
+
+    if (!isEmpty(metric)) {
+      setMetricsBucket(buckets, child, metric);
+    } else if (
+      isEmpty(metric) &&
+      detectorComponentName.includes("ANOMALY_SUMMARIZE")
+    ) {
+      setGroupsBucket(buckets, child, subEntityName, groupKey);
+    } else {
+      setEntitiesBucket(buckets, child, subEntityName);
+    }
+  }
+
+  return buckets;
+};
+
+/**
+ * Perform drilldown of anomaly grouped by anomaly summarize grouper. This involves generating the breadcrumb information
+ * and component details for the subtree for this anomaly.
+ *
+ * @param {Object} input
+ *   The subtree structure that needs to be parsed
+ *
+ * @return {Object}
+ *   The breadcrumb info and data for populating component comprising of group constituents
+ */
+const parseGroupAnomaly = input => {
+  const output = [];
+  const data = [];
+  const {
+    GROUPS: { DEFAULT_TITLE, COMPONENT_PATH }
+  } = CLASSIFICATIONS;
+  const {
+    id,
+    children,
+    properties: { subEntityName, groupKey }
+  } = input;
+  const breadcrumbInfo = {
+    title: `${subEntityName}/${groupKey}`,
+    id
+  };
+
+  for (const anomaly of children) {
+    const {
+      id,
+      startTime,
+      endTime,
+      metric,
+      dimensions,
+      avgCurrentVal: current,
+      avgBaselineVal: predicted,
+      feedback
+    } = anomaly;
+
+    data.push({
+      id,
+      startTime,
+      endTime,
+      feedback,
+      metric,
+      dimensions,
+      current,
+      predicted
+    });
+  }
+
+  output.push({
+    componentPath: COMPONENT_PATH,
+    title: DEFAULT_TITLE,
+    data
+  });
+
+  return { breadcrumbInfo, output };
+};
+
+/**
+ * Perform drilldown of composite anomaly. This involves generating the breadcrumb information
+ * and component details for the subtree for the composite anomaly
+ *
+ * @param {Object} input
+ *   The subtree structure that needs to be parsed
+ *
+ * @return {Object}
+ *   The breadcrumb info and data for populating child components from input subtree.
+ */
+const parseCompositeAnomaly = input => {
+  const output = [];
+  const buckets = generateBuckets(input);
+  const {
+    METRICS: { KEY: metricKey },
+    GROUPS: { KEY: groupKey }
+  } = CLASSIFICATIONS;
+  const { id, startTime } = input;
+  const breadcrumbInfo = {
+    id,
+    title: getFormattedBreadcrumbTime(startTime)
+  };
+
+  for (const key in buckets) {
+    const entry = buckets[key];
+
+    if (key === metricKey) {
+      output.push(...parseMetricsBucket(entry));
+    } else if (key === groupKey) {
+      output.push(...parseGroupsBucket(entry));
+    } else {
+      output.push(...parseEntitiesBucket(entry));
+    }
+  }
+
+  return { breadcrumbInfo, output };
+};
+
+/**
+ * Perform depth-first-search to retrieve anomaly in the tree
+ *
+ * @param {Number} id
+ *   The id of the anomaly to be searched
+ * @param {Object} input
+ *   The subtree structure comprising the anomaly
+ *
+ * @return {Object}
+ *   The anomaly referenced by id
+ */
+const findAnomaly = (id, input) => {
+  const { id: anomalyId, children } = input;
+
+  if (anomalyId === id) {
+    return input;
+  } else {

Review comment:
       Yeah more than aiming for conciseness I was just trying to keeping it explicit. Anyway will remove the else.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [incubator-pinot] jihaozh merged pull request #6290: [TE]frontend - Build the tree parser for composite anomalies

Posted by GitBox <gi...@apache.org>.
jihaozh merged pull request #6290:
URL: https://github.com/apache/incubator-pinot/pull/6290


   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [incubator-pinot] zhangloo333 commented on a change in pull request #6290: [TE]frontend - Build the tree parser for composite anomalies

Posted by GitBox <gi...@apache.org>.
zhangloo333 commented on a change in pull request #6290:
URL: https://github.com/apache/incubator-pinot/pull/6290#discussion_r532779193



##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";

Review comment:
       I suggest you put this time format in the constant.js or to create a constant folder. it's a common useful variable. We can inject into the function. 

##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",

Review comment:
       please keep code style consistency. I remember we enable the single quote rule. You could run prettier to format it.

##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";
+
+/**
+ * Format the timestamp into the form to be shown in the breadcrumb
+ *
+ * @param {Number} timestamp
+ *   The timestamp of anomaly creation time in milliseconds
+ *
+ * @returns {String}
+ *   Formatted timestamp. Example of the required format - "Sep 15 16:49 EST"
+ */
+const getFormattedBreadcrumbTime = timestamp => {
+  const zoneName = moment.tz.guess();
+  const timeZoneAbbreviation = moment.tz(zoneName).zoneAbbr();
+
+  return `${moment(timestamp).format(
+    BREADCRUMB_TIME_DISPLAY_FORMAT
+  )} ${timeZoneAbbreviation}`;
+};
+
+/**
+ * Parse the anomalies generated by the composite alert to populate parent-anomalies table with relevent details about
+ * children for each anomaly.
+ *
+ * @param {Array<Object>} input
+ *   The anomalies for composite alert.
+ *
+ * @returns {Array<Object>}
+ *   Parsed out contents to populate parent-anomalies table
+ */
+const populateParentAnomaliesTable = input => {

Review comment:
       Please add parentheses for input to keep clarity and obey the eslint rule(arrow-parens).
   For more detail to check `8.4 Always include parentheses around arguments for clarity and consistency. eslint: arrow-parens`

##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";
+
+/**
+ * Format the timestamp into the form to be shown in the breadcrumb
+ *
+ * @param {Number} timestamp
+ *   The timestamp of anomaly creation time in milliseconds
+ *
+ * @returns {String}
+ *   Formatted timestamp. Example of the required format - "Sep 15 16:49 EST"
+ */
+const getFormattedBreadcrumbTime = timestamp => {
+  const zoneName = moment.tz.guess();
+  const timeZoneAbbreviation = moment.tz(zoneName).zoneAbbr();
+
+  return `${moment(timestamp).format(
+    BREADCRUMB_TIME_DISPLAY_FORMAT
+  )} ${timeZoneAbbreviation}`;
+};
+
+/**
+ * Parse the anomalies generated by the composite alert to populate parent-anomalies table with relevent details about
+ * children for each anomaly.
+ *
+ * @param {Array<Object>} input
+ *   The anomalies for composite alert.
+ *
+ * @returns {Array<Object>}
+ *   Parsed out contents to populate parent-anomalies table
+ */
+const populateParentAnomaliesTable = input => {
+  const output = [];
+
+  for (const entry of input) {
+    const { id, startTime, endTime, feedback, children } = entry;
+    const entryOutput = {
+      id,
+      startTime,
+      endTime,
+      feedback
+    };
+
+    const details = {};
+    let item;
+    if (children.length > 0) {
+      for (const child of children) {
+        const { metric, properties: { subEntityName } = {} } = child;
+
+        if (!isEmpty(metric)) {
+          item = metric;
+        } else {
+          item = subEntityName;
+        }
+
+        if (item in details) {
+          details[item]++;
+        } else {
+          details[item] = 1;
+        }
+      }
+      entryOutput.details = details;
+      output.push(entryOutput);
+    }
+  }
+
+  return output;
+};
+
+/**
+ * Parse the generated bucket for metric anomalies into the schema for the entity-metrics component
+ *
+ * @param {Object} input
+ *   The metric anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ *   The content to be passed into the the leaf level entity-metrics component. Each item in the array represents
+ *   contents for the row in the table.
+ */
+const parseMetricsBucket = input => {

Review comment:
       ditto, coding style 

##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";
+
+/**
+ * Format the timestamp into the form to be shown in the breadcrumb
+ *
+ * @param {Number} timestamp
+ *   The timestamp of anomaly creation time in milliseconds
+ *
+ * @returns {String}
+ *   Formatted timestamp. Example of the required format - "Sep 15 16:49 EST"
+ */
+const getFormattedBreadcrumbTime = timestamp => {
+  const zoneName = moment.tz.guess();
+  const timeZoneAbbreviation = moment.tz(zoneName).zoneAbbr();
+
+  return `${moment(timestamp).format(
+    BREADCRUMB_TIME_DISPLAY_FORMAT
+  )} ${timeZoneAbbreviation}`;
+};
+
+/**
+ * Parse the anomalies generated by the composite alert to populate parent-anomalies table with relevent details about
+ * children for each anomaly.
+ *
+ * @param {Array<Object>} input
+ *   The anomalies for composite alert.
+ *
+ * @returns {Array<Object>}
+ *   Parsed out contents to populate parent-anomalies table
+ */
+const populateParentAnomaliesTable = input => {
+  const output = [];
+
+  for (const entry of input) {
+    const { id, startTime, endTime, feedback, children } = entry;
+    const entryOutput = {
+      id,
+      startTime,
+      endTime,
+      feedback
+    };
+
+    const details = {};
+    let item;
+    if (children.length > 0) {
+      for (const child of children) {
+        const { metric, properties: { subEntityName } = {} } = child;
+
+        if (!isEmpty(metric)) {

Review comment:
       if there is no complicated logic, you could use the ternary to shorter logic and reduce code.

##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";
+
+/**
+ * Format the timestamp into the form to be shown in the breadcrumb
+ *
+ * @param {Number} timestamp
+ *   The timestamp of anomaly creation time in milliseconds
+ *
+ * @returns {String}
+ *   Formatted timestamp. Example of the required format - "Sep 15 16:49 EST"
+ */
+const getFormattedBreadcrumbTime = timestamp => {
+  const zoneName = moment.tz.guess();
+  const timeZoneAbbreviation = moment.tz(zoneName).zoneAbbr();
+
+  return `${moment(timestamp).format(
+    BREADCRUMB_TIME_DISPLAY_FORMAT
+  )} ${timeZoneAbbreviation}`;
+};
+
+/**
+ * Parse the anomalies generated by the composite alert to populate parent-anomalies table with relevent details about
+ * children for each anomaly.
+ *
+ * @param {Array<Object>} input
+ *   The anomalies for composite alert.
+ *
+ * @returns {Array<Object>}
+ *   Parsed out contents to populate parent-anomalies table
+ */
+const populateParentAnomaliesTable = input => {
+  const output = [];
+
+  for (const entry of input) {
+    const { id, startTime, endTime, feedback, children } = entry;
+    const entryOutput = {
+      id,
+      startTime,
+      endTime,
+      feedback
+    };
+
+    const details = {};
+    let item;
+    if (children.length > 0) {
+      for (const child of children) {
+        const { metric, properties: { subEntityName } = {} } = child;
+
+        if (!isEmpty(metric)) {
+          item = metric;
+        } else {
+          item = subEntityName;
+        }
+
+        if (item in details) {
+          details[item]++;
+        } else {
+          details[item] = 1;
+        }
+      }
+      entryOutput.details = details;
+      output.push(entryOutput);
+    }
+  }
+
+  return output;
+};
+
+/**
+ * Parse the generated bucket for metric anomalies into the schema for the entity-metrics component
+ *
+ * @param {Object} input
+ *   The metric anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ *   The content to be passed into the the leaf level entity-metrics component. Each item in the array represents
+ *   contents for the row in the table.
+ */
+const parseMetricsBucket = input => {
+  return [input];
+};
+
+/**
+ * Parse the generated bucket for parent anomalies into the schema for the entity-groups component
+ *
+ * @param {Object} input
+ *   The group anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ *   The content to be passed into the the entity-groups component. Each item in the array represents
+ *   contents for the row in the table.
+ */
+const parseGroupsBucket = input => {
+  const output = [];
+
+  for (const group in input) {
+    output.push(input[group]);
+  }
+
+  return output;
+};
+
+/**
+ * Parse the generated bucket for parent anomalies into the schema for the parent-anomalies component
+ *
+ * @param {Object} input
+ *   The parent anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ *   The content to be passed into the parent-anomalies component. Each item in the array represents
+ *   contents for the row in the table.
+ */
+const parseEntitiesBucket = input => {
+  const output = [];
+
+  for (const entity in input) {
+    const { componentPath, title, data } = input[entity];
+
+    output.push({
+      componentPath,
+      title,
+      data: populateParentAnomaliesTable(data)
+    });
+  }
+
+  return output;
+};
+
+/**
+ * Add the anomaly referencing a metric to the metric bucket
+ *
+ * @param {Object} buckets
+ *   The reference to buckets object within which the anomaly needs to be classified
+ * @param {Object} anomaly
+ *   The metric anomaly that needs be classified added to the metric bucket
+ * @param {String} metric
+ *   The metric for which this anomaly was generated
+ */
+const setMetricsBucket = (buckets, anomaly, metric) => {
+  const {
+    METRICS: { KEY: metricKey, DEFAULT_TITLE, COMPONENT_PATH }
+  } = CLASSIFICATIONS;
+  const { [metricKey]: { data } = {} } = buckets;
+  const {
+    id,
+    startTime,
+    endTime,
+    feedback,
+    avgCurrentVal: current,
+    avgBaselineVal: predicted
+  } = anomaly;
+
+  const metricTableRow = {
+    id,
+    startTime,
+    endTime,
+    metric,
+    feedback,
+    current,
+    predicted
+  };
+
+  if (isEmpty(data)) {
+    const metricBucketObj = {
+      componentPath: COMPONENT_PATH,
+      title: DEFAULT_TITLE,
+      data: [metricTableRow]
+    };
+
+    set(buckets, `${metricKey}`, metricBucketObj);
+  } else {
+    data.push(metricTableRow);
+  }
+};
+
+/**
+ * Add the anomaly referencing a group constitient to the right group characterized by the subEntityName
+ *
+ * @param {Object} buckets
+ *   The reference to buckets object within which the anomaly needs to be classified
+ * @param {Object} anomaly
+ *   The anomaly produced due to the anomaly summarize grouper that needs be classified into the appropriate bucket
+ * @param {String} subEntityName
+ *   The entity name under which certain set of metrics would be grouped
+ * @param {String} groupName
+ *   The group constituent name. Each group constituent hosts anomalies from one metric.
+ */
+const setGroupsBucket = (buckets, anomaly, subEntityName, groupName) => {
+  const {
+    GROUPS: { KEY: groupKey, COMPONENT_PATH, DEFAULT_TITLE }
+  } = CLASSIFICATIONS;
+  const {
+    id,
+    startTime,
+    endTime,
+    feedback,
+    avgCurrentVal: current,
+    avgBaselineVal: predicted,
+    properties: { groupScore: criticality }
+  } = anomaly;
+  const groupTableRow = {
+    id,
+    groupName,
+    startTime,
+    endTime,
+    feedback,
+    criticality,
+    current,
+    predicted
+  };
+
+  if ([groupKey] in buckets) {
+    if (subEntityName in buckets[groupKey]) {
+      const {
+        [subEntityName]: { data }
+      } = buckets[groupKey];
+
+      data.push(groupTableRow);
+    } else {
+      set(buckets, `${groupKey}.${subEntityName}`, {
+        componentPath: COMPONENT_PATH,
+        title: `${DEFAULT_TITLE}${subEntityName}`,
+        data: [groupTableRow]
+      });
+    }
+  } else {
+    set(buckets, `${groupKey}`, {
+      [subEntityName]: {
+        componentPath: COMPONENT_PATH,
+        title: `${DEFAULT_TITLE}${subEntityName}`,
+        data: [groupTableRow]
+      }
+    });
+  }
+};
+
+/**
+ * Add the composite anomaly to the right bucket characterized by the subEntityName
+ *
+ * @param {Object} buckets
+ *   The reference to buckets object within which the anomaly needs to be classified
+ * @param {Object} anomaly
+ *   The composite anomaly that needs be classified into the appropriate bucket
+ * @param {String} subEntityName
+ *   The entity name under which this anomaly falls
+ */
+const setEntitiesBucket = (buckets, anomaly, subEntityName) => {
+  const {
+    ENTITIES: { KEY: entityKey, COMPONENT_PATH }
+  } = CLASSIFICATIONS;
+  let title;
+
+  if (isEmpty(subEntityName)) {
+    const {
+      ENTITIES: { DEFAULT_TITLE }
+    } = CLASSIFICATIONS;
+
+    title = DEFAULT_TITLE;
+  } else {
+    title = subEntityName;
+  }
+
+  if ([entityKey] in buckets) {
+    if (subEntityName in buckets[entityKey]) {
+      const {
+        [subEntityName]: { data }
+      } = buckets[entityKey];
+
+      data.push(anomaly);
+    } else {
+      set(buckets, `${entityKey}.${subEntityName}`, {
+        componentPath: COMPONENT_PATH,
+        title: title,
+        data: [anomaly]
+      });
+    }
+  } else {
+    set(buckets, `${entityKey}`, {
+      [subEntityName]: {
+        componentPath: COMPONENT_PATH,
+        title: title,
+        data: [anomaly]
+      }
+    });
+  }
+};
+
+/**
+ * Classify the child anomalies of particular anomaly into metrics, groups and parent-anomalies
+ *   -Anomalies of the yaml type METRIC_ALERT classify into "metrics"
+ *   -Anomalies of the yaml type METRIC_ALERT and grouper as ANOMALY_SUMMARIZE classify into "groups"
+ *   -Anomalies of the yaml type COMPOSITE_ALERT classify into "parent-anomalies"
+ *
+ * @param {Object} input
+ *   The subtree structure that needs to be parsed
+ *
+ * @return {Object}
+ *   The classification of children anomalies into the buckets of "metrics", "groups" and "entities".
+ *   The structure will take the form as below
+ *   {
+ *     metrics: {
+ *        componentPath: '',
+ *        title: '',
+ *        data:[{},{}] //anomaly entries
+ *      },
+ *     groups: {
+ *         groupEntity1: {
+ *            componentPath: '',
+ *            title:'',
+ *            data:[{},{}]  //each entry in array corresponds to information for 1 group constituent
+ *         },
+ *         groupEntity2: {
+ *         }
+ *      },
+ *     entities: {
+ *         entity1: {
+ *           componentPath: '',
+ *            title:'',
+ *            data:[{},{}]
+ *         },
+ *         entity2: {
+ *         }
+ *     }
+ *   }
+ */
+const generateBuckets = input => {
+  const buckets = {};
+  const { children } = input;
+
+  for (const child of children) {
+    const {
+      metric,
+      properties: { detectorComponentName = "", subEntityName, groupKey } = {}
+    } = child;
+
+    if (!isEmpty(metric)) {
+      setMetricsBucket(buckets, child, metric);
+    } else if (
+      isEmpty(metric) &&
+      detectorComponentName.includes("ANOMALY_SUMMARIZE")
+    ) {
+      setGroupsBucket(buckets, child, subEntityName, groupKey);
+    } else {
+      setEntitiesBucket(buckets, child, subEntityName);
+    }
+  }
+
+  return buckets;
+};
+
+/**
+ * Perform drilldown of anomaly grouped by anomaly summarize grouper. This involves generating the breadcrumb information
+ * and component details for the subtree for this anomaly.
+ *
+ * @param {Object} input
+ *   The subtree structure that needs to be parsed
+ *
+ * @return {Object}
+ *   The breadcrumb info and data for populating component comprising of group constituents
+ */
+const parseGroupAnomaly = input => {
+  const output = [];
+  const data = [];
+  const {
+    GROUPS: { DEFAULT_TITLE, COMPONENT_PATH }
+  } = CLASSIFICATIONS;
+  const {
+    id,
+    children,
+    properties: { subEntityName, groupKey }
+  } = input;
+  const breadcrumbInfo = {
+    title: `${subEntityName}/${groupKey}`,
+    id
+  };
+
+  for (const anomaly of children) {
+    const {
+      id,
+      startTime,
+      endTime,
+      metric,
+      dimensions,
+      avgCurrentVal: current,
+      avgBaselineVal: predicted,
+      feedback
+    } = anomaly;
+
+    data.push({
+      id,
+      startTime,
+      endTime,
+      feedback,
+      metric,
+      dimensions,
+      current,
+      predicted
+    });
+  }
+
+  output.push({
+    componentPath: COMPONENT_PATH,
+    title: DEFAULT_TITLE,
+    data
+  });
+
+  return { breadcrumbInfo, output };
+};
+
+/**
+ * Perform drilldown of composite anomaly. This involves generating the breadcrumb information
+ * and component details for the subtree for the composite anomaly
+ *
+ * @param {Object} input
+ *   The subtree structure that needs to be parsed
+ *
+ * @return {Object}
+ *   The breadcrumb info and data for populating child components from input subtree.
+ */
+const parseCompositeAnomaly = input => {
+  const output = [];
+  const buckets = generateBuckets(input);
+  const {
+    METRICS: { KEY: metricKey },
+    GROUPS: { KEY: groupKey }
+  } = CLASSIFICATIONS;
+  const { id, startTime } = input;
+  const breadcrumbInfo = {
+    id,
+    title: getFormattedBreadcrumbTime(startTime)
+  };
+
+  for (const key in buckets) {
+    const entry = buckets[key];
+
+    if (key === metricKey) {
+      output.push(...parseMetricsBucket(entry));
+    } else if (key === groupKey) {
+      output.push(...parseGroupsBucket(entry));
+    } else {
+      output.push(...parseEntitiesBucket(entry));
+    }
+  }
+
+  return { breadcrumbInfo, output };
+};
+
+/**
+ * Perform depth-first-search to retrieve anomaly in the tree
+ *
+ * @param {Number} id
+ *   The id of the anomaly to be searched
+ * @param {Object} input
+ *   The subtree structure comprising the anomaly
+ *
+ * @return {Object}
+ *   The anomaly referenced by id
+ */
+const findAnomaly = (id, input) => {
+  const { id: anomalyId, children } = input;
+
+  if (anomalyId === id) {
+    return input;
+  } else {

Review comment:
       Please keep the code concise. If an if block always executes a return statement, the subsequent else block is unnecessary. 
   Please check the Airbnb rule. 16.3 or no-else-return




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [incubator-pinot] tejasajmera commented on a change in pull request #6290: [TE]frontend - Build the tree parser for composite anomalies

Posted by GitBox <gi...@apache.org>.
tejasajmera commented on a change in pull request #6290:
URL: https://github.com/apache/incubator-pinot/pull/6290#discussion_r532978272



##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";
+
+/**
+ * Format the timestamp into the form to be shown in the breadcrumb
+ *
+ * @param {Number} timestamp
+ *   The timestamp of anomaly creation time in milliseconds
+ *
+ * @returns {String}
+ *   Formatted timestamp. Example of the required format - "Sep 15 16:49 EST"
+ */
+const getFormattedBreadcrumbTime = timestamp => {
+  const zoneName = moment.tz.guess();
+  const timeZoneAbbreviation = moment.tz(zoneName).zoneAbbr();
+
+  return `${moment(timestamp).format(
+    BREADCRUMB_TIME_DISPLAY_FORMAT
+  )} ${timeZoneAbbreviation}`;
+};
+
+/**
+ * Parse the anomalies generated by the composite alert to populate parent-anomalies table with relevent details about
+ * children for each anomaly.
+ *
+ * @param {Array<Object>} input
+ *   The anomalies for composite alert.
+ *
+ * @returns {Array<Object>}
+ *   Parsed out contents to populate parent-anomalies table
+ */
+const populateParentAnomaliesTable = input => {
+  const output = [];
+
+  for (const entry of input) {
+    const { id, startTime, endTime, feedback, children } = entry;
+    const entryOutput = {
+      id,
+      startTime,
+      endTime,
+      feedback
+    };
+
+    const details = {};
+    let item;
+    if (children.length > 0) {
+      for (const child of children) {
+        const { metric, properties: { subEntityName } = {} } = child;
+
+        if (!isEmpty(metric)) {

Review comment:
       Will do.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [incubator-pinot] zhangloo333 commented on a change in pull request #6290: [TE]frontend - Build the tree parser for composite anomalies

Posted by GitBox <gi...@apache.org>.
zhangloo333 commented on a change in pull request #6290:
URL: https://github.com/apache/incubator-pinot/pull/6290#discussion_r533016435



##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",

Review comment:
       I haven't realized my code hasn't merged yet. The command `npm run eslint-app` only can detect errors, but not fix them. You need to fix manually or set your vscode prettier with the rule of a single quote. 
   




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [incubator-pinot] tejasajmera commented on pull request #6290: [TE]frontend - Build the tree parser for composite anomalies

Posted by GitBox <gi...@apache.org>.
tejasajmera commented on pull request #6290:
URL: https://github.com/apache/incubator-pinot/pull/6290#issuecomment-738293160


   @zhangloo333 Can you please take a look again, your comments are addressed now.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [incubator-pinot] tejasajmera commented on a change in pull request #6290: [TE]frontend - Build the tree parser for composite anomalies

Posted by GitBox <gi...@apache.org>.
tejasajmera commented on a change in pull request #6290:
URL: https://github.com/apache/incubator-pinot/pull/6290#discussion_r532974984



##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",

Review comment:
       Can you tell me what is the exact command for it? Is it tied to your code changes in https://github.com/apache/incubator-pinot/pull/6245? If so, I may not be able to do as your changes are not merged yet.

##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";

Review comment:
       Sure.

##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";
+
+/**
+ * Format the timestamp into the form to be shown in the breadcrumb
+ *
+ * @param {Number} timestamp
+ *   The timestamp of anomaly creation time in milliseconds
+ *
+ * @returns {String}
+ *   Formatted timestamp. Example of the required format - "Sep 15 16:49 EST"
+ */
+const getFormattedBreadcrumbTime = timestamp => {
+  const zoneName = moment.tz.guess();
+  const timeZoneAbbreviation = moment.tz(zoneName).zoneAbbr();
+
+  return `${moment(timestamp).format(
+    BREADCRUMB_TIME_DISPLAY_FORMAT
+  )} ${timeZoneAbbreviation}`;
+};
+
+/**
+ * Parse the anomalies generated by the composite alert to populate parent-anomalies table with relevent details about
+ * children for each anomaly.
+ *
+ * @param {Array<Object>} input
+ *   The anomalies for composite alert.
+ *
+ * @returns {Array<Object>}
+ *   Parsed out contents to populate parent-anomalies table
+ */
+const populateParentAnomaliesTable = input => {

Review comment:
       Sure.

##########
File path: thirdeye/thirdeye-frontend/app/utils/anomalies-tree-parser.js
##########
@@ -0,0 +1,563 @@
+import { isEmpty } from "@ember/utils";
+import { set } from "@ember/object";
+import moment from "moment";
+
+const CLASSIFICATIONS = {
+  METRICS: {
+    KEY: "metrics",
+    COMPONENT_PATH: "entity-metrics",
+    DEFAULT_TITLE: "Metric Anomalies"
+  },
+  GROUPS: {
+    KEY: "groups",
+    COMPONENT_PATH: "entity-groups",
+    DEFAULT_TITLE: "ENTITY:"
+  },
+  ENTITIES: {
+    KEY: "entities",
+    COMPONENT_PATH: "parent-anomalies",
+    DEFAULT_TITLE: "Entity"
+  }
+};
+const BREADCRUMB_TIME_DISPLAY_FORMAT = "MMM D HH:mm";
+
+/**
+ * Format the timestamp into the form to be shown in the breadcrumb
+ *
+ * @param {Number} timestamp
+ *   The timestamp of anomaly creation time in milliseconds
+ *
+ * @returns {String}
+ *   Formatted timestamp. Example of the required format - "Sep 15 16:49 EST"
+ */
+const getFormattedBreadcrumbTime = timestamp => {
+  const zoneName = moment.tz.guess();
+  const timeZoneAbbreviation = moment.tz(zoneName).zoneAbbr();
+
+  return `${moment(timestamp).format(
+    BREADCRUMB_TIME_DISPLAY_FORMAT
+  )} ${timeZoneAbbreviation}`;
+};
+
+/**
+ * Parse the anomalies generated by the composite alert to populate parent-anomalies table with relevent details about
+ * children for each anomaly.
+ *
+ * @param {Array<Object>} input
+ *   The anomalies for composite alert.
+ *
+ * @returns {Array<Object>}
+ *   Parsed out contents to populate parent-anomalies table
+ */
+const populateParentAnomaliesTable = input => {
+  const output = [];
+
+  for (const entry of input) {
+    const { id, startTime, endTime, feedback, children } = entry;
+    const entryOutput = {
+      id,
+      startTime,
+      endTime,
+      feedback
+    };
+
+    const details = {};
+    let item;
+    if (children.length > 0) {
+      for (const child of children) {
+        const { metric, properties: { subEntityName } = {} } = child;
+
+        if (!isEmpty(metric)) {
+          item = metric;
+        } else {
+          item = subEntityName;
+        }
+
+        if (item in details) {
+          details[item]++;
+        } else {
+          details[item] = 1;
+        }
+      }
+      entryOutput.details = details;
+      output.push(entryOutput);
+    }
+  }
+
+  return output;
+};
+
+/**
+ * Parse the generated bucket for metric anomalies into the schema for the entity-metrics component
+ *
+ * @param {Object} input
+ *   The metric anomalies bucket constituents
+ *
+ * @returns {Array<Object>}
+ *   The content to be passed into the the leaf level entity-metrics component. Each item in the array represents
+ *   contents for the row in the table.
+ */
+const parseMetricsBucket = input => {

Review comment:
       Sure.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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