mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-14 04:39:54 +00:00
[client/ui] Replace fyne UI with Wails (rename ui-wails to ui)
Removes the legacy fyne-based client/ui implementation and renames the Wails replacement (client/ui-wails) to take its place at client/ui. Go imports, frontend bindings, CI workflows, goreleaser configs and the windows .syso icon path are updated to follow the rename.
This commit is contained in:
93
client/ui/frontend/Inter Font License.txt
Normal file
93
client/ui/frontend/Inter Font License.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,51 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Connection groups the daemon RPCs that drive login / connect / disconnect.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function Down(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1062334452);
|
||||
}
|
||||
|
||||
export function Login(p: $models.LoginParams): $CancellablePromise<$models.LoginResult> {
|
||||
return $Call.ByID(782816741, p).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function Logout(p: $models.LogoutParams): $CancellablePromise<void> {
|
||||
return $Call.ByID(4028053230, p);
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenURL launches the user's preferred browser to display url. Mirrors the
|
||||
* Fyne client's openURL helper so the SSO flow can pop the verification page
|
||||
* the same way as the legacy UI — WebKitGTK's window.open is blocked by the
|
||||
* embedded webview, and asking the user to copy/paste defeats the point of
|
||||
* SSO. Honors $BROWSER first, then falls back to the platform default.
|
||||
*/
|
||||
export function OpenURL(url: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(4267001345, url);
|
||||
}
|
||||
|
||||
export function Up(p: $models.UpParams): $CancellablePromise<void> {
|
||||
return $Call.ByID(1178388469, p);
|
||||
}
|
||||
|
||||
export function WaitSSOLogin(p: $models.WaitSSOParams): $CancellablePromise<string> {
|
||||
return $Call.ByID(3487329509, p);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.LoginResult.createFrom;
|
||||
@@ -0,0 +1,35 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Debug groups debug / log-level / packet-trace RPCs.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function Bundle(p: $models.DebugBundleParams): $CancellablePromise<$models.DebugBundleResult> {
|
||||
return $Call.ByID(1875836985, p).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function GetLogLevel(): $CancellablePromise<$models.LogLevel> {
|
||||
return $Call.ByID(2713455331).then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function SetLogLevel(lvl: $models.LogLevel): $CancellablePromise<void> {
|
||||
return $Call.ByID(2627038775, lvl);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.DebugBundleResult.createFrom;
|
||||
const $$createType1 = $models.LogLevel.createFrom;
|
||||
@@ -0,0 +1,29 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Forwarding groups the daemon RPCs that surface exposed/forwarded services.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* List returns the current set of forwarding rules from the daemon's
|
||||
* reverse proxy. The frontend renders these as the "exposed services" list.
|
||||
*/
|
||||
export function List(): $CancellablePromise<$models.ForwardingRule[]> {
|
||||
return $Call.ByID(3893357601).then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.ForwardingRule.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
@@ -0,0 +1,52 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Connection from "./connection.js";
|
||||
import * as Debug from "./debug.js";
|
||||
import * as Forwarding from "./forwarding.js";
|
||||
import * as Networks from "./networks.js";
|
||||
import * as Peers from "./peers.js";
|
||||
import * as Profiles from "./profiles.js";
|
||||
import * as Settings from "./settings.js";
|
||||
import * as Update from "./update.js";
|
||||
export {
|
||||
Connection,
|
||||
Debug,
|
||||
Forwarding,
|
||||
Networks,
|
||||
Peers,
|
||||
Profiles,
|
||||
Settings,
|
||||
Update
|
||||
};
|
||||
|
||||
export {
|
||||
ActiveProfile,
|
||||
Config,
|
||||
ConfigParams,
|
||||
DebugBundleParams,
|
||||
DebugBundleResult,
|
||||
Features,
|
||||
ForwardingRule,
|
||||
LocalPeer,
|
||||
LogLevel,
|
||||
LoginParams,
|
||||
LoginResult,
|
||||
LogoutParams,
|
||||
Network,
|
||||
PeerLink,
|
||||
PeerStatus,
|
||||
PortInfo,
|
||||
PortRange,
|
||||
Profile,
|
||||
ProfileRef,
|
||||
SelectNetworksParams,
|
||||
SetConfigParams,
|
||||
Status,
|
||||
SystemEvent,
|
||||
UpParams,
|
||||
UpdateAvailable,
|
||||
UpdateProgress,
|
||||
UpdateResult,
|
||||
WaitSSOParams
|
||||
} from "./models.js";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Networks groups the daemon RPCs that read and toggle routed networks.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function Deselect(p: $models.SelectNetworksParams): $CancellablePromise<void> {
|
||||
return $Call.ByID(2335193802, p);
|
||||
}
|
||||
|
||||
export function List(): $CancellablePromise<$models.Network[]> {
|
||||
return $Call.ByID(719769457).then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function Select(p: $models.SelectNetworksParams): $CancellablePromise<void> {
|
||||
return $Call.ByID(3714393053, p);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.Network.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
@@ -0,0 +1,46 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Peers serves the dashboard data: one polled Status RPC and a long-running
|
||||
* SubscribeEvents stream that re-emits every event over the Wails event bus.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* Get returns the current daemon status snapshot.
|
||||
*/
|
||||
export function Get(): $CancellablePromise<$models.Status> {
|
||||
return $Call.ByID(196038193).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch starts the background loops that feed the frontend:
|
||||
* - statusStreamLoop: push-driven snapshots on connection-state change
|
||||
* (Connected/Disconnected/Connecting, peer list, address). Drives the
|
||||
* tray icon, Status page, and Peers page.
|
||||
* - toastStreamLoop: DNS / network / auth / connectivity / update
|
||||
* SystemEvent stream. Drives OS notifications, the Recent Events
|
||||
* list, and the update-overlay flag. The daemon-side RPC is named
|
||||
* SubscribeEvents — only the loop's local alias differs to keep the
|
||||
* two streams distinguishable in this file.
|
||||
*
|
||||
* Safe to call once at boot; both loops self-restart on stream errors
|
||||
* via exponential backoff.
|
||||
*/
|
||||
export function Watch(): $CancellablePromise<void> {
|
||||
return $Call.ByID(741320382);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.Status.createFrom;
|
||||
@@ -0,0 +1,52 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Profiles groups the daemon RPCs that manage named profiles.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function Add(p: $models.ProfileRef): $CancellablePromise<void> {
|
||||
return $Call.ByID(701512397, p);
|
||||
}
|
||||
|
||||
export function GetActive(): $CancellablePromise<$models.ActiveProfile> {
|
||||
return $Call.ByID(2605259596).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function List(username: string): $CancellablePromise<$models.Profile[]> {
|
||||
return $Call.ByID(1745269178, username).then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function Remove(p: $models.ProfileRef): $CancellablePromise<void> {
|
||||
return $Call.ByID(2506403914, p);
|
||||
}
|
||||
|
||||
export function Switch(p: $models.ProfileRef): $CancellablePromise<void> {
|
||||
return $Call.ByID(3405248534, p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Username returns the OS username the daemon expects for profile lookups.
|
||||
* The frontend calls this once at boot and reuses the result.
|
||||
*/
|
||||
export function Username(): $CancellablePromise<string> {
|
||||
return $Call.ByID(1939223418);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.ActiveProfile.createFrom;
|
||||
const $$createType1 = $models.Profile.createFrom;
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
@@ -0,0 +1,35 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Settings groups the daemon RPCs that read and write the daemon config.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function GetConfig(p: $models.ConfigParams): $CancellablePromise<$models.Config> {
|
||||
return $Call.ByID(2849966711, p).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function GetFeatures(): $CancellablePromise<$models.Features> {
|
||||
return $Call.ByID(376812026).then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}
|
||||
|
||||
export function SetConfig(p: $models.SetConfigParams): $CancellablePromise<void> {
|
||||
return $Call.ByID(565510651, p);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.Config.createFrom;
|
||||
const $$createType1 = $models.Features.createFrom;
|
||||
@@ -0,0 +1,41 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Update groups the RPCs that drive the enforced-update install flow.
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function GetInstallerResult(): $CancellablePromise<$models.UpdateResult> {
|
||||
return $Call.ByID(2190725314).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Quit asks the host application to exit. The /update page calls this once
|
||||
* the daemon-side installer has reported success, mirroring the legacy
|
||||
* Fyne UI's app.Quit() in showInstallerResult. Schedules the actual exit
|
||||
* off the calling goroutine so the JS-side caller's response can return
|
||||
* before the runtime tears down.
|
||||
*/
|
||||
export function Quit(): $CancellablePromise<void> {
|
||||
return $Call.ByID(27817640);
|
||||
}
|
||||
|
||||
export function Trigger(): $CancellablePromise<$models.UpdateResult> {
|
||||
return $Call.ByID(2415339649).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.UpdateResult.createFrom;
|
||||
@@ -0,0 +1,28 @@
|
||||
//@ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as services$0 from "../../../../netbirdio/netbird/client/ui-wails/services/models.js";
|
||||
|
||||
function configure() {
|
||||
Object.freeze(Object.assign($Create.Events, {
|
||||
"netbird:event": $$createType0,
|
||||
"netbird:status": $$createType1,
|
||||
"netbird:update:available": $$createType2,
|
||||
"netbird:update:progress": $$createType3,
|
||||
}));
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = services$0.SystemEvent.createFrom;
|
||||
const $$createType1 = services$0.Status.createFrom;
|
||||
const $$createType2 = services$0.UpdateAvailable.createFrom;
|
||||
const $$createType3 = services$0.UpdateProgress.createFrom;
|
||||
|
||||
configure();
|
||||
21
client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
vendored
Normal file
21
client/ui/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import type { Events } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import type * as services$0 from "../../../../netbirdio/netbird/client/ui-wails/services/models.js";
|
||||
|
||||
declare module "@wailsio/runtime" {
|
||||
namespace Events {
|
||||
interface CustomEvents {
|
||||
"netbird:event": services$0.SystemEvent;
|
||||
"netbird:status": services$0.Status;
|
||||
"netbird:update:available": services$0.UpdateAvailable;
|
||||
"netbird:update:progress": services$0.UpdateProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as NotificationService from "./notificationservice.js";
|
||||
export {
|
||||
NotificationService
|
||||
};
|
||||
|
||||
export {
|
||||
NotificationAction,
|
||||
NotificationCategory,
|
||||
NotificationOptions
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,107 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* NotificationAction represents an action button for a notification.
|
||||
*/
|
||||
export class NotificationAction {
|
||||
"id"?: string;
|
||||
"title"?: string;
|
||||
|
||||
/**
|
||||
* (macOS-specific)
|
||||
*/
|
||||
"destructive"?: boolean;
|
||||
|
||||
/** Creates a new NotificationAction instance. */
|
||||
constructor($$source: Partial<NotificationAction> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationAction instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): NotificationAction {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new NotificationAction($$parsedSource as Partial<NotificationAction>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationCategory groups actions for notifications.
|
||||
*/
|
||||
export class NotificationCategory {
|
||||
"id"?: string;
|
||||
"actions"?: NotificationAction[];
|
||||
"hasReplyField"?: boolean;
|
||||
"replyPlaceholder"?: string;
|
||||
"replyButtonTitle"?: string;
|
||||
|
||||
/** Creates a new NotificationCategory instance. */
|
||||
constructor($$source: Partial<NotificationCategory> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationCategory instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): NotificationCategory {
|
||||
const $$createField1_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("actions" in $$parsedSource) {
|
||||
$$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]);
|
||||
}
|
||||
return new NotificationCategory($$parsedSource as Partial<NotificationCategory>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationOptions contains configuration for a notification
|
||||
*/
|
||||
export class NotificationOptions {
|
||||
"id": string;
|
||||
"title": string;
|
||||
|
||||
/**
|
||||
* (macOS and Linux only)
|
||||
*/
|
||||
"subtitle"?: string;
|
||||
"body"?: string;
|
||||
"categoryId"?: string;
|
||||
"data"?: { [_ in string]?: any };
|
||||
|
||||
/** Creates a new NotificationOptions instance. */
|
||||
constructor($$source: Partial<NotificationOptions> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("title" in $$source)) {
|
||||
this["title"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationOptions instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): NotificationOptions {
|
||||
const $$createField5_0 = $$createType2;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("data" in $$parsedSource) {
|
||||
$$parsedSource["data"] = $$createField5_0($$parsedSource["data"]);
|
||||
}
|
||||
return new NotificationOptions($$parsedSource as Partial<NotificationOptions>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = NotificationAction.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType2 = $Create.Map($Create.Any, $Create.Any);
|
||||
@@ -0,0 +1,62 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Service represents the notifications service
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function CheckNotificationAuthorization(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(2216952893);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category: $models.NotificationCategory): $CancellablePromise<void> {
|
||||
return $Call.ByID(2917562919, category);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3956282340);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications(): $CancellablePromise<void> {
|
||||
return $Call.ByID(108821341);
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(975691940, identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3966653866, identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryID: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2032615554, categoryID);
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3729049703, identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public methods that delegate to the implementation.
|
||||
*/
|
||||
export function RequestNotificationAuthorization(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(3933442950);
|
||||
}
|
||||
|
||||
export function SendNotification(options: $models.NotificationOptions): $CancellablePromise<void> {
|
||||
return $Call.ByID(3968228732, options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options: $models.NotificationOptions): $CancellablePromise<void> {
|
||||
return $Call.ByID(1886542847, options);
|
||||
}
|
||||
12
client/ui/frontend/index.html
Normal file
12
client/ui/frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NetBird</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
33
client/ui/frontend/package.json
Normal file
33
client/ui/frontend/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "netbird-ui",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:dev": "tsc && vite build --minify false --mode development",
|
||||
"build": "tsc && vite build --mode production",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wailsio/runtime": "latest",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.469.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"tailwind-merge": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
1758
client/ui/frontend/pnpm-lock.yaml
generated
Normal file
1758
client/ui/frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
client/ui/frontend/postcss.config.js
Normal file
6
client/ui/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
client/ui/frontend/public/Inter-Medium.ttf
Normal file
BIN
client/ui/frontend/public/Inter-Medium.ttf
Normal file
Binary file not shown.
1
client/ui/frontend/public/react.svg
Normal file
1
client/ui/frontend/public/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
157
client/ui/frontend/public/style.css
Normal file
157
client/ui/frontend/public/style.css
Normal file
@@ -0,0 +1,157 @@
|
||||
:root {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("./Inter-Medium.ttf") format("truetype");
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 3em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #e80000aa);
|
||||
}
|
||||
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 1.5rem auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 1rem;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.input-box .btn:hover {
|
||||
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.input-box .input {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
color: black;
|
||||
background-color: rgba(240, 240, 240, 1);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.input-box .input:hover {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.input-box .input:focus {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
BIN
client/ui/frontend/public/wails.png
Normal file
BIN
client/ui/frontend/public/wails.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
34
client/ui/frontend/src/App.tsx
Normal file
34
client/ui/frontend/src/App.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||
import Layout from "./Layout";
|
||||
import Status from "./pages/Status";
|
||||
import Settings from "./pages/Settings";
|
||||
import Networks from "./pages/Networks";
|
||||
import Peers from "./pages/Peers";
|
||||
import Profiles from "./pages/Profiles";
|
||||
import Debug from "./pages/Debug";
|
||||
import Update from "./pages/Update";
|
||||
import QuickActions from "./pages/QuickActions";
|
||||
import LoginUrl from "./pages/LoginUrl";
|
||||
import Login from "./pages/Login";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/quick" element={<QuickActions />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/login-url" element={<LoginUrl />} />
|
||||
<Route path="/update" element={<Update />} />
|
||||
<Route element={<Layout />}>
|
||||
<Route index element={<Status />} />
|
||||
<Route path="peers" element={<Peers />} />
|
||||
<Route path="networks" element={<Networks />} />
|
||||
<Route path="profiles" element={<Profiles />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="debug" element={<Debug />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
45
client/ui/frontend/src/Layout.tsx
Normal file
45
client/ui/frontend/src/Layout.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { Activity, Bug, Network, Settings as SettingsIcon, Share2, Users } from "lucide-react";
|
||||
import { cn } from "./lib/cn";
|
||||
|
||||
const nav = [
|
||||
{ to: "/", label: "Status", icon: Activity, end: true },
|
||||
{ to: "/peers", label: "Peers", icon: Share2 },
|
||||
{ to: "/networks", label: "Networks", icon: Network },
|
||||
{ to: "/profiles", label: "Profiles", icon: Users },
|
||||
{ to: "/settings", label: "Settings", icon: SettingsIcon },
|
||||
{ to: "/debug", label: "Debug", icon: Bug },
|
||||
];
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<aside className="w-48 shrink-0 border-r border-nb-gray-200 bg-nb-gray-50 dark:border-nb-gray-800 dark:bg-nb-gray-940">
|
||||
<div className="px-4 py-5 text-lg font-semibold text-netbird">NetBird</div>
|
||||
<nav className="px-2">
|
||||
{nav.map(({ to, label, icon: Icon, end }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={end}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center gap-2 rounded-md px-3 py-2 text-sm",
|
||||
isActive
|
||||
? "bg-netbird/10 text-netbird"
|
||||
: "text-nb-gray-700 hover:bg-nb-gray-100 dark:text-nb-gray-300 dark:hover:bg-nb-gray-900",
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon className="h-4 w-4" strokeWidth={1.5} />
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
client/ui/frontend/src/components/Button.tsx
Normal file
42
client/ui/frontend/src/components/Button.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ButtonHTMLAttributes, forwardRef } from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
type Variant = "primary" | "secondary" | "ghost" | "danger";
|
||||
type Size = "sm" | "md";
|
||||
|
||||
const variants: Record<Variant, string> = {
|
||||
primary: "bg-netbird text-white hover:bg-netbird-500 disabled:bg-nb-gray-300",
|
||||
secondary:
|
||||
"bg-nb-gray-100 text-nb-gray-900 hover:bg-nb-gray-200 dark:bg-nb-gray-900 dark:text-nb-gray-50 dark:hover:bg-nb-gray-800",
|
||||
ghost:
|
||||
"bg-transparent text-nb-gray-700 hover:bg-nb-gray-100 dark:text-nb-gray-200 dark:hover:bg-nb-gray-900",
|
||||
danger: "bg-red-600 text-white hover:bg-red-500",
|
||||
};
|
||||
|
||||
const sizes: Record<Size, string> = {
|
||||
sm: "h-7 px-2 text-xs",
|
||||
md: "h-9 px-3 text-sm",
|
||||
};
|
||||
|
||||
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variant;
|
||||
size?: Size;
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, Props>(function Button(
|
||||
{ variant = "primary", size = "md", className, ...rest },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60",
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
14
client/ui/frontend/src/components/Card.tsx
Normal file
14
client/ui/frontend/src/components/Card.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { HTMLAttributes } from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export function Card({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border border-nb-gray-200 bg-white p-4 dark:border-nb-gray-800 dark:bg-nb-gray-925",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
33
client/ui/frontend/src/components/Input.tsx
Normal file
33
client/ui/frontend/src/components/Input.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { InputHTMLAttributes, forwardRef } from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, Props>(function Input(
|
||||
{ label, className, id, ...rest },
|
||||
ref,
|
||||
) {
|
||||
const inputId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="text-xs font-medium text-nb-gray-600 dark:text-nb-gray-300">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={inputId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-9 rounded-md border border-nb-gray-300 bg-white px-3 text-sm",
|
||||
"focus:border-netbird focus:outline-none focus:ring-1 focus:ring-netbird",
|
||||
"dark:border-nb-gray-700 dark:bg-nb-gray-925 dark:text-nb-gray-50",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
42
client/ui/frontend/src/components/Switch.tsx
Normal file
42
client/ui/frontend/src/components/Switch.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function Switch({ checked, onChange, disabled, label, description }: Props) {
|
||||
return (
|
||||
<label className={cn("flex items-start gap-3", disabled && "opacity-60")}>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={cn(
|
||||
"mt-0.5 inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors",
|
||||
checked ? "bg-netbird" : "bg-nb-gray-300 dark:bg-nb-gray-700",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
|
||||
checked ? "translate-x-4" : "translate-x-0.5",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{(label || description) && (
|
||||
<span className="flex flex-col">
|
||||
{label && <span className="text-sm font-medium">{label}</span>}
|
||||
{description && (
|
||||
<span className="text-xs text-nb-gray-500">{description}</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
40
client/ui/frontend/src/components/Tabs.tsx
Normal file
40
client/ui/frontend/src/components/Tabs.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ReactNode, useState } from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
interface Tab {
|
||||
value: string;
|
||||
label: string;
|
||||
content: ReactNode;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
tabs: Tab[];
|
||||
initial?: string;
|
||||
}
|
||||
|
||||
export function Tabs({ tabs, initial }: Props) {
|
||||
const [active, setActive] = useState(initial ?? tabs[0]?.value);
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex shrink-0 gap-1 border-b border-nb-gray-200 dark:border-nb-gray-800">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.value}
|
||||
onClick={() => setActive(t.value)}
|
||||
className={cn(
|
||||
"border-b-2 px-3 py-2 text-sm font-medium transition-colors",
|
||||
active === t.value
|
||||
? "border-netbird text-netbird"
|
||||
: "border-transparent text-nb-gray-500 hover:text-nb-gray-800 dark:hover:text-nb-gray-200",
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{tabs.find((t) => t.value === active)?.content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
client/ui/frontend/src/hooks/useStatus.ts
Normal file
36
client/ui/frontend/src/hooks/useStatus.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import { Peers } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import type { Status } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
|
||||
|
||||
const EVENT_STATUS = "netbird:status";
|
||||
|
||||
// useStatus loads the current daemon status once and re-renders whenever the
|
||||
// peers service emits a fresh snapshot over the Wails event bus.
|
||||
export function useStatus(): { status: Status | null; error: string | null } {
|
||||
const [status, setStatus] = useState<Status | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Peers.Get()
|
||||
.then((s) => {
|
||||
if (!cancelled) setStatus(s);
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
if (!cancelled) setError(String(e));
|
||||
});
|
||||
|
||||
const off = Events.On(EVENT_STATUS, (ev: { data: Status }) => {
|
||||
setStatus(ev.data);
|
||||
setError(null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
off();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { status, error };
|
||||
}
|
||||
17
client/ui/frontend/src/index.css
Normal file
17
client/ui/frontend/src/index.css
Normal file
@@ -0,0 +1,17 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-white text-nb-gray-900 antialiased;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@apply bg-nb-gray-950 text-nb-gray-50;
|
||||
}
|
||||
6
client/ui/frontend/src/lib/cn.ts
Normal file
6
client/ui/frontend/src/lib/cn.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
10
client/ui/frontend/src/main.tsx
Normal file
10
client/ui/frontend/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
105
client/ui/frontend/src/pages/Debug.tsx
Normal file
105
client/ui/frontend/src/pages/Debug.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState } from "react";
|
||||
import { Debug as DebugSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import type { DebugBundleResult } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
|
||||
import { Button } from "../components/Button";
|
||||
import { Input } from "../components/Input";
|
||||
import { Switch } from "../components/Switch";
|
||||
import { Card } from "../components/Card";
|
||||
|
||||
export default function Debug() {
|
||||
const [anonymize, setAnonymize] = useState(true);
|
||||
const [systemInfo, setSystemInfo] = useState(true);
|
||||
const [upload, setUpload] = useState(false);
|
||||
const [uploadUrl, setUploadUrl] = useState("");
|
||||
const [logFiles, setLogFiles] = useState(0);
|
||||
|
||||
const [running, setRunning] = useState(false);
|
||||
const [result, setResult] = useState<DebugBundleResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const run = async () => {
|
||||
setRunning(true);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
try {
|
||||
const r = await DebugSvc.Bundle({
|
||||
anonymize,
|
||||
systemInfo,
|
||||
uploadUrl: upload ? uploadUrl : "",
|
||||
logFileCount: logFiles,
|
||||
});
|
||||
setResult(r);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-6">
|
||||
<h1 className="text-xl font-semibold">Debug bundle</h1>
|
||||
|
||||
<Card className="space-y-4">
|
||||
<Switch
|
||||
checked={anonymize}
|
||||
onChange={setAnonymize}
|
||||
label="Anonymize"
|
||||
description="Replace IPs and identifiers in the bundle."
|
||||
/>
|
||||
<Switch
|
||||
checked={systemInfo}
|
||||
onChange={setSystemInfo}
|
||||
label="Include system information"
|
||||
/>
|
||||
<Switch
|
||||
checked={upload}
|
||||
onChange={setUpload}
|
||||
label="Upload on create"
|
||||
/>
|
||||
{upload && (
|
||||
<Input
|
||||
label="Upload URL"
|
||||
value={uploadUrl}
|
||||
onChange={(e) => setUploadUrl(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label="Log file count"
|
||||
type="number"
|
||||
value={logFiles}
|
||||
onChange={(e) => setLogFiles(Number(e.target.value))}
|
||||
/>
|
||||
<div className="pt-2">
|
||||
<Button onClick={run} disabled={running}>
|
||||
{running ? "Generating…" : "Create bundle"}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
|
||||
{result && (
|
||||
<Card>
|
||||
{result.path && (
|
||||
<p className="text-sm">
|
||||
<span className="text-nb-gray-500">Path:</span>{" "}
|
||||
<span className="font-mono">{result.path}</span>
|
||||
</p>
|
||||
)}
|
||||
{result.uploadedKey && (
|
||||
<p className="text-sm">
|
||||
<span className="text-nb-gray-500">Uploaded key:</span>{" "}
|
||||
<span className="font-mono">{result.uploadedKey}</span>
|
||||
</p>
|
||||
)}
|
||||
{result.uploadFailureReason && (
|
||||
<p className="text-sm text-red-500">
|
||||
Upload failed: {result.uploadFailureReason}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
159
client/ui/frontend/src/pages/Login.tsx
Normal file
159
client/ui/frontend/src/pages/Login.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ExternalLink, Loader2, AlertTriangle, X, RotateCcw } from "lucide-react";
|
||||
import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import { Button } from "../components/Button";
|
||||
|
||||
type Phase = "starting" | "browser" | "connecting" | "error";
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
const [phase, setPhase] = useState<Phase>("starting");
|
||||
const [verificationUri, setVerificationUri] = useState<string>("");
|
||||
const [errorMsg, setErrorMsg] = useState<string>("");
|
||||
// attempt is bumped every time the user asks for a fresh start, which
|
||||
// re-arms the useEffect below so the daemon's Login RPC is dialed again.
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
const cancelledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
cancelledRef.current = false;
|
||||
setPhase("starting");
|
||||
setVerificationUri("");
|
||||
setErrorMsg("");
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const result = await Connection.Login({
|
||||
profileName: "",
|
||||
username: "",
|
||||
managementUrl: "",
|
||||
setupKey: "",
|
||||
preSharedKey: "",
|
||||
hostname: "",
|
||||
hint: "",
|
||||
});
|
||||
if (cancelledRef.current) return;
|
||||
|
||||
if (result.needsSsoLogin) {
|
||||
const uri = result.verificationUriComplete || result.verificationUri;
|
||||
setVerificationUri(uri);
|
||||
setPhase("browser");
|
||||
if (uri) Connection.OpenURL(uri).catch(console.error);
|
||||
|
||||
await Connection.WaitSSOLogin({
|
||||
userCode: result.userCode,
|
||||
hostname: "",
|
||||
});
|
||||
if (cancelledRef.current) return;
|
||||
}
|
||||
|
||||
setPhase("connecting");
|
||||
await Connection.Up({ profileName: "", username: "" });
|
||||
if (cancelledRef.current) return;
|
||||
|
||||
navigate("/", { replace: true });
|
||||
} catch (e) {
|
||||
if (cancelledRef.current) return;
|
||||
setErrorMsg(String(e));
|
||||
setPhase("error");
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelledRef.current = true;
|
||||
};
|
||||
}, [navigate, attempt]);
|
||||
|
||||
// restart aborts any in-flight wait by toggling the cancellation flag,
|
||||
// tells the daemon to drop whatever it's holding (a stale WaitSSOLogin
|
||||
// can wedge the daemon for a previous UserCode), and then bumps attempt
|
||||
// so the effect re-runs with a clean slate.
|
||||
const restart = useCallback(async () => {
|
||||
cancelledRef.current = true;
|
||||
try {
|
||||
await Connection.Down();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setAttempt((n) => n + 1);
|
||||
}, []);
|
||||
|
||||
// Cancel must also tell the daemon to abandon the in-flight WaitSSOLogin.
|
||||
// Without Down(), the daemon stays parked on the OAuth flow's UserCode
|
||||
// forever; subsequent Login calls re-use the cached flow but the user has
|
||||
// no way out. Down() triggers the daemon's actCancel(), which unblocks
|
||||
// WaitSSOLogin with a context-canceled error so our promise settles.
|
||||
const cancel = useCallback(async () => {
|
||||
cancelledRef.current = true;
|
||||
try {
|
||||
await Connection.Down();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
navigate("/", { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
if (phase === "error") {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-6 text-center">
|
||||
<AlertTriangle className="h-8 w-8 text-red-500" strokeWidth={1.5} />
|
||||
<h1 className="text-xl font-semibold">Login failed</h1>
|
||||
<p className="max-w-sm break-words text-sm text-nb-gray-500">{errorMsg}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={restart}>
|
||||
<RotateCcw className="h-4 w-4" strokeWidth={1.5} /> Try again
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={cancel}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (phase === "browser") {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-6 text-center">
|
||||
<h1 className="text-xl font-semibold">Continue in your browser</h1>
|
||||
<p className="max-w-sm text-sm text-nb-gray-500">
|
||||
A browser tab should have opened. Sign in there — this window will
|
||||
continue automatically once you're done.
|
||||
</p>
|
||||
{verificationUri && (
|
||||
<Button onClick={() => Connection.OpenURL(verificationUri).catch(console.error)}>
|
||||
<ExternalLink className="h-4 w-4" strokeWidth={1.5} />
|
||||
Reopen URL
|
||||
</Button>
|
||||
)}
|
||||
<p className="max-w-sm break-all font-mono text-xs text-nb-gray-500">
|
||||
{verificationUri}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-sm text-nb-gray-500">
|
||||
<Loader2 className="h-4 w-4 animate-spin" strokeWidth={1.5} />
|
||||
Waiting for sign-in…
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="secondary" onClick={restart}>
|
||||
<RotateCcw className="h-4 w-4" strokeWidth={1.5} /> Restart
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={cancel}>
|
||||
<X className="h-4 w-4" strokeWidth={1.5} /> Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const message =
|
||||
phase === "connecting" ? "Bringing the connection up…" : "Starting login…";
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-netbird" strokeWidth={1.5} />
|
||||
<p className="text-sm text-nb-gray-500">{message}</p>
|
||||
<Button variant="ghost" onClick={cancel}>
|
||||
<X className="h-4 w-4" strokeWidth={1.5} /> Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
client/ui/frontend/src/pages/LoginUrl.tsx
Normal file
35
client/ui/frontend/src/pages/LoginUrl.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import { Button } from "../components/Button";
|
||||
|
||||
export default function LoginUrl() {
|
||||
const [url, setUrl] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.hash.split("?")[1] ?? "");
|
||||
setUrl(params.get("url") ?? "");
|
||||
}, []);
|
||||
|
||||
if (!url) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-6 text-sm text-nb-gray-500">
|
||||
No login URL provided.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-6 text-center">
|
||||
<h1 className="text-xl font-semibold">Continue in your browser</h1>
|
||||
<p className="max-w-sm text-sm text-nb-gray-500">
|
||||
Open the following URL to finish signing in.
|
||||
</p>
|
||||
<Button onClick={() => Connection.OpenURL(url).catch(console.error)}>
|
||||
<ExternalLink className="h-4 w-4" strokeWidth={1.5} />
|
||||
Open URL
|
||||
</Button>
|
||||
<p className="max-w-sm break-all font-mono text-xs text-nb-gray-500">{url}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
159
client/ui/frontend/src/pages/Networks.tsx
Normal file
159
client/ui/frontend/src/pages/Networks.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { Networks as NetworksSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import type { Network } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
|
||||
import { Button } from "../components/Button";
|
||||
import { Tabs } from "../components/Tabs";
|
||||
|
||||
export default function Networks() {
|
||||
const [routes, setRoutes] = useState<Network[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = await NetworksSvc.List();
|
||||
setRoutes(list);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const toggle = async (id: string, selected: boolean) => {
|
||||
try {
|
||||
if (selected) {
|
||||
await NetworksSvc.Deselect({ networkIds: [id], append: false, all: false });
|
||||
} else {
|
||||
await NetworksSvc.Select({ networkIds: [id], append: true, all: false });
|
||||
}
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const setAll = async (ids: string[], on: boolean) => {
|
||||
try {
|
||||
if (on) {
|
||||
await NetworksSvc.Select({ networkIds: ids, append: false, all: true });
|
||||
} else {
|
||||
await NetworksSvc.Deselect({ networkIds: ids, append: false, all: true });
|
||||
}
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const overlapping = useMemo(() => filterOverlapping(routes), [routes]);
|
||||
const exitNodes = useMemo(() => routes.filter((r) => r.range === "0.0.0.0/0"), [routes]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col p-6">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold">Networks</h1>
|
||||
<Button variant="secondary" size="sm" onClick={refresh} disabled={loading}>
|
||||
<RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} strokeWidth={1.5} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="mb-2 text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs
|
||||
tabs={[
|
||||
{
|
||||
value: "all",
|
||||
label: `All (${routes.length})`,
|
||||
content: <NetworkList routes={routes} onToggle={toggle} onSetAll={setAll} />,
|
||||
},
|
||||
{
|
||||
value: "overlap",
|
||||
label: `Overlapping (${overlapping.length})`,
|
||||
content: <NetworkList routes={overlapping} onToggle={toggle} onSetAll={setAll} />,
|
||||
},
|
||||
{
|
||||
value: "exit",
|
||||
label: `Exit-node (${exitNodes.length})`,
|
||||
content: <NetworkList routes={exitNodes} onToggle={toggle} onSetAll={setAll} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkList({
|
||||
routes,
|
||||
onToggle,
|
||||
onSetAll,
|
||||
}: {
|
||||
routes: Network[];
|
||||
onToggle: (id: string, selected: boolean) => void;
|
||||
onSetAll: (ids: string[], on: boolean) => void;
|
||||
}) {
|
||||
if (routes.length === 0) {
|
||||
return <p className="p-4 text-sm text-nb-gray-500">No networks.</p>;
|
||||
}
|
||||
const ids = routes.map((r) => r.id);
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex shrink-0 gap-2 border-b border-nb-gray-200 px-4 py-2 dark:border-nb-gray-800">
|
||||
<Button size="sm" variant="ghost" onClick={() => onSetAll(ids, true)}>
|
||||
Select all
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => onSetAll(ids, false)}>
|
||||
Deselect all
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="flex-1 overflow-auto divide-y divide-nb-gray-200 dark:divide-nb-gray-800">
|
||||
{routes.map((r) => (
|
||||
<li key={r.id} className="flex items-start gap-3 px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={r.selected}
|
||||
onChange={() => onToggle(r.id, r.selected)}
|
||||
className="mt-1 h-4 w-4 accent-netbird"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium">{r.id}</p>
|
||||
<p className="truncate font-mono text-xs text-nb-gray-500">{r.range}</p>
|
||||
{r.domains.length > 0 && (
|
||||
<p className="mt-0.5 truncate text-xs text-nb-gray-500">
|
||||
{r.domains.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function filterOverlapping(routes: Network[]): Network[] {
|
||||
const byRange = new Map<string, Network[]>();
|
||||
for (const r of routes) {
|
||||
if (r.domains.length > 0) continue;
|
||||
const arr = byRange.get(r.range) ?? [];
|
||||
arr.push(r);
|
||||
byRange.set(r.range, arr);
|
||||
}
|
||||
const out: Network[] = [];
|
||||
for (const arr of byRange.values()) {
|
||||
if (arr.length > 1) out.push(...arr);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
211
client/ui/frontend/src/pages/Peers.tsx
Normal file
211
client/ui/frontend/src/pages/Peers.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { ChevronDown, ChevronRight, Network, ShieldCheck, Zap } from "lucide-react";
|
||||
import { useStatus } from "../hooks/useStatus";
|
||||
import type { PeerStatus } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
|
||||
import { Card } from "../components/Card";
|
||||
import { Input } from "../components/Input";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export default function Peers() {
|
||||
const { status } = useStatus();
|
||||
const [filter, setFilter] = useState("");
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
|
||||
const peers = useMemo(() => {
|
||||
const all = status?.peers ?? [];
|
||||
if (!filter.trim()) return all;
|
||||
const q = filter.trim().toLowerCase();
|
||||
return all.filter(
|
||||
(p) =>
|
||||
p.fqdn.toLowerCase().includes(q) ||
|
||||
p.ip.toLowerCase().includes(q) ||
|
||||
p.networks.some((n) => n.toLowerCase().includes(q)),
|
||||
);
|
||||
}, [status?.peers, filter]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col p-6">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold">
|
||||
Peers
|
||||
<span className="ml-2 text-sm font-normal text-nb-gray-500">
|
||||
{status?.peers?.length ?? 0}
|
||||
</span>
|
||||
</h1>
|
||||
<div className="w-64">
|
||||
<Input
|
||||
placeholder="Filter by FQDN / IP / network"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{peers.length === 0 ? (
|
||||
<Card className="text-sm text-nb-gray-500">
|
||||
{status?.peers?.length === 0
|
||||
? "No peers visible from this client."
|
||||
: "No peers match the filter."}
|
||||
</Card>
|
||||
) : (
|
||||
<ul className="flex-1 divide-y divide-nb-gray-200 overflow-auto rounded-lg border border-nb-gray-200 dark:divide-nb-gray-800 dark:border-nb-gray-800">
|
||||
{peers.map((p) => (
|
||||
<PeerRow
|
||||
key={p.pubKey}
|
||||
peer={p}
|
||||
expanded={expanded === p.pubKey}
|
||||
onToggle={() => setExpanded(expanded === p.pubKey ? null : p.pubKey)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PeerRow({
|
||||
peer,
|
||||
expanded,
|
||||
onToggle,
|
||||
}: {
|
||||
peer: PeerStatus;
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-nb-gray-50 dark:hover:bg-nb-gray-940"
|
||||
>
|
||||
<ChevronIcon expanded={expanded} />
|
||||
<StateBadge state={peer.connStatus} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium">{peer.fqdn || "—"}</p>
|
||||
<p className="truncate font-mono text-xs text-nb-gray-500">{peer.ip}</p>
|
||||
</div>
|
||||
<RouteIcon relayed={peer.relayed} connected={peer.connStatus === "Connected"} />
|
||||
{peer.rosenpassEnabled && (
|
||||
<ShieldCheck className="h-4 w-4 text-green-500" strokeWidth={1.5} />
|
||||
)}
|
||||
<span className="w-16 text-right text-xs text-nb-gray-500">
|
||||
{peer.connStatus === "Connected" && peer.latencyMs > 0
|
||||
? `${peer.latencyMs} ms`
|
||||
: ""}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{expanded && <PeerDetails peer={peer} />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function PeerDetails({ peer }: { peer: PeerStatus }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2 bg-nb-gray-50 px-12 py-3 text-xs dark:bg-nb-gray-940">
|
||||
<Detail label="Public key" value={peer.pubKey} mono />
|
||||
<Detail label="Last handshake" value={fmtRelative(peer.lastHandshakeUnix)} />
|
||||
<Detail label="Status since" value={fmtRelative(peer.connStatusUpdateUnix)} />
|
||||
<Detail
|
||||
label="Bytes rx / tx"
|
||||
value={`${fmtBytes(peer.bytesRx)} / ${fmtBytes(peer.bytesTx)}`}
|
||||
/>
|
||||
<Detail
|
||||
label="Local candidate"
|
||||
value={
|
||||
peer.localIceCandidateType
|
||||
? `${peer.localIceCandidateType} (${peer.localIceCandidateEndpoint || "—"})`
|
||||
: "—"
|
||||
}
|
||||
mono
|
||||
/>
|
||||
<Detail
|
||||
label="Remote candidate"
|
||||
value={
|
||||
peer.remoteIceCandidateType
|
||||
? `${peer.remoteIceCandidateType} (${peer.remoteIceCandidateEndpoint || "—"})`
|
||||
: "—"
|
||||
}
|
||||
mono
|
||||
/>
|
||||
{peer.relayed && (
|
||||
<Detail label="Relay" value={peer.relayAddress || "—"} mono />
|
||||
)}
|
||||
{peer.networks.length > 0 && (
|
||||
<Detail label="Networks" value={peer.networks.join(", ")} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||
return (
|
||||
<div className="flex min-w-0 gap-2">
|
||||
<span className="shrink-0 text-nb-gray-500">{label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-0 truncate text-nb-gray-700 dark:text-nb-gray-200",
|
||||
mono && "font-mono",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChevronIcon({ expanded }: { expanded: boolean }) {
|
||||
const Icon = expanded ? ChevronDown : ChevronRight;
|
||||
return <Icon className="h-4 w-4 shrink-0 text-nb-gray-400" strokeWidth={1.5} />;
|
||||
}
|
||||
|
||||
function StateBadge({ state }: { state: string }) {
|
||||
const cls = "h-2 w-2 rounded-full shrink-0";
|
||||
switch (state) {
|
||||
case "Connected":
|
||||
return <span className={cn(cls, "bg-green-500")} title="Connected" />;
|
||||
case "Connecting":
|
||||
return <span className={cn(cls, "bg-netbird animate-pulse")} title="Connecting" />;
|
||||
case "Idle":
|
||||
return <span className={cn(cls, "bg-yellow-500")} title="Idle" />;
|
||||
default:
|
||||
return <span className={cn(cls, "bg-nb-gray-400")} title={state || "Disconnected"} />;
|
||||
}
|
||||
}
|
||||
|
||||
function RouteIcon({ relayed, connected }: { relayed: boolean; connected: boolean }) {
|
||||
if (!connected) {
|
||||
return <span className="w-4 shrink-0" />;
|
||||
}
|
||||
if (relayed) {
|
||||
return (
|
||||
<Network
|
||||
className="h-4 w-4 text-yellow-600"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<title>Relayed</title>
|
||||
</Network>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Zap className="h-4 w-4 text-green-600" strokeWidth={1.5}>
|
||||
<title>P2P</title>
|
||||
</Zap>
|
||||
);
|
||||
}
|
||||
|
||||
function fmtBytes(n: number): string {
|
||||
if (n < 1024) return `${n} B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||||
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
||||
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
function fmtRelative(unixSec: number): string {
|
||||
if (!unixSec) return "—";
|
||||
const ageSec = Math.max(0, Math.floor(Date.now() / 1000) - unixSec);
|
||||
if (ageSec < 60) return `${ageSec}s ago`;
|
||||
if (ageSec < 3600) return `${Math.floor(ageSec / 60)}m ago`;
|
||||
if (ageSec < 86400) return `${Math.floor(ageSec / 3600)}h ago`;
|
||||
return `${Math.floor(ageSec / 86400)}d ago`;
|
||||
}
|
||||
173
client/ui/frontend/src/pages/Profiles.tsx
Normal file
173
client/ui/frontend/src/pages/Profiles.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||||
import { Plus, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
Profiles as ProfilesSvc,
|
||||
Connection,
|
||||
} from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import type { Profile } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
|
||||
import { Button } from "../components/Button";
|
||||
import { Input } from "../components/Input";
|
||||
import { Card } from "../components/Card";
|
||||
|
||||
export default function Profiles() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [profiles, setProfiles] = useState<Profile[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [adding, setAdding] = useState(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const u = username || (await ProfilesSvc.Username());
|
||||
if (!username) setUsername(u);
|
||||
const list = await ProfilesSvc.List(u);
|
||||
setProfiles(list);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}, [username]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const select = async (name: string) => {
|
||||
try {
|
||||
await ProfilesSvc.Switch({ profileName: name, username });
|
||||
await Connection.Up({ profileName: name, username });
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const deregister = async (name: string) => {
|
||||
try {
|
||||
await Connection.Logout({ profileName: name, username });
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (name: string) => {
|
||||
if (name === "default") return;
|
||||
try {
|
||||
await ProfilesSvc.Remove({ profileName: name, username });
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold">Profiles</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={refresh}>
|
||||
<RefreshCw className="h-3.5 w-3.5" strokeWidth={1.5} /> Refresh
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => setAdding(true)}>
|
||||
<Plus className="h-3.5 w-3.5" strokeWidth={1.5} /> Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
|
||||
<div className="space-y-2">
|
||||
{profiles.map((p) => (
|
||||
<Card key={p.name} className="flex items-center gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="active-profile"
|
||||
checked={p.isActive}
|
||||
onChange={() => select(p.name)}
|
||||
className="h-4 w-4 accent-netbird"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{p.name}</p>
|
||||
{p.isActive && <p className="text-xs text-nb-gray-500">Active</p>}
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onClick={() => deregister(p.name)}>
|
||||
Deregister
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
disabled={p.name === "default"}
|
||||
onClick={() => remove(p.name)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
{profiles.length === 0 && (
|
||||
<p className="text-sm text-nb-gray-500">No profiles.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{adding && (
|
||||
<AddDialog
|
||||
username={username}
|
||||
onClose={() => setAdding(false)}
|
||||
onAdded={async () => {
|
||||
setAdding(false);
|
||||
await refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddDialog({
|
||||
username,
|
||||
onClose,
|
||||
onAdded,
|
||||
}: {
|
||||
username: string;
|
||||
onClose: () => void;
|
||||
onAdded: () => void;
|
||||
}) {
|
||||
const [name, setName] = useState("");
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
||||
const submit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
try {
|
||||
await ProfilesSvc.Add({ profileName: name.trim(), username });
|
||||
onAdded();
|
||||
} catch (e) {
|
||||
setErr(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<form
|
||||
onSubmit={submit}
|
||||
className="w-80 rounded-lg border border-nb-gray-200 bg-white p-4 shadow-lg dark:border-nb-gray-800 dark:bg-nb-gray-925"
|
||||
>
|
||||
<h2 className="mb-3 text-base font-semibold">New profile</h2>
|
||||
<Input
|
||||
autoFocus
|
||||
label="Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
{err && <p className="mt-2 text-xs text-red-500">{err}</p>}
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" size="sm">
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
client/ui/frontend/src/pages/QuickActions.tsx
Normal file
40
client/ui/frontend/src/pages/QuickActions.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { CheckCircle2, Circle, Loader2, Power } from "lucide-react";
|
||||
import { useStatus } from "../hooks/useStatus";
|
||||
import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import { Button } from "../components/Button";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export default function QuickActions() {
|
||||
const { status } = useStatus();
|
||||
const state = status?.status ?? "Disconnected";
|
||||
const connected = state === "Connected";
|
||||
const connecting = state === "Connecting";
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-6">
|
||||
<Icon state={state} />
|
||||
<p className="text-lg font-medium">{state}</p>
|
||||
{connected ? (
|
||||
<Button variant="secondary" onClick={() => Connection.Down()}>
|
||||
Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => Connection.Up({ profileName: "", username: "" })} disabled={connecting}>
|
||||
<Power className="h-4 w-4" strokeWidth={1.5} /> Connect
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Icon({ state }: { state: string }) {
|
||||
const cls = "h-12 w-12";
|
||||
switch (state) {
|
||||
case "Connected":
|
||||
return <CheckCircle2 className={cn(cls, "text-green-500")} strokeWidth={1.5} />;
|
||||
case "Connecting":
|
||||
return <Loader2 className={cn(cls, "animate-spin text-netbird")} strokeWidth={1.5} />;
|
||||
default:
|
||||
return <Circle className={cn(cls, "text-nb-gray-400")} strokeWidth={1.5} />;
|
||||
}
|
||||
}
|
||||
240
client/ui/frontend/src/pages/Settings.tsx
Normal file
240
client/ui/frontend/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Settings as SettingsSvc,
|
||||
Profiles as ProfilesSvc,
|
||||
} from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import type { Config } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
|
||||
import { Button } from "../components/Button";
|
||||
import { Input } from "../components/Input";
|
||||
import { Switch } from "../components/Switch";
|
||||
import { Tabs } from "../components/Tabs";
|
||||
|
||||
interface Ctx {
|
||||
cfg: Config;
|
||||
setField: <K extends keyof Config>(k: K, v: Config[K]) => void;
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [profile, setProfile] = useState("");
|
||||
const [cfg, setCfg] = useState<Config | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const u = await ProfilesSvc.Username();
|
||||
const active = await ProfilesSvc.GetActive();
|
||||
const profileName = active.profileName || "default";
|
||||
setUsername(u);
|
||||
setProfile(profileName);
|
||||
const c = await SettingsSvc.GetConfig({ profileName, username: u });
|
||||
setCfg(c);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const setField: Ctx["setField"] = (k, v) => {
|
||||
setCfg((c) => (c ? { ...c, [k]: v } : c));
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!cfg) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await SettingsSvc.SetConfig({
|
||||
profileName: profile,
|
||||
username,
|
||||
managementUrl: cfg.managementUrl,
|
||||
adminUrl: cfg.adminUrl,
|
||||
interfaceName: cfg.interfaceName,
|
||||
wireguardPort: cfg.wireguardPort,
|
||||
mtu: cfg.mtu,
|
||||
preSharedKey: cfg.preSharedKey,
|
||||
disableAutoConnect: cfg.disableAutoConnect,
|
||||
serverSshAllowed: cfg.serverSshAllowed,
|
||||
rosenpassEnabled: cfg.rosenpassEnabled,
|
||||
rosenpassPermissive: cfg.rosenpassPermissive,
|
||||
disableNotifications: cfg.disableNotifications,
|
||||
lazyConnectionEnabled: cfg.lazyConnectionEnabled,
|
||||
blockInbound: cfg.blockInbound,
|
||||
networkMonitor: cfg.networkMonitor,
|
||||
disableClientRoutes: cfg.disableClientRoutes,
|
||||
disableServerRoutes: cfg.disableServerRoutes,
|
||||
disableDns: cfg.disableDns,
|
||||
blockLanAccess: cfg.blockLanAccess,
|
||||
enableSshRoot: cfg.enableSshRoot,
|
||||
enableSshSftp: cfg.enableSshSftp,
|
||||
enableSshLocalPortForwarding: cfg.enableSshLocalPortForwarding,
|
||||
enableSshRemotePortForwarding: cfg.enableSshRemotePortForwarding,
|
||||
disableSshAuth: cfg.disableSshAuth,
|
||||
sshJwtCacheTtl: cfg.sshJwtCacheTtl,
|
||||
});
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!cfg) {
|
||||
return <div className="p-6 text-sm text-nb-gray-500">Loading…</div>;
|
||||
}
|
||||
|
||||
const ctx: Ctx = { cfg, setField };
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between border-b border-nb-gray-200 px-6 py-3 dark:border-nb-gray-800">
|
||||
<h1 className="text-xl font-semibold">Settings</h1>
|
||||
<Button onClick={save} disabled={saving}>
|
||||
{saving ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
{error && <p className="px-6 py-2 text-sm text-red-500">{error}</p>}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ value: "conn", label: "Connection", content: <ConnectionTab {...ctx} /> },
|
||||
{ value: "net", label: "Network", content: <NetworkTab {...ctx} /> },
|
||||
{ value: "ssh", label: "SSH", content: <SSHTab {...ctx} /> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectionTab({ cfg, setField }: Ctx) {
|
||||
return (
|
||||
<div className="grid max-w-2xl gap-4 p-6">
|
||||
<Input
|
||||
label="Management URL"
|
||||
value={cfg.managementUrl}
|
||||
onChange={(e) => setField("managementUrl", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Pre-shared key"
|
||||
type="password"
|
||||
value={cfg.preSharedKey}
|
||||
onChange={(e) => setField("preSharedKey", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Interface name"
|
||||
value={cfg.interfaceName}
|
||||
onChange={(e) => setField("interfaceName", e.target.value)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="WireGuard port"
|
||||
type="number"
|
||||
value={cfg.wireguardPort}
|
||||
onChange={(e) => setField("wireguardPort", Number(e.target.value))}
|
||||
/>
|
||||
<Input
|
||||
label="MTU"
|
||||
type="number"
|
||||
value={cfg.mtu}
|
||||
onChange={(e) => setField("mtu", Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={cfg.rosenpassEnabled}
|
||||
onChange={(v) => setField("rosenpassEnabled", v)}
|
||||
label="Rosenpass (post-quantum)"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.rosenpassPermissive}
|
||||
onChange={(v) => setField("rosenpassPermissive", v)}
|
||||
label="Rosenpass permissive mode"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkTab({ cfg, setField }: Ctx) {
|
||||
return (
|
||||
<div className="grid max-w-xl gap-4 p-6">
|
||||
<Switch
|
||||
checked={cfg.networkMonitor}
|
||||
onChange={(v) => setField("networkMonitor", v)}
|
||||
label="Network monitor"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.disableDns}
|
||||
onChange={(v) => setField("disableDns", v)}
|
||||
label="Disable DNS"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.disableClientRoutes}
|
||||
onChange={(v) => setField("disableClientRoutes", v)}
|
||||
label="Disable client routes"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.disableServerRoutes}
|
||||
onChange={(v) => setField("disableServerRoutes", v)}
|
||||
label="Disable server routes"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.blockLanAccess}
|
||||
onChange={(v) => setField("blockLanAccess", v)}
|
||||
label="Block LAN access"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.blockInbound}
|
||||
onChange={(v) => setField("blockInbound", v)}
|
||||
label="Block inbound connections"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SSHTab({ cfg, setField }: Ctx) {
|
||||
return (
|
||||
<div className="grid max-w-xl gap-4 p-6">
|
||||
<Switch
|
||||
checked={cfg.serverSshAllowed}
|
||||
onChange={(v) => setField("serverSshAllowed", v)}
|
||||
label="Server SSH allowed"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.enableSshRoot}
|
||||
onChange={(v) => setField("enableSshRoot", v)}
|
||||
label="SSH root login"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.enableSshSftp}
|
||||
onChange={(v) => setField("enableSshSftp", v)}
|
||||
label="SFTP"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.enableSshLocalPortForwarding}
|
||||
onChange={(v) => setField("enableSshLocalPortForwarding", v)}
|
||||
label="Local port forwarding"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.enableSshRemotePortForwarding}
|
||||
onChange={(v) => setField("enableSshRemotePortForwarding", v)}
|
||||
label="Remote port forwarding"
|
||||
/>
|
||||
<Switch
|
||||
checked={cfg.disableSshAuth}
|
||||
onChange={(v) => setField("disableSshAuth", v)}
|
||||
label="Disable SSH auth"
|
||||
/>
|
||||
<Input
|
||||
label="JWT cache TTL (seconds)"
|
||||
type="number"
|
||||
value={cfg.sshJwtCacheTtl}
|
||||
onChange={(e) => setField("sshJwtCacheTtl", Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
183
client/ui/frontend/src/pages/Status.tsx
Normal file
183
client/ui/frontend/src/pages/Status.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { CheckCircle2, Circle, Loader2, AlertTriangle, Power, LogIn } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useStatus } from "../hooks/useStatus";
|
||||
import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import type { SystemEvent } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
|
||||
import { Button } from "../components/Button";
|
||||
import { Card } from "../components/Card";
|
||||
import { cn } from "../lib/cn";
|
||||
|
||||
export default function Status() {
|
||||
const { status, error } = useStatus();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const connState = status?.status ?? "Disconnected";
|
||||
const connected = connState === "Connected";
|
||||
const connecting = connState === "Connecting";
|
||||
// The daemon reports "NeedsLogin" on a fresh install or after a session
|
||||
// expires; "SessionExpired" once a previously good session lapses. In both
|
||||
// cases Connect would fail without a fresh SSO login.
|
||||
const needsLogin = connState === "NeedsLogin" || connState === "SessionExpired";
|
||||
// Always offer Login while we aren't Connected — including Connecting,
|
||||
// because a stuck Login on the daemon leaves us in Connecting forever and
|
||||
// the user has no other way out. Disconnect is the manual unstick path.
|
||||
const showLogin = !connected;
|
||||
|
||||
const login = () => navigate("/login");
|
||||
const connect = () => Connection.Up({ profileName: "", username: "" }).catch(console.error);
|
||||
const disconnect = () => Connection.Down().catch(console.error);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<StateIcon state={connState} />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold leading-none">{connState}</h1>
|
||||
<p className="mt-1 text-sm text-nb-gray-500">
|
||||
{status?.local.fqdn || "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{needsLogin ? (
|
||||
<Button onClick={login}>
|
||||
<LogIn className="h-4 w-4" strokeWidth={1.5} /> Login
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={connect} disabled={connected || connecting}>
|
||||
<Power className="h-4 w-4" strokeWidth={1.5} /> Connect
|
||||
</Button>
|
||||
)}
|
||||
{showLogin && !needsLogin && (
|
||||
<Button onClick={login} variant="secondary">
|
||||
<LogIn className="h-4 w-4" strokeWidth={1.5} /> Login
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={disconnect} variant="secondary" disabled={!connected}>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4" strokeWidth={1.5} />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InfoCard label="Local IP" value={status?.local.ip || "—"} />
|
||||
<InfoCard label="Peers" value={String(status?.peers?.length ?? 0)} />
|
||||
<LinkCard label="Management" link={status?.management} />
|
||||
<LinkCard label="Signal" link={status?.signal} />
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-semibold text-nb-gray-700 dark:text-nb-gray-200">
|
||||
Recent events
|
||||
</h2>
|
||||
{(() => {
|
||||
const events = dedupEvents(status?.events ?? []).slice(0, 8);
|
||||
if (events.length === 0) {
|
||||
return <p className="text-sm text-nb-gray-500">No recent events.</p>;
|
||||
}
|
||||
return (
|
||||
<ul className="space-y-2 text-sm">
|
||||
{events.map((e, i) => (
|
||||
<li key={`${e.id}-${i}`} className="flex gap-2">
|
||||
<span className="shrink-0 font-mono text-xs text-nb-gray-500">
|
||||
{e.severity}
|
||||
</span>
|
||||
<span className="text-nb-gray-700 dark:text-nb-gray-200">
|
||||
{e.userMessage || e.message}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
})()}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StateIcon({ state }: { state: string }) {
|
||||
const cls = "h-7 w-7";
|
||||
switch (state) {
|
||||
case "Connected":
|
||||
return <CheckCircle2 className={cn(cls, "text-green-500")} strokeWidth={1.5} />;
|
||||
case "Connecting":
|
||||
return <Loader2 className={cn(cls, "animate-spin text-netbird")} strokeWidth={1.5} />;
|
||||
case "Error":
|
||||
return <AlertTriangle className={cn(cls, "text-red-500")} strokeWidth={1.5} />;
|
||||
default:
|
||||
return <Circle className={cn(cls, "text-nb-gray-400")} strokeWidth={1.5} />;
|
||||
}
|
||||
}
|
||||
|
||||
function InfoCard({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<Card>
|
||||
<p className="text-xs uppercase tracking-wide text-nb-gray-500">{label}</p>
|
||||
<p className="mt-1 truncate font-mono text-sm">{value}</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// dedupEvents collapses repeated daemon events that carry the same logical
|
||||
// content. The daemon emits one "new_version_available" event per check tick,
|
||||
// so its 10-event ring buffer fills with duplicates after a quiet hour. Same
|
||||
// goes for periodic "DNS unreachable" or "auth retry" events. We key by
|
||||
// message + a small set of identity-bearing metadata fields and keep the
|
||||
// newest occurrence (the events array is already in publish order).
|
||||
function dedupEvents(events: SystemEvent[]): SystemEvent[] {
|
||||
const seen = new Set<string>();
|
||||
const out: SystemEvent[] = [];
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
const e = events[i];
|
||||
const md = e.metadata ?? {};
|
||||
const key = [
|
||||
e.severity,
|
||||
e.category,
|
||||
e.userMessage || e.message,
|
||||
md["new_version_available"] ?? "",
|
||||
md["enforced"] ?? "",
|
||||
].join("|");
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[dedup]", { key, event: e });
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.unshift(e);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function LinkCard({
|
||||
label,
|
||||
link,
|
||||
}: {
|
||||
label: string;
|
||||
link?: { url: string; connected: boolean; error?: string };
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs uppercase tracking-wide text-nb-gray-500">{label}</p>
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
link?.connected ? "bg-green-500" : "bg-nb-gray-400",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1 truncate text-xs text-nb-gray-600 dark:text-nb-gray-300">
|
||||
{link?.url || "—"}
|
||||
</p>
|
||||
{link?.error && (
|
||||
<p className="mt-1 truncate text-xs text-red-500">{link.error}</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
135
client/ui/frontend/src/pages/Update.tsx
Normal file
135
client/ui/frontend/src/pages/Update.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Update as UpdateSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
|
||||
const TIMEOUT_MS = 15 * 60 * 1000;
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
// How long the daemon is allowed to be unreachable before we treat it as
|
||||
// "daemon went down for the upgrade, treat as success and quit". Mirrors
|
||||
// the legacy Fyne UI's branch in client/ui/update.go where a connection
|
||||
// failure during polling is taken as the success signal.
|
||||
const DAEMON_DOWN_GRACE_MS = 5000;
|
||||
|
||||
type Phase =
|
||||
| { kind: "running"; dots: number }
|
||||
| { kind: "timeout" }
|
||||
| { kind: "canceled" }
|
||||
| { kind: "failed"; message: string };
|
||||
|
||||
export default function Update() {
|
||||
const [phase, setPhase] = useState<Phase>({ kind: "running", dots: 1 });
|
||||
const phaseRef = useRef(phase);
|
||||
phaseRef.current = phase;
|
||||
|
||||
const version = new URLSearchParams(
|
||||
window.location.hash.split("?")[1] ?? "",
|
||||
).get("version");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const start = Date.now();
|
||||
let firstUnreachableAt: number | null = null;
|
||||
|
||||
UpdateSvc.Trigger().catch(() => {
|
||||
// The daemon may already be down (installer launched, daemon shutting
|
||||
// down). Don't treat as failure here; the poll loop's daemon-down
|
||||
// detection handles it.
|
||||
});
|
||||
|
||||
const dotTimer = setInterval(() => {
|
||||
if (cancelled) return;
|
||||
setPhase((p) =>
|
||||
p.kind === "running" ? { kind: "running", dots: (p.dots % 3) + 1 } : p,
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
const pollTimer = setInterval(async () => {
|
||||
if (cancelled) return;
|
||||
if (phaseRef.current.kind !== "running") return;
|
||||
|
||||
if (Date.now() - start > TIMEOUT_MS) {
|
||||
clearInterval(pollTimer);
|
||||
clearInterval(dotTimer);
|
||||
setPhase({ kind: "timeout" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await UpdateSvc.GetInstallerResult();
|
||||
firstUnreachableAt = null;
|
||||
if (r.success) {
|
||||
clearInterval(pollTimer);
|
||||
clearInterval(dotTimer);
|
||||
UpdateSvc.Quit();
|
||||
return;
|
||||
}
|
||||
if (r.errorMsg) {
|
||||
clearInterval(pollTimer);
|
||||
clearInterval(dotTimer);
|
||||
setPhase(mapInstallError(r.errorMsg));
|
||||
}
|
||||
} catch {
|
||||
// RPC failed. The daemon often goes away mid-upgrade — treat a
|
||||
// sustained outage as success and quit, matching the legacy UI.
|
||||
const now = Date.now();
|
||||
if (firstUnreachableAt === null) {
|
||||
firstUnreachableAt = now;
|
||||
} else if (now - firstUnreachableAt >= DAEMON_DOWN_GRACE_MS) {
|
||||
clearInterval(pollTimer);
|
||||
clearInterval(dotTimer);
|
||||
UpdateSvc.Quit();
|
||||
}
|
||||
}
|
||||
}, POLL_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(dotTimer);
|
||||
clearInterval(pollTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const versionLine = version
|
||||
? `Updating client to: ${version}.`
|
||||
: "Updating client.";
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<div className="space-y-3 text-center">
|
||||
<p className="whitespace-pre-line text-sm text-nb-gray-700 dark:text-nb-gray-200">
|
||||
{`Your client version is older than the auto-update version set in Management.\n${versionLine}`}
|
||||
</p>
|
||||
<p className="text-base font-medium">{statusText(phase)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function statusText(p: Phase): string {
|
||||
switch (p.kind) {
|
||||
case "running":
|
||||
return "Updating" + ".".repeat(p.dots);
|
||||
case "timeout":
|
||||
return "Update timed out. Please try again.";
|
||||
case "canceled":
|
||||
return "Update canceled.";
|
||||
case "failed":
|
||||
return "Update failed: " + p.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors mapInstallError in client/ui/update.go. The daemon's installer
|
||||
// surfaces error strings rather than typed errors, so the UI sniffs the
|
||||
// message to decide whether to show the timeout/canceled wording.
|
||||
function mapInstallError(msg: string): Phase {
|
||||
const m = msg.trim().toLowerCase();
|
||||
if (m === "") {
|
||||
return { kind: "failed", message: "unknown update error" };
|
||||
}
|
||||
if (m.includes("deadline exceeded") || m.includes("timeout")) {
|
||||
return { kind: "timeout" };
|
||||
}
|
||||
if (m.includes("canceled") || m.includes("cancelled")) {
|
||||
return { kind: "canceled" };
|
||||
}
|
||||
return { kind: "failed", message: msg };
|
||||
}
|
||||
1
client/ui/frontend/src/vite-env.d.ts
vendored
Normal file
1
client/ui/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
44
client/ui/frontend/tailwind.config.ts
Normal file
44
client/ui/frontend/tailwind.config.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
netbird: {
|
||||
DEFAULT: "#f68330",
|
||||
50: "#fff6ed",
|
||||
100: "#feecd6",
|
||||
200: "#ffd4a6",
|
||||
300: "#fab677",
|
||||
400: "#f68330",
|
||||
500: "#f46d1b",
|
||||
600: "#e55311",
|
||||
700: "#be3e10",
|
||||
800: "#973215",
|
||||
900: "#7a2b14",
|
||||
},
|
||||
"nb-gray": {
|
||||
DEFAULT: "#181A1D",
|
||||
50: "#f4f6f7",
|
||||
100: "#e4e7e9",
|
||||
200: "#cbd2d6",
|
||||
300: "#a3adb5",
|
||||
400: "#7c8994",
|
||||
500: "#616e79",
|
||||
600: "#535d67",
|
||||
700: "#474e57",
|
||||
800: "#3f444b",
|
||||
900: "#2e3238",
|
||||
925: "#1e2123",
|
||||
940: "#1c1e21",
|
||||
950: "#181a1d",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
||||
|
||||
export default config;
|
||||
25
client/ui/frontend/tsconfig.json
Normal file
25
client/ui/frontend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "bindings"],
|
||||
}
|
||||
12
client/ui/frontend/vite.config.ts
Normal file
12
client/ui/frontend/vite.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import wails from "@wailsio/runtime/plugins/vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), wails("./bindings")],
|
||||
server: {
|
||||
port: 9245,
|
||||
strictPort: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user