Compare commits
13 Commits
revert-eas
...
ui-refacto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0148d926d5 | ||
|
|
8f16a19b8f | ||
|
|
504dceedf3 | ||
|
|
e5474e199f | ||
|
|
db44848e2d | ||
|
|
9417ce3b3a | ||
|
|
8fc4265995 | ||
|
|
9c50819f20 | ||
|
|
6f0eff3ba0 | ||
|
|
f8745723fc | ||
|
|
154b81645a | ||
|
|
34167c8a16 | ||
|
|
d6f08e4840 |
2
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.1.2"
|
SIGN_PIPE_VER: "v0.1.4"
|
||||||
GORELEASER_VER: "v2.14.3"
|
GORELEASER_VER: "v2.14.3"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ Pop $0
|
|||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
Function .onInit
|
Function .onInit
|
||||||
|
SetRegView 64
|
||||||
StrCpy $INSTDIR "${INSTALL_DIR}"
|
StrCpy $INSTDIR "${INSTALL_DIR}"
|
||||||
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
||||||
${If} $R0 != ""
|
${If} $R0 != ""
|
||||||
@@ -214,6 +215,10 @@ ${If} $R0 != ""
|
|||||||
|
|
||||||
${EndIf}
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
Function un.onInit
|
||||||
|
SetRegView 64
|
||||||
|
FunctionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
Section -MainProgram
|
Section -MainProgram
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
@@ -228,6 +233,7 @@ Section -MainProgram
|
|||||||
!else
|
!else
|
||||||
File /r "..\\dist\\netbird_windows_amd64\\"
|
File /r "..\\dist\\netbird_windows_amd64\\"
|
||||||
!endif
|
!endif
|
||||||
|
File "..\\client\\ui\\assets\\netbird.png"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
@@ -247,9 +253,11 @@ WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
|||||||
; Create autostart registry entry based on checkbox
|
; Create autostart registry entry based on checkbox
|
||||||
DetailPrint "Autostart enabled: $AutostartEnabled"
|
DetailPrint "Autostart enabled: $AutostartEnabled"
|
||||||
${If} $AutostartEnabled == "1"
|
${If} $AutostartEnabled == "1"
|
||||||
WriteRegStr HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}" "$INSTDIR\${UI_APP_EXE}.exe"
|
WriteRegStr HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}" '"$INSTDIR\${UI_APP_EXE}.exe"'
|
||||||
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
|
||||||
${Else}
|
${Else}
|
||||||
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
DetailPrint "Autostart not enabled by user"
|
DetailPrint "Autostart not enabled by user"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
@@ -283,6 +291,8 @@ ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
|||||||
|
|
||||||
; Remove autostart registry entry
|
; Remove autostart registry entry
|
||||||
DetailPrint "Removing autostart registry entry if exists..."
|
DetailPrint "Removing autostart registry entry if exists..."
|
||||||
|
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
|
||||||
; Handle data deletion based on checkbox
|
; Handle data deletion based on checkbox
|
||||||
@@ -321,6 +331,7 @@ DetailPrint "Removing registry keys..."
|
|||||||
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
|
||||||
|
DeleteRegKey HKCU "Software\Classes\AppUserModelId\${APP_NAME}"
|
||||||
|
|
||||||
DetailPrint "Removing application directory from PATH..."
|
DetailPrint "Removing application directory from PATH..."
|
||||||
EnVar::SetHKLM
|
EnVar::SetHKLM
|
||||||
|
|||||||
@@ -333,6 +333,10 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
c.statusRecorder.MarkSignalConnected()
|
c.statusRecorder.MarkSignalConnected()
|
||||||
|
|
||||||
relayURLs, token := parseRelayInfo(loginResp)
|
relayURLs, token := parseRelayInfo(loginResp)
|
||||||
|
if override, ok := peer.OverrideRelayURLs(); ok {
|
||||||
|
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
|
||||||
|
relayURLs = override
|
||||||
|
}
|
||||||
peerConfig := loginResp.GetPeerConfig()
|
peerConfig := loginResp.GetPeerConfig()
|
||||||
|
|
||||||
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig, logPath)
|
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig, logPath)
|
||||||
|
|||||||
@@ -944,7 +944,12 @@ func (e *Engine) handleRelayUpdate(update *mgmProto.RelayConfig) error {
|
|||||||
return fmt.Errorf("update relay token: %w", err)
|
return fmt.Errorf("update relay token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.relayManager.UpdateServerURLs(update.Urls)
|
urls := update.Urls
|
||||||
|
if override, ok := peer.OverrideRelayURLs(); ok {
|
||||||
|
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
|
||||||
|
urls = override
|
||||||
|
}
|
||||||
|
e.relayManager.UpdateServerURLs(urls)
|
||||||
|
|
||||||
// Just in case the agent started with an MGM server where the relay was disabled but was later enabled.
|
// Just in case the agent started with an MGM server where the relay was disabled but was later enabled.
|
||||||
// We can ignore all errors because the guard will manage the reconnection retries.
|
// We can ignore all errors because the guard will manage the reconnection retries.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
|
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
|
||||||
|
EnvKeyNBHomeRelayServers = "NB_HOME_RELAY_SERVERS"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsForceRelayed() bool {
|
func IsForceRelayed() bool {
|
||||||
@@ -16,3 +17,28 @@ func IsForceRelayed() bool {
|
|||||||
}
|
}
|
||||||
return strings.EqualFold(os.Getenv(EnvKeyNBForceRelay), "true")
|
return strings.EqualFold(os.Getenv(EnvKeyNBForceRelay), "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OverrideRelayURLs returns the relay server URL list set in
|
||||||
|
// NB_HOME_RELAY_SERVERS (comma-separated) and a boolean indicating whether
|
||||||
|
// the override is active. When the env var is unset, the boolean is false
|
||||||
|
// and the caller should keep the list received from the management server.
|
||||||
|
// Intended for lab/debug scenarios where a peer must pin to a specific home
|
||||||
|
// relay regardless of what management offers.
|
||||||
|
func OverrideRelayURLs() ([]string, bool) {
|
||||||
|
raw := os.Getenv(EnvKeyNBHomeRelayServers)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
urls := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p != "" {
|
||||||
|
urls = append(urls, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(urls) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return urls, true
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,10 +18,17 @@
|
|||||||
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird.exe" KeyPath="yes" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird.exe" KeyPath="yes" />
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird-ui.exe">
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird-ui.exe">
|
||||||
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
|
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
|
||||||
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
|
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
|
||||||
|
</Shortcut>
|
||||||
|
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
|
||||||
|
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
|
||||||
|
</Shortcut>
|
||||||
</File>
|
</File>
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\wintun.dll" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\wintun.dll" />
|
||||||
|
<File Id="NetbirdToastIcon" Name="netbird.png" Source=".\client\ui\assets\netbird.png" />
|
||||||
<?if $(var.ArchSuffix) = "amd64" ?>
|
<?if $(var.ArchSuffix) = "amd64" ?>
|
||||||
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
|
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
|
||||||
<?endif ?>
|
<?endif ?>
|
||||||
@@ -46,8 +53,19 @@
|
|||||||
</Directory>
|
</Directory>
|
||||||
</StandardDirectory>
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<!-- Per-user component: HKCU keypath (auto GUID via "*"), separate from
|
||||||
|
the per-machine NetbirdFiles component to satisfy ICE57. -->
|
||||||
|
<StandardDirectory Id="ProgramMenuFolder">
|
||||||
|
<Component Id="NetbirdAumidRegistry" Guid="*">
|
||||||
|
<RegistryKey Root="HKCU" Key="Software\Classes\AppUserModelId\NetBird" ForceDeleteOnUninstall="yes">
|
||||||
|
<RegistryValue Name="InstalledByMSI" Type="integer" Value="1" KeyPath="yes" />
|
||||||
|
</RegistryKey>
|
||||||
|
</Component>
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
<ComponentGroup Id="NetbirdFilesComponent">
|
<ComponentGroup Id="NetbirdFilesComponent">
|
||||||
<ComponentRef Id="NetbirdFiles" />
|
<ComponentRef Id="NetbirdFiles" />
|
||||||
|
<ComponentRef Id="NetbirdAumidRegistry" />
|
||||||
</ComponentGroup>
|
</ComponentGroup>
|
||||||
|
|
||||||
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />
|
||||||
|
|||||||
7
client/ui-wails/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.task
|
||||||
|
bin
|
||||||
|
frontend/dist
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/bindings
|
||||||
|
build/linux/appimage/build
|
||||||
|
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
||||||
100
client/ui-wails/README.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# NetBird desktop UI (Wails3 + React)
|
||||||
|
|
||||||
|
Replaces `client/ui` (Fyne). One binary on Windows / macOS / Linux,
|
||||||
|
talks to the NetBird daemon over gRPC, renders a React frontend in a
|
||||||
|
WebView.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go ≥ 1.25, Node ≥ 20, **pnpm** (`corepack enable && corepack prepare pnpm@latest --activate`)
|
||||||
|
- `wails3` CLI: `go install github.com/wailsapp/wails/v3/cmd/wails3@latest`
|
||||||
|
- `task`: `go install github.com/go-task/task/v3/cmd/task@latest`
|
||||||
|
- A running NetBird daemon (default: `unix:///var/run/netbird.sock`,
|
||||||
|
Windows `tcp://127.0.0.1:41731`)
|
||||||
|
- Linux only: `libwebkit2gtk-4.1-dev`, `libgtk-3-dev`,
|
||||||
|
`libayatana-appindicator3-dev`
|
||||||
|
|
||||||
|
## Develop without rebuilding
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd client/ui-wails
|
||||||
|
task dev
|
||||||
|
```
|
||||||
|
|
||||||
|
`task dev` runs Vite (port 9245) + the Go binary + a `*.go` watcher.
|
||||||
|
Frontend edits hot-reload instantly. Go edits trigger a rebuild and
|
||||||
|
relaunch. Pass daemon flags after `--`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task dev -- --daemon-addr=tcp://127.0.0.1:41731
|
||||||
|
```
|
||||||
|
|
||||||
|
For pure UI work (no native window, fastest loop):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend && pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task build
|
||||||
|
```
|
||||||
|
|
||||||
|
Output in `bin/`. Frontend assets are embedded into the binary.
|
||||||
|
|
||||||
|
### Cross-compile Windows from Linux
|
||||||
|
|
||||||
|
Install the mingw-w64 toolchain once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install gcc-mingw-w64-x86-64 # Debian/Ubuntu
|
||||||
|
sudo dnf install mingw64-gcc # Fedora
|
||||||
|
sudo pacman -S mingw-w64-gcc # Arch
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=1 task windows:build
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `bin/netbird-ui.exe`. macOS cross-compile from Linux is not
|
||||||
|
supported (signing and notarization need a real Mac).
|
||||||
|
|
||||||
|
### Windows console build (logs in the terminal)
|
||||||
|
|
||||||
|
Default `windows:build` links the binary as a Windows GUI app, which
|
||||||
|
detaches from the launching console — `logrus` output, `fmt.Println`,
|
||||||
|
and panics go nowhere visible. To debug tray/event/daemon issues:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=1 task windows:build:console
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces `bin/netbird-ui-console.exe`. Run it from `cmd.exe` /
|
||||||
|
PowerShell / Windows Terminal and stdout/stderr land in that
|
||||||
|
terminal. Same flag works on a native Windows build (drop the
|
||||||
|
`CGO_ENABLED=1` if your toolchain already has it set).
|
||||||
|
|
||||||
|
## Regenerating bindings
|
||||||
|
|
||||||
|
When a Go service signature changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wails3 generate bindings
|
||||||
|
```
|
||||||
|
|
||||||
|
`task dev` does this automatically on `*.go` save.
|
||||||
|
|
||||||
|
## Tray icons
|
||||||
|
|
||||||
|
Source SVGs live in `assets/svg/` (state.svg + state-macos.svg). After editing
|
||||||
|
any SVG, rasterize to the PNGs the Go side embeds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task common:generate:tray:icons
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Inkscape. Commit the resulting `assets/*.png` files alongside the
|
||||||
|
SVG change so CI doesn't need Inkscape installed.
|
||||||
58
client/ui-wails/Taskfile.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ./build/Taskfile.yml
|
||||||
|
windows: ./build/windows/Taskfile.yml
|
||||||
|
darwin: ./build/darwin/Taskfile.yml
|
||||||
|
linux: ./build/linux/Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
APP_NAME: "netbird-ui"
|
||||||
|
BIN_DIR: "bin"
|
||||||
|
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:build"
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:package"
|
||||||
|
|
||||||
|
run:
|
||||||
|
summary: Runs the application
|
||||||
|
cmds:
|
||||||
|
- task: "{{OS}}:run"
|
||||||
|
|
||||||
|
dev:
|
||||||
|
summary: Runs the application in development mode
|
||||||
|
cmds:
|
||||||
|
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
|
||||||
|
|
||||||
|
setup:docker:
|
||||||
|
summary: Builds Docker image for cross-compilation (~800MB download)
|
||||||
|
cmds:
|
||||||
|
- task: common:setup:docker
|
||||||
|
|
||||||
|
build:server:
|
||||||
|
summary: Builds the application in server mode (no GUI, HTTP server only)
|
||||||
|
cmds:
|
||||||
|
- task: common:build:server
|
||||||
|
|
||||||
|
run:server:
|
||||||
|
summary: Runs the application in server mode
|
||||||
|
cmds:
|
||||||
|
- task: common:run:server
|
||||||
|
|
||||||
|
build:docker:
|
||||||
|
summary: Builds a Docker image for server mode deployment
|
||||||
|
cmds:
|
||||||
|
- task: common:build:docker
|
||||||
|
|
||||||
|
run:docker:
|
||||||
|
summary: Builds and runs the Docker image
|
||||||
|
cmds:
|
||||||
|
- task: common:run:docker
|
||||||
BIN
client/ui-wails/assets/netbird-systemtray-connected-dark.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connected-macos.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connected.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connecting-dark.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connecting-macos.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-connecting.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-disconnected-macos.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-disconnected.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-error-dark.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-error-macos.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-error.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
BIN
client/ui-wails/assets/netbird-systemtray-update-connected.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
14
client/ui-wails/assets/svg/_base.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!--
|
||||||
|
NetBird base mark, centered in a 32×32 viewBox with badge-friendly margins.
|
||||||
|
Preserved across every state icon as required by the design plan; state
|
||||||
|
badges sit on top in the bottom-right 12×12 area (x=18..30, y=18..30).
|
||||||
|
The mark itself is taken verbatim from dashboard/src/assets/netbird.svg
|
||||||
|
(three orange/red paths) and translated into the 32×32 grid.
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g id="netbird-mark" transform="translate(2 5) scale(0.8)">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 932 B |
17
client/ui-wails/assets/svg/appicon.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
App icon source. Rasterized to build/appicon.png by
|
||||||
|
`task common:generate:icons`, which then drives `wails3 generate icons`
|
||||||
|
to produce the per-platform .ico / .icns artifacts.
|
||||||
|
|
||||||
|
The mark fills ~90% of the canvas width (with vertical centering) so
|
||||||
|
Windows Explorer and macOS Finder render a recognisable bird at small
|
||||||
|
sizes. The mark's native aspect (31:23) is wider than tall, so width is
|
||||||
|
the binding dimension.
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<g transform="translate(37 170) scale(29.7)">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 997 B |
10
client/ui-wails/assets/svg/connected-macos.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5)" fill="black">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="black"/>
|
||||||
|
<path d="M22 25 L24 27 L28 23" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 723 B |
14
client/ui-wails/assets/svg/connected.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<!-- Mark fills the canvas. Badge overlaps the bottom-right corner so most
|
||||||
|
of the mark is still visible at 16 px tray sizes. -->
|
||||||
|
<g transform="translate(0.5 4.5) scale(1.0)">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
<!-- connected badge: green check, ~25% canvas, with a thin white halo so
|
||||||
|
the green disc reads cleanly on top of the orange mark. -->
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="#0E9F6E"/>
|
||||||
|
<path d="M22 25 L24 27 L28 23" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
9
client/ui-wails/assets/svg/connecting-macos.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5)" fill="black">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="none" stroke="black" stroke-width="1.8" stroke-dasharray="2.5 2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 678 B |
9
client/ui-wails/assets/svg/connecting.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5) scale(1.0)">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="none" stroke="#F68330" stroke-width="1.8" stroke-dasharray="2.5 2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 723 B |
10
client/ui-wails/assets/svg/disconnected-macos.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5)" fill="black" opacity="0.5">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="none" stroke="black" stroke-width="1.6"/>
|
||||||
|
<line x1="21.5" y1="25" x2="28.5" y2="25" stroke="black" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 745 B |
10
client/ui-wails/assets/svg/disconnected.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5) scale(1.0)" opacity="0.45">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="none" stroke="#7c8994" stroke-width="1.6"/>
|
||||||
|
<line x1="21.5" y1="25" x2="28.5" y2="25" stroke="#7c8994" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 793 B |
11
client/ui-wails/assets/svg/error-macos.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5)" fill="black" opacity="0.7">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="black"/>
|
||||||
|
<line x1="25" y1="21.5" x2="25" y2="26" stroke="white" stroke-width="1.8" stroke-linecap="round"/>
|
||||||
|
<circle cx="25" cy="28.4" r="1.0" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 761 B |
11
client/ui-wails/assets/svg/error.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5) scale(1.0)" opacity="0.7">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="#E02424"/>
|
||||||
|
<line x1="25" y1="21.5" x2="25" y2="26" stroke="white" stroke-width="1.8" stroke-linecap="round"/>
|
||||||
|
<circle cx="25" cy="28.4" r="1.0" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 806 B |
10
client/ui-wails/assets/svg/update-connected-macos.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5)" fill="black">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="black"/>
|
||||||
|
<path d="M25 22 L25 28 M22.5 24.5 L25 22 L27.5 24.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 745 B |
10
client/ui-wails/assets/svg/update-connected.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5) scale(1.0)">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="#1C64F2"/>
|
||||||
|
<path d="M25 22 L25 28 M22.5 24.5 L25 22 L27.5 24.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 790 B |
10
client/ui-wails/assets/svg/update-disconnected-macos.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5)" fill="black" opacity="0.5">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="black"/>
|
||||||
|
<path d="M25 22 L25 28 M22.5 24.5 L25 22 L27.5 24.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 759 B |
10
client/ui-wails/assets/svg/update-disconnected.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<g transform="translate(0.5 4.5) scale(1.0)" opacity="0.45">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="25" cy="25" r="7" fill="white"/>
|
||||||
|
<circle cx="25" cy="25" r="6" fill="#1C64F2"/>
|
||||||
|
<path d="M25 22 L25 28 M22.5 24.5 L25 22 L27.5 24.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 805 B |
295
client/ui-wails/build/Taskfile.yml
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
go:mod:tidy:
|
||||||
|
summary: Runs `go mod tidy`
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
install:frontend:deps:
|
||||||
|
summary: Install frontend dependencies
|
||||||
|
dir: frontend
|
||||||
|
sources:
|
||||||
|
- package.json
|
||||||
|
- pnpm-lock.yaml
|
||||||
|
generates:
|
||||||
|
- node_modules
|
||||||
|
preconditions:
|
||||||
|
- sh: pnpm --version
|
||||||
|
msg: "Looks like pnpm isn't installed. Install with: corepack enable && corepack prepare pnpm@latest --activate"
|
||||||
|
cmds:
|
||||||
|
- pnpm install
|
||||||
|
|
||||||
|
build:frontend:
|
||||||
|
label: build:frontend (DEV={{.DEV}})
|
||||||
|
summary: Build the frontend project
|
||||||
|
dir: frontend
|
||||||
|
sources:
|
||||||
|
- "**/*"
|
||||||
|
- exclude: node_modules/**/*
|
||||||
|
generates:
|
||||||
|
- dist/**/*
|
||||||
|
deps:
|
||||||
|
- task: install:frontend:deps
|
||||||
|
- task: generate:bindings
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
cmds:
|
||||||
|
- pnpm run {{.BUILD_COMMAND}}
|
||||||
|
env:
|
||||||
|
PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}'
|
||||||
|
vars:
|
||||||
|
BUILD_COMMAND: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}'
|
||||||
|
|
||||||
|
|
||||||
|
frontend:vendor:puppertino:
|
||||||
|
summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling
|
||||||
|
sources:
|
||||||
|
- frontend/public/puppertino/puppertino.css
|
||||||
|
generates:
|
||||||
|
- frontend/public/puppertino/puppertino.css
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p frontend/public/puppertino
|
||||||
|
# If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error.
|
||||||
|
if [ ! -f frontend/public/puppertino/puppertino.css ]; then
|
||||||
|
echo "No bundled Puppertino found. Attempting to fetch from GitHub..."
|
||||||
|
if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true
|
||||||
|
echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css"
|
||||||
|
else
|
||||||
|
echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css"
|
||||||
|
fi
|
||||||
|
# Ensure index.html includes Puppertino CSS and button classes
|
||||||
|
INDEX_HTML=frontend/index.html
|
||||||
|
if [ -f "$INDEX_HTML" ]; then
|
||||||
|
if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then
|
||||||
|
# Insert Puppertino link tag after style.css link
|
||||||
|
awk '
|
||||||
|
/href="\/style.css"\/?/ && !x { print; print " <link rel=\"stylesheet\" href=\"/puppertino/puppertino.css\"/>"; x=1; next }1
|
||||||
|
' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML"
|
||||||
|
fi
|
||||||
|
# Replace default .btn with Puppertino primary button classes if present
|
||||||
|
sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
generate:bindings:
|
||||||
|
label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}})
|
||||||
|
summary: Generates bindings for the frontend
|
||||||
|
deps:
|
||||||
|
- task: go:mod:tidy
|
||||||
|
sources:
|
||||||
|
- "**/*.[jt]s"
|
||||||
|
- exclude: frontend/**/*
|
||||||
|
- frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
|
||||||
|
- "**/*.go"
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
generates:
|
||||||
|
- frontend/bindings/**/*
|
||||||
|
cmds:
|
||||||
|
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts
|
||||||
|
|
||||||
|
generate:icons:
|
||||||
|
summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms).
|
||||||
|
dir: build
|
||||||
|
sources:
|
||||||
|
- "appicon.png"
|
||||||
|
- "appicon.icon"
|
||||||
|
generates:
|
||||||
|
- "darwin/icons.icns"
|
||||||
|
- "windows/icon.ico"
|
||||||
|
cmds:
|
||||||
|
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
|
||||||
|
|
||||||
|
generate:tray:icons:
|
||||||
|
summary: Rebuild Windows multi-res .ico files from the per-state PNGs.
|
||||||
|
desc: |
|
||||||
|
The colored tray PNGs (assets/netbird-systemtray-<state>.png) and the
|
||||||
|
macOS template variants are committed to the repo as the canonical
|
||||||
|
source. This task only regenerates the Windows multi-resolution .ico
|
||||||
|
files from those PNGs by downscaling each to 16/24/32/48 px and
|
||||||
|
packing them with icotool, so Shell_NotifyIcon picks the frame
|
||||||
|
matching the user's DPI instead of downscaling a single large PNG.
|
||||||
|
|
||||||
|
Run after replacing any of the colored PNGs (e.g. when copying a new
|
||||||
|
version of the icons from client/ui/assets). The SVG sources in
|
||||||
|
assets/svg/ are kept for reference but are not built by default.
|
||||||
|
dir: assets
|
||||||
|
sources:
|
||||||
|
- "netbird-systemtray-connected.png"
|
||||||
|
- "netbird-systemtray-disconnected.png"
|
||||||
|
- "netbird-systemtray-connecting.png"
|
||||||
|
- "netbird-systemtray-error.png"
|
||||||
|
- "netbird-systemtray-update-connected.png"
|
||||||
|
- "netbird-systemtray-update-disconnected.png"
|
||||||
|
generates:
|
||||||
|
- "netbird-systemtray-*.ico"
|
||||||
|
preconditions:
|
||||||
|
- sh: command -v magick >/dev/null 2>&1 || command -v convert >/dev/null 2>&1
|
||||||
|
msg: "ImageMagick is required to downscale PNGs (apt install imagemagick)"
|
||||||
|
- sh: command -v icotool >/dev/null 2>&1
|
||||||
|
msg: "icotool is required to pack tray .ico files (apt install icoutils)"
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
set -euo pipefail
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
resize=$(command -v magick || echo convert)
|
||||||
|
for state in connected disconnected connecting error update-connected update-disconnected; do
|
||||||
|
for sz in 16 24 32 48; do
|
||||||
|
"$resize" "netbird-systemtray-$state.png" -resize ${sz}x${sz} "$tmp/$state-$sz.png"
|
||||||
|
done
|
||||||
|
icotool -c -o "netbird-systemtray-$state.ico" \
|
||||||
|
"$tmp/$state-16.png" "$tmp/$state-24.png" "$tmp/$state-32.png" "$tmp/$state-48.png"
|
||||||
|
done
|
||||||
|
|
||||||
|
dev:frontend:
|
||||||
|
summary: Runs the frontend in development mode
|
||||||
|
dir: frontend
|
||||||
|
deps:
|
||||||
|
- task: install:frontend:deps
|
||||||
|
cmds:
|
||||||
|
- pnpm exec vite --port {{.VITE_PORT}} --strictPort
|
||||||
|
|
||||||
|
update:build-assets:
|
||||||
|
summary: Updates the build assets
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
|
||||||
|
|
||||||
|
build:server:
|
||||||
|
summary: Builds the application in server mode (no GUI, HTTP server only)
|
||||||
|
desc: |
|
||||||
|
Builds the application with the server build tag enabled.
|
||||||
|
Server mode runs as a pure HTTP server without native GUI dependencies.
|
||||||
|
Usage: task build:server
|
||||||
|
deps:
|
||||||
|
- task: build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
cmds:
|
||||||
|
- go build -tags server {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: "{{.BUILD_FLAGS}}"
|
||||||
|
|
||||||
|
run:server:
|
||||||
|
summary: Builds and runs the application in server mode
|
||||||
|
deps:
|
||||||
|
- task: build:server
|
||||||
|
cmds:
|
||||||
|
- ./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}
|
||||||
|
|
||||||
|
build:docker:
|
||||||
|
summary: Builds a Docker image for server mode deployment
|
||||||
|
desc: |
|
||||||
|
Creates a minimal Docker image containing the server mode binary.
|
||||||
|
The image is based on distroless for security and small size.
|
||||||
|
Usage: task build:docker [TAG=myapp:latest]
|
||||||
|
cmds:
|
||||||
|
- docker build -t {{.TAG | default (printf "%s:latest" .APP_NAME)}} -f build/docker/Dockerfile.server .
|
||||||
|
vars:
|
||||||
|
TAG: "{{.TAG}}"
|
||||||
|
preconditions:
|
||||||
|
- sh: docker info > /dev/null 2>&1
|
||||||
|
msg: "Docker is required. Please install Docker first."
|
||||||
|
- sh: test -f build/docker/Dockerfile.server
|
||||||
|
msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it."
|
||||||
|
|
||||||
|
run:docker:
|
||||||
|
summary: Builds and runs the Docker image
|
||||||
|
desc: |
|
||||||
|
Builds the Docker image and runs it, exposing port 8080.
|
||||||
|
Usage: task run:docker [TAG=myapp:latest] [PORT=8080]
|
||||||
|
Note: The internal container port is always 8080. The PORT variable
|
||||||
|
only changes the host port mapping. Ensure your app uses port 8080
|
||||||
|
or modify the Dockerfile to match your ServerOptions.Port setting.
|
||||||
|
deps:
|
||||||
|
- task: build:docker
|
||||||
|
vars:
|
||||||
|
TAG:
|
||||||
|
ref: .TAG
|
||||||
|
cmds:
|
||||||
|
- docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}}
|
||||||
|
vars:
|
||||||
|
TAG: "{{.TAG}}"
|
||||||
|
PORT: "{{.PORT}}"
|
||||||
|
|
||||||
|
setup:docker:
|
||||||
|
summary: Builds Docker image for cross-compilation (~800MB download)
|
||||||
|
desc: |
|
||||||
|
Builds the Docker image needed for cross-compiling to any platform.
|
||||||
|
Run this once to enable cross-platform builds from any OS.
|
||||||
|
cmds:
|
||||||
|
- docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/
|
||||||
|
preconditions:
|
||||||
|
- sh: docker info > /dev/null 2>&1
|
||||||
|
msg: "Docker is required. Please install Docker first."
|
||||||
|
|
||||||
|
ios:device:list:
|
||||||
|
summary: Lists connected iOS devices (UDIDs)
|
||||||
|
cmds:
|
||||||
|
- xcrun xcdevice list
|
||||||
|
|
||||||
|
ios:run:device:
|
||||||
|
summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl)
|
||||||
|
vars:
|
||||||
|
PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/<YourProject>.xcodeproj
|
||||||
|
SCHEME: '{{.SCHEME}}' # e.g., ios.dev
|
||||||
|
CONFIG: '{{.CONFIG | default "Debug"}}'
|
||||||
|
DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}'
|
||||||
|
UDID: '{{.UDID}}' # from `task ios:device:list`
|
||||||
|
BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev
|
||||||
|
TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing
|
||||||
|
preconditions:
|
||||||
|
- sh: xcrun -f xcodebuild
|
||||||
|
msg: "xcodebuild not found. Please install Xcode."
|
||||||
|
- sh: xcrun -f devicectl
|
||||||
|
msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)."
|
||||||
|
- sh: test -n '{{.PROJECT}}'
|
||||||
|
msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)."
|
||||||
|
- sh: test -n '{{.SCHEME}}'
|
||||||
|
msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)."
|
||||||
|
- sh: test -n '{{.UDID}}'
|
||||||
|
msg: "Set UDID to your device UDID (see: task ios:device:list)."
|
||||||
|
- sh: test -n '{{.BUNDLE_ID}}'
|
||||||
|
msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)."
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}"
|
||||||
|
XCB_ARGS=(
|
||||||
|
-project "{{.PROJECT}}"
|
||||||
|
-scheme "{{.SCHEME}}"
|
||||||
|
-configuration "{{.CONFIG}}"
|
||||||
|
-destination "id={{.UDID}}"
|
||||||
|
-derivedDataPath "{{.DERIVED}}"
|
||||||
|
-allowProvisioningUpdates
|
||||||
|
-allowProvisioningDeviceRegistration
|
||||||
|
)
|
||||||
|
# Optionally inject signing identifiers if provided
|
||||||
|
if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi
|
||||||
|
if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi
|
||||||
|
xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true
|
||||||
|
# If xcpretty isn't installed, run without it
|
||||||
|
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
||||||
|
xcodebuild "${XCB_ARGS[@]}" build
|
||||||
|
fi
|
||||||
|
# Find built .app
|
||||||
|
APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1)
|
||||||
|
if [ -z "$APP_PATH" ]; then
|
||||||
|
echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Installing: $APP_PATH"
|
||||||
|
xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH"
|
||||||
|
echo "Launching: {{.BUNDLE_ID}}"
|
||||||
|
xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!--
|
||||||
|
macOS Icon Composer source. icon.json references this SVG by name and
|
||||||
|
applies its own scale/translation/fill, so we leave the artwork in its
|
||||||
|
native 31×23 viewBox.
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 31 23">
|
||||||
|
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||||
|
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||||
|
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 732 B |
51
client/ui-wails/build/appicon.icon/icon.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"fill" : {
|
||||||
|
"automatic-gradient" : "extended-gray:1.00000,1.00000"
|
||||||
|
},
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"fill-specializations" : [
|
||||||
|
{
|
||||||
|
"appearance" : "dark",
|
||||||
|
"value" : {
|
||||||
|
"solid" : "srgb:0.92143,0.92145,0.92144,1.00000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "tinted",
|
||||||
|
"value" : {
|
||||||
|
"solid" : "srgb:0.83742,0.83744,0.83743,1.00000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"image-name" : "wails_icon_vector.svg",
|
||||||
|
"name" : "wails_icon_vector",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 1.25,
|
||||||
|
"translation-in-points" : [
|
||||||
|
36.890625,
|
||||||
|
4.96875
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"specular" : true,
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
client/ui-wails/build/appicon.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
78
client/ui-wails/build/config.yml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# This file contains the configuration for this project.
|
||||||
|
# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
|
||||||
|
# Note that this will overwrite any changes you have made to the assets.
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
# This information is used to generate the build assets.
|
||||||
|
info:
|
||||||
|
companyName: "My Company" # The name of the company
|
||||||
|
productName: "My Product" # The name of the application
|
||||||
|
productIdentifier: "com.mycompany.myproduct" # The unique product identifier
|
||||||
|
description: "A program that does X" # The application description
|
||||||
|
copyright: "(c) 2025, My Company" # Copyright text
|
||||||
|
comments: "Some Product Comments" # Comments
|
||||||
|
version: "0.0.1" # The application version
|
||||||
|
# cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
|
||||||
|
# # Should match the name of your .icon file without the extension
|
||||||
|
# # If not set and Assets.car exists, defaults to "appicon"
|
||||||
|
|
||||||
|
# iOS build configuration (uncomment to customise iOS project generation)
|
||||||
|
# Note: Keys under `ios` OVERRIDE values under `info` when set.
|
||||||
|
# ios:
|
||||||
|
# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier)
|
||||||
|
# bundleID: "com.mycompany.myproduct"
|
||||||
|
# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName)
|
||||||
|
# displayName: "My Product"
|
||||||
|
# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion)
|
||||||
|
# version: "0.0.1"
|
||||||
|
# # The company/organisation name for templates and project settings
|
||||||
|
# company: "My Company"
|
||||||
|
# # Additional comments to embed in Info.plist metadata
|
||||||
|
# comments: "Some Product Comments"
|
||||||
|
|
||||||
|
# Dev mode configuration
|
||||||
|
dev_mode:
|
||||||
|
root_path: .
|
||||||
|
log_level: warn
|
||||||
|
debounce: 1000
|
||||||
|
ignore:
|
||||||
|
dir:
|
||||||
|
- .git
|
||||||
|
- node_modules
|
||||||
|
- frontend
|
||||||
|
- bin
|
||||||
|
file:
|
||||||
|
- .DS_Store
|
||||||
|
- .gitignore
|
||||||
|
- .gitkeep
|
||||||
|
watched_extension:
|
||||||
|
- "*.go"
|
||||||
|
- "*.js" # Watch for changes to JS/TS files included using the //wails:include directive.
|
||||||
|
- "*.ts" # The frontend directory will be excluded entirely by the setting above.
|
||||||
|
git_ignore: true
|
||||||
|
executes:
|
||||||
|
- cmd: wails3 build DEV=true
|
||||||
|
type: blocking
|
||||||
|
- cmd: wails3 task common:dev:frontend
|
||||||
|
type: background
|
||||||
|
- cmd: wails3 task run
|
||||||
|
type: primary
|
||||||
|
|
||||||
|
# File Associations
|
||||||
|
# More information at: https://v3.wails.io/noit/done/yet
|
||||||
|
fileAssociations:
|
||||||
|
# - ext: wails
|
||||||
|
# name: Wails
|
||||||
|
# description: Wails Application File
|
||||||
|
# iconName: wailsFileIcon
|
||||||
|
# role: Editor
|
||||||
|
# - ext: jpg
|
||||||
|
# name: JPEG
|
||||||
|
# description: Image File
|
||||||
|
# iconName: jpegFileIcon
|
||||||
|
# role: Editor
|
||||||
|
# mimeType: image/jpeg # (optional)
|
||||||
|
|
||||||
|
# Other data
|
||||||
|
other:
|
||||||
|
- name: My Other Data
|
||||||
34
client/ui-wails/build/darwin/Info.dev.plist
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>NetBird</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>netbird-ui</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.netbird.client</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.0.1</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>This is a comment</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.0.1</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icons</string>
|
||||||
|
<key>CFBundleIconName</key>
|
||||||
|
<string>appicon</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.15.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>© 2026, My Company</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
29
client/ui-wails/build/darwin/Info.plist
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>NetBird</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>netbird-ui</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.netbird.client</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.0.1</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>This is a comment</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.0.1</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icons</string>
|
||||||
|
<key>CFBundleIconName</key>
|
||||||
|
<string>appicon</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.15.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>© 2026, My Company</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
208
client/ui-wails/build/darwin/Taskfile.yml
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Signing configuration - edit these values for your project
|
||||||
|
# SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)"
|
||||||
|
# KEYCHAIN_PROFILE: "my-notarize-profile"
|
||||||
|
# ENTITLEMENTS: "build/darwin/entitlements.plist"
|
||||||
|
|
||||||
|
# Docker image for cross-compilation (used when building on non-macOS)
|
||||||
|
CROSS_IMAGE: wails-cross
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application
|
||||||
|
cmds:
|
||||||
|
- task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}'
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH}}'
|
||||||
|
DEV: '{{.DEV}}'
|
||||||
|
OUTPUT: '{{.OUTPUT}}'
|
||||||
|
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
|
||||||
|
vars:
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
|
||||||
|
build:native:
|
||||||
|
summary: Builds the application natively on macOS
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
DEV:
|
||||||
|
ref: .DEV
|
||||||
|
- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
env:
|
||||||
|
GOOS: darwin
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
CGO_CFLAGS: "-mmacosx-version-min=10.15"
|
||||||
|
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||||
|
|
||||||
|
build:docker:
|
||||||
|
summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts)
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:build:frontend
|
||||||
|
- task: common:generate:icons
|
||||||
|
preconditions:
|
||||||
|
- sh: docker info > /dev/null 2>&1
|
||||||
|
msg: "Docker is required for cross-compilation. Please install Docker."
|
||||||
|
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
|
||||||
|
msg: |
|
||||||
|
Docker image '{{.CROSS_IMAGE}}' not found.
|
||||||
|
Build it first: wails3 task setup:docker
|
||||||
|
cmds:
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}}
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
|
||||||
|
- mkdir -p {{.BIN_DIR}}
|
||||||
|
- mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
|
||||||
|
vars:
|
||||||
|
DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
# Mount Go module cache for faster builds
|
||||||
|
GO_CACHE_MOUNT:
|
||||||
|
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
|
||||||
|
# Extract replace directives from go.mod and create -v mounts for each
|
||||||
|
# Handles both relative (=> ../) and absolute (=> /) paths
|
||||||
|
REPLACE_MOUNTS:
|
||||||
|
sh: |
|
||||||
|
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
|
||||||
|
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
|
||||||
|
# Convert relative paths to absolute
|
||||||
|
if [ "${path#/}" = "$path" ]; then
|
||||||
|
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
|
||||||
|
fi
|
||||||
|
# Only mount if directory exists
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
echo "-v $path:$path:ro"
|
||||||
|
fi
|
||||||
|
done | tr '\n' ' '
|
||||||
|
|
||||||
|
build:universal:
|
||||||
|
summary: Builds darwin universal binary (arm64 + amd64)
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
ARCH: amd64
|
||||||
|
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
ARCH: arm64
|
||||||
|
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
cmds:
|
||||||
|
- task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}'
|
||||||
|
|
||||||
|
build:universal:lipo:native:
|
||||||
|
summary: Creates universal binary using native lipo (macOS)
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
- rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
|
||||||
|
build:universal:lipo:go:
|
||||||
|
summary: Creates universal binary using wails3 tool lipo (Linux/Windows)
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
- rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages the application into a `.app` bundle
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: create:app:bundle
|
||||||
|
|
||||||
|
package:universal:
|
||||||
|
summary: Packages darwin universal binary (arm64 + amd64)
|
||||||
|
deps:
|
||||||
|
- task: build:universal
|
||||||
|
cmds:
|
||||||
|
- task: create:app:bundle
|
||||||
|
|
||||||
|
|
||||||
|
create:app:bundle:
|
||||||
|
summary: Creates an `.app` bundle
|
||||||
|
cmds:
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
|
||||||
|
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
|
||||||
|
- |
|
||||||
|
if [ -f build/darwin/Assets.car ]; then
|
||||||
|
cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
|
||||||
|
fi
|
||||||
|
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
|
||||||
|
- cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents"
|
||||||
|
- task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}'
|
||||||
|
|
||||||
|
codesign:adhoc:
|
||||||
|
summary: Ad-hoc signs the app bundle (macOS only)
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
|
||||||
|
|
||||||
|
codesign:skip:
|
||||||
|
summary: Skips codesigning when cross-compiling
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."'
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
|
||||||
|
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
|
||||||
|
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
|
||||||
|
- |
|
||||||
|
if [ -f build/darwin/Assets.car ]; then
|
||||||
|
cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
|
||||||
|
fi
|
||||||
|
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
|
||||||
|
- cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist"
|
||||||
|
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'
|
||||||
|
|
||||||
|
sign:
|
||||||
|
summary: Signs the application bundle with Developer ID
|
||||||
|
desc: |
|
||||||
|
Signs the .app bundle for distribution.
|
||||||
|
Configure SIGN_IDENTITY in the vars section at the top of this file.
|
||||||
|
deps:
|
||||||
|
- task: package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
|
||||||
|
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
|
||||||
|
|
||||||
|
sign:notarize:
|
||||||
|
summary: Signs and notarizes the application bundle
|
||||||
|
desc: |
|
||||||
|
Signs the .app bundle and submits it for notarization.
|
||||||
|
Configure SIGN_IDENTITY and KEYCHAIN_PROFILE in the vars section at the top of this file.
|
||||||
|
|
||||||
|
Setup (one-time):
|
||||||
|
wails3 signing credentials --apple-id "you@email.com" --team-id "TEAMID" --password "app-specific-password" --profile "my-profile"
|
||||||
|
deps:
|
||||||
|
- task: package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
|
||||||
|
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
|
||||||
|
- sh: '[ -n "{{.KEYCHAIN_PROFILE}}" ]'
|
||||||
|
msg: "KEYCHAIN_PROFILE is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
|
||||||
BIN
client/ui-wails/build/darwin/icons.icns
Normal file
203
client/ui-wails/build/docker/Dockerfile.cross
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# Cross-compile Wails v3 apps to any platform
|
||||||
|
#
|
||||||
|
# Darwin: Zig + macOS SDK
|
||||||
|
# Linux: Native GCC when host matches target, Zig for cross-arch
|
||||||
|
# Windows: Zig + bundled mingw
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker build -t wails-cross -f Dockerfile.cross .
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross darwin arm64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross darwin amd64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross linux amd64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross linux arm64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross windows amd64
|
||||||
|
# docker run --rm -v $(pwd):/app wails-cross windows arm64
|
||||||
|
|
||||||
|
FROM golang:1.25-bookworm
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
# Install base tools, GCC, and GTK/WebKit dev packages
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl xz-utils nodejs npm pkg-config gcc libc6-dev \
|
||||||
|
libgtk-3-dev libwebkit2gtk-4.1-dev \
|
||||||
|
libgtk-4-dev libwebkitgtk-6.0-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Zig - automatically selects correct binary for host architecture
|
||||||
|
ARG ZIG_VERSION=0.14.0
|
||||||
|
RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
|
curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" \
|
||||||
|
| tar -xJ -C /opt \
|
||||||
|
&& ln -s /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig
|
||||||
|
|
||||||
|
# Download macOS SDK (required for darwin targets)
|
||||||
|
ARG MACOS_SDK_VERSION=14.5
|
||||||
|
RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \
|
||||||
|
| tar -xJ -C /opt \
|
||||||
|
&& mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk
|
||||||
|
|
||||||
|
ENV MACOS_SDK_PATH=/opt/macos-sdk
|
||||||
|
|
||||||
|
# Create Zig CC wrappers for cross-compilation targets
|
||||||
|
# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch)
|
||||||
|
|
||||||
|
# Darwin arm64
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
-mmacosx-version-min=*) ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-darwin-arm64
|
||||||
|
|
||||||
|
# Darwin amd64
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
-mmacosx-version-min=*) ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-darwin-amd64
|
||||||
|
|
||||||
|
# Windows amd64 - uses Zig's bundled mingw
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
-Wl,*) ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -target x86_64-windows-gnu $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-windows-amd64
|
||||||
|
|
||||||
|
# Windows arm64 - uses Zig's bundled mingw
|
||||||
|
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64
|
||||||
|
#!/bin/sh
|
||||||
|
ARGS=""
|
||||||
|
SKIP_NEXT=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ $SKIP_NEXT -eq 1 ]; then
|
||||||
|
SKIP_NEXT=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-target) SKIP_NEXT=1 ;;
|
||||||
|
-Wl,*) ;;
|
||||||
|
*) ARGS="$ARGS $arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec zig cc -target aarch64-windows-gnu $ARGS
|
||||||
|
ZIGWRAP
|
||||||
|
RUN chmod +x /usr/local/bin/zcc-windows-arm64
|
||||||
|
|
||||||
|
# Build script
|
||||||
|
COPY <<'SCRIPT' /usr/local/bin/build.sh
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
OS=${1:-darwin}
|
||||||
|
ARCH=${2:-arm64}
|
||||||
|
|
||||||
|
case "${OS}-${ARCH}" in
|
||||||
|
darwin-arm64|darwin-aarch64)
|
||||||
|
export CC=zcc-darwin-arm64
|
||||||
|
export GOARCH=arm64
|
||||||
|
export GOOS=darwin
|
||||||
|
;;
|
||||||
|
darwin-amd64|darwin-x86_64)
|
||||||
|
export CC=zcc-darwin-amd64
|
||||||
|
export GOARCH=amd64
|
||||||
|
export GOOS=darwin
|
||||||
|
;;
|
||||||
|
linux-arm64|linux-aarch64)
|
||||||
|
export CC=gcc
|
||||||
|
export GOARCH=arm64
|
||||||
|
export GOOS=linux
|
||||||
|
;;
|
||||||
|
linux-amd64|linux-x86_64)
|
||||||
|
export CC=gcc
|
||||||
|
export GOARCH=amd64
|
||||||
|
export GOOS=linux
|
||||||
|
;;
|
||||||
|
windows-arm64|windows-aarch64)
|
||||||
|
export CC=zcc-windows-arm64
|
||||||
|
export GOARCH=arm64
|
||||||
|
export GOOS=windows
|
||||||
|
;;
|
||||||
|
windows-amd64|windows-x86_64)
|
||||||
|
export CC=zcc-windows-amd64
|
||||||
|
export GOARCH=amd64
|
||||||
|
export GOOS=windows
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: <os> <arch>"
|
||||||
|
echo " os: darwin, linux, windows"
|
||||||
|
echo " arch: amd64, arm64"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
export CGO_CFLAGS="-w"
|
||||||
|
|
||||||
|
# Build frontend if exists and not already built (host may have built it)
|
||||||
|
if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then
|
||||||
|
(cd frontend && npm install --silent && npm run build --silent)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build
|
||||||
|
APP=${APP_NAME:-$(basename $(pwd))}
|
||||||
|
mkdir -p bin
|
||||||
|
|
||||||
|
EXT=""
|
||||||
|
LDFLAGS="-s -w"
|
||||||
|
if [ "$GOOS" = "windows" ]; then
|
||||||
|
EXT=".exe"
|
||||||
|
LDFLAGS="-s -w -H windowsgui"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAGS="production"
|
||||||
|
if [ -n "$EXTRA_TAGS" ]; then
|
||||||
|
TAGS="${TAGS},${EXTRA_TAGS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
go build -tags "$TAGS" -trimpath -buildvcs=false -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} .
|
||||||
|
echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}"
|
||||||
|
SCRIPT
|
||||||
|
RUN chmod +x /usr/local/bin/build.sh
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENTRYPOINT ["/usr/local/bin/build.sh"]
|
||||||
|
CMD ["darwin", "arm64"]
|
||||||
41
client/ui-wails/build/docker/Dockerfile.server
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Wails Server Mode Dockerfile
|
||||||
|
# Multi-stage build for minimal image size
|
||||||
|
|
||||||
|
# Build stage
|
||||||
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Remove local replace directive if present (for production builds)
|
||||||
|
RUN sed -i '/^replace/d' go.mod || true
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod tidy
|
||||||
|
|
||||||
|
# Build the server binary
|
||||||
|
RUN go build -tags server -ldflags="-s -w" -o server .
|
||||||
|
|
||||||
|
# Runtime stage - minimal image
|
||||||
|
FROM gcr.io/distroless/static-debian12
|
||||||
|
|
||||||
|
# Copy the binary
|
||||||
|
COPY --from=builder /app/server /server
|
||||||
|
|
||||||
|
# Copy frontend assets
|
||||||
|
COPY --from=builder /app/frontend/dist /frontend/dist
|
||||||
|
|
||||||
|
# Expose the default port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Bind to all interfaces (required for Docker)
|
||||||
|
# Can be overridden at runtime with -e WAILS_SERVER_HOST=...
|
||||||
|
ENV WAILS_SERVER_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
ENTRYPOINT ["/server"]
|
||||||
235
client/ui-wails/build/linux/Taskfile.yml
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Signing configuration - edit these values for your project
|
||||||
|
# PGP_KEY: "path/to/signing-key.asc"
|
||||||
|
# SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
|
||||||
|
#
|
||||||
|
# Password is stored securely in system keychain. Run: wails3 setup signing
|
||||||
|
|
||||||
|
# Docker image for cross-compilation (used when building on non-Linux or no CC available)
|
||||||
|
CROSS_IMAGE: wails-cross
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application for Linux
|
||||||
|
cmds:
|
||||||
|
# Linux requires CGO - use Docker when:
|
||||||
|
# 1. Cross-compiling from non-Linux, OR
|
||||||
|
# 2. No C compiler is available, OR
|
||||||
|
# 3. Target architecture differs from host architecture (cross-arch compilation)
|
||||||
|
- task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}'
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH}}'
|
||||||
|
DEV: '{{.DEV}}'
|
||||||
|
OUTPUT: '{{.OUTPUT}}'
|
||||||
|
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
|
||||||
|
vars:
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
# Determine target architecture (defaults to host ARCH if not specified)
|
||||||
|
TARGET_ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
# Check if a C compiler is available (gcc or clang)
|
||||||
|
HAS_CC:
|
||||||
|
sh: '(command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1) && echo "true" || echo "false"'
|
||||||
|
|
||||||
|
build:native:
|
||||||
|
summary: Builds the application natively on Linux
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
DEV:
|
||||||
|
ref: .DEV
|
||||||
|
- task: common:generate:icons
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
cmds:
|
||||||
|
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
|
||||||
|
build:docker:
|
||||||
|
summary: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available)
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:build:frontend
|
||||||
|
- task: common:generate:icons
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
preconditions:
|
||||||
|
- sh: docker info > /dev/null 2>&1
|
||||||
|
msg: "Docker is required for cross-compilation to Linux. Please install Docker."
|
||||||
|
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
|
||||||
|
msg: |
|
||||||
|
Docker image '{{.CROSS_IMAGE}}' not found.
|
||||||
|
Build it first: wails3 task setup:docker
|
||||||
|
cmds:
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}}
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
|
||||||
|
- mkdir -p {{.BIN_DIR}}
|
||||||
|
- mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
|
||||||
|
vars:
|
||||||
|
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
|
||||||
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
|
# Mount Go module cache for faster builds
|
||||||
|
GO_CACHE_MOUNT:
|
||||||
|
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
|
||||||
|
# Extract replace directives from go.mod and create -v mounts for each
|
||||||
|
REPLACE_MOUNTS:
|
||||||
|
sh: |
|
||||||
|
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
|
||||||
|
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
|
||||||
|
# Convert relative paths to absolute
|
||||||
|
if [ "${path#/}" = "$path" ]; then
|
||||||
|
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
|
||||||
|
fi
|
||||||
|
# Only mount if directory exists
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
echo "-v $path:$path:ro"
|
||||||
|
fi
|
||||||
|
done | tr '\n' ' '
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages the application for Linux
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: create:appimage
|
||||||
|
- task: create:deb
|
||||||
|
- task: create:rpm
|
||||||
|
- task: create:aur
|
||||||
|
|
||||||
|
create:appimage:
|
||||||
|
summary: Creates an AppImage
|
||||||
|
dir: build/linux/appimage
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
cmds:
|
||||||
|
- cp "{{.APP_BINARY}}" "{{.APP_NAME}}"
|
||||||
|
- cp ../../appicon.png "{{.APP_NAME}}.png"
|
||||||
|
- wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
|
||||||
|
vars:
|
||||||
|
APP_NAME: '{{.APP_NAME}}'
|
||||||
|
APP_BINARY: '../../../bin/{{.APP_NAME}}'
|
||||||
|
ICON: '{{.APP_NAME}}.png'
|
||||||
|
DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
|
||||||
|
OUTPUT_DIR: '../../../bin'
|
||||||
|
|
||||||
|
create:deb:
|
||||||
|
summary: Creates a deb package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:deb
|
||||||
|
|
||||||
|
create:rpm:
|
||||||
|
summary: Creates a rpm package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:rpm
|
||||||
|
|
||||||
|
create:aur:
|
||||||
|
summary: Creates a arch linux packager package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:aur
|
||||||
|
|
||||||
|
generate:deb:
|
||||||
|
summary: Creates a deb package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||||
|
|
||||||
|
generate:rpm:
|
||||||
|
summary: Creates a rpm package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||||
|
|
||||||
|
generate:aur:
|
||||||
|
summary: Creates a arch linux packager package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||||
|
|
||||||
|
generate:dotdesktop:
|
||||||
|
summary: Generates a `.desktop` file
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.ROOT_DIR}}/build/linux/appimage
|
||||||
|
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}"
|
||||||
|
# Wrap Exec= with `env WEBKIT_DISABLE_DMABUF_RENDERER=1 ...` so launches
|
||||||
|
# from any desktop environment use the working renderer. See build/linux/Taskfile.yml :run for the matching dev-mode env block.
|
||||||
|
- sed -i -E 's|^Exec=([^ ]+)(.*)$|Exec=env WEBKIT_DISABLE_DMABUF_RENDERER=1 \1\2|' {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop
|
||||||
|
vars:
|
||||||
|
APP_NAME: '{{.APP_NAME}}'
|
||||||
|
EXEC: '{{.APP_NAME}}'
|
||||||
|
ICON: '{{.APP_NAME}}'
|
||||||
|
CATEGORIES: 'Development;'
|
||||||
|
OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
|
env:
|
||||||
|
# WebKitGTK 2.50's default DMA-BUF renderer fails on RDP, VirtualBox/QEMU,
|
||||||
|
# and some bare WMs (Fluxbox, dwm) where DRM dumb-buffer access is
|
||||||
|
# restricted. Disabling it falls back to the GLES2/cairo path which works
|
||||||
|
# everywhere. Production launchers must set this too.
|
||||||
|
WEBKIT_DISABLE_DMABUF_RENDERER: "1"
|
||||||
|
|
||||||
|
sign:deb:
|
||||||
|
summary: Signs the DEB package
|
||||||
|
desc: |
|
||||||
|
Signs the .deb package with a PGP key.
|
||||||
|
Configure PGP_KEY in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
deps:
|
||||||
|
- task: create:deb
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.PGP_KEY}}" ]'
|
||||||
|
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
|
||||||
|
|
||||||
|
sign:rpm:
|
||||||
|
summary: Signs the RPM package
|
||||||
|
desc: |
|
||||||
|
Signs the .rpm package with a PGP key.
|
||||||
|
Configure PGP_KEY in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
deps:
|
||||||
|
- task: create:rpm
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" --pgp-key {{.PGP_KEY}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.PGP_KEY}}" ]'
|
||||||
|
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
|
||||||
|
|
||||||
|
sign:packages:
|
||||||
|
summary: Signs all Linux packages (DEB and RPM)
|
||||||
|
desc: |
|
||||||
|
Signs both .deb and .rpm packages with a PGP key.
|
||||||
|
Configure PGP_KEY in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
cmds:
|
||||||
|
- task: sign:deb
|
||||||
|
- task: sign:rpm
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.PGP_KEY}}" ]'
|
||||||
|
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
|
||||||
35
client/ui-wails/build/linux/appimage/build.sh
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Copyright (c) 2018-Present Lea Anthony
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
# Fail script on any error
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
# Define variables
|
||||||
|
APP_DIR="${APP_NAME}.AppDir"
|
||||||
|
|
||||||
|
# Create AppDir structure
|
||||||
|
mkdir -p "${APP_DIR}/usr/bin"
|
||||||
|
cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
|
||||||
|
cp "${ICON_PATH}" "${APP_DIR}/"
|
||||||
|
cp "${DESKTOP_FILE}" "${APP_DIR}/"
|
||||||
|
|
||||||
|
if [[ $(uname -m) == *x86_64* ]]; then
|
||||||
|
# Download linuxdeploy and make it executable
|
||||||
|
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||||
|
chmod +x linuxdeploy-x86_64.AppImage
|
||||||
|
|
||||||
|
# Run linuxdeploy to bundle the application
|
||||||
|
./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||||
|
else
|
||||||
|
# Download linuxdeploy and make it executable (arm64)
|
||||||
|
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
|
||||||
|
chmod +x linuxdeploy-aarch64.AppImage
|
||||||
|
|
||||||
|
# Run linuxdeploy to bundle the application (arm64)
|
||||||
|
./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rename the generated AppImage
|
||||||
|
mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
|
||||||
|
|
||||||
13
client/ui-wails/build/linux/desktop
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Name=NetBird
|
||||||
|
Comment=NetBird desktop client
|
||||||
|
# The Exec line includes %u to pass the URL to the application
|
||||||
|
Exec=/usr/local/bin/netbird-ui %u
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Icon=netbird-ui
|
||||||
|
Categories=Utility;
|
||||||
|
StartupWMClass=netbird-ui
|
||||||
|
|
||||||
|
|
||||||
10
client/ui-wails/build/linux/netbird-ui.desktop
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=netbird-ui
|
||||||
|
Exec=netbird-ui
|
||||||
|
Icon=netbird-ui
|
||||||
|
Categories=Development;
|
||||||
|
Terminal=false
|
||||||
|
Keywords=wails
|
||||||
|
Version=1.0
|
||||||
|
StartupNotify=false
|
||||||
67
client/ui-wails/build/linux/nfpm/nfpm.yaml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Feel free to remove those if you don't want/need to use them.
|
||||||
|
# Make sure to check the documentation at https://nfpm.goreleaser.com
|
||||||
|
#
|
||||||
|
# The lines below are called `modelines`. See `:help modeline`
|
||||||
|
|
||||||
|
name: "netbird-ui"
|
||||||
|
arch: ${GOARCH}
|
||||||
|
platform: "linux"
|
||||||
|
version: "0.0.1"
|
||||||
|
section: "default"
|
||||||
|
priority: "extra"
|
||||||
|
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||||
|
description: "NetBird desktop client"
|
||||||
|
vendor: "NetBird"
|
||||||
|
homepage: "https://wails.io"
|
||||||
|
license: "MIT"
|
||||||
|
release: "1"
|
||||||
|
|
||||||
|
contents:
|
||||||
|
- src: "./bin/netbird-ui"
|
||||||
|
dst: "/usr/local/bin/netbird-ui"
|
||||||
|
- src: "./build/appicon.png"
|
||||||
|
dst: "/usr/share/icons/hicolor/128x128/apps/netbird-ui.png"
|
||||||
|
- src: "./build/linux/netbird-ui.desktop"
|
||||||
|
dst: "/usr/share/applications/netbird-ui.desktop"
|
||||||
|
|
||||||
|
# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1
|
||||||
|
depends:
|
||||||
|
- libgtk-3-0
|
||||||
|
- libwebkit2gtk-4.1-0
|
||||||
|
|
||||||
|
# Distribution-specific overrides for different package formats and WebKit versions
|
||||||
|
overrides:
|
||||||
|
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0)
|
||||||
|
rpm:
|
||||||
|
depends:
|
||||||
|
- gtk3
|
||||||
|
- webkit2gtk4.1
|
||||||
|
|
||||||
|
# Arch Linux packages (WebKit 4.1)
|
||||||
|
archlinux:
|
||||||
|
depends:
|
||||||
|
- gtk3
|
||||||
|
- webkit2gtk-4.1
|
||||||
|
|
||||||
|
# scripts section to ensure desktop database is updated after install
|
||||||
|
scripts:
|
||||||
|
postinstall: "./build/linux/nfpm/scripts/postinstall.sh"
|
||||||
|
# You can also add preremove, postremove if needed
|
||||||
|
# preremove: "./build/linux/nfpm/scripts/preremove.sh"
|
||||||
|
# postremove: "./build/linux/nfpm/scripts/postremove.sh"
|
||||||
|
|
||||||
|
# replaces:
|
||||||
|
# - foobar
|
||||||
|
# provides:
|
||||||
|
# - bar
|
||||||
|
# depends:
|
||||||
|
# - gtk3
|
||||||
|
# - libwebkit2gtk
|
||||||
|
# recommends:
|
||||||
|
# - whatever
|
||||||
|
# suggests:
|
||||||
|
# - something-else
|
||||||
|
# conflicts:
|
||||||
|
# - not-foo
|
||||||
|
# - not-bar
|
||||||
|
# changelog: "changelog.yaml"
|
||||||
21
client/ui-wails/build/linux/nfpm/scripts/postinstall.sh
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Update desktop database for .desktop file changes
|
||||||
|
# This makes the application appear in application menus and registers its capabilities.
|
||||||
|
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||||
|
echo "Updating desktop database..."
|
||||||
|
update-desktop-database -q /usr/share/applications
|
||||||
|
else
|
||||||
|
echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update MIME database for custom URL schemes (x-scheme-handler)
|
||||||
|
# This ensures the system knows how to handle your custom protocols.
|
||||||
|
if command -v update-mime-database >/dev/null 2>&1; then
|
||||||
|
echo "Updating MIME database..."
|
||||||
|
update-mime-database -n /usr/share/mime
|
||||||
|
else
|
||||||
|
echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
1
client/ui-wails/build/linux/nfpm/scripts/postremove.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
1
client/ui-wails/build/linux/nfpm/scripts/preinstall.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
1
client/ui-wails/build/linux/nfpm/scripts/preremove.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#!/bin/bash
|
||||||
236
client/ui-wails/build/windows/Taskfile.yml
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ../Taskfile.yml
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Signing configuration - edit these values for your project
|
||||||
|
# SIGN_CERTIFICATE: "path/to/certificate.pfx"
|
||||||
|
# SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE
|
||||||
|
# TIMESTAMP_SERVER: "http://timestamp.digicert.com"
|
||||||
|
#
|
||||||
|
# Password is stored securely in system keychain. Run: wails3 setup signing
|
||||||
|
|
||||||
|
# Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows)
|
||||||
|
CROSS_IMAGE: wails-cross
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application for Windows
|
||||||
|
cmds:
|
||||||
|
# CGO Windows builds from Linux use mingw-w64 (lighter than docker).
|
||||||
|
# Docker is only needed if mingw-w64 is unavailable.
|
||||||
|
- task: build:native
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH}}'
|
||||||
|
DEV: '{{.DEV}}'
|
||||||
|
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
|
||||||
|
vars:
|
||||||
|
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
|
||||||
|
|
||||||
|
build:console:
|
||||||
|
summary: Builds a console-attached Windows binary so logs go to the terminal.
|
||||||
|
desc: |
|
||||||
|
Same as `windows:build` but links against the console PE subsystem
|
||||||
|
instead of windowsgui, so stdout/stderr (logrus, panics) print to the
|
||||||
|
terminal that launched the .exe. Useful for chasing tray, event-stream,
|
||||||
|
or daemon-RPC bugs that have no other feedback channel on Windows.
|
||||||
|
|
||||||
|
Output is bin/netbird-ui-console.exe — kept distinct so the production
|
||||||
|
binary built by `windows:build` isn't shadowed.
|
||||||
|
|
||||||
|
Cross-compile from Linux works the same way:
|
||||||
|
CGO_ENABLED=1 task windows:build:console
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
DEV:
|
||||||
|
ref: .DEV
|
||||||
|
- task: common:generate:icons
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ "{{OS}}" = "windows" ] || [ "{{.CGO_ENABLED}}" != "1" ] || command -v {{.CC}}'
|
||||||
|
msg: "{{.CC}} not found. Install with: sudo apt-get install gcc-mingw-w64-x86-64 (Debian/Ubuntu) / sudo dnf install mingw64-gcc (Fedora)"
|
||||||
|
cmds:
|
||||||
|
- task: generate:syso
|
||||||
|
- go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}-console.exe"
|
||||||
|
- cmd: powershell Remove-item *.syso
|
||||||
|
platforms: [windows]
|
||||||
|
- cmd: rm -f *.syso
|
||||||
|
platforms: [linux, darwin]
|
||||||
|
vars:
|
||||||
|
# Identical to build:native's flags except no -H windowsgui, so the
|
||||||
|
# binary attaches to the launching console.
|
||||||
|
BUILD_FLAGS: '-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"'
|
||||||
|
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
|
||||||
|
CC: '{{.CC | default "x86_64-w64-mingw32-gcc"}}'
|
||||||
|
env:
|
||||||
|
GOOS: windows
|
||||||
|
CGO_ENABLED: '{{.CGO_ENABLED}}'
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
CC: '{{.CC}}'
|
||||||
|
|
||||||
|
build:native:
|
||||||
|
summary: Builds for Windows natively, or cross-compiles from Linux/macOS via mingw-w64.
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:go:mod:tidy
|
||||||
|
- task: common:build:frontend
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS:
|
||||||
|
ref: .BUILD_FLAGS
|
||||||
|
DEV:
|
||||||
|
ref: .DEV
|
||||||
|
- task: common:generate:icons
|
||||||
|
preconditions:
|
||||||
|
# When cross-compiling with CGO from a non-Windows host, the mingw-w64
|
||||||
|
# cross-gcc must be present. Native Windows builds skip this check.
|
||||||
|
- sh: '[ "{{OS}}" = "windows" ] || [ "{{.CGO_ENABLED}}" != "1" ] || command -v {{.CC}}'
|
||||||
|
msg: "{{.CC}} not found. Install with: sudo apt-get install gcc-mingw-w64-x86-64 (Debian/Ubuntu) / sudo dnf install mingw64-gcc (Fedora)"
|
||||||
|
cmds:
|
||||||
|
- task: generate:syso
|
||||||
|
- go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe"
|
||||||
|
- cmd: powershell Remove-item *.syso
|
||||||
|
platforms: [windows]
|
||||||
|
- cmd: rm -f *.syso
|
||||||
|
platforms: [linux, darwin]
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}'
|
||||||
|
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
|
||||||
|
CC: '{{.CC | default "x86_64-w64-mingw32-gcc"}}'
|
||||||
|
env:
|
||||||
|
GOOS: windows
|
||||||
|
CGO_ENABLED: '{{.CGO_ENABLED}}'
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
CC: '{{.CC}}'
|
||||||
|
|
||||||
|
build:docker:
|
||||||
|
summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows)
|
||||||
|
internal: true
|
||||||
|
deps:
|
||||||
|
- task: common:build:frontend
|
||||||
|
- task: common:generate:icons
|
||||||
|
preconditions:
|
||||||
|
- sh: docker info > /dev/null 2>&1
|
||||||
|
msg: "Docker is required for CGO cross-compilation. Please install Docker."
|
||||||
|
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
|
||||||
|
msg: |
|
||||||
|
Docker image '{{.CROSS_IMAGE}}' not found.
|
||||||
|
Build it first: wails3 task setup:docker
|
||||||
|
cmds:
|
||||||
|
- task: generate:syso
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}}
|
||||||
|
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
|
||||||
|
- rm -f *.syso
|
||||||
|
vars:
|
||||||
|
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
|
||||||
|
# Mount Go module cache for faster builds
|
||||||
|
GO_CACHE_MOUNT:
|
||||||
|
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
|
||||||
|
# Extract replace directives from go.mod and create -v mounts for each
|
||||||
|
REPLACE_MOUNTS:
|
||||||
|
sh: |
|
||||||
|
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
|
||||||
|
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
|
||||||
|
# Convert relative paths to absolute
|
||||||
|
if [ "${path#/}" = "$path" ]; then
|
||||||
|
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
|
||||||
|
fi
|
||||||
|
# Only mount if directory exists
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
echo "-v $path:$path:ro"
|
||||||
|
fi
|
||||||
|
done | tr '\n' ' '
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages the application
|
||||||
|
cmds:
|
||||||
|
- task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}'
|
||||||
|
vars:
|
||||||
|
FORMAT: '{{.FORMAT | default "nsis"}}'
|
||||||
|
|
||||||
|
generate:syso:
|
||||||
|
summary: Generates Windows `.syso` file
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
|
||||||
|
create:nsis:installer:
|
||||||
|
summary: Creates an NSIS installer
|
||||||
|
dir: build/windows/nsis
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
# Create the Microsoft WebView2 bootstrapper if it doesn't exist
|
||||||
|
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
|
||||||
|
- |
|
||||||
|
{{if eq OS "windows"}}
|
||||||
|
makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
|
||||||
|
{{else}}
|
||||||
|
makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi
|
||||||
|
{{end}}
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
|
||||||
|
|
||||||
|
create:msix:package:
|
||||||
|
summary: Creates an MSIX package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- |-
|
||||||
|
wails3 tool msix \
|
||||||
|
--config "{{.ROOT_DIR}}/wails.json" \
|
||||||
|
--name "{{.APP_NAME}}" \
|
||||||
|
--executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
|
||||||
|
--arch "{{.ARCH}}" \
|
||||||
|
--out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
|
||||||
|
{{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
|
||||||
|
{{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
|
||||||
|
{{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}}
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
CERT_PATH: '{{.CERT_PATH | default ""}}'
|
||||||
|
PUBLISHER: '{{.PUBLISHER | default ""}}'
|
||||||
|
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
|
||||||
|
|
||||||
|
install:msix:tools:
|
||||||
|
summary: Installs tools required for MSIX packaging
|
||||||
|
cmds:
|
||||||
|
- wails3 tool msix-install-tools
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
|
||||||
|
|
||||||
|
sign:
|
||||||
|
summary: Signs the Windows executable
|
||||||
|
desc: |
|
||||||
|
Signs the .exe with an Authenticode certificate.
|
||||||
|
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
|
||||||
|
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"
|
||||||
|
|
||||||
|
sign:installer:
|
||||||
|
summary: Signs the NSIS installer
|
||||||
|
desc: |
|
||||||
|
Creates and signs the NSIS installer.
|
||||||
|
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
|
||||||
|
Password is retrieved from system keychain (run: wails3 setup signing)
|
||||||
|
deps:
|
||||||
|
- task: create:nsis:installer
|
||||||
|
cmds:
|
||||||
|
- wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
|
||||||
|
preconditions:
|
||||||
|
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
|
||||||
|
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"
|
||||||
BIN
client/ui-wails/build/windows/icon.ico
Normal file
|
After Width: | Height: | Size: 18 KiB |
15
client/ui-wails/build/windows/info.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fixed": {
|
||||||
|
"file_version": "0.0.1"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"0000": {
|
||||||
|
"ProductVersion": "0.0.1",
|
||||||
|
"CompanyName": "NetBird",
|
||||||
|
"FileDescription": "NetBird desktop client",
|
||||||
|
"LegalCopyright": "© 2026, My Company",
|
||||||
|
"ProductName": "NetBird",
|
||||||
|
"Comments": "This is a comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
client/ui-wails/build/windows/msix/app_manifest.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Package
|
||||||
|
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||||
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||||
|
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
|
||||||
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||||
|
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||||
|
IgnorableNamespaces="uap3">
|
||||||
|
|
||||||
|
<Identity
|
||||||
|
Name="io.netbird.client"
|
||||||
|
Publisher="CN=NetBird"
|
||||||
|
Version="0.0.1.0"
|
||||||
|
ProcessorArchitecture="x64" />
|
||||||
|
|
||||||
|
<Properties>
|
||||||
|
<DisplayName>NetBird</DisplayName>
|
||||||
|
<PublisherDisplayName>NetBird</PublisherDisplayName>
|
||||||
|
<Description>NetBird desktop client</Description>
|
||||||
|
<Logo>Assets\StoreLogo.png</Logo>
|
||||||
|
</Properties>
|
||||||
|
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
</Dependencies>
|
||||||
|
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="en-us" />
|
||||||
|
</Resources>
|
||||||
|
|
||||||
|
<Applications>
|
||||||
|
<Application Id="io.netbird.client" Executable="netbird-ui" EntryPoint="Windows.FullTrustApplication">
|
||||||
|
<uap:VisualElements
|
||||||
|
DisplayName="NetBird"
|
||||||
|
Description="NetBird desktop client"
|
||||||
|
BackgroundColor="transparent"
|
||||||
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
|
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||||
|
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||||
|
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||||
|
</uap:VisualElements>
|
||||||
|
|
||||||
|
<Extensions>
|
||||||
|
<desktop:Extension Category="windows.fullTrustProcess" Executable="netbird-ui" />
|
||||||
|
|
||||||
|
|
||||||
|
</Extensions>
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
|
||||||
|
<Capabilities>
|
||||||
|
<rescap:Capability Name="runFullTrust" />
|
||||||
|
|
||||||
|
</Capabilities>
|
||||||
|
</Package>
|
||||||
54
client/ui-wails/build/windows/msix/template.xml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MsixPackagingToolTemplate
|
||||||
|
xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
|
||||||
|
<Settings
|
||||||
|
AllowTelemetry="false"
|
||||||
|
ApplyACLsToPackageFiles="true"
|
||||||
|
GenerateCommandLineFile="true"
|
||||||
|
AllowPromptForPassword="false">
|
||||||
|
</Settings>
|
||||||
|
<Installer
|
||||||
|
Path="netbird-ui"
|
||||||
|
Arguments=""
|
||||||
|
InstallLocation="C:\Program Files\NetBird\NetBird">
|
||||||
|
</Installer>
|
||||||
|
<PackageInformation
|
||||||
|
PackageName="NetBird"
|
||||||
|
PackageDisplayName="NetBird"
|
||||||
|
PublisherName="CN=NetBird"
|
||||||
|
PublisherDisplayName="NetBird"
|
||||||
|
Version="0.0.1.0"
|
||||||
|
PackageDescription="NetBird desktop client">
|
||||||
|
<Capabilities>
|
||||||
|
<Capability Name="runFullTrust" />
|
||||||
|
|
||||||
|
</Capabilities>
|
||||||
|
<Applications>
|
||||||
|
<Application
|
||||||
|
Id="io.netbird.client"
|
||||||
|
Description="NetBird desktop client"
|
||||||
|
DisplayName="NetBird"
|
||||||
|
ExecutableName="netbird-ui"
|
||||||
|
EntryPoint="Windows.FullTrustApplication">
|
||||||
|
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="en-us" />
|
||||||
|
</Resources>
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
</Dependencies>
|
||||||
|
<Properties>
|
||||||
|
<Framework>false</Framework>
|
||||||
|
<DisplayName>NetBird</DisplayName>
|
||||||
|
<PublisherDisplayName>NetBird</PublisherDisplayName>
|
||||||
|
<Description>NetBird desktop client</Description>
|
||||||
|
<Logo>Assets\AppIcon.png</Logo>
|
||||||
|
</Properties>
|
||||||
|
</PackageInformation>
|
||||||
|
<SaveLocation PackagePath="netbird-ui.msix" />
|
||||||
|
<PackageIntegrity>
|
||||||
|
<CertificatePath></CertificatePath>
|
||||||
|
</PackageIntegrity>
|
||||||
|
</MsixPackagingToolTemplate>
|
||||||
114
client/ui-wails/build/windows/nsis/project.nsi
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
Unicode true
|
||||||
|
|
||||||
|
####
|
||||||
|
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||||
|
## mentioned underneath.
|
||||||
|
## If the keyword is not defined, "wails_tools.nsh" will populate them.
|
||||||
|
## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually
|
||||||
|
## from outside of Wails for debugging and development of the installer.
|
||||||
|
##
|
||||||
|
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||||
|
## > wails build --target windows/amd64 --nsis
|
||||||
|
## Then you can call makensis on this file with specifying the path to your binary:
|
||||||
|
## For a AMD64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a ARM64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a installer with both architectures:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||||
|
####
|
||||||
|
## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
|
||||||
|
####
|
||||||
|
## !define INFO_PROJECTNAME "my-project" # Default "netbird-ui"
|
||||||
|
## !define INFO_COMPANYNAME "My Company" # Default "NetBird"
|
||||||
|
## !define INFO_PRODUCTNAME "My Product Name" # Default "NetBird"
|
||||||
|
## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.0.1"
|
||||||
|
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© 2026, My Company"
|
||||||
|
###
|
||||||
|
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||||
|
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
####
|
||||||
|
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||||
|
####
|
||||||
|
## Include the wails tools
|
||||||
|
####
|
||||||
|
!include "wails_tools.nsh"
|
||||||
|
|
||||||
|
# The version information for this two must consist of 4 parts
|
||||||
|
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
|
||||||
|
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||||
|
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||||
|
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||||
|
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||||
|
|
||||||
|
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||||
|
ManifestDPIAware true
|
||||||
|
|
||||||
|
!include "MUI.nsh"
|
||||||
|
|
||||||
|
!define MUI_ICON "..\icon.ico"
|
||||||
|
!define MUI_UNICON "..\icon.ico"
|
||||||
|
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||||
|
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||||
|
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||||
|
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||||
|
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||||
|
|
||||||
|
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||||
|
#!uninstfinalize 'signtool --file "%1"'
|
||||||
|
#!finalize 'signtool --file "%1"'
|
||||||
|
|
||||||
|
Name "${INFO_PRODUCTNAME}"
|
||||||
|
OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||||
|
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||||
|
ShowInstDetails show # This will always show the installation details.
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
!insertmacro wails.checkArchitecture
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
!insertmacro wails.webview2runtime
|
||||||
|
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
|
||||||
|
!insertmacro wails.files
|
||||||
|
|
||||||
|
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
|
||||||
|
!insertmacro wails.associateFiles
|
||||||
|
!insertmacro wails.associateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.writeUninstaller
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "uninstall"
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||||
|
|
||||||
|
RMDir /r $INSTDIR
|
||||||
|
|
||||||
|
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
|
||||||
|
!insertmacro wails.unassociateFiles
|
||||||
|
!insertmacro wails.unassociateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.deleteUninstaller
|
||||||
|
SectionEnd
|
||||||
236
client/ui-wails/build/windows/nsis/wails_tools.nsh
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# DO NOT EDIT - Generated automatically by `wails build`
|
||||||
|
|
||||||
|
!include "x64.nsh"
|
||||||
|
!include "WinVer.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
|
!ifndef INFO_PROJECTNAME
|
||||||
|
!define INFO_PROJECTNAME "netbird-ui"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COMPANYNAME
|
||||||
|
!define INFO_COMPANYNAME "NetBird"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTNAME
|
||||||
|
!define INFO_PRODUCTNAME "NetBird"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTVERSION
|
||||||
|
!define INFO_PRODUCTVERSION "0.0.1"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COPYRIGHT
|
||||||
|
!define INFO_COPYRIGHT "© 2026, My Company"
|
||||||
|
!endif
|
||||||
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
!endif
|
||||||
|
!ifndef UNINST_KEY_NAME
|
||||||
|
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||||
|
|
||||||
|
!ifndef REQUEST_EXECUTION_LEVEL
|
||||||
|
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_AMD64_BINARY
|
||||||
|
!define SUPPORTS_AMD64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_ARM64_BINARY
|
||||||
|
!define SUPPORTS_ARM64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "amd64_arm64"
|
||||||
|
!else
|
||||||
|
!define ARCH "amd64"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "arm64"
|
||||||
|
!else
|
||||||
|
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||||
|
!endif
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!macro wails.checkArchitecture
|
||||||
|
!ifndef WAILS_WIN10_REQUIRED
|
||||||
|
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||||
|
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
${If} ${AtLeastWin10}
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
IfSilent silentArch notSilentArch
|
||||||
|
silentArch:
|
||||||
|
SetErrorLevel 65
|
||||||
|
Abort
|
||||||
|
notSilentArch:
|
||||||
|
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||||
|
Quit
|
||||||
|
${else}
|
||||||
|
IfSilent silentWin notSilentWin
|
||||||
|
silentWin:
|
||||||
|
SetErrorLevel 64
|
||||||
|
Abort
|
||||||
|
notSilentWin:
|
||||||
|
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||||
|
Quit
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.files
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.writeUninstaller
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||||
|
|
||||||
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||||
|
IntFmt $0 "0x%08X" $0
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.deleteUninstaller
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.setShellContext
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||||
|
SetShellVarContext all
|
||||||
|
${else}
|
||||||
|
SetShellVarContext current
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Install webview2 by launching the bootstrapper
|
||||||
|
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||||
|
!macro wails.webview2runtime
|
||||||
|
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||||
|
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
# If the admin key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||||
|
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||||
|
SetDetailsPrint listonly
|
||||||
|
|
||||||
|
InitPluginsDir
|
||||||
|
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||||
|
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||||
|
File "MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||||
|
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||||
|
|
||||||
|
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateFiles
|
||||||
|
; Create file associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateFiles
|
||||||
|
; Delete app associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateCustomProtocols
|
||||||
|
; Create custom protocols associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateCustomProtocols
|
||||||
|
; Delete app custom protocol associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
22
client/ui-wails/build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<assemblyIdentity type="win32" name="io.netbird.client" version="0.0.1" processorArchitecture="*"/>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges>
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
</assembly>
|
||||||
93
client/ui-wails/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.
|
||||||
12
client/ui-wails/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-wails/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-wails/frontend/pnpm-lock.yaml
generated
Normal file
6
client/ui-wails/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
client/ui-wails/frontend/public/Inter-Medium.ttf
Normal file
1
client/ui-wails/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-wails/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-wails/frontend/public/wails.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
32
client/ui-wails/frontend/src/App.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<HashRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/quick" element={<QuickActions />} />
|
||||||
|
<Route path="/login" 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-wails/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-wails/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-wails/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-wails/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-wails/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-wails/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-wails/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-wails/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-wails/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-wails/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-wails/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>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
client/ui-wails/frontend/src/pages/LoginUrl.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
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={() => window.open(url, "_blank")}>
|
||||||
|
<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-wails/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-wails/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-wails/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-wails/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-wails/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>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
client/ui-wails/frontend/src/pages/Status.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { CheckCircle2, Circle, Loader2, AlertTriangle, Power } from "lucide-react";
|
||||||
|
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 connState = status?.status ?? "Disconnected";
|
||||||
|
const connected = connState === "Connected";
|
||||||
|
const connecting = connState === "Connecting";
|
||||||
|
|
||||||
|
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">
|
||||||
|
<Button onClick={connect} disabled={connected || connecting}>
|
||||||
|
<Power className="h-4 w-4" strokeWidth={1.5} /> Connect
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
client/ui-wails/frontend/src/pages/Update.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { Update as UpdateSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 15 * 60 * 1000;
|
||||||
|
|
||||||
|
export default function Update() {
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
UpdateSvc.Trigger().catch((e) => !cancelled && setError(String(e)));
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
if (Date.now() - start > TIMEOUT_MS) {
|
||||||
|
setError("Update timed out.");
|
||||||
|
clearInterval(timer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const r = await UpdateSvc.GetInstallerResult();
|
||||||
|
if (r.success) {
|
||||||
|
setDone(true);
|
||||||
|
clearInterval(timer);
|
||||||
|
} else if (r.errorMsg) {
|
||||||
|
setError(r.errorMsg);
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// installer not finished yet
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full items-center justify-center p-6">
|
||||||
|
<div className="text-center">
|
||||||
|
{done ? (
|
||||||
|
<h1 className="text-xl font-semibold text-green-500">Update complete</h1>
|
||||||
|
) : error ? (
|
||||||
|
<h1 className="text-xl font-semibold text-red-500">{error}</h1>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mx-auto mb-3 h-8 w-8 animate-spin text-netbird" strokeWidth={1.5} />
|
||||||
|
<h1 className="text-xl font-semibold">Updating…</h1>
|
||||||
|
<p className="mt-1 text-sm text-nb-gray-500">
|
||||||
|
Please don't close this window.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
client/ui-wails/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
44
client/ui-wails/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-wails/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"],
|
||||||
|
}
|
||||||