You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@inlong.apache.org by do...@apache.org on 2022/10/27 03:08:41 UTC

[inlong] branch master updated: [INLONG-6273][Dashboard] Ability to load plugins asynchronously (#6295)

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

dockerzhang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/inlong.git


The following commit(s) were added to refs/heads/master by this push:
     new d9b633eed [INLONG-6273][Dashboard] Ability to load plugins asynchronously (#6295)
d9b633eed is described below

commit d9b633eedd393e84bedcf5e67e994f91349c10f7
Author: Daniel <le...@apache.org>
AuthorDate: Thu Oct 27 11:08:37 2022 +0800

    [INLONG-6273][Dashboard] Ability to load plugins asynchronously (#6295)
---
 .../{consume/extends/index.tsx => DataStatic.ts}   |  18 +-
 inlong-dashboard/src/metas/DataWithBackend.ts      | 122 ++++++++++
 inlong-dashboard/src/metas/clusters/agent.tsx      |  20 --
 .../{index.tsx => common/ClusterDefaultInfo.ts}    | 128 +++++-----
 .../index.tsx => clusters/common/ClusterInfo.ts}   |   8 +-
 .../index.tsx => clusters/defaults/Agent.ts}       |   7 +-
 .../index.tsx => clusters/defaults/DataProxy.ts}   |   7 +-
 .../clusters/{pulsar.tsx => defaults/Pulsar.ts}    |  44 ++--
 .../clusters/{tubeMQ.tsx => defaults/TubeMq.ts}    |  35 +--
 .../clusters/defaults/index.ts}                    |  36 ++-
 .../index.tsx => clusters/extends/index.ts}        |   6 +-
 .../extends/index.tsx => clusters/index.ts}        |   9 +-
 inlong-dashboard/src/metas/consume/index.tsx       | 266 ---------------------
 .../metas/consumes/common/ConsumeDefaultInfo.ts    | 192 +++++++++++++++
 .../index.tsx => consumes/common/ConsumeInfo.ts}   |   8 +-
 .../metas/{consume => consumes/common}/status.tsx  |   0
 .../src/metas/consumes/defaults/Pulsar.ts          |  83 +++++++
 .../tubeMQ.tsx => consumes/defaults/TubeMq.ts}     |  56 ++---
 .../consumes/defaults/index.ts}                    |  26 +-
 .../index.tsx => consumes/extends/index.ts}        |   6 +-
 .../extends/index.tsx => consumes/index.ts}        |   9 +-
 .../src/metas/groups/common/GroupDefaultInfo.ts    | 124 ++++++++++
 .../dataProxy.tsx => groups/common/GroupInfo.ts}   |   6 +-
 .../src/metas/{group => groups/common}/status.tsx  |   0
 .../{group/index.tsx => groups/defaults/Pulsar.ts} | 239 ++++--------------
 .../src/metas/groups/defaults/TubeMq.ts            |  74 ++++++
 .../config.tsx => metas/groups/defaults/index.ts}  |  26 +-
 .../extends/index.tsx => groups/extends/index.ts}  |   6 +-
 .../{consume/extends/index.tsx => groups/index.ts} |   9 +-
 inlong-dashboard/src/metas/index.ts                |  82 +++++++
 .../src/metas/nodes/common/NodeDefaultInfo.ts      |  89 +++++++
 .../extends/index.tsx => nodes/common/NodeInfo.ts} |   8 +-
 inlong-dashboard/src/metas/nodes/common/dao.ts     |  93 +++++++
 .../src/metas/nodes/{hive.tsx => defaults/Hive.ts} |  35 +--
 .../extends/index.tsx => nodes/defaults/index.ts}  |  15 +-
 .../extends/index.tsx => nodes/extends/index.ts}   |   6 +-
 .../{consume/extends/index.tsx => nodes/index.ts}  |  11 +-
 inlong-dashboard/src/metas/nodes/index.tsx         |  87 -------
 .../src/metas/sources/common/SourceDefaultInfo.ts  | 118 +++++++++
 .../index.tsx => sources/common/SourceInfo.ts}     |   8 +-
 .../index.tsx => sources/defaults/AutoPush.ts}     |   7 +-
 .../metas/sources/{file.ts => defaults/File.ts}    |  39 +--
 .../sources/{binLog.ts => defaults/MysqlBinlog.ts} |  94 ++++----
 .../config.tsx => metas/sources/defaults/index.ts} |  32 ++-
 .../extends/index.tsx => sources/extends/index.ts} |   6 +-
 inlong-dashboard/src/metas/sources/index.ts        |  83 +------
 .../common/StreamDefaultInfo.ts}                   | 127 +++++-----
 .../autoPush.ts => streams/common/StreamInfo.ts}   |   6 +-
 .../metas/{stream => streams/common}/status.tsx    |   0
 .../index.tsx => streams/defaults/index.ts}        |  10 +-
 .../extends/index.tsx => streams/extends/index.ts} |   6 +-
 .../extends/index.tsx => streams/index.ts}         |   9 +-
 .../metas/{consume/extends/index.tsx => types.ts}  |  32 ++-
 .../src/pages/Clusters/CreateModal.tsx             |  15 +-
 inlong-dashboard/src/pages/Clusters/index.tsx      |  55 +++--
 .../src/pages/ConsumeDashboard/config.tsx          | 107 +++++----
 .../src/pages/ConsumeDashboard/index.tsx           |   6 +-
 .../src/pages/ConsumeDetail/Info/config.tsx        |   8 +-
 .../src/pages/ConsumeDetail/Info/index.tsx         |  22 +-
 .../src/pages/GroupDashboard/config.tsx            | 104 ++++----
 .../src/pages/GroupDashboard/index.tsx             |   6 +-
 .../pages/GroupDetail/DataSources/DetailModal.tsx  |  79 ++----
 .../src/pages/GroupDetail/DataSources/index.tsx    | 119 +++++----
 .../GroupDetail/DataStream/StreamItemModal.tsx     |  93 +++++--
 .../src/pages/GroupDetail/DataStream/config.tsx    |  20 +-
 .../src/pages/GroupDetail/DataStream/index.tsx     |   8 +-
 .../src/pages/GroupDetail/Info/config.tsx          |   9 +-
 .../src/pages/GroupDetail/Info/index.tsx           |  28 ++-
 inlong-dashboard/src/pages/Nodes/DetailModal.tsx   |  66 +++--
 inlong-dashboard/src/pages/Nodes/index.tsx         | 128 +++++-----
 .../src/pages/ProcessDetail/Access.tsx             |   9 +-
 .../src/pages/ProcessDetail/AccessConfig.tsx       |  20 +-
 .../src/pages/ProcessDetail/Consume.tsx            |   7 +-
 .../src/pages/ProcessDetail/ConsumeConfig.tsx      |  11 +-
 inlong-dashboard/tsconfig.json                     |   3 +-
 75 files changed, 2023 insertions(+), 1473 deletions(-)

diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/DataStatic.ts
similarity index 65%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/DataStatic.ts
index 8a7c0bd8e..0059abdf8 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/DataStatic.ts
@@ -17,8 +17,18 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import i18n from '@/i18n';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export abstract class DataStatic {
+  static I18nMap: Record<string, unknown> = {};
+
+  static I18n(i18nkey: string): PropertyDecorator {
+    return (target: any, propertyKey: string) => {
+      const { I18nMap } = target.constructor;
+      target.constructor.I18nMap = {
+        ...I18nMap,
+        [propertyKey]: i18nkey.indexOf('.') !== -1 ? i18n.t(i18nkey) : i18nkey,
+      };
+    };
+  }
+}
diff --git a/inlong-dashboard/src/metas/DataWithBackend.ts b/inlong-dashboard/src/metas/DataWithBackend.ts
new file mode 100644
index 000000000..34cf70b39
--- /dev/null
+++ b/inlong-dashboard/src/metas/DataWithBackend.ts
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+import { ColumnType } from 'antd/es/table';
+import type { FieldItemType } from '@/metas/common';
+import { isDevelopEnv } from '@/utils';
+import { DataStatic } from './DataStatic';
+
+interface PositionObjectType extends Record<string, unknown> {
+  position: ['before' | 'after', string];
+}
+
+function sortListPosition(list: PositionObjectType[], primaryPositionKey = 'name') {
+  const output: Record<string, unknown>[] = [];
+  const notFoundPosMap: Record<string, PositionObjectType> = {};
+  const _list = [...list];
+  let loopCount = 0;
+  while (_list.length) {
+    loopCount++;
+    if (loopCount > 500) {
+      console.error(
+        '[Apache InLong Error] The number of loops of the sorting algorithm array has reached the maximum limit, please check or adjust the configuration.',
+      );
+      break;
+    }
+    const listItem = _list.shift();
+    if (!listItem) continue;
+    if (listItem.position) {
+      const [positionType, positionName] = listItem.position;
+      const index = output.findIndex(item => item[primaryPositionKey] === positionName);
+      if (index !== -1) {
+        output.splice(positionType === 'before' ? index : index + 1, 0, listItem);
+      } else {
+        notFoundPosMap[positionName] = listItem;
+      }
+    } else {
+      output.push(listItem);
+    }
+    const currentItemName = listItem[primaryPositionKey] as string;
+    if (notFoundPosMap[currentItemName]) {
+      _list.push(notFoundPosMap[currentItemName]);
+      delete notFoundPosMap[currentItemName];
+    }
+  }
+
+  const notFoundPosList = Object.keys(notFoundPosMap).map(name => notFoundPosMap[name]);
+  return output.concat(notFoundPosList);
+}
+
+export abstract class DataWithBackend extends DataStatic {
+  static FieldList: FieldItemType[] = [];
+  static ColumnList: ColumnType<Record<string, any>>[] = [];
+
+  static FormField(config: FieldItemType): PropertyDecorator {
+    return (target: any, propertyKey: string) => {
+      const { I18nMap, FieldList } = target.constructor;
+      const newFieldList = [...FieldList];
+
+      if (isDevelopEnv()) {
+        // Hot refresh of the development env will have old data
+        const existIndex = newFieldList.findIndex(item => item.name === propertyKey);
+        if (existIndex !== -1) {
+          newFieldList.splice(existIndex, 1);
+        }
+      }
+
+      newFieldList.push({
+        ...config,
+        name: propertyKey,
+        label: I18nMap[propertyKey],
+      });
+
+      // console.log('--', target.constructor, target.constructor.timer);
+      // if (target.constructor.timer) {
+      //   clearTimeout(target.constructor.timer);
+      // }
+      // target.constructor.timer = setTimeout(() => {
+      //   sortListPosition(newFieldList);
+      // }, 0);
+      // const sortedFieldList = sortListPosition(newFieldList);
+
+      target.constructor.FieldList = newFieldList;
+    };
+  }
+
+  static TableColumn(config?: ColumnType<Record<string, any>>): PropertyDecorator {
+    return (target: any, propertyKey: string) => {
+      const { I18nMap, ColumnList } = target.constructor;
+      const oldIndex = ColumnList.findIndex(item => item.name === propertyKey);
+      const subColumnList = [...ColumnList];
+      if (oldIndex !== -1) {
+        subColumnList.splice(oldIndex, 1);
+      }
+      subColumnList.push({
+        ...(typeof config === 'object' ? config : {}),
+        dataIndex: propertyKey,
+        title: I18nMap[propertyKey],
+      });
+      target.constructor.ColumnList = subColumnList;
+    };
+  }
+
+  abstract parse<T, K>(data: T): K;
+
+  abstract stringify<T, K>(data: T): K;
+}
diff --git a/inlong-dashboard/src/metas/clusters/agent.tsx b/inlong-dashboard/src/metas/clusters/agent.tsx
deleted file mode 100644
index f2cf14150..000000000
--- a/inlong-dashboard/src/metas/clusters/agent.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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.
- */
-
-import type { FieldItemType } from '@/metas/common';
-
-export const agent: FieldItemType[] = [];
diff --git a/inlong-dashboard/src/metas/clusters/index.tsx b/inlong-dashboard/src/metas/clusters/common/ClusterDefaultInfo.ts
similarity index 52%
rename from inlong-dashboard/src/metas/clusters/index.tsx
rename to inlong-dashboard/src/metas/clusters/common/ClusterDefaultInfo.ts
index 95d103903..0cb4c385c 100644
--- a/inlong-dashboard/src/metas/clusters/index.tsx
+++ b/inlong-dashboard/src/metas/clusters/common/ClusterDefaultInfo.ts
@@ -17,67 +17,45 @@
  * under the License.
  */
 
-import React from 'react';
-import i18n from '@/i18n';
+import { DataWithBackend } from '@/metas/DataWithBackend';
 import UserSelect from '@/components/UserSelect';
-import type { FieldItemType } from '@/metas/common';
-import { genFields, genForm, genTable } from '@/metas/common';
-import { agent } from './agent';
-import { dataProxy } from './dataProxy';
-import { pulsar } from './pulsar';
-import { tubeMQ } from './tubeMQ';
+import { clusters, defaultValue } from '..';
 
-const allClusters = [
-  {
-    label: 'Agent',
-    value: 'AGENT',
-    fields: agent,
-  },
-  {
-    label: 'DataProxy',
-    value: 'DATAPROXY',
-    fields: dataProxy,
-  },
-  {
-    label: 'Pulsar',
-    value: 'PULSAR',
-    fields: pulsar,
-  },
-  {
-    label: 'TubeMQ',
-    value: 'TUBEMQ',
-    fields: tubeMQ,
-  },
-];
+const { I18n, FormField, TableColumn } = DataWithBackend;
 
-const defaultCommonFields: FieldItemType[] = [
-  {
+export class ClusterDefaultInfo extends DataWithBackend {
+  id: number;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('pages.Clusters.Name'),
-    name: 'name',
     rules: [{ required: true }],
     props: {
       maxLength: 128,
     },
-    _renderTable: true,
-  },
-  {
-    type: 'radio',
-    name: 'type',
-    label: i18n.t('pages.Clusters.Type'),
-    initialValue: allClusters[0].value,
+  })
+  @TableColumn()
+  @I18n('pages.Clusters.Name')
+  name: string;
+
+  @FormField({
+    type: clusters.length > 3 ? 'select' : 'radio',
+    initialValue: defaultValue,
     rules: [{ required: true }],
     props: {
-      options: allClusters.map(item => ({
-        label: item.label,
-        value: item.value,
-      })),
+      options: clusters
+        .filter(item => item.value)
+        .map(item => ({
+          label: item.label,
+          value: item.value,
+        })),
     },
-  },
-  {
+  })
+  @TableColumn()
+  @I18n('pages.Clusters.Type')
+  type: string;
+
+  @FormField({
     type: 'select',
-    label: i18n.t('pages.Clusters.Tag'),
-    name: 'clusterTags',
     rules: [{ required: true }],
     props: {
       mode: 'multiple',
@@ -103,33 +81,39 @@ const defaultCommonFields: FieldItemType[] = [
         },
       },
     },
-    _renderTable: true,
-  },
-  {
-    type: <UserSelect mode="multiple" />,
-    label: i18n.t('pages.Clusters.InCharges'),
-    name: 'inCharges',
+  })
+  @TableColumn()
+  @I18n('pages.Clusters.Tag')
+  clusterTags: string;
+
+  @FormField({
+    type: UserSelect,
     rules: [{ required: true }],
-    _renderTable: true,
-  },
-  {
+    props: {
+      mode: 'multiple',
+      currentUserClosable: false,
+    },
+  })
+  @TableColumn()
+  @I18n('pages.Clusters.InCharges')
+  inCharges: string;
+
+  @FormField({
     type: 'textarea',
-    label: i18n.t('pages.Clusters.Description'),
-    name: 'description',
     props: {
       maxLength: 256,
     },
-  },
-];
+  })
+  @I18n('pages.Clusters.Description')
+  description: string;
+
+  version?: number;
 
-export const clusters = allClusters.map(item => {
-  const itemFields = defaultCommonFields.concat(item.fields);
-  const fields = genFields(itemFields);
+  parse(data) {
+    return data;
+  }
 
-  return {
-    ...item,
-    fields,
-    form: genForm(fields),
-    table: genTable(fields),
-  };
-});
+  stringify(data) {
+    return data;
+  }
+}
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/clusters/common/ClusterInfo.ts
similarity index 83%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/clusters/common/ClusterInfo.ts
index 8a7c0bd8e..f14292db9 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/clusters/common/ClusterInfo.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { ClusterDefaultInfo } from './ClusterDefaultInfo';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export class ClusterInfo extends ClusterDefaultInfo {
+  // You can extends ClusterInfo at here...
+}
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/clusters/defaults/Agent.ts
similarity index 80%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/clusters/defaults/Agent.ts
index 8a7c0bd8e..d0f817001 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/clusters/defaults/Agent.ts
@@ -17,8 +17,7 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import { ClusterInfo } from '../common/ClusterInfo';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export default class AgentCluster extends ClusterInfo implements DataWithBackend {}
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/clusters/defaults/DataProxy.ts
similarity index 80%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/clusters/defaults/DataProxy.ts
index 8a7c0bd8e..8a0a6fb6f 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/clusters/defaults/DataProxy.ts
@@ -17,8 +17,7 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import { ClusterInfo } from '../common/ClusterInfo';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export default class DataProxyCluster extends ClusterInfo implements DataWithBackend {}
diff --git a/inlong-dashboard/src/metas/clusters/pulsar.tsx b/inlong-dashboard/src/metas/clusters/defaults/Pulsar.ts
similarity index 73%
rename from inlong-dashboard/src/metas/clusters/pulsar.tsx
rename to inlong-dashboard/src/metas/clusters/defaults/Pulsar.ts
index b1cd76276..443b7296d 100644
--- a/inlong-dashboard/src/metas/clusters/pulsar.tsx
+++ b/inlong-dashboard/src/metas/clusters/defaults/Pulsar.ts
@@ -18,42 +18,48 @@
  */
 
 import i18n from '@/i18n';
-import type { FieldItemType } from '@/metas/common';
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import { ClusterInfo } from '../common/ClusterInfo';
 
