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
+105 -50
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,33 +128,35 @@ export default function App() {
</SidePanel>
<main className={styles.main}>
<Routes>
<Route index element={<Navigate to="/scenarios" replace />} />
<Route path="/environments" element={<EnvironmentsPage />} />
<Route path="/environments/new" element={<CreateEnvironmentPage />} />
<Route path="/environments/:id/edit" element={<EditEnvironmentPage />} />
<Route path="/environments/:id" element={<EnvironmentDetailPage />} />
<Route path="/sessions" element={<SessionsPage />} />
<Route path="/sessions/:id" element={<SessionDetailPage />} />
<Route path="/scenarios" element={<ScenariosPage />} />
<Route path="/scenarios/new" element={<CreateScenarioPage />} />
<Route path="/scenarios/:id/edit" element={<EditScenarioPage />} />
<Route path="/scenarios/:id/steps/new" element={<CreateStepPage />} />
<Route path="/scenarios/:id/steps/:stepId/edit" element={<EditStepPage />} />
<Route path="/scenarios/:id/runs" element={<RunsPage />} />
<Route path="/scenarios/:id/runs/:runId" element={<RunDetailPage />} />
<Route path="/scenarios/:id" element={<ScenarioDetailPage />} />
<Route path="/credentials" element={<CredentialsPage />} />
<Route path="/credentials/new" element={<CreateCredentialPage />} />
<Route path="/credentials/:id/edit" element={<EditCredentialPage />} />
<Route path="/credentials/:id" element={<CredentialDetailPage />} />
<Route path="/snippets" element={<SnippetsPage />} />
<Route path="/snippets/new" element={<CreateSnippetPage />} />
<Route path="/snippets/:id/edit" element={<EditSnippetPage />} />
<Route path="/snippets/:id" element={<SnippetDetailPage />} />
<Route path="/runs" element={<AllRunsPage />} />
<Route path="*" element={<Navigate to="/scenarios" replace />} />
</Routes>
<Suspense fallback={<p>Loading</p>}>
<Routes>
<Route index element={<Navigate to="/scenarios" replace />} />
<Route path="/environments" element={<EnvironmentsPage />} />
<Route path="/environments/new" element={<CreateEnvironmentPage />} />
<Route path="/environments/:id/edit" element={<EditEnvironmentPage />} />
<Route path="/environments/:id" element={<EnvironmentDetailPage />} />
<Route path="/sessions" element={<SessionsPage />} />
<Route path="/sessions/:id" element={<SessionDetailPage />} />
<Route path="/scenarios" element={<ScenariosPage />} />
<Route path="/scenarios/new" element={<CreateScenarioPage />} />
<Route path="/scenarios/:id/edit" element={<EditScenarioPage />} />
<Route path="/scenarios/:id/steps/new" element={<CreateStepPage />} />
<Route path="/scenarios/:id/steps/:stepId/edit" element={<EditStepPage />} />
<Route path="/scenarios/:id/runs" element={<RunsPage />} />
<Route path="/scenarios/:id/runs/:runId" element={<RunDetailPage />} />
<Route path="/scenarios/:id" element={<ScenarioDetailPage />} />
<Route path="/credentials" element={<CredentialsPage />} />
<Route path="/credentials/new" element={<CreateCredentialPage />} />
<Route path="/credentials/:id/edit" element={<EditCredentialPage />} />
<Route path="/credentials/:id" element={<CredentialDetailPage />} />
<Route path="/snippets" element={<SnippetsPage />} />
<Route path="/snippets/new" element={<CreateSnippetPage />} />
<Route path="/snippets/:id/edit" element={<EditSnippetPage />} />
<Route path="/snippets/:id" element={<SnippetDetailPage />} />
<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 */
}