import type { DatasetRefPayload, ManagedDatasetOption, ManagedDatasetSourceOption, } from './datasetOptions'; import huaweiIcon from '#/assets/datacenter/huawei-icon.svg'; import mysqlIcon from '#/assets/datacenter/mysql-icon.svg'; import postgresqlIcon from '#/assets/datacenter/postgresql-icon.svg'; import { groupManagedDatasetOptions, loadManagedDatasetOptions, } from './datasetOptions'; const SOURCE_LOGO_MAP: Record = { EXCEL: 'excel', EXCEL_MATERIALIZED: 'excel', GAUSSDB_NATIVE: 'gaussdb', GBASE_8A: 'gbase', GBASE_8S: 'gbase', MYSQL: 'mysql', ORACLE: 'oracle', POSTGRESQL: 'postgresql', PROJECT_MYSQL: 'mysql', }; type NodeLike = { data?: Record; id: string; }; type UpdateNodeData = ( nodeId: string, data: | ((node: Record) => Record) | Record, ) => void; type FlowInstance = { updateNodeData: UpdateNodeData; }; type RenderContext = FlowInstance | undefined; type RendererState = { loadingOptions: boolean; options: ManagedDatasetOption[]; optionsLoaded: boolean; pickerOpen: boolean; sources: ManagedDatasetSourceOption[]; tableSearchText: string; updateNodeData?: UpdateNodeData; }; function getState(parent: HTMLElement): RendererState { const holder = parent as HTMLElement & { __datasetState?: RendererState }; if (!holder.__datasetState) { holder.__datasetState = { pickerOpen: false, loadingOptions: false, optionsLoaded: false, options: [], sources: [], tableSearchText: '', }; } return holder.__datasetState; } function getUpdateNodeData(parent: HTMLElement, flowInstance?: RenderContext) { const state = getState(parent); if (flowInstance?.updateNodeData) { state.updateNodeData = flowInstance.updateNodeData.bind(flowInstance); } return state.updateNodeData; } function getDatasetKey(datasetRef?: DatasetRefPayload | null) { const tableId = datasetRef?.tableId; return tableId === null || tableId === undefined ? '' : String(tableId); } function getSourceKey(datasetRef?: DatasetRefPayload | null) { const sourceId = datasetRef?.sourceId; return sourceId === null || sourceId === undefined ? '' : String(sourceId); } function createSourceOnlyDatasetRef( source: ManagedDatasetSourceOption, ): DatasetRefPayload { return { sourceId: source.sourceId, catalogId: null, catalogName: '', tableId: null, tableName: '', versionId: null, }; } function escapeHtml(value?: null | number | string) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function getSourceLogoType(sourceType?: string) { return SOURCE_LOGO_MAP[sourceType || ''] || 'default'; } function buildSourceLogo(sourceType?: string) { const logoType = getSourceLogoType(sourceType); if (logoType === 'mysql') { return ``; } if (logoType === 'postgresql') { return ``; } if (logoType === 'gaussdb') { return ``; } if (logoType === 'oracle') { return ` `; } if (logoType === 'gbase') { return ` `; } if (logoType === 'excel') { return ` `; } return ` `; } function buildStyles() { return ` `; } function filterTableOptions( options: ManagedDatasetOption[], searchText: string, ) { const keyword = searchText.trim().toLowerCase(); if (!keyword) { return options; } return options.filter((option) => option.keywords.includes(keyword)); } function ensureOptionsLoaded( state: RendererState, parent: HTMLElement, node: NodeLike, flowInstance: RenderContext, rerender: ( parent: HTMLElement, node: NodeLike, flowInstance?: RenderContext, ) => void, ) { if (state.loadingOptions || state.optionsLoaded) { return; } state.loadingOptions = true; loadManagedDatasetOptions() .then((options) => { state.options = options; state.sources = groupManagedDatasetOptions(options); state.optionsLoaded = true; }) .finally(() => { state.loadingOptions = false; rerender(parent, node, flowInstance); }); } function buildPickerListItem( title: string, meta: string, active: boolean, action: string, extraAttr: string, iconHtml: string = '', ) { return ` `; } function buildSourceList( options: ManagedDatasetSourceOption[], activeKey: string, emptyText: string, ) { if (options.length === 0) { return `
${emptyText}
`; } return options .map((option) => buildPickerListItem( option.sourceName, `${option.tables.length} 张表`, activeKey === String(option.sourceId), 'select-source', `data-source-id="${escapeHtml(option.sourceId)}"`, buildSourceLogo(option.sourceType), ), ) .join(''); } function buildTableList( options: ManagedDatasetOption[], activeKey: string, emptyText: string, ) { if (options.length === 0) { return `
${emptyText}
`; } return options .map((option) => buildPickerListItem( option.tableName, `${option.sourceName} / ${option.catalogName}`, activeKey === String(option.datasetRef.tableId), 'select-dataset', `data-table-id="${escapeHtml(option.datasetRef.tableId)}"`, buildSourceLogo(option.sourceType), ), ) .join(''); } function buildSearchSummary(currentSource?: ManagedDatasetSourceOption) { if (!currentSource) { return '请选择连接服务'; } return ` ${buildSourceLogo(currentSource.sourceType)} ${escapeHtml(currentSource.sourceName)} `; } function bindInteractiveElements(parent: HTMLElement) { parent .querySelectorAll('button, input, select, textarea') .forEach((element) => { element.addEventListener('pointerdown', (event) => event.stopPropagation(), ); element.addEventListener('mousedown', (event) => event.stopPropagation()); }); } export function rerenderSearchNode( parent: HTMLElement, node: NodeLike, flowInstance?: RenderContext, ) { const state = getState(parent); const updateNodeData = getUpdateNodeData(parent, flowInstance); ensureOptionsLoaded(state, parent, node, flowInstance, rerenderSearchNode); const datasetRef = (node.data?.datasetRef || null) as DatasetRefPayload | null; const sourceKey = getSourceKey(datasetRef); const currentSource = state.sources.find( (item) => String(item.sourceId) === sourceKey, ); parent.innerHTML = ` ${buildStyles()}
连接服务
${ state.pickerOpen ? `
${state.loadingOptions ? '
正在加载连接服务...
' : buildSourceList(state.sources, sourceKey, '暂无可用连接服务')}
` : '' }
`; bindInteractiveElements(parent); parent .querySelector('[data-action="toggle-picker"]') ?.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); state.pickerOpen = !state.pickerOpen; rerenderSearchNode(parent, node, flowInstance); }); parent .querySelectorAll('[data-action="select-source"]') .forEach((element) => { element.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); const sourceId = element.dataset.sourceId; const source = state.sources.find( (item) => String(item.sourceId) === String(sourceId), ); if (!source) { return; } state.pickerOpen = false; const nextDatasetRef = createSourceOnlyDatasetRef(source); node.data = { ...node.data, datasetRef: nextDatasetRef, sourceName: source.sourceName, sourceType: source.sourceType, }; updateNodeData?.(node.id, { datasetRef: nextDatasetRef, sourceName: source.sourceName, sourceType: source.sourceType, }); rerenderSearchNode(parent, node, flowInstance); }); }); } export function rerenderSaveNode( parent: HTMLElement, node: NodeLike, flowInstance?: RenderContext, ) { const state = getState(parent); const updateNodeData = getUpdateNodeData(parent, flowInstance); ensureOptionsLoaded(state, parent, node, flowInstance, rerenderSaveNode); const datasetRef = (node.data?.datasetRef || null) as DatasetRefPayload | null; const activeKey = getDatasetKey(datasetRef); const filtered = filterTableOptions(state.options, state.tableSearchText); parent.innerHTML = ` ${buildStyles()}
已接入表
${state.loadingOptions ? '
正在加载已接入表...
' : buildTableList(filtered, activeKey, '没有匹配的已接入表')}
`; bindInteractiveElements(parent); parent .querySelector('[data-role="table-search"]') ?.addEventListener('input', (event) => { event.stopPropagation(); state.tableSearchText = (event.currentTarget as HTMLInputElement).value || ''; rerenderSaveNode(parent, node, flowInstance); }); parent .querySelectorAll('[data-action="select-dataset"]') .forEach((element) => { element.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); const tableId = element.dataset.tableId; const option = state.options.find( (item) => String(item.datasetRef.tableId) === String(tableId), ); if (!option) { return; } node.data = { ...node.data, datasetRef: option.datasetRef, sourceName: option.sourceName, sourceType: option.sourceType, }; updateNodeData?.(node.id, { datasetRef: option.datasetRef, sourceName: option.sourceName, sourceType: option.sourceType, }); rerenderSaveNode(parent, node, flowInstance); }); }); }