You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@skywalking.apache.org by ha...@apache.org on 2018/09/10 08:40:54 UTC

[incubator-skywalking-ui] 01/02: Refactor endpoint page with v6 protocol

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

hanahmily pushed a commit to branch 6.0.0/dev
in repository https://gitbox.apache.org/repos/asf/incubator-skywalking-ui.git

commit 2fab03fff22ec23f78994ca16ade1cf6c827d875
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Mon Sep 10 14:18:06 2018 +0800

    Refactor endpoint page with v6 protocol
---
 .roadhogrc.mock.js                                 |  10 +-
 mock/metadata.js                                   |  10 +-
 mock/metric.js                                     |   8 ++
 mock/topology.js                                   |  49 ++++++++
 src/common/menu.js                                 |   4 +-
 src/common/router.js                               |   4 +-
 src/models/{service.js => endpoint.js}             | 100 ++++++++--------
 .../{Service/Service.js => Endpoint/Endpoint.js}   | 128 ++++++++++-----------
 src/utils/time.js                                  |   5 +
 src/utils/utils.js                                 |   7 ++
 10 files changed, 199 insertions(+), 126 deletions(-)

diff --git a/.roadhogrc.mock.js b/.roadhogrc.mock.js
index 9fa1e25..1021e7f 100644
--- a/.roadhogrc.mock.js
+++ b/.roadhogrc.mock.js
@@ -1,7 +1,7 @@
 import mockjs from 'mockjs';
 import fs from 'fs';
 import { delay } from 'roadhog-api-doc';
-import { getTopology } from './mock/topology';
+import { getTopology, getServiceTopology } from './mock/topology';
 import { getAllApplication, getApplication } from './mock/application';
 import { searchServer, getServer } from './mock/server';
 import { searchService, getService } from './mock/service';
@@ -9,8 +9,8 @@ import { Alarms, getNoticeAlarm, AlarmTrend } from './mock/alarm';
 import { TraceBrief, Trace } from './mock/trace'
 import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
 import { graphql } from 'graphql';
-import { ClusterBrief, getAllServices } from './mock/metadata';
-import { Thermodynamic } from './mock/metric';
+import { ClusterBrief, getAllServices, searchEndpoint } from './mock/metadata';
+import { IntValues, Thermodynamic } from './mock/metric';
 import { getTopN } from './mock/aggregation';
 
 const noMock = process.env.NO_MOCK === 'true';
@@ -19,6 +19,8 @@ const resolvers = {
   Query: {
     getTopN,
     getAllServices,
+    getServiceTopology,
+    searchEndpoint,
   }
 }
 
@@ -30,6 +32,7 @@ const schema = makeExecutableSchema({ typeDefs: [
   fs.readFileSync('query-protocol/metric.graphqls', 'utf8'),
   fs.readFileSync('query-protocol/aggregation.graphqls', 'utf8'),
   fs.readFileSync('query-protocol/trace.graphqls', 'utf8'),
+  fs.readFileSync('query-protocol/topology.graphqls', 'utf8'),
 ], resolvers });
 
 addMockFunctionsToSchema({
@@ -42,6 +45,7 @@ addMockFunctionsToSchema({
     Alarms,
     TraceBrief,
     Trace,
+    IntValues,
   },
   preserveResolvers: true 
 });
diff --git a/mock/metadata.js b/mock/metadata.js
index 351f7f6..12fe2c3 100644
--- a/mock/metadata.js
+++ b/mock/metadata.js
@@ -28,8 +28,14 @@ export default {
   ,
   getAllServices: () => {
     const data = mockjs.mock({
-      'serviceId|20-50': [{ 'id|+1': 3, name: function() { return `app-${this.id}`; } }], // eslint-disable-line
+      'serviceId|20-50': [{ 'id|+1': 3, name: function() { return `service-${this.id}`; } }], // eslint-disable-line
     });
     return data.serviceId;
-  }
+  },
+  searchEndpoint: () => {
+    const data = mockjs.mock({
+      'endpointId|20-50': [{ 'id|+1': 3, name: function() { return `endpoint-${this.id}`; } }], // eslint-disable-line
+    });
+    return data.endpointId;
+  },
 };
diff --git a/mock/metric.js b/mock/metric.js
index 673cc63..410d006 100644
--- a/mock/metric.js
+++ b/mock/metric.js
@@ -30,4 +30,12 @@ export default {
     },
     responseTimeStep: 50,
   }),
