feat: Set a reminder to clear cache for dashboard templates (#550)

* feat: set a reminder to clear cache for dashboard templates

* fix: set time

* fix: remove unused imports

* fix: set first reminder

* fix: update reminder content

* fix: update modal style

* fix: code style
diff --git a/src/assets/lang/en.ts b/src/assets/lang/en.ts
index ce2cf26..f29b106 100644
--- a/src/assets/lang/en.ts
+++ b/src/assets/lang/en.ts
@@ -254,6 +254,10 @@
   addExcludingKeywordsOfContent: 'Please input a keyword of excluding content',
   noticeTag: 'Please press enter after inputting a tag.',
   conditionNotice: 'Notice: Please press enter after inputting a tag, key of content, exclude key of content.',
+  cacheModalTitle: 'Clear cache reminder',
+  yes: 'Yes',
+  no: 'No',
+  cacheReminderContent: 'SkyWalking detected dashboard template updates, do you want to update?',
 };
 
 export default m;
diff --git a/src/assets/lang/zh.ts b/src/assets/lang/zh.ts
index c3725ff..3000465 100644
--- a/src/assets/lang/zh.ts
+++ b/src/assets/lang/zh.ts
@@ -252,6 +252,10 @@
   addExcludingKeywordsOfContent: '请输入一个内容不包含的关键词',
   NoticeTag: '请输入一个标签之后回车',
   conditionNotice: '请输入一个标签、内容关键词或者内容不包含的关键词之后回车',
+  cacheModalTitle: '清除缓存提醒',
+  yes: '是的',
+  no: '不',
+  cacheReminderContent: 'SkyWalking检测到仪表板模板更新,是否需要更新?',
 };
 
 export default m;
diff --git a/src/components/rk-modal.vue b/src/components/rk-modal.vue
index 93357f2..0dd33c6 100644
--- a/src/components/rk-modal.vue
+++ b/src/components/rk-modal.vue
@@ -13,152 +13,152 @@
 See the License for the specific language governing permissions and
 limitations under the License. -->
 <template>
-    <div class="rk-modal-bg" v-show="show" @mousemove="modalMove" @mouseup="cancelMove">
-        <div class="rk-modal-container">
-            <div class="rk-modal-header" @mousedown="setStartingPoint">
-                <span class="title">{{ this.title }}</span>
-                <div class="r rk-modal-close" @click="cancel">
-                    <svg class="icon">
-                        <use xlink:href="#close"></use>
-                    </svg>
-                </div>
-            </div>
-            <div class="rk-modal-main">
-                <slot></slot>
-            </div>
-            <div v-if="showButton" class="rk-modal-footer">
-                <rk-button class="cancel" @click="cancel">{{ $t('cancel') }}</rk-button>
-                <rk-button @click="confirm">{{ $t('confirm') }}</rk-button>
-            </div>
+  <div class="rk-modal-bg" v-show="show" @mousemove="modalMove" @mouseup="cancelMove">
+    <div class="rk-modal-container">
+      <div class="rk-modal-header" @mousedown="setStartingPoint">
+        <span class="title">{{ this.title }}</span>
+        <div class="r rk-modal-close" @click="cancel">
+          <svg class="icon">
+            <use xlink:href="#close"></use>
+          </svg>
         </div>
+      </div>
+      <div class="rk-modal-main">
+        <slot></slot>
+      </div>
+      <div v-if="showButton" class="rk-modal-footer">
+        <rk-button class="cancel" @click="cancel">{{ $t('cancel') }}</rk-button>
+        <rk-button @click="confirm">{{ $t('confirm') }}</rk-button>
+      </div>
     </div>
+  </div>
 </template>
 <script lang="js">
