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

wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-rocketbot-ui.git


The following commit(s) were added to refs/heads/master by this push:
     new 319ce5f  Feat:Import and export the configuration file for the 
dashboard layout (#288)
319ce5f is described below

commit 319ce5f525ce5b986e4ebe6f5607e0c1dc625636
Author: Juntao Zhang <[email protected]>
AuthorDate: Wed Jun 3 18:10:19 2020 +0800

    Feat:Import and export the configuration file for the dashboard layout 
(#288)
---
 src/assets/styles/lib.scss                         |   3 +
 .../modules/dashboard/dashboard-data-layout.ts     |  14 +++
 .../modules/dashboard/dashboard-data-query.ts      |   5 +-
 src/store/modules/dashboard/mutation-types.ts      |   2 +
 src/utils/readFile.ts                              |  33 +++++++
 src/utils/saveFile.ts                              |  28 ++++++
 src/views/components/dashboard/dashboard-item.vue  |   8 +-
 src/views/components/dashboard/tool-bar-btns.vue   | 109 +++++++++++++++++++++
 src/views/components/dashboard/tool-bar.vue        |  57 ++---------
 src/views/components/dashboard/tool-group.vue      |   3 +
 src/views/components/dashboard/tool-nav.vue        |  52 +++++++++-
 src/views/constant.ts                              |  26 +++++
 src/views/containers/dashboard.vue                 |   2 +
 .../topology/endpoint/endpoints-survey.vue         |   2 +
 src/views/containers/topology/endpoint/index.vue   |  57 ++++++++++-
 src/views/containers/topology/instance/index.vue   |  57 ++++++++++-
 .../topology/instance/instances-survey.vue         |   2 +
 src/views/containers/topology/topology.vue         |  45 ++++++++-
 18 files changed, 448 insertions(+), 57 deletions(-)

diff --git a/src/assets/styles/lib.scss b/src/assets/styles/lib.scss
index 8841ccd..b2e7297 100644
--- a/src/assets/styles/lib.scss
+++ b/src/assets/styles/lib.scss
@@ -127,6 +127,9 @@
 .ml-5 {
   margin-left: 5px;
 }
+.ml-10 {
+  margin-left: 10px;
+}
 .mr-0 {
   margin-right: 0px;
 }
diff --git a/src/store/modules/dashboard/dashboard-data-layout.ts 
b/src/store/modules/dashboard/dashboard-data-layout.ts
index 1d83948..0b44edb 100644
--- a/src/store/modules/dashboard/dashboard-data-layout.ts
+++ b/src/store/modules/dashboard/dashboard-data-layout.ts
@@ -65,6 +65,10 @@ const mutations: MutationTree<State> = {
   [types.SET_COMPS_TREE](state: State, data: CompsTree[]) {
     state.tree = data;
   },
+  [types.IMPORT_TREE](state: State, data: CompsTree[]) {
+    state.tree.push(...data);
+    window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+  },
   [types.SET_GROUP_QUERY](state: State, params: any) {
     state.tree[state.group].query = params;
   },
@@ -137,12 +141,22 @@ const mutations: MutationTree<State> = {
     state.tree[state.group].children.push({ name: params.name, children: [] });
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
   },
+  [types.IMPORT_COMPS_TREE](state: State, params: any) {
+    state.tree[state.group].children.push(params);
+    window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+  },
   [types.DELETE_COMPS_GROUP](state: State, index: number) {
     state.tree.splice(index, 1);
+    if (!state.tree[state.group]) {
+      state.group--;
+    }
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
   },
   [types.DELETE_COMPS_TREE](state: State, index: number) {
     state.tree[state.group].children.splice(index, 1);
+    if (!state.tree[state.group].children[state.current]) {
+      state.current--;
+    }
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
   },
   [types.ADD_COMP](state: State) {
diff --git a/src/store/modules/dashboard/dashboard-data-query.ts 
b/src/store/modules/dashboard/dashboard-data-query.ts
index 45d30d0..1e7b013 100644
--- a/src/store/modules/dashboard/dashboard-data-query.ts
+++ b/src/store/modules/dashboard/dashboard-data-query.ts
@@ -28,14 +28,17 @@ const actions: ActionTree<State, any> = {
     params: {
       index: number;
       duration: any;
+      itemConfig: any;
     },
   ) {
     const { currentDatabase, currentEndpoint, currentInstance, currentService 
} = context.rootState.rocketOption;
     const dashboard: string = `${window.localStorage.getItem('dashboard')}`;
     const tree = JSON.parse(dashboard) || context.state.tree;
     const normal = tree[context.state.group].type === 'database' ? false : 
true;
-    const config = 
tree[context.state.group].children[context.state.current].children[params.index];
+    const config =
+      params.itemConfig || 
tree[context.state.group].children[context.state.current].children[params.index];
     const names = ['readSampledRecords', 'sortMetrics'];
+
     if (!config) {
       return;
     }
diff --git a/src/store/modules/dashboard/mutation-types.ts 
b/src/store/modules/dashboard/mutation-types.ts
index 2668093..f5aaa35 100644
--- a/src/store/modules/dashboard/mutation-types.ts
+++ b/src/store/modules/dashboard/mutation-types.ts
@@ -41,6 +41,8 @@ export const SET_CURRENT_GROUP = 'SET_CURRENT_GROUP';
 export const SET_CURRENT_GROUP_WITH_CURRENT = 'SET_CURRENT_GROUP_WITH_CURRENT';
 export const SET_CURRENT_COMPS = 'SET_CURRENT_COMPS';
 export const ADD_COMPS_GROUP = 'ADD_COMPS_GROUP';
+export const IMPORT_TREE = 'IMPORT_TREE';
+export const IMPORT_COMPS_TREE = 'IMPORT_COMPS_TREE';
 export const ADD_COMPS_TREE = 'ADD_COMPS_TREE';
 export const DELETE_COMPS_GROUP = 'DELETE_COMPS_GROUP';
 export const DELETE_COMPS_TREE = 'DELETE_COMPS_TREE';
diff --git a/src/utils/readFile.ts b/src/utils/readFile.ts
new file mode 100644
index 0000000..07667d6
--- /dev/null
+++ b/src/utils/readFile.ts
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const readFile = (event: any) => {
+  return new Promise((resolve) => {
+    const { files } = event.target;
+    if (files.length < 1) {
+      return;
+    }
+    const file = files[0];
+    const reader: FileReader = new FileReader();
+    reader.readAsText(file);
+    reader.onload = function() {
+      if (typeof this.result === 'string') {
+        resolve(JSON.parse(this.result));
+      }
+    };
+  });
+};
diff --git a/src/utils/saveFile.ts b/src/utils/saveFile.ts
new file mode 100644
index 0000000..79883a6
--- /dev/null
+++ b/src/utils/saveFile.ts
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const saveFile = (data: any, name: string) => {
+  const newData = JSON.stringify(data);
+  const tagA = document.createElement('a');
+  tagA.download = name;
+  tagA.style.display = 'none';
+  const blob = new Blob([newData]);
+  tagA.href = URL.createObjectURL(blob);
+  document.body.appendChild(tagA);
+  tagA.click();
+  document.body.removeChild(tagA);
+};
diff --git a/src/views/components/dashboard/dashboard-item.vue 
b/src/views/components/dashboard/dashboard-item.vue
index 78061b0..0f10c62 100644
--- a/src/views/components/dashboard/dashboard-item.vue
+++ b/src/views/components/dashboard/dashboard-item.vue
@@ -64,6 +64,7 @@ limitations under the License. -->
   import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
   import charts from './charts';
   import { QueryTypes } from './constant';
+  import { TopologyType, ObjectsType } from '../../constant';
   import { MetricsType, CalculationType } from './charts/constant';
   import { uuid } from '@/utils/uuid.ts';
 
@@ -84,6 +85,7 @@ limitations under the License. -->
     @Prop() private item!: any;
     @Prop() private index!: number;
     @Prop() private type!: string;
+    @Prop() private updateObjects!: string;
 
     private pageTypes = ['TOPOLOGY_ENDPOINT', 'TOPOLOGY_INSTANCE'];
     private dialogConfigVisible = false;
@@ -102,8 +104,9 @@ limitations under the License. -->
       this.height = this.item.height;
       this.unit = this.item.unit;
       this.itemConfig = this.item;
+      const types = [ObjectsType.UPDATE_INSTANCES, 
ObjectsType.UPDATE_ENDPOINTS] as any[];
 
-      if (this.pageTypes.includes(this.type)) {
+      if (this.updateObjects && !types.includes(this.updateObjects)) {
         return;
       }
       this.chartRender();
@@ -113,9 +116,12 @@ limitations under the License. -->
       if (this.rocketGlobal.edit) {
         return;
       }
+      const pageTypes = [TopologyType.TOPOLOGY_ENDPOINT, 
TopologyType.TOPOLOGY_INSTANCE] as any[];
+
       this.GET_QUERY({
         duration: this.durationTime,
         index: this.index,
+        itemConfig: pageTypes.includes(this.type) ? this.itemConfig : 
undefined,
       }).then((params: Array<{ metricName: string; [key: string]: any; config: 
any }>) => {
         if (!params) {
           return;
diff --git a/src/views/components/dashboard/tool-bar-btns.vue 
b/src/views/components/dashboard/tool-bar-btns.vue
new file mode 100644
index 0000000..4543a40
--- /dev/null
+++ b/src/views/components/dashboard/tool-bar-btns.vue
@@ -0,0 +1,109 @@
+<template>
+  <div class="flex-h btn-box">
+    <div class="rk-dashboard-bar-btn">
+      <span v-tooltip:bottom="{ content: rocketGlobal.edit ? 'view' : 'edit' 
}">
+        <svg
+          class="icon lg vm cp rk-btn ghost"
+          :style="`color:${!rocketGlobal.edit ? '' : '#ffc107'}`"
+          @click="handleSetEdit"
+        >
+          <use :xlink:href="!rocketGlobal.edit ? '#lock' : '#lock-open'"></use>
+        </svg>
+      </span>
+    </div>
+    <div class="rk-dashboard-bar-btn">
+      <span v-tooltip:bottom="{ content: 'import' }">
+        <input id="tool-bar-file" type="file" name="file" title="" 
accept=".json" @change="importData" />
+        <label class="rk-btn ghost input-label" for="tool-bar-file">
+          <svg class="icon lg vm cp " :style="`marginTop: 0px`">
+            <use :xlink:href="'#folder_open'"></use>
+          </svg>
+        </label>
+      </span>
+    </div>
+    <div class="rk-dashboard-bar-btn">
+      <span v-tooltip:bottom="{ content: 'export' }">
+        <svg class="icon lg vm cp rk-btn ghost" @click="exportData">
+          <use :xlink:href="'#save_alt'"></use>
+        </svg>
+      </span>
+    </div>
+
+    <div class="rk-dashboard-bar-btn">
+      <svg class="icon lg vm cp rk-btn ghost" @click="handleOption">
+        <use xlink:href="#retry"></use>
+      </svg>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { Vue, Component, Prop } from 'vue-property-decorator';
+  import { Action, Mutation } from 'vuex-class';
+  import { readFile } from '@/utils/readFile';
+  import { saveFile } from '@/utils/saveFile';
+  @Component({})
+  export default class ToolBarBtns extends Vue {
+    @Prop() private compType!: any;
+    @Prop() private rocketGlobal!: any;
+    @Prop() private rocketComps!: any;
+    @Prop() private durationTime!: any;
+    @Prop() private rocketOption: any;
+    @Mutation('SET_COMPS_TREE') private SET_COMPS_TREE: any;
+    @Mutation('IMPORT_TREE') private IMPORT_TREE: any;
+    @Action('SET_EDIT') private SET_EDIT: any;
+    @Action('MIXHANDLE_GET_OPTION') private MIXHANDLE_GET_OPTION: any;
+
+    private handleOption() {
+      return this.MIXHANDLE_GET_OPTION({
+        compType: this.compType,
+        duration: this.durationTime,
+        keywordServiceName: this.rocketOption.keywordService,
+      });
+    }
+    private handleSetEdit() {
+      this.SET_EDIT(!this.rocketGlobal.edit);
+    }
+    private async importData(event: any) {
+      try {
+        const data: any = await readFile(event);
+        if (!Array.isArray(data)) {
+          throw new Error();
+        }
+        const { children, name, type } = data[0];
+        if (children && name && type) {
+          this.IMPORT_TREE(data);
+        } else {
+          throw new Error('error');
+        }
+        const el: any = document.getElementById('tool-bar-file');
+        el!.value = '';
+      } catch (e) {
+        this.$modal.show('dialog', { text: 'ERROR' });
+      }
+    }
+    private exportData() {
+      const data = this.rocketComps.tree;
+      const name = 'dashboard.json';
+      saveFile(data, name);
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .rk-dashboard-bar-btn {
+    padding: 0 5px;
+    border-right: 2px solid #252a2f;
+    height: 19px;
+  }
+  #tool-bar-file {
+    display: none;
+  }
+  .input-label {
+    display: inline;
+    line-height: inherit;
+  }
+  .btn-box {
+    height: 58px;
+  }
+</style>
diff --git a/src/views/components/dashboard/tool-bar.vue 
b/src/views/components/dashboard/tool-bar.vue
index 026908d..39234bf 100644
--- a/src/views/components/dashboard/tool-bar.vue
+++ b/src/views/components/dashboard/tool-bar.vue
@@ -13,24 +13,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.
 See the License for the specific language governing permissions and
 limitations under the License. -->
 <template>
-  <div>
+  <div class="rk-dashboard-bar flex-h">
+    <ToolBarBtns
+      :rocketGlobal="rocketGlobal"
+      :rocketComps="rocketComps"
+      :compType="compType"
+      :durationTime="durationTime"
+      :rocketOption="rocketOption"
+    ></ToolBarBtns>
     <div class="rk-dashboard-bar flex-h" v-if="compType !== 
dashboardType.DATABASE">
-      <div class="rk-dashboard-bar-reload">
-        <span v-tooltip:bottom="{ content: rocketGlobal.edit ? 'view' : 'edit' 
}">
-          <svg
-            class="icon lg vm cp rk-btn ghost"
-            :style="`color:${!rocketGlobal.edit ? '' : '#ffc107'}`"
-            @click="handleSetEdit"
-          >
-            <use :xlink:href="!rocketGlobal.edit ? '#lock' : 
'#lock-open'"></use>
-          </svg>
-        </span>
-      </div>
-      <div class="rk-dashboard-bar-reload">
-        <svg class="icon lg vm cp rk-btn ghost" @click="handleOption">
-          <use xlink:href="#retry"></use>
-        </svg>
-      </div>
       <div class="sm grey service-search" v-if="compType === 
dashboardType.SERVICE">
         <div>{{ this.$t('serviceFilter') }}</div>
         <input type="text" :value="rocketOption.keywordService" 
@change="searchServices($event.target.value)" />
@@ -61,20 +52,6 @@ limitations under the License. -->
       />
     </div>
     <div class="rk-dashboard-bar flex-h" v-else>
-      <div class="rk-dashboard-bar-reload">
-        <svg
-          class="icon lg vm cp rk-btn ghost"
-          :style="`color:${!rocketGlobal.edit ? '' : '#ffc107'}`"
-          @click="handleSetEdit"
-        >
-          <use :xlink:href="!rocketGlobal.edit ? '#lock' : '#lock-open'"></use>
-        </svg>
-      </div>
-      <div class="rk-dashboard-bar-reload">
-        <svg class="icon lg vm cp rk-btn ghost" @click="handleOption">
-          <use xlink:href="#retry"></use>
-        </svg>
-      </div>
       <ToolBarSelect
         @onChoose="SELECT_DATABASE"
         :title="this.$t('currentDatabase')"
@@ -90,10 +67,11 @@ limitations under the License. -->
   import { Vue, Component, Prop } from 'vue-property-decorator';
   import ToolBarSelect from './tool-bar-select.vue';
   import ToolBarEndpointSelect from './tool-bar-endpoint-select.vue';
+  import ToolBarBtns from './tool-bar-btns.vue';
   import { State, Action, Mutation } from 'vuex-class';
   import { DASHBOARDTYPE } from './constant';
 
-  @Component({ components: { ToolBarSelect, ToolBarEndpointSelect } })
+  @Component({ components: { ToolBarSelect, ToolBarEndpointSelect, ToolBarBtns 
} })
   export default class ToolBar extends Vue {
     @Prop() private compType!: any;
     @Prop() private stateDashboard!: any;
@@ -103,7 +81,6 @@ limitations under the License. -->
     @State('rocketOption') private rocketOption: any;
     @Mutation('ADD_COMP') private ADD_COMP: any;
     @Mutation('SET_KEYWORDSERVICE') private SET_KEYWORDSERVICE: any;
-    @Action('SET_EDIT') private SET_EDIT: any;
     @Action('SELECT_SERVICE') private SELECT_SERVICE: any;
     @Action('SELECT_DATABASE') private SELECT_DATABASE: any;
     @Action('SELECT_ENDPOINT') private SELECT_ENDPOINT: any;
@@ -118,16 +95,6 @@ limitations under the License. -->
       }
       return current[current.length - 1].k;
     }
-    private handleOption() {
-      return this.MIXHANDLE_GET_OPTION({
-        compType: this.compType,
-        duration: this.durationTime,
-        keywordServiceName: this.rocketOption.keywordService,
-      });
-    }
-    private handleSetEdit() {
-      this.SET_EDIT(this.rocketGlobal.edit ? false : true);
-    }
     private selectService(i: any) {
       this.SELECT_SERVICE({ service: i, duration: this.durationTime });
     }
@@ -164,8 +131,4 @@ limitations under the License. -->
       }
     }
   }
-  .rk-dashboard-bar-reload {
-    padding: 15px 5px;
-    border-right: 2px solid #252a2f;
-  }
 </style>
diff --git a/src/views/components/dashboard/tool-group.vue 
b/src/views/components/dashboard/tool-group.vue
index 59b1b8d..f5b68ac 100644
--- a/src/views/components/dashboard/tool-group.vue
+++ b/src/views/components/dashboard/tool-group.vue
@@ -60,11 +60,14 @@ limitations under the License. -->
   import { Component, Prop } from 'vue-property-decorator';
   import { Mutation, Action, Getter } from 'vuex-class';
   import { DASHBOARDTYPE } from './constant';
+  import { readFile } from '@/utils/readFile';
+  import { saveFile } from '@/utils/saveFile';
 
   @Component({})
   export default class ToolGroup extends Vue {
     @Prop() private rocketGlobal: any;
     @Prop() private rocketComps: any;
+    @Mutation('SET_COMPS_TREE') private SET_COMPS_TREE: any;
     @Mutation('DELETE_COMPS_GROUP') private DELETE_COMPS_GROUP: any;
     @Mutation('ADD_COMPS_GROUP') private ADD_COMPS_GROUP: any;
     @Action('MIXHANDLE_CHANGE_GROUP') private MIXHANDLE_CHANGE_GROUP: any;
diff --git a/src/views/components/dashboard/tool-nav.vue 
b/src/views/components/dashboard/tool-nav.vue
index 7585f39..1ccb2c0 100644
--- a/src/views/components/dashboard/tool-nav.vue
+++ b/src/views/components/dashboard/tool-nav.vue
@@ -32,7 +32,7 @@ limitations under the License. -->
         <use xlink:href="#file-deletion"></use>
       </svg>
     </span>
-    <a class="rk-dashboard-nav-add" v-clickout="handleHide" 
v-if="rocketGlobal.edit">
+    <a class="rk-dashboard-nav-add mr-10" v-clickout="handleHide" 
v-if="rocketGlobal.edit">
       <svg class="icon vm" @click="show = !show">
         <use xlink:href="#todo-add"></use>
       </svg>
@@ -62,6 +62,19 @@ limitations under the License. -->
         <a class="rk-btn r vm long tc confirm" @click="handleCreate">{{ 
$t('confirm') }}</a>
       </div>
     </a>
+    <a class="rk-dashboard-import mr-10">
+      <input id="tool-nav-file" class="ipt" type="file" name="file" title="" 
accept=".json" @change="importData" />
+      <label for="tool-nav-file" class="input-label">
+        <svg class="icon open vm">
+          <use xlink:href="#folder_open"></use>
+        </svg>
+      </label>
+    </a>
+    <a>
+      <svg class="icon vm" @click="exportData">
+        <use xlink:href="#save_alt"></use>
+      </svg>
+    </a>
   </nav>
 </template>
 
@@ -69,11 +82,14 @@ limitations under the License. -->
   import Vue from 'vue';
   import { Component, Prop, Model } from 'vue-property-decorator';
   import { State, Mutation, Action } from 'vuex-class';
+  import { readFile } from '@/utils/readFile';
+  import { saveFile } from '@/utils/saveFile';
 
   @Component
   export default class ToolNav extends Vue {
     @Prop() private rocketGlobal: any;
     @Prop() private rocketComps: any;
+    @Mutation('IMPORT_COMPS_TREE') private IMPORT_COMPS_TREE: any;
     @Mutation('SET_CURRENT_COMPS') private SET_CURRENT_COMPS: any;
     @Mutation('DELETE_COMPS_TREE') private DELETE_COMPS_TREE: any;
     @Mutation('ADD_COMPS_TREE') private ADD_COMPS_TREE: any;
@@ -96,6 +112,27 @@ limitations under the License. -->
       this.handleHide();
       // this.template = 'nouse';
     }
+    private async importData(event: any) {
+      try {
+        const data: any = await readFile(event);
+        const { children, name } = data;
+        if (children && name && children[0] && children[0].width) {
+          this.IMPORT_COMPS_TREE(data);
+        } else {
+          throw new Error();
+        }
+        const el: any = document.getElementById('tool-nav-file');
+        el!.value = '';
+      } catch (e) {
+        this.$modal.show('dialog', { text: 'ERROR' });
+      }
+    }
+    private exportData() {
+      const { tree, group, current } = this.rocketComps;
+      const currentData = tree[group].children[current];
+      const name = `${currentData.name}.comps.json`;
+      saveFile(currentData, name);
+    }
   }
 </script>
 
@@ -164,4 +201,17 @@ limitations under the License. -->
   .confirm {
     margin: 10px 0;
   }
+  .rk-dashboard-import {
+    .icon.open {
+      margin-top: 2px;
+    }
+    .ipt {
+      display: none;
+    }
+    .input-label {
+      display: inline;
+      line-height: inherit;
+      cursor: pointer;
+    }
+  }
 </style>
diff --git a/src/views/constant.ts b/src/views/constant.ts
new file mode 100644
index 0000000..5e0a3fe
--- /dev/null
+++ b/src/views/constant.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export enum TopologyType {
+  TOPOLOGY_ENDPOINT = 'TOPOLOGY_ENDPOINT',
+  TOPOLOGY_INSTANCE = 'TOPOLOGY_INSTANCE',
+}
+
+export enum ObjectsType {
+  UPDATE_INSTANCES = 'UPDATE_INSTANCES',
+  UPDATE_ENDPOINTS = 'UPDATE_ENDPOINTS',
+}
diff --git a/src/views/containers/dashboard.vue 
b/src/views/containers/dashboard.vue
index b616fac..beffe15 100644
--- a/src/views/containers/dashboard.vue
+++ b/src/views/containers/dashboard.vue
@@ -17,6 +17,7 @@ limitations under the License. -->
     <ToolGroup :rocketGlobal="rocketGlobal" :rocketComps="rocketComps" />
     <ToolBar
       :rocketGlobal="rocketGlobal"
+      :rocketComps="rocketComps"
       :compType="compType"
       :durationTime="durationTime"
       :stateDashboard="stateDashboardOption"
@@ -36,6 +37,7 @@ limitations under the License. -->
         + Add An Item
       </div>
     </div>
+    <v-dialog width="300px" />
   </div>
 </template>
 
diff --git a/src/views/containers/topology/endpoint/endpoints-survey.vue 
b/src/views/containers/topology/endpoint/endpoints-survey.vue
index 218969d..9670d26 100644
--- a/src/views/containers/topology/endpoint/endpoints-survey.vue
+++ b/src/views/containers/topology/endpoint/endpoints-survey.vue
@@ -22,6 +22,7 @@ limitations under the License. -->
       :item="i"
       :index="index"
       :type="'TOPOLOGY_ENDPOINT'"
+      :updateObjects="updateObjects"
     />
   </div>
 </template>
@@ -39,6 +40,7 @@ limitations under the License. -->
   })
   export default class InstancesSurvey extends Vue {
     @Prop() private endpointComps: any;
+    @Prop() private updateObjects!: string;
   }
 </script>
 
diff --git a/src/views/containers/topology/endpoint/index.vue 
b/src/views/containers/topology/endpoint/index.vue
index 609fa41..13e2a62 100644
--- a/src/views/containers/topology/endpoint/index.vue
+++ b/src/views/containers/topology/endpoint/index.vue
@@ -15,6 +15,25 @@ limitations under the License. -->
 <template>
   <div>
     <div class="rk-dashboard-bar flex-h">
+      <span class="flex-h">
+        <div class="rk-dashboard-bar-btn">
+          <span v-tooltip:bottom="{ content: 'import' }">
+            <input id="endpoint-file" type="file" name="file" title="" 
accept=".json" @change="importData" />
+            <label class="rk-btn ghost input-label" for="endpoint-file">
+              <svg class="icon lg vm cp " :style="`marginTop: 0px`">
+                <use :xlink:href="'#folder_open'"></use>
+              </svg>
+            </label>
+          </span>
+        </div>
+        <div class="rk-dashboard-bar-btn">
+          <span v-tooltip:bottom="{ content: 'export' }">
+            <svg class="icon lg vm cp rk-btn ghost" @click="exportData">
+              <use :xlink:href="'#save_alt'"></use>
+            </svg>
+          </span>
+        </div>
+      </span>
       <ToolBarSelect :selectable="false" :title="this.$t('currentService')" 
:current="current" icon="package" />
       <ToolBarEndpointSelect
         @onChoose="selectEndpoint"
@@ -24,7 +43,7 @@ limitations under the License. -->
         icon="code"
       />
     </div>
-    <endpoints-survey :endpointComps="endpointComps" />
+    <endpoints-survey :endpointComps="endpointComps" 
:updateObjects="updateObjects" />
   </div>
 </template>
 
@@ -35,6 +54,8 @@ limitations under the License. -->
   import EndpointsSurvey from './endpoints-survey.vue';
   import ToolBarSelect from '@/views/components/dashboard/tool-bar-select.vue';
   import ToolBarEndpointSelect from 
'@/views/components/dashboard/tool-bar-endpoint-select.vue';
+  import { readFile } from '@/utils/readFile';
+  import { saveFile } from '@/utils/saveFile';
 
   interface Endpoint {
     label: string;
@@ -59,6 +80,7 @@ limitations under the License. -->
     @Action('MIXHANDLE_CHANGE_GROUP_WITH_CURRENT') private 
MIXHANDLE_CHANGE_GROUP_WITH_CURRENT: any;
     @Prop() private current!: { key: number | string; label: number | string };
     @Prop() private endpointComps: any;
+    @Prop() private updateObjects!: string;
 
     private selectEndpoint(i: any) {
       this.SELECT_ENDPOINT({ endpoint: i, duration: this.durationTime });
@@ -71,6 +93,24 @@ limitations under the License. -->
         this.selectEndpoint(this.stateDashboardOption.endpoints[0]);
       });
     }
+    private async importData(event: any) {
+      try {
+        const data: any = await readFile(event);
+        if (!Array.isArray(data)) {
+          throw new Error();
+        }
+        this.$emit('changeEndpointComps', data);
+        const el: any = document.getElementById('endpoint-file');
+        el!.value = '';
+      } catch (e) {
+        this.$modal.show('dialog', { text: 'ERROR' });
+      }
+    }
+    private exportData() {
+      const data = this.endpointComps;
+      const name = 'endpointComps.json';
+      saveFile(data, name);
+    }
   }
 </script>
 
