- 收敛 easyflow-ui-admin 的 lint、格式和类型问题 - 修正 demo 页面与管理端前端构建失败点 - 验证 pnpm lint 与 pnpm build 均已通过
629 lines
18 KiB
TypeScript
629 lines
18 KiB
TypeScript
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<string, string> = {
|
|
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<string, any>;
|
|
id: string;
|
|
};
|
|
|
|
type UpdateNodeData = (
|
|
nodeId: string,
|
|
data:
|
|
| ((node: Record<string, any>) => Record<string, any>)
|
|
| Record<string, any>,
|
|
) => 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 `<img class="dataset-node-brand-image" src="${mysqlIcon}" alt="" />`;
|
|
}
|
|
if (logoType === 'postgresql') {
|
|
return `<img class="dataset-node-brand-image" src="${postgresqlIcon}" alt="" />`;
|
|
}
|
|
if (logoType === 'gaussdb') {
|
|
return `<img class="dataset-node-brand-image" src="${huaweiIcon}" alt="" />`;
|
|
}
|
|
if (logoType === 'oracle') {
|
|
return `
|
|
<svg class="dataset-node-brand-svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
<rect x="4.6" y="7.2" width="14.8" height="9.6" rx="4.8" fill="#ea4335" />
|
|
<rect x="7.1" y="9.35" width="9.8" height="5.3" rx="2.65" fill="white" />
|
|
<rect x="8.4" y="10.65" width="7.2" height="2.7" rx="1.35" fill="#ea4335" />
|
|
</svg>
|
|
`;
|
|
}
|
|
if (logoType === 'gbase') {
|
|
return `
|
|
<svg class="dataset-node-brand-svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
<circle cx="12" cy="12" r="8" fill="#10b981" />
|
|
<path
|
|
d="M15.85 9.2C15.1 8.02 13.8 7.3 12.38 7.3C9.94 7.3 8.1 9.14 8.1 11.98C8.1 14.87 10.05 16.7 12.56 16.7C13.88 16.7 15.03 16.18 15.88 15.15V12.55H12.45"
|
|
stroke="white"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="1.7"
|
|
/>
|
|
</svg>
|
|
`;
|
|
}
|
|
if (logoType === 'excel') {
|
|
return `
|
|
<svg class="dataset-node-brand-svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
<rect x="4.4" y="4.2" width="15.2" height="15.6" rx="3.2" fill="#16a34a" />
|
|
<path
|
|
d="M9 9L11.1 12L9 15M15 9L12.9 12L15 15"
|
|
stroke="white"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="1.65"
|
|
/>
|
|
</svg>
|
|
`;
|
|
}
|
|
return `
|
|
<svg class="dataset-node-brand-svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
<ellipse cx="12" cy="6.5" rx="5.75" ry="2.5" fill="#94a3b8" />
|
|
<path d="M6.25 6.5V12.2C6.25 13.58 8.82 14.7 12 14.7C15.18 14.7 17.75 13.58 17.75 12.2V6.5" fill="#cbd5e1" />
|
|
<path d="M6.25 12.2V17.35C6.25 18.72 8.82 19.85 12 19.85C15.18 19.85 17.75 18.72 17.75 17.35V12.2" fill="#e2e8f0" />
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
function buildStyles() {
|
|
return `
|
|
<style>
|
|
.dataset-node-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.dataset-node-label {
|
|
font-size: 12px;
|
|
color: var(--tf-text-muted);
|
|
}
|
|
.dataset-node-picker-anchor {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
.dataset-node-picker-trigger {
|
|
width: 100%;
|
|
min-height: 38px;
|
|
border: 1px solid var(--tf-border-color);
|
|
border-radius: 8px;
|
|
padding: 7px 10px 7px 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
background: var(--tf-bg-surface);
|
|
color: var(--tf-text-primary);
|
|
text-align: left;
|
|
box-sizing: border-box;
|
|
}
|
|
.dataset-node-picker-trigger:hover {
|
|
border-color: var(--tf-border-color-strong);
|
|
}
|
|
.dataset-node-picker-trigger.is-open {
|
|
border-color: var(--tf-color-primary);
|
|
}
|
|
.dataset-node-picker-value {
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.dataset-node-picker-placeholder {
|
|
font-size: 12px;
|
|
color: var(--tf-text-secondary);
|
|
}
|
|
.dataset-node-picker-text {
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.dataset-node-picker-main {
|
|
min-width: 0;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.dataset-node-picker-arrow {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: var(--tf-text-secondary);
|
|
flex-shrink: 0;
|
|
}
|
|
.dataset-node-picker {
|
|
position: absolute;
|
|
top: calc(100% + 6px);
|
|
left: 0;
|
|
width: 100%;
|
|
border: 1px solid var(--tf-border-color);
|
|
border-radius: 8px;
|
|
background: var(--tf-bg-surface);
|
|
box-shadow: var(--tf-shadow-medium);
|
|
overflow: hidden;
|
|
z-index: 40;
|
|
}
|
|
.dataset-node-picker-body,
|
|
.dataset-node-list {
|
|
padding: 6px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
max-height: 240px;
|
|
overflow: auto;
|
|
}
|
|
.dataset-node-picker-item {
|
|
width: 100%;
|
|
border: none;
|
|
background: transparent;
|
|
border-radius: 6px;
|
|
padding: 9px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
color: inherit;
|
|
}
|
|
.dataset-node-picker-item:hover {
|
|
background: var(--tf-bg-muted);
|
|
}
|
|
.dataset-node-picker-item.is-active {
|
|
background: var(--tf-bg-muted);
|
|
color: var(--tf-color-primary);
|
|
}
|
|
.dataset-node-picker-item-inner {
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.dataset-node-picker-item-main {
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.dataset-node-picker-item-title {
|
|
font-size: 12px;
|
|
color: inherit;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.dataset-node-picker-item-meta {
|
|
display: block;
|
|
font-size: 11px;
|
|
line-height: 16px;
|
|
color: var(--tf-text-secondary);
|
|
}
|
|
.dataset-node-empty {
|
|
font-size: 12px;
|
|
color: var(--tf-text-secondary);
|
|
}
|
|
.dataset-node-brand {
|
|
width: 20px;
|
|
height: 20px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.dataset-node-brand-image,
|
|
.dataset-node-brand-svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
display: block;
|
|
object-fit: contain;
|
|
}
|
|
.dataset-node-list-box {
|
|
border: 1px solid var(--tf-border-color);
|
|
border-radius: 8px;
|
|
background: var(--tf-bg-surface);
|
|
overflow: hidden;
|
|
}
|
|
.dataset-node-search {
|
|
width: 100%;
|
|
border: 1px solid var(--tf-border-color);
|
|
border-radius: 8px;
|
|
padding: 7px 10px;
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
color: var(--tf-text-primary);
|
|
background: var(--tf-bg-surface);
|
|
box-sizing: border-box;
|
|
}
|
|
.dataset-node-search:focus {
|
|
outline: none;
|
|
border-color: var(--tf-color-primary);
|
|
}
|
|
</style>
|
|
`;
|
|
}
|
|
|
|
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 `
|
|
<button
|
|
type="button"
|
|
class="dataset-node-picker-item nopan nodrag nowheel ${active ? 'is-active' : ''}"
|
|
data-action="${action}"
|
|
${extraAttr}
|
|
>
|
|
<span class="dataset-node-picker-item-inner">
|
|
${iconHtml ? `<span class="dataset-node-brand">${iconHtml}</span>` : ''}
|
|
<span class="dataset-node-picker-item-main">
|
|
<span class="dataset-node-picker-item-title">${escapeHtml(title)}</span>
|
|
${meta ? `<span class="dataset-node-picker-item-meta">${escapeHtml(meta)}</span>` : ''}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
function buildSourceList(
|
|
options: ManagedDatasetSourceOption[],
|
|
activeKey: string,
|
|
emptyText: string,
|
|
) {
|
|
if (options.length === 0) {
|
|
return `<div class="dataset-node-empty">${emptyText}</div>`;
|
|
}
|
|
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 `<div class="dataset-node-empty">${emptyText}</div>`;
|
|
}
|
|
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 '<span class="dataset-node-picker-placeholder">请选择连接服务</span>';
|
|
}
|
|
return `
|
|
<span class="dataset-node-brand">${buildSourceLogo(currentSource.sourceType)}</span>
|
|
<span class="dataset-node-picker-text">
|
|
<span class="dataset-node-picker-main">${escapeHtml(currentSource.sourceName)}</span>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
function bindInteractiveElements(parent: HTMLElement) {
|
|
parent
|
|
.querySelectorAll<HTMLElement>('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()}
|
|
<div class="dataset-node-section">
|
|
<div class="dataset-node-label">连接服务</div>
|
|
<div class="dataset-node-picker-anchor">
|
|
<button type="button" class="dataset-node-picker-trigger nopan nodrag nowheel ${state.pickerOpen ? 'is-open' : ''}" data-action="toggle-picker">
|
|
<span class="dataset-node-picker-value">${buildSearchSummary(currentSource)}</span>
|
|
<span class="dataset-node-picker-arrow">
|
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path></svg>
|
|
</span>
|
|
</button>
|
|
${
|
|
state.pickerOpen
|
|
? `
|
|
<div class="dataset-node-picker">
|
|
<div class="dataset-node-picker-body">
|
|
${state.loadingOptions ? '<div class="dataset-node-empty">正在加载连接服务...</div>' : buildSourceList(state.sources, sourceKey, '暂无可用连接服务')}
|
|
</div>
|
|
</div>
|
|
`
|
|
: ''
|
|
}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
bindInteractiveElements(parent);
|
|
|
|
parent
|
|
.querySelector<HTMLElement>('[data-action="toggle-picker"]')
|
|
?.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
state.pickerOpen = !state.pickerOpen;
|
|
rerenderSearchNode(parent, node, flowInstance);
|
|
});
|
|
|
|
parent
|
|
.querySelectorAll<HTMLElement>('[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()}
|
|
<div class="dataset-node-section">
|
|
<div class="dataset-node-label">已接入表</div>
|
|
<input class="dataset-node-search nopan nodrag nowheel" data-role="table-search" placeholder="搜索连接 / 库 / 表" value="${escapeHtml(state.tableSearchText)}" />
|
|
<div class="dataset-node-list-box">
|
|
<div class="dataset-node-list">
|
|
${state.loadingOptions ? '<div class="dataset-node-empty">正在加载已接入表...</div>' : buildTableList(filtered, activeKey, '没有匹配的已接入表')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
bindInteractiveElements(parent);
|
|
|
|
parent
|
|
.querySelector<HTMLInputElement>('[data-role="table-search"]')
|
|
?.addEventListener('input', (event) => {
|
|
event.stopPropagation();
|
|
state.tableSearchText =
|
|
(event.currentTarget as HTMLInputElement).value || '';
|
|
rerenderSaveNode(parent, node, flowInstance);
|
|
});
|
|
|
|
parent
|
|
.querySelectorAll<HTMLElement>('[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);
|
|
});
|
|
});
|
|
}
|