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:
+105
-50
@@ -1,3 +1,4 @@
|
|||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import { NavLink, Navigate, Route, Routes } from 'react-router-dom';
|
import { NavLink, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Globe, Monitor, ClipboardList, Activity, KeyRound, Braces } from 'lucide-react';
|
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 styles from './App.module.css';
|
||||||
import { SidePanel, ThemeSwitcher } from './ui';
|
import { SidePanel, ThemeSwitcher } from './ui';
|
||||||
import liqaLogo from './assets/liqa.svg';
|
import liqaLogo from './assets/liqa.svg';
|
||||||
import { EnvironmentsPage } from './pages/environment/EnvironmentsPage';
|
|
||||||
import { EnvironmentDetailPage } from './pages/environment/EnvironmentDetailPage';
|
const EnvironmentsPage = lazy(() =>
|
||||||
import { CreateEnvironmentPage } from './pages/environment/CreateEnvironmentPage';
|
import('./pages/environment/EnvironmentsPage').then((m) => ({ default: m.EnvironmentsPage })),
|
||||||
import { EditEnvironmentPage } from './pages/environment/EditEnvironmentPage';
|
);
|
||||||
import { SessionsPage } from './pages/session/SessionsPage';
|
const EnvironmentDetailPage = lazy(() =>
|
||||||
import { SessionDetailPage } from './pages/session/SessionDetailPage';
|
import('./pages/environment/EnvironmentDetailPage').then((m) => ({ default: m.EnvironmentDetailPage })),
|
||||||
import { ScenariosPage } from './pages/scenario/ScenariosPage';
|
);
|
||||||
import { ScenarioDetailPage } from './pages/scenario/ScenarioDetailPage';
|
const CreateEnvironmentPage = lazy(() =>
|
||||||
import { CreateScenarioPage } from './pages/scenario/CreateScenarioPage';
|
import('./pages/environment/CreateEnvironmentPage').then((m) => ({ default: m.CreateEnvironmentPage })),
|
||||||
import { EditScenarioPage } from './pages/scenario/EditScenarioPage';
|
);
|
||||||
import { CreateStepPage } from './pages/scenario/CreateStepPage';
|
const EditEnvironmentPage = lazy(() =>
|
||||||
import { EditStepPage } from './pages/scenario/EditStepPage';
|
import('./pages/environment/EditEnvironmentPage').then((m) => ({ default: m.EditEnvironmentPage })),
|
||||||
import { RunsPage } from './pages/run/RunsPage';
|
);
|
||||||
import { RunDetailPage } from './pages/run/RunDetailPage';
|
|
||||||
import { AllRunsPage } from './pages/run/AllRunsPage';
|
const SessionsPage = lazy(() =>
|
||||||
import { CredentialsPage } from './pages/credential/CredentialsPage';
|
import('./pages/session/SessionsPage').then((m) => ({ default: m.SessionsPage })),
|
||||||
import { CreateCredentialPage } from './pages/credential/CreateCredentialPage';
|
);
|
||||||
import { EditCredentialPage } from './pages/credential/EditCredentialPage';
|
const SessionDetailPage = lazy(() =>
|
||||||
import { CredentialDetailPage } from './pages/credential/CredentialDetailPage';
|
import('./pages/session/SessionDetailPage').then((m) => ({ default: m.SessionDetailPage })),
|
||||||
import { SnippetsPage } from './pages/snippet/SnippetsPage';
|
);
|
||||||
import { CreateSnippetPage } from './pages/snippet/CreateSnippetPage';
|
|
||||||
import { EditSnippetPage } from './pages/snippet/EditSnippetPage';
|
const ScenariosPage = lazy(() =>
|
||||||
import { SnippetDetailPage } from './pages/snippet/SnippetDetailPage';
|
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 }[] = [
|
const NAV: { path: string; labelKey: string; Icon: LucideIcon }[] = [
|
||||||
{ path: '/environments', labelKey: 'nav.environments', Icon: Globe },
|
{ path: '/environments', labelKey: 'nav.environments', Icon: Globe },
|
||||||
@@ -75,33 +128,35 @@ export default function App() {
|
|||||||
</SidePanel>
|
</SidePanel>
|
||||||
|
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<Routes>
|
<Suspense fallback={<p>Loading…</p>}>
|
||||||
<Route index element={<Navigate to="/scenarios" replace />} />
|
<Routes>
|
||||||
<Route path="/environments" element={<EnvironmentsPage />} />
|
<Route index element={<Navigate to="/scenarios" replace />} />
|
||||||
<Route path="/environments/new" element={<CreateEnvironmentPage />} />
|
<Route path="/environments" element={<EnvironmentsPage />} />
|
||||||
<Route path="/environments/:id/edit" element={<EditEnvironmentPage />} />
|
<Route path="/environments/new" element={<CreateEnvironmentPage />} />
|
||||||
<Route path="/environments/:id" element={<EnvironmentDetailPage />} />
|
<Route path="/environments/:id/edit" element={<EditEnvironmentPage />} />
|
||||||
<Route path="/sessions" element={<SessionsPage />} />
|
<Route path="/environments/:id" element={<EnvironmentDetailPage />} />
|
||||||
<Route path="/sessions/:id" element={<SessionDetailPage />} />
|
<Route path="/sessions" element={<SessionsPage />} />
|
||||||
<Route path="/scenarios" element={<ScenariosPage />} />
|
<Route path="/sessions/:id" element={<SessionDetailPage />} />
|
||||||
<Route path="/scenarios/new" element={<CreateScenarioPage />} />
|
<Route path="/scenarios" element={<ScenariosPage />} />
|
||||||
<Route path="/scenarios/:id/edit" element={<EditScenarioPage />} />
|
<Route path="/scenarios/new" element={<CreateScenarioPage />} />
|
||||||
<Route path="/scenarios/:id/steps/new" element={<CreateStepPage />} />
|
<Route path="/scenarios/:id/edit" element={<EditScenarioPage />} />
|
||||||
<Route path="/scenarios/:id/steps/:stepId/edit" element={<EditStepPage />} />
|
<Route path="/scenarios/:id/steps/new" element={<CreateStepPage />} />
|
||||||
<Route path="/scenarios/:id/runs" element={<RunsPage />} />
|
<Route path="/scenarios/:id/steps/:stepId/edit" element={<EditStepPage />} />
|
||||||
<Route path="/scenarios/:id/runs/:runId" element={<RunDetailPage />} />
|
<Route path="/scenarios/:id/runs" element={<RunsPage />} />
|
||||||
<Route path="/scenarios/:id" element={<ScenarioDetailPage />} />
|
<Route path="/scenarios/:id/runs/:runId" element={<RunDetailPage />} />
|
||||||
<Route path="/credentials" element={<CredentialsPage />} />
|
<Route path="/scenarios/:id" element={<ScenarioDetailPage />} />
|
||||||
<Route path="/credentials/new" element={<CreateCredentialPage />} />
|
<Route path="/credentials" element={<CredentialsPage />} />
|
||||||
<Route path="/credentials/:id/edit" element={<EditCredentialPage />} />
|
<Route path="/credentials/new" element={<CreateCredentialPage />} />
|
||||||
<Route path="/credentials/:id" element={<CredentialDetailPage />} />
|
<Route path="/credentials/:id/edit" element={<EditCredentialPage />} />
|
||||||
<Route path="/snippets" element={<SnippetsPage />} />
|
<Route path="/credentials/:id" element={<CredentialDetailPage />} />
|
||||||
<Route path="/snippets/new" element={<CreateSnippetPage />} />
|
<Route path="/snippets" element={<SnippetsPage />} />
|
||||||
<Route path="/snippets/:id/edit" element={<EditSnippetPage />} />
|
<Route path="/snippets/new" element={<CreateSnippetPage />} />
|
||||||
<Route path="/snippets/:id" element={<SnippetDetailPage />} />
|
<Route path="/snippets/:id/edit" element={<EditSnippetPage />} />
|
||||||
<Route path="/runs" element={<AllRunsPage />} />
|
<Route path="/snippets/:id" element={<SnippetDetailPage />} />
|
||||||
<Route path="*" element={<Navigate to="/scenarios" replace />} />
|
<Route path="/runs" element={<AllRunsPage />} />
|
||||||
</Routes>
|
<Route path="*" element={<Navigate to="/scenarios" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function CreateEnvironmentPage() {
|
|||||||
{error && <p className={styles.error}>{error}</p>}
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} noValidate>
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
<Card className={styles.formCard}>
|
<Card className={styles.formCardFull}>
|
||||||
<div className={styles.formFields}>
|
<div className={styles.formFields}>
|
||||||
<Input
|
<Input
|
||||||
label={t('environments.form_name')}
|
label={t('environments.form_name')}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export function EditEnvironmentPage() {
|
|||||||
|
|
||||||
{!loading && env && (
|
{!loading && env && (
|
||||||
<form onSubmit={handleSubmit} noValidate>
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
<Card className={styles.formCard}>
|
<Card className={styles.formCardFull}>
|
||||||
<div className={styles.formFields}>
|
<div className={styles.formFields}>
|
||||||
<Input
|
<Input
|
||||||
label={t('environments.form_name')}
|
label={t('environments.form_name')}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function EnvironmentDetailPage() {
|
|||||||
const [env, setEnv] = useState<Environment | null>(null);
|
const [env, setEnv] = useState<Environment | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const dataEntries = Object.entries(env?.data ?? {}).filter(([, v]) => v);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -107,14 +108,12 @@ export function EnvironmentDetailPage() {
|
|||||||
<div className={styles.stepsSection}>
|
<div className={styles.stepsSection}>
|
||||||
<h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2>
|
<h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2>
|
||||||
<Card>
|
<Card>
|
||||||
{Object.entries(env.data).filter(([, v]) => v).length === 0 ? (
|
{dataEntries.length === 0 ? (
|
||||||
<p className={styles.muted}>{t('environments.no_data')}</p>
|
<p className={styles.muted}>{t('environments.no_data')}</p>
|
||||||
) : (
|
) : (
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
layout="grid"
|
layout="grid"
|
||||||
items={Object.entries(env.data)
|
items={dataEntries.map(([key, value]) => ({
|
||||||
.filter(([, v]) => v)
|
|
||||||
.map(([key, value]) => ({
|
|
||||||
term: key,
|
term: key,
|
||||||
detail: (
|
detail: (
|
||||||
<a href={value} target="_blank" rel="noopener noreferrer">
|
<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 }) {
|
function EnvironmentCard({ env, onDelete }: { env: Environment; onDelete: (id: string) => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
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 handleExport = async () => {
|
||||||
const data = await environments.exportEnvironment(env.id);
|
const data = await environments.exportEnvironment(env.id);
|
||||||
|
|||||||
@@ -79,10 +79,11 @@
|
|||||||
/* ── Chevron rotation ────────────────────────────────────── */
|
/* ── Chevron rotation ────────────────────────────────────── */
|
||||||
.chevron {
|
.chevron {
|
||||||
transition: transform 220ms ease;
|
transition: transform 220ms ease;
|
||||||
/* Points right (→) by default = collapse direction */
|
transform: rotate(180deg);
|
||||||
|
/* Points left (←) by default = collapse direction */
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed .chevron {
|
.collapsed .chevron {
|
||||||
transform: rotate(180deg);
|
transform: rotate(0deg);
|
||||||
/* Points left (←) = expand direction */
|
/* Points right (→) = expand direction */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"}
|
||||||
@@ -25,6 +25,49 @@ export default defineConfig({
|
|||||||
__APP_VERSION__: JSON.stringify(APP_VERSION),
|
__APP_VERSION__: JSON.stringify(APP_VERSION),
|
||||||
},
|
},
|
||||||
plugins: [react()],
|
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: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/environments': 'http://localhost:13000',
|
'/environments': 'http://localhost:13000',
|
||||||
|
|||||||
Reference in New Issue
Block a user