@@ -80,4 +120,19 @@ limitations under the License. -->
     color: #efefef;
     background-color: #333840;
   }
+  .rk-dashboard-bar-btn {
+    padding: 0 5px;
+    border-right: 2px solid #252a2f;
+    height: 19px;
+  }
+  #endpoint-file {
+    display: none;
+  }
+  .input-label {
+    display: inline !important;
+    line-height: inherit;
+  }
+  .input-label.rk-btn {
+    line-height: 22px !important;
+  }
 </style>
diff --git a/src/views/containers/topology/instance/index.vue 
b/src/views/containers/topology/instance/index.vue
index 017fa68..5a3e1f6 100644
--- a/src/views/containers/topology/instance/index.vue
+++ b/src/views/containers/topology/instance/index.vue
@@ -16,6 +16,25 @@ limitations under the License. -->
 <template>
   <div style="height: 100%">
     <div class="rk-dashboard-bar flex-h">
+      <span class="flex-h">
+        <div class="rk-dashboard-bar-btn">
+          <span v-tooltip:bottom="{ content: 'import' }">
+            <input id="instance-file" type="file" name="file" title="" 
accept=".json" @change="importData" />
+            <label class="rk-btn ghost input-label" for="instance-file">
+              <svg class="icon lg vm cp " :style="`marginTop: 0px`">
+                <use :xlink:href="'#folder_open'"></use>
+              </svg>
+            </label>
+          </span>
+        </div>
+        <div class="rk-dashboard-bar-btn">
+          <span v-tooltip:bottom="{ content: 'export' }">
+            <svg class="icon lg vm cp rk-btn ghost" @click="exportData">
+              <use :xlink:href="'#save_alt'"></use>
+            </svg>
+          </span>
+        </div>
+      </span>
       <ToolBarSelect :selectable="false" :title="this.$t('currentService')" 
