Commit 6dcdc008b1abf385b45caface7fa1f128434db1c

Authored by ww
1 parent 20b454eb

feat: 新增使用node创建规则节点

  1 +import { join } from 'path';
  2 +import { pathExists, ensureFile, writeFile } from 'fs-extra';
  3 +import { NodeItemConfigType } from '/@/views/rule/designer/types/node';
  4 +import { camelCase, upperFirst, snakeCase } from 'lodash-es';
  5 +import { execSync } from 'child_process';
  6 +
  7 +type GroupNodeType = { [key: string]: NodeItemConfigType[] };
  8 +
  9 +const RULE_CHAIN_FILE_PATH = join(process.cwd(), '/src/views/rule/designer');
  10 +
  11 +const list: NodeItemConfigType[] = [
  12 + {
  13 + type: 'FLOW',
  14 + scope: 'TENANT',
  15 + name: 'acknowledge',
  16 + clazz: 'org.thingsboard.rule.engine.flow.TbAckNode',
  17 + configurationDescriptor: {
  18 + nodeDefinition: {
  19 + details:
  20 + "After acknowledgement, the message is pushed to related rule nodes. Useful if you don't care what happens to this message next.",
  21 + description: 'Acknowledges the incoming message',
  22 + inEnabled: true,
  23 + outEnabled: true,
  24 + relationTypes: ['Success', 'Failure'],
  25 + customRelations: false,
  26 + ruleChainNode: false,
  27 + defaultConfiguration: {
  28 + version: 0,
  29 + },
  30 + uiResources: ['static/rulenode/rulenode-core-config.js'],
  31 + configDirective: 'tbNodeEmptyConfig',
  32 + icon: '',
  33 + iconUrl: '',
  34 + docUrl: '',
  35 + },
  36 + },
  37 + actions: null,
  38 + },
  39 + {
  40 + createdTime: 1668743016418,
  41 + type: 'FILTER',
  42 + scope: 'TENANT',
  43 + name: 'check alarm status',
  44 + clazz: 'org.thingsboard.rule.engine.filter.TbCheckAlarmStatusNode',
  45 + configurationDescriptor: {
  46 + nodeDefinition: {
  47 + details:
  48 + 'If the alarm status matches the specified one - msg is success if does not match - msg is failure.',
  49 + description: 'Checks alarm status.',
  50 + inEnabled: true,
  51 + outEnabled: true,
  52 + relationTypes: ['True', 'False', 'Failure'],
  53 + customRelations: false,
  54 + ruleChainNode: false,
  55 + defaultConfiguration: {
  56 + alarmStatusList: ['ACTIVE_ACK', 'ACTIVE_UNACK'],
  57 + },
  58 + uiResources: ['static/rulenode/rulenode-core-config.js'],
  59 + configDirective: 'tbFilterNodeCheckAlarmStatusConfig',
  60 + icon: '',
  61 + iconUrl: '',
  62 + docUrl: '',
  63 + },
  64 + },
  65 + actions: null,
  66 + },
  67 +];
  68 +
  69 +const getCategoryConfigName = (name: string) => {
  70 + return `${upperFirst(camelCase(name))}CategoryConfig`;
  71 +};
  72 +
  73 +const getNodeConfigName = (name: string) => {
  74 + return `${upperFirst(camelCase(name))}Config`;
  75 +};
  76 +
  77 +const getCagegoryEnumName = (name: string) => {
  78 + return `${upperFirst(name.toLowerCase())}CategoryComponentEnum`;
  79 +};
  80 +
  81 +const getEnumKeyName = (name: string) => {
  82 + return snakeCase(name).toUpperCase();
  83 +};
  84 +
  85 +const createFile = async (fileName: string, fileContent?: string, replace = false) => {
  86 + const path = join(RULE_CHAIN_FILE_PATH, './packages', fileName);
  87 +
  88 + const flag = await pathExists(path);
  89 +
  90 + if (flag && !replace) return false;
  91 +
  92 + await ensureFile(path);
  93 +
  94 + fileContent && (await writeFile(path, fileContent, { encoding: 'utf-8' }));
  95 +};
  96 +
  97 +const groupByType = () => {
  98 + const group: { [key: string]: NodeItemConfigType[] } = {};
  99 +
  100 + list.forEach((item) => {
  101 + if (!group[item.type]) group[item.type] = [];
  102 + group[item.type].push(item);
  103 + });
  104 +
  105 + return group;
  106 +};
  107 +
  108 +const generateCategoryEnumFile = async (data: GroupNodeType) => {
  109 + const defaultContent = `
  110 +export enum EntryCategoryComponentEnum {
  111 + INPUT = 'Input',
  112 +}
  113 + `;
  114 +
  115 + const fileContent = Object.keys(data).reduce((prev, next) => {
  116 + const enumName = getCagegoryEnumName(next);
  117 +
  118 + const enumKeys = data[next].map((item) => getEnumKeyName(item.name));
  119 +
  120 + const content = `export enum ${enumName} {
  121 + ${enumKeys.map((name) => `${name} = '${upperFirst(camelCase(name))}'`)}
  122 + }`;
  123 +
  124 + return `${prev} \n ${content}`;
  125 + }, defaultContent);
  126 +
  127 + createFile('../enum/category.ts', fileContent, true);
  128 + return fileContent;
  129 +};
  130 +
  131 +const generateRuleNodeEnumFile = async (data: GroupNodeType) => {
  132 + const categoryKeys = Object.keys(data).map((type) => type.toUpperCase());
  133 + const filePath = join(RULE_CHAIN_FILE_PATH, './packages/index.type.ts');
  134 + const fileContent = `
  135 +export enum RuleNodeTypeEnum {
  136 + ${categoryKeys.map((item) => `${item} = '${item}'`).join(',\n')}
  137 +}
  138 + `;
  139 +
  140 + await writeFile(filePath, fileContent, {
  141 + encoding: 'utf-8',
  142 + });
  143 +
  144 + return fileContent;
  145 +};
  146 +
  147 +const generateCategoryIndexFile = async (type: string, data: NodeItemConfigType[]) => {
  148 + const getComponentsName = data.map((temp) => `${upperFirst(camelCase(temp.name))}`);
  149 + const importContent = getComponentsName.map(
  150 + (name) => `import { ${name}Config } from './${name}';\n`
  151 + );
  152 +
  153 + const components = getComponentsName.map((item) => `${item}Config`);
  154 +
  155 + const content = `import type { CategoryConfigType, NodeItemConfigType } from '../../types/node';
  156 +import { RuleNodeTypeEnum } from '../index.type';
  157 +${importContent.join('')}
  158 +
  159 +export const ${getCategoryConfigName(type)}: CategoryConfigType = {
  160 + category: RuleNodeTypeEnum.${type.toUpperCase()},
  161 + backgroundColor: '#ede550',
  162 + title: '筛选器',
  163 + icon: 'tabler:circuit-ground',
  164 + description: '使用配置条件筛选传入消息',
  165 +};
  166 +
  167 +export const ${upperFirst(type.toLowerCase())}Components: NodeItemConfigType[] = [${components}];
  168 +`;
  169 +
  170 + createFile(`./${upperFirst(type.toLowerCase())}/index.ts`, content);
  171 +
  172 + return content;
  173 +};
  174 +
  175 +const generateNodeIndexFile = async (type: string, data: NodeItemConfigType) => {
  176 + const categoryEnumName = getCagegoryEnumName(type);
  177 +
  178 + const nodeConfigName = getNodeConfigName(data.name);
  179 +
  180 + const content = `
  181 + import { ${categoryEnumName} } from '../../../enum/category';
  182 + import { useCreateNodeKey } from '../../../hook/useCreateNodeKey';
  183 + import type { NodeItemConfigType } from '../../../types/node';
  184 + import { RuleNodeTypeEnum } from '../../index.type';
  185 +
  186 + const keys = useCreateNodeKey(${categoryEnumName}.${getEnumKeyName(data.name)});
  187 +
  188 + export interface ${upperFirst(camelCase(data.name))}DataType {
  189 +
  190 + }
  191 +
  192 + export const ${nodeConfigName}: NodeItemConfigType = {
  193 + ...keys,
  194 + clazz: '${data.clazz}',
  195 + categoryType: RuleNodeTypeEnum.${type.toUpperCase()},
  196 + name: '${data.name}',
  197 + backgroundColor: '#ede550',
  198 + configurationDescriptor: ${JSON.stringify(data.configurationDescriptor, null, 2)}
  199 + };
  200 + `;
  201 +
  202 + createFile(
  203 + `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/index.ts`,
  204 + content
  205 + );
  206 +
  207 + return content;
  208 +};
  209 +
  210 +const generateNodeVueTemplateFile = async (type: string, data: NodeItemConfigType) => {
  211 + const content = `
  212 + <script lang="ts" setup>
  213 + import type { CreateModalDefineExposeType } from '../../../types';
  214 + import { BasicForm, useForm } from '/@/components/Form';
  215 + import { formSchemas } from './create.config';
  216 + import { NodeData } from '../../../types/node';
  217 +
  218 + defineProps<{
  219 + config: NodeData;
  220 + }>();
  221 +
  222 + const [register, { validate, getFieldsValue, setFieldsValue, resetFields }] = useForm({
  223 + schemas: formSchemas,
  224 + showActionButtonGroup: false,
  225 + });
  226 +
  227 + const getValue: CreateModalDefineExposeType['getFieldsValue'] = async () => {
  228 + await validate();
  229 + const value = getFieldsValue() || {};
  230 + return value;
  231 + };
  232 +
  233 + const setValue: CreateModalDefineExposeType['setFieldsValue'] = (value) => {
  234 + resetFields();
  235 + setFieldsValue(value);
  236 + };
  237 +
  238 + defineExpose({
  239 + setFieldsValue: setValue,
  240 + getFieldsValue: getValue,
  241 + } as CreateModalDefineExposeType);
  242 + </script>
  243 +
  244 + <template>
  245 + <BasicForm @register="register" />
  246 + </template>
  247 + `;
  248 +
  249 + createFile(
  250 + `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/create.vue`,
  251 + content
  252 + );
  253 + return content;
  254 +};
  255 +
  256 +const generateNodeVueConfigFile = async (type: string, data: NodeItemConfigType) => {
  257 + const content = `
  258 + import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
  259 + import { FormSchema } from '/@/components/Form';
  260 +
  261 + export const formSchemas: FormSchema[] = [
  262 + ];
  263 + `;
  264 +
  265 + createFile(
  266 + `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/create.config.ts`,
  267 + content
  268 + );
  269 + return content;
  270 +};
  271 +
  272 +const generateNodeConfigFile = async (type: string, data: NodeItemConfigType) => {
  273 + const nodeConfigName = getNodeConfigName(data.name);
  274 + const categoryConfigName = getCategoryConfigName(type);
  275 + const content = `
  276 + import { cloneDeep } from 'lodash-es';
  277 + import { PublicNodeItemClass } from '../../../types/node';
  278 + import type {
  279 + CategoryConfigType,
  280 + CreateComponentType,
  281 + NodeItemConfigType,
  282 + } from '../../../types/node';
  283 + import { ${categoryConfigName} } from '..';
  284 + import { ${nodeConfigName} } from '.';
  285 +
  286 + export class Config extends PublicNodeItemClass implements CreateComponentType {
  287 + public config: NodeItemConfigType = cloneDeep(${nodeConfigName});
  288 +
  289 + public categoryConfig: CategoryConfigType = cloneDeep(${categoryConfigName});
  290 +
  291 + constructor() {
  292 + super();
  293 + }
  294 + }
  295 + `;
  296 +
  297 + createFile(
  298 + `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/config.ts`,
  299 + content
  300 + );
  301 + return content;
  302 +};
  303 +
  304 +const bootstrap = async () => {
  305 + const groupData = groupByType();
  306 + await generateRuleNodeEnumFile(groupData);
  307 + await generateCategoryEnumFile(groupData);
  308 +
  309 + for (const type of Object.keys(groupData)) {
  310 + const item = groupData[type];
  311 + await generateCategoryIndexFile(type, item);
  312 + for (const temp of item) {
  313 + await generateNodeConfigFile(type, temp);
  314 + await generateNodeIndexFile(type, temp);
  315 + await generateNodeVueConfigFile(type, temp);
  316 + await generateNodeVueTemplateFile(type, temp);
  317 + }
  318 + }
  319 + execSync(`npx eslint --fix ${RULE_CHAIN_FILE_PATH}/*.{vue,ts}`);
  320 +};
  321 +
  322 +bootstrap();
