feat!(web): migrate UI from SvelteKit to React + shadcn/ui + ReUI
CI / changes (push) Successful in 17s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m1s
CI / bird2 (push) Successful in 17s
CI / release (push) Failing after 2m22s
CI / changes (push) Successful in 17s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m1s
CI / bird2 (push) Successful in 17s
CI / release (push) Failing after 2m22s
Web UI полностью переведён с SvelteKit на новый стек: React 19, TanStack Router/Query/Table/Virtual, shadcn/ui (base-nova) и ReUI enterprise-компоненты (data-grid, filters, autocomplete). Новый код разложен по слоям: packages/ui (shadcn-примитивы), apps/web (роуты, shared-обёртки, ReUI-адаптации). BREAKING CHANGE: меняется структура и инструментинг фронтенда. - apps/web/ — новый Vite + React-проект (@evobgp/web), file-based роуты TanStack Router; экраны dashboard, modules, monitoring, network, operations, schedule, settings, tenant-settings, access, directories. - packages/ui/ — shadcn/ui-примитивы (@evobgp/ui) с общими стилями globals.css и cn-утилитой; CLI shadcn запускается из apps/web. - apps/web/src/components/reui/ — enterprise-паттерны ReUI. - pnpm workspace (pnpm-workspace.yaml, pnpm-lock.yaml, tsconfig.base.json) заменяет npm-проект в web/. - web/ переименован в web-legacy-svelte/ (архив-референс для миграции); импорты оттуда запрещены правилом WEB-22. - CI (.gitea/workflows/ci.yaml): job web переведён на Node 22 + pnpm 10 (typecheck/lint/build через pnpm --filter @evobgp/web); пути триггеров обновлены под apps/web|packages/ui. - deploy/docker/evobgp-web/Dockerfile: сборка из корня репозитория, pnpm install --frozen-lockfile, выход dist из apps/web/dist. - .cursor/rules/web-shadcn.mdc, context7-stack.mdc, engineering.mdc, AGENTS.md — обновлены под React-стек (WEB-01..WEB-22, DOC-SYNC-06/07). Проверки WEB-19 локально: typecheck, lint, build — exit 0. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
/** Режимы расписания (5-field cron, UTC) — совместимы с robfig/cron в scheduler. */
|
||||
export type ScheduleMode = 'minutes' | 'hours' | 'daily' | 'weekly' | 'custom';
|
||||
|
||||
export type ScheduleEditor = {
|
||||
mode: ScheduleMode;
|
||||
/** Интервал в минутах (режим minutes). */
|
||||
intervalMinutes: string;
|
||||
/** Интервал в часах (режим hours). */
|
||||
intervalHours: string;
|
||||
minute: string;
|
||||
hour: string;
|
||||
/** 0 = воскресенье … 6 = суббота */
|
||||
weekday: string;
|
||||
customCron: string;
|
||||
};
|
||||
|
||||
export const scheduleModeOptions: { value: ScheduleMode; label: string }[] = [
|
||||
{ value: 'minutes', label: 'Каждые N минут' },
|
||||
{ value: 'hours', label: 'Каждые N часов' },
|
||||
{ value: 'daily', label: 'Ежедневно в указанное время' },
|
||||
{ value: 'weekly', label: 'Еженедельно в указанный день' },
|
||||
{ value: 'custom', label: 'Cron вручную (расширенный)' }
|
||||
];
|
||||
|
||||
export const weekdayOptions = [
|
||||
{ value: '0', label: 'Воскресенье' },
|
||||
{ value: '1', label: 'Понедельник' },
|
||||
{ value: '2', label: 'Вторник' },
|
||||
{ value: '3', label: 'Среда' },
|
||||
{ value: '4', label: 'Четверг' },
|
||||
{ value: '5', label: 'Пятница' },
|
||||
{ value: '6', label: 'Суббота' }
|
||||
] as const;
|
||||
|
||||
export function defaultScheduleEditor(cron = '0 3 * * *'): ScheduleEditor {
|
||||
return cronToEditor(cron);
|
||||
}
|
||||
|
||||
function splitCron(cron: string): [string, string, string, string, string] | null {
|
||||
const parts = cron.trim().split(/\s+/);
|
||||
if (parts.length !== 5) return null;
|
||||
return [parts[0], parts[1], parts[2], parts[3], parts[4]];
|
||||
}
|
||||
|
||||
function clampInt(raw: string, min: number, max: number): number {
|
||||
const n = Math.floor(Number(String(raw).trim()));
|
||||
if (!Number.isFinite(n)) return min;
|
||||
return Math.min(max, Math.max(min, n));
|
||||
}
|
||||
|
||||
function pad2(n: number): string {
|
||||
return String(n).padStart(2, '0');
|
||||
}
|
||||
|
||||
/** Пытается разобрать cron в редактор; неизвестные выражения → custom. */
|
||||
export function cronToEditor(cron: string): ScheduleEditor {
|
||||
const base: ScheduleEditor = {
|
||||
mode: 'custom',
|
||||
intervalMinutes: '30',
|
||||
intervalHours: '2',
|
||||
minute: '0',
|
||||
hour: '3',
|
||||
weekday: '0',
|
||||
customCron: cron.trim() || '0 3 * * *'
|
||||
};
|
||||
|
||||
const parts = splitCron(cron);
|
||||
if (!parts) return base;
|
||||
|
||||
const [min, hour, dom, month, dow] = parts;
|
||||
|
||||
const minEvery = min.match(/^\*\/(\d+)$/);
|
||||
if (minEvery && hour === '*' && dom === '*' && month === '*' && dow === '*') {
|
||||
return { ...base, mode: 'minutes', intervalMinutes: minEvery[1], customCron: cron.trim() };
|
||||
}
|
||||
|
||||
const hourEvery = hour.match(/^\*\/(\d+)$/);
|
||||
if (hourEvery && !min.includes('*') && dom === '*' && month === '*' && dow === '*') {
|
||||
return {
|
||||
...base,
|
||||
mode: 'hours',
|
||||
intervalHours: hourEvery[1],
|
||||
minute: min,
|
||||
customCron: cron.trim()
|
||||
};
|
||||
}
|
||||
|
||||
if (!min.includes('*') && !hour.includes('*') && dom === '*' && month === '*' && dow === '*') {
|
||||
return { ...base, mode: 'daily', minute: min, hour, customCron: cron.trim() };
|
||||
}
|
||||
|
||||
if (!min.includes('*') && !hour.includes('*') && dom === '*' && month === '*' && dow !== '*') {
|
||||
return { ...base, mode: 'weekly', minute: min, hour, weekday: dow, customCron: cron.trim() };
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
/** Собирает 5-field cron из редактора. */
|
||||
export function editorToCron(editor: ScheduleEditor): string {
|
||||
switch (editor.mode) {
|
||||
case 'minutes': {
|
||||
const n = clampInt(editor.intervalMinutes, 1, 59);
|
||||
return `*/${n} * * * *`;
|
||||
}
|
||||
case 'hours': {
|
||||
const n = clampInt(editor.intervalHours, 1, 23);
|
||||
const m = clampInt(editor.minute, 0, 59);
|
||||
return `${m} */${n} * * *`;
|
||||
}
|
||||
case 'daily': {
|
||||
const m = clampInt(editor.minute, 0, 59);
|
||||
const h = clampInt(editor.hour, 0, 23);
|
||||
return `${m} ${h} * * *`;
|
||||
}
|
||||
case 'weekly': {
|
||||
const m = clampInt(editor.minute, 0, 59);
|
||||
const h = clampInt(editor.hour, 0, 23);
|
||||
const d = clampInt(editor.weekday, 0, 6);
|
||||
return `${m} ${h} * * ${d}`;
|
||||
}
|
||||
case 'custom':
|
||||
return editor.customCron.trim() || '0 3 * * *';
|
||||
}
|
||||
}
|
||||
|
||||
/** Человекочитаемое описание расписания для таблицы и подсказок. */
|
||||
export function describeCron(cron: string): string {
|
||||
const ed = cronToEditor(cron);
|
||||
switch (ed.mode) {
|
||||
case 'minutes': {
|
||||
const n = clampInt(ed.intervalMinutes, 1, 59);
|
||||
return `Каждые ${n} ${minutesLabel(n)} (UTC)`;
|
||||
}
|
||||
case 'hours': {
|
||||
const n = clampInt(ed.intervalHours, 1, 23);
|
||||
const m = clampInt(ed.minute, 0, 59);
|
||||
return `Каждые ${n} ${hoursLabel(n)}, в :${pad2(m)} (UTC)`;
|
||||
}
|
||||
case 'daily': {
|
||||
const h = clampInt(ed.hour, 0, 23);
|
||||
const m = clampInt(ed.minute, 0, 59);
|
||||
return `Ежедневно в ${pad2(h)}:${pad2(m)} UTC`;
|
||||
}
|
||||
case 'weekly': {
|
||||
const wd =
|
||||
weekdayOptions.find((w) => w.value === String(clampInt(ed.weekday, 0, 6)))?.label ??
|
||||
ed.weekday;
|
||||
const h = clampInt(ed.hour, 0, 23);
|
||||
const m = clampInt(ed.minute, 0, 59);
|
||||
return `Каждую ${wd.toLowerCase()} в ${pad2(h)}:${pad2(m)} UTC`;
|
||||
}
|
||||
case 'custom':
|
||||
return `Cron: ${cron.trim()}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Синхронизирует form.schedule из редактора. */
|
||||
export function applyScheduleEditor(editor: ScheduleEditor): {
|
||||
editor: ScheduleEditor;
|
||||
cron: string;
|
||||
} {
|
||||
if (editor.mode === 'custom') {
|
||||
const cron = editor.customCron.trim() || '0 3 * * *';
|
||||
return { editor: { ...editor, customCron: cron }, cron };
|
||||
}
|
||||
const cron = editorToCron(editor);
|
||||
return { editor: { ...editor, customCron: cron }, cron };
|
||||
}
|
||||
|
||||
function minutesLabel(n: number): string {
|
||||
if (n % 10 === 1 && n % 100 !== 11) return 'минуту';
|
||||
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return 'минуты';
|
||||
return 'минут';
|
||||
}
|
||||
|
||||
function hoursLabel(n: number): string {
|
||||
if (n % 10 === 1 && n % 100 !== 11) return 'час';
|
||||
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return 'часа';
|
||||
return 'часов';
|
||||
}
|
||||
Reference in New Issue
Block a user