mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-18 19:32:47 +08:00
295 lines
10 KiB
TypeScript
295 lines
10 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
import Editor, { type BeforeMount, type OnMount } from './MonacoEditor';
|
|
import { t as defaultTranslate } from '../i18n';
|
|
import { useOptionalI18n } from '../i18n/provider';
|
|
interface TableDesignerSqlPreviewProps {
|
|
sql: string;
|
|
darkMode?: boolean;
|
|
height?: string | number;
|
|
}
|
|
|
|
export type SqlChangeHighlightKind =
|
|
| 'add'
|
|
| 'comment'
|
|
| 'constraint'
|
|
| 'create'
|
|
| 'drop'
|
|
| 'modify'
|
|
| 'rename';
|
|
|
|
export interface SqlChangeHighlight {
|
|
line: string;
|
|
lineNumber: number;
|
|
kind: SqlChangeHighlightKind;
|
|
label: string;
|
|
}
|
|
|
|
type SqlPreviewTranslator = (key: string) => string;
|
|
type SqlChangeHighlightLabelKey =
|
|
| 'table_designer.sql_preview.change.add'
|
|
| 'table_designer.sql_preview.change.comment'
|
|
| 'table_designer.sql_preview.change.constraint'
|
|
| 'table_designer.sql_preview.change.create'
|
|
| 'table_designer.sql_preview.change.create_index'
|
|
| 'table_designer.sql_preview.change.drop'
|
|
| 'table_designer.sql_preview.change.modify'
|
|
| 'table_designer.sql_preview.change.rename';
|
|
|
|
const SQL_PREVIEW_LIGHT_THEME = 'gonavi-sql-preview-light';
|
|
const SQL_PREVIEW_DARK_THEME = 'gonavi-sql-preview-dark';
|
|
const SQL_PREVIEW_CHANGE_LABEL_FALLBACKS: Record<SqlChangeHighlightLabelKey, string> = {
|
|
'table_designer.sql_preview.change.add': 'Add change',
|
|
'table_designer.sql_preview.change.comment': 'Comment change',
|
|
'table_designer.sql_preview.change.constraint': 'Constraint change',
|
|
'table_designer.sql_preview.change.create': 'New table structure',
|
|
'table_designer.sql_preview.change.create_index': 'Create index',
|
|
'table_designer.sql_preview.change.drop': 'Drop change',
|
|
'table_designer.sql_preview.change.modify': 'Column property change',
|
|
'table_designer.sql_preview.change.rename': 'Rename change',
|
|
};
|
|
|
|
const CHANGE_LINE_RULES: Array<{
|
|
kind: SqlChangeHighlightKind;
|
|
labelKey: SqlChangeHighlightLabelKey;
|
|
pattern: RegExp;
|
|
}> = [
|
|
{ kind: 'rename', labelKey: 'table_designer.sql_preview.change.rename', pattern: /\b(RENAME\s+COLUMN|CHANGE\s+COLUMN|RENAME\s+TO|SP_RENAME)\b/i },
|
|
{ kind: 'add', labelKey: 'table_designer.sql_preview.change.add', pattern: /\b(ADD\s+COLUMN|ADD\s+PRIMARY\s+KEY)\b/i },
|
|
{ kind: 'drop', labelKey: 'table_designer.sql_preview.change.drop', pattern: /\b(DROP\s+COLUMN|DROP\s+PRIMARY\s+KEY)\b/i },
|
|
{ kind: 'modify', labelKey: 'table_designer.sql_preview.change.modify', pattern: /\b(MODIFY\s+COLUMN|ALTER\s+COLUMN|SET\s+DATA\s+TYPE|SET\s+DEFAULT|DROP\s+DEFAULT|SET\s+NOT\s+NULL|DROP\s+NOT\s+NULL)\b/i },
|
|
{ kind: 'constraint', labelKey: 'table_designer.sql_preview.change.constraint', pattern: /\b(ADD\s+CONSTRAINT|DROP\s+CONSTRAINT)\b/i },
|
|
{ kind: 'comment', labelKey: 'table_designer.sql_preview.change.comment', pattern: /\b(COMMENT\s+ON\s+COLUMN|COMMENT\s+ON\s+TABLE)\b/i },
|
|
{ kind: 'create', labelKey: 'table_designer.sql_preview.change.create_index', pattern: /\bCREATE\s+(UNIQUE\s+)?((CLUSTERED|NONCLUSTERED)\s+)?INDEX\b/i },
|
|
];
|
|
|
|
const CREATE_TABLE_PATTERN = /^\s*CREATE\s+TABLE\b/i;
|
|
|
|
const resolveSqlPreviewChangeLabel = (
|
|
labelKey: SqlChangeHighlightLabelKey,
|
|
translate: SqlPreviewTranslator = defaultTranslate,
|
|
): string => {
|
|
const translated = translate(labelKey);
|
|
return translated === labelKey ? SQL_PREVIEW_CHANGE_LABEL_FALLBACKS[labelKey] : translated;
|
|
};
|
|
|
|
const getCreateTableLineHighlight = (
|
|
line: string,
|
|
lineNumber: number,
|
|
translate?: SqlPreviewTranslator,
|
|
): SqlChangeHighlight | null => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('--')) return null;
|
|
return {
|
|
line,
|
|
lineNumber,
|
|
kind: 'create',
|
|
label: resolveSqlPreviewChangeLabel('table_designer.sql_preview.change.create', translate),
|
|
};
|
|
};
|
|
|
|
const getAlterLineHighlight = (
|
|
line: string,
|
|
lineNumber: number,
|
|
translate?: SqlPreviewTranslator,
|
|
): SqlChangeHighlight | null => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('--')) return null;
|
|
|
|
const matchedRule = CHANGE_LINE_RULES.find((rule) => rule.pattern.test(trimmed));
|
|
if (!matchedRule) return null;
|
|
|
|
return {
|
|
line,
|
|
lineNumber,
|
|
kind: matchedRule.kind,
|
|
label: resolveSqlPreviewChangeLabel(matchedRule.labelKey, translate),
|
|
};
|
|
};
|
|
|
|
export const resolveSqlChangeHighlights = (
|
|
sql: string,
|
|
translate?: SqlPreviewTranslator,
|
|
): SqlChangeHighlight[] => {
|
|
const lines = sql.split(/\r?\n/);
|
|
const isCreateTableSql = lines.some((line) => CREATE_TABLE_PATTERN.test(line));
|
|
|
|
return lines
|
|
.map((line, index) => (
|
|
isCreateTableSql
|
|
? getCreateTableLineHighlight(line, index + 1, translate)
|
|
: getAlterLineHighlight(line, index + 1, translate)
|
|
))
|
|
.filter((highlight): highlight is SqlChangeHighlight => Boolean(highlight));
|
|
};
|
|
|
|
const registerSqlPreviewThemes: BeforeMount = (monaco) => {
|
|
monaco.editor.defineTheme(SQL_PREVIEW_LIGHT_THEME, {
|
|
base: 'vs',
|
|
inherit: true,
|
|
rules: [
|
|
{ token: 'keyword', foreground: '006C9C', fontStyle: 'bold' },
|
|
{ token: 'operator', foreground: '8250DF' },
|
|
{ token: 'number', foreground: 'B45309' },
|
|
{ token: 'string', foreground: '15803D' },
|
|
{ token: 'comment', foreground: '64748B', fontStyle: 'italic' },
|
|
{ token: 'predefined', foreground: '0F766E' },
|
|
],
|
|
colors: {
|
|
'editor.background': '#00000000',
|
|
'editor.lineHighlightBackground': '#0F172A0A',
|
|
'editorGutter.background': '#00000000',
|
|
'editorLineNumber.foreground': '#94A3B8',
|
|
},
|
|
});
|
|
|
|
monaco.editor.defineTheme(SQL_PREVIEW_DARK_THEME, {
|
|
base: 'vs-dark',
|
|
inherit: true,
|
|
rules: [
|
|
{ token: 'keyword', foreground: '7DD3FC', fontStyle: 'bold' },
|
|
{ token: 'operator', foreground: 'C4B5FD' },
|
|
{ token: 'number', foreground: 'FDBA74' },
|
|
{ token: 'string', foreground: '86EFAC' },
|
|
{ token: 'comment', foreground: '94A3B8', fontStyle: 'italic' },
|
|
{ token: 'predefined', foreground: '5EEAD4' },
|
|
],
|
|
colors: {
|
|
'editor.background': '#00000000',
|
|
'editor.lineHighlightBackground': '#FFFFFF12',
|
|
'editorGutter.background': '#00000000',
|
|
'editorLineNumber.foreground': '#64748B',
|
|
},
|
|
});
|
|
};
|
|
|
|
const getLineDecorationClassName = (kind: SqlChangeHighlightKind): string =>
|
|
`gonavi-sql-preview-change-line gonavi-sql-preview-change-line-${kind}`;
|
|
|
|
const getLineDecorationMarkerClassName = (kind: SqlChangeHighlightKind): string =>
|
|
`gonavi-sql-preview-change-marker gonavi-sql-preview-change-marker-${kind}`;
|
|
|
|
const TableDesignerSqlPreview: React.FC<TableDesignerSqlPreviewProps> = ({
|
|
sql,
|
|
darkMode = false,
|
|
height = '360px',
|
|
}) => {
|
|
const decorationIdsRef = useRef<string[]>([]);
|
|
const editorRef = useRef<any>(null);
|
|
const monacoRef = useRef<any>(null);
|
|
const i18n = useOptionalI18n();
|
|
const translate = i18n?.t ?? defaultTranslate;
|
|
const changeHighlights = useMemo(() => resolveSqlChangeHighlights(sql, translate), [sql, translate]);
|
|
|
|
const applyChangeDecorations = useCallback(() => {
|
|
const editor = editorRef.current;
|
|
const monaco = monacoRef.current;
|
|
const model = editor?.getModel?.();
|
|
if (!editor || !monaco || !model) return;
|
|
|
|
const lineCount = model.getLineCount();
|
|
const decorations = changeHighlights
|
|
.filter((highlight) => highlight.lineNumber <= lineCount)
|
|
.map((highlight) => {
|
|
const endColumn = Math.max(1, model.getLineMaxColumn(highlight.lineNumber));
|
|
return {
|
|
range: new monaco.Range(highlight.lineNumber, 1, highlight.lineNumber, endColumn),
|
|
options: {
|
|
className: getLineDecorationClassName(highlight.kind),
|
|
hoverMessage: { value: highlight.label },
|
|
isWholeLine: true,
|
|
linesDecorationsClassName: getLineDecorationMarkerClassName(highlight.kind),
|
|
},
|
|
};
|
|
});
|
|
|
|
decorationIdsRef.current = editor.deltaDecorations(decorationIdsRef.current, decorations);
|
|
}, [changeHighlights]);
|
|
|
|
const handleEditorMount: OnMount = (editor, monaco) => {
|
|
editorRef.current = editor;
|
|
monacoRef.current = monaco;
|
|
applyChangeDecorations();
|
|
};
|
|
|
|
useEffect(() => {
|
|
applyChangeDecorations();
|
|
}, [applyChangeDecorations, sql]);
|
|
|
|
return (
|
|
<div
|
|
data-table-designer-sql-preview="true"
|
|
style={{
|
|
maxHeight: 400,
|
|
overflow: 'hidden',
|
|
borderRadius: 8,
|
|
border: darkMode ? '1px solid #333' : '1px solid #eee',
|
|
}}
|
|
>
|
|
<style>
|
|
{`
|
|
.gonavi-sql-preview-change-line {
|
|
border-left: 3px solid transparent;
|
|
}
|
|
.gonavi-sql-preview-change-line-add,
|
|
.gonavi-sql-preview-change-line-create {
|
|
background: rgba(22, 163, 74, 0.14);
|
|
border-left-color: #16a34a;
|
|
}
|
|
.gonavi-sql-preview-change-line-drop {
|
|
background: rgba(220, 38, 38, 0.14);
|
|
border-left-color: #dc2626;
|
|
}
|
|
.gonavi-sql-preview-change-line-modify,
|
|
.gonavi-sql-preview-change-line-rename,
|
|
.gonavi-sql-preview-change-line-constraint,
|
|
.gonavi-sql-preview-change-line-comment {
|
|
background: rgba(217, 119, 6, 0.16);
|
|
border-left-color: #d97706;
|
|
}
|
|
.gonavi-sql-preview-change-marker {
|
|
width: 4px !important;
|
|
margin-left: 2px;
|
|
border-radius: 999px;
|
|
}
|
|
.gonavi-sql-preview-change-marker-add,
|
|
.gonavi-sql-preview-change-marker-create {
|
|
background: #16a34a;
|
|
}
|
|
.gonavi-sql-preview-change-marker-drop {
|
|
background: #dc2626;
|
|
}
|
|
.gonavi-sql-preview-change-marker-modify,
|
|
.gonavi-sql-preview-change-marker-rename,
|
|
.gonavi-sql-preview-change-marker-constraint,
|
|
.gonavi-sql-preview-change-marker-comment {
|
|
background: #d97706;
|
|
}
|
|
`}
|
|
</style>
|
|
<Editor
|
|
beforeMount={registerSqlPreviewThemes}
|
|
defaultLanguage="sql"
|
|
height={height}
|
|
language="sql"
|
|
onMount={handleEditorMount}
|
|
options={{
|
|
automaticLayout: true,
|
|
fontFamily: 'var(--gn-font-mono)',
|
|
fontSize: 13,
|
|
lineNumbers: 'on',
|
|
lineDecorationsWidth: 14,
|
|
minimap: { enabled: false },
|
|
padding: { top: 8, bottom: 8 },
|
|
readOnly: true,
|
|
scrollBeyondLastLine: false,
|
|
wordWrap: 'on',
|
|
}}
|
|
theme={darkMode ? SQL_PREVIEW_DARK_THEME : SQL_PREVIEW_LIGHT_THEME}
|
|
value={sql}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TableDesignerSqlPreview;
|