Removes more Wild Cloud cruft.
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@@ -154,7 +154,7 @@ function MobileControls({
|
||||
{hasCert ? (
|
||||
<>
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
||||
<span>TLS: {cert!.coveredBy ? `*.${cert!.domain}` : cert!.domain}</span>
|
||||
<span>TLS: {(cert!.cert.wildcard || cert!.coveredBy) ? `*.${cert!.domain}` : cert!.domain}</span>
|
||||
{cert!.cert.daysLeft != null && <span className="text-muted-foreground">({cert!.cert.daysLeft}d)</span>}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import {
|
||||
Globe, Wifi, Lock, Shield, ShieldAlert, Router,
|
||||
AlertCircle, Loader2, Plus, RotateCw, Pencil,
|
||||
AlertCircle, Loader2, Plus, RotateCw,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -57,6 +57,68 @@ export interface DomainTopologyProps {
|
||||
|
||||
const handleStyle = { opacity: 0, width: 4, height: 4 };
|
||||
|
||||
function BackendNode({ data, leftHandle, rightHandle, bottomHandle }: {
|
||||
data: Record<string, unknown>;
|
||||
leftHandle: React.ReactNode;
|
||||
rightHandle: React.ReactNode;
|
||||
bottomHandle: React.ReactNode;
|
||||
}) {
|
||||
const onChangeBackend = data.onChangeBackend as ((addr: string) => void) | undefined;
|
||||
const hasRoutes = data.hasRoutes as boolean;
|
||||
const editable = !!onChangeBackend && !hasRoutes;
|
||||
|
||||
const [editing, setEditing] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSave = () => {
|
||||
const val = inputRef.current?.value.trim();
|
||||
if (val && onChangeBackend) onChangeBackend(val);
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{leftHandle}
|
||||
<TopologyNode
|
||||
label={editing ? '' : (data.label as string)}
|
||||
sublabel={editing ? undefined : (data.sublabel as string | undefined)}
|
||||
status="ok"
|
||||
className="font-mono"
|
||||
interactive={editable && !editing}
|
||||
onClick={editable && !editing ? () => setEditing(true) : undefined}
|
||||
tooltip={editable && !editing ? 'Click to edit backend address' : undefined}
|
||||
>
|
||||
{editing && (
|
||||
<div className="nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
defaultValue={data.label as string}
|
||||
className="h-5 text-[10px] font-mono px-1.5"
|
||||
autoFocus
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') handleSave();
|
||||
if (e.key === 'Escape') setEditing(false);
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-1 mt-1">
|
||||
<Button variant="ghost" size="sm" className="h-4 text-[9px] px-1 flex-1 !cursor-pointer"
|
||||
style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()} onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-4 text-[9px] px-1 flex-1 !cursor-pointer"
|
||||
style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()} onClick={() => setEditing(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TopologyNode>
|
||||
{rightHandle}
|
||||
{bottomHandle}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FlowNode({ data }: NodeProps) {
|
||||
const v = data.variant as string;
|
||||
|
||||
@@ -172,6 +234,7 @@ function FlowNode({ data }: NodeProps) {
|
||||
if (v === 'tls') {
|
||||
const status = data.status as NodeStatus;
|
||||
const certDomain = data.certDomain as string | undefined;
|
||||
const isWildcard = data.isWildcardCert as boolean;
|
||||
const daysLeft = data.daysLeft as number;
|
||||
const hasCert = data.hasCert as boolean;
|
||||
const provisionDomain = data.provisionDomain as string;
|
||||
@@ -192,7 +255,7 @@ function FlowNode({ data }: NodeProps) {
|
||||
tooltip={
|
||||
hasCert ? (
|
||||
<div className="space-y-1">
|
||||
<div>Certificate: {certDomain}</div>
|
||||
<div>Certificate: {certDomain} ({isWildcard ? 'wildcard' : 'single domain'})</div>
|
||||
<div>Expires: {expiry ?? 'unknown'} ({daysLeft}d)</div>
|
||||
{issuer && <div>Issuer: {issuer}</div>}
|
||||
</div>
|
||||
@@ -230,63 +293,7 @@ function FlowNode({ data }: NodeProps) {
|
||||
}
|
||||
|
||||
if (v === 'backend') {
|
||||
const onChangeBackend = data.onChangeBackend as ((addr: string) => void) | undefined;
|
||||
const hasRoutes = data.hasRoutes as boolean;
|
||||
const editable = !!onChangeBackend && !hasRoutes;
|
||||
|
||||
const [editing, setEditing] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSave = () => {
|
||||
const val = inputRef.current?.value.trim();
|
||||
if (val && onChangeBackend) onChangeBackend(val);
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{leftHandle}
|
||||
<TopologyNode
|
||||
label={data.label as string}
|
||||
sublabel={data.sublabel as string | undefined}
|
||||
status="ok"
|
||||
className="font-mono"
|
||||
interactive={editable && !editing}
|
||||
onClick={editable ? () => setEditing(true) : undefined}
|
||||
tooltip={editable ? 'Click to edit backend address' : undefined}
|
||||
>
|
||||
{editable && !editing && (
|
||||
<Pencil className="absolute top-1.5 right-1.5 h-2.5 w-2.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
)}
|
||||
{editing && (
|
||||
<div className="mt-1.5 nopan nodrag" style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
defaultValue={data.label as string}
|
||||
className="h-5 text-[10px] font-mono px-1.5"
|
||||
autoFocus
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') handleSave();
|
||||
if (e.key === 'Escape') setEditing(false);
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-1 mt-1">
|
||||
<Button variant="ghost" size="sm" className="h-4 text-[9px] px-1 flex-1 !cursor-pointer"
|
||||
style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()} onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-4 text-[9px] px-1 flex-1 !cursor-pointer"
|
||||
style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()} onClick={() => setEditing(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TopologyNode>
|
||||
{rightHandle}
|
||||
{bottomHandle}
|
||||
</>
|
||||
);
|
||||
return <BackendNode data={data} leftHandle={leftHandle} rightHandle={rightHandle} bottomHandle={bottomHandle} />;
|
||||
}
|
||||
|
||||
if (v === 'lan') {
|
||||
@@ -474,7 +481,8 @@ export function DomainTopology(props: DomainTopologyProps) {
|
||||
// TLS
|
||||
const hasCert = !!cert?.cert.exists;
|
||||
const tlsStatus: NodeStatus = hasCert ? 'ok' : 'error';
|
||||
const certDomain = hasCert ? (cert!.coveredBy ? `*.${cert!.domain}` : cert!.domain) : undefined;
|
||||
const isWildcardCert = !!(cert?.cert.wildcard || cert?.coveredBy);
|
||||
const certDomain = hasCert ? (isWildcardCert ? `*.${cert!.domain}` : cert!.domain) : undefined;
|
||||
const daysLeft = cert?.cert.daysLeft ?? 0;
|
||||
const provisionDomain = domain.subdomains ? `*.${domain.domain}` : domain.domain;
|
||||
|
||||
@@ -556,7 +564,7 @@ export function DomainTopology(props: DomainTopologyProps) {
|
||||
id: 'tls', type: 'topology',
|
||||
position: { x: tlsX, y: pipelineY },
|
||||
data: {
|
||||
variant: 'tls', status: tlsStatus, certDomain, daysLeft, hasCert, provisionDomain,
|
||||
variant: 'tls', status: tlsStatus, certDomain, isWildcardCert, daysLeft, hasCert, provisionDomain,
|
||||
expiry: cert?.cert.expiry ? new Date(cert.cert.expiry).toLocaleDateString() : undefined,
|
||||
issuer: cert?.cert.issuerCN,
|
||||
isProvisioningCert, isRenewingCert, onProvisionCert, onRenewCert,
|
||||
|
||||
@@ -57,18 +57,20 @@ export const TopologyNode = forwardRef<HTMLDivElement, TopologyNodeProps>(
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon && <span className="shrink-0 text-muted-foreground">{icon}</span>}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={cn('h-1.5 w-1.5 rounded-full shrink-0', statusDotColors[status])} />
|
||||
<span className="font-medium truncate">{label}</span>
|
||||
{label && (
|
||||
<div className="flex items-center gap-2">
|
||||
{icon && <span className="shrink-0 text-muted-foreground">{icon}</span>}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={cn('h-1.5 w-1.5 rounded-full shrink-0', statusDotColors[status])} />
|
||||
<span className="font-medium truncate">{label}</span>
|
||||
</div>
|
||||
{sublabel && (
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5 truncate">{sublabel}</div>
|
||||
)}
|
||||
</div>
|
||||
{sublabel && (
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5 truncate">{sublabel}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { apiClient } from './client';
|
||||
export interface CertInfo {
|
||||
exists: boolean;
|
||||
domain?: string;
|
||||
wildcard?: boolean;
|
||||
expiry?: string;
|
||||
certPath?: string;
|
||||
keyPath?: string;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { apiClient } from './client';
|
||||
|
||||
export interface DnsmasqStatus {
|
||||
running: boolean;
|
||||
status?: string;
|
||||
}
|
||||
import type { DnsmasqStatus } from '../../types';
|
||||
|
||||
export const dnsmasqApi = {
|
||||
async getStatus(): Promise<DnsmasqStatus> {
|
||||
|
||||
@@ -9,5 +9,5 @@ export { certApi } from './cert';
|
||||
export type { CertStatusResponse, CertInfo } from './cert';
|
||||
export { cloudflareApi } from './cloudflare';
|
||||
export type { CloudflareVerifyResponse, CloudflareZone } from './cloudflare';
|
||||
export { operatorApi, centralDomainApi, dnsSettingsApi, ddnsConfigApi, dhcpConfigApi, nftablesConfigApi, haproxyRoutesApi } from './settings';
|
||||
export { dnsSettingsApi, ddnsConfigApi, dhcpConfigApi, nftablesConfigApi, haproxyRoutesApi } from './settings';
|
||||
export type { OperatorSettings, CentralDomainSettings, DNSSettings, DDNSConfig, DHCPConfig, NftablesConfig, HAProxyCustomRoute, HAProxyRoutes } from './settings';
|
||||
|
||||
@@ -10,10 +10,6 @@ export interface Message {
|
||||
type: 'info' | 'success' | 'error';
|
||||
}
|
||||
|
||||
export interface LoadingState {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
export interface Messages {
|
||||
[key: string]: Message;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const formatTimestamp = (timestamp: string): string => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
Reference in New Issue
Block a user