@@ -32,7 +32,8 @@ @@ -32,7 +32,8 @@
32 "reinstall": "rimraf yarn.lock && rimraf package.lock.json && rimraf node_modules && npm run bootstrap", 32 "reinstall": "rimraf yarn.lock && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
33 "prepare": "husky install", 33 "prepare": "husky install",
34 "gen:icon": "esno ./build/generate/icon/index.ts", 34 "gen:icon": "esno ./build/generate/icon/index.ts",
35 - "gen:iconfont": "esno ./build/generate/iconfont/index.ts" 35 + "gen:iconfont": "esno ./build/generate/iconfont/index.ts",
  36 + "gen:rule": "esno ./build/generate/ruleChain/index.ts"
36 }, 37 },
37 "dependencies": { 38 "dependencies": {
38 "@fingerprintjs/fingerprintjs": "^3.4.1", 39 "@fingerprintjs/fingerprintjs": "^3.4.1",
@@ -2,6 +2,6 @@ export enum FilterCategoryComponentEnum { @@ -2,6 +2,6 @@ export enum FilterCategoryComponentEnum {
2 CHECK_ALARM_STATUS = 'CheckAlarmStatus', 2 CHECK_ALARM_STATUS = 'CheckAlarmStatus',
3 } 3 }
4 4
5 -export enum EntryComponentEnum { 5 +export enum EntryCategoryComponentEnum {
6 INPUT = 'Input', 6 INPUT = 'Input',
7 } 7 }
1 import type { VueFlowStore, Getters, Elements } from '@vue-flow/core'; 1 import type { VueFlowStore, Getters, Elements } from '@vue-flow/core';
2 import { ComputedRef, ref, unref } from 'vue'; 2 import { ComputedRef, ref, unref } from 'vue';
3 import { BasicNodeBindData, EdgeData, NodeData } from '../types/node'; 3 import { BasicNodeBindData, EdgeData, NodeData } from '../types/node';
4 -import { EntryComponentEnum } from '../enum/category'; 4 +import { EntryCategoryComponentEnum } from '../enum/category';
5 import { useBasicDataTransform } from './useBasicDataTransform'; 5 import { useBasicDataTransform } from './useBasicDataTransform';
6 import { getRuleChainData, saveRuleChainData } from '/@/api/ruleDesigner'; 6 import { getRuleChainData, saveRuleChainData } from '/@/api/ruleDesigner';
7 import { ConnectionItemType, RuleChainType } from '../types/ruleNode'; 7 import { ConnectionItemType, RuleChainType } from '../types/ruleNode';
8 import { useInputNode } from './useInputNode'; 8 import { useInputNode } from './useInputNode';
9 import { buildUUID } from '/@/utils/uuid'; 9 import { buildUUID } from '/@/utils/uuid';
10 10
11 -const ignoreNodeKeys: string[] = [EntryComponentEnum.INPUT]; 11 +const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT];
12 12
13 export function useSaveAndRedo() { 13 export function useSaveAndRedo() {
14 const changeMarker = ref(false); 14 const changeMarker = ref(false);
@@ -77,7 +77,7 @@ export function useSaveAndRedo() { @@ -77,7 +77,7 @@ export function useSaveAndRedo() {
77 edges: ComputedRef<Getters['getEdges']> 77 edges: ComputedRef<Getters['getEdges']>
78 ) { 78 ) {
79 const inputNode = unref(edges).find( 79 const inputNode = unref(edges).find(
80 - (item) => (item.sourceNode.data as NodeData).config?.key === EntryComponentEnum.INPUT 80 + (item) => (item.sourceNode.data as NodeData).config?.key === EntryCategoryComponentEnum.INPUT
81 ); 81 );
82 82
83 if (inputNode) { 83 if (inputNode) {
1 -import { EntryComponentEnum } from '../../../enum/category'; 1 +import { EntryCategoryComponentEnum } from '../../../enum/category';
2 import { useCreateNodeKey } from '../../../hook/useCreateNodeKey'; 2 import { useCreateNodeKey } from '../../../hook/useCreateNodeKey';
3 import type { NodeItemConfigType } from '../../../types/node'; 3 import type { NodeItemConfigType } from '../../../types/node';
4 import { RuleNodeTypeEnum } from '../../index.type'; 4 import { RuleNodeTypeEnum } from '../../index.type';
5 5
6 -const keys = useCreateNodeKey(EntryComponentEnum.INPUT); 6 +const keys = useCreateNodeKey(EntryCategoryComponentEnum.INPUT);
7 7
8 export const InputConfig: NodeItemConfigType = { 8 export const InputConfig: NodeItemConfigType = {
9 ...keys, 9 ...keys,
10 categoryType: RuleNodeTypeEnum.ENTRY, 10 categoryType: RuleNodeTypeEnum.ENTRY,
11 - clazz: EntryComponentEnum.INPUT, 11 + clazz: EntryCategoryComponentEnum.INPUT,
12 maxConnectionPoint: 1, 12 maxConnectionPoint: 1,
13 backgroundColor: '#95E898', 13 backgroundColor: '#95E898',
14 configurationDescriptor: { 14 configurationDescriptor: {