-export const pulsar: FieldItemType[] = [
-  {
+const { I18n, FormField } = DataWithBackend;
+
+export default class PulsarCluster extends ClusterInfo implements DataWithBackend {
+  @FormField({
     type: 'input',
-    label: 'Service URL',
-    name: 'url',
     tooltip: i18n.t('pages.Clusters.Pulsar.ServiceUrlHelper'),
     rules: [{ required: true }],
     props: {
       placeholder: 'pulsar://127.0.0.1:6650,127.0.1.2:6650',
     },
-  },
-  {
+  })
+  @I18n('Service URL')
+  url: string;
+
+  @FormField({
     type: 'input',
-    label: 'Admin URL',
-    name: 'adminUrl',
     tooltip: i18n.t('pages.Clusters.Pulsar.AdminUrlHelper'),
     rules: [{ required: true }],
     props: {
       placeholder: 'http://127.0.0.1:8080,127.0.1.2:8080',
     },
-  },
-  {
+  })
+  @I18n('Admin URL')
+  adminUrl: string;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('pages.Clusters.Pulsar.Tenant'),
-    name: 'tenant',
     rules: [{ required: true }],
     initialValue: 'public',
-  },
-  {
+  })
+  @I18n('pages.Clusters.Pulsar.Tenant')
+  tenant: string;
+
+  @FormField({
     type: 'input',
-    label: 'Token',
-    name: 'token',
     props: {
       placeholder: 'Required if the cluster is configured with Token',
     },
-  },
-];
+  })
+  @I18n('Token')
+  token: string;
+}
diff --git a/inlong-dashboard/src/metas/clusters/tubeMQ.tsx b/inlong-dashboard/src/metas/clusters/defaults/TubeMq.ts
similarity index 75%
copy from inlong-dashboard/src/metas/clusters/tubeMQ.tsx
copy to inlong-dashboard/src/metas/clusters/defaults/TubeMq.ts
index 3a669e324..6519c4a45 100644
--- a/inlong-dashboard/src/metas/clusters/tubeMQ.tsx
+++ b/inlong-dashboard/src/metas/clusters/defaults/TubeMq.ts
@@ -18,35 +18,40 @@
  */
 
 import i18n from '@/i18n';
-import type { FieldItemType } from '@/metas/common';
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import { ClusterInfo } from '../common/ClusterInfo';
 
-export const tubeMQ: FieldItemType[] = [
-  {
+const { I18n, FormField } = DataWithBackend;
+
+export default class TubeMqCluster extends ClusterInfo implements DataWithBackend {
+  @FormField({
     type: 'input',
-    label: 'RPC URL',
-    name: 'url',
     rules: [{ required: true }],
     tooltip: i18n.t('pages.Clusters.Tube.MasterRpcUrlHelper'),
     props: {
       placeholder: '127.0.0.1:8715,127.0.1.2:8715',
     },
-  },
-  {
+  })
+  @I18n('RPC URL')
+  url: string;
+
+  @FormField({
     type: 'input',
-    label: 'Web URL',
-    name: 'masterWebUrl',
     rules: [{ required: true }],
     tooltip: i18n.t('pages.Clusters.Tube.MasterWebUrlHelper'),
     props: {
       placeholder: 'http://127.0.0.1:8080',
     },
-  },
-  {
+  })
+  @I18n('Web URL')
+  masterWebUrl: string;
+
+  @FormField({
     type: 'input',
-    label: 'Token',
-    name: 'token',
     props: {
       placeholder: 'Required if the cluster is configured with Token',
     },
-  },
-];
+  })
+  @I18n('Token')
+  token: string;
+}
diff --git a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx b/inlong-dashboard/src/metas/clusters/defaults/index.ts
similarity index 56%
copy from inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
copy to inlong-dashboard/src/metas/clusters/defaults/index.ts
index 9641bacd1..5ba19e0c7 100644
--- a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
+++ b/inlong-dashboard/src/metas/clusters/defaults/index.ts
@@ -17,18 +17,32 @@
  * under the License.
  */
 