:current="current" icon="package" />
       <ToolBarSelect
         @onChoose="selectInstance"
@@ -25,7 +44,7 @@ limitations under the License. -->
         icon="disk"
       />
     </div>
-    <instances-survey :instanceComps="instanceComps" />
+    <instances-survey :instanceComps="instanceComps" 
:updateObjects="updateObjects" />
   </div>
 </template>
 
@@ -37,6 +56,8 @@ limitations under the License. -->
   import Vue from 'vue';
   import { Component, PropSync, Watch, Prop } from 'vue-property-decorator';
   import { Action, Getter, State } from 'vuex-class';
+  import { readFile } from '@/utils/readFile';
+  import { saveFile } from '@/utils/saveFile';
 
   interface Instance {
     label: string;
@@ -60,6 +81,7 @@ limitations under the License. -->
     @Action('MIXHANDLE_CHANGE_GROUP_WITH_CURRENT') private 
MIXHANDLE_CHANGE_GROUP_WITH_CURRENT: any;
     @Prop() private current!: { key: number | string; label: number | string };
     @Prop() private instanceComps: any;
+    @Prop() private updateObjects!: string;
 
     private selectInstance(i: any) {
       this.SELECT_INSTANCE({ instance: i, duration: this.durationTime });
@@ -71,7 +93,38 @@ limitations under the License. -->
         this.selectInstance(this.stateDashboardOption.instances[0]);
       });
     }
