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 # 벤치마크 실행
|
pnpm run bench # 벤치마크 실행
|
||||||
```
|
```
|
||||||
|
|
||||||
|
개발 시 관리자 UI는 `http://localhost:3002/dashboard` 로 접속하고, 운영에서는 `http://<host>:3000/dashboard` 를 사용한다.
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
import { Router, Route } from '@solidjs/router';
|
import { Router, Route } from '@solidjs/router';
|
||||||
import { Show } from 'solid-js';
|
import { lazy, Show, Suspense } 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 { AuthProvider, useAuth } from './auth';
|
import { AuthProvider, useAuth } from './auth';
|
||||||
import { LoginGate } from './components/LoginGate';
|
import { LoginGate } from './components/LoginGate';
|
||||||
import { Panel } from './ui';
|
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() {
|
function AuthenticatedApp() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
|
||||||
|
|
@ -24,6 +33,7 @@ function AuthenticatedApp() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show when={auth.session()?.authenticated} fallback={<LoginGate />}>
|
<Show when={auth.session()?.authenticated} fallback={<LoginGate />}>
|
||||||
|
<Suspense fallback={<RouteLoadingFallback />}>
|
||||||
<Router base="/dashboard">
|
<Router base="/dashboard">
|
||||||
<Route path="/" component={Dashboard} />
|
<Route path="/" component={Dashboard} />
|
||||||
<Route path="/users" component={Users} />
|
<Route path="/users" component={Users} />
|
||||||
|
|
@ -33,6 +43,7 @@ function AuthenticatedApp() {
|
||||||
<Route path="/detail-logs" component={DetailLogs} />
|
<Route path="/detail-logs" component={DetailLogs} />
|
||||||
<Route path="/scripts" component={Scripts} />
|
<Route path="/scripts" component={Scripts} />
|
||||||
</Router>
|
</Router>
|
||||||
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { MonacoEditor } from 'solid-monaco';
|
import { Dynamic } from 'solid-js/web';
|
||||||
import { createSignal, onCleanup, onMount } from 'solid-js';
|
import { createSignal, lazy, onCleanup, onMount, Suspense } from 'solid-js';
|
||||||
|
|
||||||
const THEME_STORAGE_KEY = 'kyush-theme';
|
const THEME_STORAGE_KEY = 'kyush-theme';
|
||||||
|
const MonacoEditor = lazy(() => import('solid-monaco').then((module) => ({ default: module.MonacoEditor })));
|
||||||
|
|
||||||
interface ScriptEditorProps {
|
interface ScriptEditorProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -85,11 +86,24 @@ export async function onResponse(ctx) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="script-editor">
|
<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"
|
language="typescript"
|
||||||
value={props.value || defaultCode}
|
value={props.value || defaultCode}
|
||||||
path={props.path}
|
path={props.path}
|
||||||
onChange={(value) => props.onChange(value)}
|
onChange={(value: string) => props.onChange(value)}
|
||||||
theme={editorTheme()}
|
theme={editorTheme()}
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
|
|
@ -101,6 +115,7 @@ export async function onResponse(ctx) {
|
||||||
padding: { top: 16, bottom: 16 },
|
padding: { top: 16, bottom: 16 },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { createResource, createSignal, Show, type Component } from 'solid-js';
|
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 { api } from '../api/client';
|
||||||
import { Layout } from '../components/Layout';
|
import { Layout } from '../components/Layout';
|
||||||
import type { Backend } from '../types';
|
import type { Backend } from '../types';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createMemo, createResource, createSignal, Show, type Component } from 'solid-js';
|
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 { api } from '../api/client';
|
||||||
import { Layout } from '../components/Layout';
|
import { Layout } from '../components/Layout';
|
||||||
import type { RequestLog } from '../types';
|
import type { RequestLog } from '../types';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { createMemo, createResource, createSignal, Show, type Component } from 'solid-js';
|
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 { api } from '../api/client';
|
||||||
import { Layout } from '../components/Layout';
|
import { Layout } from '../components/Layout';
|
||||||
import {
|
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 { api } from '../api/client';
|
||||||
import { Layout } from '../components/Layout';
|
import { Layout } from '../components/Layout';
|
||||||
import { ScriptEditor } from '../components/ScriptEditor';
|
import Play from 'lucide-solid/icons/play';
|
||||||
import { Play, Plus, Power, PowerOff, RefreshCw, RotateCcw, Save, Trash2 } from 'lucide-solid';
|
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 type { ScriptType, UserScript } from '../types';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
|
@ -25,6 +31,7 @@ import {
|
||||||
} from '../ui';
|
} from '../ui';
|
||||||
|
|
||||||
type NoticeTone = 'success' | 'warning' | 'danger' | 'info';
|
type NoticeTone = 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
const ScriptEditor = lazy(() => import('../components/ScriptEditor').then((module) => ({ default: module.ScriptEditor })));
|
||||||
|
|
||||||
interface ScriptFormState {
|
interface ScriptFormState {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
@ -471,11 +478,13 @@ export const Scripts: Component = () => {
|
||||||
<Tabs.Trigger value="test">Test</Tabs.Trigger>
|
<Tabs.Trigger value="test">Test</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Content value="editor">
|
<Tabs.Content value="editor">
|
||||||
|
<Suspense fallback={<Panel title="Loading Editor" description="Preparing the Monaco runtime for this script." class="script-editor__fallback-panel" />}>
|
||||||
<ScriptEditor
|
<ScriptEditor
|
||||||
value={form().script_code}
|
value={form().script_code}
|
||||||
path={form().id ? `inmemory://model/scripts/${form().id}.ts` : 'inmemory://model/scripts/draft.ts'}
|
path={form().id ? `inmemory://model/scripts/${form().id}.ts` : 'inmemory://model/scripts/draft.ts'}
|
||||||
onChange={(value) => setForm((current) => ({ ...current, script_code: value }))}
|
onChange={(value) => setForm((current) => ({ ...current, script_code: value }))}
|
||||||
/>
|
/>
|
||||||
|
</Suspense>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="test">
|
<Tabs.Content value="test">
|
||||||
<div class="ui-stack">
|
<div class="ui-stack">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { createMemo, createResource, createSignal, For, Show, type Component } from 'solid-js';
|
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 { api } from '../api/client';
|
||||||
import { Layout } from '../components/Layout';
|
import { Layout } from '../components/Layout';
|
||||||
import type { User } from '../types';
|
import type { User } from '../types';
|
||||||
|
|
@ -262,7 +267,7 @@ export const Users: Component = () => {
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger as={Button} class="ui-button--icon" aria-label="More actions">
|
<DropdownMenu.Trigger as={Button} class="ui-button--icon" aria-label="More actions">
|
||||||
<span class="ui-button__icon" aria-hidden="true">
|
<span class="ui-button__icon" aria-hidden="true">
|
||||||
<MoreHorizontal />
|
<Ellipsis />
|
||||||
</span>
|
</span>
|
||||||
<span class="ui-button__label">More</span>
|
<span class="ui-button__label">More</span>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
import { A, useLocation } from '@solidjs/router';
|
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 { For, createMemo, createSignal, onCleanup, onMount, type JSX, type ParentComponent } from 'solid-js';
|
||||||
import SnakegroundBg from '../../components/SnakegroundBg';
|
import SnakegroundBg from '../../components/SnakegroundBg';
|
||||||
import { useAuth } from '../../auth';
|
import { useAuth } from '../../auth';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
.script-editor {
|
.script-editor {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -21,6 +23,56 @@
|
||||||
max-width: 100% !important;
|
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 {
|
.script-target {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +120,10 @@
|
||||||
.script-editor > * {
|
.script-editor > * {
|
||||||
height: 520px !important;
|
height: 520px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.script-editor__fallback-panel {
|
||||||
|
min-height: 520px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
|
@ -78,4 +134,18 @@
|
||||||
.script-editor > * {
|
.script-editor > * {
|
||||||
height: 420px !important;
|
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({
|
export default defineConfig({
|
||||||
base: '/dashboard/',
|
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: {
|
server: {
|
||||||
port: 3002,
|
port: 3002,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,13 @@ client/src/
|
||||||
## Dev And Production
|
## Dev And Production
|
||||||
|
|
||||||
- 개발 서버 포트: `3002`
|
- 개발 서버 포트: `3002`
|
||||||
|
- 개발 브라우저 진입 경로: `http://localhost:3002/dashboard`
|
||||||
- 개발 API 프록시: `/admin` -> `http://localhost:3000`
|
- 개발 API 프록시: `/admin` -> `http://localhost:3000`
|
||||||
- 운영에서는 Express 서버가 빌드된 `client/dist`를 직접 서빙한다
|
- 운영에서는 Express 서버가 빌드된 `client/dist`를 직접 서빙한다
|
||||||
- 운영 브라우저 진입 경로: `http://<host>:3000/dashboard`
|
- 운영 브라우저 진입 경로: `http://<host>:3000/dashboard`
|
||||||
|
|
||||||
SPA는 `/dashboard`를 라우터 base로 사용하고, 관리자 API는 계속 `/admin/**`로 호출한다.
|
SPA는 `/dashboard`를 라우터 base로 사용하고, 관리자 API는 계속 `/admin/**`로 호출한다.
|
||||||
|
관리자 라우트는 route-level lazy loading을 사용하고, `Scripts` 화면의 Monaco editor는 editor 영역에서만 지연 로딩된다.
|
||||||
|
|
||||||
## Admin Auth Notes
|
## Admin Auth Notes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ async function main() {
|
||||||
log(colors.green, '===============================================');
|
log(colors.green, '===============================================');
|
||||||
log(colors.green, 'Development servers started:');
|
log(colors.green, 'Development servers started:');
|
||||||
log(colors.green, ` - Express API Server: http://localhost:3000`);
|
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.green, '===============================================');
|
||||||
log(colors.yellow, 'Press Ctrl+C to stop all servers');
|
log(colors.yellow, 'Press Ctrl+C to stop all servers');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue