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/01/29 07:25:49 UTC

[incubator-skywalking-ui] branch feature/5.0.0 updated (104c4e8 -> e78dc70)

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

hanahmily pushed a change to branch feature/5.0.0
in repository https://gitbox.apache.org/repos/asf/incubator-skywalking-ui.git.


    from 104c4e8  Add loading application list from backend
     new 4e4407d  Add topology graphql query
     new e78dc70  Refactor page

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/main/frontend/.roadhogrc.mock.js               |   3 +-
 src/main/frontend/mock/application.js              |  10 +-
 src/main/frontend/mock/topology.js                 |   5 +-
 .../frontend/src/components/Page/Panel/index.js    |  20 +++-
 .../src/components/Topology/AppTopology.js         |   8 +-
 src/main/frontend/src/models/application.js        |  56 ++---------
 src/main/frontend/src/models/global.js             |   8 +-
 src/main/frontend/src/models/topology.js           |  53 ++++++----
 .../frontend/src/routes/Application/Application.js | 110 +++++++++++----------
 src/main/frontend/src/routes/Topology/Topology.js  |  28 ++----
 src/main/frontend/src/utils/utils.js               | 103 ++++++++++++++++++-
 11 files changed, 249 insertions(+), 155 deletions(-)

-- 
To stop receiving notification emails like this one, please contact
hanahmily@apache.org.

[incubator-skywalking-ui] 01/02: Add topology graphql query

Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 4e4407d26f120e73c73af0df5a401e83ccb601f9
Author: hanahmily <ha...@gmail.com>
AuthorDate: Fri Jan 26 15:56:05 2018 +0800

    Add topology graphql query
---
 src/main/frontend/mock/topology.js                 |  5 +-
 .../src/components/Topology/AppTopology.js         |  8 ++--
 src/main/frontend/src/models/topology.js           | 53 +++++++++++++---------
 src/main/frontend/src/routes/Topology/Topology.js  | 28 ++++--------
 src/main/frontend/src/utils/utils.js               |  4 +-
 5 files changed, 51 insertions(+), 47 deletions(-)

diff --git a/src/main/frontend/mock/topology.js b/src/main/frontend/mock/topology.js
index e8b4733..2e56c10 100644
--- a/src/main/frontend/mock/topology.js
+++ b/src/main/frontend/mock/topology.js
@@ -12,13 +12,14 @@ export default {
                   'id|+1': 1,
                   name: '@name',
                   'type|1': ['DUBBO', 'tomcat', 'SPRINGMVC'],
-                  'calls|1000-2000': 1,
+                  'callsPerSec|1000-2000': 1,
                   'sla|1-100.1-2': 1,
                   'apdex|0.2': 1,
+                  'responseTimePerSec|500-1000': 1,
+                  'isAlarm|1': true,
                   'numOfServer|1-100': 1,
                   'numOfServerAlarm|1-100': 1,
                   'numOfServiceAlarm|1-100': 1,
-                  'isIncomingNode|1': true,
                 },
               ],
             });
