mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-17 15:56:39 +00:00
Add better error page
This commit is contained in:
4
proxy/web/dist/index.html
vendored
4
proxy/web/dist/index.html
vendored
@@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NetBird Service</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<script type="module" crossorigin src="/assets/index-ClfM9m3s.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/style-B1NSEbha.css">
|
||||
<script type="module" crossorigin src="/assets/index-BWSM6sR5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/style-IH-yd16d.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Go template variables injected here -->
|
||||
|
||||
2
proxy/web/public/robots.txt
Normal file
2
proxy/web/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
66
proxy/web/src/ErrorPage.tsx
Normal file
66
proxy/web/src/ErrorPage.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {BookText, RotateCw, Server, Globe, UserIcon, WaypointsIcon} from "lucide-react";
|
||||
import { Title } from "@/components/Title";
|
||||
import { Description } from "@/components/Description";
|
||||
import Button from "@/components/Button";
|
||||
import { PoweredByNetBird } from "@/components/PoweredByNetBird";
|
||||
import { StatusCard } from "@/components/StatusCard";
|
||||
import type { ErrorData } from "@/data";
|
||||
|
||||
export function ErrorPage({ code, title, message, proxy = true, peer = true, destination = true, requestId }: ErrorData) {
|
||||
useEffect(() => {
|
||||
document.title = `${title} - NetBird Service`;
|
||||
}, [title]);
|
||||
|
||||
const [timestamp] = useState(() => new Date().toISOString());
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-center mt-24 px-4 max-w-3xl mx-auto">
|
||||
{/* Error Code */}
|
||||
<div className="text-sm text-netbird font-normal font-mono mb-3 z-10 relative">
|
||||
{code} {title === "Service Unavailable" ? "Bad Gateway" : title.split(" ").slice(0, 2).join(" ")}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<Title className="text-3xl!">{title}</Title>
|
||||
|
||||
{/* Description */}
|
||||
<Description className="mt-2 mb-8 max-w-md">{message}</Description>
|
||||
|
||||
{/* Status Cards */}
|
||||
<div className="hidden sm:flex items-start justify-center w-full mt-6 mb-16 z-10 relative">
|
||||
<StatusCard icon={UserIcon} label="You" line={false} />
|
||||
<StatusCard icon={WaypointsIcon} label="Proxy" success={proxy} />
|
||||
<StatusCard icon={Server} label="Routing Peer" success={peer} />
|
||||
<StatusCard icon={Globe} label="Destination" success={destination} />
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 justify-center items-center mb-6 z-10 relative">
|
||||
<Button variant="primary" onClick={() => window.location.reload()}>
|
||||
<RotateCw size={16} />
|
||||
Refresh Page
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => window.open("https://docs.netbird.io", "_blank")}
|
||||
>
|
||||
<BookText size={16} />
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Request Info */}
|
||||
<div className="text-center text-xs text-nb-gray-300 uppercase z-10 relative font-mono flex flex-col sm:flex-row gap-2 sm:gap-10 mt-4 mb-3">
|
||||
<div>
|
||||
<span className="text-nb-gray-400">REQUEST-ID:</span> {requestId}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-nb-gray-400">TIMESTAMP:</span> {timestamp}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PoweredByNetBird />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
26
proxy/web/src/components/ConnectionLine.tsx
Normal file
26
proxy/web/src/components/ConnectionLine.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { X } from "lucide-react";
|
||||
|
||||
interface ConnectionLineProps {
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
export function ConnectionLine({ success = true }: ConnectionLineProps) {
|
||||
if (success) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center h-12 w-full px-5">
|
||||
<div className="w-full border-t-2 border-dashed border-green-500" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center h-12 min-w-10 px-5 relative">
|
||||
<div className="w-full border-t-2 border-dashed border-nb-gray-900" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center">
|
||||
<X size={18} className="text-netbird" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
proxy/web/src/components/StatusCard.tsx
Normal file
38
proxy/web/src/components/StatusCard.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { ConnectionLine } from "./ConnectionLine";
|
||||
|
||||
interface StatusCardProps {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
detail?: string;
|
||||
success?: boolean;
|
||||
line?: boolean;
|
||||
}
|
||||
|
||||
export function StatusCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
detail,
|
||||
success = true,
|
||||
line = true,
|
||||
}: StatusCardProps) {
|
||||
return (
|
||||
<>
|
||||
{line && <ConnectionLine success={success} />}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-14 h-14 rounded-md flex items-center justify-center from-nb-gray-940 to-nb-gray-930/70 bg-gradient-to-br border border-nb-gray-910">
|
||||
<Icon size={20} className="text-nb-gray-200" />
|
||||
</div>
|
||||
<span className="text-sm text-nb-gray-200 font-normal mt-1">{label}</span>
|
||||
<span className={`text-xs font-medium uppercase ${success ? "text-green-500" : "text-netbird"}`}>
|
||||
{success ? "Connected" : "Unreachable"}
|
||||
</span>
|
||||
{detail && (
|
||||
<span className="text-xs text-nb-gray-400 truncate text-center">
|
||||
{detail}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,10 @@ export interface ErrorData {
|
||||
code: number
|
||||
title: string
|
||||
message: string
|
||||
proxy?: boolean
|
||||
peer?: boolean
|
||||
destination?: boolean
|
||||
requestId?: string
|
||||
}
|
||||
|
||||
// Data injected by Go templates
|
||||
@@ -25,5 +29,27 @@ declare global {
|
||||
}
|
||||
|
||||
export function getData(): Data {
|
||||
return window.__DATA__ ?? {}
|
||||
const data = window.__DATA__ ?? {}
|
||||
|
||||
// Dev mode: allow ?page=error query param to preview error page
|
||||
if (import.meta.env.DEV) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const page = params.get('page')
|
||||
if (page === 'error') {
|
||||
return {
|
||||
...data,
|
||||
page: 'error',
|
||||
error: data.error ?? {
|
||||
code: 503,
|
||||
title: 'Service Unavailable',
|
||||
message: 'The service you are trying to access is temporarily unavailable. Please try again later.',
|
||||
proxy: true,
|
||||
peer: false,
|
||||
destination: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { ErrorPage } from './pages/ErrorPage.tsx'
|
||||
import { ErrorPage } from './ErrorPage.tsx'
|
||||
import { getData } from '@/data'
|
||||
|
||||
const data = getData()
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { BookText, RotateCw } from "lucide-react";
|
||||
import { Title } from "@/components/Title";
|
||||
import { Description } from "@/components/Description";
|
||||
import { PoweredByNetBird } from "@/components/PoweredByNetBird";
|
||||
import { Card } from "@/components/Card";
|
||||
import Button from "@/components/Button";
|
||||
import type { ErrorData } from "@/data";
|
||||
|
||||
export function ErrorPage({ code, title, message }: ErrorData) {
|
||||
useEffect(() => {
|
||||
document.title = `${title} - NetBird Service`;
|
||||
}, [title]);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-center mt-40 px-4 max-w-xl mx-auto">
|
||||
<Card className="text-center">
|
||||
<div className="text-5xl font-bold text-nb-gray-200 mb-4">{code}</div>
|
||||
<Title>{title}</Title>
|
||||
<Description className="mt-2">{message}</Description>
|
||||
<div className="mt-6 flex gap-3 justify-center">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
<RotateCw size={16} />
|
||||
Refresh Page
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => window.open("https://docs.netbird.io", "_blank")}
|
||||
>
|
||||
<BookText size={16} />
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<PoweredByNetBird />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -108,14 +108,25 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, data any, statusCode ...i
|
||||
w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// ErrorStatus represents the connection status for each component in the error page.
|
||||
type ErrorStatus struct {
|
||||
Proxy bool
|
||||
Peer bool
|
||||
Destination bool
|
||||
}
|
||||
|
||||
// ServeErrorPage renders a user-friendly error page with the given details.
|
||||
func ServeErrorPage(w http.ResponseWriter, r *http.Request, code int, title, message string) {
|
||||
func ServeErrorPage(w http.ResponseWriter, r *http.Request, code int, title, message, requestID string, status ErrorStatus) {
|
||||
ServeHTTP(w, r, map[string]any{
|
||||
"page": "error",
|
||||
"error": map[string]any{
|
||||
"code": code,
|
||||
"title": title,
|
||||
"message": message,
|
||||
"code": code,
|
||||
"title": title,
|
||||
"message": message,
|
||||
"proxy": status.Proxy,
|
||||
"peer": status.Peer,
|
||||
"destination": status.Destination,
|
||||
"requestId": requestID,
|
||||
},
|
||||
}, code)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user