refactor(client): split feature chunks and polish environment ui

- lazy-load route pages and add entity-based manual chunking in Vite

- prevent environment pages from crashing when legacy records miss data

- fix sidebar toggle chevron direction and make env forms full width
This commit is contained in:
2026-04-10 21:32:07 +03:00
parent fd515ee459
commit d511c85891
8 changed files with 159 additions and 61 deletions
+78 -23
View File
@@ -1,3 +1,4 @@
import { lazy, Suspense } from 'react';
import { NavLink, Navigate, Route, Routes } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Globe, Monitor, ClipboardList, Activity, KeyRound, Braces } from 'lucide-react';
@@ -5,29 +6,81 @@ import type { LucideIcon } from 'lucide-react';
import styles from './App.module.css';
import { SidePanel, ThemeSwitcher } from './ui';
import liqaLogo from './assets/liqa.svg';
import { EnvironmentsPage } from './pages/environment/EnvironmentsPage';
import { EnvironmentDetailPage } from './pages/environment/EnvironmentDetailPage';
import { CreateEnvironmentPage } from './pages/environment/CreateEnvironmentPage';
import { EditEnvironmentPage } from './pages/environment/EditEnvironmentPage';
import { SessionsPage } from './pages/session/SessionsPage';
import { SessionDetailPage } from './pages/session/SessionDetailPage';
import { ScenariosPage } from './pages/scenario/ScenariosPage';
import { ScenarioDetailPage } from './pages/scenario/ScenarioDetailPage';
import { CreateScenarioPage } from './pages/scenario/CreateScenarioPage';
import { EditScenarioPage } from './pages/scenario/EditScenarioPage';
import { CreateStepPage } from './pages/scenario/CreateStepPage';
import { EditStepPage } from './pages/scenario/EditStepPage';
import { RunsPage } from './pages/run/RunsPage';
import { RunDetailPage } from './pages/run/RunDetailPage';
import { AllRunsPage } from './pages/run/AllRunsPage';
import { CredentialsPage } from './pages/credential/CredentialsPage';
import { CreateCredentialPage } from './pages/credential/CreateCredentialPage';
import { EditCredentialPage } from './pages/credential/EditCredentialPage';
import { CredentialDetailPage } from './pages/credential/CredentialDetailPage';
import { SnippetsPage } from './pages/snippet/SnippetsPage';
import { CreateSnippetPage } from './pages/snippet/CreateSnippetPage';
import { EditSnippetPage } from './pages/snippet/EditSnippetPage';
import { SnippetDetailPage } from './pages/snippet/SnippetDetailPage';
const EnvironmentsPage = lazy(() =>
import('./pages/environment/EnvironmentsPage').then((m) => ({ default: m.EnvironmentsPage })),
);
const EnvironmentDetailPage = lazy(() =>
import('./pages/environment/EnvironmentDetailPage').then((m) => ({ default: m.EnvironmentDetailPage })),
);
const CreateEnvironmentPage = lazy(() =>
import('./pages/environment/CreateEnvironmentPage').then((m) => ({ default: m.CreateEnvironmentPage })),
);
const EditEnvironmentPage = lazy(() =>
import('./pages/environment/EditEnvironmentPage').then((m) => ({ default: m.EditEnvironmentPage })),
);
const SessionsPage = lazy(() =>
import('./pages/session/SessionsPage').then((m) => ({ default: m.SessionsPage })),
);
const SessionDetailPage = lazy(() =>
import('./pages/session/SessionDetailPage').then((m) => ({ default: m.SessionDetailPage })),
);
const ScenariosPage = lazy(() =>
import('./pages/scenario/ScenariosPage').then((m) => ({ default: m.ScenariosPage })),
);
const ScenarioDetailPage = lazy(() =>
import('./pages/scenario/ScenarioDetailPage').then((m) => ({ default: m.ScenarioDetailPage })),
);
const CreateScenarioPage = lazy(() =>
import('./pages/scenario/CreateScenarioPage').then((m) => ({ default: m.CreateScenarioPage })),
);
const EditScenarioPage = lazy(() =>
import('./pages/scenario/EditScenarioPage').then((m) => ({ default: m.EditScenarioPage })),
);
const CreateStepPage = lazy(() =>
import('./pages/scenario/CreateStepPage').then((m) => ({ default: m.CreateStepPage })),
);
const EditStepPage = lazy(() =>
import('./pages/scenario/EditStepPage').then((m) => ({ default: m.EditStepPage })),
);
const RunsPage = lazy(() =>
import('./pages/run/RunsPage').then((m) => ({ default: m.RunsPage })),
);
const RunDetailPage = lazy(() =>
import('./pages/run/RunDetailPage').then((m) => ({ default: m.RunDetailPage })),
);
const AllRunsPage = lazy(() =>
import('./pages/run/AllRunsPage').then((m) => ({ default: m.AllRunsPage })),
);
const CredentialsPage = lazy(() =>
import('./pages/credential/CredentialsPage').then((m) => ({ default: m.CredentialsPage })),
);
const CreateCredentialPage = lazy(() =>
import('./pages/credential/CreateCredentialPage').then((m) => ({ default: m.CreateCredentialPage })),
);
const EditCredentialPage = lazy(() =>
import('./pages/credential/EditCredentialPage').then((m) => ({ default: m.EditCredentialPage })),
);
const CredentialDetailPage = lazy(() =>
import('./pages/credential/CredentialDetailPage').then((m) => ({ default: m.CredentialDetailPage })),
);
const SnippetsPage = lazy(() =>
import('./pages/snippet/SnippetsPage').then((m) => ({ default: m.SnippetsPage })),
);
const CreateSnippetPage = lazy(() =>
import('./pages/snippet/CreateSnippetPage').then((m) => ({ default: m.CreateSnippetPage })),
);
const EditSnippetPage = lazy(() =>
import('./pages/snippet/EditSnippetPage').then((m) => ({ default: m.EditSnippetPage })),
);
const SnippetDetailPage = lazy(() =>
import('./pages/snippet/SnippetDetailPage').then((m) => ({ default: m.SnippetDetailPage })),
);
const NAV: { path: string; labelKey: string; Icon: LucideIcon }[] = [
{ path: '/environments', labelKey: 'nav.environments', Icon: Globe },
@@ -75,6 +128,7 @@ export default function App() {
</SidePanel>
<main className={styles.main}>
<Suspense fallback={<p>Loading</p>}>
<Routes>
<Route index element={<Navigate to="/scenarios" replace />} />
<Route path="/environments" element={<EnvironmentsPage />} />
@@ -102,6 +156,7 @@ export default function App() {
<Route path="/runs" element={<AllRunsPage />} />
<Route path="*" element={<Navigate to="/scenarios" replace />} />
</Routes>
</Suspense>
</main>
</div>
);
@@ -66,7 +66,7 @@ export function CreateEnvironmentPage() {
{error && <p className={styles.error}>{error}</p>}
<form onSubmit={handleSubmit} noValidate>
<Card className={styles.formCard}>
<Card className={styles.formCardFull}>
<div className={styles.formFields}>
<Input
label={t('environments.form_name')}
@@ -92,7 +92,7 @@ export function EditEnvironmentPage() {
{!loading && env && (
<form onSubmit={handleSubmit} noValidate>
<Card className={styles.formCard}>
<Card className={styles.formCardFull}>
<div className={styles.formFields}>
<Input
label={t('environments.form_name')}
@@ -15,6 +15,7 @@ export function EnvironmentDetailPage() {
const [env, setEnv] = useState<Environment | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const dataEntries = Object.entries(env?.data ?? {}).filter(([, v]) => v);
useEffect(() => {
if (!id) return;
@@ -107,14 +108,12 @@ export function EnvironmentDetailPage() {
<div className={styles.stepsSection}>
<h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2>
<Card>
{Object.entries(env.data).filter(([, v]) => v).length === 0 ? (
{dataEntries.length === 0 ? (
<p className={styles.muted}>{t('environments.no_data')}</p>
) : (
<DescriptionList
layout="grid"
items={Object.entries(env.data)
.filter(([, v]) => v)
.map(([key, value]) => ({
items={dataEntries.map(([key, value]) => ({
term: key,
detail: (
<a href={value} target="_blank" rel="noopener noreferrer">
@@ -20,7 +20,7 @@ import styles from '../Page.module.css';
function EnvironmentCard({ env, onDelete }: { env: Environment; onDelete: (id: string) => void }) {
const { t } = useTranslation();
const navigate = useNavigate();
const dataEntries = Object.entries(env.data).filter(([, v]) => v);
const dataEntries = Object.entries(env.data ?? {}).filter(([, v]) => v);
const handleExport = async () => {
const data = await environments.exportEnvironment(env.id);
+4 -3
View File
@@ -79,10 +79,11 @@
/* ── Chevron rotation ────────────────────────────────────── */
.chevron {
transition: transform 220ms ease;
/* Points right (→) by default = collapse direction */
transform: rotate(180deg);
/* Points left (←) by default = collapse direction */
}
.collapsed .chevron {
transform: rotate(180deg);
/* Points left () = expand direction */
transform: rotate(0deg);
/* Points right () = expand direction */
}
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx"],"version":"5.8.3"}
{"root":["./src/App.tsx","./src/declarations.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/index.ts","./src/api/types.ts","./src/hooks/useTheme.ts","./src/i18n/index.ts","./src/lib/hljs.ts","./src/pages/credential/CreateCredentialPage.tsx","./src/pages/credential/CredentialDetailPage.tsx","./src/pages/credential/CredentialsPage.tsx","./src/pages/credential/EditCredentialPage.tsx","./src/pages/environment/CreateEnvironmentPage.tsx","./src/pages/environment/EditEnvironmentPage.tsx","./src/pages/environment/EnvironmentDetailPage.tsx","./src/pages/environment/EnvironmentsPage.tsx","./src/pages/run/AllRunsPage.tsx","./src/pages/run/RunDetailPage.tsx","./src/pages/run/RunsPage.tsx","./src/pages/scenario/CreateScenarioPage.tsx","./src/pages/scenario/CreateStepPage.tsx","./src/pages/scenario/EditScenarioPage.tsx","./src/pages/scenario/EditStepPage.tsx","./src/pages/scenario/ScenarioDetailPage.tsx","./src/pages/scenario/ScenariosPage.tsx","./src/pages/session/SessionDetailPage.tsx","./src/pages/session/SessionsPage.tsx","./src/pages/snippet/CreateSnippetPage.tsx","./src/pages/snippet/EditSnippetPage.tsx","./src/pages/snippet/SnippetDetailPage.tsx","./src/pages/snippet/SnippetsPage.tsx","./src/ui/index.ts","./src/ui/AutoRefreshIndicator/AutoRefreshIndicator.tsx","./src/ui/Badge/Badge.tsx","./src/ui/Breadcrumbs/Breadcrumbs.tsx","./src/ui/Button/Button.tsx","./src/ui/Card/Card.tsx","./src/ui/CodeBlock/CodeBlock.tsx","./src/ui/CodeEditor/CodeEditor.tsx","./src/ui/ContextMenu/ContextMenu.tsx","./src/ui/DescriptionList/DescriptionList.tsx","./src/ui/Input/Input.tsx","./src/ui/Notification/Notification.tsx","./src/ui/Pagination/Pagination.tsx","./src/ui/Search/Search.tsx","./src/ui/Select/Select.tsx","./src/ui/SidePanel/SidePanel.tsx","./src/ui/Table/Table.tsx","./src/ui/Textarea/Textarea.tsx","./src/ui/ThemeSwitcher/ThemeSwitcher.tsx","./src/ui/Timestamp/Timestamp.tsx","./src/ui/UuidBadge/UuidBadge.tsx"],"version":"5.8.3"}
+43
View File
@@ -25,6 +25,49 @@ export default defineConfig({
__APP_VERSION__: JSON.stringify(APP_VERSION),
},
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes('/client/src/') && !id.includes('/node_modules/')) {
return;
}
if (id.includes('/node_modules/lucide-react')) {
return 'icons';
}
if (id.includes('/node_modules/monaco-editor') || id.includes('/node_modules/@monaco-editor')) {
return 'editor';
}
if (id.includes('/node_modules/')) {
return 'vendor';
}
if (id.includes('/src/pages/environment/')) {
return 'environments';
}
if (id.includes('/src/pages/scenario/')) {
return 'scenarios';
}
if (id.includes('/src/pages/run/')) {
return 'runs';
}
if (id.includes('/src/pages/snippet/')) {
return 'snippets';
}
if (id.includes('/src/pages/credential/')) {
return 'credentials';
}
if (id.includes('/src/pages/session/')) {
return 'sessions';
}
if (id.includes('/src/api/')) {
return 'api';
}
},
},
},
},
server: {
proxy: {
'/environments': 'http://localhost:13000',