diff --git a/src/main/frontend/src/components/Topology/AppTopology.js b/src/main/frontend/src/components/Topology/AppTopology.js
index 0c6f5ac..e22e12c 100644
--- a/src/main/frontend/src/components/Topology/AppTopology.js
+++ b/src/main/frontend/src/components/Topology/AppTopology.js
@@ -5,7 +5,7 @@ export default class AppTopology extends Base {
   getStyle = () => {
     return [
       {
-        selector: 'node[calls]',
+        selector: 'node[sla]',
         style: {
           width: 120,
           height: 120,
@@ -19,7 +19,7 @@ export default class AppTopology extends Base {
         },
       },
       {
-        selector: 'node[!calls]',
+        selector: 'node[!sla]',
         style: {
           width: 60,
           height: 60,
@@ -54,7 +54,7 @@ export default class AppTopology extends Base {
   getNodeLabel = () => {
     return [
       {
-        query: 'node[calls]',
+        query: 'node[sla]',
         halign: 'center',
         valign: 'center',
         halignBox: 'center',
@@ -64,7 +64,7 @@ export default class AppTopology extends Base {
           return `
           <div class="${styles.circle}">
             <div class="node-percentage">${data.sla}%</div>
-            <div>${data.calls} calls/s</div>
+            <div>${data.callsPerSec} calls/s</div>
             <div>
               <img src="data.png" class="${styles.logo}"/>${data.numOfServer}
               <img src="alert.png" class="${styles.logo}"/>
diff --git a/src/main/frontend/src/models/topology.js b/src/main/frontend/src/models/topology.js
index c9c00ab..4d43bc7 100644
--- a/src/main/frontend/src/models/topology.js
+++ b/src/main/frontend/src/models/topology.js
@@ -1,6 +1,6 @@
-import { query } from '../services/graphql';
+import { generateBaseModal } from '../utils/utils';
 
-export default {
+export default generateBaseModal({
   namespace: 'topology',
   state: {
     getClusterTopology: {
@@ -8,22 +8,33 @@ export default {
       calls: [],
     },
   },
-  effects: {
-    *fetch({ payload }, { call, put }) {
-      const response = yield call(query, 'topology', payload);
-      yield put({
-        type: 'save',
-        payload: response,
-      });
-    },
-  },
-
-  reducers: {
-    save(state, action) {
-      return {
-        ...state,
-        ...action.payload.data,
-      };
-    },
-  },
-};
+  query: `
+    query Topology($duration: Duration!) {
+      getClusterTopology(duration: $duration) {
+        nodes {
+          id
+          name
+          type
+          ... on ApplicationNode {
+            sla
+            callsPerSec
+            responseTimePerSec
+            apdex
+            isAlarm
+            numOfServer
+            numOfServerAlarm
+            numOfServiceAlarm
+          }
+        },
+        calls: {
+          source
+          target
+          isAlert
+          callType
+          callsPerSec
+          responseTimePerSec
+        },
+      }
+    }
+  `,
+});
diff --git a/src/main/frontend/src/routes/Topology/Topology.js b/src/main/frontend/src/routes/Topology/Topology.js
index e7297fe..0e6d986 100644
--- a/src/main/frontend/src/routes/Topology/Topology.js
+++ b/src/main/frontend/src/routes/Topology/Topology.js
@@ -1,43 +1,35 @@
-import React, { Component } from 'react';
+import React, { PureComponent } from 'react';
 import { connect } from 'dva';
 import { ChartCard } from '../../components/Charts';
 import { AppTopology } from '../../components/Topology';
+import { Panel } from '../../components/Page';
 
 @connect(state => ({
   topology: state.topology,
   duration: state.global.duration,
 }))
-export default class Topology extends Component {
-  state = {
+export default class Topology extends PureComponent {
+  static defaultProps = {
     graphHeight: 600,
-  }
-  componentDidMount() {
+  };
+  handleChange = (duration) => {
     this.props.dispatch({
       type: 'topology/fetch',
-      payload: {},
+      payload: { duration },
     });
   }
-  shouldComponentUpdate(nextProps) {
-    if (this.props.duration !== nextProps.duration) {
-      this.props.dispatch({
-        type: 'topology/fetch',
-        payload: {},
-      });
-    }
-    return this.props.topology !== nextProps.topology;
-  }
   render() {
     return (
-      <div ref={(el) => { this.graph = el; }}>
+      <Panel duration={this.props.duration} onDurationChange={this.handleChange}>
         <ChartCard
           title="Topolgy Graph"
         >
           <AppTopology
-            height={this.state.graphHeight}
+            height={this.props.graphHeight}
             elements={this.props.topology.getClusterTopology}
           />
         </ChartCard>
-      </div>
+      </Panel>
     );
   }
 }
diff --git a/src/main/frontend/src/utils/utils.js b/src/main/frontend/src/utils/utils.js
index 58ebee2..ee0bac3 100644
--- a/src/main/frontend/src/utils/utils.js
+++ b/src/main/frontend/src/utils/utils.js
@@ -9,8 +9,8 @@ function createTimeMeasure(measureType, step, format, displayFormat = format) {
 
 function getMeasureList() {
   return [createTimeMeasure('months', 'MONTH', 'YYYY-MM'), createTimeMeasure('days', 'DAY', 'YYYY-MM-DD'),
-    createTimeMeasure('hours', 'HOUR', ' YYYY-MM-DD HH', 'YYYY-MM-DD HH:00:00'), createTimeMeasure('minutes', 'MINUTE', ' YYYY-MM-DD HHmm', 'HH:mm:00'),
-    createTimeMeasure('seconds', 'SECOND', ' YYYY-MM-DD HHmmss', 'HH:mm:ss')];
+    createTimeMeasure('hours', 'HOUR', 'YYYY-MM-DD HH', 'YYYY-MM-DD HH:00:00'), createTimeMeasure('minutes', 'MINUTE', 'YYYY-MM-DD HHmm', 'HH:mm:00'),
+    createTimeMeasure('seconds', 'SECOND', 'YYYY-MM-DD HHmmss', 'HH:mm:ss')];
 }
 
 export function fixedZero(val) {

-- 
To stop receiving notification emails like this one, please contact
hanahmily@apache.org.

[incubator-skywalking-ui] 02/02: Refactor page

Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit e78dc70c7eb17d070ac3854562dc76f0090f916f
Author: hanahmily <ha...@gmail.com>
AuthorDate: Mon Jan 29 15:22:52 2018 +0800

    Refactor page
---
 src/main/frontend/.roadhogrc.mock.js               |   3 +-
 src/main/frontend/mock/application.js              |  10 +-
 .../frontend/src/components/Page/Panel/index.js    |  20 +++-
 src/main/frontend/src/models/application.js        |  56 ++---------
 src/main/frontend/src/models/global.js             |   8 +-
 .../frontend/src/routes/Application/Application.js | 110 +++++++++++----------
 src/main/frontend/src/utils/utils.js               |  99 +++++++++++++++++++
 7 files changed, 198 insertions(+), 108 deletions(-)

diff --git a/src/main/frontend/.roadhogrc.mock.js b/src/main/frontend/.roadhogrc.mock.js
index cf8d767..ac3c99b 100644
--- a/src/main/frontend/.roadhogrc.mock.js
+++ b/src/main/frontend/.roadhogrc.mock.js
@@ -5,7 +5,7 @@ import { getNotices } from './mock/notices';
 import { delay } from 'roadhog-api-doc';
 import { getDashboard } from './mock/dashboard';
 import { getTopology } from './mock/topology';
-import { getApplication } from './mock/application';
+import { getAllApplication, getApplication } from './mock/application';
 import { getServer } from './mock/server';
 import { getService } from './mock/service';
 import { getAlarm } from './mock/alarm';
@@ -19,6 +19,7 @@ const proxy = {
   // 支持值为 Object 和 Array
   'POST /api/dashboard': getDashboard,
   'POST /api/topology': getTopology,
+  'POST /api/application/all': getAllApplication,
   'POST /api/application': getApplication,
   'POST /api/server': getServer,
   'POST /api/service': getService,
diff --git a/src/main/frontend/mock/application.js b/src/main/frontend/mock/application.js
index 253d923..ac8369e 100644
--- a/src/main/frontend/mock/application.js
+++ b/src/main/frontend/mock/application.js
@@ -1,11 +1,19 @@
 import mockjs from 'mockjs';
 
 export default {
+  getAllApplication(req, res) {
+    res.json(mockjs.mock(
+      {
+        data: {
+          'applicationId|20-50': [{ 'key|+1': 3, label: function() { return `app-${this.key}`; } }],
+        },
+      }
+    ));
+  },
   getApplication(req, res) {
     res.json(mockjs.mock(
       {
         data: {
-          'getAllApplication|20-50': [{ 'key|+1': 3, name: function() { return `app-${this.key}`; } }],
           'getSlowService|10': [{ 'key|+1': 1, name: '@name', 'avgResponseTime|200-1000': 1 }],
           'getServerThroughput|10': [{ 'key|+1': 1, name: '@name', 'tps|100-10000': 1 }],
           getApplicationTopology: () => {
diff --git a/src/main/frontend/src/components/Page/Panel/index.js b/src/main/frontend/src/components/Page/Panel/index.js
index a3c630f..ba88752 100644
--- a/src/main/frontend/src/components/Page/Panel/index.js
+++ b/src/main/frontend/src/components/Page/Panel/index.js
@@ -2,17 +2,27 @@ import React, { Component } from 'react';
 
 export default class Panel extends Component {
   componentDidMount() {
-    const { duration, onDurationChange } = this.props;
-    onDurationChange(duration);
+    const { globalVariables, variables, onChange } = this.props;
+    if (!this.isRender(this.props)) {
+      return;
+    }
+    onChange({ ...globalVariables, ...variables });
   }
   shouldComponentUpdate(nextProps) {
-    const { duration, onDurationChange } = this.props;
-    if (duration !== nextProps.duration) {
-      onDurationChange(duration);
+    const { globalVariables, variables, onChange } = nextProps;
+    if (!this.isRender(nextProps)) {
+      return false;
+    }
+    if (globalVariables !== this.props.globalVariables || variables !== this.props.variables) {
+      onChange({ ...globalVariables, ...variables });
       return false;
     }
     return true;
   }
+  isRender = props => [props.variables, props.globalVariables]
+    .reduce((acc, curr) =>
+      (acc && (curr === undefined
+        || (curr !== undefined && Object.keys(curr).length > 0))), true);
   render() {
     const { children } = this.props;
     return children && (<div> {children} </div>);
diff --git a/src/main/frontend/src/models/application.js b/src/main/frontend/src/models/application.js
index 9faee98..85488d9 100644
--- a/src/main/frontend/src/models/application.js
+++ b/src/main/frontend/src/models/application.js
@@ -1,16 +1,15 @@
-import { generateBaseModal } from '../utils/utils';
-import { query as queryService } from '../services/graphql';
+import { generateModal } from '../utils/utils';
 
-const allAppQuery = `
-  query AllApplication($duration: Duration!) {
-    getAllApplication(duration: $duration) {
+const optionsQuery = `
+  query ApplicationOption($duration: Duration!) {
+    applicationId: getAllApplication(duration: $duration) {
       key: id
-      name
+      label: name
     }
   }
 `;
 
-const appQuery = `
+const dataQuery = `
   query Application($applicationId: ID!, $duration: Duration!) {
     getSlowService(applicationId: $applicationId, duration: $duration) {
       key: id
@@ -50,46 +49,8 @@ const appQuery = `
   }
 `;
 
-export default generateBaseModal({
+export default generateModal({
   namespace: 'application',
-  effects: {
-    *loadAllApp({ payload }, { call, put }) {
-      const { data: { getAllApplication: allApplication } } = yield call(queryService, 'application', { variables: payload, query: allAppQuery });
-      const applicationId = allApplication && allApplication.length > 0 && allApplication[0].key;
-      if (!applicationId) {
-        return;
-      }
-      yield put({
-        type: 'saveApplication',
-        payload: { allApplication, applicationId },
-      });
-      const response = yield put({
-        type: 'fetch',
-        payload: { variables: { ...payload, applicationId }, query: appQuery },
-      });
-      yield put({
-        type: 'save',
-        payload: response,
-      });
-    },
-  },
-  reducers: {
-    saveApplication(preState, action) {
-      const { applicationId } = preState;
-      const { allApplication, applicationId: newApplicationId } = action.payload;
-      if (allApplication.find(_ => _.key === applicationId)) {
-        return {
-          ...preState,
-          allApplication,
-        };
-      }
-      return {
-        ...preState,
-        allApplication,
-        applicationId: newApplicationId,
-      };
-    },
-  },
   state: {
     allApplication: [],
     getSlowService: [],
@@ -99,5 +60,6 @@ export default generateBaseModal({
       calls: [],
     },
   },
-  query: appQuery,
+  optionsQuery,
+  dataQuery,
 });
diff --git a/src/main/frontend/src/models/global.js b/src/main/frontend/src/models/global.js
index 311ff7c..831e1a1 100644
--- a/src/main/frontend/src/models/global.js
+++ b/src/main/frontend/src/models/global.js
@@ -64,11 +64,13 @@ export default {
       };
     },
     changeSelectedTime(state, { payload }) {
+      const duration = generateDuration(payload);
       return {
         ...state,
         selectedTime: payload,
-        duration: generateDuration(payload),
+        duration,
         isShowSelectTime: false,
+        globalVariables: { duration: duration.input },
       };
     },
     toggleSelectTime(state) {
@@ -79,9 +81,11 @@ export default {
     },
     reload(state) {
       const { selectedTime } = state;
+      const duration = generateDuration(selectedTime);
       return {
         ...state,
-        duration: generateDuration(selectedTime),
+        duration,
+        globalVariables: { duration: duration.input },
       };
     },
   },
diff --git a/src/main/frontend/src/routes/Application/Application.js b/src/main/frontend/src/routes/Application/Application.js
index 1b047af..ceff4e7 100644
--- a/src/main/frontend/src/routes/Application/Application.js
+++ b/src/main/frontend/src/routes/Application/Application.js
@@ -7,72 +7,74 @@ import { Panel } from '../../components/Page';
 const { Option } = Select;
 const { Item: FormItem } = Form;
 
+const tableColumns = [{
+  title: 'Name',
+  dataIndex: 'name',
+  key: 'name',
+}, {
+  title: 'Duration',
+  dataIndex: 'avgResponseTime',
+  key: 'avgResponseTime',
+}];
+
+const applicationThroughputColumns = [{
+  title: 'Name',
+  dataIndex: 'name',
+  key: 'name',
+}, {
+  title: 'Tps',
+  dataIndex: 'tps',
+  key: 'tps',
+}];
+
+const middleColResponsiveProps = {
+  xs: 24,
+  sm: 24,
+  md: 12,
+  lg: 12,
+  xl: 12,
+  style: { marginBottom: 24, marginTop: 24 },
+};
+
 @connect(state => ({
   application: state.application,
-  duration: state.global.duration,
+  globalVariables: state.global.globalVariables,
 }))
 @Form.create({
   mapPropsToFields(props) {
+    const { variables: { values, labels } } = props.application;
     return {
       applicationId: Form.createFormField({
-        value: props.application.applicationId,
+        value: { key: values.applicationId, label: labels.applicationId },
       }),
     };
   },
 })
 export default class Application extends PureComponent {
-  handleDurationChange = (duration) => {
+  componentDidMount() {
     this.props.dispatch({
-      type: 'application/loadAllApp',
-      payload: { duration },
+      type: 'application/initOptions',
+      payload: { variables: this.props.globalVariables },
     });
   }
-  handleChange = (applicationId) => {
+  handleSelect = (selected) => {
     this.props.dispatch({
-      type: 'application/fetchItem',
+      type: 'application/saveVariables',
       payload: {
-        variables:
-        {
-          applicationId,
-          duration: this.props.duration,
-        },
-        data:
-        {
-          applicationId,
-        },
+        values: { applicationId: selected.key },
+        labels: { applicationId: selected.label },
       },
     });
   }
+  handleChange = (variables) => {
+    this.props.dispatch({
+      type: 'application/fetchData',
+      payload: { variables },
+    });
+  }
   render() {
     const { getFieldDecorator } = this.props.form;
-    const tableColumns = [{
-      title: 'Name',
-      dataIndex: 'name',
-      key: 'name',
-    }, {
-      title: 'Duration',
-      dataIndex: 'avgResponseTime',
-      key: 'avgResponseTime',
-    }];
-
-    const applicationThroughputColumns = [{
-      title: 'Name',
-      dataIndex: 'name',
-      key: 'name',
-    }, {
-      title: 'Tps',
-      dataIndex: 'tps',
-      key: 'tps',
-    }];
-
-    const middleColResponsiveProps = {
-      xs: 24,
-      sm: 24,
-      md: 12,
-      lg: 12,
-      xl: 12,
-      style: { marginBottom: 24, marginTop: 24 },
-    };
+    const { variables: { values, options }, data } = this.props.application;
     return (
       <div>
         <Form layout="inline">
@@ -82,22 +84,26 @@ export default class Application extends PureComponent {
                 showSearch
                 style={{ width: 200 }}
                 placeholder="Select a application"
-                optionFilterProp="children"
-                onSelect={this.handleChange.bind(this)}
+                labelInValue
+                onSelect={this.handleSelect.bind(this)}
               >
-                {this.props.application.allApplication.map((app) => {
-                    return (<Option value={app.key}>{app.name}</Option>);
+                {options.applicationId && options.applicationId.map((app) => {
+                    return (<Option value={app.key}>{app.label}</Option>);
                   })}
               </Select>
             )}
           </FormItem>
         </Form>
-        <Panel duration={this.props.duration} onDurationChange={this.handleDurationChange}>
+        <Panel
+          variables={values}
+          globalVariables={this.props.globalVariables}
+          onChange={this.handleChange}
+        >
           <Card
             bordered={false}
             bodyStyle={{ padding: 0, marginTop: 24 }}
           >
-            <AppTopology elements={this.props.application.getApplicationTopology} layout={{ name: 'concentric', minNodeSpacing: 200 }} />
+            <AppTopology elements={data.getApplicationTopology} layout={{ name: 'concentric', minNodeSpacing: 200 }} />
           </Card>
           <Row gutter={24}>
             <Col {...middleColResponsiveProps}>
@@ -109,7 +115,7 @@ export default class Application extends PureComponent {
                 <Table
                   size="small"
                   columns={tableColumns}
-                  dataSource={this.props.application.getSlowService}
+                  dataSource={data.getSlowService}
                   pagination={{
                     style: { marginBottom: 0 },
                     pageSize: 10,
@@ -126,7 +132,7 @@ export default class Application extends PureComponent {
                 <Table
                   size="small"
                   columns={applicationThroughputColumns}
-                  dataSource={this.props.application.getServerThroughput}
+                  dataSource={data.getServerThroughput}
                   pagination={{
                     style: { marginBottom: 0 },
                     pageSize: 10,
diff --git a/src/main/frontend/src/utils/utils.js b/src/main/frontend/src/utils/utils.js
index ee0bac3..43a024a 100644
--- a/src/main/frontend/src/utils/utils.js
+++ b/src/main/frontend/src/utils/utils.js
@@ -161,3 +161,102 @@ export function generateBaseModal({ namespace, query, state, effects = {}, reduc
     },
   };
 }
+
+export function generateModal({ namespace, dataQuery, optionsQuery, state = {} }) {
+  return {
+    namespace,
+    state: {
+      variables: {
+        values: {},
+        labels: {},
+        options: {},
+      },
+      data: state,
+    },
+    effects: {
+      *initOptions({ payload }, { call, put }) {
+        const { variables } = payload;
+        const response = yield call(queryService, `${namespace}/all`, { variables, query: optionsQuery });
+        yield put({
+          type: 'saveOptions',
+          payload: response.data,
+        });
+      },
+      *fetchData({ payload }, { call, put }) {
+        const { variables } = payload;
+        const response = yield call(queryService, namespace, { variables, query: dataQuery });
+        yield put({
+          type: 'saveData',
+          payload: response.data,
+        });
+      },
+    },
+    reducers: {
+      saveOptions(preState, { payload: allOptions }) {
+        const { variables } = preState;
+        const { values, labels, options } = variables;
+        const amendOptions = {};
+        const defaultValues = {};
+        const defaultLabels = {};
+        Object.keys(allOptions).forEach((_) => {
+          const thisOptions = allOptions[_];
+          if (!values[_] && thisOptions.length > 0) {
+            defaultValues[_] = thisOptions[0].key;
+            defaultLabels[_] = thisOptions[0].label;
+          }
+          const key = values[_];
+          if (!thisOptions.find(o => o.key === key)) {
+            amendOptions[_] = [...thisOptions, { key, label: labels[_] }];
+          }
+        });
+        return {
+          ...preState,
+          variables: {
+            ...variables,
+            options: {
+              ...options,
+              ...allOptions,
+              ...amendOptions,
+            },
+            values: {
+              ...values,
+              ...defaultValues,
+            },
+            labels: {
+              ...labels,
+              ...defaultLabels,
+            },
+          },
+        };
+      },
+      saveData(preState, { payload }) {
+        const { data } = preState;
+        return {
+          ...preState,
+          data: {
+            ...data,
+            ...payload,
+          },
+        };
+      },
+      saveVariables(preState, { payload: { values: variableValues, labels = {} } }) {
+        const { variables: preVariables } = preState;
+        const { values: preValues, lables: preLabels } = preVariables;
+        return {
+          ...preState,
+          variables: {
+            ...preVariables,
+            values: {
+              ...preValues,
+              ...variableValues,
+            },
+            labels: {
+              ...preLabels,
+              ...labels,
+            },
+          },
+        };
+      },
+    },
+  };
+}

-- 
To stop receiving notification emails like this one, please contact
hanahmily@apache.org.