-import { streamForm } from '@/metas/stream';
-import { pickObjectArray } from '@/utils';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const getFilterFormContent = (defaultValues = {} as any) => [
+export const allDefaultClusters: MetaExportWithBackendList = [
   {
-    type: 'inputsearch',
-    name: 'keyword',
-    initialValue: defaultValues.keyword,
+    label: 'ALL',
+    value: '',
+    LoadEntity: () => import('../common/ClusterInfo').then(r => ({ default: r.ClusterInfo })),
+  },
+  {
+    label: 'Agent',
+    value: 'AGENT',
+    LoadEntity: () => import('./Agent'),
+  },
+  {
+    label: 'DataProxy',
+    value: 'DATAPROXY',
+    LoadEntity: () => import('./DataProxy'),
+  },
+  {
+    label: 'Pulsar',
+    value: 'PULSAR',
+    LoadEntity: () => import('./Pulsar'),
+  },
+  {
+    label: 'TubeMQ',
+    value: 'TUBEMQ',
+    LoadEntity: () => import('./TubeMq'),
   },
-  ...pickObjectArray(['status'], streamForm).map(item => ({
-    ...item,
-    visible: true,
-    initialValue: defaultValues[item.name],
-  })),
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/clusters/extends/index.ts
similarity index 83%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/clusters/extends/index.ts
index 8a7c0bd8e..4d794befd 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/clusters/extends/index.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
+export const allExtendsClusters: MetaExportWithBackendList = [
+  // You can extends at here...
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/clusters/index.ts
similarity index 78%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/clusters/index.ts
index 8a7c0bd8e..2c6643083 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/clusters/index.ts
@@ -17,8 +17,9 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { allDefaultClusters } from './defaults';
+import { allExtendsClusters } from './extends';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export const clusters = allDefaultClusters.concat(allExtendsClusters);
+
+export const defaultValue = clusters[0].value;
diff --git a/inlong-dashboard/src/metas/consume/index.tsx b/inlong-dashboard/src/metas/consume/index.tsx
deleted file mode 100644
index 32ef3be34..000000000
--- a/inlong-dashboard/src/metas/consume/index.tsx
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * 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.
- */
-
-import React from 'react';
-import UserSelect from '@/components/UserSelect';
-import type { FieldItemType } from '@/metas/common';
-import { genFields, genForm, genTable } from '@/metas/common';
-import i18n from '@/i18n';
-import { timestampFormat } from '@/utils';
-import {
-  statusList,
-  lastConsumerStatusList,
-  genStatusTag,
-  genLastConsumerStatusTag,
-} from './status';
-import { consumeExtends } from './extends';
-
-const consumeDefault: FieldItemType[] = [
-  {
-    type: 'input',
-    label: i18n.t('meta.Consume.ConsumerGroupName'),
-    name: 'consumerGroup',
-    extra: i18n.t('meta.Consume.ConsumerGroupNameRules'),
-    rules: [
-      { required: true },
-      {
-        pattern: /^[0-9a-z_-]+$/,
-        message: i18n.t('meta.Consume.ConsumerGroupNameRules'),
-      },
-    ],
-    _renderTable: true,
-  },
-  {
-    type: <UserSelect mode="multiple" currentUserClosable={false} />,
-    label: i18n.t('meta.Consume.Owner'),
-    name: 'inCharges',
-    extra: i18n.t('meta.Consume.OwnersExtra'),
-    rules: [{ required: true }],
-    _renderTable: true,
-  },
-  {
-    type: 'select',
-    label: i18n.t('meta.Consume.TargetInlongGroupID'),
-    name: 'inlongGroupId',
-    extraNames: ['mqType'],
-    rules: [{ required: true }],
-    props: {
-      showSearch: true,
-      filterOption: false,
-      options: {
-        requestTrigger: ['onOpen', 'onSearch'],
-        requestService: keyword => ({
-          url: '/group/list',
-          method: 'POST',
-          data: {
-            keyword,
-            pageNum: 1,
-            pageSize: 20,
-            status: 130,
-          },
-        }),
-        requestParams: {
-          formatResult: result =>
-            result?.list?.map(item => ({
-              ...item,
-              label: `${item.inlongGroupId} (${item.mqType})`,
-              value: item.inlongGroupId,
-            })),
-        },
-      },
-      onChange: (value, option) => ({
-        topic: undefined,
-        mqType: option.mqType,
-      }),
-    },
-    _renderTable: true,
-  },
-  {
-    type: 'text',
-    label: i18n.t('meta.Consume.MQType'),
-    name: 'mqType',
-    visible: false,
-    _renderTable: true,
-  },
-  {
-    type: 'select',
-    label: i18n.t('meta.Consume.TopicName'),
-    name: 'topic',
-    rules: [{ required: true }],
-    props: values => ({
-      mode: values.mqType === 'PULSAR' ? 'multiple' : '',
-      options: {
-        requestService: `/group/getTopic/${values.inlongGroupId}`,
-        requestParams: {
-          formatResult: result =>
-            result.mqType === 'TUBEMQ'
-              ? [
-                  {
-                    label: result.mqResource,
-                    value: result.mqResource,
-                  },
-                ]
-              : result.streamTopics?.map(item => ({
-                  ...item,
-                  label: item.mqResource,
-                  value: item.mqResource,
-                })) || [],
-        },
-      },
-      onChange: (value, option) => {
-        if (typeof value !== 'string') {
-          return {
-            inlongStreamId: option.map(item => item.streamTopics).join(','),
-          };
-        }
-      },
-    }),
-    visible: values => !!values.inlongGroupId,
-    _renderTable: true,
-  },
-  {
-    type: 'select',
-    label: i18n.t('basic.Status'),
-    name: 'status',
-    props: {
-      allowClear: true,
-      options: statusList,
-      dropdownMatchSelectWidth: false,
-    },
-    visible: false,
-    _renderTable: {
-      render: text => genStatusTag(text),
-    },
-  },
-  {
-    type: 'input',
-    label: i18n.t('pages.ConsumeDashboard.config.RecentConsumeTime'),
-    name: 'lastConsumeTime',
-    visible: false,
-    _renderTable: {
-      render: text => text && timestampFormat(text),
-    },
-  },
-  {
-    type: 'select',
-    label: i18n.t('pages.ConsumeDashboard.config.OperatingStatus'),
-    name: 'lastConsumeStatus',
-    props: {
-      allowClear: true,
-      dropdownMatchSelectWidth: false,
-      options: lastConsumerStatusList,
-    },
-    visible: false,
-    _renderTable: {
-      render: text => text && genLastConsumerStatusTag(text),
-    },
-  },
-  {
-    type: 'radio',
-    label: i18n.t('meta.Consume.FilterEnabled'),
-    name: 'filterEnabled',
-    initialValue: 0,
-    props: {
-      options: [
-        {
-          label: i18n.t('meta.Consume.Yes'),
-          value: 1,
-        },
-        {
-          label: i18n.t('meta.Consume.No'),
-          value: 0,
-        },
-      ],
-    },
-    rules: [{ required: true }],
-    visible: values => !!values.mqType && values.mqType !== 'PULSAR',
-  },
-  {
-    type: 'input',
-    label: i18n.t('meta.Consume.TargetInlongStreamID'),
-    name: 'inlongStreamId',
-    rules: [{ required: true }],
-    visible: values => values.filterEnabled,
-  },
-  {
-    type: 'text',
-    label: i18n.t('meta.Consume.MQAddress'),
-    name: 'masterUrl',
-  },
-  {
-    type: 'radio',
-    label: 'isDlq',
-    name: 'isDlq',
-    initialValue: 0,
-    rules: [{ required: true }],
-    props: {
-      options: [
-        {
-          label: i18n.t('meta.Consume.Yes'),
-          value: 1,
-        },
-        {
-          label: i18n.t('meta.Consume.No'),
-          value: 0,
-        },
-      ],
-    },
-    visible: values => values.mqType === 'PULSAR',
-  },
-  {
-    type: 'input',
-    label: 'deadLetterTopic',
-    name: 'deadLetterTopic',
-    rules: [{ required: true }],
-    visible: values => values?.isDlq && values.mqType === 'PULSAR',
-  },
-  {
-    type: 'radio',
-    label: 'isRlq',
-    name: 'isRlq',
-    initialValue: 0,
-    rules: [{ required: true }],
-    props: {
-      options: [
-        {
-          label: i18n.t('meta.Consume.Yes'),
-          value: 1,
-        },
-        {
-          label: i18n.t('meta.Consume.No'),
-          value: 0,
-        },
-      ],
-    },
-    visible: values => values?.isDlq && values.mqType === 'PULSAR',
-  },
-  {
-    type: 'input',
-    label: 'retryLetterTopic',
-    name: 'retryLetterTopic',
-    rules: [{ required: true }],
-    visible: values => values?.isDlq && values?.isRlq && values.mqType === 'PULSAR',
-  },
-];
-
-export const consume = genFields(consumeDefault, consumeExtends);
-
-export const consumeForm = genForm(consume);
-
-export const consumeTable = genTable(consume);
diff --git a/inlong-dashboard/src/metas/consumes/common/ConsumeDefaultInfo.ts b/inlong-dashboard/src/metas/consumes/common/ConsumeDefaultInfo.ts
new file mode 100644
index 000000000..bc8e65875
--- /dev/null
+++ b/inlong-dashboard/src/metas/consumes/common/ConsumeDefaultInfo.ts
@@ -0,0 +1,192 @@
+/*
+ * 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.
+ */
+
+import UserSelect from '@/components/UserSelect';
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import i18n from '@/i18n';
+import { timestampFormat } from '@/utils';
+import {
+  statusList,
+  lastConsumerStatusList,
+  genStatusTag,
+  genLastConsumerStatusTag,
+} from './status';
+
+const { I18n, FormField, TableColumn } = DataWithBackend;
+
+export class ConsumeDefaultInfo extends DataWithBackend {
+  readonly id: number;
+
+  @FormField({
+    type: 'input',
+    extra: i18n.t('meta.Consume.ConsumerGroupNameRules'),
+    rules: [
+      { required: true },
+      {
+        pattern: /^[0-9a-z_-]+$/,
+        message: i18n.t('meta.Consume.ConsumerGroupNameRules'),
+      },
+    ],
+  })
+  @TableColumn()
+  @I18n('meta.Consume.ConsumerGroupName')
+  consumerGroup: string;
+
+  @FormField({
+    type: UserSelect,
+    extra: i18n.t('meta.Consume.OwnersExtra'),
+    rules: [{ required: true }],
+    props: {
+      mode: 'multiple',
+      currentUserClosable: false,
+    },
+  })
+  @TableColumn()
+  @I18n('meta.Consume.Owner')
+  inCharges: string;
+
+  @FormField({
+    type: 'select',
+    extraNames: ['mqType'],
+    rules: [{ required: true }],
+    props: {
+      showSearch: true,
+      filterOption: false,
+      options: {
+        requestTrigger: ['onOpen', 'onSearch'],
+        requestService: keyword => ({
+          url: '/group/list',
+          method: 'POST',
+          data: {
+            keyword,
+            pageNum: 1,
+            pageSize: 20,
+            status: 130,
+          },
+        }),
+        requestParams: {
+          formatResult: result =>
+            result?.list?.map(item => ({
+              ...item,
+              label: `${item.inlongGroupId} (${item.mqType})`,
+              value: item.inlongGroupId,
+            })),
+        },
+      },
+      onChange: (value, option) => ({
+        topic: undefined,
+        mqType: option.mqType,
+      }),
+    },
+  })
+  @TableColumn()
+  @I18n('meta.Consume.TargetInlongGroupID')
+  inlongGroupId: string;
+
+  @TableColumn()
+  @I18n('meta.Consume.MQType')
+  mqType: string;
+
+  @FormField({
+    type: 'select',
+    rules: [{ required: true }],
+    props: values => ({
+      mode: values.mqType === 'PULSAR' ? 'multiple' : '',
+      options: {
+        requestService: `/group/getTopic/${values.inlongGroupId}`,
+        requestParams: {
+          formatResult: result =>
+            result.mqType === 'TUBEMQ'
+              ? [
+                  {
+                    label: result.mqResource,
+                    value: result.mqResource,
+                  },
+                ]
+              : result.streamTopics?.map(item => ({
+                  ...item,
+                  label: item.mqResource,
+                  value: item.mqResource,
+                })) || [],
+        },
+      },
+      onChange: (value, option) => {
+        if (typeof value !== 'string') {
+          return {
+            inlongStreamId: option.map(item => item.streamTopics).join(','),
+          };
+        }
+      },
+    }),
+    visible: values => Boolean(values.inlongGroupId),
+  })
+  @TableColumn()
+  @I18n('meta.Consume.TopicName')
+  topic: string;
+
+  @FormField({
+    type: 'select',
+    props: {
+      allowClear: true,
+      options: statusList,
+      dropdownMatchSelectWidth: false,
+    },
+    visible: false,
+  })
+  @TableColumn({
+    render: text => genStatusTag(text),
+  })
+  @I18n('basic.Status')
+  readonly status: string;
+
+  @TableColumn({
+    render: text => text && timestampFormat(text),
+  })
+  @I18n('pages.ConsumeDashboard.config.RecentConsumeTime')
+  readonly lastConsumeTime: string;
+
+  @FormField({
+    type: 'select',
+    props: {
+      allowClear: true,
+      dropdownMatchSelectWidth: false,
+      options: lastConsumerStatusList,
+    },
+    visible: false,
+  })
+  @TableColumn({
+    render: text => text && genLastConsumerStatusTag(text),
+  })
+  @I18n('pages.ConsumeDashboard.config.OperatingStatus')
+  readonly lastConsumeStatus: string;
+
+  @FormField({
+    type: 'text',
+  })
+  @I18n('meta.Consume.MQAddress')
+  readonly masterUrl: string;
+
+  parse(data) {
+    return data;
+  }
+
+  stringify(data) {
+    return data;
+  }
+}
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/consumes/common/ConsumeInfo.ts
similarity index 83%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/consumes/common/ConsumeInfo.ts
index 8a7c0bd8e..a5ea8178d 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/consumes/common/ConsumeInfo.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { ConsumeDefaultInfo } from './ConsumeDefaultInfo';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export class ConsumeInfo extends ConsumeDefaultInfo {
+  // You can extends ConsumeInfo at here...
+}
diff --git a/inlong-dashboard/src/metas/consume/status.tsx b/inlong-dashboard/src/metas/consumes/common/status.tsx
similarity index 100%
rename from inlong-dashboard/src/metas/consume/status.tsx
rename to inlong-dashboard/src/metas/consumes/common/status.tsx
diff --git a/inlong-dashboard/src/metas/consumes/defaults/Pulsar.ts b/inlong-dashboard/src/metas/consumes/defaults/Pulsar.ts
new file mode 100644
index 000000000..f5a8ae6a3
--- /dev/null
+++ b/inlong-dashboard/src/metas/consumes/defaults/Pulsar.ts
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import i18n from '@/i18n';
+import { ConsumeInfo } from '../common/ConsumeInfo';
+
+const { I18n, FormField } = DataWithBackend;
+
+export default class PulsarConsume extends ConsumeInfo implements DataWithBackend {
+  @FormField({
+    type: 'radio',
+    initialValue: 0,
+    rules: [{ required: true }],
+    props: {
+      options: [
+        {
+          label: i18n.t('meta.Consume.Yes'),
+          value: 1,
+        },
+        {
+          label: i18n.t('meta.Consume.No'),
+          value: 0,
+        },
+      ],
+    },
+  })
+  @I18n('isDlq')
+  isDlq: 0 | 1;
+
+  @FormField({
+    type: 'input',
+    rules: [{ required: true }],
+    visible: values => values?.isDlq,
+  })
+  @I18n('deadLetterTopic')
+  deadLetterTopic: string;
+
+  @FormField({
+    type: 'radio',
+    initialValue: 0,
+    rules: [{ required: true }],
+    props: {
+      options: [
+        {
+          label: i18n.t('meta.Consume.Yes'),
+          value: 1,
+        },
+        {
+          label: i18n.t('meta.Consume.No'),
+          value: 0,
+        },
+      ],
+    },
+    visible: values => values?.isDlq,
+  })
+  @I18n('isRlq')
+  isRlq: 0 | 1;
+
+  @FormField({
+    type: 'input',
+    rules: [{ required: true }],
+    visible: values => values?.isDlq && values?.isRlq,
+  })
+  @I18n('retryLetterTopic')
+  retryLetterTopic: string;
+}
diff --git a/inlong-dashboard/src/metas/clusters/tubeMQ.tsx b/inlong-dashboard/src/metas/consumes/defaults/TubeMq.ts
similarity index 57%
rename from inlong-dashboard/src/metas/clusters/tubeMQ.tsx
rename to inlong-dashboard/src/metas/consumes/defaults/TubeMq.ts
index 3a669e324..9aa4ef7da 100644
--- a/inlong-dashboard/src/metas/clusters/tubeMQ.tsx
+++ b/inlong-dashboard/src/metas/consumes/defaults/TubeMq.ts
@@ -17,36 +17,38 @@
  * under the License.
  */
 
+import { DataWithBackend } from '@/metas/DataWithBackend';
 import i18n from '@/i18n';
-import type { FieldItemType } from '@/metas/common';
+import { ConsumeInfo } from '../common/ConsumeInfo';
 
-export const tubeMQ: FieldItemType[] = [
-  {
-    type: 'input',
-    label: 'RPC URL',
-    name: 'url',
-    rules: [{ required: true }],
-    tooltip: i18n.t('pages.Clusters.Tube.MasterRpcUrlHelper'),
+const { I18n, FormField } = DataWithBackend;
+
+export default class TubeMqConsume extends ConsumeInfo implements DataWithBackend {
+  @FormField({
+    type: 'radio',
+    initialValue: 0,
     props: {
-      placeholder: '127.0.0.1:8715,127.0.1.2:8715',
+      options: [
+        {
+          label: i18n.t('meta.Consume.Yes'),
+          value: 1,
+        },
+        {
+          label: i18n.t('meta.Consume.No'),
+          value: 0,
+        },
+      ],
     },
-  },
-  {
-    type: 'input',
-    label: 'Web URL',
-    name: 'masterWebUrl',
     rules: [{ required: true }],
-    tooltip: i18n.t('pages.Clusters.Tube.MasterWebUrlHelper'),
-    props: {
-      placeholder: 'http://127.0.0.1:8080',
-    },
-  },
-  {
+  })
+  @I18n('meta.Consume.FilterEnabled')
+  filterEnabled: 0 | 1;
+
+  @FormField({
     type: 'input',
-    label: 'Token',
-    name: 'token',
-    props: {
-      placeholder: 'Required if the cluster is configured with Token',
-    },
-  },
-];
+    rules: [{ required: true }],
+    visible: values => values.filterEnabled,
+  })
+  @I18n('meta.Consume.TargetInlongStreamID')
+  inlongStreamId: string;
+}
diff --git a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx b/inlong-dashboard/src/metas/consumes/defaults/index.ts
similarity index 65%
copy from inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
copy to inlong-dashboard/src/metas/consumes/defaults/index.ts
index 9641bacd1..792430a45 100644
--- a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
+++ b/inlong-dashboard/src/metas/consumes/defaults/index.ts
@@ -17,18 +17,22 @@
  * under the License.
  */
 
-import { streamForm } from '@/metas/stream';
-import { pickObjectArray } from '@/utils';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const getFilterFormContent = (defaultValues = {} as any) => [
+export const allDefaultConsumes: MetaExportWithBackendList = [
   {
-    type: 'inputsearch',
-    name: 'keyword',
-    initialValue: defaultValues.keyword,
+    label: 'ALL',
+    value: '',
+    LoadEntity: () => import('../common/ConsumeInfo').then(r => ({ default: r.ConsumeInfo })),
+  },
+  {
+    label: 'Pulsar',
+    value: 'PULSAR',
+    LoadEntity: () => import('./Pulsar'),
+  },
+  {
+    label: 'TubeMq',
+    value: 'TUBEMQ',
+    LoadEntity: () => import('./TubeMq'),
   },
-  ...pickObjectArray(['status'], streamForm).map(item => ({
-    ...item,
-    visible: true,
-    initialValue: defaultValues[item.name],
-  })),
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/consumes/extends/index.ts
similarity index 83%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/consumes/extends/index.ts
index 8a7c0bd8e..ce5443978 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/consumes/extends/index.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
+export const allExtendsConsumes: MetaExportWithBackendList = [
+  // You can extends at here...
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/consumes/index.ts
similarity index 78%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/consumes/index.ts
index 8a7c0bd8e..2ed0bd403 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/consumes/index.ts
@@ -17,8 +17,9 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { allDefaultConsumes } from './defaults';
+import { allExtendsConsumes } from './extends';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export const consumes = allDefaultConsumes.concat(allExtendsConsumes);
+
+export const defaultValue = consumes[0].value;
diff --git a/inlong-dashboard/src/metas/groups/common/GroupDefaultInfo.ts b/inlong-dashboard/src/metas/groups/common/GroupDefaultInfo.ts
new file mode 100644
index 000000000..762c66a25
--- /dev/null
+++ b/inlong-dashboard/src/metas/groups/common/GroupDefaultInfo.ts
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+import UserSelect from '@/components/UserSelect';
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import i18n from '@/i18n';
+import { statusList, genStatusTag } from './status';
+import { groups, defaultValue } from '..';
+
+const { I18n, FormField, TableColumn } = DataWithBackend;
+
+export class GroupDefaultInfo extends DataWithBackend {
+  readonly id: number;
+
+  @FormField({
+    type: 'input',
+    props: {
+      maxLength: 32,
+    },
+    rules: [
+      { required: true },
+      {
+        pattern: /^[a-z_\-\d]+$/,
+        message: i18n.t('meta.Group.InlongGroupIdRules'),
+      },
+    ],
+  })
+  @TableColumn()
+  @I18n('meta.Group.InlongGroupId')
+  inlongGroupId: string;
+
+  @FormField({
+    type: 'input',
+    props: {
+      maxLength: 32,
+    },
+  })
+  @I18n('meta.Group.InlongGroupName')
+  name: string;
+
+  @FormField({
+    type: UserSelect,
+    extra: i18n.t('meta.Group.InlongGroupOwnersExtra'),
+    rules: [{ required: true }],
+    props: {
+      mode: 'multiple',
+      currentUserClosable: false,
+    },
+  })
+  @TableColumn()
+  @I18n('meta.Group.InlongGroupOwners')
+  inCharges: string;
+
+  @FormField({
+    type: 'textarea',
+    props: {
+      showCount: true,
+      maxLength: 100,
+    },
+  })
+  @I18n('meta.Group.InlongGroupIntroduction')
+  description: string;
+
+  @FormField({
+    type: 'radio',
+    initialValue: defaultValue,
+    rules: [{ required: true }],
+    props: {
+      options: groups.filter(item => Boolean(item.value)),
+    },
+  })
+  @TableColumn()
+  @I18n('meta.Group.MQType')
+  mqType: string;
+
+  @FormField({
+    type: 'text',
+  })
+  @I18n('MQ Resource')
+  readonly mqResource: string;
+
+  @FormField({
+    type: 'select',
+    props: {
+      allowClear: true,
+      options: statusList,
+      dropdownMatchSelectWidth: false,
+    },
+    visible: false,
+  })
+  @TableColumn({
+    render: text => genStatusTag(text),
+  })
+  @I18n('basic.Status')
+  readonly status: string;
+
+  @TableColumn()
+  @I18n('basic.CreateTime')
+  readonly createTime: string;
+
+  parse(data) {
+    return data;
+  }
+
+  stringify(data) {
+    return data;
+  }
+}
diff --git a/inlong-dashboard/src/metas/clusters/dataProxy.tsx b/inlong-dashboard/src/metas/groups/common/GroupInfo.ts
similarity index 84%
rename from inlong-dashboard/src/metas/clusters/dataProxy.tsx
rename to inlong-dashboard/src/metas/groups/common/GroupInfo.ts
index 5219858a2..a4977155e 100644
--- a/inlong-dashboard/src/metas/clusters/dataProxy.tsx
+++ b/inlong-dashboard/src/metas/groups/common/GroupInfo.ts
@@ -17,6 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { GroupDefaultInfo } from './GroupDefaultInfo';
 
-export const dataProxy: FieldItemType[] = [];
+export class GroupInfo extends GroupDefaultInfo {
+  // You can extends GroupInfo at here...
+}
diff --git a/inlong-dashboard/src/metas/group/status.tsx b/inlong-dashboard/src/metas/groups/common/status.tsx
similarity index 100%
rename from inlong-dashboard/src/metas/group/status.tsx
rename to inlong-dashboard/src/metas/groups/common/status.tsx
diff --git a/inlong-dashboard/src/metas/group/index.tsx b/inlong-dashboard/src/metas/groups/defaults/Pulsar.ts
similarity index 53%
rename from inlong-dashboard/src/metas/group/index.tsx
rename to inlong-dashboard/src/metas/groups/defaults/Pulsar.ts
index 1ec6cd1e4..d62249492 100644
--- a/inlong-dashboard/src/metas/group/index.tsx
+++ b/inlong-dashboard/src/metas/groups/defaults/Pulsar.ts
@@ -17,110 +17,15 @@
  * under the License.
  */
 
-import React from 'react';
-import UserSelect from '@/components/UserSelect';
-import type { FieldItemType } from '@/metas/common';
-import { genFields, genForm, genTable } from '@/metas/common';
+import { DataWithBackend } from '@/metas/DataWithBackend';
 import i18n from '@/i18n';
-import { statusList, genStatusTag } from './status';
-import { groupExtends } from './extends';
+import { GroupInfo } from '../common/GroupInfo';
 
-const groupDefault: FieldItemType[] = [
-  {
-    type: 'input',
-    label: i18n.t('meta.Group.InlongGroupId'),
-    name: 'inlongGroupId',
-    props: {
-      maxLength: 32,
-    },
-    rules: [
-      { required: true },
-      {
-        pattern: /^[a-z_\-\d]+$/,
-        message: i18n.t('meta.Group.InlongGroupIdRules'),
-      },
-    ],
-    _renderTable: true,
-  },
-  {
-    type: 'input',
-    label: i18n.t('meta.Group.InlongGroupName'),
-    name: 'name',
-    props: {
-      maxLength: 32,
-    },
-  },
-  {
-    type: <UserSelect mode="multiple" currentUserClosable={false} />,
-    label: i18n.t('meta.Group.InlongGroupOwners'),
-    name: 'inCharges',
-    rules: [
-      {
-        required: true,
-      },
-    ],
-    extra: i18n.t('meta.Group.InlongGroupOwnersExtra'),
-    _renderTable: true,
-  },
-  {
-    type: 'textarea',
-    label: i18n.t('meta.Group.InlongGroupIntroduction'),
-    name: 'description',
-    props: {
-      showCount: true,
-      maxLength: 100,
-    },
-  },
-  {
-    type: 'radio',
-    label: i18n.t('meta.Group.MQType'),
-    name: 'mqType',
-    initialValue: 'TUBEMQ',
-    rules: [{ required: true }],
-    props: {
-      options: [
-        {
-          label: 'TubeMQ',
-          value: 'TUBEMQ',
-        },
-        {
-          label: 'Pulsar',
-          value: 'PULSAR',
-        },
-      ],
-    },
-    _renderTable: true,
-  },
-  {
-    type: 'text',
-    label: 'MQ Resource',
-    name: 'mqResource',
-  },
-  {
-    type: 'input',
-    label: i18n.t('basic.CreateTime'),
-    name: 'createTime',
-    visible: false,
-    _renderTable: true,
-  },
-  {
-    type: 'select',
-    label: i18n.t('basic.Status'),
-    name: 'status',
-    props: {
-      allowClear: true,
-      options: statusList,
-      dropdownMatchSelectWidth: false,
-    },
-    visible: false,
-    _renderTable: {
-      render: text => genStatusTag(text),
-    },
-  },
-  {
+const { I18n, FormField } = DataWithBackend;
+
+export default class PulsarGroup extends GroupInfo implements DataWithBackend {
+  @FormField({
     type: 'radio',
-    label: i18n.t('meta.Group.QueueModule'),
-    name: 'queueModule',
     initialValue: 'SERIAL',
     rules: [{ required: true }],
     props: {
@@ -135,12 +40,12 @@ const groupDefault: FieldItemType[] = [
         },
       ],
     },
-    visible: values => values.mqType === 'PULSAR',
-  },
-  {
+  })
+  @I18n('meta.Group.QueueModule')
+  queueModule: string;
+
+  @FormField({
     type: 'inputnumber',
-    label: i18n.t('meta.Group.PartitionNum'),
-    name: 'partitionNum',
     initialValue: 3,
     rules: [{ required: true }],
     props: {
@@ -148,60 +53,13 @@ const groupDefault: FieldItemType[] = [
       max: 20,
       precision: 0,
     },
-    visible: values => values.mqType === 'PULSAR' && values.queueModule === 'PARALLEL',
-  },
-  {
-    type: 'inputnumber',
-    label: i18n.t('meta.Group.NumberOfAccess'),
-    name: 'dailyRecords',
-    rules: [{ required: true }],
-    suffix: i18n.t('meta.Group.TenThousand/Day'),
-    props: {
-      min: 1,
-      precision: 0,
-    },
-    visible: values => values.mqType === 'TUBEMQ',
-  },
-  {
-    type: 'inputnumber',
-    label: i18n.t('meta.Group.AccessSize'),
-    name: 'dailyStorage',
-    rules: [{ required: true }],
-    suffix: i18n.t('meta.Group.GB/Day'),
-    props: {
-      min: 1,
-      precision: 0,
-    },
-    visible: values => values.mqType === 'TUBEMQ',
-  },
-  {
-    type: 'inputnumber',
-    label: i18n.t('meta.Group.AccessPeakPerSecond'),
-    name: 'peakRecords',
-    rules: [{ required: true }],
-    suffix: i18n.t('meta.Group.Stripe/Second'),
-    props: {
-      min: 1,
-      precision: 0,
-    },
-    visible: values => values.mqType === 'TUBEMQ',
-  },
-  {
-    type: 'inputnumber',
-    label: i18n.t('meta.Group.SingleStripMaximumLength'),
-    name: 'maxLength',
-    rules: [{ required: true }],
-    suffix: 'Byte',
-    props: {
-      min: 1,
-      precision: 0,
-    },
-    visible: values => values.mqType === 'TUBEMQ',
-  },
-  {
+    visible: values => values.queueModule === 'PARALLEL',
+  })
+  @I18n('meta.Group.PartitionNum')
+  partitionNum: number;
+
+  @FormField({
     type: 'inputnumber',
-    label: 'ensemble',
-    name: 'ensemble',
     initialValue: 3,
     suffix: i18n.t('meta.Group.EnsembleSuffix'),
     extra: i18n.t('meta.Group.EnsembleExtra'),
@@ -224,12 +82,12 @@ const groupDefault: FieldItemType[] = [
       max: 10,
       precision: 0,
     },
-    visible: values => values.mqType === 'PULSAR',
-  },
-  {
+  })
+  @I18n('ensemble')
+  ensemble: number;
+
+  @FormField({
     type: 'inputnumber',
-    label: 'Write Quorum',
-    name: 'writeQuorum',
     initialValue: 3,
     suffix: i18n.t('meta.Group.WriteQuorumSuffix'),
     extra: i18n.t('meta.Group.WriteQuorumExtra'),
@@ -238,12 +96,12 @@ const groupDefault: FieldItemType[] = [
       max: 10,
       precision: 0,
     },
-    visible: values => values.mqType === 'PULSAR',
-  },
-  {
+  })
+  @I18n('Write Quorum')
+  writeQuorum: number;
+
+  @FormField({
     type: 'inputnumber',
-    label: 'ACK Quorum',
-    name: 'ackQuorum',
     initialValue: 2,
     suffix: i18n.t('meta.Group.AckQuorumSuffix'),
     extra: i18n.t('meta.Group.AckQuorumExtra'),
@@ -252,12 +110,12 @@ const groupDefault: FieldItemType[] = [
       max: 10,
       precision: 0,
     },
-    visible: values => values.mqType === 'PULSAR',
-  },
-  {
+  })
+  @I18n('ACK Quorum')
+  ackQuorum: number;
+
+  @FormField({
     type: 'inputnumber',
-    label: 'Time To Live',
-    name: 'ttl',
     initialValue: 24,
     rules: [
       ({ getFieldValue }) => ({
@@ -293,12 +151,12 @@ const groupDefault: FieldItemType[] = [
       min: 1,
       precision: 0,
     },
-    visible: values => values.mqType === 'PULSAR',
-  },
-  {
+  })
+  @I18n('Time To Live')
+  ttl: number;
+
+  @FormField({
     type: 'inputnumber',
-    label: 'Retention Time',
-    name: 'retentionTime',
     initialValue: 72,
     rules: [
       ({ getFieldValue }) => ({
@@ -342,12 +200,12 @@ const groupDefault: FieldItemType[] = [
       min: -1,
       precision: 0,
     },
-    visible: values => values.mqType === 'PULSAR',
-  },
-  {
+  })
+  @I18n('Retention Time')
+  retentionTime: number;
+
+  @FormField({
     type: 'inputnumber',
-    label: 'Retention Size',
-    name: 'retentionSize',
     initialValue: -1,
     suffix: {
       type: 'select',
@@ -375,12 +233,7 @@ const groupDefault: FieldItemType[] = [
       min: -1,
       precision: 0,
     },
-    visible: values => values.mqType === 'PULSAR',
-  },
-];
-
-export const group = genFields(groupDefault, groupExtends);
-
-export const groupForm = genForm(group);
-
-export const groupTable = genTable(group);
+  })
+  @I18n('Retention Size')
+  retentionSize: number;
+}
diff --git a/inlong-dashboard/src/metas/groups/defaults/TubeMq.ts b/inlong-dashboard/src/metas/groups/defaults/TubeMq.ts
new file mode 100644
index 000000000..e38ef6889
--- /dev/null
+++ b/inlong-dashboard/src/metas/groups/defaults/TubeMq.ts
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import i18n from '@/i18n';
+import { GroupInfo } from '../common/GroupInfo';
+
+const { I18n, FormField } = DataWithBackend;
+
+export default class TubeMqGroup extends GroupInfo implements DataWithBackend {
+  @FormField({
+    type: 'inputnumber',
+    rules: [{ required: true }],
+    suffix: i18n.t('meta.Group.TenThousand/Day'),
+    props: {
+      min: 1,
+      precision: 0,
+    },
+  })
+  @I18n('meta.Group.NumberOfAccess')
+  dailyRecords: number;
+
+  @FormField({
+    type: 'inputnumber',
+    rules: [{ required: true }],
+    suffix: i18n.t('meta.Group.GB/Day'),
+    props: {
+      min: 1,
+      precision: 0,
+    },
+  })
+  @I18n('meta.Group.AccessSize')
+  dailyStorage: number;
+
+  @FormField({
+    type: 'inputnumber',
+    rules: [{ required: true }],
+    suffix: i18n.t('meta.Group.Stripe/Second'),
+    props: {
+      min: 1,
+      precision: 0,
+    },
+  })
+  @I18n('meta.Group.AccessPeakPerSecond')
+  peakRecords: number;
+
+  @FormField({
+    type: 'inputnumber',
+    rules: [{ required: true }],
+    suffix: 'Byte',
+    props: {
+      min: 1,
+      precision: 0,
+    },
+  })
+  @I18n('meta.Group.SingleStripMaximumLength')
+  maxLength: number;
+}
diff --git a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx b/inlong-dashboard/src/metas/groups/defaults/index.ts
similarity index 65%
copy from inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
copy to inlong-dashboard/src/metas/groups/defaults/index.ts
index 9641bacd1..adab3a02b 100644
--- a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
+++ b/inlong-dashboard/src/metas/groups/defaults/index.ts
@@ -17,18 +17,22 @@
  * under the License.
  */
 
-import { streamForm } from '@/metas/stream';
-import { pickObjectArray } from '@/utils';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const getFilterFormContent = (defaultValues = {} as any) => [
+export const allDefaultGroups: MetaExportWithBackendList = [
   {
-    type: 'inputsearch',
-    name: 'keyword',
-    initialValue: defaultValues.keyword,
+    label: 'ALL',
+    value: '',
+    LoadEntity: () => import('../common/GroupInfo').then(r => ({ default: r.GroupInfo })),
+  },
+  {
+    label: 'Pulsar',
+    value: 'PULSAR',
+    LoadEntity: () => import('./Pulsar'),
+  },
+  {
+    label: 'TubeMq',
+    value: 'TUBEMQ',
+    LoadEntity: () => import('./TubeMq'),
   },
-  ...pickObjectArray(['status'], streamForm).map(item => ({
-    ...item,
-    visible: true,
-    initialValue: defaultValues[item.name],
-  })),
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/groups/extends/index.ts
similarity index 83%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/groups/extends/index.ts
index 8a7c0bd8e..abe3010b7 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/groups/extends/index.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
+export const allExtendsGroups: MetaExportWithBackendList = [
+  // You can extends at here...
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/groups/index.ts
similarity index 79%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/groups/index.ts
index 8a7c0bd8e..a794094eb 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/groups/index.ts
@@ -17,8 +17,9 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { allDefaultGroups } from './defaults';
+import { allExtendsGroups } from './extends';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export const groups = allDefaultGroups.concat(allExtendsGroups);
+
+export const defaultValue = groups[0].value;
diff --git a/inlong-dashboard/src/metas/index.ts b/inlong-dashboard/src/metas/index.ts
new file mode 100644
index 000000000..3b4645c6a
--- /dev/null
+++ b/inlong-dashboard/src/metas/index.ts
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+import { useState, useEffect, useCallback } from 'react';
+import type { MetaExportWithBackend, MetaExportWithBackendList } from '@/metas/types';
+import { consumes, defaultValue as defaultConsume } from './consumes';
+import { groups, defaultValue as defaultGroup } from './groups';
+import { clusters, defaultValue as defaultCluster } from './clusters';
+import { nodes, defaultValue as defaultNode } from './nodes';
+import { streams, defaultValue as defaultStream } from './streams';
+import { sources, defaultValue as defaultSource } from './sources';
+
+export interface UseLoadMetaResult {
+  loading: boolean;
+  Entity: MetaExportWithBackend;
+}
+
+export type MetaTypeKeys = 'consume' | 'group' | 'cluster' | 'node' | 'stream' | 'source';
+
+const metasMap: Record<MetaTypeKeys, [MetaExportWithBackendList, string?]> = {
+  consume: [consumes, defaultConsume],
+  group: [groups, defaultGroup],
+  cluster: [clusters, defaultCluster],
+  node: [nodes, defaultNode],
+  stream: [streams, defaultStream],
+  source: [sources, defaultSource],
+};
+
+export const useDefaultMeta = (metaType: MetaTypeKeys) => {
+  const [options = [], defaultValue] = metasMap[metaType];
+  return {
+    defaultValue: defaultValue || options[0].value,
+    options: options.map(item => ({ label: item.label, value: item.value })),
+  };
+};
+
+export const useLoadMeta = (metaType: MetaTypeKeys, subType: string): UseLoadMetaResult => {
+  const [loading, setLoading] = useState<boolean>(false);
+  const [Entity, setEntity] = useState<{ default: MetaExportWithBackend }>();
+
+  const load = useCallback(
+    async subType => {
+      const subList = metasMap[metaType]?.[0];
+      const LoadEntity = subList?.find(item => item.value === subType)?.LoadEntity;
+      if (LoadEntity) {
+        setLoading(true);
+        try {
+          const result = await LoadEntity();
+          setEntity(result);
+        } finally {
+          setLoading(false);
+        }
+      }
+    },
+    [metaType],
+  );
+
+  useEffect(() => {
+    load(subType);
+  }, [subType, load]);
+
+  return {
+    loading,
+    Entity: Entity?.default,
+  };
+};
diff --git a/inlong-dashboard/src/metas/nodes/common/NodeDefaultInfo.ts b/inlong-dashboard/src/metas/nodes/common/NodeDefaultInfo.ts
new file mode 100644
index 000000000..4024634cc
--- /dev/null
+++ b/inlong-dashboard/src/metas/nodes/common/NodeDefaultInfo.ts
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import UserSelect from '@/components/UserSelect';
+import { nodes, defaultValue } from '..';
+
+const { I18n, FormField, TableColumn } = DataWithBackend;
+
+export class NodeDefaultInfo extends DataWithBackend {
+  readonly id: number;
+
+  @FormField({
+    type: 'input',
+    rules: [{ required: true }],
+    props: {
+      maxLength: 128,
+    },
+  })
+  @TableColumn()
+  @I18n('meta.Nodes.Name')
+  name: string;
+
+  @FormField({
+    type: nodes.length > 3 ? 'select' : 'radio',
+    initialValue: defaultValue,
+    rules: [{ required: true }],
+    props: {
+      options: nodes
+        .filter(item => item.value)
+        .map(item => ({
+          label: item.label,
+          value: item.value,
+        })),
+    },
+  })
+  @TableColumn()
+  @I18n('meta.Nodes.Type')
+  type: string;
+
+  @FormField({
+    type: UserSelect,
+    rules: [{ required: true }],
+    props: {
+      mode: 'multiple',
+      currentUserClosable: false,
+    },
+  })
+  @TableColumn()
+  @I18n('meta.Nodes.Owners')
+  inCharges: string;
+
+  clusterTags: string;
+
+  @FormField({
+    type: 'textarea',
+    props: {
+      maxLength: 256,
+    },
+  })
+  @I18n('meta.Nodes.Description')
+  description?: string;
+
+  readonly version?: number;
+
+  parse(data) {
+    return data;
+  }
+
+  stringify(data) {
+    return data;
+  }
+}
diff --git a/inlong-dashboard/src/metas/stream/extends/index.tsx b/inlong-dashboard/src/metas/nodes/common/NodeInfo.ts
similarity index 84%
rename from inlong-dashboard/src/metas/stream/extends/index.tsx
rename to inlong-dashboard/src/metas/nodes/common/NodeInfo.ts
index e278d9701..dea21b5fb 100644
--- a/inlong-dashboard/src/metas/stream/extends/index.tsx
+++ b/inlong-dashboard/src/metas/nodes/common/NodeInfo.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { NodeDefaultInfo } from './NodeDefaultInfo';
 
-export const fieldsExtends: FieldItemType[] = [
-  // You can extended fields here...
-];
+export class NodeInfo extends NodeDefaultInfo {
+  // You can extends NodeInfo at here...
+}
diff --git a/inlong-dashboard/src/metas/nodes/common/dao.ts b/inlong-dashboard/src/metas/nodes/common/dao.ts
new file mode 100644
index 000000000..93c643ed2
--- /dev/null
+++ b/inlong-dashboard/src/metas/nodes/common/dao.ts
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+import { useRequest } from '@/hooks';
+import request from '@/utils/request';
+import type { NodeInfo } from './NodeInfo';
+import { nodes } from '..';
+
+export const useListNodeDao = ({ options }) => {
+  return useRequest<{
+    total: number;
+    list: NodeInfo[];
+  }>(
+    {
+      url: '/node/list',
+      method: 'POST',
+      data: {
+        ...options,
+      },
+    },
+    {
+      refreshDeps: [options],
+    },
+  );
+};
+
+export const useFindNodeDao = ({ onSuccess }) => {
+  return useRequest<NodeInfo, [string]>(
+    async id => {
+      const result = await request(`/node/get/${id}`);
+      const LoadEntity = nodes.find(item => item.value === result.type)?.LoadEntity;
+      const Entity = (await LoadEntity())?.default;
+      const parseData = new Entity().parse(result);
+
+      return {
+        ...parseData,
+        inCharges: result.inCharges?.split(','),
+        clusterTags: result.clusterTags?.split(','),
+      };
+    },
+    {
+      manual: true,
+      onSuccess,
+    },
+  );
+};
+
+export const useSaveNodeDao = () => {
+  return {
+    runAsync: async data => {
+      const LoadEntity = nodes.find(item => item.value === data.type)?.LoadEntity;
+      const Entity = (await LoadEntity())?.default;
+      const stringifyData = new Entity().stringify(data);
+      const result = await request({
+        url: `/node/${Boolean(data.id) ? 'update' : 'save'}`,
+        method: 'POST',
+        data: {
+          ...stringifyData,
+          inCharges: (data.inCharges as any)?.join(','),
+          clusterTags: (data.clusterTags as any)?.join(','),
+        },
+      });
+
+      return result;
+    },
+  };
+};
+
+export const useDeleteNodeDao = () => {
+  return {
+    runAsync: id =>
+      request({
+        url: `/node/delete/${id}`,
+        method: 'DELETE',
+      }),
+  };
+};
diff --git a/inlong-dashboard/src/metas/nodes/hive.tsx b/inlong-dashboard/src/metas/nodes/defaults/Hive.ts
similarity index 73%
rename from inlong-dashboard/src/metas/nodes/hive.tsx
rename to inlong-dashboard/src/metas/nodes/defaults/Hive.ts
index 74436f2dc..35dd66a57 100644
--- a/inlong-dashboard/src/metas/nodes/hive.tsx
+++ b/inlong-dashboard/src/metas/nodes/defaults/Hive.ts
@@ -18,30 +18,35 @@
  */
 
 import i18n from '@/i18n';
-import type { FieldItemType } from '@/metas/common';
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import { NodeInfo } from '../common/NodeInfo';
 
-export const hive: FieldItemType[] = [
-  {
+const { I18n, FormField } = DataWithBackend;
+
+export default class HiveNode extends NodeInfo implements DataWithBackend {
+  @FormField({
     type: 'input',
-    label: 'JDBC URL',
-    name: 'jdbcUrl',
     rules: [{ required: true }],
     initialValue: 'jdbc:hive2://127.0.0.1:10000',
-  },
-  {
+  })
+  @I18n('JDBC URL')
+  jdbcUrl: string;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sinks.Hive.DataPath'),
-    name: 'dataPath',
     rules: [{ required: true }],
     tooltip: i18n.t('meta.Sinks.DataPathHelp'),
     initialValue: 'hdfs://127.0.0.1:9000/user/hive/warehouse/default',
-  },
-  {
+  })
+  @I18n('meta.Sinks.Hive.DataPath')
+  dataPath: string;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sinks.Hive.ConfDir'),
-    name: 'hiveConfDir',
     rules: [{ required: true }],
     tooltip: i18n.t('meta.Sinks.Hive.ConfDirHelp'),
     initialValue: '/usr/hive/conf',
-  },
-];
+  })
+  @I18n('meta.Sinks.Hive.ConfDir')
+  hiveConfDir: string;
+}
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/nodes/defaults/index.ts
similarity index 70%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/nodes/defaults/index.ts
index 8a7c0bd8e..6af7c9114 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/nodes/defaults/index.ts
@@ -17,8 +17,17 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
+export const allDefaultNodes: MetaExportWithBackendList = [
+  {
+    label: 'ALL',
+    value: '',
+    LoadEntity: () => import('../common/NodeInfo').then(r => ({ default: r.NodeInfo })),
+  },
+  {
+    label: 'Hive',
+    value: 'HIVE',
+    LoadEntity: () => import('./Hive'),
+  },
 ];
diff --git a/inlong-dashboard/src/metas/group/extends/index.tsx b/inlong-dashboard/src/metas/nodes/extends/index.ts
similarity index 83%
rename from inlong-dashboard/src/metas/group/extends/index.tsx
rename to inlong-dashboard/src/metas/nodes/extends/index.ts
index 7f5f1eab7..8824d752a 100644
--- a/inlong-dashboard/src/metas/group/extends/index.tsx
+++ b/inlong-dashboard/src/metas/nodes/extends/index.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const groupExtends: FieldItemType[] = [
-  // You can extended group fields here...
+export const allExtendsNodes: MetaExportWithBackendList = [
+  // You can extends at here...
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/nodes/index.ts
similarity index 77%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/nodes/index.ts
index 8a7c0bd8e..b8ddbb30d 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/nodes/index.ts
@@ -17,8 +17,11 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { allDefaultNodes } from './defaults';
+import { allExtendsNodes } from './extends';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export * as dao from './common/dao';
+
+export const nodes = allDefaultNodes.concat(allExtendsNodes);
+
+export const defaultValue = nodes[0].value;
diff --git a/inlong-dashboard/src/metas/nodes/index.tsx b/inlong-dashboard/src/metas/nodes/index.tsx
deleted file mode 100644
index c2d47b5fa..000000000
--- a/inlong-dashboard/src/metas/nodes/index.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.
- */
-
-import React from 'react';
-import i18n from '@/i18n';
-import UserSelect from '@/components/UserSelect';
-import type { FieldItemType } from '@/metas/common';
-import { genFields, genForm, genTable } from '@/metas/common';
-import { hive } from './hive';
-
-const allNodes = [
-  {
-    label: 'Hive',
-    value: 'HIVE',
-    fields: hive,
-  },
-];
-
-const defaultCommonFields: FieldItemType[] = [
-  {
-    type: 'input',
-    label: i18n.t('meta.Nodes.Name'),
-    name: 'name',
-    rules: [{ required: true }],
-    props: {
-      maxLength: 128,
-    },
-    _renderTable: true,
-  },
-  {
-    type: 'select',
-    label: i18n.t('meta.Nodes.Type'),
-    name: 'type',
-    initialValue: allNodes[0].value,
-    rules: [{ required: true }],
-    props: {
-      options: allNodes.map(item => ({
-        label: item.label,
-        value: item.value,
-      })),
-    },
-    _renderTable: true,
-  },
-  {
-    type: <UserSelect mode="multiple" currentUserClosable={false} />,
-    label: i18n.t('meta.Nodes.Owners'),
-    name: 'inCharges',
-    rules: [{ required: true }],
-    _renderTable: true,
-  },
-  {
-    type: 'textarea',
-    label: i18n.t('meta.Nodes.Description'),
-    name: 'description',
-    props: {
-      maxLength: 256,
-    },
-  },
-];
-
-export const nodes = allNodes.map(item => {
-  const itemFields = defaultCommonFields.concat(item.fields);
-  const fields = genFields(itemFields);
-
-  return {
-    ...item,
-    fields,
-    form: genForm(fields),
-    table: genTable(fields),
-  };
-});
diff --git a/inlong-dashboard/src/metas/sources/common/SourceDefaultInfo.ts b/inlong-dashboard/src/metas/sources/common/SourceDefaultInfo.ts
new file mode 100644
index 000000000..c39ce9d4a
--- /dev/null
+++ b/inlong-dashboard/src/metas/sources/common/SourceDefaultInfo.ts
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import { statusList, genStatusTag } from './status';
+import { sources, defaultValue } from '..';
+
+const { I18n, FormField, TableColumn } = DataWithBackend;
+
+export class SourceDefaultInfo extends DataWithBackend {
+  readonly id: number;
+
+  @FormField({
+    // This field is not visible or editable, but form value should exists.
+    type: 'text',
+    hidden: true,
+  })
+  @I18n('inlongGroupId')
+  readonly inlongGroupId: string;
+
+  @FormField({
+    type: 'select',
+    props: values => ({
+      disabled: Boolean(values.id),
+      options: {
+        requestService: {
+          url: '/stream/list',
+          method: 'POST',
+          data: {
+            pageNum: 1,
+            pageSize: 1000,
+            inlongGroupId: values.inlongGroupId,
+          },
+        },
+        requestParams: {
+          formatResult: result =>
+            result?.list.map(item => ({
+              label: item.inlongStreamId,
+              value: item.inlongStreamId,
+            })) || [],
+        },
+      },
+    }),
+    rules: [{ required: true }],
+  })
+  @TableColumn()
+  @I18n('pages.GroupDetail.Sources.DataStreams')
+  inlongStreamId: string;
+
+  @FormField({
+    type: 'input',
+    rules: [{ required: true }],
+    props: values => ({
+      disabled: Boolean(values.id),
+      maxLength: 128,
+    }),
+  })
+  @TableColumn()
+  @I18n('meta.Sources.Name')
+  sourceName: string;
+
+  @FormField({
+    type: sources.length > 3 ? 'select' : 'radio',
+    rules: [{ required: true }],
+    initialValue: defaultValue,
+    props: values => ({
+      disabled: Boolean(values.id),
+      options: sources
+        .filter(item => item.value)
+        .map(item => ({
+          label: item.label,
+          value: item.value,
+        })),
+    }),
+  })
+  @TableColumn()
+  @I18n('meta.Sources.Type')
+  sourceType: string;
+
+  @FormField({
+    type: 'select',
+    props: {
+      allowClear: true,
+      options: statusList,
+      dropdownMatchSelectWidth: false,
+    },
+    visible: false,
+  })
+  @TableColumn({
+    render: text => genStatusTag(text),
+  })
+  @I18n('basic.Status')
+  readonly status: string;
+
+  parse(data) {
+    return data;
+  }
+
+  stringify(data) {
+    return data;
+  }
+}
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/sources/common/SourceInfo.ts
similarity index 84%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/sources/common/SourceInfo.ts
index 8a7c0bd8e..6e19c7b79 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/sources/common/SourceInfo.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { SourceDefaultInfo } from './SourceDefaultInfo';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export class SourceInfo extends SourceDefaultInfo {
+  // You can extends SourceInfo at here...
+}
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/sources/defaults/AutoPush.ts
similarity index 80%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/sources/defaults/AutoPush.ts
index 8a7c0bd8e..b1bc5dc98 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/sources/defaults/AutoPush.ts
@@ -17,8 +17,7 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { DataWithBackend } from '@/metas/DataWithBackend';
+import { SourceInfo } from '../common/SourceInfo';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export default class AutoPushSource extends SourceInfo implements DataWithBackend {}
diff --git a/inlong-dashboard/src/metas/sources/file.ts b/inlong-dashboard/src/metas/sources/defaults/File.ts
similarity index 73%
rename from inlong-dashboard/src/metas/sources/file.ts
rename to inlong-dashboard/src/metas/sources/defaults/File.ts
index cff1696c0..1e2b9e115 100644
--- a/inlong-dashboard/src/metas/sources/file.ts
+++ b/inlong-dashboard/src/metas/sources/defaults/File.ts
@@ -17,15 +17,16 @@
  * under the License.
  */
 
+import { DataWithBackend } from '@/metas/DataWithBackend';
 import i18n from '@/i18n';
 import rulesPattern from '@/utils/pattern';
-import type { FieldItemType } from '@/metas/common';
+import { SourceInfo } from '../common/SourceInfo';
 
-export const file: FieldItemType[] = [
-  {
+const { I18n, FormField, TableColumn } = DataWithBackend;
+
+export default class PulsarSource extends SourceInfo implements DataWithBackend {
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sources.File.DataSourceIP'),
-    name: 'agentIp',
     rules: [
       {
         pattern: rulesPattern.ip,
@@ -36,26 +37,30 @@ export const file: FieldItemType[] = [
     props: values => ({
       disabled: values?.status === 101,
     }),
-    _renderTable: true,
-  },
-  {
+  })
+  @TableColumn()
+  @I18n('meta.Sources.File.DataSourceIP')
+  agentIp: string;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sources.File.FilePath'),
-    name: 'pattern',
     tooltip: i18n.t('meta.Sources.File.FilePathHelp'),
     rules: [{ required: true }],
     props: values => ({
       disabled: values?.status === 101,
     }),
-    _renderTable: true,
-  },
-  {
+  })
+  @TableColumn()
+  @I18n('meta.Sources.File.FilePath')
+  pattern: string;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sources.File.TimeOffset'),
-    name: 'timeOffset',
     tooltip: i18n.t('meta.Sources.File.TimeOffsetHelp'),
     props: values => ({
       disabled: values?.status === 101,
     }),
-  },
-];
+  })
+  @I18n('meta.Sources.File.TimeOffset')
+  timeOffset: string;
+}
diff --git a/inlong-dashboard/src/metas/sources/binLog.ts b/inlong-dashboard/src/metas/sources/defaults/MysqlBinlog.ts
similarity index 71%
rename from inlong-dashboard/src/metas/sources/binLog.ts
rename to inlong-dashboard/src/metas/sources/defaults/MysqlBinlog.ts
index 28c3c59c8..63d34e074 100644
--- a/inlong-dashboard/src/metas/sources/binLog.ts
+++ b/inlong-dashboard/src/metas/sources/defaults/MysqlBinlog.ts
@@ -17,24 +17,26 @@
  * under the License.
  */
 
+import { DataWithBackend } from '@/metas/DataWithBackend';
 import i18n from '@/i18n';
-import type { FieldItemType } from '@/metas/common';
+import { SourceInfo } from '../common/SourceInfo';
 
-export const binLog: FieldItemType[] = [
-  {
-    name: 'hostname',
+const { I18n, FormField, TableColumn } = DataWithBackend;
+
+export default class TubeMqSource extends SourceInfo implements DataWithBackend {
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sources.Db.Server'),
     rules: [{ required: true }],
     props: values => ({
       disabled: values?.status === 101,
     }),
-    _renderTable: true,
-  },
-  {
-    name: 'port',
+  })
+  @TableColumn()
+  @I18n('meta.Sources.Db.Server')
+  hostname: string;
+
+  @FormField({
     type: 'inputnumber',
-    label: i18n.t('meta.Sources.Db.Port'),
     initialValue: 3306,
     rules: [{ required: true }],
     props: values => ({
@@ -42,52 +44,56 @@ export const binLog: FieldItemType[] = [
       min: 0,
       max: 65535,
     }),
-    _renderTable: true,
-  },
-  {
-    name: 'user',
+  })
+  @TableColumn()
+  @I18n('meta.Sources.Db.Port')
+  port: number;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sources.Db.User'),
     rules: [{ required: true }],
     props: values => ({
       disabled: values?.status === 101,
     }),
-  },
-  {
-    name: 'password',
+  })
+  @I18n('meta.Sources.Db.User')
+  user: string;
+
+  @FormField({
     type: 'password',
-    label: i18n.t('meta.Sources.Db.Password'),
     rules: [{ required: true }],
     props: values => ({
       disabled: values?.status === 101,
     }),
-  },
-  {
-    name: 'historyFilename',
+  })
+  @I18n('meta.Sources.Db.Password')
+  password: string;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sources.Db.HistoryFilename'),
     rules: [{ required: true }],
     initialValue: '/data/inlong-agent/.history',
     props: values => ({
       disabled: values?.status === 101,
     }),
-    _renderTable: true,
-  },
-  {
-    name: 'serverTimezone',
+  })
+  @I18n('meta.Sources.Db.HistoryFilename')
+  historyFilename: string;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sources.Db.ServerTimezone'),
     tooltip: 'UTC, UTC+8, Asia/Shanghai, ...',
     initialValue: 'UTC',
     rules: [{ required: true }],
     props: values => ({
       disabled: values?.status === 101,
     }),
-  },
-  {
-    name: 'intervalMs',
+  })
+  @I18n('meta.Sources.Db.ServerTimezone')
+  serverTimezone: string;
+
+  @FormField({
     type: 'inputnumber',
-    label: i18n.t('meta.Sources.Db.IntervalMs'),
     initialValue: 1000,
     rules: [{ required: true }],
     suffix: 'ms',
@@ -96,11 +102,12 @@ export const binLog: FieldItemType[] = [
       min: 1000,
       max: 3600000,
     }),
-  },
-  {
-    name: 'allMigration',
+  })
+  @I18n('meta.Sources.Db.IntervalMs')
+  intervalMs: number;
+
+  @FormField({
     type: 'radio',
-    label: i18n.t('meta.Sources.Db.AllMigration'),
     rules: [{ required: true }],
     initialValue: false,
     props: values => ({
@@ -116,16 +123,19 @@ export const binLog: FieldItemType[] = [
         },
       ],
     }),
-  },
-  {
-    name: 'tableWhiteList',
+  })
+  @I18n('meta.Sources.Db.AllMigration')
+  allMigration: boolean;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Sources.Db.TableWhiteList'),
     tooltip: i18n.t('meta.Sources.Db.TableWhiteListHelp'),
     rules: [{ required: true }],
     props: values => ({
       disabled: values?.status === 101,
     }),
     visible: values => !values?.allMigration,
-  },
-];
+  })
+  @I18n('meta.Sources.Db.TableWhiteList')
+  tableWhiteList: boolean;
+}
diff --git a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx b/inlong-dashboard/src/metas/sources/defaults/index.ts
similarity index 59%
copy from inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
copy to inlong-dashboard/src/metas/sources/defaults/index.ts
index 9641bacd1..c3988449f 100644
--- a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
+++ b/inlong-dashboard/src/metas/sources/defaults/index.ts
@@ -17,18 +17,28 @@
  * under the License.
  */
 