+  IntValues: () => (mockjs.mock(
+    {
+      'values|60': [{
+        'id|+1': 1,
+        value: '@natural(0, 1000)',
+      }],
+    }
+  )),
 };
diff --git a/mock/topology.js b/mock/topology.js
index e0b816b..5480589 100644
--- a/mock/topology.js
+++ b/mock/topology.js
@@ -19,6 +19,55 @@
 import mockjs from 'mockjs';
 
 export default {
+  getServiceTopology: () => {
+    const upNodes = mockjs.mock({
+      'nodes|1-5': [
+        {
+          'id|+1': 100,
+          name: '@url',
+          'type|1': ['DUBBO', 'USER', 'SPRINGMVC'],
+        },
+      ],
+    });
+    const centerNodes = mockjs.mock({
+      nodes: [
+        {
+          'id|+1': 1,
+          name: '@url',
+          'type|1': ['DUBBO', 'tomcat', 'SPRINGMVC'],
+        },
+      ],
+    });
+    const downNodes = mockjs.mock({
+      'nodes|2-5': [
+        {
+          'id|+1': 200,
+          name: '@url',
+          'type|1': ['Oracle', 'MYSQL', 'REDIS'],
+        },
+      ],
+    });
+    downNodes.nodes.push({ id: -111 });
+    const nodes = upNodes.nodes.concat(centerNodes.nodes, downNodes.nodes);
+    const calls = upNodes.nodes.map(node => (mockjs.mock({
+      source: node.id,
+      target: 1,
+      'isAlert|1': true,
+      'callType|1': ['rpc', 'http', 'dubbo'],
+      'cpm|0-1000': 1,
+    }))).concat(downNodes.nodes.map(node => (mockjs.mock({
+      source: 1,
+      target: node.id,
+      'isAlert|1': true,
+      'callType|1': ['rpc', 'http', 'dubbo'],
+      'cpm|0-2000': 1,
+    }))));
+    calls.push({ source: '-175', target: 1, isAlert: false, callType: 'GRPC', cpm: 0, avgResponseTime: 52 });
+    return {
+      nodes,
+      calls,
+    };
+  },
   getTopology(req, res) {
     res.json(mockjs.mock(
       {
diff --git a/src/common/menu.js b/src/common/menu.js
index cbf1815..bc98328 100644
--- a/src/common/menu.js
+++ b/src/common/menu.js
@@ -34,8 +34,8 @@ const menuData = [{
       name: 'Application',
       path: 'application',
     }, {
-      name: 'Service',
-      path: 'service',
+      name: 'Endpoint',
+      path: 'endpoint',
     }, {
       name: 'Alarm',
       path: 'alarm',
diff --git a/src/common/router.js b/src/common/router.js
index 00e3934..db30edd 100644
--- a/src/common/router.js
+++ b/src/common/router.js
@@ -100,8 +100,8 @@ export const getRouterData = (app) => {
     '/monitor/application': {
       component: dynamicWrapper(app, ['application'], () => import('../routes/Application/Application')),
     },
-    '/monitor/service': {
-      component: dynamicWrapper(app, ['service'], () => import('../routes/Service/Service')),
+    '/monitor/endpoint': {
+      component: dynamicWrapper(app, ['endpoint'], () => import('../routes/Endpoint/Endpoint')),
     },
     '/trace': {
       component: dynamicWrapper(app, ['trace'], () => import('../routes/Trace/Trace')),
diff --git a/src/models/service.js b/src/models/endpoint.js
similarity index 64%
rename from src/models/service.js
rename to src/models/endpoint.js
index bd873ec..662546a 100644
--- a/src/models/service.js
+++ b/src/models/endpoint.js
@@ -16,12 +16,12 @@
  */
 
 
-import { generateModal, saveOptionsInState } from '../utils/models';
-import { query } from '../services/graphql';
+import { base, saveOptionsInState } from '../utils/models';
+import { exec } from '../services/graphql';
 
 const optionsQuery = `
-  query ApplicationOption($duration: Duration!) {
-    applicationId: getAllApplication(duration: $duration) {
+  query ServiceOption($duration: Duration!) {
+    serviceId: getAllServices(duration: $duration) {
       key: id
       label: name
     }
@@ -29,15 +29,30 @@ const optionsQuery = `
 `;
 
 const dataQuery = `
-  query Service($serviceId: ID!, $duration: Duration!, $traceCondition: TraceQueryCondition!) {
-    getServiceResponseTimeTrend(serviceId: $serviceId, duration: $duration) {
-      trendList
+  query Endpoint($endpointId: ID!, $duration: Duration!, $traceCondition: TraceQueryCondition!) {
+    getEndpointResponseTimeTrend: getLinearIntValues(metric: {
+      name: "endpointResponseTimeTrend"
+      id: $endpointId
+    }, duration: $duration) {
+      values {
+        value
+      }
     }
-    getServiceThroughputTrend(serviceId: $serviceId, duration: $duration) {
-      trendList
+    getEndpointThroughputTrend: getLinearIntValues(metric: {
+      name: "endpointResponseTimeTrend"
+      id: $endpointId
+    }, duration: $duration) {
+      values {
+        value
+      }
     }
-    getServiceSLATrend(serviceId: $serviceId, duration: $duration) {
-      trendList
+    getEndpointSLATrend: getLinearIntValues(metric: {
+      name: "endpointResponseTimeTrend"
+      id: $endpointId
+    }, duration: $duration) {
+      values {
+        value
+      }
     }
     queryBasicTraces(condition: $traceCondition) {
       traces {
@@ -50,26 +65,6 @@ const dataQuery = `
       }
       total
     }
-    getServiceTopology(serviceId: $serviceId, duration: $duration) {
-      nodes {
-        id
-        name
-        type
-        ... on ServiceNode {
-          sla
-          calls
-          numOfServiceAlarm
-        }
-      }
-      calls {
-        source
-        target
-        isAlert
-        callType
-        cpm
-        avgResponseTime
-      }
-    }
   }
 `;
 
@@ -111,19 +106,19 @@ const spanQuery = `query Spans($traceId: ID!) {
   }
 }`;
 
-export default generateModal({
-  namespace: 'service',
+export default base({
+  namespace: 'endpoint',
   state: {
-    getServiceResponseTimeTrend: {
-      trendList: [],
+    getEndpointResponseTimeTrend: {
+      values: [],
     },
-    getServiceThroughputTrend: {
-      trendList: [],
+    getEndpointThroughputTrend: {
+      values: [],
     },
-    getServiceSLATrend: {
-      trendList: [],
+    getEndpointSLATrend: {
+      values: [],
     },
-    getServiceTopology: {
+    getEndpointTopology: {
       nodes: [],
       calls: [],
     },
@@ -136,7 +131,7 @@ export default generateModal({
   optionsQuery,
   effects: {
     *fetchSpans({ payload }, { call, put }) {
-      const response = yield call(query, 'spans', { query: spanQuery, variables: payload.variables });
+      const response = yield call(exec, { query: spanQuery, variables: payload.variables });
       yield put({
         type: 'saveSpans',
         payload: response,
@@ -157,21 +152,21 @@ export default generateModal({
         },
       };
     },
-    saveAppInfo(preState, { payload: allOptions }) {
+    saveServiceInfo(preState, { payload: allOptions }) {
       const rawState = saveOptionsInState(null, preState, { payload: allOptions });
       const { data } = rawState;
-      if (data.appInfo) {
+      if (data.serviceInfo) {
         return rawState;
       }
       const { variables: { values } } = rawState;
-      if (!values.applicationId) {
+      if (!values.serviceId) {
         return rawState;
       }
       return {
         ...rawState,
         data: {
           ...data,
-          appInfo: { applicationId: values.applicationId },
+          serviceInfo: { serviceId: values.serviceId },
         },
       };
     },
@@ -189,26 +184,25 @@ export default generateModal({
   subscriptions: {
     setup({ history, dispatch }) {
       return history.listen(({ pathname, state }) => {
-        if (pathname === '/monitor/service' && state) {
-          console.info(state);
+        if (pathname === '/monitor/endpoint' && state) {
           dispatch({
             type: 'saveVariables',
             payload: {
               values: {
-                serviceId: state.key,
-                applicationId: state.applicationId,
+                endpointId: state.key,
+                serviceId: state.serviceId,
               },
               labels: {
-                serviceId: state.label,
-                applicationId: state.applicationName,
+                endpointId: state.label,
+                serviceId: state.serviceName,
               },
             },
           });
           dispatch({
             type: 'saveData',
             payload: {
-              appInfo: { applicationId: state.applicationId },
-              serviceInfo: { key: state.key, label: state.label },
+              serviceInfo: { serviceId: state.serviceId },
+              endpointInfo: { key: state.key, label: state.label },
             },
           });
         }
diff --git a/src/routes/Service/Service.js b/src/routes/Endpoint/Endpoint.js
similarity index 67%
rename from src/routes/Service/Service.js
rename to src/routes/Endpoint/Endpoint.js
index 62e467f..d6add80 100644
--- a/src/routes/Service/Service.js
+++ b/src/routes/Endpoint/Endpoint.js
@@ -21,9 +21,9 @@ import { connect } from 'dva';
 import { Row, Col, Form, Button, Icon, Select } from 'antd';
 import {
   ChartCard, MiniArea, MiniBar, Sankey,
-} from '../../components/Charts';
-import { axis } from '../../utils/time';
-import { avgTimeSeries } from '../../utils/utils';
+} from 'components/Charts';
+import { axisY } from '../../utils/time';
+import { avgTS } from '../../utils/utils';
 import { Panel, Search } from '../../components/Page';
 import TraceList from '../../components/Trace/TraceList';
 import TraceTimeline from '../Trace/TraceTimeline';
@@ -32,29 +32,29 @@ const { Item: FormItem } = Form;
 const { Option } = Select;
 
 @connect(state => ({
-  service: state.service,
+  endpoint: state.endpoint,
   duration: state.global.duration,
   globalVariables: state.global.globalVariables,
-  loading: state.loading.models.service,
+  loading: state.loading.models.endpoint,
 }))
 @Form.create({
   mapPropsToFields(props) {
-    const { variables: { values, labels } } = props.service;
+    const { variables: { values, labels } } = props.endpoint;
     return {
-      applicationId: Form.createFormField({
-        value: { key: values.applicationId ? values.applicationId : '', label: labels.applicationId ? labels.applicationId : '' },
-      }),
       serviceId: Form.createFormField({
         value: { key: values.serviceId ? values.serviceId : '', label: labels.serviceId ? labels.serviceId : '' },
       }),
+      endpointId: Form.createFormField({
+        value: { key: values.endpointId ? values.endpointId : '', label: labels.endpointId ? labels.endpointId : '' },
+      }),
     };
   },
 })
-export default class Service extends PureComponent {
+export default class Endpoint extends PureComponent {
   componentDidMount() {
     this.props.dispatch({
-      type: 'service/initOptions',
-      payload: { variables: this.props.globalVariables, reducer: 'saveAppInfo' },
+      type: 'endpoint/initOptions',
+      payload: { variables: this.props.globalVariables, reducer: 'saveServiceInfo' },
     });
   }
 
@@ -63,21 +63,21 @@ export default class Service extends PureComponent {
       return;
     }
     this.props.dispatch({
-      type: 'service/initOptions',
-      payload: { variables: nextProps.globalVariables, reducer: 'saveAppInfo' },
+      type: 'endpoint/initOptions',
+      payload: { variables: nextProps.globalVariables, reducer: 'saveServiceInfo' },
     });
   }
 
-  handleAppSelect = (selected) => {
+  handleServiceSelect = (selected) => {
     this.props.dispatch({
-      type: 'service/save',
+      type: 'endpoint/save',
       payload: {
         variables: {
-          values: { applicationId: selected.key, serviceId: null },
-          labels: { applicationId: selected.label, serviceId: null },
+          values: { serviceId: selected.key, endpointId: null },
+          labels: { serviceId: selected.label, endpointId: null },
         },
         data: {
-          appInfo: { applicationId: selected.key },
+          serviceInfo: { serviceId: selected.key },
         },
       },
     });
@@ -85,36 +85,36 @@ export default class Service extends PureComponent {
 
   handleSelect = (selected) => {
     this.props.dispatch({
-      type: 'service/save',
+      type: 'endpoint/save',
       payload: {
         variables: {
-          values: { serviceId: selected.key },
-          labels: { serviceId: selected.label },
+          values: { endpointId: selected.key },
+          labels: { endpointId: selected.label },
         },
         data: {
-          serviceInfo: selected,
+          endpointInfo: selected,
         },
       },
     });
   }
 
   handleChange = (variables) => {
-    const { variables: { values } } = this.props.service;
-    if (!values.applicationId) {
+    const { variables: { values } } = this.props.endpoint;
+    if (!values.serviceId) {
       return;
     }
-    const { key: serviceId, label: serviceName, duration } = variables;
-    if (!serviceId) {
+    const { key: endpointId, label: endpointName, duration } = variables;
+    if (!endpointId) {
       return;
     }
     this.props.dispatch({
-      type: 'service/fetchData',
+      type: 'endpoint/fetchData',
       payload: { variables: {
-        serviceId,
+        endpointId,
         duration,
         traceCondition: {
-          applicationId: values.applicationId,
-          operationName: serviceName,
+          endpointId: parseInt(values.endpointId, 10),
+          operationName: endpointName,
           queryDuration: duration,
           traceState: 'ALL',
           queryOrder: 'BY_DURATION',
@@ -131,30 +131,30 @@ export default class Service extends PureComponent {
   handleShowTrace = (traceId) => {
     const { dispatch } = this.props;
     dispatch({
-      type: 'service/fetchSpans',
+      type: 'endpoint/fetchSpans',
       payload: { variables: { traceId } },
     });
   }
 
   handleGoBack = () => {
     this.props.dispatch({
-      type: 'service/hideTimeline',
+      type: 'endpoint/hideTimeline',
     });
   }
 
-  edgeWith = edge => edge.cpm * edge.avgResponseTime;
+  edgeWith = edge => edge.cpm;
 
   renderPanel = () => {
-    const { service, duration } = this.props;
-    const { variables: { values }, data } = service;
-    const { getServiceResponseTimeTrend, getServiceThroughputTrend,
-      getServiceSLATrend, getServiceTopology, queryBasicTraces } = data;
-    if (!values.serviceId) {
+    const { endpoint, duration } = this.props;
+    const { variables: { values }, data } = endpoint;
+    const { getEndpointResponseTimeTrend, getEndpointThroughputTrend,
+      getEndpointSLATrend, getEndpointTopology, queryBasicTraces } = data;
+    if (!values.endpointId) {
       return null;
     }
     return (
       <Panel
-        variables={data.serviceInfo}
+        variables={data.endpointInfo}
         globalVariables={this.props.globalVariables}
         onChange={this.handleChange}
       >
@@ -162,35 +162,35 @@ export default class Service extends PureComponent {
           <Col xs={24} sm={24} md={24} lg={8} xl={8} style={{ marginTop: 8 }}>
             <ChartCard
               title="Avg Throughput"
-              total={`${avgTimeSeries(getServiceThroughputTrend.trendList)} cpm`}
+              total={`${avgTS(getEndpointThroughputTrend.values)} cpm`}
               contentHeight={46}
             >
               <MiniArea
                 color="#975FE4"
-                data={axis(duration, getServiceThroughputTrend.trendList)}
+                data={axisY(duration, getEndpointThroughputTrend.values)}
               />
             </ChartCard>
           </Col>
           <Col xs={24} sm={24} md={24} lg={8} xl={8} style={{ marginTop: 8 }}>
             <ChartCard
               title="Avg Response Time"
-              total={`${avgTimeSeries(getServiceResponseTimeTrend.trendList)} ms`}
+              total={`${avgTS(getEndpointResponseTimeTrend.values)} ms`}
               contentHeight={46}
             >
               <MiniArea
-                data={axis(duration, getServiceResponseTimeTrend.trendList)}
+                data={axisY(duration, getEndpointResponseTimeTrend.values)}
               />
             </ChartCard>
           </Col>
           <Col xs={24} sm={24} md={24} lg={8} xl={8} style={{ marginTop: 8 }}>
             <ChartCard
               title="Avg SLA"
-              total={`${(avgTimeSeries(getServiceSLATrend.trendList) / 100).toFixed(2)} %`}
+              total={`${(avgTS(getEndpointSLATrend.values) / 100).toFixed(2)} %`}
             >
               <MiniBar
                 animate={false}
                 height={46}
-                data={axis(duration, getServiceSLATrend.trendList,
+                data={axisY(duration, getEndpointSLATrend.values,
                   ({ x, y }) => ({ x, y: y / 100 }))}
               />
             </ChartCard>
@@ -209,7 +209,7 @@ export default class Service extends PureComponent {
             </ChartCard>
           </Col>
         </Row>
-        {this.renderSankey(getServiceTopology)}
+        {this.renderSankey(getEndpointTopology)}
       </Panel>
     );
   }
@@ -238,13 +238,13 @@ export default class Service extends PureComponent {
           >
             <Sankey
               data={nData}
-              edgeTooltip={['target*source*cpm*avgResponseTime*isAlert', (target, source, cpm, avgResponseTime) => {
+              edgeTooltip={['target*source*cpm', (target, source, cpm) => {
                 return {
                   name: `${source.name} to ${target.name} </span>`,
-                  value: `${cpm < 1 ? '<1' : cpm} cpm ${avgResponseTime}ms`,
+                  value: `${cpm} cpm`,
                 };
               }]}
-              edgeColor={['isAlert', isAlert => (isAlert ? '#DC143C' : '#bbb')]}
+              edgeColor="#bbb"
             />
           </ChartCard>
         </Col>
@@ -252,9 +252,9 @@ export default class Service extends PureComponent {
   }
 
   render() {
-    const { form, service } = this.props;
+    const { form, endpoint } = this.props;
     const { getFieldDecorator } = form;
-    const { variables: { options }, data } = service;
+    const { variables: { options }, data } = endpoint;
     const { showTimeline, queryTrace, currentTraceId } = data;
     return (
       <div>
@@ -271,31 +271,31 @@ export default class Service extends PureComponent {
           <Col span={showTimeline ? 0 : 24}>
             <Form layout="inline">
               <FormItem>
-                {getFieldDecorator('applicationId')(
+                {getFieldDecorator('serviceId')(
                   <Select
                     showSearch
                     optionFilterProp="children"
                     style={{ width: 200 }}
-                    placeholder="Select a application"
+                    placeholder="Select a service"
                     labelInValue
-                    onSelect={this.handleAppSelect.bind(this)}
+                    onSelect={this.handleServiceSelect.bind(this)}
                   >
-                    {options.applicationId && options.applicationId.map(app =>
-                      <Option key={app.key} value={app.key}>{app.label}</Option>)}
+                    {options.serviceId && options.serviceId.map(service =>
+                      <Option key={service.key} value={service.key}>{service.label}</Option>)}
                   </Select>
                 )}
               </FormItem>
-              {data.appInfo ? (
+              {data.serviceInfo ? (
                 <FormItem>
-                  {getFieldDecorator('serviceId')(
+                  {getFieldDecorator('endpointId')(
                     <Search
-                      placeholder="Search a service"
+                      placeholder="Search a endpoint"
                       onSelect={this.handleSelect.bind(this)}
-                      url="/service/search"
-                      variables={data.appInfo}
+                      url="/graphql"
+                      variables={data.serviceInfo}
                       query={`
-                        query SearchService($applicationId: ID!, $keyword: String!) {
-                          searchService(applicationId: $applicationId, keyword: $keyword, topN: 10) {
+                        query SearchEndpoint($serviceId: ID!, $keyword: String!) {
+                          searchEndpoint(serviceId: $serviceId, keyword: $keyword, limit: 10) {
                             key: id
                             label: name
                           }
diff --git a/src/utils/time.js b/src/utils/time.js
index e2476ca..b4e7301 100644
--- a/src/utils/time.js
+++ b/src/utils/time.js
@@ -22,6 +22,11 @@ export function axis({ display }, data, tranformFunc) {
     (tranformFunc ? tranformFunc({ x: v, y: data[i] }) : { x: v, y: data[i] }));
 }
 
+export function axisY({ display }, data, tranformFunc) {
+  return display.range.map((v, i) =>
+    (tranformFunc ? tranformFunc({ x: v, y: data[i] ? data[i].value : null }) : { x: v, y: data[i] ? data[i].value : null }));
+}
+
 export function generateDuration({ from, to }) {
   const start = from();
   const end = to();
diff --git a/src/utils/utils.js b/src/utils/utils.js
index 2969ebf..d6c4e3b 100644
--- a/src/utils/utils.js
+++ b/src/utils/utils.js
@@ -27,6 +27,13 @@ export function avgTimeSeries(list) {
       (acc, curr) => acc + curr) / filteredList.length).toFixed(2)) : 0;
 }
 
+export function avgTS(list) {
+  const filteredList = list.map(_ => _.value).filter(_ => _ > 0);
+  return filteredList.length > 0 ?
+    parseFloat((filteredList.reduce(
+      (acc, curr) => acc + curr) / filteredList.length).toFixed(2)) : 0;
+}
+
 export function getPlainNode(nodeList, parentPath = '') {
   const arr = [];
   nodeList.forEach((node) => {