You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2023/09/12 12:46:05 UTC

[skywalking-banyandb] branch main updated: Add Property CRUD Function (#328)

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 b034d20e Add Property CRUD Function (#328)
b034d20e is described below

commit b034d20e00e77a26116a8852cd847f2a771cdbf1
Author: Wu ChuSheng <a1...@gmail.com>
AuthorDate: Tue Sep 12 20:45:59 2023 +0800

    Add Property CRUD Function (#328)
    
    * Add Property Aside
    * Add Property Table
    * Add Property CRUD
    
    ---------
    
    Co-authored-by: Gao Hongtao <ha...@gmail.com>
---
 ui/src/api/index.js                            |  36 ++++
 ui/src/components/Aside/index.vue              |  79 ++++++++-
 ui/src/components/Editor/index.vue             |   6 +-
 ui/src/components/IndexRule/Editor.vue         |   4 +-
 ui/src/components/IndexRule/index.vue          |   4 +-
 ui/src/components/IndexRuleBinding/Editor.vue  |   4 +-
 ui/src/components/IndexRuleBinding/index.vue   |   4 +-
 ui/src/components/MeasureEditor/index.vue      |   6 +-
 ui/src/components/Property/PropertyEditror.vue | 230 +++++++++++++++++++++++++
 ui/src/components/Property/PropertyRead.vue    | 189 ++++++++++++++++++++
 ui/src/components/Property/TagEditor.vue       |  95 ++++++++++
 ui/src/components/Read/index.vue               |   6 +-
 ui/src/components/TopNav/index.vue             |  50 +++---
 ui/src/router/index.js                         |  18 +-
 ui/src/views/Property/index.vue                |  40 ++++-
 15 files changed, 711 insertions(+), 60 deletions(-)

diff --git a/ui/src/api/index.js b/ui/src/api/index.js
index 059782c9..c491405c 100644
--- a/ui/src/api/index.js
+++ b/ui/src/api/index.js
@@ -139,4 +139,40 @@ export function deleteIndexRuleOrIndexRuleBinding(type, group, name) {
         url: `/api/v1/${type}/schema/${group}/${name}`,
         method: 'delete'
     })
+}
+
+export function getPropertyByGroup(group) {
+    return request({
+        url: `/api/v1/property/lists/${group}`,
+        method: 'get'
+    })
+}
+
+export function getPropertyList(group, name, ids, tags) {
+    return request({
+        url: `/api/v1/property/lists/${group}/${name}/${ids}/${tags}`,
+        method: 'get'
+    })
+}
+
+export function getPropertyDetail(group, name, id, tags) {
+    return request({
+        url: `/api/v1/property/${group}/${name}/${id}/${tags}`,
+        method: 'get'
+    })
+}
+
+export function deleteProperty(group, name, id, tags) {
+    return request({
+        url: `/api/v1/property/${group}/${name}/${id}/${tags}`,
+        method: 'delete'
+    })
+}
+
+export function applyProperty(group, name, id, data) {
+    return request({
+        url: `/api/v1/property/${group}/${name}/${id}`,
+        method: 'put',
+        data: data
+    })
 }
\ No newline at end of file
diff --git a/ui/src/components/Aside/index.vue b/ui/src/components/Aside/index.vue
index 581e1c44..afd3c9e8 100644
--- a/ui/src/components/Aside/index.vue
+++ b/ui/src/components/Aside/index.vue
@@ -19,12 +19,13 @@
 
 <script setup>
 import RigheMenu from '@/components/RightMenu/index.vue'
-import { deleteIndexRuleOrIndexRuleBinding, getindexRuleList, getindexRuleBindingList, getGroupList, getStreamOrMeasureList, deleteStreamOrMeasure, deleteGroup, createGroup, editGroup, createResources } from '@/api/index'
+import { deleteIndexRuleOrIndexRuleBinding, getindexRuleList, getindexRuleBindingList, getGroupList, getStreamOrMeasureList, deleteStreamOrMeasure, deleteGroup, createGroup, editGroup } from '@/api/index'
 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 { Search } from '@element-plus/icons-vue'
+
 const router = useRouter()
 const route = useRoute()
 const { proxy } = getCurrentInstance()
@@ -269,6 +270,11 @@ function getGroupLists() {
                 let group = res.data.group
                 data.groupLists = group
                 deleteOtherGroup()
+                if (props.type == 'property') {
+                    data.showSearch = true
+                    $loadingClose()
+                    return
+                }
                 let promise = data.groupLists.map((item) => {
                     let type = props.type
                     let name = item.metadata.name
@@ -335,8 +341,13 @@ function getGroupLists() {
         })
 }
 function deleteOtherGroup() {
+    let flag = {
+        'CATALOG_MEASURE': 'measure',
+        'CATALOG_STREAM': 'stream',
+        'CATALOG_UNSPECIFIED': 'property'
+    }
     for (let i = 0; i < data.groupLists.length; i++) {
-        let type = data.groupLists[i].catalog == 'CATALOG_MEASURE' ? 'measure' : 'stream'
+        let type = flag[data.groupLists[i].catalog]
         if (type !== props.type) {
             data.groupLists.splice(i, 1)
             i--
@@ -345,6 +356,25 @@ function deleteOtherGroup() {
 }
 // to resources
 function openResources(index, childIndex) {
+    if (props.type == 'property') {
+        const group = data.groupLists[index].metadata.name
+        const route = {
+            name: 'property',
+            params: {
+                group: group,
+                operator: 'read',
+                type: props.type
+            }
+        }
+        router.push(route)
+        const add = {
+            label: group,
+            type: 'Read',
+            route
+        }
+        data.active = `${group}`
+        return $bus.emit('AddTabs', add)
+    }
     const group = data.groupLists[index].children[childIndex].metadata.group
     const name = data.groupLists[index].children[childIndex].metadata.name
     const route = {
@@ -403,11 +433,13 @@ function shrinkDown(e) {
 function rightClickGroup(e, index) {
     data.rightMenuList = groupMenu
     if (props.type == 'measure') {
-        data.rightMenuList.push({
+        const rightMenuList = JSON.parse(JSON.stringify(groupMenu))
+        rightMenuList.push({
             icon: "el-icon-document",
             name: "new resources",
             id: "create resources"
         })
+        data.rightMenuList = rightMenuList
     }
     data.clickIndex = index
     data.rightClickType = 'group'
@@ -815,7 +847,10 @@ $bus.on('changeIsCollapse', (obj) => {
     emit('setWidth', obj.width)
 })
 $bus.on('changeAside', (obj) => {
-    data.activeMenu = `${obj.group}-${obj.name}`
+    if (obj.group && obj.name)
+        data.activeMenu = `${obj.group}-${obj.name}`
+    else
+        data.activeMenu = `${obj.group}`
 })
 $bus.on('resetAside', () => {
     data.activeMenu = ''
@@ -826,7 +861,6 @@ $bus.on('resetAside', () => {
 $bus.on('refreshAside', () => {
     getGroupLists()
 })
-
 getGroupLists()
 initActiveMenu()
 
@@ -836,12 +870,23 @@ initActiveMenu()
 <template>
     <div style="display: flex; flex-direction: column; width: 100%;">
         <div class="size flex" style="display: flex; flex-direction: column; width: 100%;">
-            <el-input v-if="data.showSearch && props.type == 'measure'" class="aside-search" v-model="data.search"
+            <el-input v-if="data.showSearch && props.type != 'stream'" class="aside-search" v-model="data.search"
                 placeholder="Search" :prefix-icon="Search" clearable />
             <el-menu v-if="data.groupLists.length > 0" :collapse="data.isCollapse" :default-active="data.activeMenu">
                 <div v-for="(item, index) in data.groupLists" :key="item.metadata.name"
                     @contextmenu.prevent="rightClickGroup($event, index)">
-                    <el-sub-menu :index="`${item.metadata.name}-${index}`">
+                    <el-menu-item v-if="props.type == 'property'" @click="openResources(index)"
+                        :index="`${item.metadata.name}`">
+                        <template #title>
+                            <el-icon>
+                                <Document />
+                            </el-icon>
+                            <span slot="title" :title="item.metadata.name" style="width: 70%" class="text-overflow-hidden">
+                                {{ item.metadata.name }}
+                            </span>
+                        </template>
+                    </el-menu-item>
+                    <el-sub-menu v-else :index="`${item.metadata.name}-${index}`">
                         <template #title>
                             <el-icon>
                                 <Folder />
@@ -952,6 +997,25 @@ initActiveMenu()
                                 </div>
                             </div>
                         </div>
+                        <!-- <div v-if="props.type == 'property'">
+                            <div v-for="(child, childIndex) in item.children" :key="child.metadata.id">
+                                <div @contextmenu.prevent="rightClickResources($event, index, childIndex)">
+                                    <el-menu-item
+                                        :index="`${child.metadata.container.group}-${child.metadata.container.name}`"
+                                        @click="openResources(index, childIndex)">
+                                        <template #title>
+                                            <el-icon>
+                                                <Document />
+                                            </el-icon>
+                                            <span slot="title" :title="child.metadata.container.name" style="width: 90%"
+                                                class="text-overflow-hidden">
+                                                {{ child.metadata.container.name }}
+                                            </span>
+                                        </template>
+                                    </el-menu-item>
+                                </div>
+                            </div>
+                        </div> -->
                     </el-sub-menu>
                 </div>
             </el-menu>
@@ -974,6 +1038,7 @@ initActiveMenu()
                     <el-select v-model="data.groupForm.catalog" placeholder="please select" style="width: 100%">
                         <el-option label="CATALOG_STREAM" value="CATALOG_STREAM"></el-option>
                         <el-option label="CATALOG_MEASURE" value="CATALOG_MEASURE"></el-option>
+                        <el-option label="CATALOG_UNSPECIFIED" value="CATALOG_UNSPECIFIED"></el-option>
                     </el-select>
                 </el-form-item>
                 <el-form-item label="shard num" :label-width="data.formLabelWidth" prop="shardNum">
diff --git a/ui/src/components/Editor/index.vue b/ui/src/components/Editor/index.vue
index ef91ed46..5c4851a3 100644
--- a/ui/src/components/Editor/index.vue
+++ b/ui/src/components/Editor/index.vue
@@ -321,9 +321,7 @@ function initData() {
 </template>
 
 <style lang="scss" scoped>
-::v-deep {
-    .el-card {
-        margin: 15px;
-    }
+:deep(.el-card) {
+    margin: 15px;
 }
 </style>
\ No newline at end of file
diff --git a/ui/src/components/IndexRule/Editor.vue b/ui/src/components/IndexRule/Editor.vue
index 4e4cef35..5e0a575f 100644
--- a/ui/src/components/IndexRule/Editor.vue
+++ b/ui/src/components/IndexRule/Editor.vue
@@ -324,9 +324,7 @@ function initData() {
 </template>
 
 <style lang="scss" scoped>
-::v-deep {
-  .el-card {
+:deep(.el-card) {
     margin: 15px;
-  }
 }
 </style>
\ No newline at end of file
diff --git a/ui/src/components/IndexRule/index.vue b/ui/src/components/IndexRule/index.vue
index ce597f32..ae66b8c1 100644
--- a/ui/src/components/IndexRule/index.vue
+++ b/ui/src/components/IndexRule/index.vue
@@ -103,9 +103,7 @@ function initData() {
 </template>
 
 <style lang="scss" scoped>
-::v-deep {
-  .el-card {
+:deep(.el-card) {
     margin: 15px;
-  }
 }
 </style>
\ No newline at end of file
diff --git a/ui/src/components/IndexRuleBinding/Editor.vue b/ui/src/components/IndexRuleBinding/Editor.vue
index cb6b2029..b8ce515d 100644
--- a/ui/src/components/IndexRuleBinding/Editor.vue
+++ b/ui/src/components/IndexRuleBinding/Editor.vue
@@ -256,9 +256,7 @@ function initData() {
 </template>
 
 <style lang="scss" scoped>
-::v-deep {
-  .el-card {
+:deep(.el-card) {
     margin: 15px;
-  }
 }
 </style>
\ No newline at end of file
diff --git a/ui/src/components/IndexRuleBinding/index.vue b/ui/src/components/IndexRuleBinding/index.vue
index af0e1922..366aa52d 100644
--- a/ui/src/components/IndexRuleBinding/index.vue
+++ b/ui/src/components/IndexRuleBinding/index.vue
@@ -101,9 +101,7 @@ function initData() {
 </template>
 
 <style lang="scss" scoped>
-::v-deep {
-  .el-card {
+:deep(.el-card) {
     margin: 15px;
-  }
 }
 </style>
\ No newline at end of file
diff --git a/ui/src/components/MeasureEditor/index.vue b/ui/src/components/MeasureEditor/index.vue
index 3e9173c1..d1efae64 100644
--- a/ui/src/components/MeasureEditor/index.vue
+++ b/ui/src/components/MeasureEditor/index.vue
@@ -30,9 +30,7 @@
 </template>
 
 <style lang="scss"  scoped>
-::v-deep {
-    .el-card {
-        margin: 15px;
-    }
+:deep(.el-card) {
+    margin: 15px;
 }
 </style>
\ No newline at end of file
diff --git a/ui/src/components/Property/PropertyEditror.vue b/ui/src/components/Property/PropertyEditror.vue
new file mode 100644
index 00000000..72382096
--- /dev/null
+++ b/ui/src/components/Property/PropertyEditror.vue
@@ -0,0 +1,230 @@
+<!--
+  ~ 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 { applyProperty } from '@/api';
+import { reactive, ref } from 'vue';
+import { getCurrentInstance } from '@vue/runtime-core'
+import TagEditor from './TagEditor.vue';
+import { ElMessage } from 'element-plus';
+const $loadingCreate = getCurrentInstance().appContext.config.globalProperties.$loadingCreate
+const $loadingClose = getCurrentInstance().appContext.config.globalProperties.$loadingClose
+const showDialog = ref(false)
+const title = ref('')
+const tagEditorRef = ref()
+const ruleForm = ref()
+const formData = reactive({
+    strategy: 'STRATEGY_UNSPECIFIED',
+    group: '',
+    name: '',
+    containerID: '',
+    modRevision: 0,
+    createRevision: 0,
+    id: '',
+    tags: []
+})
+const validateTags = (rule, value, callback) => {
+    if (value.length <= 0) {
+        callback(new Error('Please add the tags'))
+    } else {
+        callback()
+    }
+}
+const rules = {
+    strategy: [{ required: true, message: 'please select the apply method.', trigger: 'blur' }],
+    group: [{ required: true, message: 'Please enter the group.', trigger: 'blur' }],
+    name: [{ required: true, message: 'Please enter the name.', trigger: 'blur' }],
+    containerID: [{ required: true, message: 'Please enter the container id.', trigger: 'blur' }],
+    modRevision: [{ required: true, message: 'Please enter the container Mod Revision.', trigger: 'blur' }],
+    createRevision: [{ required: true, message: 'Please enter the container Create Revision.', trigger: 'blur' }],
+    id: [{ required: true, message: 'Please enter the ID.', trigger: 'blur' }],
+    tags: [{ required: true, validator: validateTags, trigger: 'blur' }],
+}
+const strategyGroup = [
+    { label: 'STRATEGY_UNSPECIFIED', value: 'STRATEGY_UNSPECIFIED' },
+    { label: 'STRATEGY_MERGE', value: 'STRATEGY_MERGE' },
+    { label: 'STRATEGY_REPLACE', value: 'STRATEGY_REPLACE' }
+]
+const formConfig = [
+    { label: 'Strategy', prop: 'strategy', type: 'select', selectGroup: strategyGroup },
+    { label: 'Container Group', prop: 'group', type: 'input', disabled: true },
+    { label: 'Container Name', prop: 'name', type: 'input' },
+    { label: 'Container ID', prop: 'containerID', type: 'input' },
+    { label: 'Container ModRevision', prop: 'modRevision', type: 'number' },
+    { label: 'Container CreateRevision', prop: 'createRevision', type: 'number' },
+    { label: 'ID', prop: 'id', type: 'input' },
+]
+let promiseResolve
+
+const initData = () => {
+    formData.strategy = 'STRATEGY_UNSPECIFIED'
+    formData.group = ''
+    formData.name = ''
+    formData.containerID = ''
+    formData.modRevision = 0
+    formData.createRevision = 0
+    formData.id = ''
+    formData.tags = []
+}
+const closeDialog = () => {
+    showDialog.value = false
+    initData()
+}
+const openEditTag = (index) => {
+    tagEditorRef.value.openDialog(formData.tags[index])
+        .then(res => {
+            formData.tags[index].key = res.key
+            formData.tags[index].value = res.value
+        })
+}
+const deleteTag = (index) => {
+    formData.tags.splice(index, 1)
+}
+const openAddTag = () => {
+    tagEditorRef.value.openDialog()
+        .then(res => {
+            formData.tags.push(res)
+        })
+}
+const confirmApply = async () => {
+    if (!ruleForm.value) return
+    await ruleForm.value.validate((valid) => {
+        if (valid) {
+            $loadingCreate()
+            const param = {
+                strategy: formData.strategy,
+                property: {
+                    metadata: {
+                        container: {
+                            createRevision: formData.createRevision,
+                            group: formData.group,
+                            id: formData.containerID,
+                            modRevision: formData.modRevision,
+                            name: formData.name
+                        },
+                        id: formData.id
+                    },
+                    tags: formData.tags.map(item => {
+                        return {
+                            key: item.key,
+                            value: JSON.parse(item.value)
+                        }
+                    })
+                }
+            }
+            applyProperty(formData.group, formData.name, formData.id, param)
+                .then(res => {
+                    if (res.status == 200) {
+                        ElMessage({
+                            message: 'successed',
+                            type: "success",
+                            duration: 5000
+                        })
+                        showDialog.value = false
+                        promiseResolve()
+                    }
+                })
+                .catch(err => {
+                    ElMessage({
+                        message: 'Please refresh and try again. Error: ' + err,
+                        type: "error",
+                        duration: 3000
+                    })
+                })
+                .finally(() => {
+                    $loadingClose()
+                })
+        }
+    })
+}
+const openDialog = (edit, data) => {
+    showDialog.value = true
+    if (edit === true) {
+        title.value = 'Edit Property'
+    } else {
+        title.value = 'Apply Property'
+    }
+    formData.group = data?.group || ''
+    formData.name = data?.name || ''
+    formData.containerID = data?.containerID || ''
+    formData.modRevision = data?.modRevision || 0
+    formData.createRevision = data?.createRevision || 0
+    formData.id = data?.id || ''
+    formData.tags = JSON.parse(JSON.stringify(data?.tags || []))
+    return new Promise((resolve) => {
+        promiseResolve = resolve
+    })
+}
+defineExpose({
+    openDialog
+})
+</script>
+
+<template>
+    <el-dialog v-model="showDialog" :title="title" width="50%">
+        <el-form ref="ruleForm" :rules="rules" :model="formData" label-position="left">
+            <el-form-item v-for="item in formConfig" :key="item.prop" :label="item.label" :prop="item.prop"
+                label-width="200">
+                <el-select v-if="item.type === 'select'" v-model="formData[item.prop]" placeholder="please select"
+                    style="width: 100%;">
+                    <el-option v-for="option in item.selectGroup" :key="option.value" :label="option.label"
+                        :value="option.value"></el-option>
+                </el-select>
+                <el-input v-if="item.type === 'input'" v-model="formData[item.prop]" :disabled="item.disabled"
+                    autocomplete="off"></el-input>
+                <el-input-number v-if="item.type === 'number'" v-model="formData[item.prop]" :min="0"></el-input-number>
+            </el-form-item>
+            <el-form-item label="Tags" prop="tags" label-width="200">
+                <el-button size="small" type="primary" color="#6E38F7" @click="openAddTag">Add Tag</el-button>
+                <el-table style="margin-top: 10px;" :data="formData.tags" border>
+                    <el-table-column label="Key" prop="key"></el-table-column>
+                    <el-table-column label="Value" prop="value"></el-table-column>
+                    <el-table-column label="Operator" width="150">
+                        <template #default="scope">
+                            <el-button link type="primary" @click.prevent="openEditTag(scope.$index)"
+                                style="color: var(--color-main); font-weight: bold;">Edit</el-button>
+                            <el-popconfirm @confirm="deleteTag(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>
+        <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>
+    <TagEditor ref="tagEditorRef"></TagEditor>
+</template>
+
+<style lang="scss" scoped>
+.footer {
+    width: 100%;
+    display: flex;
+    justify-content: center;
+}
+</style>
\ No newline at end of file
diff --git a/ui/src/components/Property/PropertyRead.vue b/ui/src/components/Property/PropertyRead.vue
new file mode 100644
index 00000000..6142b10b
--- /dev/null
+++ b/ui/src/components/Property/PropertyRead.vue
@@ -0,0 +1,189 @@
+<!--
+  ~ 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 { getPropertyByGroup, deleteProperty } from '@/api/index';
+import { watch, getCurrentInstance } from "@vue/runtime-core";
+import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { onMounted, reactive, ref } from 'vue';
+import { RefreshRight } from '@element-plus/icons-vue';
+import PropertyEditror from './PropertyEditror.vue';
+
+const { proxy } = getCurrentInstance()
+// Loading
+const route = useRoute()
+const $bus = getCurrentInstance().appContext.config.globalProperties.mittBus
+const $loadingCreate = getCurrentInstance().appContext.config.globalProperties.$loadingCreate
+const $loadingClose = proxy.$loadingClose
+const propertyEditorRef = ref()
+const data = reactive({
+    group: "",
+    tableData: []
+})
+const getProperty = () => {
+    $loadingCreate()
+    const group = route.params.group
+    getPropertyByGroup(group)
+        .then(res => {
+            if (res.status == 200 && group == route.params.group) {
+                data.tableData = res.data.property.map(item => {
+                    item.tags.forEach(tag => {
+                        tag.value = JSON.stringify(tag.value)
+                    })
+                    return item
+                })
+            }
+        })
+        .catch((err) => {
+            ElMessage({
+                message: 'An error occurred while obtaining group data. Please refresh and try again. Error: ' + err,
+                type: "error",
+                duration: 3000
+            })
+        })
+        .finally(() => {
+            $loadingClose()
+        })
+}
+const openEditField = (index) => {
+    const item = data.tableData[index]
+    const param = {
+        group: item.metadata.container.group,
+        name: item.metadata.container.name,
+        containerID: item.metadata.container.id,
+        modRevision: item.metadata.container.modRevision,
+        createRevision: item.metadata.container.createRevision,
+        id: item.metadata.id,
+        tags: JSON.parse(JSON.stringify(item.tags))
+    }
+    propertyEditorRef?.value.openDialog(true, param)
+        .then(() => {
+            getProperty()
+        })
+}
+const openAddProperty = () => {
+    let dataForm = {
+        group: data.group
+    }
+    propertyEditorRef?.value.openDialog(false, dataForm)
+        .then(() => {
+            getProperty()
+        })
+}
+const deleteTableData = (index) => {
+    const item = data.tableData[index]
+    $loadingCreate()
+    deleteProperty(item.metadata.container.group, item.metadata.container.name, item.metadata.id, item.tags)
+        .then((res) => {
+            if (res.status == 200) {
+                ElMessage({
+                    message: 'successed',
+                    type: "success",
+                    duration: 5000
+                })
+                getProperty()
+            }
+        })
+        .catch(err => {
+            ElMessage({
+                message: 'Please refresh and try again. Error: ' + err,
+                type: "error",
+                duration: 3000
+            })
+        })
+        .finally(() => {
+            $loadingClose()
+        })
+}
+watch(() => route, () => {
+    data.group = route.params.group
+    data.tableData = []
+    getProperty()
+}, {
+    deep: true,
+    immediate: true
+})
+onMounted(() => {
+    getProperty()
+})
+</script>
+<template>
+    <div>
+        <el-card shadow="always">
+            <template #header>
+                <div class="flex">
+                    <span class="text-bold">Group:</span>
+                    <span style="margin-right: 20px;">{{ data.group }}</span>
+                    <span class="text-bold">Operation:</span>
+                    <span>Read</span>
+                </div>
+            </template>
+            <div class="button-group-operator">
+                <el-button size="small" type="primary" color="#6E38F7" @click="openAddProperty">Apply
+                    Property</el-button>
+                <el-button size="small" :icon="RefreshRight" @click="getProperty" plain></el-button>
+            </div>
+
+            <el-table :data="data.tableData" style="width: 100%; margin-top: 20px;" border>
+                <el-table-column label="Container">
+                    <el-table-column label="Group" prop="metadata.container.group" width="100"></el-table-column>
+                    <el-table-column label="Name" prop="metadata.container.name" width="120"></el-table-column>
+                    <el-table-column label="ID" prop="metadata.container.id" width="100"></el-table-column>
+                    <el-table-column label="ModRevision" prop="metadata.container.modRevision"
+                        width="120"></el-table-column>
+                    <el-table-column label="CreateRevision" prop="metadata.container.createRevision"
+                        width="140"></el-table-column>
+                </el-table-column>
+                <el-table-column label="ID" prop="metadata.id" width="150"></el-table-column>
+                <el-table-column label="Tags">
+                    <template #default="scope">
+                        <el-table :data="scope.row.tags">
+                            <el-table-column label="Key" prop="key" width="150"></el-table-column>
+                            <el-table-column label="Value" prop="value"></el-table-column>
+                        </el-table>
+                    </template>
+                </el-table-column>
+                <el-table-column label="Operator" width="150">
+                    <template #default="scope">
+                        <el-button link type="primary" @click.prevent="openEditField(scope.$index)"
+                            style="color: var(--color-main); font-weight: bold;">Edit</el-button>
+                        <el-popconfirm @confirm="deleteTableData(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-card>
+        <PropertyEditror ref="propertyEditorRef"></PropertyEditror>
+    </div>
+</template>
+<style lang="scss" scoped>
+:deep(.el-card) {
+    margin: 15px;
+}
+
+.button-group-operator {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+}
+</style>
\ No newline at end of file
diff --git a/ui/src/components/Property/TagEditor.vue b/ui/src/components/Property/TagEditor.vue
new file mode 100644
index 00000000..66601711
--- /dev/null
+++ b/ui/src/components/Property/TagEditor.vue
@@ -0,0 +1,95 @@
+<!--
+  ~ 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 { reactive, ref } from 'vue'
+
+const showDialog = ref(false)
+//const editValue = ref(false)
+const ruleForm = ref()
+const title = ref('')
+const formData = reactive({
+    key: '',
+    value: ''
+})
+let promiseResolve
+const initData = () => {
+    formData.key = ''
+    formData.value = ''
+}
+const closeDialog = () => {
+    initData()
+    showDialog.value = false
+}
+const confirmApply = async () => {
+    if (!ruleForm.value) return
+    await ruleForm.value.validate((valid) => {
+        if (valid) {
+            promiseResolve(JSON.parse(JSON.stringify(formData)))
+            initData()
+            showDialog.value = false
+        }
+    })
+}
+const openDialog = (data) => {
+    if (data) {
+        formData.key = data.key
+        formData.value = data.value
+        title.value = 'Edit Tag'
+    } else {
+        title.value = 'Add Tag'
+    }
+    showDialog.value = true
+    return new Promise((resolve) => {
+        promiseResolve = resolve
+    })
+}
+defineExpose({
+    openDialog
+})
+</script>
+
+<template>
+    <el-dialog v-model="showDialog" :title="title" width="30%">
+        <el-form ref="ruleForm" :model="formData" label-position="left">
+            <el-form-item label="Key" prop="key" required label-width="150">
+                <el-input v-model="formData.key" autocomplete="off"></el-input>
+            </el-form-item>
+            <el-form-item label="Value" prop="value" required label-width="150">
+                <el-input v-model="formData.value" type="textarea" autocomplete="off"></el-input>
+            </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>
\ No newline at end of file
diff --git a/ui/src/components/Read/index.vue b/ui/src/components/Read/index.vue
index 8c0376c4..a21f6d7a 100644
--- a/ui/src/components/Read/index.vue
+++ b/ui/src/components/Read/index.vue
@@ -437,9 +437,7 @@ function changeFields() {
 </template>
 
 <style lang="scss" scoped>
-::v-deep {
-    .el-card {
-        margin: 15px;
-    }
+:deep(.el-card) {
+    margin: 15px;
 }
 </style>
\ No newline at end of file
diff --git a/ui/src/components/TopNav/index.vue b/ui/src/components/TopNav/index.vue
index 4ed5db9c..c24d2208 100644
--- a/ui/src/components/TopNav/index.vue
+++ b/ui/src/components/TopNav/index.vue
@@ -33,9 +33,8 @@ const data = reactive({
 function tabClick() {
     router.push(data.tabsList[data.activeTabIndex].route)
     const group = data.tabsList[data.activeTabIndex].route.params.group
-    const name = data.tabsList[data.activeTabIndex].route.params.name
-
-    if (group && name) {
+    const name = data.tabsList[data.activeTabIndex].route.params.name ? data.tabsList[data.activeTabIndex].route.params.name : null
+    if (group) {
         $bus.emit('changeAside', { group, name })
     }
 }
@@ -67,7 +66,7 @@ function initData() {
     const add = { route: routeData }
     if (operator == 'read') {
         routeData.name = type
-        add.label = name
+        add.label = type == 'property' ? group : name
         add.type = type == 'index-rule' || type == 'index-rule-binding' ? `Read-${type}` : 'Read'
     } else if (operator == 'edit') {
         routeData.name = `edit-${type}`
@@ -79,6 +78,7 @@ function initData() {
         add.type = type == 'index-rule' || type == 'index-rule-binding' ? `Create-${type}` : 'Create'
     }
     data.tabsList.push(add)
+    $bus.emit('changeAside', { group, name })
 }
 $bus.on('AddTabs', (tab) => {
     const index = data.tabsList.findIndex(item => {
@@ -133,30 +133,28 @@ initData()
     box-shadow: 0 0 10px 0 var(--color-placeholder-font);
 }
 
-::v-deep {
-    .el-tabs--card>.el-tabs__header .el-tabs__nav {
-        border-top: none !important;
-        padding: 0 !important;
-    }
+:deep(.el-tabs--card>.el-tabs__header .el-tabs__nav) {
+    border-top: none !important;
+    padding: 0 !important;
+}
 
-    .el-tabs__header.is-top {
-        padding: 0 !important;
-        margin: 0 !important;
-    }
+:deep(.el-tabs__header.is-top) {
+    padding: 0 !important;
+    margin: 0 !important;
+}
 
-    .el-tabs__item.is-active {
-        color: var(--color-main) !important;
-        background-color: var(--color-main-background) !important;
-        border: 1px solid var(--color-main) !important;
-        border-bottom: none !important;
-        border-top-left-radius: 4px;
-        border-top-right-radius: 4px;
-    }
+:deep(.el-tabs__item.is-active) {
+    color: var(--color-main) !important;
+    background-color: var(--color-main-background) !important;
+    border: 1px solid var(--color-main) !important;
+    border-bottom: none !important;
+    border-top-left-radius: 4px;
+    border-top-right-radius: 4px;
+}
 
-    .el-tabs__content {
-        padding: 0 !important;
-        margin: 0 !important;
-        height: 0 !important;
-    }
+:deep(.el-tabs__content) {
+    padding: 0 !important;
+    margin: 0 !important;
+    height: 0 !important;
 }
 </style>
\ No newline at end of file
diff --git a/ui/src/router/index.js b/ui/src/router/index.js
index 79e417c7..0b9f104b 100644
--- a/ui/src/router/index.js
+++ b/ui/src/router/index.js
@@ -137,7 +137,23 @@ const router = createRouter({
         {
           path: '/banyandb/property',
           name: 'Property',
-          component: () => import('@/views/Property/index.vue')
+          redirect: '/banyandb/property/start',
+          component: () => import('@/views/Property/index.vue'),
+          children: [
+            {
+              path: '/banyandb/property/start',
+              name: 'propertyStart',
+              component: () => import('@/components/Start/index.vue'),
+              meta: {
+                type: 'property'
+              }
+            },
+            {
+              path: '/banyandb/property/operator-read/:type/:operator/:group',
+              name: 'property',
+              component: () => import('@/components/Property/PropertyRead.vue')
+            }
+          ]
         }
       ]
     },
diff --git a/ui/src/views/Property/index.vue b/ui/src/views/Property/index.vue
index 4c303543..4f66dcb8 100644
--- a/ui/src/views/Property/index.vue
+++ b/ui/src/views/Property/index.vue
@@ -18,13 +18,49 @@
 -->
 
 <script setup>
+import Aside from '@/components/Aside/index.vue'
+import TopNav from '@/components/TopNav/index.vue'
+import { reactive } from 'vue';
+
+const data = reactive({
+  width: '200px'
+})
+
+function setWidth(width) {
+  data.width = width
+}
 </script>
 
 <template>
-    <div class="about">
-    <h1>This is the property page</h1>
+  <div class="size">
+    <el-container>
+      <el-aside :width="data.width" class="bd-top flex" style="position:relative; background-color: white;">
+        <Aside type="property" @setWidth="setWidth"></Aside>
+      </el-aside>
+      <el-main>
+        <TopNav></TopNav>
+        <RouterView></RouterView>
+      </el-main>
+    </el-container>
   </div>
 </template>
 
 <style lang="scss" scoped>
+.el-container {
+  height: 100%;
+  padding: 0;
+  margin: 0;
+
+  .el-header {
+    background-color: #ffffff;
+    padding: 0;
+    margin: 0;
+  }
+
+  .el-main {
+    background-color: #eaedf1;
+    padding: 0;
+    margin: 0;
+  }
+}
 </style>
\ No newline at end of file