-    import RkButton from '@/components/rk-button';
-    export default {
-        name: 'RkModal',
-        components: {RkButton},
-        props: {
-            show: {
-                type: Boolean,
-                default: false,
-            },
-            showButton: {
-                type: Boolean,
-                default: false,
-            },
-            title: {
-                type: String,
-                default: '',
-            },
-        },
-        data() {
-            return {
-                x: 0,
-                y: 0,
-                node: null,
-                isCanMove: false,
-            };
-        },
-        mounted() {
-            this.node = document.querySelector('.rk-modal-container');
-        },
-        methods: {
-            cancel() {
-                this.$emit('update:show', false);
-                this.$emit('cancelModalCallback');
-            },
+  import RkButton from '@/components/rk-button';
+  export default {
+    name: 'RkModal',
+    components: {RkButton},
+    props: {
+      show: {
+        type: Boolean,
+        default: false,
+      },
+      showButton: {
+        type: Boolean,
+        default: false,
+      },
+      title: {
+        type: String,
+        default: '',
+      },
+    },
+    data() {
+      return {
+        x: 0,
+        y: 0,
+        node: null,
+        isCanMove: false,
+      };
+    },
+    mounted() {
+      this.node = document.querySelector('.rk-modal-container');
+    },
+    methods: {
+      cancel() {
+        this.$emit('update:show', false);
+        this.$emit('cancelModalCallback');
+      },
 
-            confirm() {
-                this.$emit('update:show', false);
-                this.$emit('confirmModalCallback');
-            },
+      confirm() {
+        this.$emit('update:show', false);
+        this.$emit('confirmModalCallback');
+      },
 
-            setStartingPoint(e) {
-                this.x = e.clientX - this.node.offsetLeft;
-                this.y = e.clientY - this.node.offsetTop;
-                this.isCanMove = true;
-            },
+      setStartingPoint(e) {
+        this.x = e.clientX - this.node.offsetLeft;
+        this.y = e.clientY - this.node.offsetTop;
+        this.isCanMove = true;
+      },
 
-            modalMove(e) {
-                if (this.isCanMove) {
-                    this.node.style.left = e.clientX - this.x + 'px';
-                    this.node.style.top = e.clientY - this.y + 'px';
-                }
-            },
+      modalMove(e) {
+        if (this.isCanMove) {
+          this.node.style.left = e.clientX - this.x + 'px';
+          this.node.style.top = e.clientY - this.y + 'px';
+        }
+      },
 
-            cancelMove() {
-                this.isCanMove = false;
-            },
-        },
-    };
+      cancelMove() {
+        this.isCanMove = false;
+      },
+    },
+  };
 </script>
 
 <style lang="scss">