+    private async importData(event: any) {
+      try {
+        const data: any = await readFile(event);
+        if (!Array.isArray(data)) {
+          throw new Error();
+        }
+        this.$emit('changeInstanceComps', data);
+        const el: any = document.getElementById('instance-file');
+        el!.value = '';
+      } catch (e) {
+        this.$modal.show('dialog', { text: 'ERROR' });
+      }
+    }
+    private exportData() {
+      const data = this.instanceComps;
+      const name = 'instanceComps.json';
+      saveFile(data, name);
+    }
   }
 </script>
 
-<style lang="less" scoped></style>
+<style lang="scss" scoped>
+  .rk-dashboard-bar-btn {
+    padding: 0 5px;
+    border-right: 2px solid #252a2f;
+    height: 19px;
+  }
+  #instance-file {
+    display: none;
+  }
+  .input-label {
+    display: inline;
+    line-height: inherit;
+  }
+</style>
diff --git a/src/views/containers/topology/instance/instances-survey.vue 
b/src/views/containers/topology/instance/instances-survey.vue
index d65c52e..90f01e5 100644
--- a/src/views/containers/topology/instance/instances-survey.vue
+++ b/src/views/containers/topology/instance/instances-survey.vue
@@ -22,6 +22,7 @@ limitations under the License. -->
       :item="i"
       :index="index"
       :type="'TOPOLOGY_INSTANCE'"