-import { streamForm } from '@/metas/stream';
-import { pickObjectArray } from '@/utils';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const getFilterFormContent = (defaultValues = {} as any) => [
+export const allDefaultSources: MetaExportWithBackendList = [
   {
-    type: 'inputsearch',
-    name: 'keyword',
-    initialValue: defaultValues.keyword,
+    label: 'ALL',
+    value: '',
+    LoadEntity: () => import('../common/SourceInfo').then(r => ({ default: r.SourceInfo })),
+  },
+
+  {
+    label: 'Auto Push',
+    value: 'AUTO_PUSH',
+    LoadEntity: () => import('./AutoPush'),
+  },
+  {
+    label: 'MySQL BinLog',
+    value: 'MYSQL_BINLOG',
+    LoadEntity: () => import('./MysqlBinlog'),
+  },
+  {
+    label: 'File',
+    value: 'FILE',
+    LoadEntity: () => import('./File'),
   },
-  ...pickObjectArray(['status'], streamForm).map(item => ({
-    ...item,
-    visible: true,
-    initialValue: defaultValues[item.name],
-  })),
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/sources/extends/index.ts
similarity index 83%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/sources/extends/index.ts
index 8a7c0bd8e..38e4eb2d9 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/sources/extends/index.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
+export const allExtendsSources: MetaExportWithBackendList = [
+  // You can extends at here...
 ];
diff --git a/inlong-dashboard/src/metas/sources/index.ts b/inlong-dashboard/src/metas/sources/index.ts
index e6817cf82..58928e352 100644
--- a/inlong-dashboard/src/metas/sources/index.ts
+++ b/inlong-dashboard/src/metas/sources/index.ts
@@ -17,84 +17,9 @@
  * under the License.
  */
 
-import i18n from '@/i18n';
-import type { FieldItemType } from '@/metas/common';
-import { genFields, genForm, genTable } from '@/metas/common';
-import { statusList, genStatusTag } from './common/status';
-import { autoPush } from './autoPush';
-import { binLog } from './binLog';
-import { file } from './file';
+import { allDefaultSources } from './defaults';
+import { allExtendsSources } from './extends';
 
-const allSources = [
-  {
-    label: 'MySQL BinLog',
-    value: 'MYSQL_BINLOG',
-    fields: binLog,
-  },
-  {
-    label: 'File',
-    value: 'FILE',
-    fields: file,
-  },
-  {
-    label: 'Auto Push',
-    value: 'AUTO_PUSH',
-    fields: autoPush,
-  },
-];
+export const sources = allDefaultSources.concat(allExtendsSources);
 
-const defaultCommonFields: FieldItemType[] = [
-  {
-    name: 'sourceName',
-    type: 'input',
-    label: i18n.t('meta.Sources.Name'),
-    rules: [{ required: true }],
-    props: values => ({
-      disabled: !!values.id,
-      maxLength: 128,
-    }),
-    _renderTable: true,
-  },
-  {
-    name: 'sourceType',
-    type: 'select',
-    label: i18n.t('meta.Sources.Type'),
-    rules: [{ required: true }],
-    initialValue: allSources[0].value,
-    props: values => ({
-      disabled: !!values.id,
-      options: allSources.map(item => ({
-        label: item.label,
-        value: item.value,
-      })),
-    }),
-  },
-  {
-    name: 'status',
-    type: 'select',
-    label: i18n.t('basic.Status'),
-    props: {
-      allowClear: true,
-      options: statusList,
-      dropdownMatchSelectWidth: false,
-    },
-    visible: false,
-    _renderTable: {
-      render: text => genStatusTag(text),
-    },
-  },
-];
-
-export const sources = allSources.map(item => {
-  const itemFields = defaultCommonFields.concat(item.fields);
-  const fields = genFields(itemFields);
-
-  return {
-    ...item,
-    fields,
-    form: genForm(fields),
-    table: genTable(fields),
-    toFormValues: null,
-    toSubmitValues: null,
-  };
-});
+export const defaultValue = sources[0].value;
diff --git a/inlong-dashboard/src/metas/stream/index.tsx b/inlong-dashboard/src/metas/streams/common/StreamDefaultInfo.ts
similarity index 75%
rename from inlong-dashboard/src/metas/stream/index.tsx
rename to inlong-dashboard/src/metas/streams/common/StreamDefaultInfo.ts
index 5fbddffbf..87f153467 100644
--- a/inlong-dashboard/src/metas/stream/index.tsx
+++ b/inlong-dashboard/src/metas/streams/common/StreamDefaultInfo.ts
@@ -17,19 +17,19 @@
  * under the License.
  */
 
-import EditableTable from '@/components/EditableTable';
+import { DataWithBackend } from '@/metas/DataWithBackend';
 import i18n from '@/i18n';
+import EditableTable from '@/components/EditableTable';
 import { fieldTypes as sourceFieldsTypes } from '@/metas/sinks/common/sourceFields';
-import type { FieldItemType } from '@/metas/common';
-import { genFields, genForm, genTable } from '@/metas/common';
 import { statusList, genStatusTag } from './status';
-import { fieldsExtends } from './extends';
 
-const fieldsDefault: FieldItemType[] = [
-  {
+const { I18n, FormField, TableColumn } = DataWithBackend;
+
+export class StreamDefaultInfo extends DataWithBackend {
+  readonly id: number;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Stream.StreamId'),
-    name: 'inlongStreamId',
     props: {
       maxLength: 32,
     },
@@ -40,55 +40,53 @@ const fieldsDefault: FieldItemType[] = [
         message: i18n.t('meta.Stream.StreamIdRules'),
       },
     ],
-    _renderTable: true,
-  },
-  {
+  })
+  @TableColumn()
+  @I18n('meta.Stream.StreamId')
+  inlongStreamId: string;
+
+  @FormField({
     type: 'input',
-    label: i18n.t('meta.Stream.StreamName'),
-    name: 'name',
-    _renderTable: true,
-  },
-  {
+  })
+  @TableColumn()
+  @I18n('meta.Stream.StreamName')
+  name: string;
+
+  @FormField({
     type: 'textarea',
-    label: i18n.t('meta.Stream.Description'),
-    name: 'description',
     props: {
       showCount: true,
       maxLength: 256,
     },
-  },
-  {
-    type: 'text',
-    label: i18n.t('basic.Creator'),
-    name: 'creator',
-    visible: false,
-    _renderTable: true,
-  },
-  {
-    type: 'text',
-    label: i18n.t('basic.CreateTime'),
-    name: 'createTime',
-    visible: false,
-    _renderTable: true,
-  },
-  {
+  })
+  @I18n('meta.Stream.Description')
+  description: string;
+
+  @TableColumn()
+  @I18n('basic.Creator')
+  readonly creator: string;
+
+  @TableColumn()
+  @I18n('basic.CreateTime')
+  readonly createTime: string;
+
+  @FormField({
     type: 'select',
-    label: i18n.t('basic.Status'),
-    name: 'status',
     props: {
       allowClear: true,
       options: statusList,
       dropdownMatchSelectWidth: false,
     },
     visible: false,
-    _renderTable: {
-      render: text => genStatusTag(text),
-    },
-  },
-  {
+  })
+  @TableColumn({
+    render: text => genStatusTag(text),
+  })
+  @I18n('basic.Status')
+  status: string;
+
+  @FormField({
     type: 'radio',
-    label: i18n.t('meta.Stream.DataType'),
-    name: 'dataType',
     initialValue: 'CSV',
     tooltip: i18n.t('meta.Stream.DataTypeCsvHelp'),
     props: {
@@ -112,11 +110,12 @@ const fieldsDefault: FieldItemType[] = [
       ],
     },
     rules: [{ required: true }],
-  },
-  {
+  })
+  @I18n('meta.Stream.DataType')
+  dataType: string;
+
+  @FormField({
     type: 'radio',
-    label: i18n.t('meta.Stream.DataEncoding'),
-    name: 'dataEncoding',
     initialValue: 'UTF-8',
     props: {
       options: [
@@ -131,11 +130,12 @@ const fieldsDefault: FieldItemType[] = [
       ],
     },
     rules: [{ required: true }],
-  },
-  {
+  })
+  @I18n('meta.Stream.DataEncoding')
+  dataEncoding: string;
+
+  @FormField({
     type: 'select',
-    label: i18n.t('meta.Stream.DataSeparator'),
-    name: 'dataSeparator',
     initialValue: '124',
     props: {
       dropdownMatchSelectWidth: false,
@@ -180,11 +180,12 @@ const fieldsDefault: FieldItemType[] = [
         max: 127,
       },
     ],
-  },
-  {
+  })
+  @I18n('meta.Stream.DataSeparator')
+  dataSeparator: string;
+
+  @FormField({
     type: EditableTable,
-    label: i18n.t('meta.Stream.SourceDataField'),
-    name: 'rowTypeFields',
     props: {
       size: 'small',
       columns: [
@@ -215,11 +216,15 @@ const fieldsDefault: FieldItemType[] = [
         },
       ],
     },
-  },
-];
-
-export const stream = genFields(fieldsDefault, fieldsExtends);
+  })
+  @I18n('meta.Stream.SourceDataField')
+  rowTypeFields: Record<string, string>[];
 
-export const streamForm = genForm(stream);
+  parse(data) {
+    return data;
+  }
 
-export const streamTable = genTable(stream);
+  stringify(data) {
+    return data;
+  }
+}
diff --git a/inlong-dashboard/src/metas/sources/autoPush.ts b/inlong-dashboard/src/metas/streams/common/StreamInfo.ts
similarity index 84%
rename from inlong-dashboard/src/metas/sources/autoPush.ts
rename to inlong-dashboard/src/metas/streams/common/StreamInfo.ts
index 6fe579a35..80114c739 100644
--- a/inlong-dashboard/src/metas/sources/autoPush.ts
+++ b/inlong-dashboard/src/metas/streams/common/StreamInfo.ts
@@ -17,6 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { StreamDefaultInfo } from './StreamDefaultInfo';
 