-    .rk-modal-bg {
-        position: fixed;
-        top: 0;
-        left: 0;
-        width: 100%;
-        height: 100%;
-        background: rgba(0, 0, 0, .5);
-        z-index: 10;
+  .rk-modal-bg {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.5);
+    z-index: 10;
+  }
+
+  .rk-modal-container {
+    background: #fff;
+    border-radius: 10px;
+    overflow: hidden;
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+  }
+
+  .rk-modal-header {
+    height: 35px;
+    background: #333840;
+    color: #efefef;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: move;
+  }
+
+  .rk-modal-close {
+    position: absolute;
+    right: 10px;
+    top: 5px;
+    cursor: pointer;
+    color: #d8d8d8;
+    transition: color 0.3s;
+
+    .icon {
+      width: 18px;
+      height: 20px;
     }
 
-    .rk-modal-container {
-        background: #fff;
-        border-radius: 10px;
-        overflow: hidden;
-        position: fixed;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
+    &:hover {
+      color: #3d92ff;
     }
+  }
 
-    .rk-modal-header {
-        height: 35px;
-        background: #333840;
-        color: #efefef;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        cursor: move;
-    }
+  .rk-modal-main {
+    padding: 15px;
+  }
 
-    .rk-modal-close {
-        position: absolute;
-        right: 10px;
-        top: 5px;
-        cursor: pointer;
-        color: #d8d8d8;
-        transition: color 0.3s;
-
-        .icon {
-            width: 18px;
-            height: 20px;
-        }
-
-        &:hover {
-            color: #3d92ff;
-        }
-    }
-
-    .rk-modal-main {
-        padding: 15px 40px;
-    }
-
-    .rk-modal-footer {
-        display: flex;
-        align-items: center;
-        justify-content: right;
-        height: 35px;
-        border-top: 1px solid #ddd;
-        padding-right: 10px;
-    }
-    .rk-modal-footer .cancel{
-        margin-right: 10px;
-        border: 1px solid #ddd;
-        color: #333;
-        background-color: #fff;
-    }
+  .rk-modal-footer {
+    display: flex;
+    align-items: center;
+    justify-content: right;
+    height: 35px;
+    border-top: 1px solid #ddd;
+    padding-right: 10px;
+  }
+  .rk-modal-footer .cancel {
+    margin-right: 10px;
+    border: 1px solid #ddd;
+    color: #333;
+    background-color: #fff;
+  }
 </style>
diff --git a/src/store/modules/dashboard/dashboard-data-layout.ts b/src/store/modules/dashboard/dashboard-data-layout.ts
index bf69345..7e5da73 100644
--- a/src/store/modules/dashboard/dashboard-data-layout.ts
+++ b/src/store/modules/dashboard/dashboard-data-layout.ts
@@ -55,7 +55,8 @@
   },
   [types.IMPORT_TREE](state: State, data: CompsTree[]) {
     state.tree.push(...data);
-    window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.SET_GROUP_QUERY](state: State, params: any) {
     state.tree[state.group].query = params;
@@ -73,6 +74,7 @@
   [types.SET_CURRENT_SERVICE_GROUP](state: State, serviceGroup: string) {
     state.tree.splice(state.group, 1, Object.assign(state.tree[state.group], { serviceGroup }));
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.SET_CURRENT_GROUP_WITH_CURRENT](state: State, { index, current = 0 }: { index: number; current: number }) {
     state.group = index;
@@ -91,6 +93,7 @@
     state.tree.push(group);
 
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.ADD_COMPS_TREE](state: State, params: { name: string }) {
     if (!params.name) {
@@ -98,10 +101,12 @@
     }
     state.tree[state.group].children.push({ name: params.name, children: [] });
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.IMPORT_COMPS_TREE](state: State, params: any) {
     state.tree[state.group].children.push(params);
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.DELETE_COMPS_GROUP](state: State, index: number) {
     state.tree.splice(index, 1);
@@ -109,6 +114,7 @@
       state.group--;
     }
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.DELETE_COMPS_TREE](state: State, index: number) {
     state.tree[state.group].children.splice(index, 1);
@@ -116,6 +122,7 @@
       state.current--;
     }
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.ADD_COMP](state: State) {
     const type = state.tree[state.group].type;
@@ -129,16 +136,19 @@
     };
     state.tree[state.group].children[state.current].children.push(comp);
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.DELETE_COMP](state: State, index: number) {
     state.tree[state.group].children[state.current].children.splice(index, 1);
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.EDIT_COMP_CONFIG](state: State, params: { values: any; index: number }) {
     const temp = state.tree[state.group].children[state.current].children[params.index];
 
     state.tree[state.group].children[state.current].children[params.index] = { ...temp, ...params.values };
     window.localStorage.setItem('dashboard', JSON.stringify(state.tree));
+    localStorage.setItem('isUpdatedTemplates', 'true');
   },
   [types.SET_TEMPLATES](state: State, templates) {
     state.templates = templates;
diff --git a/src/store/modules/dashboard/dashboard-data.ts b/src/store/modules/dashboard/dashboard-data.ts
index da6054c..67c2310 100644
--- a/src/store/modules/dashboard/dashboard-data.ts
+++ b/src/store/modules/dashboard/dashboard-data.ts
@@ -19,7 +19,7 @@
 import { CompsTree, Event } from '@/types/dashboard';
 import { AxiosResponse } from 'axios';
 import graph from '@/graph';
-import dashboardLayout from './dashboard-data-layout';
+import dashboardLayout, { State as layoutState } from './dashboard-data-layout';
 import dashboardQuery from './dashboard-data-query';
 import { QueryEventCondition } from '../../../types/dashboard';
 import { dateFormatTime } from '@/utils/dateFormat';
diff --git a/src/views/containers/dashboard.vue b/src/views/containers/dashboard.vue
index b973bdc..3e9f2f5 100644
--- a/src/views/containers/dashboard.vue
+++ b/src/views/containers/dashboard.vue
@@ -40,6 +40,17 @@
         + Add An Item
       </div>
     </div>
+    <rk-modal :show.sync="showCacheModal" :title="$t('cacheModalTitle')">
+      <div class="reminder-content">{{ $t('cacheReminderContent') }}</div>
+      <div class="reminder-btns">
+        <a class="rk-cache-modal-btn bg-blue mr-10" @click="clearDashboardTemps">
+          {{ $t('yes') }}
+        </a>
+        <a class="rk-cache-modal-btn" @click="closeCacheModal">
+          {{ $t('no') }}
+        </a>
+      </div>
+    </rk-modal>
   </div>
 </template>
 
@@ -53,6 +64,7 @@
   import { State as globalState } from '@/store/modules/global';
   import { State as optionState } from '@/store/modules/global/selectors';
   import { State as dataState } from '@/store/modules/dashboard/dashboard-data';
+  import { State as layoutState } from '@/store/modules/dashboard/dashboard-data-layout';
   import { PageTypes } from '@/constants/constant';
 
   interface ITemplate {
@@ -73,7 +85,7 @@
   export default class Dashboard extends Vue {
     @State('rocketbot') private rocketGlobal!: globalState;
     @State('rocketOption') private stateDashboardOption!: optionState;
-    @State('rocketData') private rocketComps!: dataState;
+    @State('rocketData') private rocketComps!: dataState & layoutState;
     @Action('MIXHANDLE_GET_OPTION') private MIXHANDLE_GET_OPTION: any;
     @Action('GET_ALL_TEMPLATES') private GET_ALL_TEMPLATES: any;
     @Getter('durationTime') private durationTime: any;
@@ -81,9 +93,12 @@
     @Mutation('ADD_COMP') private ADD_COMP: any;
     @Mutation('SET_EDIT') private SET_EDIT: any;
     @Mutation('SET_TEMPLATES') private SET_TEMPLATES: any;
+    @Mutation('UPDATE_DASHBOARD') private UPDATE_DASHBOARD: any;
 
     private isRouterAlive: boolean = true;
     private templatesErrors: boolean = false;
+    private showCacheModal: boolean = false;
+    private intervalCache: any;
     public reload(): void {
       this.isRouterAlive = false;
       this.$nextTick(() => {
@@ -108,32 +123,59 @@
     private beforeMount() {
       this.GET_ALL_TEMPLATES().then((templateResp: ITemplate[]) => {
         const dashboardTemplate = templateResp.filter((item: ITemplate) => item.type === 'DASHBOARD');
-        const templatesConfig = dashboardTemplate.map((item: ITemplate) => JSON.parse(item.configuration)).flat(1);
+        const templatesConfig = dashboardTemplate.map((item: ITemplate) => {
+          return { ...JSON.parse(item.configuration)[0], activated: item.activated, disabled: item.disabled };
+        });
         this.SET_TEMPLATES(templatesConfig);
-        if (window.localStorage.getItem('version') !== '8.0') {
+        if (window.localStorage.getItem('version') === '8.0') {
+          const data: string = `${window.localStorage.getItem('dashboard')}`;
+          this.SET_COMPS_TREE(JSON.parse(data));
+        } else {
           window.localStorage.removeItem('dashboard');
           const template = templateResp.filter((item: ITemplate) => item.type === 'DASHBOARD' && item.activated);
           const templatesConfiguration = template.map((item: ITemplate) => JSON.parse(item.configuration)).flat(1);
           this.SET_COMPS_TREE(templatesConfiguration || []);
           window.localStorage.setItem('version', '8.0');
           window.localStorage.setItem('dashboard', JSON.stringify(templatesConfiguration));
-          this.handleOption();
-        } else {
-          const data: string = `${window.localStorage.getItem('dashboard')}`;
-          this.SET_COMPS_TREE(JSON.parse(data));
-          this.handleOption();
         }
+        this.handleOption();
+        this.checkCacheTime();
+        this.intervalCache = setInterval(() => {
+          this.checkCacheTime();
+        }, 3600000); // 1h
       });
     }
-    private setDashboardTemplates(allTemplate: ITemplate[]) {
-      const template = allTemplate.filter((item: ITemplate) => item.type === 'DASHBOARD' && item.activated);
-      const templatesConfiguration = template.map((item: ITemplate) => JSON.parse(item.configuration)).flat(1);
+    private checkCacheTime() {
+      const templatesCacheTime = localStorage.getItem('templatesCacheTime');
+      if (templatesCacheTime) {
+        const diffTime = new Date().getTime() - Number(templatesCacheTime);
+        const diffDay = diffTime / 1000 / (60 * 60 * 24);
+        if (diffDay >= 3) {
+          const isUpdatedTemplates = localStorage.getItem('isUpdatedTemplates');
+          if (isUpdatedTemplates === 'true') {
+            this.showCacheModal = true;
+          }
+        }
+      } else {
+        this.showCacheModal = true;
+        localStorage.setItem('templatesCacheTime', String(new Date().getTime()));
+      }
+    }
+    private clearDashboardTemps() {
+      const templatesConfiguration = this.rocketComps.templates.filter((item: any) => item.activated);
       this.SET_COMPS_TREE(templatesConfiguration || []);
-      window.localStorage.setItem('version', '8.0');
-      window.localStorage.setItem('dashboard', JSON.stringify(templatesConfiguration));
+      localStorage.setItem('dashboard', JSON.stringify(templatesConfiguration));
+      localStorage.setItem('templatesCacheTime', String(new Date().getTime()));
+      this.UPDATE_DASHBOARD();
+      this.showCacheModal = false;
+    }
+    private closeCacheModal() {
+      this.showCacheModal = false;
+      localStorage.setItem('templatesCacheTime', String(new Date().getTime()));
     }
     private beforeDestroy() {
       this.SET_EDIT(false);
+      clearInterval(this.intervalCache);
     }
   }
 </script>
@@ -162,4 +204,23 @@
     display: inline-block;
     font-size: 16px;
   }
+  .reminder-btns {
+    margin-top: 15px;
+    text-align: right;
+  }
+  .rk-cache-modal-btn {
+    display: inline-block;
+    height: 30px;
+    width: 80px;
+    text-align: center;
+    line-height: 26px;
+    border-radius: 3px;
+    border: 1px solid #ddd;
+    color: #333;
+    outline: none;
+    cursor: pointer;
+  }
+  .bg-blue {
+    color: #fff;
+  }
 </style>