+      :updateObjects="updateObjects"
     />
   </div>
 </template>
@@ -39,6 +40,7 @@ limitations under the License. -->
   })
   export default class InstancesSurvey extends Vue {
     @Prop() private instanceComps: any;
+    @Prop() private updateObjects!: string;
   }
 </script>
 
diff --git a/src/views/containers/topology/topology.vue 
b/src/views/containers/topology/topology.vue
index 5a458ad..759f871 100644
--- a/src/views/containers/topology/topology.vue
+++ b/src/views/containers/topology/topology.vue
@@ -25,8 +25,20 @@ limitations under the License. -->
     <TopoAside />
     <TopoGroup />
     <rk-sidebox :show="dialog.length" @update:show="dialog = ''" :fixed="true" 
width="80%">
-      <window-endpoint v-if="dialog === 'endpoint'" :current="this.current" 
:endpointComps="endpointComps" />
-      <window-instance v-if="dialog === 'instance'" :current="this.current" 
:instanceComps="instanceComps" />
+      <window-endpoint
+        v-if="dialog === 'endpoint'"
+        :current="this.current"
+        :endpointComps="endpointComps"
+        @changeEndpointComps="changeEndpointComps"
+        :updateObjects="updateObjects"
+      />
+      <window-instance
+        v-if="dialog === 'instance'"
+        :current="this.current"
+        :instanceComps="instanceComps"
+        @changeInstanceComps="changeInstanceComps"
+        :updateObjects="updateObjects"
+      />
       <window-trace v-if="dialog === 'trace'" :current="this.current" />
       <window-alarm v-if="dialog === 'alarm'" :current="this.current" />
     </rk-sidebox>