-export const autoPush: FieldItemType[] = [];
+export class StreamInfo extends StreamDefaultInfo {
+  // You can extends StreamInfo at here...
+}
diff --git a/inlong-dashboard/src/metas/stream/status.tsx b/inlong-dashboard/src/metas/streams/common/status.tsx
similarity index 100%
rename from inlong-dashboard/src/metas/stream/status.tsx
rename to inlong-dashboard/src/metas/streams/common/status.tsx
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/streams/defaults/index.ts
similarity index 75%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/streams/defaults/index.ts
index 8a7c0bd8e..7d92d260d 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/streams/defaults/index.ts
@@ -17,8 +17,12 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
+export const allDefaultStreams: MetaExportWithBackendList = [
+  {
+    label: 'ALL',
+    value: '',
+    LoadEntity: () => import('../common/StreamInfo').then(r => ({ default: r.StreamInfo })),
+  },
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/streams/extends/index.ts
similarity index 83%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/streams/extends/index.ts
index 8a7c0bd8e..309324157 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/streams/extends/index.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import type { MetaExportWithBackendList } from '@/metas/types';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
+export const allExtendsStreams: MetaExportWithBackendList = [
+  // You can extends at here...
 ];
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/streams/index.ts
similarity index 79%
copy from inlong-dashboard/src/metas/consume/extends/index.tsx
copy to inlong-dashboard/src/metas/streams/index.ts
index 8a7c0bd8e..2b7f10519 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/streams/index.ts
@@ -17,8 +17,9 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { allDefaultStreams } from './defaults';
+import { allExtendsStreams } from './extends';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+export const streams = allDefaultStreams.concat(allExtendsStreams);
+
+export const defaultValue = streams[0].value;
diff --git a/inlong-dashboard/src/metas/consume/extends/index.tsx b/inlong-dashboard/src/metas/types.ts
similarity index 53%
rename from inlong-dashboard/src/metas/consume/extends/index.tsx
rename to inlong-dashboard/src/metas/types.ts
index 8a7c0bd8e..00c8c1548 100644
--- a/inlong-dashboard/src/metas/consume/extends/index.tsx
+++ b/inlong-dashboard/src/metas/types.ts
@@ -17,8 +17,32 @@
  * under the License.
  */
 
-import type { FieldItemType } from '@/metas/common';
+import { DataStatic } from './DataStatic';
+import { DataWithBackend } from './DataWithBackend';
 
-export const consumeExtends: FieldItemType[] = [
-  // You can extended consume fields here...
-];
+class MetaClassStatic implements DataStatic {}
+
+export type MetaExportStatic = typeof MetaClassStatic;
+
+export type MetaExportStaticList = {
+  label: string;
+  value: string;
+  LoadEntity: () => Promise<{ default: MetaExportStatic }>;
+}[];
+
+class MetaClassWithBackend extends DataWithBackend implements DataWithBackend {
+  parse(data) {
+    return data;
+  }
+  stringify(data) {
+    return data;
+  }
+}
+
+export type MetaExportWithBackend = typeof MetaClassWithBackend;
+
+export type MetaExportWithBackendList = {
+  label: string;
+  value: string;
+  LoadEntity: () => Promise<{ default: MetaExportWithBackend }>;
+}[];
diff --git a/inlong-dashboard/src/pages/Clusters/CreateModal.tsx b/inlong-dashboard/src/pages/Clusters/CreateModal.tsx
index 6e8ff38c8..3618f1914 100644
--- a/inlong-dashboard/src/pages/Clusters/CreateModal.tsx
+++ b/inlong-dashboard/src/pages/Clusters/CreateModal.tsx
@@ -23,7 +23,7 @@ import { ModalProps } from 'antd/es/modal';
 import FormGenerator, { useForm } from '@/components/FormGenerator';
 import { useRequest, useUpdateEffect } from '@/hooks';
 import request from '@/utils/request';
-import { clusters } from '@/metas/clusters';
+import { useDefaultMeta, useLoadMeta } from '@/metas';
 import i18n from '@/i18n';
 
 export interface Props extends ModalProps {
@@ -34,7 +34,9 @@ export interface Props extends ModalProps {
 const Comp: React.FC<Props> = ({ id, ...modalProps }) => {
   const [form] = useForm();
 
-  const [type, setType] = useState(clusters[0].value);
+  const { defaultValue } = useDefaultMeta('cluster');
+
+  const [type, setType] = useState(defaultValue);
 
   const { data: savedData, run: getData } = useRequest(
     id => ({
@@ -82,14 +84,15 @@ const Comp: React.FC<Props> = ({ id, ...modalProps }) => {
       }
     } else {
       form.resetFields();
-      setType(clusters[0].value);
+      setType(defaultValue);
     }
   }, [modalProps.visible]);
 
+  const { Entity } = useLoadMeta('cluster', type);
+
   const content = useMemo(() => {
-    const current = clusters.find(item => item.value === type);
-    return current?.form;
-  }, [type]);
+    return Entity ? Entity.FieldList : [];
+  }, [Entity]);
 
   return (
     <Modal
diff --git a/inlong-dashboard/src/pages/Clusters/index.tsx b/inlong-dashboard/src/pages/Clusters/index.tsx
index 594b79b9a..0ef46ce1d 100644
--- a/inlong-dashboard/src/pages/Clusters/index.tsx
+++ b/inlong-dashboard/src/pages/Clusters/index.tsx
@@ -25,37 +25,19 @@ import HighTable from '@/components/HighTable';
 import { PageContainer } from '@/components/PageContainer';
 import { defaultSize } from '@/configs/pagination';
 import { useRequest } from '@/hooks';
-import { clusters } from '@/metas/clusters';
+import { useDefaultMeta, useLoadMeta } from '@/metas';
 import CreateModal from './CreateModal';
 import request from '@/utils/request';
 import { timestampFormat } from '@/utils';
 
-const getFilterFormContent = defaultValues => [
-  {
-    type: 'inputsearch',
-    name: 'keyword',
-  },
-  {
-    type: 'radiobutton',
-    name: 'type',
-    label: i18n.t('pages.Clusters.Type'),
-    initialValue: defaultValues.type,
-    props: {
-      buttonStyle: 'solid',
-      options: clusters.map(item => ({
-        label: item.label,
-        value: item.value,
-      })),
-    },
-  },
-];
-
 const Comp: React.FC = () => {
+  const { defaultValue, options: clusters } = useDefaultMeta('cluster');
+
   const [options, setOptions] = useState({
     keyword: '',
     pageSize: defaultSize,
     pageNum: 1,
-    type: clusters[0].value,
+    type: defaultValue,
   });
 
   const [createModal, setCreateModal] = useState<Record<string, unknown>>({
@@ -122,11 +104,32 @@ const Comp: React.FC = () => {
     total: data?.total,
   };
 
+  const getFilterFormContent = useCallback(
+    defaultValues => [
+      {
+        type: 'inputsearch',
+        name: 'keyword',
+      },
+      {
+        type: 'radiobutton',
+        name: 'type',
+        label: i18n.t('pages.Clusters.Type'),
+        initialValue: defaultValues.type,
+        props: {
+          buttonStyle: 'solid',
+          options: clusters,
+        },
+      },
+    ],
+    [clusters],
+  );
+
+  const { Entity } = useLoadMeta('cluster', options.type);
+
   const columns = useMemo(() => {
-    const current = clusters.find(item => item.value === options.type);
-    if (!current?.table) return [];
+    if (!Entity) return [];
 
-    return current.table.concat([
+    return Entity.ColumnList?.concat([
       {
         title: i18n.t('pages.Clusters.LastModifier'),
         dataIndex: 'modifier',
@@ -159,7 +162,7 @@ const Comp: React.FC = () => {
         ),
       } as any,
     ]);
-  }, [options.type, onDelete]);
+  }, [Entity, onDelete]);
 
   return (
     <PageContainer useDefaultBreadcrumb={false}>
diff --git a/inlong-dashboard/src/pages/ConsumeDashboard/config.tsx b/inlong-dashboard/src/pages/ConsumeDashboard/config.tsx
index 48b9a2df6..486a31d38 100644
--- a/inlong-dashboard/src/pages/ConsumeDashboard/config.tsx
+++ b/inlong-dashboard/src/pages/ConsumeDashboard/config.tsx
@@ -22,8 +22,8 @@ import { Button } from 'antd';
 import { Link } from 'react-router-dom';
 import i18n from '@/i18n';
 import { DashTotal, DashToBeAssigned, DashPending, DashRejected } from '@/components/Icons';
-import { consumeForm, consumeTable } from '@/metas/consume';
-import { pickObjectArray } from '@/utils';
+import { useDefaultMeta, useLoadMeta } from '@/metas';
+import { statusList, lastConsumerStatusList } from '@/metas/consumes/common/status';
 
 export const dashCardList = [
   {
@@ -48,55 +48,72 @@ export const dashCardList = [
   },
 ];
 
-export const getFilterFormContent = defaultValues =>
-  [
-    {
-      type: 'inputsearch',
-      name: 'keyword',
-      initialValue: defaultValues.keyword,
-      props: {
-        allowClear: true,
-      },
+export const getFilterFormContent = defaultValues => [
+  {
+    type: 'inputsearch',
+    name: 'keyword',
+    initialValue: defaultValues.keyword,
+    props: {
+      allowClear: true,
+    },
+  },
+  {
+    type: 'select',
+    name: 'status',
+    label: i18n.t('basic.Status'),
+    initialValue: defaultValues.status,
+    props: {
+      allowClear: true,
+      options: statusList,
+      dropdownMatchSelectWidth: false,
+    },
+  },
+  {
+    type: 'select',
+    name: 'lastConsumeStatus',
+    label: i18n.t('pages.ConsumeDashboard.config.OperatingStatus'),
+    initialValue: defaultValues.lastConsumeStatus,
+    props: {
+      allowClear: true,
+      options: lastConsumerStatusList,
+      dropdownMatchSelectWidth: false,
     },
-  ].concat(
-    pickObjectArray(['status', 'lastConsumeStatus'], consumeForm).map(item => ({
-      ...item,
-      visible: true,
-      initialValue: defaultValues[item.name],
-    })),
-  );
+  },
+];
+
+export const useColumns = ({ onDelete }) => {
+  const { defaultValue } = useDefaultMeta('consume');
+
+  const { Entity } = useLoadMeta('consume', defaultValue);
 
-export const getColumns = ({ onDelete }) => {
   const genCreateUrl = record => `/consume/create/${record.id}`;
   const genDetailUrl = record =>
     [0, 10].includes(record.status) ? genCreateUrl(record) : `/consume/detail/${record.id}`;
 
-  return consumeTable
-    .map(item => {
-      if (item.dataIndex === 'consumerGroup') {
-        return { ...item, render: (text, record) => <Link to={genDetailUrl(record)}>{text}</Link> };
-      }
-      return item;
-    })
-    .concat([
-      {
-        title: i18n.t('basic.Operating'),
-        dataIndex: 'action',
-        render: (text, record) => (
-          <>
+  return Entity?.ColumnList?.map(item => {
+    if (item.dataIndex === 'consumerGroup') {
+      return { ...item, render: (text, record) => <Link to={genDetailUrl(record)}>{text}</Link> };
+    }
+    return item;
+  }).concat([
+    {
+      title: i18n.t('basic.Operating'),
+      dataIndex: 'action',
+      render: (text, record) => (
+        <>
+          <Button type="link">
+            <Link to={genDetailUrl(record)}>{i18n.t('basic.Detail')}</Link>
+          </Button>
+          {[20, 22].includes(record?.status) && (
             <Button type="link">
-              <Link to={genDetailUrl(record)}>{i18n.t('basic.Detail')}</Link>
-            </Button>
-            {[20, 22].includes(record?.status) && (
-              <Button type="link">
-                <Link to={genCreateUrl(record)}>{i18n.t('basic.Edit')}</Link>
-              </Button>
-            )}
-            <Button type="link" onClick={() => onDelete(record)}>
-              {i18n.t('basic.Delete')}
+              <Link to={genCreateUrl(record)}>{i18n.t('basic.Edit')}</Link>
             </Button>
-          </>
-        ),
-      },
-    ]);
+          )}
+          <Button type="link" onClick={() => onDelete(record)}>
+            {i18n.t('basic.Delete')}
+          </Button>
+        </>
+      ),
+    },
+  ]);
 };
diff --git a/inlong-dashboard/src/pages/ConsumeDashboard/index.tsx b/inlong-dashboard/src/pages/ConsumeDashboard/index.tsx
index 6c25382e2..d47c55678 100644
--- a/inlong-dashboard/src/pages/ConsumeDashboard/index.tsx
+++ b/inlong-dashboard/src/pages/ConsumeDashboard/index.tsx
@@ -26,7 +26,7 @@ import { useRequest, useHistory } from '@/hooks';
 import { useTranslation } from 'react-i18next';
 import request from '@/utils/request';
 import { defaultSize } from '@/configs/pagination';
-import { dashCardList, getFilterFormContent, getColumns } from './config';
+import { dashCardList, getFilterFormContent, useColumns } from './config';
 
 const Comp: React.FC = () => {
   const { t } = useTranslation();
@@ -97,6 +97,8 @@ const Comp: React.FC = () => {
     title: summary[item.dataIndex] || 0,
   }));
 
+  const columns = useColumns({ onDelete });
+
   return (
     <PageContainer useDefaultBreadcrumb={false} useDefaultContainer={false}>
       <Container>
@@ -116,7 +118,7 @@ const Comp: React.FC = () => {
               onFilter,
             }}
             table={{
-              columns: getColumns({ onDelete }),
+              columns,
               rowKey: 'id',
               dataSource: data?.list,
               pagination,
diff --git a/inlong-dashboard/src/pages/ConsumeDetail/Info/config.tsx b/inlong-dashboard/src/pages/ConsumeDetail/Info/config.tsx
index 157d81313..c9feafaea 100644
--- a/inlong-dashboard/src/pages/ConsumeDetail/Info/config.tsx
+++ b/inlong-dashboard/src/pages/ConsumeDetail/Info/config.tsx
@@ -17,12 +17,14 @@
  * under the License.
  */
 
-import { consumeForm } from '@/metas/consume';
+import { useLoadMeta } from '@/metas';
 import { excludeObjectArray } from '@/utils';
 
-export const getFormContent = ({ editing, isCreate }) => {
+export const useFormContent = ({ mqType, editing, isCreate }) => {
+  const { Entity } = useLoadMeta('consume', mqType);
+
   const excludeKeys = isCreate ? ['masterUrl'] : [];
-  const fields = excludeObjectArray(excludeKeys, consumeForm);
+  const fields = excludeObjectArray(excludeKeys, Entity?.FieldList || []);
 
   return isCreate
     ? fields
diff --git a/inlong-dashboard/src/pages/ConsumeDetail/Info/index.tsx b/inlong-dashboard/src/pages/ConsumeDetail/Info/index.tsx
index c8279f040..5ca8fde8c 100644
--- a/inlong-dashboard/src/pages/ConsumeDetail/Info/index.tsx
+++ b/inlong-dashboard/src/pages/ConsumeDetail/Info/index.tsx
@@ -17,14 +17,15 @@
  * under the License.
  */
 
-import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
+import React, { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
 import { Button, message, Space } from 'antd';
 import FormGenerator, { useForm } from '@/components/FormGenerator';
 import { useBoolean, useRequest } from '@/hooks';
 import request from '@/utils/request';
 import { useTranslation } from 'react-i18next';
+import { useDefaultMeta } from '@/metas';
 import { CommonInterface } from '../common';
-import { getFormContent } from './config';
+import { useFormContent } from './config';
 
 type Props = CommonInterface;
 
@@ -32,6 +33,10 @@ const Comp = ({ id, readonly, isCreate }: Props, ref) => {
   const { t } = useTranslation();
   const [editing, { setTrue, setFalse }] = useBoolean(false);
 
+  const { defaultValue } = useDefaultMeta('consume');
+
+  const [mqType, setMqType] = useState(defaultValue);
+
   const [form] = useForm();
 
   const isUpdate = useMemo(() => {
@@ -47,6 +52,7 @@ const Comp = ({ id, readonly, isCreate }: Props, ref) => {
     }),
     onSuccess: data => {
       form.setFieldsValue(data);
+      setMqType(data.mqType);
     },
   });
 
@@ -86,15 +92,19 @@ const Comp = ({ id, readonly, isCreate }: Props, ref) => {
     setFalse();
   };
 
+  const formContent = useFormContent({
+    mqType,
+    editing,
+    isCreate,
+  });
+
   return (
     <div style={{ position: 'relative' }}>
       <FormGenerator
         form={form}
-        content={getFormContent({
-          editing,
-          isCreate,
-        })}
+        content={formContent}
         initialValues={data}
+        onValuesChange={(c, values) => setMqType(values.mqType)}
         useMaxWidth={800}
       />
 
diff --git a/inlong-dashboard/src/pages/GroupDashboard/config.tsx b/inlong-dashboard/src/pages/GroupDashboard/config.tsx
index d6e3731df..9707d6ea7 100644
--- a/inlong-dashboard/src/pages/GroupDashboard/config.tsx
+++ b/inlong-dashboard/src/pages/GroupDashboard/config.tsx
@@ -22,8 +22,8 @@ import { Link } from 'react-router-dom';
 import i18n from '@/i18n';
 import { DashTotal, DashToBeAssigned, DashPending, DashRejected } from '@/components/Icons';
 import { Button } from 'antd';
-import { groupForm, groupTable } from '@/metas/group';
-import { pickObjectArray } from '@/utils';
+import { useDefaultMeta, useLoadMeta } from '@/metas';
+import { statusList } from '@/metas/groups/common/status';
 
 export const dashCardList = [
   {
@@ -48,62 +48,68 @@ export const dashCardList = [
   },
 ];
 
-export const getFilterFormContent = defaultValues =>
-  [
-    {
-      type: 'inputsearch',
-      name: 'keyword',
-      initialValue: defaultValues.keyword,
-      props: {
-        allowClear: true,
-      },
+export const getFilterFormContent = defaultValues => [
+  {
+    type: 'inputsearch',
+    name: 'keyword',
+    initialValue: defaultValues.keyword,
+    props: {
+      allowClear: true,
+    },
+  },
+  {
+    type: 'select',
+    name: 'status',
+    label: i18n.t('basic.Status'),
+    initialValue: defaultValues.status,
+    props: {
+      allowClear: true,
+      options: statusList,
+      dropdownMatchSelectWidth: false,
     },
-  ].concat(
-    pickObjectArray(['status'], groupForm).map(item => ({
-      ...item,
-      visible: true,
-      initialValue: defaultValues[item.name],
-    })),
-  );
+  },
+];
+
+export const useColumns = ({ onDelete, openModal }) => {
+  const { defaultValue } = useDefaultMeta('group');
+
+  const { Entity } = useLoadMeta('group', defaultValue);
 
-export const getColumns = ({ onDelete, openModal }) => {
   const genCreateUrl = record => `/group/create/${record.inlongGroupId}`;
   const genDetailUrl = record =>
     [0, 100].includes(record.status)
       ? genCreateUrl(record)
       : `/group/detail/${record.inlongGroupId}`;
 
-  return groupTable
-    .map(item => {
-      if (item.dataIndex === 'inlongGroupId') {
-        return { ...item, render: (text, record) => <Link to={genDetailUrl(record)}>{text}</Link> };
-      }
-      return item;
-    })
-    .concat([
-      {
-        title: i18n.t('basic.Operating'),
-        dataIndex: 'action',
-        render: (text, record) => (
-          <>
+  return Entity?.ColumnList?.map(item => {
+    if (item.dataIndex === 'inlongGroupId') {
+      return { ...item, render: (text, record) => <Link to={genDetailUrl(record)}>{text}</Link> };
+    }
+    return item;
+  }).concat([
+    {
+      title: i18n.t('basic.Operating'),
+      dataIndex: 'action',
+      render: (text, record) => (
+        <>
+          <Button type="link">
+            <Link to={genDetailUrl(record)}>{i18n.t('basic.Detail')}</Link>
+          </Button>
+          {[102].includes(record?.status) && (
             <Button type="link">
-              <Link to={genDetailUrl(record)}>{i18n.t('basic.Detail')}</Link>
+              <Link to={genCreateUrl(record)}>{i18n.t('basic.Edit')}</Link>
             </Button>
-            {[102].includes(record?.status) && (
-              <Button type="link">
-                <Link to={genCreateUrl(record)}>{i18n.t('basic.Edit')}</Link>
-              </Button>
-            )}
-            <Button type="link" onClick={() => onDelete(record)}>
-              {i18n.t('basic.Delete')}
+          )}
+          <Button type="link" onClick={() => onDelete(record)}>
+            {i18n.t('basic.Delete')}
+          </Button>
+          {record?.status && (record?.status === 120 || record?.status === 130) && (
+            <Button type="link" onClick={() => openModal(record)}>
+              {i18n.t('pages.GroupDashboard.config.ExecuteLog')}
             </Button>
-            {record?.status && (record?.status === 120 || record?.status === 130) && (
-              <Button type="link" onClick={() => openModal(record)}>
-                {i18n.t('pages.GroupDashboard.config.ExecuteLog')}
-              </Button>
-            )}
-          </>
-        ),
-      },
-    ]);
+          )}
+        </>
+      ),
+    },
+  ]);
 };
diff --git a/inlong-dashboard/src/pages/GroupDashboard/index.tsx b/inlong-dashboard/src/pages/GroupDashboard/index.tsx
index 2df87be92..605407436 100644
--- a/inlong-dashboard/src/pages/GroupDashboard/index.tsx
+++ b/inlong-dashboard/src/pages/GroupDashboard/index.tsx
@@ -27,7 +27,7 @@ import { useTranslation } from 'react-i18next';
 import { useRequest, useHistory } from '@/hooks';
 import { defaultSize } from '@/configs/pagination';
 import ExecutionLogModal from './ExecutionLogModal';
-import { dashCardList, getFilterFormContent, getColumns } from './config';
+import { dashCardList, getFilterFormContent, useColumns } from './config';
 
 const Comp: React.FC = () => {
   const { t } = useTranslation();
@@ -108,6 +108,8 @@ const Comp: React.FC = () => {
     title: summary[item.dataIndex] || 0,
   }));
 
+  const columns = useColumns({ onDelete, openModal });
+
   return (
     <PageContainer useDefaultBreadcrumb={false} useDefaultContainer={false}>
       <Container>
@@ -127,7 +129,7 @@ const Comp: React.FC = () => {
               onFilter,
             }}
             table={{
-              columns: getColumns({ onDelete, openModal }),
+              columns,
               rowKey: 'id',
               dataSource: data?.list,
               pagination,
diff --git a/inlong-dashboard/src/pages/GroupDetail/DataSources/DetailModal.tsx b/inlong-dashboard/src/pages/GroupDetail/DataSources/DetailModal.tsx
index 22fad1916..3cd253ea6 100644
--- a/inlong-dashboard/src/pages/GroupDetail/DataSources/DetailModal.tsx
+++ b/inlong-dashboard/src/pages/GroupDetail/DataSources/DetailModal.tsx
@@ -17,14 +17,13 @@
  * under the License.
  */
 
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
 import { Modal, message } from 'antd';
 import { ModalProps } from 'antd/es/modal';
 import FormGenerator, { useForm } from '@/components/FormGenerator';
 import { useRequest, useUpdateEffect } from '@/hooks';
 import { useTranslation } from 'react-i18next';
-import { FormItemProps } from '@/components/FormGenerator';
-import { sources } from '@/metas/sources';
+import { useDefaultMeta, useLoadMeta } from '@/metas';
 import request from '@/utils/request';
 
 export interface Props extends ModalProps {
@@ -33,46 +32,23 @@ export interface Props extends ModalProps {
   inlongGroupId?: string;
 }
 
-const sourcesMap: Record<string, typeof sources[0]> = sources.reduce(
-  (acc, cur) => ({
-    ...acc,
-    [cur.value]: cur,
-  }),
-  {},
-);
-
 const Comp: React.FC<Props> = ({ id, inlongGroupId, ...modalProps }) => {
   const [form] = useForm();
   const { t } = useTranslation();
 
-  const [type, setType] = useState(sources[0].value);
+  const { defaultValue } = useDefaultMeta('source');
 
-  const toFormVals = useCallback(
-    v => {
-      const mapFunc = sourcesMap[type]?.toFormValues;
-      return mapFunc ? mapFunc(v) : v;
-    },
-    [type],
-  );
+  const [type, setType] = useState(defaultValue);
 
-  const toSubmitVals = useCallback(
-    v => {
-      const mapFunc = sourcesMap[type]?.toSubmitValues;
-      return mapFunc ? mapFunc(v) : v;
-    },
-    [type],
-  );
+  const { Entity } = useLoadMeta('source', type);
 
   const { data, run: getData } = useRequest(
     id => ({
       url: `/source/get/${id}`,
-      params: {
-        sourceType: type,
-      },
     }),
     {
       manual: true,
-      formatResult: result => toFormVals(result),
+      formatResult: result => new Entity()?.parse(result) || result,
       onSuccess: result => {
         form.setFieldsValue(result);
         setType(result.sourceType);
@@ -82,7 +58,7 @@ const Comp: React.FC<Props> = ({ id, inlongGroupId, ...modalProps }) => {
 
   const onOk = async () => {
     const values = await form.validateFields();
-    const submitData = toSubmitVals(values);
+    const submitData = new Entity()?.stringify(values) || values;
     const isUpdate = Boolean(id);
     if (isUpdate) {
       submitData.id = id;
@@ -105,53 +81,26 @@ const Comp: React.FC<Props> = ({ id, inlongGroupId, ...modalProps }) => {
       // open
       if (id) {
         getData(id);
+      } else {
+        form.setFieldsValue({ inlongGroupId });
       }
     } else {
       form.resetFields();
-      setType(sources[0].value);
+      setType(defaultValue);
     }
   }, [modalProps.visible]);
 
   const formContent = useMemo(() => {
-    const currentForm = sourcesMap[type]?.form;
-    return [
-      {
-        type: 'select',
-        label: t('pages.GroupDetail.Sources.DataStreams'),
-        name: 'inlongStreamId',
-        props: {
-          disabled: !!id,
-          options: {
-            requestService: {
-              url: '/stream/list',
-              method: 'POST',
-              data: {
-                pageNum: 1,
-                pageSize: 1000,
-                inlongGroupId,
-              },
-            },
-            requestParams: {
-              formatResult: result =>
-                result?.list.map(item => ({
-                  label: item.inlongStreamId,
-                  value: item.inlongStreamId,
-                })) || [],
-            },
-          },
-        },
-        rules: [{ required: true }],
-      } as FormItemProps,
-    ].concat(currentForm);
-  }, [type, id, t, inlongGroupId]);
+    return Entity ? Entity.FieldList : [];
+  }, [Entity]);
 
   return (
     <>
-      <Modal {...modalProps} title={sourcesMap[type]?.label} width={666} onOk={onOk}>
+      <Modal {...modalProps} title="Source" width={666} onOk={onOk}>
         <FormGenerator
           content={formContent}
           onValuesChange={(c, values) => setType(values.sourceType)}
-          initialValues={id ? data : {}}
+          initialValues={id ? data : { inlongGroupId }}
           form={form}
           useMaxWidth
         />
diff --git a/inlong-dashboard/src/pages/GroupDetail/DataSources/index.tsx b/inlong-dashboard/src/pages/GroupDetail/DataSources/index.tsx
index ffb358263..980c0325d 100644
--- a/inlong-dashboard/src/pages/GroupDetail/DataSources/index.tsx
+++ b/inlong-dashboard/src/pages/GroupDetail/DataSources/index.tsx
@@ -17,13 +17,13 @@
  * under the License.
  */
 
-import React, { useState, forwardRef, useMemo } from 'react';
+import React, { useState, forwardRef, useMemo, useCallback } from 'react';
 import { Button, Modal, message } from 'antd';
 import HighTable from '@/components/HighTable';
 import { defaultSize } from '@/configs/pagination';
 import { useRequest } from '@/hooks';
+import { useDefaultMeta, useLoadMeta } from '@/metas';
 import DetailModal from './DetailModal';
-import { sources } from '@/metas/sources';
 import i18n from '@/i18n';
 import request from '@/utils/request';
 import { pickObjectArray } from '@/utils';
@@ -31,30 +31,14 @@ import { CommonInterface } from '../common';
 
 type Props = CommonInterface;
 
-const getFilterFormContent = defaultValues =>
-  [
-    {
-      type: 'inputsearch',
-      name: 'keyword',
-      initialValue: defaultValues.keyword,
-      props: {
-        allowClear: true,
-      },
-    },
-  ].concat(
-    pickObjectArray(['sourceType', 'status'], sources[0].form).map(item => ({
-      ...item,
-      visible: true,
-      initialValue: defaultValues[item.name],
-    })),
-  );
-
 const Comp = ({ inlongGroupId, readonly }: Props, ref) => {
+  const { defaultValue } = useDefaultMeta('source');
+
   const [options, setOptions] = useState({
     // keyword: '',
     pageSize: defaultSize,
     pageNum: 1,
-    sourceType: sources[0].value,
+    sourceType: defaultValue,
   });
 
   const [createModal, setCreateModal] = useState<Record<string, unknown>>({
@@ -78,42 +62,45 @@ const Comp = ({ inlongGroupId, readonly }: Props, ref) => {
     },
   );
 
-  const onEdit = ({ id }) => {
+  const onEdit = useCallback(({ id }) => {
     setCreateModal({ visible: true, id });
-  };
-
-  const onDelete = ({ id }) => {
-    Modal.confirm({
-      title: i18n.t('pages.GroupDetail.Sources.DeleteConfirm'),
-      onOk: async () => {
-        await request({
-          url: `/source/delete/${id}`,
-          method: 'DELETE',
-          params: {
-            sourceType: options.sourceType,
-          },
-        });
-        await getList();
-        message.success(i18n.t('pages.GroupDetail.Sources.DeleteSuccessfully'));
-      },
-    });
-  };
+  }, []);
+
+  const onDelete = useCallback(
+    ({ id }) => {
+      Modal.confirm({
+        title: i18n.t('pages.GroupDetail.Sources.DeleteConfirm'),
+        onOk: async () => {
+          await request({
+            url: `/source/delete/${id}`,
+            method: 'DELETE',
+            params: {
+              sourceType: options.sourceType,
+            },
+          });
+          await getList();
+          message.success(i18n.t('pages.GroupDetail.Sources.DeleteSuccessfully'));
+        },
+      });
+    },
+    [getList, options.sourceType],
+  );
 
-  const onChange = ({ current: pageNum, pageSize }) => {
+  const onChange = useCallback(({ current: pageNum, pageSize }) => {
     setOptions(prev => ({
       ...prev,
       pageNum,
       pageSize,
     }));
-  };
+  }, []);
 
-  const onFilter = allValues => {
+  const onFilter = useCallback(allValues => {
     setOptions(prev => ({
       ...prev,
       ...allValues,
       pageNum: 1,
     }));
-  };
+  }, []);
 
   const pagination = {
     pageSize: options.pageSize,
@@ -121,26 +108,33 @@ const Comp = ({ inlongGroupId, readonly }: Props, ref) => {
     total: data?.total,
   };
 
-  const columnsMap = useMemo(
-    () =>
-      sources.reduce(
-        (acc, cur) => ({
-          ...acc,
-          [cur.value]: cur.table,
-        }),
-        {},
+  const { Entity } = useLoadMeta('source', options.sourceType);
+
+  const getFilterFormContent = useCallback(
+    defaultValues =>
+      [
+        {
+          type: 'inputsearch',
+          name: 'keyword',
+          initialValue: defaultValues.keyword,
+          props: {
+            allowClear: true,
+          },
+        },
+      ].concat(
+        pickObjectArray(['sourceType', 'status'], Entity?.FieldList).map(item => ({
+          ...item,
+          visible: true,
+          initialValue: defaultValues[item.name],
+        })),
       ),
-    [],
+    [Entity?.FieldList],
   );
 
-  const columns = [
-    {
-      title: i18n.t('pages.GroupDetail.Sources.DataStreams'),
-      dataIndex: 'inlongStreamId',
-    },
-  ]
-    .concat(columnsMap[options.sourceType])
-    .concat([
+  const columns = useMemo(() => {
+    if (!Entity) return [];
+
+    return Entity.ColumnList?.concat([
       {
         title: i18n.t('basic.Operating'),
         dataIndex: 'action',
@@ -157,8 +151,9 @@ const Comp = ({ inlongGroupId, readonly }: Props, ref) => {
               </Button>
             </>
           ),
-      } as any,
+      },
     ]);
+  }, [Entity, onDelete, onEdit, readonly]);
 
   return (
     <>
diff --git a/inlong-dashboard/src/pages/GroupDetail/DataStream/StreamItemModal.tsx b/inlong-dashboard/src/pages/GroupDetail/DataStream/StreamItemModal.tsx
index a2c32dd9d..46a704fbe 100644
--- a/inlong-dashboard/src/pages/GroupDetail/DataStream/StreamItemModal.tsx
+++ b/inlong-dashboard/src/pages/GroupDetail/DataStream/StreamItemModal.tsx
@@ -17,16 +17,14 @@
  * under the License.
  */
 
-import React from 'react';
+import React, { useCallback } from 'react';
 import { Modal, message } from 'antd';
 import { ModalProps } from 'antd/es/modal';
 import FormGenerator, { useForm } from '@/components/FormGenerator';
 import { useUpdateEffect, useRequest } from '@/hooks';
 import i18n from '@/i18n';
-import { groupForm } from '@/metas/group';
-import { streamForm } from '@/metas/stream';
+import { useLoadMeta, useDefaultMeta } from '@/metas';
 import request from '@/utils/request';
-import { pickObjectArray } from '@/utils';
 import { dataToValues, valuesToData } from './helper';
 
 export interface Props extends ModalProps {
@@ -36,28 +34,77 @@ export interface Props extends ModalProps {
   mqType: string;
 }
 
-export const genFormContent = (isCreate, mqType) => {
-  return [
-    ...streamForm,
-    ...pickObjectArray(['dailyRecords', 'dailyStorage', 'peakRecords', 'maxLength'], groupForm).map(
-      item => ({
-        ...item,
-        visible: mqType === 'PULSAR',
-      }),
-    ),
-  ].map(item => {
-    const obj = { ...item };
+const Comp: React.FC<Props> = ({ inlongGroupId, inlongStreamId, mqType, ...modalProps }) => {
+  const [form] = useForm();
 
-    if (!isCreate && (obj.name === 'inlongStreamId' || obj.name === 'dataType')) {
-      obj.type = 'text';
-    }
+  const { defaultValue } = useDefaultMeta('stream');
 
-    return obj;
-  });
-};
+  const { Entity } = useLoadMeta('stream', defaultValue);
 
-const Comp: React.FC<Props> = ({ inlongGroupId, inlongStreamId, mqType, ...modalProps }) => {
-  const [form] = useForm();
+  const genFormContent = useCallback(
+    (isCreate, mqType) => {
+      return [
+        ...(Entity?.FieldList || []),
+        {
+          type: 'inputnumber',
+          label: i18n.t('meta.Group.NumberOfAccess'),
+          name: 'dailyRecords',
+          rules: [{ required: true }],
+          suffix: i18n.t('meta.Group.TenThousand/Day'),
+          props: {
+            min: 1,
+            precision: 0,
+          },
+          visible: mqType === 'PULSAR',
+        },
+        {
+          type: 'inputnumber',
+          label: i18n.t('meta.Group.AccessSize'),
+          name: 'dailyStorage',
+          rules: [{ required: true }],
+          suffix: i18n.t('meta.Group.GB/Day'),
+          props: {
+            min: 1,
+            precision: 0,
+          },
+          visible: mqType === 'PULSAR',
+        },
+        {
+          type: 'inputnumber',
+          label: i18n.t('meta.Group.AccessPeakPerSecond'),
+          name: 'peakRecords',
+          rules: [{ required: true }],
+          suffix: i18n.t('meta.Group.Stripe/Second'),
+          props: {
+            min: 1,
+            precision: 0,
+          },
+          visible: mqType === 'PULSAR',
+        },
+        {
+          type: 'inputnumber',
+          label: i18n.t('meta.Group.SingleStripMaximumLength'),
+          name: 'maxLength',
+          rules: [{ required: true }],
+          suffix: 'Byte',
+          props: {
+            min: 1,
+            precision: 0,
+          },
+          visible: mqType === 'PULSAR',
+        },
+      ].map(item => {
+        const obj = { ...item };
+
+        if (!isCreate && (obj.name === 'inlongStreamId' || obj.name === 'dataType')) {
+          obj.type = 'text';
+        }
+
+        return obj;
+      });
+    },
+    [Entity?.FieldList],
+  );
 
   const { data: savedData, run: getStreamData } = useRequest(
     {
diff --git a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx b/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
index 9641bacd1..a86a11b7d 100644
--- a/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
+++ b/inlong-dashboard/src/pages/GroupDetail/DataStream/config.tsx
@@ -17,8 +17,8 @@
  * under the License.
  */
 
-import { streamForm } from '@/metas/stream';
-import { pickObjectArray } from '@/utils';
+import i18n from '@/i18n';
+import { statusList } from '@/metas/streams/common/status';
 
 export const getFilterFormContent = (defaultValues = {} as any) => [
   {
@@ -26,9 +26,15 @@ export const getFilterFormContent = (defaultValues = {} as any) => [
     name: 'keyword',
     initialValue: defaultValues.keyword,
   },
-  ...pickObjectArray(['status'], streamForm).map(item => ({
-    ...item,
-    visible: true,
-    initialValue: defaultValues[item.name],
-  })),
+  {
+    type: 'select',
+    name: 'status',
+    label: i18n.t('basic.Status'),
+    initialValue: defaultValues.status,
+    props: {
+      allowClear: true,
+      options: statusList,
+      dropdownMatchSelectWidth: false,
+    },
+  },
 ];
diff --git a/inlong-dashboard/src/pages/GroupDetail/DataStream/index.tsx b/inlong-dashboard/src/pages/GroupDetail/DataStream/index.tsx
index e17cf2d09..0a93524e4 100644
--- a/inlong-dashboard/src/pages/GroupDetail/DataStream/index.tsx
+++ b/inlong-dashboard/src/pages/GroupDetail/DataStream/index.tsx
@@ -24,7 +24,7 @@ import { defaultSize } from '@/configs/pagination';
 import { useRequest } from '@/hooks';
 import request from '@/utils/request';
 import { useTranslation } from 'react-i18next';
-import { streamTable } from '@/metas/stream';
+import { useLoadMeta, useDefaultMeta } from '@/metas';
 import { CommonInterface } from '../common';
 import StreamItemModal from './StreamItemModal';
 import { getFilterFormContent } from './config';
@@ -34,6 +34,8 @@ type Props = CommonInterface;
 const Comp = ({ inlongGroupId, readonly, mqType }: Props, ref) => {
   const { t } = useTranslation();
 
+  const { defaultValue } = useDefaultMeta('stream');
+
   const [options, setOptions] = useState({
     pageSize: defaultSize,
     pageNum: 1,
@@ -123,7 +125,9 @@ const Comp = ({ inlongGroupId, readonly, mqType }: Props, ref) => {
     total: data?.total,
   };
 
-  const columns = streamTable.concat([
+  const { Entity } = useLoadMeta('stream', defaultValue);
+
+  const columns = Entity?.ColumnList?.concat([
     {
       title: t('basic.Operating'),
       dataIndex: 'action',
diff --git a/inlong-dashboard/src/pages/GroupDetail/Info/config.tsx b/inlong-dashboard/src/pages/GroupDetail/Info/config.tsx
index 5c9245e9c..bea9d36b8 100644
--- a/inlong-dashboard/src/pages/GroupDetail/Info/config.tsx
+++ b/inlong-dashboard/src/pages/GroupDetail/Info/config.tsx
@@ -17,13 +17,14 @@
  * under the License.
  */
 
-// import React from 'react';
-import { groupForm } from '@/metas/group';
+import { useLoadMeta } from '@/metas';
 import { excludeObjectArray } from '@/utils';
 
-export const getFormContent = ({ editing, isCreate, isUpdate }) => {
+export const useFormContent = ({ mqType, editing, isCreate, isUpdate }) => {
+  const { Entity } = useLoadMeta('group', mqType);
+
   const excludeKeys = ['ensemble'].concat(isCreate ? 'mqResource' : '');
-  const fields = excludeObjectArray(excludeKeys, groupForm);
+  const fields = excludeObjectArray(excludeKeys, Entity?.FieldList || []);
 
   return isCreate
     ? fields.map(item => {
diff --git a/inlong-dashboard/src/pages/GroupDetail/Info/index.tsx b/inlong-dashboard/src/pages/GroupDetail/Info/index.tsx
index 78c9f81fe..3947acb2a 100644
--- a/inlong-dashboard/src/pages/GroupDetail/Info/index.tsx
+++ b/inlong-dashboard/src/pages/GroupDetail/Info/index.tsx
@@ -17,15 +17,16 @@
  * under the License.
  */
 
-import React, { useEffect, useMemo, useImperativeHandle, forwardRef } from 'react';
+import React, { useEffect, useMemo, useImperativeHandle, forwardRef, useState } from 'react';
 import { Button, Space, message } from 'antd';
 import FormGenerator, { useForm } from '@/components/FormGenerator';
 import { useRequest, useBoolean, useSelector } from '@/hooks';
 import { useTranslation } from 'react-i18next';
+import { useDefaultMeta } from '@/metas';
 import request from '@/utils/request';
 import { State } from '@/models';
 import { CommonInterface } from '../common';
-import { getFormContent } from './config';
+import { useFormContent } from './config';
 
 type Props = CommonInterface;
 
@@ -33,6 +34,10 @@ const Comp = ({ inlongGroupId, readonly, isCreate }: Props, ref) => {
   const { t } = useTranslation();
   const [editing, { setTrue, setFalse }] = useBoolean(isCreate);
 
+  const { defaultValue } = useDefaultMeta('consume');
+
+  const [mqType, setMqType] = useState(defaultValue);
+
   const { userName } = useSelector<State, State>(state => state);
 
   const [form] = useForm();
@@ -48,7 +53,10 @@ const Comp = ({ inlongGroupId, readonly, isCreate }: Props, ref) => {
       ...data,
       inCharges: data.inCharges.split(','),
     }),
-    onSuccess: data => form.setFieldsValue(data),
+    onSuccess: data => {
+      form.setFieldsValue(data);
+      setMqType(data.mqType);
+    },
   });
 
   const onOk = async () => {
@@ -100,16 +108,20 @@ const Comp = ({ inlongGroupId, readonly, isCreate }: Props, ref) => {
     setFalse();
   };
 
+  const formContent = useFormContent({
+    mqType,
+    editing,
+    isCreate,
+    isUpdate,
+  });
+
   return (
     <div style={{ position: 'relative' }}>
       <FormGenerator
         form={form}
-        content={getFormContent({
-          editing,
-          isCreate,
-          isUpdate,
-        })}
+        content={formContent}
         initialValues={data}
+        onValuesChange={(c, values) => setMqType(values.mqType)}
         useMaxWidth={600}
       />
 
diff --git a/inlong-dashboard/src/pages/Nodes/DetailModal.tsx b/inlong-dashboard/src/pages/Nodes/DetailModal.tsx
index c4426a3e0..1c89bdc5c 100644
--- a/inlong-dashboard/src/pages/Nodes/DetailModal.tsx
+++ b/inlong-dashboard/src/pages/Nodes/DetailModal.tsx
@@ -21,11 +21,13 @@ import React, { useState, useMemo } from 'react';
 import { Modal, message } from 'antd';
 import { ModalProps } from 'antd/es/modal';
 import FormGenerator, { useForm } from '@/components/FormGenerator';
-import { useRequest, useUpdateEffect } from '@/hooks';
-import request from '@/utils/request';
-import { nodes } from '@/metas/nodes';
+import { useUpdateEffect } from '@/hooks';
+import { dao } from '@/metas/nodes';
+import { useDefaultMeta, useLoadMeta } from '@/metas';
 import i18n from '@/i18n';
 
+const { useFindNodeDao, useSaveNodeDao } = dao;
+
 export interface Props extends ModalProps {
   // Require when edit
   id?: string;
@@ -34,44 +36,29 @@ export interface Props extends ModalProps {
 const Comp: React.FC<Props> = ({ id, ...modalProps }) => {
   const [form] = useForm();
 
-  const [type, setType] = useState(nodes[0].value);
+  const { defaultValue } = useDefaultMeta('node');
+
+  const [type, setType] = useState(defaultValue);
 
-  const { data: savedData, run: getData } = useRequest(
-    id => ({
-      url: `/node/get/${id}`,
-    }),
-    {
-      manual: true,
-      formatResult: result => ({
-        ...result,
-        inCharges: result.inCharges?.split(','),
-        clusterTags: result.clusterTags?.split(','),
-      }),
-      onSuccess: result => {
-        form.setFieldsValue(result);
-        setType(result.type);
-      },
+  const { data: savedData, run: getData } = useFindNodeDao({
+    onSuccess: result => {
+      form.setFieldsValue(result);
+      setType(result.type);
     },
-  );
+  });
 
-  const onOk = async () => {
+  const { runAsync: save } = useSaveNodeDao();
+
+  const onOk = async e => {
     const values = await form.validateFields();
-    const isUpdate = id;
-    const submitData = {
-      ...values,
-      inCharges: values.inCharges?.join(','),
-      clusterTags: values.clusterTags?.join(','),
-    };
+    const isUpdate = Boolean(id);
+    const data = { ...values };
     if (isUpdate) {
-      submitData.id = id;
-      submitData.version = savedData?.version;
+      data.id = id;
+      data.version = savedData?.version;
     }
-    await request({
-      url: `/node/${isUpdate ? 'update' : 'save'}`,
-      method: 'POST',
-      data: submitData,
-    });
-    await modalProps?.onOk(submitData);
+    await save(data);
+    await modalProps?.onOk(e);
     message.success(i18n.t('basic.OperatingSuccess'));
   };
 
@@ -83,14 +70,15 @@ const Comp: React.FC<Props> = ({ id, ...modalProps }) => {
       }
     } else {
       form.resetFields();
-      setType(nodes[0].value);
+      setType(defaultValue);
     }
   }, [modalProps.visible]);
 
+  const { Entity } = useLoadMeta('node', type);
+
   const content = useMemo(() => {
-    const current = nodes.find(item => item.value === type);
-    return current?.form;
-  }, [type]);
+    return Entity ? Entity.FieldList : [];
+  }, [Entity]);
 
   return (
     <Modal {...modalProps} title={id ? i18n.t('basic.Detail') : i18n.t('basic.Create')} onOk={onOk}>
diff --git a/inlong-dashboard/src/pages/Nodes/index.tsx b/inlong-dashboard/src/pages/Nodes/index.tsx
index 03c69e3e3..fd2c279ae 100644
--- a/inlong-dashboard/src/pages/Nodes/index.tsx
+++ b/inlong-dashboard/src/pages/Nodes/index.tsx
@@ -23,59 +23,29 @@ import i18n from '@/i18n';
 import HighTable from '@/components/HighTable';
 import { PageContainer } from '@/components/PageContainer';
 import { defaultSize } from '@/configs/pagination';
-import { useRequest } from '@/hooks';
-import { nodes } from '@/metas/nodes';
+import { dao } from '@/metas/nodes';
+import { useDefaultMeta, useLoadMeta } from '@/metas';
 import DetailModal from './DetailModal';
-import request from '@/utils/request';
-
-const getFilterFormContent = defaultValues => [
-  {
-    type: 'inputsearch',
-    name: 'keyword',
-  },
-  {
-    type: 'radiobutton',
-    name: 'type',
-    label: i18n.t('meta.Nodes.Type'),
-    initialValue: defaultValues.type,
-    props: {
-      buttonStyle: 'solid',
-      options: nodes.map(item => ({
-        label: item.label,
-        value: item.value,
-      })),
-    },
-  },
-];
+
+const { useListNodeDao, useDeleteNodeDao } = dao;
 
 const Comp: React.FC = () => {
+  const { defaultValue, options: nodes } = useDefaultMeta('node');
+
   const [options, setOptions] = useState({
     keyword: '',
     pageSize: defaultSize,
     pageNum: 1,
-    type: nodes[0].value,
+    type: defaultValue,
   });
 
   const [detailModal, setDetailModal] = useState<Record<string, unknown>>({
     visible: false,
   });
 
-  const {
-    data,
-    loading,
-    run: getList,
-  } = useRequest(
-    {
-      url: '/node/list',
-      method: 'POST',
-      data: {
-        ...options,
-      },
-    },
-    {
-      refreshDeps: [options],
-    },
-  );
+  const { data, loading, run: getList } = useListNodeDao({ options });
+
+  const { runAsync: destory } = useDeleteNodeDao();
 
   const onEdit = ({ id }) => {
     setDetailModal({ visible: true, id });
@@ -86,16 +56,13 @@ const Comp: React.FC = () => {
       Modal.confirm({
         title: i18n.t('basic.DeleteConfirm'),
         onOk: async () => {
-          await request({
-            url: `/node/delete/${id}`,
-            method: 'DELETE',
-          });
+          await destory(id);
           await getList();
           message.success(i18n.t('basic.DeleteSuccess'));
         },
       });
     },
-    [getList],
+    [getList, destory],
   );
 
   const onChange = ({ current: pageNum, pageSize }) => {
@@ -120,33 +87,52 @@ const Comp: React.FC = () => {
     total: data?.total,
   };
 
+  const getFilterFormContent = useCallback(
+    defaultValues => [
+      {
+        type: 'inputsearch',
+        name: 'keyword',
+      },
+      {
+        type: 'radiobutton',
+        name: 'type',
+        label: i18n.t('meta.Nodes.Type'),
+        initialValue: defaultValues.type,
+        props: {
+          buttonStyle: 'solid',
+          options: nodes,
+        },
+      },
+    ],
+    [nodes],
+  );
+
+  const { Entity } = useLoadMeta('node', options.type);
+
   const columns = useMemo(() => {
-    const current = nodes.find(item => item.value === options.type);
-    if (!current?.table) return [];
-
-    return current.table
-      .map(item => ({
-        ...item,
-        ellipsisMulti: 2,
-      }))
-      .concat([
-        {
-          title: i18n.t('basic.Operating'),
-          dataIndex: 'action',
-          width: 200,
-          render: (text, record) => (
-            <>
-              <Button type="link" onClick={() => onEdit(record)}>
-                {i18n.t('basic.Edit')}
-              </Button>
-              <Button type="link" onClick={() => onDelete(record)}>
-                {i18n.t('basic.Delete')}
-              </Button>
-            </>
-          ),
-        } as any,
-      ]);
-  }, [options.type, onDelete]);
+    if (!Entity) return [];
+
+    return Entity.ColumnList?.map(item => ({
+      ...item,
+      ellipsisMulti: 2,
+    })).concat([
+      {
+        title: i18n.t('basic.Operating'),
+        dataIndex: 'action',
+        width: 200,
+        render: (text, record) => (
+          <>
+            <Button type="link" onClick={() => onEdit(record)}>
+              {i18n.t('basic.Edit')}
+            </Button>
+            <Button type="link" onClick={() => onDelete(record)}>
+              {i18n.t('basic.Delete')}
+            </Button>
+          </>
+        ),
+      } as any,
+    ]);
+  }, [Entity, onDelete]);
 
   return (
     <PageContainer useDefaultBreadcrumb={false}>
diff --git a/inlong-dashboard/src/pages/ProcessDetail/Access.tsx b/inlong-dashboard/src/pages/ProcessDetail/Access.tsx
index 6b35401f7..b3afabeb9 100644
--- a/inlong-dashboard/src/pages/ProcessDetail/Access.tsx
+++ b/inlong-dashboard/src/pages/ProcessDetail/Access.tsx
@@ -20,7 +20,7 @@
 import React, { useMemo, forwardRef, useImperativeHandle, useEffect } from 'react';
 import FormGenerator, { useForm } from '@/components/FormGenerator';
 import { CommonInterface } from './common';
-import { getFormContent } from './AccessConfig';
+import { useGroupFormContent, getFormContent } from './AccessConfig';
 
 export type Props = CommonInterface;
 
@@ -57,6 +57,12 @@ const Comp = ({ defaultData, isViwer, suffixContent, noExtraForm, isFinished }:
     onOk,
   }));
 
+  const groupFormContent = useGroupFormContent({
+    mqType: defaultData?.processInfo?.formData?.groupInfo?.mqType,
+    isViwer,
+    isFinished,
+  });
+
   // Easy to set some default values of the form
   const dataLoaded = useMemo(() => {
     return !!(defaultData && Object.keys(defaultData).length);
@@ -74,6 +80,7 @@ const Comp = ({ defaultData, isViwer, suffixContent, noExtraForm, isFinished }:
           suffixContent,
           noExtraForm,
           isFinished,
+          groupFormContent,
         })}
       />
     )
diff --git a/inlong-dashboard/src/pages/ProcessDetail/AccessConfig.tsx b/inlong-dashboard/src/pages/ProcessDetail/AccessConfig.tsx
index e9ac2e02c..f025e0637 100644
--- a/inlong-dashboard/src/pages/ProcessDetail/AccessConfig.tsx
+++ b/inlong-dashboard/src/pages/ProcessDetail/AccessConfig.tsx
@@ -20,10 +20,12 @@
 import React from 'react';
 import { Divider, Table } from 'antd';
 import i18n from '@/i18n';
-import { groupForm } from '@/metas/group';
+import { useLoadMeta } from '@/metas';
 
-const getContent = (isFinished, isViwer) =>
-  groupForm.map(item => {
+export const useGroupFormContent = ({ mqType = '', isFinished, isViwer }) => {
+  const { Entity } = useLoadMeta('group', mqType);
+
+  return Entity?.FieldList?.map(item => {
     const obj = { ...item };
 
     const canEditSet = new Set([
@@ -52,8 +54,16 @@ const getContent = (isFinished, isViwer) =>
 
     return obj;
   });
+};
 
-export const getFormContent = ({ isViwer, formData, suffixContent, noExtraForm, isFinished }) => {
+export const getFormContent = ({
+  isViwer,
+  formData,
+  suffixContent,
+  noExtraForm,
+  isFinished,
+  groupFormContent = [],
+}) => {
   const array = [
     {
       type: (
@@ -62,7 +72,7 @@ export const getFormContent = ({ isViwer, formData, suffixContent, noExtraForm,
         </Divider>
       ),
     },
-    ...(getContent(isFinished, isViwer) || []),
+    ...groupFormContent,
     {
       type: (
         <Divider orientation="left">
diff --git a/inlong-dashboard/src/pages/ProcessDetail/Consume.tsx b/inlong-dashboard/src/pages/ProcessDetail/Consume.tsx
index 700d7f7ed..6dc2f57f8 100644
--- a/inlong-dashboard/src/pages/ProcessDetail/Consume.tsx
+++ b/inlong-dashboard/src/pages/ProcessDetail/Consume.tsx
@@ -20,7 +20,7 @@
 import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
 import FormGenerator, { useForm } from '@/components/FormGenerator';
 import { CommonInterface } from './common';
-import { getFormContent } from './ConsumeConfig';
+import { useConsumeFormContent, getFormContent } from './ConsumeConfig';
 
 type Props = CommonInterface;
 
@@ -53,6 +53,10 @@ const Comp = (
     onOk,
   }));
 
+  const consumeFormContent = useConsumeFormContent(
+    defaultData?.processInfo?.formData?.consumeInfo?.mqType,
+  );
+
   return (
     Object.keys(defaultData).length && (
       <FormGenerator
@@ -64,6 +68,7 @@ const Comp = (
           noExtraForm,
           defaultData?.processInfo?.formData,
           suffixContent,
+          consumeFormContent,
         )}
       />
     )
diff --git a/inlong-dashboard/src/pages/ProcessDetail/ConsumeConfig.tsx b/inlong-dashboard/src/pages/ProcessDetail/ConsumeConfig.tsx
index ce5b71ee8..b4245f4d7 100644
--- a/inlong-dashboard/src/pages/ProcessDetail/ConsumeConfig.tsx
+++ b/inlong-dashboard/src/pages/ProcessDetail/ConsumeConfig.tsx
@@ -20,10 +20,12 @@
 import React from 'react';
 import { Divider } from 'antd';
 import i18n from '@/i18n';
-import { consumeForm } from '@/metas/consume';
+import { useLoadMeta } from '@/metas';
 
-const getContent = () => {
-  return consumeForm.map(item => {
+export const useConsumeFormContent = (mqType = '') => {
+  const { Entity } = useLoadMeta('consume', mqType);
+
+  return Entity?.FieldList?.map(item => {
     const obj = { ...item };
     if (typeof obj.suffix !== 'string') {
       delete obj.suffix;
@@ -45,6 +47,7 @@ export const getFormContent = (
   noExtraForm: boolean,
   formData: Record<string, any> = {},
   suffixContent,
+  consumeFormContent = [],
 ) => {
   const array = [
     {
@@ -54,7 +57,7 @@ export const getFormContent = (
         </Divider>
       ),
     },
-    ...(getContent() || []),
+    ...consumeFormContent,
   ];
 
   const extraForm =
diff --git a/inlong-dashboard/tsconfig.json b/inlong-dashboard/tsconfig.json
index 67f1e4818..03bfa1b28 100644
--- a/inlong-dashboard/tsconfig.json
+++ b/inlong-dashboard/tsconfig.json
@@ -25,7 +25,8 @@
     "isolatedModules": true,
     "noEmit": true,
     "jsx": "react",
-    "noFallthroughCasesInSwitch": true
+    "noFallthroughCasesInSwitch": true,
+    "experimentalDecorators": true
   },
   "exclude": [
     "build",