feat(monaco,lucide): lazy load large resource
This commit is contained in:
parent
ee27e4c676
commit
9f0ff8f6c3
13 changed files with 194 additions and 51 deletions
|
|
@ -53,6 +53,8 @@ pnpm test # 서버 테스트 실행
|
|||
pnpm run bench # 벤치마크 실행
|
||||
```
|
||||
|
||||
개발 시 관리자 UI는 `http://localhost:3002/dashboard` 로 접속하고, 운영에서는 `http://<host>:3000/dashboard` 를 사용한다.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|
|
|
|||
|
|
@ -1,16 +1,25 @@
|
|||
import { Router, Route } from '@solidjs/router';
|
||||
import { Show } from 'solid-js';
|
||||
import { Dashboard } from './routes/Dashboard';
|
||||
import { Users } from './routes/Users';
|
||||
import { Backends } from './routes/Backends';
|
||||
import { Permissions } from './routes/Permissions';
|
||||
import { Analytics } from './routes/Analytics';
|
||||
import { DetailLogs } from './routes/DetailLogs';
|
||||
import { Scripts } from './routes/Scripts';
|
||||
import { lazy, Show, Suspense } from 'solid-js';
|
||||
import { AuthProvider, useAuth } from './auth';
|
||||
import { LoginGate } from './components/LoginGate';
|
||||
import { Panel } from './ui';
|
||||
|
||||
const Dashboard = lazy(() => import('./routes/Dashboard').then((module) => ({ default: module.Dashboard })));
|
||||
const Users = lazy(() => import('./routes/Users').then((module) => ({ default: module.Users })));
|
||||
const Backends = lazy(() => import('./routes/Backends').then((module) => ({ default: module.Backends })));
|
||||
const Permissions = lazy(() => import('./routes/Permissions').then((module) => ({ default: module.Permissions })));
|
||||
const Analytics = lazy(() => import('./routes/Analytics').then((module) => ({ default: module.Analytics })));
|
||||
const DetailLogs = lazy(() => import('./routes/DetailLogs').then((module) => ({ default: module.DetailLogs })));
|
||||
const Scripts = lazy(() => import('./routes/Scripts').then((module) => ({ default: module.Scripts })));
|
||||
|
||||
function RouteLoadingFallback() {
|
||||
return (
|
||||
<div class="auth-screen">
|
||||
<Panel class="auth-screen__panel" title="Loading Admin Page" description="Preparing the selected dashboard view." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticatedApp() {
|
||||
const auth = useAuth();
|
||||
|
||||
|
|
@ -24,6 +33,7 @@ function AuthenticatedApp() {
|
|||
}
|
||||
>
|
||||
<Show when={auth.session()?.authenticated} fallback={<LoginGate />}>
|
||||
<Suspense fallback={<RouteLoadingFallback />}>
|
||||
<Router base="/dashboard">
|
||||
<Route path="/" component={Dashboard} />
|
||||
<Route path="/users" component={Users} />
|
||||
|
|
@ -33,6 +43,7 @@ function AuthenticatedApp() {
|
|||
<Route path="/detail-logs" component={DetailLogs} />
|
||||
<Route path="/scripts" component={Scripts} />
|
||||
</Router>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</Show>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { MonacoEditor } from 'solid-monaco';
|
||||
import { createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import { Dynamic } from 'solid-js/web';
|
||||
import { createSignal, lazy, onCleanup, onMount, Suspense } from 'solid-js';
|
||||
|
||||
const THEME_STORAGE_KEY = 'kyush-theme';
|
||||
const MonacoEditor = lazy(() => import('solid-monaco').then((module) => ({ default: module.MonacoEditor })));
|
||||
|
||||
interface ScriptEditorProps {
|
||||
value: string;
|
||||
|
|
@ -85,11 +86,24 @@ export async function onResponse(ctx) {
|
|||
|
||||
return (
|
||||
<div class="script-editor">
|
||||
<MonacoEditor
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="script-editor__loading" role="status" aria-live="polite">
|
||||
<div class="script-editor__skeleton script-editor__skeleton--toolbar" />
|
||||
<div class="script-editor__skeleton script-editor__skeleton--line" />
|
||||
<div class="script-editor__skeleton script-editor__skeleton--line script-editor__skeleton--short" />
|
||||
<div class="script-editor__skeleton script-editor__skeleton--line" />
|
||||
<div class="script-editor__skeleton script-editor__skeleton--line script-editor__skeleton--medium" />
|
||||
<p class="script-editor__loading-copy">Loading editor runtime...</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Dynamic
|
||||
component={MonacoEditor}
|
||||
language="typescript"
|
||||
value={props.value || defaultCode}
|
||||
path={props.path}
|
||||
onChange={(value) => props.onChange(value)}
|
||||
onChange={(value: string) => props.onChange(value)}
|
||||
theme={editorTheme()}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
|
|
@ -101,6 +115,7 @@ export async function onResponse(ctx) {
|
|||
padding: { top: 16, bottom: 16 },
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { createResource, createSignal, Show, type Component } from 'solid-js';
|
||||
import { Pencil, Plus, Trash2 } from 'lucide-solid';
|
||||
import Pencil from 'lucide-solid/icons/pencil';
|
||||
import Plus from 'lucide-solid/icons/plus';
|
||||
import Trash2 from 'lucide-solid/icons/trash-2';
|
||||
import { api } from '../api/client';
|
||||
import { Layout } from '../components/Layout';
|
||||
import type { Backend } from '../types';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createMemo, createResource, createSignal, Show, type Component } from 'solid-js';
|
||||
import { RefreshCcw } from 'lucide-solid';
|
||||
import RefreshCcw from 'lucide-solid/icons/refresh-ccw';
|
||||
import { api } from '../api/client';
|
||||
import { Layout } from '../components/Layout';
|
||||
import type { RequestLog } from '../types';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { createMemo, createResource, createSignal, Show, type Component } from 'solid-js';
|
||||
import { Plus, ShieldMinus } from 'lucide-solid';
|
||||
import Plus from 'lucide-solid/icons/plus';
|
||||
import ShieldMinus from 'lucide-solid/icons/shield-minus';
|
||||
import { api } from '../api/client';
|
||||
import { Layout } from '../components/Layout';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import { createMemo, createResource, createSignal, Show, type Component } from 'solid-js';
|
||||
import { createMemo, createResource, createSignal, lazy, Show, Suspense, type Component } from 'solid-js';
|
||||
import { api } from '../api/client';
|
||||
import { Layout } from '../components/Layout';
|
||||
import { ScriptEditor } from '../components/ScriptEditor';
|
||||
import { Play, Plus, Power, PowerOff, RefreshCw, RotateCcw, Save, Trash2 } from 'lucide-solid';
|
||||
import Play from 'lucide-solid/icons/play';
|
||||
import Plus from 'lucide-solid/icons/plus';
|
||||
import Power from 'lucide-solid/icons/power';
|
||||
import PowerOff from 'lucide-solid/icons/power-off';
|
||||
import RefreshCw from 'lucide-solid/icons/refresh-cw';
|
||||
import RotateCcw from 'lucide-solid/icons/rotate-ccw';
|
||||
import Save from 'lucide-solid/icons/save';
|
||||
import Trash2 from 'lucide-solid/icons/trash-2';
|
||||
import type { ScriptType, UserScript } from '../types';
|
||||
import {
|
||||
Alert,
|
||||
|
|
@ -25,6 +31,7 @@ import {
|
|||
} from '../ui';
|
||||
|
||||
type NoticeTone = 'success' | 'warning' | 'danger' | 'info';
|
||||
const ScriptEditor = lazy(() => import('../components/ScriptEditor').then((module) => ({ default: module.ScriptEditor })));
|
||||
|
||||
interface ScriptFormState {
|
||||
id?: number;
|
||||
|
|
@ -471,11 +478,13 @@ export const Scripts: Component = () => {
|
|||
<Tabs.Trigger value="test">Test</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="editor">
|
||||
<Suspense fallback={<Panel title="Loading Editor" description="Preparing the Monaco runtime for this script." class="script-editor__fallback-panel" />}>
|
||||
<ScriptEditor
|
||||
value={form().script_code}
|
||||
path={form().id ? `inmemory://model/scripts/${form().id}.ts` : 'inmemory://model/scripts/draft.ts'}
|
||||
onChange={(value) => setForm((current) => ({ ...current, script_code: value }))}
|
||||
/>
|
||||
</Suspense>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="test">
|
||||
<div class="ui-stack">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { createMemo, createResource, createSignal, For, Show, type Component } from 'solid-js';
|
||||
import { Copy, KeyRound, MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-solid';
|
||||
import Copy from 'lucide-solid/icons/copy';
|
||||
import Ellipsis from 'lucide-solid/icons/ellipsis';
|
||||
import KeyRound from 'lucide-solid/icons/key-round';
|
||||
import Pencil from 'lucide-solid/icons/pencil';
|
||||
import Plus from 'lucide-solid/icons/plus';
|
||||
import Trash2 from 'lucide-solid/icons/trash-2';
|
||||
import { api } from '../api/client';
|
||||
import { Layout } from '../components/Layout';
|
||||
import type { User } from '../types';
|
||||
|
|
@ -262,7 +267,7 @@ export const Users: Component = () => {
|
|||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger as={Button} class="ui-button--icon" aria-label="More actions">
|
||||
<span class="ui-button__icon" aria-hidden="true">
|
||||
<MoreHorizontal />
|
||||
<Ellipsis />
|
||||
</span>
|
||||
<span class="ui-button__label">More</span>
|
||||
</DropdownMenu.Trigger>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
import { A, useLocation } from '@solidjs/router';
|
||||
import { ChartColumn, FileCode, LayoutDashboard, LogOut, Logs, Moon, Server, ShieldCheck, Sun, Users } from 'lucide-solid';
|
||||
import ChartColumn from 'lucide-solid/icons/chart-column';
|
||||
import FileCode from 'lucide-solid/icons/file-code';
|
||||
import LayoutDashboard from 'lucide-solid/icons/layout-dashboard';
|
||||
import LogOut from 'lucide-solid/icons/log-out';
|
||||
import Logs from 'lucide-solid/icons/logs';
|
||||
import Moon from 'lucide-solid/icons/moon';
|
||||
import Server from 'lucide-solid/icons/server';
|
||||
import ShieldCheck from 'lucide-solid/icons/shield-check';
|
||||
import Sun from 'lucide-solid/icons/sun';
|
||||
import Users from 'lucide-solid/icons/users';
|
||||
import { For, createMemo, createSignal, onCleanup, onMount, type JSX, type ParentComponent } from 'solid-js';
|
||||
import SnakegroundBg from '../../components/SnakegroundBg';
|
||||
import { useAuth } from '../../auth';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
.script-editor {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
min-height: 600px;
|
||||
overflow: hidden;
|
||||
|
|
@ -21,6 +23,56 @@
|
|||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.script-editor__loading {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
justify-content: center;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.script-editor__loading-copy {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-1);
|
||||
}
|
||||
|
||||
.script-editor__skeleton {
|
||||
border-radius: var(--radius-2);
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
color-mix(in srgb, var(--color-bg-overlay) 92%, transparent) 0%,
|
||||
color-mix(in srgb, var(--color-bg-surface) 92%, transparent) 50%,
|
||||
color-mix(in srgb, var(--color-bg-overlay) 92%, transparent) 100%
|
||||
);
|
||||
background-size: 220% 100%;
|
||||
animation: script-editor-shimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.script-editor__skeleton--toolbar {
|
||||
height: 40px;
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
.script-editor__skeleton--line {
|
||||
height: 18px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.script-editor__skeleton--medium {
|
||||
width: 72%;
|
||||
}
|
||||
|
||||
.script-editor__skeleton--short {
|
||||
width: 54%;
|
||||
}
|
||||
|
||||
.script-editor__fallback-panel {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.script-target {
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
@ -68,6 +120,10 @@
|
|||
.script-editor > * {
|
||||
height: 520px !important;
|
||||
}
|
||||
|
||||
.script-editor__fallback-panel {
|
||||
min-height: 520px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
|
@ -78,4 +134,18 @@
|
|||
.script-editor > * {
|
||||
height: 420px !important;
|
||||
}
|
||||
|
||||
.script-editor__fallback-panel {
|
||||
min-height: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes script-editor-shimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,24 @@ import solidPlugin from 'vite-plugin-solid';
|
|||
|
||||
export default defineConfig({
|
||||
base: '/dashboard/',
|
||||
plugins: [solidPlugin()],
|
||||
plugins: [
|
||||
solidPlugin(),
|
||||
{
|
||||
name: 'dashboard-trailing-slash-redirect',
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
if (req.url === '/dashboard') {
|
||||
res.statusCode = 302;
|
||||
res.setHeader('Location', '/dashboard/');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
server: {
|
||||
port: 3002,
|
||||
proxy: {
|
||||
|
|
|
|||
|
|
@ -48,11 +48,13 @@ client/src/
|
|||
## Dev And Production
|
||||
|
||||
- 개발 서버 포트: `3002`
|
||||
- 개발 브라우저 진입 경로: `http://localhost:3002/dashboard`
|
||||
- 개발 API 프록시: `/admin` -> `http://localhost:3000`
|
||||
- 운영에서는 Express 서버가 빌드된 `client/dist`를 직접 서빙한다
|
||||
- 운영 브라우저 진입 경로: `http://<host>:3000/dashboard`
|
||||
|
||||
SPA는 `/dashboard`를 라우터 base로 사용하고, 관리자 API는 계속 `/admin/**`로 호출한다.
|
||||
관리자 라우트는 route-level lazy loading을 사용하고, `Scripts` 화면의 Monaco editor는 editor 영역에서만 지연 로딩된다.
|
||||
|
||||
## Admin Auth Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ async function main() {
|
|||
log(colors.green, '===============================================');
|
||||
log(colors.green, 'Development servers started:');
|
||||
log(colors.green, ` - Express API Server: http://localhost:3000`);
|
||||
log(colors.green, ` - Vite Admin Dashboard: http://localhost:3001`);
|
||||
log(colors.green, ` - Vite Admin Dashboard: http://localhost:3002/dashboard`);
|
||||
log(colors.green, '===============================================');
|
||||
log(colors.yellow, 'Press Ctrl+C to stop all servers');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue