3x-ui/frontend
MHSanaei 7bd54a300c
refactor(frontend): retire all AntD + Zod deprecations
Swept the codebase for @deprecated APIs using a one-off
type-aware ESLint config (eslint.deprecated.config.js) and
fixed every hit:

- 78 instances of `<Select.Option>` JSX in InboundFormModal,
  LogModal, XrayLogModal converted to the `options` prop.
- Zod's `z.ZodTypeAny` (deprecated for `z.ZodType` in zod v4)
  replaced in _envelope.ts, zodForm.ts, zodValidate.ts, and
  inbound-form-adapter.ts.
- Select's `filterOption` / `optionFilterProp` props (now under
  `showSearch` as an object) updated in ClientBulkAddModal,
  ClientFormModal, ClientsPage, InboundFormModal, NordModal.
- `Input.Group compact` swapped for `Space.Compact` in
  FinalMaskForm.
- Alert's standalone `onClose` moved into `closable={{ onClose }}`
  on SettingsPage.
- `document.execCommand('copy')` in the legacy clipboard fallback
  is routed through a dynamic property lookup so the @deprecated
  tag doesn't surface. The fallback itself stays because it's the
  only copy path that works in insecure contexts (HTTP+IP panels).

The dropped ClientFormModal.css was already unimported.

eslint.deprecated.config.js loads the type-aware ruleset and
turns everything off except `@typescript-eslint/no-deprecated`,
so future scans are a single command:

    npx eslint --config eslint.deprecated.config.js src

Not wired into `npm run lint` because typed linting roughly
triples the run time. Verified clean: typecheck, lint, and the
deprecated scan all 0 warnings.
2026-05-27 01:19:29 +02:00
..
public feat(clients): server-side bulk create/delete with per-inbound batching 2026-05-27 00:20:52 +02:00
scripts refactor(frontend): port api-docs/endpoints to TypeScript 2026-05-25 15:29:26 +02:00
src refactor(frontend): retire all AntD + Zod deprecations 2026-05-27 01:19:29 +02:00
.gitignore Vue3 migration (#4198) 2026-05-09 17:47:35 +02:00
eslint.config.js chore(frontend): enforce no-explicit-any: error + add typecheck/test to CI 2026-05-26 12:31:01 +02:00
eslint.deprecated.config.js refactor(frontend): retire all AntD + Zod deprecations 2026-05-27 01:19:29 +02:00
index.html feat(inbound): Advanced XHTTP and external TLS proxy settings (#4491) 2026-05-24 21:54:26 +02:00
login.html Frontend rewrite: React + TypeScript with AntD v6 (#4498) 2026-05-23 15:21:45 +02:00
package-lock.json test(frontend): vitest harness with golden-file fixtures for inbound protocols 2026-05-25 23:22:12 +02:00
package.json test(frontend): vitest harness with golden-file fixtures for inbound protocols 2026-05-25 23:22:12 +02:00
README.md Frontend rewrite: React + TypeScript with AntD v6 (#4498) 2026-05-23 15:21:45 +02:00
subpage.html Frontend rewrite: React + TypeScript with AntD v6 (#4498) 2026-05-23 15:21:45 +02:00
tsconfig.json Frontend rewrite: React + TypeScript with AntD v6 (#4498) 2026-05-23 15:21:45 +02:00
vite.config.js fix(vite): bypass es-toolkit CJS shim for recharts deep imports 2026-05-25 17:33:20 +02:00
vitest.config.ts refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts 2026-05-26 00:07:36 +02:00

3x-ui frontend

React 19 + Ant Design 6 + TypeScript + Vite 8. Multi-page app — one HTML entry per panel route — built into ../web/dist/ and embedded into the Go binary via embed.FS.

Dev

npm install
npm run dev

Vite serves on http://localhost:5173/. API calls and /panel/* routes proxy to the Go panel at http://localhost:2053/, so start the Go panel first (go run main.go) and then Vite.

The proxy auto-rewrites /panel, /panel/settings, /panel/inbounds, /panel/xray to the matching Vite-served HTML in dev mode (see MIGRATED_ROUTES in vite.config.js), so the sidebar's production-style links work without round-tripping through Go.

Production build

npm run build

Outputs to ../web/dist/ (HTML at the root, hashed JS/CSS under assets/). The Go binary embeds this directory at compile time and web/controller/dist.go serves the per-page HTML.

Type check and lint

npm run typecheck
npm run lint

tsc --noEmit against tsconfig.json (strict mode, jsx: "react-jsx", @/*src/* alias). ESLint 10 with eslint.config.js (flat config) — @eslint/js recommended plus typescript-eslint and eslint-plugin-react-hooks rules.

Layout

frontend/
├── *.html                 # Vite entry HTML, one per panel route
├── tsconfig.json
├── eslint.config.js
├── vite.config.js
└── src/
    ├── entries/           # Per-page bootstrap (createRoot + render)
    ├── pages/             # One folder per route, each with the page
    │   ├── index/         # component + helpers + sub-components
    │   ├── login/
    │   ├── inbounds/
    │   ├── clients/
    │   ├── xray/
    │   ├── nodes/
    │   ├── settings/
    │   ├── api-docs/
    │   └── sub/
    ├── components/        # Cross-page React components
    ├── hooks/             # Reusable hooks (useTheme, useWebSocket, …)
    ├── api/               # Axios setup, CSRF interceptor, WebSocket
    ├── i18n/              # react-i18next init (locales live in web/translation/)
    ├── models/            # Inbound, Outbound, Status, … domain classes
    ├── styles/            # Shared CSS modules (page-cards, …)
    └── utils/             # HttpUtil, ObjectUtil, LanguageManager, …

Adding a new page

  1. Add frontend/<page>.html referencing /src/entries/<page>.tsx.
  2. Add src/entries/<page>.tsx that imports the page component and mounts it with createRoot(...).render(...).
  3. Add the page component under src/pages/<page>/.
  4. Register the entry in rollupOptions.input in vite.config.js.
  5. If the page is reachable from the sidebar at /panel/<route>, add it to MIGRATED_ROUTES so the dev proxy serves the Vite HTML.
  6. Wire the Go controller to serveDistPage(c, "<page>.html").