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

hanahmily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git


The following commit(s) were added to refs/heads/main by this push:
     new baf47bfa feat(UI): Add the `stages` to `Groups` (#640)
baf47bfa is described below

commit baf47bfa41b489f9980da662e4d97ac1f5277053
Author: Fine0830 <[email protected]>
AuthorDate: Wed Apr 2 16:01:45 2025 +0800

    feat(UI): Add the `stages` to `Groups` (#640)
---
 CHANGES.md                                  |   1 +
 ui/src/components/GroupTree/StageEditor.vue | 108 ++++++++++
 ui/src/components/GroupTree/data.js         | 127 +++++++++++
 ui/src/components/GroupTree/index.vue       | 322 +++++++++++++---------------
 4 files changed, 384 insertions(+), 174 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 51dadf40..9d450231 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -34,6 +34,7 @@ Release Notes.
 - Add lifecycle management for the node.
 - Property: Introduce the schema style to the property.
 - Add time range parameters to stream index filter.
+- UI: Add the `stages` to groups.
 - Add time range return value from stream local index filter.
 - Deduplicate the documents on building the series index.
 
diff --git a/ui/src/components/GroupTree/StageEditor.vue 
b/ui/src/components/GroupTree/StageEditor.vue
new file mode 100644
index 00000000..7d22fc7b
--- /dev/null
+++ b/ui/src/components/GroupTree/StageEditor.vue
@@ -0,0 +1,108 @@
+<!--
+  ~ Licensed to 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. Apache Software Foundation (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.
+-->
+
+<script setup>
+  import { ref } from 'vue';
+  import { StageConfig } from './data';
+
+  const showDialog = ref(false);
+  const ruleForm = ref();
+  const title = ref('');
+  const stage = ref(StageConfig);
+  let promiseResolve;
+  const closeDialog = () => {
+    showDialog.value = false;
+    stage.value = StageConfig;
+  };
+  const confirmApply = async () => {
+    if (!ruleForm.value) return;
+    await ruleForm.value.validate((valid) => {
+      if (valid) {
+        promiseResolve(JSON.parse(JSON.stringify(stage.value)));
+        showDialog.value = false;
+      }
+    });
+  };
+  const openDialog = (data) => {
+    if (data) {
+      stage.value = data;
+      title.value = 'Edit Stage';
+    } else {
+      title.value = 'Add Stage';
+    }
+    showDialog.value = true;
+    return new Promise((resolve) => {
+      promiseResolve = resolve;
+    });
+  };
+  defineExpose({
+    openDialog,
+  });
+</script>
+
+<template>
+  <el-dialog v-model="showDialog" :title="title" width="600px">
+    <el-form ref="ruleForm" :model="stage" label-position="left">
+      <el-form-item label="Name" prop="name" required label-width="200">
+        <el-input v-model="stage.name" autocomplete="off" style="width: 100%" 
/>
+      </el-form-item>
+      <el-form-item label="Shard Number" prop="shardNum" required 
label-width="200">
+        <el-input-number v-model="stage.shardNum" autocomplete="off" 
style="width: 100%" />
+      </el-form-item>
+      <el-form-item label="TTL Unit" prop="ttlUnit" required label-width="200">
+        <el-select v-model="stage.ttlUnit" placeholder="please select" 
style="width: 100%">
+          <el-option label="Hour" value="UNIT_HOUR"></el-option>
+          <el-option label="Day" value="UNIT_DAY"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="TTL Number" prop="ttlNum" required 
label-width="200">
+        <el-input-number v-model="stage.ttlNum" autocomplete="off" 
style="width: 100%" />
+      </el-form-item>
+      <el-form-item label="Segment Interval Unit" prop="segmentIntervalUnit" 
required label-width="200">
+        <el-select v-model="stage.segmentIntervalUnit" placeholder="please 
select" style="width: 100%">
+          <el-option label="Hour" value="UNIT_HOUR"></el-option>
+          <el-option label="Day" value="UNIT_DAY"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="Segment Interval Number" prop="segmentIntervalNum" 
required label-width="200">
+        <el-input-number v-model="stage.segmentIntervalNum" autocomplete="off" 
style="width: 100%" />
+      </el-form-item>
+      <el-form-item label="Node Selector" prop="nodeSelector" required 
label-width="200">
+        <el-input v-model="stage.nodeSelector" autocomplete="off" />
+      </el-form-item>
+      <el-form-item label="Close" prop="close" label-width="200">
+        <el-switch v-model="stage.close" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <span class="dialog-footer footer">
+        <el-button @click="closeDialog">Cancel</el-button>
+        <el-button type="primary" @click="confirmApply"> Confirm </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<style scoped lang="scss">
+  .footer {
+    width: 100%;
+    display: flex;
+    justify-content: center;
+  }
+</style>
diff --git a/ui/src/components/GroupTree/data.js 
b/ui/src/components/GroupTree/data.js
new file mode 100644
index 00000000..779f36a6
--- /dev/null
+++ b/ui/src/components/GroupTree/data.js
@@ -0,0 +1,127 @@
+/*
+ * Licensed to 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. Apache Software Foundation (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 StageFields = [
+  { label: 'Name', key: 'name' },
+  { label: 'Shard number', key: 'shardNum' },
+  { label: 'TTL unit', key: 'ttlUnit' },
+  { label: 'TTL number', key: 'ttlNum' },
+  { label: 'Segment interval unit', key: 'segmentIntervalUnit' },
+  { label: 'Segment interval number', key: 'segmentIntervalNum' },
+  { label: 'Node selector', key: 'nodeSelector' },
+  { label: 'Close', key: 'close' },
+];
+export const StageConfig = {
+  name: '',
+  shardNum: 1,
+  ttlUnit: 'UNIT_DAY',
+  ttlNum: 3,
+  segmentIntervalUnit: 'UNIT_DAY',
+  segmentIntervalNum: 1,
+  NodeSelector: '',
+};
+
+export const Rules = {
+  name: [
+    {
+      required: true,
+      message: 'Please enter the name of the group',
+      trigger: 'blur',
+    },
+  ],
+  catalog: [
+    {
+      required: true,
+      message: 'Please select the type of the group',
+      trigger: 'blur',
+    },
+  ],
+  shardNum: [
+    {
+      required: true,
+      message: 'Please select the shard num of the group',
+      trigger: 'blur',
+    },
+  ],
+  segmentIntervalUnit: [
+    {
+      required: true,
+      message: 'Please select the segment interval unit of the group',
+      trigger: 'blur',
+    },
+  ],
+  segmentIntervalNum: [
+    {
+      required: true,
+      message: 'Please select the segment Interval num of the group',
+      trigger: 'blur',
+    },
+  ],
+  ttlUnit: [
+    {
+      required: true,
+      message: 'Please select the ttl unit of the group',
+      trigger: 'blur',
+    },
+  ],
+  ttlNum: [
+    {
+      required: true,
+      message: 'Please select the ttl num of the group',
+      trigger: 'blur',
+    },
+  ],
+  stages: [
+    {
+      required: false,
+      message: 'Please ass the stages of the group',
+      trigger: 'blur',
+    },
+  ],
+};
+
+export const DefaultProps = {
+  children: 'children',
+  label: 'name',
+};
+
+export const TargetTypes = {
+  Group: 'group',
+  Resources: 'resources',
+};
+// catalog to group type
+export const CatalogToGroupType = {
+  CATALOG_MEASURE: 'measure',
+  CATALOG_STREAM: 'stream',
+  CATALOG_PROPERTY: 'property',
+};
+
+// group type to catalog
+export const GroupTypeToCatalog = {
+  measure: 'CATALOG_MEASURE',
+  stream: 'CATALOG_STREAM',
+  property: 'CATALOG_PROPERTY',
+};
+
+export const TypeMap = {
+  topNAggregation: 'topn-agg',
+  indexRule: 'index-rule',
+  indexRuleBinding: 'index-rule-binding',
+  children: 'children',
+};
diff --git a/ui/src/components/GroupTree/index.vue 
b/ui/src/components/GroupTree/index.vue
index 298e0b1b..5c442770 100644
--- a/ui/src/components/GroupTree/index.vue
+++ b/ui/src/components/GroupTree/index.vue
@@ -33,8 +33,18 @@
   import { ElMessage, ElMessageBox } from 'element-plus';
   import { watch, getCurrentInstance } from '@vue/runtime-core';
   import { useRouter, useRoute } from 'vue-router';
-  import { ref, reactive } from 'vue';
+  import { ref, reactive, onMounted, computed } from 'vue';
   import { Search } from '@element-plus/icons-vue';
+  import StageEditor from './StageEditor.vue';
+  import {
+    StageFields,
+    Rules,
+    DefaultProps,
+    TargetTypes,
+    CatalogToGroupType,
+    GroupTypeToCatalog,
+    TypeMap,
+  } from './data';
 
   const router = useRouter();
   const route = useRoute();
@@ -46,6 +56,7 @@
   const filterText = ref('');
   const currentNode = ref({});
   const resizable = ref();
+  const stageEditorRef = ref(null);
   // Data
   const data = reactive({
     groupLists: [],
@@ -59,102 +70,54 @@
     // create/edit group
     dialogGroupVisible: false,
     setGroup: 'create',
-    groupForm: {
-      name: null,
-      catalog: 'CATALOG_STREAM',
-      shardNum: 1,
-      segmentIntervalUnit: 'UNIT_DAY',
-      segmentIntervalNum: 1,
-      ttlUnit: 'UNIT_DAY',
-      ttlNum: 3,
-    },
     activeNode: '',
     formLabelWidth: '170px',
+    clickIndex: NaN,
   });
-
-  const defaultProps = {
-    children: 'children',
-    label: 'name',
-  };
-
-  const TargetTypes = {
-    Group: 'group',
-    Resources: 'resources',
-  };
-  // catalog to group type
-  const catalogToGroupType = {
-    CATALOG_MEASURE: 'measure',
-    CATALOG_STREAM: 'stream',
-    CATALOG_PROPERTY: 'property',
-  };
-
-  // group type to catalog
-  const groupTypeToCatalog = {
-    measure: 'CATALOG_MEASURE',
-    stream: 'CATALOG_STREAM',
-    property: 'CATALOG_PROPERTY',
-  };
-
-  const TypeMap = {
-    topNAggregation: 'topn-agg',
-    indexRule: 'index-rule',
-    indexRuleBinding: 'index-rule-binding',
-    children: 'children',
-  };
-
-  // rules
-  const rules = {
-    name: [
-      {
-        required: true,
-        message: 'Please enter the name of the group',
-        trigger: 'blur',
-      },
-    ],
-    catalog: [
-      {
-        required: true,
-        message: 'Please select the type of the group',
-        trigger: 'blur',
-      },
-    ],
-    shardNum: [
-      {
-        required: true,
-        message: 'Please select the shard num of the group',
-        trigger: 'blur',
-      },
-    ],
-    segmentIntervalUnit: [
-      {
-        required: true,
-        message: 'Please select the segment interval unit of the group',
-        trigger: 'blur',
-      },
-    ],
-    segmentIntervalNum: [
-      {
-        required: true,
-        message: 'Please select the segment Interval num of the group',
-        trigger: 'blur',
-      },
-    ],
-    ttlUnit: [
-      {
-        required: true,
-        message: 'Please select the ttl unit of the group',
-        trigger: 'blur',
+  const groupForm = reactive({
+    name: '',
+    catalog: 'CATALOG_STREAM',
+    shardNum: 1,
+    segmentIntervalUnit: 'UNIT_DAY',
+    segmentIntervalNum: 1,
+    ttlUnit: 'UNIT_DAY',
+    ttlNum: 3,
+    stages: [],
+  });
+  const getGroupForm = computed(() => ({
+    group: {
+      metadata: {
+        group: '',
+        name: groupForm.name,
       },
-    ],
-    ttlNum: [
-      {
-        required: true,
-        message: 'Please select the ttl num of the group',
-        trigger: 'blur',
+      catalog: groupForm.catalog,
+      resourceOpts: {
+        shardNum: groupForm.shardNum,
+        segmentInterval: {
+          unit: groupForm.segmentIntervalUnit,
+          num: groupForm.segmentIntervalNum,
+        },
+        ttl: {
+          unit: groupForm.ttlUnit,
+          num: groupForm.ttlNum,
+        },
+        stages: groupForm.stages.map((d) => ({
+          name: d.name,
+          shardNum: d.shardNum,
+          nodeSelector: d.nodeSelector,
+          close: d.close,
+          ttl: {
+            unit: d.ttlUnit,
+            num: d.ttlNum,
+          },
+          segmentInterval: {
+            unit: d.segmentIntervalUnit,
+            num: d.segmentIntervalNum,
+          },
+        })),
       },
-    ],
-  };
-
+    },
+  }));
   // Eventbus
   const $bus = getCurrentInstance().appContext.config.globalProperties.mittBus;
 
@@ -169,6 +132,9 @@
 
   // emit event
   const emit = defineEmits(['setWidth']);
+  onMounted(() => {
+    getGroupLists();
+  });
 
   // init data
   function getGroupLists() {
@@ -176,7 +142,7 @@
     loading.value = true;
     getGroupList().then((res) => {
       if (res.status === 200) {
-        data.groupLists = res.data.group.filter((d) => 
catalogToGroupType[d.catalog] === props.type);
+        data.groupLists = res.data.group.filter((d) => 
CatalogToGroupType[d.catalog] === props.type);
         let promise = data.groupLists.map((item) => {
           const type = props.type;
           const name = item.metadata.name;
@@ -447,17 +413,26 @@
 
   function openCreateGroup() {
     data.setGroup = 'create';
-    data.groupForm.catalog = groupTypeToCatalog[props.type];
+    groupForm.catalog = GroupTypeToCatalog[props.type];
     data.dialogGroupVisible = true;
   }
   function openEditGroup() {
-    data.groupForm.name = currentNode.value.name;
-    data.groupForm.catalog = currentNode.value.catalog;
-    data.groupForm.shardNum = currentNode.value.resourceOpts?.shardNum;
-    data.groupForm.segmentIntervalUnit = 
currentNode.value.resourceOpts?.segmentInterval?.unit;
-    data.groupForm.segmentIntervalNum = 
currentNode.value.resourceOpts?.segmentInterval?.num;
-    data.groupForm.ttlUnit = currentNode.value.resourceOpts?.ttl?.unit;
-    data.groupForm.ttlNum = currentNode.value.resourceOpts?.ttl?.num;
+    groupForm.name = currentNode.value.name;
+    groupForm.catalog = currentNode.value.catalog;
+    groupForm.shardNum = currentNode.value.resourceOpts?.shardNum;
+    groupForm.segmentIntervalUnit = 
currentNode.value.resourceOpts?.segmentInterval?.unit;
+    groupForm.segmentIntervalNum = 
currentNode.value.resourceOpts?.segmentInterval?.num;
+    groupForm.ttlUnit = currentNode.value.resourceOpts?.ttl?.unit;
+    groupForm.ttlNum = currentNode.value.resourceOpts?.ttl?.num;
+    groupForm.stages = currentNode.value.resourceOpts?.stages.map((d) => ({
+      ...d,
+      ttlUnit: d.ttl.unit,
+      ttlNum: d.ttl.num,
+      segmentIntervalUnit: d.segmentInterval.unit,
+      segmentIntervalNum: d.segmentInterval.num,
+      segmentInterval: undefined,
+      ttl: undefined,
+    }));
     data.dialogGroupVisible = true;
     data.setGroup = 'edit';
   }
@@ -502,7 +477,7 @@
       if (Object.keys(TypeMap).includes(currentNode.value.type)) {
         return 
deleteSecondaryDataModelFunction(TypeMap[currentNode.value.type]);
       }
-      if (props.type === TargetTypes.Group) {
+      if (currentNode.value.type === TargetTypes.Group) {
         return deleteGroupFunction();
       }
       return deleteResource();
@@ -560,30 +535,11 @@
   function confirmForm() {
     data.setGroup === 'create' ? createGroupFunction() : editGroupFunction();
   }
+
   function createGroupFunction() {
     ruleForm.value.validate((valid) => {
       if (valid) {
-        const dataList = {
-          group: {
-            metadata: {
-              group: '',
-              name: data.groupForm.name,
-            },
-            catalog: data.groupForm.catalog,
-            resourceOpts: {
-              shardNum: data.groupForm.shardNum,
-              segmentInterval: {
-                unit: data.groupForm.segmentIntervalUnit,
-                num: data.groupForm.segmentIntervalNum,
-              },
-              ttl: {
-                unit: data.groupForm.ttlUnit,
-                num: data.groupForm.ttlNum,
-              },
-            },
-          },
-        };
-        createGroup(dataList)
+        createGroup(getGroupForm.value)
           .then((res) => {
             if (res.status === 200) {
               getGroupLists();
@@ -601,30 +557,10 @@
     });
   }
   function editGroupFunction() {
-    const name = data.groupLists[data.clickIndex].metadata.name;
+    const name = currentNode.value.name;
     ruleForm.value.validate((valid) => {
       if (valid) {
-        const dataList = {
-          group: {
-            metadata: {
-              group: '',
-              name: data.groupForm.name,
-            },
-            catalog: data.groupForm.catalog,
-            resourceOpts: {
-              shardNum: data.groupForm.shardNum,
-              segmentInterval: {
-                unit: data.groupForm.segmentIntervalUnit,
-                num: data.groupForm.segmentIntervalNum,
-              },
-              ttl: {
-                unit: data.groupForm.ttlUnit,
-                num: data.groupForm.ttlNum,
-              },
-            },
-          },
-        };
-        editGroup(name, dataList)
+        editGroup(name, getGroupForm.value)
           .then((res) => {
             if (res.status === 200) {
               getGroupLists();
@@ -644,15 +580,14 @@
   // init form data
   function clearGroupForm() {
     data.dialogGroupVisible = false;
-    data.groupForm = {
-      name: null,
-      catalog: 'CATALOG_STREAM',
-      shardNum: 1,
-      segmentIntervalUnit: 'UNIT_DAY',
-      segmentIntervalNum: 1,
-      ttlUnit: 'UNIT_DAY',
-      ttlNum: 3,
-    };
+    groupForm.name = '';
+    groupForm.catalog = 'CATALOG_STREAM';
+    groupForm.shardNum = 1;
+    groupForm.segmentIntervalUnit = 'UNIT_DAY';
+    groupForm.segmentIntervalNum = 1;
+    groupForm.ttlUnit = 'UNIT_DAY';
+    groupForm.ttlNum = 3;
+    groupForm.stages = [];
   }
   function initActiveNode() {
     const { group, name, type } = route.params;
@@ -690,15 +625,29 @@
     document.removeEventListener('mouseup', onMouseUp);
   }
 
-  getGroupLists();
+  const openAddStage = () => {
+    stageEditorRef.value.openDialog().then((res) => {
+      groupForm.stages.push(res);
+    });
+  };
 
-  $bus.on('resetAside', (data) => {
+  const openEditStage = (index) => {
+    stageEditorRef.value.openDialog(groupForm.stages[index]).then((res) => {
+      groupForm.stages[index] = res;
+    });
+  };
+
+  const deleteStage = (index) => {
+    groupForm.stages.splice(index, 1);
+  };
+
+  $bus.on('resetAside', () => {
     router.push({
       name: `${props.type}Start`,
     });
   });
 
-  $bus.on('refreshAside', (data) => {
+  $bus.on('refreshAside', () => {
     getGroupLists();
   });
   watch(filterText, (val) => {
@@ -725,7 +674,7 @@
           ref="treeRef"
           v-loading="loading"
           :data="data.groupLists"
-          :props="defaultProps"
+          :props="DefaultProps"
           :filter-node-method="filterNode"
           @node-click="viewResources"
           @node-contextmenu="openOperationMenus"
@@ -752,43 +701,67 @@
       <div class="resizer" @mousedown="mouseDown"></div>
     </div>
     <el-dialog
-      width="25%"
+      width="1100px"
       center
       :title="`${data.setGroup} group`"
       v-model="data.dialogGroupVisible"
       :show-close="false"
+      :destroy-on-close="true"
     >
-      <el-form ref="ruleForm" :rules="rules" :model="data.groupForm" 
label-position="left">
-        <el-form-item label="group name" :label-width="data.formLabelWidth" 
prop="name">
-          <el-input :disabled="data.setGroup === 'edit'" 
v-model="data.groupForm.name" autocomplete="off"> </el-input>
+      <el-form ref="ruleForm" :rules="Rules" :model="groupForm" 
label-position="left">
+        <el-form-item label="Group name" :label-width="data.formLabelWidth" 
prop="name">
+          <el-input :disabled="data.setGroup === 'edit'" 
v-model="groupForm.name" autocomplete="off"> </el-input>
         </el-form-item>
-        <el-form-item label="group type" :label-width="data.formLabelWidth" 
prop="catalog">
-          <el-select v-model="data.groupForm.catalog" placeholder="please 
select" style="width: 100%">
+        <el-form-item label="Group type" :label-width="data.formLabelWidth" 
prop="catalog">
+          <el-select v-model="groupForm.catalog" placeholder="please select" 
style="width: 100%">
             <el-option label="Stream" value="CATALOG_STREAM"></el-option>
             <el-option label="Measure" value="CATALOG_MEASURE"></el-option>
             <el-option label="Property" value="CATALOG_PROPERTY"></el-option>
           </el-select>
         </el-form-item>
-        <el-form-item label="shard num" :label-width="data.formLabelWidth" 
prop="shardNum">
-          <el-input-number v-model="data.groupForm.shardNum" :min="1" />
+        <el-form-item label="Shard num" :label-width="data.formLabelWidth" 
prop="shardNum">
+          <el-input-number v-model="groupForm.shardNum" :min="1" />
         </el-form-item>
-        <el-form-item label="segment interval unit" 
:label-width="data.formLabelWidth" prop="segmentIntervalUnit">
-          <el-select v-model="data.groupForm.segmentIntervalUnit" 
placeholder="please select" style="width: 100%">
+        <el-form-item label="Segment interval unit" 
:label-width="data.formLabelWidth" prop="segmentIntervalUnit">
+          <el-select v-model="groupForm.segmentIntervalUnit" 
placeholder="please select" style="width: 100%">
             <el-option label="Hour" value="UNIT_HOUR"></el-option>
             <el-option label="Day" value="UNIT_DAY"></el-option>
           </el-select>
         </el-form-item>
-        <el-form-item label="segment interval num" 
:label-width="data.formLabelWidth" prop="segmentIntervalNum">
-          <el-input-number v-model="data.groupForm.segmentIntervalNum" 
:min="1" />
+        <el-form-item label="Segment interval num" 
:label-width="data.formLabelWidth" prop="segmentIntervalNum">
+          <el-input-number v-model="groupForm.segmentIntervalNum" :min="1" />
         </el-form-item>
-        <el-form-item label="ttl unit" :label-width="data.formLabelWidth" 
prop="ttlUnit">
-          <el-select v-model="data.groupForm.ttlUnit" placeholder="please 
select" style="width: 100%">
+        <el-form-item label="TTL unit" :label-width="data.formLabelWidth" 
prop="ttlUnit">
+          <el-select v-model="groupForm.ttlUnit" placeholder="please select" 
style="width: 100%">
             <el-option label="Hour" value="UNIT_HOUR"></el-option>
             <el-option label="Day" value="UNIT_DAY"></el-option>
           </el-select>
         </el-form-item>
-        <el-form-item label="ttl num" :label-width="data.formLabelWidth" 
prop="ttlNum">
-          <el-input-number v-model="data.groupForm.ttlNum" :min="1" />
+        <el-form-item label="TTL num" :label-width="data.formLabelWidth" 
prop="ttlNum">
+          <el-input-number v-model="groupForm.ttlNum" :min="1" />
+        </el-form-item>
+        <el-form-item label="Stages" :label-width="data.formLabelWidth" 
prop="stages">
+          <el-button size="small" type="primary" color="#6E38F7" 
@click="openAddStage">Add Stage</el-button>
+          <el-table style="margin-top: 10px" :data="groupForm.stages" border>
+            <el-table-column v-for="fiels in StageFields" :label="fiels.label" 
:prop="fiels.key" :key="fiels.key" />
+            <el-table-column label="Operator" width="150">
+              <template #default="scope">
+                <el-button
+                  link
+                  type="primary"
+                  @click.prevent="openEditStage(scope.$index)"
+                  style="color: var(--color-main); font-weight: bold"
+                >
+                  Edit
+                </el-button>
+                <el-popconfirm @confirm="deleteStage(scope.$index)" title="Are 
you sure to delete this?">
+                  <template #reference>
+                    <el-button link type="danger" style="color: red; 
font-weight: bold">Delete</el-button>
+                  </template>
+                </el-popconfirm>
+              </template>
+            </el-table-column>
+          </el-table>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer footer">
@@ -804,6 +777,7 @@
       <div v-for="m in data.operationMenus" @click="m.fn">{{ m.label }}</div>
     </div>
   </div>
+  <StageEditor ref="stageEditorRef" />
 </template>
 
 <style lang="scss" scoped>

Reply via email to