@@ -37,6 +49,7 @@ limitations under the License. -->
   import { State, Action, Getter, Mutation } from 'vuex-class';
   import { AxiosResponse } from 'axios';
   import { State as topoState } from '@/store/modules/topology';
+  import { TopologyType, ObjectsType } from '../../constant';
   import WindowEndpoint from '@/views/containers/topology/endpoint/index.vue';
   import WindowInstance from '@/views/containers/topology/instance/index.vue';
   import WindowTrace from '@/views/containers/topology/trace/index.vue';
@@ -67,7 +80,19 @@ limitations under the License. -->
     private dialog: string = '';
     private instanceComps: any = [];
     private endpointComps: any = [];
+    private updateObjects: string = '';
+
     private created() {
+      if (window.localStorage.getItem('topologyInstances') || 
window.localStorage.getItem('topologyEndpoints')) {
+        const instanceComps: string = 
`${window.localStorage.getItem('topologyInstances')}`;
+        this.instanceComps = JSON.parse(instanceComps);
+        const endpointComps: string = 
`${window.localStorage.getItem('topologyEndpoints')}`;
+        this.endpointComps = JSON.parse(endpointComps);
+      } else {
+        this.queryTemplates();
+      }
+    }
+    private queryTemplates() {
       this.GET_ALL_TEMPLATES().then(
         (
           allTemplates: Array<{
@@ -79,11 +104,13 @@ limitations under the License. -->
           }>,
         ) => {
           const template =
-            allTemplates.filter((item: any) => item.type === 
'TOPOLOGY_INSTANCE' && item.activated)[0] || {};
+            allTemplates.filter((item: any) => item.type === 
TopologyType.TOPOLOGY_INSTANCE && item.activated)[0] || {};
           this.instanceComps = JSON.parse(template.configuration) || [];
+          window.localStorage.setItem('topologyInstances', 
JSON.stringify(this.instanceComps));
           const endpointTemplate =
-            allTemplates.filter((item: any) => item.type === 
'TOPOLOGY_ENDPOINT' && item.activated)[0] || {};
+            allTemplates.filter((item: any) => item.type === 
TopologyType.TOPOLOGY_ENDPOINT && item.activated)[0] || {};
           this.endpointComps = JSON.parse(endpointTemplate.configuration) || 
[];
+          window.localStorage.setItem('topologyEndpoints', 
JSON.stringify(this.endpointComps));
         },
       );
     }
@@ -94,6 +121,16 @@ limitations under the License. -->
       this.CLEAR_TOPO_INFO();
       this.CLEAR_TOPO();
     }
+    private changeInstanceComps(data: any) {
+      this.updateObjects = ObjectsType.UPDATE_INSTANCES;
+      this.instanceComps.push(...data);
+      window.localStorage.setItem('topologyInstances', 
JSON.stringify(this.instanceComps));
+    }
+    private changeEndpointComps(data: any) {
+      this.updateObjects = ObjectsType.UPDATE_ENDPOINTS;
+      this.endpointComps.push(...data);
+      window.localStorage.setItem('topologyEndpoints', 
JSON.stringify(this.endpointComps));
+    }
   }
 </script>
 <style lang="scss">

Reply via email to