chore: enhance frontend configuration for backend integration
- Added environment variables for backend URLs in the Dockerfile to facilitate dynamic configuration. - Updated Next.js configuration to include rewrites for health and API endpoints, improving backend connectivity. - Modified settings page to handle backend URL locking and normalization, enhancing user experience and data integrity.
This commit is contained in:
@@ -125,6 +125,10 @@ jobs:
|
||||
file: Dockerfile.frontend
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
BACKEND_INTERNAL_URL=http://backend:8000
|
||||
NEXT_PUBLIC_BACKEND_URL=same-origin
|
||||
NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
||||
|
||||
@@ -8,6 +8,12 @@ COPY packages/contracts/package.json packages/contracts/
|
||||
RUN npm ci --workspace=@mmapp/contracts --include-workspace-root --ignore-scripts
|
||||
|
||||
FROM deps AS build
|
||||
ARG BACKEND_INTERNAL_URL=http://backend:8000
|
||||
ARG NEXT_PUBLIC_BACKEND_URL=same-origin
|
||||
ARG NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live
|
||||
ENV BACKEND_INTERNAL_URL=$BACKEND_INTERNAL_URL
|
||||
ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL
|
||||
ENV NEXT_PUBLIC_DEFAULT_DATA_SOURCE=$NEXT_PUBLIC_DEFAULT_DATA_SOURCE
|
||||
COPY packages/contracts packages/contracts
|
||||
COPY next.config.ts tsconfig.json postcss.config.mjs components.json ./
|
||||
COPY app app
|
||||
|
||||
@@ -825,7 +825,7 @@ export default function SettingsPage() {
|
||||
const [copied, setCopied] = useState<string | null>(null)
|
||||
|
||||
// data source
|
||||
const { mode, setMode, backendUrl, setBackendUrl, backendStatus, checkBackend } = useDataSource()
|
||||
const { mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, backendStatus, checkBackend } = useDataSource()
|
||||
const evo = useEvoBGP()
|
||||
const [evoBaseDraft, setEvoBaseDraft] = useState("")
|
||||
const [evoEnabledDraft, setEvoEnabledDraft] = useState(false)
|
||||
@@ -834,6 +834,17 @@ export default function SettingsPage() {
|
||||
const [evoSaveErr, setEvoSaveErr] = useState<string | null>(null)
|
||||
const [urlDraft, setUrlDraft] = useState(backendUrl)
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
|
||||
useEffect(() => {
|
||||
setUrlDraft(backendUrl)
|
||||
}, [backendUrl])
|
||||
|
||||
const commitBackendUrl = useCallback(() => {
|
||||
const normalized = urlDraft.trim().replace(/\/$/, "")
|
||||
setUrlDraft(normalized)
|
||||
setBackendUrl(normalized)
|
||||
void checkBackend()
|
||||
}, [urlDraft, setBackendUrl, checkBackend])
|
||||
const [evoTestResult, setEvoTestResult] = useState<{ ok: boolean; message: string } | null>(null)
|
||||
const [evoBusy, setEvoBusy] = useState<"test" | "refresh" | null>(null)
|
||||
const [showEvoKey, setShowEvoKey] = useState(false)
|
||||
@@ -984,17 +995,31 @@ export default function SettingsPage() {
|
||||
</SettingRow>
|
||||
|
||||
{/* backend URL — visible in both modes so user can configure before switching */}
|
||||
<SettingRow label="URL бекенда" description="Адрес Node.js/Fastify сервера (запускается командой npm run dev в /backend)">
|
||||
<SettingRow
|
||||
label="URL бекенда"
|
||||
description={
|
||||
backendUrlLocked
|
||||
? "В Docker-образе URL API задаётся через прокси фронтенда на backend; поле только для просмотра."
|
||||
: "Адрес Node.js/Fastify сервера (запускается командой npm run dev в /backend)"
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
className="w-56 h-8 text-sm font-mono"
|
||||
value={urlDraft}
|
||||
onChange={e => setUrlDraft(e.target.value)}
|
||||
onBlur={() => { setBackendUrl(urlDraft.replace(/\/$/, "")) }}
|
||||
onBlur={commitBackendUrl}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
commitBackendUrl()
|
||||
}
|
||||
}}
|
||||
readOnly={backendUrlLocked}
|
||||
placeholder="http://localhost:8000"
|
||||
/>
|
||||
<Button variant="outline" size="sm" className="h-8 px-2.5 shrink-0"
|
||||
onClick={() => { setBackendUrl(urlDraft.replace(/\/$/, "")); checkBackend() }}
|
||||
onClick={commitBackendUrl}
|
||||
title="Проверить соединение">
|
||||
<RefreshCwIcon className="size-3.5" />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export const LOCAL_DEFAULT_BACKEND_URL = "http://localhost:8000"
|
||||
|
||||
export type ConfiguredBackendUrl =
|
||||
| { kind: "local" }
|
||||
| { kind: "fixed"; url: string }
|
||||
| { kind: "same-origin" }
|
||||
|
||||
export function configuredBackendUrl(): ConfiguredBackendUrl {
|
||||
const raw = process.env.NEXT_PUBLIC_BACKEND_URL
|
||||
if (raw === undefined || raw === "local") return { kind: "local" }
|
||||
if (raw === "" || raw === "same-origin") return { kind: "same-origin" }
|
||||
return { kind: "fixed", url: raw.replace(/\/$/, "") }
|
||||
}
|
||||
|
||||
export function defaultDataSourceMode(): "mock" | "live" {
|
||||
return process.env.NEXT_PUBLIC_DEFAULT_DATA_SOURCE === "live" ? "live" : "mock"
|
||||
}
|
||||
|
||||
export function isBackendUrlLocked(): boolean {
|
||||
const cfg = configuredBackendUrl()
|
||||
return cfg.kind === "same-origin" || cfg.kind === "fixed"
|
||||
}
|
||||
|
||||
export function resolveStoredBackendUrl(stored: string | null): string {
|
||||
const cfg = configuredBackendUrl()
|
||||
if (cfg.kind === "fixed") return cfg.url
|
||||
if (cfg.kind === "same-origin" && typeof window !== "undefined") {
|
||||
return window.location.origin
|
||||
}
|
||||
const trimmed = stored?.trim().replace(/\/$/, "")
|
||||
return trimmed || LOCAL_DEFAULT_BACKEND_URL
|
||||
}
|
||||
+35
-20
@@ -1,6 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from "react"
|
||||
import {
|
||||
configuredBackendUrl,
|
||||
defaultDataSourceMode,
|
||||
isBackendUrlLocked,
|
||||
LOCAL_DEFAULT_BACKEND_URL,
|
||||
resolveStoredBackendUrl,
|
||||
} from "@/lib/backend-url"
|
||||
|
||||
// ── types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -11,6 +18,7 @@ interface DataSourceContextValue {
|
||||
setMode: (m: DataSourceMode) => void
|
||||
backendUrl: string
|
||||
setBackendUrl: (url: string) => void
|
||||
backendUrlLocked: boolean
|
||||
/** undefined = не проверялось, true = OK, false = недоступен */
|
||||
backendStatus: boolean | undefined
|
||||
checkBackend: () => Promise<void>
|
||||
@@ -22,31 +30,35 @@ const DataSourceContext = createContext<DataSourceContextValue | null>(null)
|
||||
|
||||
const LS_MODE = "routerlists:data-source"
|
||||
const LS_BACKEND = "routerlists:backend-url"
|
||||
const DEFAULT_URL = "http://localhost:8000"
|
||||
|
||||
function readStoredMode(): DataSourceMode {
|
||||
if (typeof window === "undefined") return "mock"
|
||||
if (typeof window === "undefined") return defaultDataSourceMode()
|
||||
const stored = localStorage.getItem(LS_MODE) as DataSourceMode | null
|
||||
return stored === "mock" || stored === "live" ? stored : "mock"
|
||||
return stored === "mock" || stored === "live" ? stored : defaultDataSourceMode()
|
||||
}
|
||||
|
||||
function readStoredBackendUrl(): string {
|
||||
if (typeof window === "undefined") return DEFAULT_URL
|
||||
return localStorage.getItem(LS_BACKEND) ?? DEFAULT_URL
|
||||
if (typeof window === "undefined") return LOCAL_DEFAULT_BACKEND_URL
|
||||
return resolveStoredBackendUrl(localStorage.getItem(LS_BACKEND))
|
||||
}
|
||||
|
||||
function normalizeBackendUrl(url: string): string {
|
||||
return url.trim().replace(/\/$/, "")
|
||||
}
|
||||
|
||||
export function DataSourceProvider({ children }: { children: React.ReactNode }) {
|
||||
const [mode, setModeState] = useState<DataSourceMode>("mock")
|
||||
const [backendUrl, setBackendUrlState] = useState(DEFAULT_URL)
|
||||
const [mode, setModeState] = useState<DataSourceMode>(() =>
|
||||
typeof window === "undefined" ? defaultDataSourceMode() : readStoredMode(),
|
||||
)
|
||||
const [backendUrl, setBackendUrlState] = useState(() =>
|
||||
typeof window === "undefined" ? LOCAL_DEFAULT_BACKEND_URL : readStoredBackendUrl(),
|
||||
)
|
||||
const [backendStatus, setBackendStatus] = useState<boolean | undefined>(undefined)
|
||||
const backendUrlLocked = isBackendUrlLocked()
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
const nextMode = readStoredMode()
|
||||
const nextBackendUrl = readStoredBackendUrl()
|
||||
setModeState(nextMode)
|
||||
setBackendUrlState(nextBackendUrl)
|
||||
})
|
||||
if (configuredBackendUrl().kind !== "same-origin") return
|
||||
setBackendUrlState(window.location.origin)
|
||||
}, [])
|
||||
|
||||
const setMode = useCallback((m: DataSourceMode) => {
|
||||
@@ -55,27 +67,30 @@ export function DataSourceProvider({ children }: { children: React.ReactNode })
|
||||
}, [])
|
||||
|
||||
const setBackendUrl = useCallback((url: string) => {
|
||||
setBackendUrlState(url)
|
||||
localStorage.setItem(LS_BACKEND, url)
|
||||
}, [])
|
||||
if (backendUrlLocked) return
|
||||
const normalized = normalizeBackendUrl(url)
|
||||
setBackendUrlState(normalized)
|
||||
localStorage.setItem(LS_BACKEND, normalized)
|
||||
}, [backendUrlLocked])
|
||||
|
||||
const checkBackend = useCallback(async () => {
|
||||
const url = (localStorage.getItem(LS_BACKEND) ?? DEFAULT_URL).replace(/\/$/, "")
|
||||
const url = normalizeBackendUrl(backendUrl)
|
||||
try {
|
||||
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) })
|
||||
setBackendStatus(res.ok)
|
||||
} catch {
|
||||
setBackendStatus(false)
|
||||
}
|
||||
}, [])
|
||||
}, [backendUrl])
|
||||
|
||||
// Auto-check when mode switches to live (defer to avoid set-state-in-effect rule on async loader)
|
||||
useEffect(() => {
|
||||
if (mode === "live") queueMicrotask(() => { void checkBackend() })
|
||||
}, [mode, checkBackend])
|
||||
|
||||
return (
|
||||
<DataSourceContext.Provider value={{ mode, setMode, backendUrl, setBackendUrl, backendStatus, checkBackend }}>
|
||||
<DataSourceContext.Provider
|
||||
value={{ mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, backendStatus, checkBackend }}
|
||||
>
|
||||
{children}
|
||||
</DataSourceContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const backendInternalUrl = (process.env.BACKEND_INTERNAL_URL ?? "http://127.0.0.1:8000").replace(
|
||||
/\/$/,
|
||||
"",
|
||||
);
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
async rewrites() {
|
||||
return [
|
||||
{ source: "/health", destination: `${backendInternalUrl}/health` },
|
||||
{ source: "/api/:path*", destination: `${backendInternalUrl}/api/:path*` },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Reference in New Issue
Block a user