Showing
14 changed files
with
369 additions
and
19 deletions
1 | 1 | import type { VueFlowStore, Getters, Elements } from '@vue-flow/core'; |
2 | -import { ComputedRef, ref, unref } from 'vue'; | |
2 | +import { ComputedRef, computed, ref, unref } from 'vue'; | |
3 | 3 | import { BasicNodeBindData, EdgeData, NodeData } from '../types/node'; |
4 | 4 | import { EntryCategoryComponentEnum } from '../enum/category'; |
5 | 5 | import { useBasicDataTransform } from './useBasicDataTransform'; |
... | ... | @@ -7,6 +7,7 @@ import { getRuleChainData, saveRuleChainData } from '/@/api/ruleDesigner'; |
7 | 7 | import { ConnectionItemType, RuleChainType } from '../types/ruleNode'; |
8 | 8 | import { useInputNode } from './useInputNode'; |
9 | 9 | import { buildUUID } from '/@/utils/uuid'; |
10 | +import { useRoute } from 'vue-router'; | |
10 | 11 | |
11 | 12 | const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT]; |
12 | 13 | |
... | ... | @@ -17,6 +18,10 @@ export function useSaveAndRedo() { |
17 | 18 | |
18 | 19 | const redoDataRef = ref<Elements>([]); |
19 | 20 | |
21 | + const route = useRoute(); | |
22 | + | |
23 | + const getRuleChainId = computed(() => (route.params as Record<'id', string>).id); | |
24 | + | |
20 | 25 | const { mergeData, deconstructionData } = useBasicDataTransform(); |
21 | 26 | |
22 | 27 | const triggerChange = () => { |
... | ... | @@ -124,7 +129,7 @@ export function useSaveAndRedo() { |
124 | 129 | firstNodeIndex, |
125 | 130 | ruleChainId: { |
126 | 131 | entityType: 'RULE_CHAIN', |
127 | - id: '992ddda0-3d97-11ee-8b46-0dfb0900ab17', | |
132 | + id: unref(getRuleChainId), | |
128 | 133 | }, |
129 | 134 | }); |
130 | 135 | |
... | ... | @@ -140,9 +145,7 @@ export function useSaveAndRedo() { |
140 | 145 | try { |
141 | 146 | loading.value = true; |
142 | 147 | |
143 | - const id = '992ddda0-3d97-11ee-8b46-0dfb0900ab17'; | |
144 | - | |
145 | - const data = await getRuleChainData(id); | |
148 | + const data = await getRuleChainData(unref(getRuleChainId)); | |
146 | 149 | |
147 | 150 | const elements = parseRuleChain(data); |
148 | 151 | ... | ... |
... | ... | @@ -5,7 +5,12 @@ |
5 | 5 | import { Panel, PanelPosition, VueFlow } from '@vue-flow/core'; |
6 | 6 | import { computed, onMounted, Ref, ref, unref } from 'vue'; |
7 | 7 | import './style'; |
8 | - import { BasicConnectionArrow, BasicConnectionLine, BasicEdge, Sidebar } from './src/components'; | |
8 | + import { | |
9 | + BasicConnectionArrow, | |
10 | + BasicConnectionLine, | |
11 | + BasicEdge, | |
12 | + MenuSidebar, | |
13 | + } from './src/components'; | |
9 | 14 | import type { FlowElRef } from './types/flow'; |
10 | 15 | import { useDragCreate } from './hook/useDragCreate'; |
11 | 16 | import { createFlowContext } from './hook/useFlowContext'; |
... | ... | @@ -88,7 +93,8 @@ |
88 | 93 | |
89 | 94 | <template> |
90 | 95 | <main ref="rootElRef" class="w-full h-full flex relative" @drop="handleOnDrop"> |
91 | - <Sidebar /> | |
96 | + <!-- <Sidebar /> --> | |
97 | + <MenuSidebar /> | |
92 | 98 | |
93 | 99 | <Spin :spinning="loading" wrapperClassName="w-full h-ful rule-chain-designer-loading-container"> |
94 | 100 | <VueFlow | ... | ... |
1 | 1 | import { BasicConnectionModalEnum } from '../enum'; |
2 | 2 | import { FetchNodeComFlagTypeENum } from '../enum/node'; |
3 | -import type { CategoryConfigType, NodeItemConfigType } from '../types/node'; | |
3 | +import type { NodeItemConfigType } from '../types/node'; | |
4 | 4 | import { ActionCategoryConfig, ActionComponents } from './Action'; |
5 | 5 | import { EnrichmentCategoryConfig, EnrichmentComponents } from './Enrichment'; |
6 | 6 | import { ExternalCategoryConfig, ExternalComponents } from './External'; |
7 | 7 | import { FilterCategoryConfig, FilterComponents } from './Filter'; |
8 | 8 | import { FlowCategoryConfig, FlowComponents } from './Flow'; |
9 | 9 | import { TransformationCategoryConfig, TransformationComponents } from './Transformation'; |
10 | -import { RuleNodeTypeEnum } from './index.type'; | |
10 | +import { CategoryItemType, RuleNodeTypeEnum } from './index.type'; | |
11 | 11 | |
12 | 12 | const createModules = import.meta.glob('../packages/**/create.vue'); |
13 | 13 | const connectionModules = import.meta.glob('../packages/**/connection.vue'); |
... | ... | @@ -63,7 +63,7 @@ export const fetchNodeExtraContent = async (config: NodeItemConfigType) => { |
63 | 63 | |
64 | 64 | export const allComponents: Record< |
65 | 65 | Exclude<RuleNodeTypeEnum, RuleNodeTypeEnum.ENTRY>, |
66 | - { category: CategoryConfigType; components: NodeItemConfigType[] } | |
66 | + CategoryItemType | |
67 | 67 | > = { |
68 | 68 | [RuleNodeTypeEnum.FILTER]: { |
69 | 69 | category: FilterCategoryConfig, | ... | ... |
1 | +import { CategoryConfigType, NodeItemConfigType } from '../types/node'; | |
2 | + | |
1 | 3 | export enum RuleNodeTypeEnum { |
2 | 4 | ENTRY = 'ENTRY', |
3 | 5 | FLOW = 'FLOW', |
... | ... | @@ -7,3 +9,8 @@ export enum RuleNodeTypeEnum { |
7 | 9 | FILTER = 'FILTER', |
8 | 10 | TRANSFORMATION = 'TRANSFORMATION', |
9 | 11 | } |
12 | + | |
13 | +export interface CategoryItemType { | |
14 | + category: CategoryConfigType; | |
15 | + components: NodeItemConfigType[]; | |
16 | +} | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { CategoryItemType, RuleNodeTypeEnum } from '../../../packages/index.type'; | |
3 | + import Icon from '/@/components/Icon'; | |
4 | + import { Tooltip } from 'ant-design-vue'; | |
5 | + | |
6 | + const emit = defineEmits<{ | |
7 | + (eventName: 'update:activeKey', activeKey?: RuleNodeTypeEnum); | |
8 | + }>(); | |
9 | + | |
10 | + defineProps<{ | |
11 | + activeKey?: RuleNodeTypeEnum; | |
12 | + category?: CategoryItemType; | |
13 | + }>(); | |
14 | + | |
15 | + const handleClick = (category?: RuleNodeTypeEnum) => { | |
16 | + emit('update:activeKey', category); | |
17 | + }; | |
18 | +</script> | |
19 | + | |
20 | +<template> | |
21 | + <Tooltip color="#fff" placement="right"> | |
22 | + <template #title> | |
23 | + <p class="text-dark-900 font-bold text-xs m-0 py-1"> | |
24 | + {{ category?.category.title }} | |
25 | + </p> | |
26 | + <p class="text-gray-500 text-xs m-0 py-1"> | |
27 | + {{ category?.category.description }} | |
28 | + </p> | |
29 | + </template> | |
30 | + <div | |
31 | + class="h-16 mt-2 flex justify-center items-center flex-col cursor-pointer rounded" | |
32 | + :class="activeKey === category?.category.category ? 'bg-blue-100 text-blue-400' : ''" | |
33 | + @click="handleClick(category?.category.category)" | |
34 | + > | |
35 | + <Icon :icon="category?.category.icon" class="svg:text-3xl" /> | |
36 | + <div class="select-none">{{ category?.category.title }}</div> | |
37 | + </div> | |
38 | + </Tooltip> | |
39 | +</template> | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { cloneDeep } from 'lodash'; | |
3 | + import { computed, ref, unref } from 'vue'; | |
4 | + import { allComponents } from '../../../packages'; | |
5 | + import { CategoryItemType, RuleNodeTypeEnum } from '../../../packages/index.type'; | |
6 | + import CategoryItem from './CategoryItem.vue'; | |
7 | + import NodeItem from './NodeItem.vue'; | |
8 | + | |
9 | + const props = defineProps<{ | |
10 | + searchText?: string; | |
11 | + }>(); | |
12 | + | |
13 | + const activeKey = ref<RuleNodeTypeEnum | undefined>(RuleNodeTypeEnum.FILTER); | |
14 | + | |
15 | + const getCurrentCategoryNode = computed<CategoryItemType>(() => { | |
16 | + const category = cloneDeep(allComponents[unref(activeKey)!]) as CategoryItemType; | |
17 | + const { searchText } = props; | |
18 | + if (searchText) { | |
19 | + category.components = category.components.filter((item) => | |
20 | + item.name.toUpperCase().includes(searchText.toUpperCase()) | |
21 | + ); | |
22 | + } | |
23 | + return category; | |
24 | + }); | |
25 | +</script> | |
26 | + | |
27 | +<template> | |
28 | + <section class="absolute top-11 w-full flex flex-auto h-[calc(100%-2.75rem)] bg-neutral-100"> | |
29 | + <nav class="w-20 min-w-20 p-2 border-t border-light-50"> | |
30 | + <CategoryItem | |
31 | + v-for="category in allComponents" | |
32 | + :key="category.category.category" | |
33 | + v-model:activeKey="activeKey" | |
34 | + :category="category" | |
35 | + /> | |
36 | + </nav> | |
37 | + <body class="p-4 w-full flex flex-col gap-2 items-center overflow-x-hidden overflow-y-auto"> | |
38 | + <NodeItem | |
39 | + v-for="config in getCurrentCategoryNode.components" | |
40 | + :key="config.clazz" | |
41 | + :config="config" | |
42 | + :category="getCurrentCategoryNode.category" | |
43 | + /> | |
44 | + </body> | |
45 | + </section> | |
46 | +</template> | ... | ... |
1 | +<script setup lang="ts"> | |
2 | + import { Input, Tooltip } from 'ant-design-vue'; | |
3 | + import { Icon } from '/@/components/Icon'; | |
4 | + | |
5 | + const emit = defineEmits(['fold', 'search']); | |
6 | + | |
7 | + const handleOnFold = () => emit('fold'); | |
8 | + | |
9 | + const handleInputChange = (event: Event) => { | |
10 | + const value = (event.target as HTMLInputElement).value; | |
11 | + emit('search', value); | |
12 | + }; | |
13 | +</script> | |
14 | + | |
15 | +<template> | |
16 | + <head class="h-11 w-full flex justify-between items-center bg-neutral-100"> | |
17 | + <Input | |
18 | + placeholder="搜索节点" | |
19 | + class="!mx-2 flex-auto" | |
20 | + @change="handleInputChange" | |
21 | + :allowClear="true" | |
22 | + > | |
23 | + <template #suffix> | |
24 | + <Icon icon="material-symbols:search" /> | |
25 | + </template> | |
26 | + </Input> | |
27 | + <div class="w-14 min-w-14 flex justify-center"> | |
28 | + <Tooltip title="折叠"> | |
29 | + <Icon | |
30 | + icon="ant-design:menu-fold-outlined" | |
31 | + class="cursor-pointer svg:text-xl" | |
32 | + @click="handleOnFold" | |
33 | + /> | |
34 | + </Tooltip> | |
35 | + </div> | |
36 | + </head> | |
37 | +</template> | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { Tooltip } from 'ant-design-vue'; | |
3 | + import { computed, ref, toRaw, unref } from 'vue'; | |
4 | + import { EFFECT_SYMBOL, TRANSFER_DATA_KEY } from '../../../hook/useDragCreate'; | |
5 | + import { CategoryConfigType, NodeItemConfigType } from '../../../types/node'; | |
6 | + | |
7 | + const props = defineProps<{ | |
8 | + category?: CategoryConfigType; | |
9 | + config?: NodeItemConfigType; | |
10 | + }>(); | |
11 | + | |
12 | + const visible = ref(false); | |
13 | + | |
14 | + const getNodeDefinition = computed(() => { | |
15 | + const { config } = props; | |
16 | + const { configurationDescriptor } = config || {}; | |
17 | + const { nodeDefinition } = configurationDescriptor || {}; | |
18 | + return nodeDefinition || {}; | |
19 | + }); | |
20 | + | |
21 | + const getIcon = computed(() => { | |
22 | + const { icon } = unref(getNodeDefinition); | |
23 | + const { icon: categoryIcon } = props.category || {}; | |
24 | + return icon || categoryIcon || 'tabler:circuit-ground'; | |
25 | + }); | |
26 | + | |
27 | + const getIconUrl = computed(() => { | |
28 | + const { iconUrl } = unref(getNodeDefinition); | |
29 | + return iconUrl; | |
30 | + }); | |
31 | + | |
32 | + const getBackgroundColor = computed(() => { | |
33 | + const { config, category } = props; | |
34 | + const { backgroundColor } = config || {}; | |
35 | + const { backgroundColor: categoryBackgroundColor } = category || {}; | |
36 | + return backgroundColor || categoryBackgroundColor; | |
37 | + }); | |
38 | + | |
39 | + const handleOnDragStart = (event: DragEvent, options: object) => { | |
40 | + if (event.dataTransfer) { | |
41 | + event.dataTransfer.setData( | |
42 | + TRANSFER_DATA_KEY, | |
43 | + JSON.stringify({ offsetX: event.offsetX, offsetY: event.offsetY, options }) | |
44 | + ); | |
45 | + event.dataTransfer.effectAllowed = EFFECT_SYMBOL; | |
46 | + } | |
47 | + }; | |
48 | + | |
49 | + const handleDragStart = (event: DragEvent) => { | |
50 | + visible.value = false; | |
51 | + handleOnDragStart(event, toRaw(unref(props))); | |
52 | + }; | |
53 | +</script> | |
54 | + | |
55 | +<template> | |
56 | + <Tooltip | |
57 | + v-model:visible="visible" | |
58 | + placement="right" | |
59 | + color="#fff" | |
60 | + trigger="hover" | |
61 | + :mouse-enter-delay="0.5" | |
62 | + overlay-class-name="!max-w-90" | |
63 | + > | |
64 | + <template #title> | |
65 | + <section class="text-dark-900"> | |
66 | + <h1 class="font-bold dark:text-dark-50"> | |
67 | + {{ config?.name }} | |
68 | + </h1> | |
69 | + <p class="italic text-gray-600"> | |
70 | + {{ getNodeDefinition.description }} | |
71 | + </p> | |
72 | + <p v-html="getNodeDefinition.details"></p> | |
73 | + </section> | |
74 | + </template> | |
75 | + <main | |
76 | + class="node flex items-center cursor-pointer w-44 h-12 min-h-12 rounded border relative px-4 py-2 border-gray-700 z-10" | |
77 | + :style="{ backgroundColor: getBackgroundColor }" | |
78 | + :draggable="true" | |
79 | + @dragstart="handleDragStart" | |
80 | + > | |
81 | + <div> | |
82 | + <img v-if="getIconUrl" class="w-4 h-4" :src="getIconUrl" alt="" /> | |
83 | + <Icon v-if="!getIconUrl" class="text-2xl dark:text-light-50" :icon="getIcon" /> | |
84 | + </div> | |
85 | + <div class="flex flex-1 text-xs flex-col ml-2 text-left truncate"> | |
86 | + <span class="text-gray-700 w-full truncate text-xs dark:text-light-50"> | |
87 | + {{ config?.name }} | |
88 | + </span> | |
89 | + </div> | |
90 | + <div class="w-4 h-4 bg-dark-50 rounded-1 border absolute -left-2 border-light-50"></div> | |
91 | + <div class="w-4 h-4 bg-dark-50 rounded-1 border absolute -right-2 border-light-50"></div> | |
92 | + </main> | |
93 | + </Tooltip> | |
94 | +</template> | |
95 | + | |
96 | +<style lang="css" scoped> | |
97 | + .node::before { | |
98 | + content: ''; | |
99 | + position: absolute; | |
100 | + width: 100%; | |
101 | + height: 100%; | |
102 | + left: 0; | |
103 | + display: none; | |
104 | + background-color: #00000027; | |
105 | + } | |
106 | + | |
107 | + .node:hover::before { | |
108 | + display: block; | |
109 | + } | |
110 | +</style> | ... | ... |
1 | +export { default as MenuSidebar } from './index.vue'; | ... | ... |
1 | +<script setup lang="ts"> | |
2 | + import NodeFilter from './NodeFilter.vue'; | |
3 | + import CategoryPanel from './CategoryPanel.vue'; | |
4 | + import { computed, CSSProperties, ref, unref } from 'vue'; | |
5 | + import { Icon } from '/@/components/Icon'; | |
6 | + import { Tooltip } from 'ant-design-vue'; | |
7 | + | |
8 | + const expand = ref(true); | |
9 | + | |
10 | + const searchText = ref(); | |
11 | + | |
12 | + const handleOnFold = () => { | |
13 | + expand.value = false; | |
14 | + }; | |
15 | + | |
16 | + const getSidebarStyle = computed<CSSProperties>(() => { | |
17 | + return { | |
18 | + flex: `0 0 ${unref(expand) ? '320px' : 0}`, | |
19 | + width: unref(expand) ? '320px' : 0, | |
20 | + minWidth: unref(expand) ? '320px' : 0, | |
21 | + }; | |
22 | + }); | |
23 | + | |
24 | + const handleSearch = (value: string) => { | |
25 | + searchText.value = value; | |
26 | + }; | |
27 | +</script> | |
28 | + | |
29 | +<template> | |
30 | + <section | |
31 | + :style="getSidebarStyle" | |
32 | + class="h-full shadow shadow-lg relative z-50 shadow-dark-600 overflow-hidden transition-all" | |
33 | + > | |
34 | + <NodeFilter @fold="handleOnFold" @search="handleSearch" /> | |
35 | + <CategoryPanel :searchText="searchText" /> | |
36 | + </section> | |
37 | + <Tooltip title="展开"> | |
38 | + <Icon | |
39 | + icon="ant-design:menu-unfold-outlined" | |
40 | + class="cursor-pointer svg:text-2xl absolute top-5 left-5 z-40 !flex justify-center items-center w-10 h-10 rounded-1 bg-blue-200 svg:text-light-50 shadow-lg" | |
41 | + @click="expand = true" | |
42 | + /> | |
43 | + </Tooltip> | |
44 | +</template> | ... | ... |
1 | +import { h } from 'vue'; | |
1 | 2 | import { BasicColumn } from '/@/components/Table'; |
3 | +import { dateUtil } from '/@/utils/dateUtil'; | |
4 | +import Icon from '/@/components/Icon'; | |
5 | +import { Modal } from 'ant-design-vue'; | |
6 | +import { JsonPreview } from '/@/components/CodeEditor'; | |
7 | + | |
8 | +const handleOpenJsonPreviewModal = (title: string, content: string) => { | |
9 | + Modal.info({ | |
10 | + title, | |
11 | + width: 600, | |
12 | + content: h(JsonPreview, { data: JSON.parse(content) }), | |
13 | + }); | |
14 | +}; | |
2 | 15 | |
3 | 16 | export const columns: BasicColumn[] = [ |
4 | 17 | { |
5 | 18 | title: '事件时间', |
6 | 19 | dataIndex: 'createdTime', |
20 | + width: 200, | |
21 | + format(text) { | |
22 | + return dateUtil(text).format('YYYY-MM-DD HH:mm:ss'); | |
23 | + }, | |
7 | 24 | }, |
8 | 25 | { |
9 | 26 | title: '服务器', |
10 | 27 | dataIndex: 'body.server', |
28 | + ellipsis: true, | |
11 | 29 | }, |
12 | 30 | { |
13 | 31 | title: '类型', |
14 | - dataIndex: 'type', | |
32 | + dataIndex: 'body.type', | |
33 | + ellipsis: true, | |
15 | 34 | }, |
16 | 35 | { |
17 | 36 | title: '实体类型', |
18 | - dataIndex: 'entityId.entityType', | |
37 | + dataIndex: 'body.entityName', | |
38 | + ellipsis: true, | |
19 | 39 | }, |
20 | 40 | { |
21 | 41 | title: '消息ID', |
22 | - dataIndex: 'id.id', | |
42 | + dataIndex: 'body.entityId', | |
43 | + ellipsis: true, | |
23 | 44 | }, |
24 | 45 | { |
25 | 46 | title: '消息类型', |
26 | - dataIndex: 'msgtype', | |
47 | + dataIndex: 'body.msgType', | |
48 | + ellipsis: true, | |
27 | 49 | }, |
28 | 50 | { |
29 | 51 | title: '关联类型', |
30 | - dataIndex: 'relatedType', | |
52 | + dataIndex: 'body.relationType', | |
53 | + ellipsis: true, | |
31 | 54 | }, |
32 | 55 | { |
33 | 56 | title: '数据', |
34 | - dataIndex: 'data', | |
57 | + dataIndex: 'body.data', | |
58 | + ellipsis: true, | |
59 | + width: 80, | |
60 | + customRender({ text }) { | |
61 | + return ( | |
62 | + text && | |
63 | + h(Icon, { | |
64 | + icon: 'material-symbols:more-horiz', | |
65 | + class: 'cursor-pointer svg:text-2xl', | |
66 | + onClick: () => handleOpenJsonPreviewModal('数据', text), | |
67 | + }) | |
68 | + ); | |
69 | + }, | |
35 | 70 | }, |
36 | 71 | { |
37 | 72 | title: '元数据', |
38 | - dataIndex: 'metadaeta', | |
73 | + dataIndex: 'body.metadata', | |
74 | + ellipsis: true, | |
75 | + width: 80, | |
76 | + customRender({ text }) { | |
77 | + return ( | |
78 | + text && | |
79 | + h(Icon, { | |
80 | + icon: 'material-symbols:more-horiz', | |
81 | + class: 'cursor-pointer svg:text-2xl', | |
82 | + onClick: () => handleOpenJsonPreviewModal('元数据', text), | |
83 | + }) | |
84 | + ); | |
85 | + }, | |
39 | 86 | }, |
40 | 87 | { |
41 | 88 | title: '错误', |
42 | 89 | dataIndex: 'error', |
90 | + width: 80, | |
91 | + slots: { | |
92 | + customRender: 'error', | |
93 | + }, | |
43 | 94 | }, |
44 | 95 | ]; | ... | ... |
... | ... | @@ -46,6 +46,8 @@ |
46 | 46 | const result = await getRuleNodeEventList( |
47 | 47 | unref(getNodeId)!, |
48 | 48 | { |
49 | + sortProperty: 'createdTime', | |
50 | + sortOrder: 'DESC', | |
49 | 51 | page: params.page - 1, |
50 | 52 | pageSize: params.pageSize, |
51 | 53 | tenantId: unref(getTenantId), |
... | ... | @@ -130,6 +132,7 @@ |
130 | 132 | </template> |
131 | 133 | <template #error="{ text }"> |
132 | 134 | <Icon |
135 | + v-if="text" | |
133 | 136 | icon="material-symbols:more-horiz" |
134 | 137 | class="cursor-pointer svg:text-2xl" |
135 | 138 | @click="handleOpenDetailModal(text)" | ... | ... |
... | ... | @@ -18,12 +18,14 @@ export const columns: BasicColumn[] = [ |
18 | 18 | }, |
19 | 19 | { |
20 | 20 | title: '消息处理', |
21 | - dataIndex: 'body.event', | |
21 | + dataIndex: 'body.messagesProcessed', | |
22 | 22 | ellipsis: true, |
23 | + width: 100, | |
23 | 24 | }, |
24 | 25 | { |
25 | 26 | title: '错误发生', |
26 | - dataIndex: 'body.event', | |
27 | + dataIndex: 'body.errorsOccurred', | |
27 | 28 | ellipsis: true, |
29 | + width: 100, | |
28 | 30 | }, |
29 | 31 | ]; | ... | ... |