mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-04 08:06:37 +00:00
Add prototype UI clients
This commit is contained in:
13
client/ui-wails/frontend/index.html
Normal file
13
client/ui-wails/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>ui-wails</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./src/main.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2923
client/ui-wails/frontend/package-lock.json
generated
Normal file
2923
client/ui-wails/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
client/ui-wails/frontend/package.json
Normal file
30
client/ui-wails/frontend/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.23.24",
|
||||
"lottie-react": "^2.4.1",
|
||||
"lucide-react": "^0.552.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
1
client/ui-wails/frontend/package.json.md5
Executable file
1
client/ui-wails/frontend/package.json.md5
Executable file
@@ -0,0 +1 @@
|
||||
bb3ec0f66e5cb142b1ed17a142701dc6
|
||||
6
client/ui-wails/frontend/postcss.config.cjs
Normal file
6
client/ui-wails/frontend/postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
59
client/ui-wails/frontend/src/App.css
Normal file
59
client/ui-wails/frontend/src/App.css
Normal file
@@ -0,0 +1,59 @@
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#logo {
|
||||
display: block;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
margin: auto;
|
||||
padding: 10% 0 0;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-origin: content-box;
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
|
||||
.input-box .btn {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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;
|
||||
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);
|
||||
}
|
||||
150
client/ui-wails/frontend/src/App.tsx
Normal file
150
client/ui-wails/frontend/src/App.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Wifi,
|
||||
WifiOff,
|
||||
Settings,
|
||||
Network,
|
||||
User,
|
||||
Bug,
|
||||
LogOut,
|
||||
Home,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { useStore } from './store/useStore';
|
||||
import Overview from './pages/Overview';
|
||||
import SettingsPage from './pages/Settings';
|
||||
import NetworksPage from './pages/Networks';
|
||||
import ProfilesPage from './pages/Profiles';
|
||||
import DebugPage from './pages/Debug';
|
||||
import Peers from './pages/Peers';
|
||||
|
||||
type Page = 'overview' | 'settings' | 'networks' | 'profiles' | 'debug' | 'peers';
|
||||
|
||||
function App() {
|
||||
const [currentPage, setCurrentPage] = useState<Page>('overview');
|
||||
const { status, connected, refreshStatus, refreshConfig, refreshProfiles } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Initial data load
|
||||
refreshStatus();
|
||||
refreshConfig();
|
||||
refreshProfiles();
|
||||
}, [refreshStatus, refreshConfig, refreshProfiles]);
|
||||
|
||||
const navItems = [
|
||||
{ id: 'overview', label: 'Overview', icon: Home },
|
||||
{ id: 'peers', label: 'Peers', icon: Users },
|
||||
{ id: 'networks', label: 'Networks', icon: Network },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings },
|
||||
{ id: 'profiles', label: 'Profiles', icon: User },
|
||||
{ id: 'debug', label: 'Debug', icon: Bug },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-dark-bg text-text-light overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<motion.div
|
||||
initial={{ x: -300 }}
|
||||
animate={{ x: 0 }}
|
||||
className="w-64 bg-dark-bg-card border-r border-icy-blue/20 flex flex-col"
|
||||
>
|
||||
{/* Logo & Status */}
|
||||
<div className="p-6 border-b border-icy-blue/20">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center transition-all ${
|
||||
connected
|
||||
? 'bg-icy-blue/10 border border-icy-blue/30 neon-pulse'
|
||||
: 'bg-dark-bg border border-icy-blue/10'
|
||||
}`}>
|
||||
{connected ? (
|
||||
<Wifi className="w-6 h-6 text-icy-blue" />
|
||||
) : (
|
||||
<WifiOff className="w-6 h-6 text-text-muted" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-text-light">NetBird</h1>
|
||||
<p className="text-xs text-text-muted">{status}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection indicator */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full transition-all ${
|
||||
connected ? 'bg-icy-blue shadow-icy-glow' : 'bg-text-muted'
|
||||
}`}
|
||||
/>
|
||||
<span className={`text-sm ${
|
||||
connected ? 'text-icy-blue font-medium' : 'text-text-muted'
|
||||
}`}>
|
||||
{connected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = currentPage === item.id;
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
whileHover={{ scale: 1.02, x: 4 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => setCurrentPage(item.id as Page)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-all ${
|
||||
isActive
|
||||
? 'bg-icy-blue/10 text-icy-blue border border-icy-blue/30 shadow-icy-glow'
|
||||
: 'text-text-muted hover:text-text-light hover:bg-dark-bg hover:border-icy-blue/30 hover:shadow-icy-glow border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-icy-blue/20">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => useStore.getState().logout()}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-lg text-text-muted hover:text-text-light hover:bg-dark-bg hover:border-icy-blue/20 border border-transparent transition-all"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span className="font-medium">Logout</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentPage}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full"
|
||||
>
|
||||
{currentPage === 'overview' && <Overview onNavigate={setCurrentPage} />}
|
||||
{currentPage === 'peers' && <Peers onNavigate={setCurrentPage} />}
|
||||
{currentPage === 'settings' && <SettingsPage />}
|
||||
{currentPage === 'networks' && <NetworksPage />}
|
||||
{currentPage === 'profiles' && <ProfilesPage />}
|
||||
{currentPage === 'debug' && <DebugPage />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
9316
client/ui-wails/frontend/src/assets/button-full.json
Normal file
9316
client/ui-wails/frontend/src/assets/button-full.json
Normal file
File diff suppressed because it is too large
Load Diff
93
client/ui-wails/frontend/src/assets/fonts/OFL.txt
Normal file
93
client/ui-wails/frontend/src/assets/fonts/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
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.
|
||||
Binary file not shown.
BIN
client/ui-wails/frontend/src/assets/images/logo-universal.png
Normal file
BIN
client/ui-wails/frontend/src/assets/images/logo-universal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
199
client/ui-wails/frontend/src/index.css
Normal file
199
client/ui-wails/frontend/src/index.css
Normal file
@@ -0,0 +1,199 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: dark;
|
||||
color: #f8f8fc;
|
||||
background-color: #121218;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(163, 215, 229, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(163, 215, 229, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(163, 215, 229, 0.3);
|
||||
}
|
||||
|
||||
/* Enhanced Glass morphism effects */
|
||||
.glass {
|
||||
background: rgba(28, 28, 35, 0.6);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(163, 215, 229, 0.2);
|
||||
box-shadow:
|
||||
0 8px 32px 0 rgba(163, 215, 229, 0.15),
|
||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.05),
|
||||
inset 0 -1px 1px 0 rgba(163, 215, 229, 0.05);
|
||||
}
|
||||
|
||||
.glass-hover:hover {
|
||||
background: rgba(28, 28, 35, 0.8);
|
||||
border-color: rgba(163, 215, 229, 0.4);
|
||||
box-shadow:
|
||||
0 12px 48px 0 rgba(163, 215, 229, 0.25),
|
||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 -1px 1px 0 rgba(163, 215, 229, 0.1);
|
||||
}
|
||||
|
||||
/* Icy blue glow animations */
|
||||
@keyframes icyGlow {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 20px rgba(163, 215, 229, 0.5),
|
||||
0 0 40px rgba(163, 215, 229, 0.3),
|
||||
0 0 60px rgba(163, 215, 229, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 30px rgba(163, 215, 229, 0.8),
|
||||
0 0 60px rgba(163, 215, 229, 0.5),
|
||||
0 0 90px rgba(163, 215, 229, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes neonPulse {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 10px rgba(163, 215, 229, 0.6),
|
||||
0 0 20px rgba(163, 215, 229, 0.4),
|
||||
0 0 30px rgba(163, 215, 229, 0.2),
|
||||
inset 0 0 10px rgba(163, 215, 229, 0.2);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 20px rgba(163, 215, 229, 0.9),
|
||||
0 0 40px rgba(163, 215, 229, 0.6),
|
||||
0 0 60px rgba(163, 215, 229, 0.3),
|
||||
inset 0 0 20px rgba(163, 215, 229, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icy-glow-animate {
|
||||
animation: icyGlow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.neon-pulse {
|
||||
animation: neonPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Neon border effect */
|
||||
.neon-border {
|
||||
position: relative;
|
||||
border: 2px solid rgba(163, 215, 229, 0.4);
|
||||
box-shadow:
|
||||
0 0 10px rgba(163, 215, 229, 0.4),
|
||||
inset 0 0 10px rgba(163, 215, 229, 0.1);
|
||||
}
|
||||
|
||||
.neon-border-strong {
|
||||
border: 2px solid rgba(163, 215, 229, 0.6);
|
||||
box-shadow:
|
||||
0 0 15px rgba(163, 215, 229, 0.6),
|
||||
0 0 30px rgba(163, 215, 229, 0.3),
|
||||
inset 0 0 15px rgba(163, 215, 229, 0.15);
|
||||
}
|
||||
|
||||
/* Shimmer effect for special elements */
|
||||
.shimmer {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(163, 215, 229, 0.0) 0%,
|
||||
rgba(163, 215, 229, 0.2) 50%,
|
||||
rgba(163, 215, 229, 0.0) 100%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
animation: shimmer 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Frosted glass background */
|
||||
.frosted {
|
||||
background: rgba(18, 18, 24, 0.7);
|
||||
backdrop-filter: blur(30px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(200%);
|
||||
border: 1px solid rgba(163, 215, 229, 0.15);
|
||||
box-shadow:
|
||||
0 8px 32px 0 rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Icy gradient overlay */
|
||||
.icy-gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(163, 215, 229, 0.1) 0%,
|
||||
rgba(140, 200, 215, 0.05) 50%,
|
||||
rgba(163, 215, 229, 0.1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Neon text glow */
|
||||
.text-neon {
|
||||
text-shadow:
|
||||
0 0 10px rgba(163, 215, 229, 0.8),
|
||||
0 0 20px rgba(163, 215, 229, 0.5),
|
||||
0 0 30px rgba(163, 215, 229, 0.3);
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
.transition-all {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Card glow on hover */
|
||||
.card-glow-hover:hover {
|
||||
box-shadow:
|
||||
0 0 20px rgba(163, 215, 229, 0.3),
|
||||
0 0 40px rgba(163, 215, 229, 0.2),
|
||||
0 8px 32px 0 rgba(163, 215, 229, 0.15);
|
||||
border-color: rgba(163, 215, 229, 0.4);
|
||||
}
|
||||
14
client/ui-wails/frontend/src/main.tsx
Normal file
14
client/ui-wails/frontend/src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
|
||||
const container = document.getElementById('root')
|
||||
|
||||
const root = createRoot(container!)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
)
|
||||
204
client/ui-wails/frontend/src/pages/Debug.tsx
Normal file
204
client/ui-wails/frontend/src/pages/Debug.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Bug, Package, AlertCircle, CheckCircle2, Copy, Check } from 'lucide-react';
|
||||
|
||||
export default function DebugPage() {
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [anonymize, setAnonymize] = useState(true);
|
||||
const [bundlePath, setBundlePath] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCreateBundle = async () => {
|
||||
try {
|
||||
setCreating(true);
|
||||
setError('');
|
||||
setBundlePath('');
|
||||
setCopied(false);
|
||||
|
||||
// TODO: Implement debug bundle creation via IPC
|
||||
// const path = await window.electronAPI.daemon.createDebugBundle(anonymize);
|
||||
// setBundlePath(path);
|
||||
|
||||
// Simulated for now
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
setBundlePath('/tmp/netbird-debug-bundle-20241030.zip');
|
||||
} catch (err) {
|
||||
setError('Failed to create debug bundle');
|
||||
console.error('Debug bundle error:', err);
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyPath = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(bundlePath);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy path:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-text-light text-neon mb-2">Debug Bundle</h1>
|
||||
<p className="text-text-muted">Create diagnostic bundle for troubleshooting</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Info Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="glass rounded-glass p-6 shadow-glass"
|
||||
>
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="p-3 bg-icy-blue/20 rounded-lg">
|
||||
<AlertCircle className="w-6 h-6 text-icy-blue" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-text-light mb-2">What's included?</h3>
|
||||
<ul className="space-y-2 text-sm text-text-muted">
|
||||
<li>• System information</li>
|
||||
<li>• NetBird configuration</li>
|
||||
<li>• Network interfaces</li>
|
||||
<li>• Routing tables</li>
|
||||
<li>• Daemon logs</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Anonymize option */}
|
||||
<div
|
||||
className="flex items-start gap-3 p-4 rounded-lg hover:bg-icy-blue/5 transition-all cursor-pointer border border-transparent hover:border-icy-blue/20"
|
||||
onClick={() => setAnonymize(!anonymize)}
|
||||
>
|
||||
<div
|
||||
className={`relative w-12 h-6 rounded-full p-1 transition-all ${
|
||||
anonymize ? 'bg-icy-blue shadow-icy-glow' : 'bg-text-muted/30'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: anonymize ? 24 : 0 }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||
className="w-4 h-4 bg-white rounded-full shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-text-light">Anonymize sensitive data</h3>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Replace IP addresses, emails, and other identifying information
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Create Button */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={handleCreateBundle}
|
||||
disabled={creating}
|
||||
className="w-full py-4 bg-icy-blue/30 text-icy-blue hover:bg-icy-blue/40 rounded-lg font-bold flex items-center justify-center gap-2 neon-border hover:neon-border-strong transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Package className="w-5 h-5" />
|
||||
{creating ? 'Creating Bundle...' : 'Create Debug Bundle'}
|
||||
</motion.button>
|
||||
|
||||
{/* Success message */}
|
||||
{bundlePath && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="glass rounded-glass p-6 border-2 border-icy-blue/30 shadow-glass"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-icy-blue/20 rounded-lg">
|
||||
<CheckCircle2 className="w-6 h-6 text-icy-blue" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-text-light mb-2">Bundle Created!</h3>
|
||||
<p className="text-sm text-text-muted mb-3">
|
||||
Your debug bundle has been created successfully
|
||||
</p>
|
||||
<div className="p-3 bg-dark-bg-card rounded-lg border border-icy-blue/10">
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
<p className="text-xs text-text-muted">File location:</p>
|
||||
<button
|
||||
onClick={handleCopyPath}
|
||||
className="p-1 hover:bg-icy-blue/20 rounded transition-all"
|
||||
title="Copy path"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-text-muted hover:text-icy-blue" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-icy-blue font-mono break-all">{bundlePath}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="glass rounded-glass p-6 border-2 border-red-500/30 shadow-glass"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-red-500/20 rounded-lg">
|
||||
<AlertCircle className="w-6 h-6 text-red-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-text-light mb-2">Error</h3>
|
||||
<p className="text-sm text-text-muted">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Additional Info */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="glass rounded-glass p-6 shadow-glass"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-text-muted/20 rounded-lg">
|
||||
<Bug className="w-6 h-6 text-text-muted" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-text-light mb-2">Need Help?</h3>
|
||||
<p className="text-sm text-text-muted mb-3">
|
||||
If you're experiencing issues, create a debug bundle and share it with the NetBird
|
||||
support team.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/netbirdio/netbird/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-icy-blue hover:underline"
|
||||
>
|
||||
Report an issue on GitHub →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
175
client/ui-wails/frontend/src/pages/Networks.tsx
Normal file
175
client/ui-wails/frontend/src/pages/Networks.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { RefreshCw, Globe, CheckCircle2, Circle } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
|
||||
export default function NetworksPage() {
|
||||
const { networks, networkFilter, setNetworkFilter, refreshNetworks, toggleNetwork } = useStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
refreshNetworks();
|
||||
}, [refreshNetworks]);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setLoading(true);
|
||||
await refreshNetworks();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleToggleNetwork = async (networkId: string) => {
|
||||
try {
|
||||
await toggleNetwork(networkId);
|
||||
} catch (error) {
|
||||
console.error('Toggle network error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredNetworks = networks.filter((network) => {
|
||||
if (networkFilter === 'all') return true;
|
||||
// Add filtering logic for overlapping and exit-nodes when available
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-8">
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex items-center justify-between mb-8"
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-text-light text-neon mb-2">Networks</h1>
|
||||
<p className="text-text-muted">Manage network routes and exit nodes</p>
|
||||
</div>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05, rotate: loading ? 360 : 0 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
className="p-3 bg-icy-blue/20 text-icy-blue rounded-lg hover:bg-icy-blue/30 neon-border hover:neon-border-strong transition-all disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
|
||||
{/* Filter tabs */}
|
||||
<div className="flex gap-2">
|
||||
{['all', 'overlapping', 'exit-nodes'].map((filter) => (
|
||||
<motion.button
|
||||
key={filter}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => setNetworkFilter(filter as any)}
|
||||
className={`px-6 py-2 rounded-lg font-medium transition-all ${
|
||||
networkFilter === filter
|
||||
? 'bg-icy-blue/30 text-icy-blue border border-icy-blue/30'
|
||||
: 'bg-dark-bg-card text-text-muted hover:text-text-light'
|
||||
}`}
|
||||
>
|
||||
{filter === 'all' ? 'All Networks' : filter === 'overlapping' ? 'Overlapping' : 'Exit Nodes'}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Networks list */}
|
||||
<div className="space-y-3">
|
||||
{filteredNetworks.length === 0 ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="glass rounded-glass p-12 text-center shadow-glass"
|
||||
>
|
||||
<Globe className="w-16 h-16 text-text-muted mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold text-text-light mb-2">No Networks Found</h3>
|
||||
<p className="text-text-muted">There are no networks available at the moment</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
filteredNetworks.map((network, index) => (
|
||||
<motion.div
|
||||
key={network.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className="glass rounded-glass p-6 cursor-pointer transition-all hover:bg-icy-blue/5"
|
||||
onClick={() => handleToggleNetwork(network.id)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={`p-3 rounded-lg ${
|
||||
network.selected
|
||||
? 'bg-icy-blue/20 text-icy-blue'
|
||||
: 'bg-text-muted/20 text-text-muted'
|
||||
}`}
|
||||
>
|
||||
{network.selected ? (
|
||||
<CheckCircle2 className="w-6 h-6" />
|
||||
) : (
|
||||
<Circle className="w-6 h-6" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-bold text-text-light">{network.id}</h3>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||
network.selected
|
||||
? 'bg-icy-blue/20 text-icy-blue'
|
||||
: 'bg-text-muted/20 text-text-muted'
|
||||
}`}
|
||||
>
|
||||
{network.selected ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-text-muted">Range:</span>
|
||||
<span className="text-text-light font-mono">{network.networkRange}</span>
|
||||
</div>
|
||||
|
||||
{network.domains && network.domains.length > 0 && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="text-text-muted">Domains:</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{network.domains.map((domain) => (
|
||||
<span
|
||||
key={domain}
|
||||
className="px-2 py-1 bg-dark-bg-card rounded text-text-light font-mono text-xs border border-icy-blue/10"
|
||||
>
|
||||
{domain}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{network.resolvedIPs && network.resolvedIPs.length > 0 && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="text-text-muted">IPs:</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{network.resolvedIPs.map((ip) => (
|
||||
<span
|
||||
key={ip}
|
||||
className="px-2 py-1 bg-dark-bg-card rounded text-text-light font-mono text-xs border border-icy-blue/10"
|
||||
>
|
||||
{ip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
282
client/ui-wails/frontend/src/pages/Overview.tsx
Normal file
282
client/ui-wails/frontend/src/pages/Overview.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
import { useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Wifi, WifiOff, Power, User, Shield, Zap, Globe, Activity, Users } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
|
||||
type Page = 'overview' | 'settings' | 'networks' | 'profiles' | 'debug' | 'peers';
|
||||
|
||||
interface OverviewProps {
|
||||
onNavigate: (page: Page) => void;
|
||||
}
|
||||
|
||||
export default function Overview({ onNavigate }: OverviewProps) {
|
||||
const { status, connected, loading, error, connect, disconnect, activeProfile, config, peers, refreshPeers } = useStore();
|
||||
|
||||
const connectedPeers = peers.filter(peer => peer.connStatus === 'Connected').length;
|
||||
|
||||
// Auto-refresh peers data every 5 seconds when connected
|
||||
useEffect(() => {
|
||||
if (connected && status === 'Connected') {
|
||||
// Initial refresh
|
||||
refreshPeers().catch(err => console.error('Failed to refresh peers:', err));
|
||||
|
||||
// Set up interval for continuous refresh
|
||||
const interval = setInterval(() => {
|
||||
if (connected && status === 'Connected') {
|
||||
refreshPeers().catch(err => console.error('Failed to refresh peers:', err));
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [connected, status, refreshPeers]);
|
||||
|
||||
const handleToggleConnection = async () => {
|
||||
if (connected) {
|
||||
await disconnect();
|
||||
} else {
|
||||
await connect();
|
||||
}
|
||||
};
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Shield,
|
||||
label: 'Allow SSH',
|
||||
enabled: config?.serverSSHAllowed,
|
||||
description: 'SSH server access',
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
label: 'Auto Connect',
|
||||
enabled: config?.autoConnect,
|
||||
description: 'Connect on startup',
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
label: 'Rosenpass',
|
||||
enabled: config?.rosenpassEnabled,
|
||||
description: 'Quantum resistance',
|
||||
},
|
||||
{
|
||||
icon: Activity,
|
||||
label: 'Lazy Connection',
|
||||
enabled: config?.lazyConnectionEnabled,
|
||||
description: 'On-demand peers',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Connection Status Card */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="glass rounded-glass p-8 shadow-glass"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-text-light mb-2 text-neon">Connection Status</h2>
|
||||
<p className="text-text-muted">Manage your NetBird VPN connection</p>
|
||||
</div>
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: connected ? [1, 1.05, 1] : 1,
|
||||
}}
|
||||
transition={{ duration: 2, repeat: connected ? Infinity : 0 }}
|
||||
className={`p-4 rounded-full ${
|
||||
connected ? 'bg-dark-bg-card neon-border-strong neon-pulse' : 'bg-dark-bg-card border border-icy-blue/20'
|
||||
}`}
|
||||
>
|
||||
{connected ? (
|
||||
<Wifi className="w-12 h-12 text-icy-blue drop-shadow-[0_0_10px_rgba(163,215,229,0.8)]" />
|
||||
) : (
|
||||
<WifiOff className="w-12 h-12 text-text-muted" />
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Status display and Peers Counter */}
|
||||
<div className="flex items-center justify-between gap-4 mb-6 flex-wrap">
|
||||
<div
|
||||
className={`px-6 py-3 rounded-lg font-semibold text-lg transition-all ${
|
||||
connected
|
||||
? 'bg-icy-blue/10 text-icy-blue neon-border shimmer'
|
||||
: 'bg-dark-bg-card text-text-muted border border-icy-blue/20'
|
||||
}`}
|
||||
>
|
||||
{status}
|
||||
</div>
|
||||
|
||||
{/* Connected Peers Counter - Only show when connected */}
|
||||
{connected && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => onNavigate('peers')}
|
||||
className="flex items-center gap-2 px-4 py-3 frosted rounded-lg neon-border cursor-pointer hover:neon-border-strong transition-all"
|
||||
>
|
||||
<Users className="w-5 h-5 text-icy-blue drop-shadow-[0_0_8px_rgba(163,215,229,0.6)]" />
|
||||
<span className="text-lg font-semibold text-text-light">
|
||||
<span className="text-icy-blue text-neon">{connectedPeers}</span>
|
||||
<span className="text-text-muted"> / {peers.length}</span>
|
||||
</span>
|
||||
<span className="text-xs text-text-muted">peers</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mb-4"
|
||||
>
|
||||
<p className="text-red-400 font-medium">⚠️ {error}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Connection Button */}
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handleToggleConnection}
|
||||
disabled={loading}
|
||||
className={`px-12 py-6 rounded-lg font-bold text-lg transition-all ${
|
||||
connected
|
||||
? 'bg-red-500/20 text-red-400 border-2 border-red-500/50 hover:bg-red-500/30'
|
||||
: 'bg-icy-blue/20 text-icy-blue border-2 border-icy-blue/50 hover:bg-icy-blue/30 neon-border'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
<Power className="w-8 h-8 inline mr-3" />
|
||||
{loading
|
||||
? connected
|
||||
? 'Disconnecting...'
|
||||
: 'Connecting...'
|
||||
: connected
|
||||
? 'Disconnect'
|
||||
: 'Connect'}
|
||||
</motion.button>
|
||||
{/* Status text below button */}
|
||||
<div className="text-center">
|
||||
<p className="text-text-light font-semibold text-xl">
|
||||
{loading
|
||||
? connected
|
||||
? 'Disconnecting...'
|
||||
: 'Connecting...'
|
||||
: status === 'NeedsLogin'
|
||||
? 'Login Required'
|
||||
: connected
|
||||
? 'Connected'
|
||||
: 'Disconnected'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Profile Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="glass rounded-glass p-6 shadow-glass"
|
||||
>
|
||||
<h3 className="text-xl font-bold text-text-light mb-4">Active Profile</h3>
|
||||
|
||||
{activeProfile ? (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
onClick={() => onNavigate('profiles')}
|
||||
className="flex items-center gap-4 p-4 frosted rounded-lg neon-border cursor-pointer hover:neon-border-strong transition-all"
|
||||
>
|
||||
<div className="p-3 bg-dark-bg-card rounded-lg border border-icy-blue/20">
|
||||
<User className="w-6 h-6 text-icy-blue drop-shadow-[0_0_8px_rgba(163,215,229,0.6)]" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-text-light">{activeProfile.name}</div>
|
||||
{activeProfile.email && (
|
||||
<div className="text-sm text-text-muted">{activeProfile.email}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted font-medium px-3 py-1 bg-dark-bg-card rounded">
|
||||
Click to manage
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
onClick={() => onNavigate('profiles')}
|
||||
className="text-center py-8 text-text-muted cursor-pointer hover:bg-dark-bg-card/30 rounded-lg transition-all"
|
||||
>
|
||||
<User className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>No active profile</p>
|
||||
<p className="text-sm mt-1">Click to configure a profile</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{features.map((feature, index) => {
|
||||
const Icon = feature.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.label}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 + index * 0.1 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => onNavigate('settings')}
|
||||
className={`frosted rounded-md p-6 transition-all cursor-pointer ${
|
||||
feature.enabled
|
||||
? 'neon-border'
|
||||
: 'border border-icy-blue/10 hover:border-icy-blue/20'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={`p-3 rounded-lg transition-all ${
|
||||
feature.enabled
|
||||
? 'bg-icy-blue/10 text-icy-blue border border-icy-blue/30'
|
||||
: 'bg-dark-bg-card text-text-muted border border-icy-blue/10'
|
||||
}`}
|
||||
>
|
||||
<Icon className={`w-6 h-6 ${feature.enabled ? 'drop-shadow-[0_0_8px_rgba(163,215,229,0.6)]' : ''}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold mb-1 ${
|
||||
feature.enabled ? 'text-icy-blue' : 'text-text-light'
|
||||
}`}>{feature.label}</h3>
|
||||
<p className="text-sm text-text-muted">{feature.description}</p>
|
||||
<div
|
||||
className={`mt-2 inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-all ${
|
||||
feature.enabled
|
||||
? 'bg-icy-blue/10 text-icy-blue border border-icy-blue/30'
|
||||
: 'bg-dark-bg-card text-text-muted border border-icy-blue/20'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-1.5 h-1.5 rounded-full transition-all ${
|
||||
feature.enabled ? 'bg-icy-blue icy-glow-animate' : 'bg-text-muted'
|
||||
}`} />
|
||||
{feature.enabled ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
382
client/ui-wails/frontend/src/pages/Peers.tsx
Normal file
382
client/ui-wails/frontend/src/pages/Peers.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Search, Users, Wifi, WifiOff, Shield, Activity, RefreshCw, Filter, Network, Copy, Check } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
|
||||
type Page = 'overview' | 'settings' | 'networks' | 'profiles' | 'debug' | 'peers';
|
||||
|
||||
interface PeersProps {
|
||||
onNavigate: (page: Page) => void;
|
||||
}
|
||||
|
||||
type ConnectionFilter = 'all' | 'connected' | 'disconnected' | 'relayed';
|
||||
|
||||
export default function Peers({ onNavigate }: PeersProps) {
|
||||
const { peers, refreshPeers, connected } = useStore();
|
||||
const [search, setSearch] = useState('');
|
||||
const [connectionFilter, setConnectionFilter] = useState<ConnectionFilter>('all');
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [copiedItems, setCopiedItems] = useState<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
refreshPeers();
|
||||
// Refresh peers every 5 seconds when connected
|
||||
const interval = setInterval(() => {
|
||||
if (connected) {
|
||||
refreshPeers();
|
||||
}
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [connected, refreshPeers]);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await refreshPeers();
|
||||
setTimeout(() => setRefreshing(false), 500);
|
||||
};
|
||||
|
||||
const handleCopy = async (text: string, itemId: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedItems(prev => ({ ...prev, [itemId]: true }));
|
||||
setTimeout(() => {
|
||||
setCopiedItems(prev => ({ ...prev, [itemId]: false }));
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Filter and search peers
|
||||
const filteredPeers = useMemo(() => {
|
||||
const filtered = peers.filter(peer => {
|
||||
// Connection filter
|
||||
if (connectionFilter === 'connected' && peer.connStatus !== 'Connected') return false;
|
||||
if (connectionFilter === 'disconnected' && peer.connStatus === 'Connected') return false;
|
||||
if (connectionFilter === 'relayed' && !peer.relayed) return false;
|
||||
|
||||
// Search filter
|
||||
if (search) {
|
||||
const searchLower = search.toLowerCase();
|
||||
return (
|
||||
peer.fqdn.toLowerCase().includes(searchLower) ||
|
||||
peer.ip.toLowerCase().includes(searchLower) ||
|
||||
peer.pubKey.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort by IP address to maintain stable list order
|
||||
return filtered.sort((a, b) => {
|
||||
// Convert IP addresses to comparable format
|
||||
const ipToNumber = (ip: string) => {
|
||||
const parts = ip.split('.').map(Number);
|
||||
return (parts[0] || 0) * 16777216 + (parts[1] || 0) * 65536 + (parts[2] || 0) * 256 + (parts[3] || 0);
|
||||
};
|
||||
return ipToNumber(a.ip) - ipToNumber(b.ip);
|
||||
});
|
||||
}, [peers, search, connectionFilter]);
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatLatency = (ms: number) => {
|
||||
if (ms === 0) return 'N/A';
|
||||
return `${ms.toFixed(0)}ms`;
|
||||
};
|
||||
|
||||
const getConnectionColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Connected':
|
||||
return 'text-icy-blue';
|
||||
case 'Connecting':
|
||||
return 'text-yellow-400';
|
||||
default:
|
||||
return 'text-text-muted';
|
||||
}
|
||||
};
|
||||
|
||||
const getConnectionIcon = (status: string) => {
|
||||
return status === 'Connected' ? Wifi : WifiOff;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-8">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-icy-blue/20 rounded-lg neon-border">
|
||||
<Users className="w-8 h-8 text-icy-blue drop-shadow-[0_0_10px_rgba(163,215,229,0.8)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-text-light text-neon">Peers</h1>
|
||||
<p className="text-text-muted">
|
||||
{filteredPeers.length} of {peers.length} peer{peers.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="p-3 bg-icy-blue/20 text-icy-blue rounded-lg hover:bg-icy-blue/30 neon-border hover:neon-border-strong transition-all disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-5 h-5 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="glass rounded-glass p-6 shadow-glass"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Connection Filter - First Row */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['all', 'connected', 'disconnected', 'relayed'] as ConnectionFilter[]).map((filter) => (
|
||||
<button
|
||||
key={filter}
|
||||
onClick={() => setConnectionFilter(filter)}
|
||||
className={`px-4 py-3 rounded-lg font-medium transition-all ${
|
||||
connectionFilter === filter
|
||||
? 'bg-icy-blue/30 text-icy-blue border border-icy-blue/30'
|
||||
: 'bg-dark-bg-card text-text-muted hover:bg-icy-blue/10 border border-transparent'
|
||||
}`}
|
||||
>
|
||||
{filter.charAt(0).toUpperCase() + filter.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search - Second Row */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-text-muted w-5 h-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by FQDN, IP, or public key..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3 bg-dark-bg-card border border-icy-blue/20 rounded-lg text-text-light placeholder-text-muted focus:outline-none focus:border-icy-blue/50 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Peer List */}
|
||||
<AnimatePresence mode="popLayout">
|
||||
{filteredPeers.length === 0 ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="glass rounded-glass p-12 text-center shadow-glass"
|
||||
>
|
||||
<Users className="w-16 h-16 mx-auto mb-4 text-text-muted opacity-50" />
|
||||
<h3 className="text-xl font-semibold text-text-light mb-2">No peers found</h3>
|
||||
<p className="text-text-muted">
|
||||
{!connected
|
||||
? 'Connect to NetBird to see your peers'
|
||||
: search || connectionFilter !== 'all'
|
||||
? 'Try adjusting your search or filters'
|
||||
: 'No peers are currently available'}
|
||||
</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredPeers.map((peer, index) => {
|
||||
const Icon = getConnectionIcon(peer.connStatus);
|
||||
return (
|
||||
<motion.div
|
||||
key={peer.pubKey}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className="glass rounded-glass p-6 hover:bg-icy-blue/5 transition-all shadow-glass"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Status Icon */}
|
||||
<div
|
||||
className={`p-3 rounded-lg ${
|
||||
peer.connStatus === 'Connected'
|
||||
? 'bg-icy-blue/30 text-icy-blue'
|
||||
: 'bg-text-muted/20 text-text-muted'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
{/* Peer Info */}
|
||||
<div className="flex-1 space-y-3">
|
||||
{/* Main Info */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-xl font-semibold text-text-light">
|
||||
{peer.fqdn || peer.ip || 'Unknown Peer'}
|
||||
</h3>
|
||||
{peer.fqdn && (
|
||||
<button
|
||||
onClick={() => handleCopy(peer.fqdn, `fqdn-${peer.pubKey}`)}
|
||||
className="p-1 hover:bg-icy-blue/20 rounded transition-all"
|
||||
title="Copy FQDN"
|
||||
>
|
||||
{copiedItems[`fqdn-${peer.pubKey}`] ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-text-muted hover:text-icy-blue" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<p className="text-sm text-text-muted">{peer.ip}</p>
|
||||
<button
|
||||
onClick={() => handleCopy(peer.ip, `ip-${peer.pubKey}`)}
|
||||
className="p-1 hover:bg-icy-blue/20 rounded transition-all"
|
||||
title="Copy IP"
|
||||
>
|
||||
{copiedItems[`ip-${peer.pubKey}`] ? (
|
||||
<Check className="w-3 h-3 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3 text-text-muted hover:text-icy-blue" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{peer.rosenpassEnabled && (
|
||||
<span className="px-2 py-1 bg-icy-blue/20 text-icy-blue text-xs font-medium rounded border border-icy-blue/30">
|
||||
<Shield className="w-3 h-3 inline mr-1" />
|
||||
Quantum-Safe
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`px-3 py-1 rounded text-sm font-medium ${
|
||||
peer.connStatus === 'Connected'
|
||||
? 'bg-icy-blue/20 text-icy-blue border border-icy-blue/30'
|
||||
: 'bg-text-muted/20 text-text-muted border border-text-muted/20'
|
||||
}`}
|
||||
>
|
||||
{peer.connStatus}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection Details Grid */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Connection Type */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-text-muted uppercase">Connection</p>
|
||||
<p className="text-sm font-medium text-text-light">
|
||||
{peer.relayed ? (
|
||||
<span className="text-yellow-400">
|
||||
<Network className="w-4 h-4 inline mr-1" />
|
||||
Relayed
|
||||
</span>
|
||||
) : peer.connStatus === 'Connected' ? (
|
||||
<span className="text-icy-blue">Direct P2P</span>
|
||||
) : (
|
||||
<span className="text-text-muted">-</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Latency */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-text-muted uppercase">Latency</p>
|
||||
<p className="text-sm font-medium text-text-light">
|
||||
<Activity className="w-4 h-4 inline mr-1 text-icy-blue" />
|
||||
{formatLatency(peer.latency)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Data Transferred */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-text-muted uppercase">Received</p>
|
||||
<p className="text-sm font-medium text-text-light">
|
||||
{formatBytes(peer.bytesRx)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-text-muted uppercase">Sent</p>
|
||||
<p className="text-sm font-medium text-text-light">
|
||||
{formatBytes(peer.bytesTx)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ICE Candidates */}
|
||||
{peer.connStatus === 'Connected' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 pt-2 border-t border-icy-blue/10">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-text-muted uppercase">Local Endpoint</p>
|
||||
<p className="text-xs font-mono text-text-light break-all">
|
||||
{peer.localIceCandidateType && `${peer.localIceCandidateType}: `}
|
||||
{peer.localIceCandidateEndpoint || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-text-muted uppercase">Remote Endpoint</p>
|
||||
<p className="text-xs font-mono text-text-light break-all">
|
||||
{peer.remoteIceCandidateType && `${peer.remoteIceCandidateType}: `}
|
||||
{peer.remoteIceCandidateEndpoint || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Networks */}
|
||||
{peer.networks && peer.networks.length > 0 && (
|
||||
<div className="space-y-1 pt-2">
|
||||
<p className="text-xs text-text-muted uppercase">Networks</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{peer.networks.map((network) => (
|
||||
<span
|
||||
key={network}
|
||||
className="px-2 py-1 bg-dark-bg-card text-text-light text-xs rounded border border-icy-blue/20"
|
||||
>
|
||||
{network}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Public Key - Collapsed by default */}
|
||||
<details className="pt-2">
|
||||
<summary className="text-xs text-text-muted uppercase cursor-pointer hover:text-icy-blue transition-colors">
|
||||
Public Key
|
||||
</summary>
|
||||
<p className="text-xs font-mono text-text-light break-all mt-2 p-2 bg-dark-bg-card rounded border border-icy-blue/10">
|
||||
{peer.pubKey}
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
237
client/ui-wails/frontend/src/pages/Profiles.tsx
Normal file
237
client/ui-wails/frontend/src/pages/Profiles.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { User, CheckCircle2, RefreshCw, Trash2, Plus, X } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
|
||||
export default function ProfilesPage() {
|
||||
const { profiles, activeProfile, refreshProfiles, switchProfile, addProfile, removeProfile } = useStore();
|
||||
const [deletingProfile, setDeletingProfile] = useState<string | null>(null);
|
||||
const [isAddingProfile, setIsAddingProfile] = useState(false);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [newProfileName, setNewProfileName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
refreshProfiles();
|
||||
}, [refreshProfiles]);
|
||||
|
||||
const handleSwitchProfile = async (profileId: string) => {
|
||||
console.log('Switching to profile:', profileId);
|
||||
try {
|
||||
await switchProfile(profileId);
|
||||
console.log('Switch profile call completed');
|
||||
// Refresh profiles to get updated active state
|
||||
await refreshProfiles();
|
||||
console.log('Profiles refreshed after switch');
|
||||
} catch (error) {
|
||||
console.error('Switch profile error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddProfileClick = () => {
|
||||
setShowAddForm(true);
|
||||
setNewProfileName('');
|
||||
};
|
||||
|
||||
const handleAddProfileSubmit = async () => {
|
||||
if (!newProfileName || newProfileName.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsAddingProfile(true);
|
||||
await addProfile(newProfileName.trim());
|
||||
await refreshProfiles();
|
||||
setShowAddForm(false);
|
||||
setNewProfileName('');
|
||||
} catch (error) {
|
||||
console.error('Add profile error:', error);
|
||||
alert('Failed to add profile');
|
||||
} finally {
|
||||
setIsAddingProfile(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddProfileCancel = () => {
|
||||
setShowAddForm(false);
|
||||
setNewProfileName('');
|
||||
};
|
||||
|
||||
const handleDeleteProfile = async (profileId: string, event: React.MouseEvent) => {
|
||||
event.stopPropagation(); // Prevent profile switching when clicking delete
|
||||
|
||||
if (!confirm(`Are you sure you want to delete the profile "${profileId}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingProfile(profileId);
|
||||
await removeProfile(profileId);
|
||||
await refreshProfiles();
|
||||
} catch (error) {
|
||||
console.error('Delete profile error:', error);
|
||||
alert('Failed to delete profile');
|
||||
} finally {
|
||||
setDeletingProfile(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Use profiles as-is without sorting
|
||||
const sortedProfiles = profiles;
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-text-light mb-2">Profiles</h1>
|
||||
<p className="text-text-muted">Manage your NetBird profiles</p>
|
||||
</motion.div>
|
||||
|
||||
{/* All Profiles */}
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xl font-bold text-text-light">All Profiles</h2>
|
||||
{sortedProfiles.length === 0 ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="glass rounded-glass p-12 text-center shadow-glass"
|
||||
>
|
||||
<User className="w-16 h-16 text-text-muted mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold text-text-light mb-2">No Profiles</h3>
|
||||
<p className="text-text-muted">Add a profile to get started</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
sortedProfiles.map((profile, index) => {
|
||||
// Use the active flag from the profile (set by daemon)
|
||||
const isActive = profile.active;
|
||||
return (
|
||||
<motion.div
|
||||
key={profile.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className={`glass glass-hover rounded-glass p-6 cursor-pointer transition-all ${
|
||||
isActive ? 'border-2 border-icy-blue/20' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
console.log('Clicked profile:', profile.id, 'isActive:', isActive);
|
||||
if (!isActive) {
|
||||
handleSwitchProfile(profile.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center ${
|
||||
isActive ? 'bg-icy-blue/20' : 'bg-text-muted/20'
|
||||
}`}
|
||||
>
|
||||
<User
|
||||
className={`w-6 h-6 ${
|
||||
isActive ? 'text-icy-blue' : 'text-text-muted'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-bold text-text-light">{profile.name}</h3>
|
||||
{isActive && (
|
||||
<span className="px-2 py-1 bg-icy-blue/20 text-icy-blue rounded-full text-xs font-medium">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{profile.email && (
|
||||
<p className="text-sm text-text-muted mt-1">{profile.email}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isActive && <CheckCircle2 className="w-5 h-5 text-icy-blue" />}
|
||||
{!isActive && (
|
||||
<button
|
||||
onClick={(e) => handleDeleteProfile(profile.id, e)}
|
||||
disabled={deletingProfile === profile.id}
|
||||
className="p-2 rounded-lg hover:bg-red-500/20 text-text-muted hover:text-red-400 transition-all disabled:opacity-50"
|
||||
title="Delete profile"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Add Profile Button / Form */}
|
||||
{!showAddForm ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: sortedProfiles.length * 0.05 }}
|
||||
className="glass glass-hover rounded-glass p-6 cursor-pointer transition-all border-2 border-dashed border-text-muted/20 hover:border-icy-blue/40"
|
||||
onClick={handleAddProfileClick}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center bg-text-muted/10">
|
||||
<Plus className="w-6 h-6 text-text-muted" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-text-muted">Add Profile</h3>
|
||||
<p className="text-sm text-text-muted/70 mt-1">Create a new profile</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="glass rounded-glass p-6 border-2 border-icy-blue/40 shadow-glass"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center bg-icy-blue/20">
|
||||
<Plus className="w-6 h-6 text-icy-blue" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={newProfileName}
|
||||
onChange={(e) => setNewProfileName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleAddProfileSubmit();
|
||||
if (e.key === 'Escape') handleAddProfileCancel();
|
||||
}}
|
||||
placeholder="Enter profile name..."
|
||||
className="w-full px-3 py-2 bg-background-dark border border-text-muted/20 rounded-lg text-text-light placeholder-text-muted/50 focus:outline-none focus:border-icy-blue/50"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleAddProfileCancel}
|
||||
className="p-2 rounded-lg hover:bg-text-muted/10 text-text-muted hover:text-text-light transition-all"
|
||||
title="Cancel"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddProfileSubmit}
|
||||
disabled={isAddingProfile || !newProfileName.trim()}
|
||||
className="px-4 py-2 rounded-lg bg-icy-blue text-text-light hover:bg-icy-blue/80 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isAddingProfile ? 'Adding...' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
355
client/ui-wails/frontend/src/pages/Settings.tsx
Normal file
355
client/ui-wails/frontend/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,355 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Save, Shield, Zap, Globe, Activity, Lock, Monitor } from 'lucide-react';
|
||||
import { useStore } from '../store/useStore';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { config, refreshConfig, updateConfig } = useStore();
|
||||
const [formData, setFormData] = useState({
|
||||
managementUrl: '',
|
||||
preSharedKey: '',
|
||||
interfaceName: '',
|
||||
wireguardPort: 51820,
|
||||
mtu: 1280,
|
||||
serverSSHAllowed: false,
|
||||
autoConnect: false,
|
||||
rosenpassEnabled: false,
|
||||
rosenpassPermissive: false,
|
||||
lazyConnectionEnabled: false,
|
||||
blockInbound: false,
|
||||
networkMonitor: false,
|
||||
disableDns: false,
|
||||
disableClientRoutes: false,
|
||||
disableServerRoutes: false,
|
||||
blockLanAccess: false,
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setFormData(config);
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSaved(false);
|
||||
await updateConfig(formData);
|
||||
await refreshConfig();
|
||||
setSaved(true);
|
||||
// Auto-clear success message after 3 seconds
|
||||
setTimeout(() => setSaved(false), 3000);
|
||||
} catch (error: any) {
|
||||
console.error('Save error:', error);
|
||||
setError(error?.message || 'Failed to save settings');
|
||||
// Auto-clear error after 5 seconds
|
||||
setTimeout(() => setError(null), 5000);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSettings = [
|
||||
{
|
||||
key: 'serverSSHAllowed',
|
||||
icon: Shield,
|
||||
label: 'Allow SSH',
|
||||
description: 'Enable SSH server role for remote access',
|
||||
},
|
||||
{
|
||||
key: 'autoConnect',
|
||||
icon: Zap,
|
||||
label: 'Auto Connect',
|
||||
description: 'Automatically connect when the service starts',
|
||||
},
|
||||
{
|
||||
key: 'rosenpassEnabled',
|
||||
icon: Globe,
|
||||
label: 'Enable Rosenpass',
|
||||
description: 'Add post-quantum encryption layer',
|
||||
},
|
||||
{
|
||||
key: 'rosenpassPermissive',
|
||||
icon: Globe,
|
||||
label: 'Rosenpass Permissive Mode',
|
||||
description: 'Allow fallback if Rosenpass fails',
|
||||
},
|
||||
{
|
||||
key: 'lazyConnectionEnabled',
|
||||
icon: Activity,
|
||||
label: 'Enable Lazy Connections',
|
||||
description: 'Defer peer initialization until needed (experimental)',
|
||||
},
|
||||
{
|
||||
key: 'blockInbound',
|
||||
icon: Lock,
|
||||
label: 'Block Inbound Connections',
|
||||
description: 'Prevent inbound connections via firewall',
|
||||
},
|
||||
{
|
||||
key: 'networkMonitor',
|
||||
icon: Monitor,
|
||||
label: 'Network Monitor',
|
||||
description: 'Restart connection on network changes',
|
||||
},
|
||||
{
|
||||
key: 'blockLanAccess',
|
||||
icon: Lock,
|
||||
label: 'Block LAN Access',
|
||||
description: 'Disable access to local network',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-8"
|
||||
>
|
||||
<h1 className="text-3xl font-bold text-text-light text-neon mb-2">Settings</h1>
|
||||
<p className="text-text-muted">Configure your NetBird connection</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Connection Settings */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="glass rounded-glass p-6 shadow-glass"
|
||||
>
|
||||
<h2 className="text-xl font-bold text-text-light mb-6">Connection</h2>
|
||||
<div className="space-y-4">
|
||||
<InputField
|
||||
label="Management URL"
|
||||
value={formData.managementUrl}
|
||||
onChange={(value) => setFormData({ ...formData, managementUrl: value })}
|
||||
placeholder="https://api.netbird.io"
|
||||
/>
|
||||
<InputField
|
||||
label="Pre-shared Key"
|
||||
value={formData.preSharedKey}
|
||||
onChange={(value) => setFormData({ ...formData, preSharedKey: value })}
|
||||
placeholder="Optional WireGuard PSK"
|
||||
type="password"
|
||||
/>
|
||||
<InputField
|
||||
label="Interface Name"
|
||||
value={formData.interfaceName}
|
||||
onChange={(value) => setFormData({ ...formData, interfaceName: value })}
|
||||
placeholder="wt0"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InputField
|
||||
label="WireGuard Port"
|
||||
value={formData.wireguardPort.toString()}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, wireguardPort: parseInt(value) || 51820 })
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
<InputField
|
||||
label="MTU"
|
||||
value={formData.mtu.toString()}
|
||||
onChange={(value) => setFormData({ ...formData, mtu: parseInt(value) || 1280 })}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Feature Toggles */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="glass rounded-glass p-6 shadow-glass"
|
||||
>
|
||||
<h2 className="text-xl font-bold text-text-light mb-6">Features</h2>
|
||||
<div className="space-y-3">
|
||||
{toggleSettings.map((setting, index) => {
|
||||
const Icon = setting.icon;
|
||||
const isEnabled = formData[setting.key as keyof typeof formData] as boolean;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={setting.key}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className="flex items-start gap-4 p-4 rounded-lg hover:bg-icy-blue/5 transition-all cursor-pointer border border-transparent hover:border-icy-blue/20"
|
||||
onClick={() => setFormData({ ...formData, [setting.key]: !isEnabled })}
|
||||
>
|
||||
<div
|
||||
className={`p-3 rounded-lg transition-all ${
|
||||
isEnabled ? 'bg-icy-blue/20 text-icy-blue border border-icy-blue/30' : 'bg-text-muted/20 text-text-muted border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<Icon className={`w-5 h-5 ${isEnabled ? 'drop-shadow-[0_0_8px_rgba(163,215,229,0.6)]' : ''}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-text-light">{setting.label}</h3>
|
||||
<p className="text-sm text-text-muted mt-1">{setting.description}</p>
|
||||
</div>
|
||||
<div
|
||||
className={`relative w-12 h-6 rounded-full p-1 transition-all ${
|
||||
isEnabled ? 'bg-icy-blue shadow-icy-glow' : 'bg-text-muted/30'
|
||||
}`}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: isEnabled ? 24 : 0 }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||
className="w-4 h-4 bg-white rounded-full shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Advanced Settings */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="glass rounded-glass p-6 shadow-glass"
|
||||
>
|
||||
<h2 className="text-xl font-bold text-text-light mb-6">Advanced</h2>
|
||||
<div className="space-y-3">
|
||||
<CheckboxField
|
||||
label="Disable DNS Management"
|
||||
checked={formData.disableDns}
|
||||
onChange={(checked) => setFormData({ ...formData, disableDns: checked })}
|
||||
description="Keep system DNS unchanged"
|
||||
/>
|
||||
<CheckboxField
|
||||
label="Disable Client Routes"
|
||||
checked={formData.disableClientRoutes}
|
||||
onChange={(checked) => setFormData({ ...formData, disableClientRoutes: checked })}
|
||||
description="Don't route traffic to peers"
|
||||
/>
|
||||
<CheckboxField
|
||||
label="Disable Server Routes"
|
||||
checked={formData.disableServerRoutes}
|
||||
onChange={(checked) => setFormData({ ...formData, disableServerRoutes: checked })}
|
||||
description="Don't act as a router for peers"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Feedback Messages */}
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-red-500/10 border border-red-500/30 rounded-lg p-4"
|
||||
>
|
||||
<p className="text-red-400 font-medium">⚠️ {error}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{saved && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-green-500/10 border border-green-500/30 rounded-lg p-4"
|
||||
>
|
||||
<p className="text-green-400 font-medium">✓ Settings saved successfully!</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
<motion.button
|
||||
whileHover={{ scale: saving ? 1 : 1.02 }}
|
||||
whileTap={{ scale: saving ? 1 : 0.98 }}
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="w-full py-4 bg-icy-blue/30 text-icy-blue hover:bg-icy-blue/40 rounded-lg font-bold flex items-center justify-center gap-2 neon-border hover:neon-border-strong transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-5 h-5" />
|
||||
Save Settings
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
type = 'text',
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-muted mb-2">{label}</label>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full px-4 py-3 bg-dark-bg-card border border-icy-blue/20 rounded-lg text-text-light placeholder-text-muted focus:border-icy-blue focus:outline-none focus:ring-2 focus:ring-icy-blue/20 transition-all"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckboxField({
|
||||
label,
|
||||
checked,
|
||||
onChange,
|
||||
description,
|
||||
}: {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-start gap-3 p-4 rounded-lg hover:bg-icy-blue/5 transition-all cursor-pointer"
|
||||
onClick={() => onChange(!checked)}
|
||||
>
|
||||
<div
|
||||
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all ${
|
||||
checked ? 'bg-icy-blue border-icy-blue' : 'border-text-muted/30'
|
||||
}`}
|
||||
>
|
||||
{checked && (
|
||||
<svg className="w-3 h-3 text-dark-bg" fill="currentColor" viewBox="0 0 12 12">
|
||||
<path d="M10 3L4.5 8.5L2 6" stroke="currentColor" strokeWidth="2" fill="none" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-text-light">{label}</h3>
|
||||
<p className="text-sm text-text-muted mt-1">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
321
client/ui-wails/frontend/src/store/useStore.ts
Normal file
321
client/ui-wails/frontend/src/store/useStore.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface Config {
|
||||
managementUrl: string;
|
||||
preSharedKey: string;
|
||||
interfaceName: string;
|
||||
wireguardPort: number;
|
||||
mtu: number;
|
||||
serverSSHAllowed: boolean;
|
||||
autoConnect: boolean;
|
||||
rosenpassEnabled: boolean;
|
||||
rosenpassPermissive: boolean;
|
||||
lazyConnectionEnabled: boolean;
|
||||
blockInbound: boolean;
|
||||
networkMonitor: boolean;
|
||||
disableDns: boolean;
|
||||
disableClientRoutes: boolean;
|
||||
disableServerRoutes: boolean;
|
||||
blockLanAccess: boolean;
|
||||
}
|
||||
|
||||
interface Network {
|
||||
id: string;
|
||||
networkRange: string;
|
||||
domains: string[];
|
||||
resolvedIPs: string[];
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
id: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface Peer {
|
||||
ip: string;
|
||||
pubKey: string;
|
||||
connStatus: string;
|
||||
connStatusUpdate: string;
|
||||
relayed: boolean;
|
||||
localIceCandidateType: string;
|
||||
remoteIceCandidateType: string;
|
||||
fqdn: string;
|
||||
localIceCandidateEndpoint: string;
|
||||
remoteIceCandidateEndpoint: string;
|
||||
lastWireguardHandshake: string;
|
||||
bytesRx: number;
|
||||
bytesTx: number;
|
||||
rosenpassEnabled: boolean;
|
||||
networks: string[];
|
||||
latency: number;
|
||||
relayAddress: string;
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
// Connection state
|
||||
status: string;
|
||||
connected: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Configuration
|
||||
config: Config | null;
|
||||
|
||||
// Networks
|
||||
networks: Network[];
|
||||
networkFilter: 'all' | 'overlapping' | 'exit-nodes';
|
||||
|
||||
// Profiles
|
||||
profiles: Profile[];
|
||||
activeProfile: Profile | null;
|
||||
|
||||
// Peers
|
||||
peers: Peer[];
|
||||
localPeer: any | null;
|
||||
|
||||
// Actions
|
||||
setStatus: (status: string, connected: boolean) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setConfig: (config: Config) => void;
|
||||
setNetworks: (networks: Network[]) => void;
|
||||
setNetworkFilter: (filter: 'all' | 'overlapping' | 'exit-nodes') => void;
|
||||
setProfiles: (profiles: Profile[]) => void;
|
||||
setActiveProfile: (profile: Profile | null) => void;
|
||||
setPeers: (peers: Peer[]) => void;
|
||||
setLocalPeer: (localPeer: any) => void;
|
||||
|
||||
// Daemon operations
|
||||
connect: () => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
|
||||
// Data refresh operations
|
||||
refreshStatus: () => Promise<void>;
|
||||
refreshConfig: () => Promise<void>;
|
||||
updateConfig: (config: Config) => Promise<void>;
|
||||
refreshNetworks: () => Promise<void>;
|
||||
toggleNetwork: (networkId: string) => Promise<void>;
|
||||
refreshProfiles: () => Promise<void>;
|
||||
switchProfile: (profileId: string) => Promise<void>;
|
||||
deleteProfile: (profileId: string) => Promise<void>;
|
||||
addProfile: (name: string) => Promise<void>;
|
||||
removeProfile: (profileId: string) => Promise<void>;
|
||||
refreshPeers: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useStore = create<AppState>((set, get) => ({
|
||||
// Initial state
|
||||
status: 'Disconnected',
|
||||
connected: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
config: null,
|
||||
networks: [],
|
||||
networkFilter: 'all',
|
||||
profiles: [],
|
||||
activeProfile: null,
|
||||
peers: [],
|
||||
localPeer: null,
|
||||
|
||||
// State setters
|
||||
setStatus: (status, connected) => set({ status, connected }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
setConfig: (config) => set({ config }),
|
||||
setNetworks: (networks) => set({ networks }),
|
||||
setNetworkFilter: (filter) => set({ networkFilter: filter }),
|
||||
setProfiles: (profiles) => set({ profiles }),
|
||||
setActiveProfile: (profile) => set({ activeProfile: profile }),
|
||||
setPeers: (peers) => set({ peers }),
|
||||
setLocalPeer: (localPeer) => set({ localPeer }),
|
||||
|
||||
// Daemon operations (placeholder implementations)
|
||||
connect: async () => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
// TODO: Call Wails Go backend method
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
||||
set({ status: 'Connected', connected: true });
|
||||
} catch (error: any) {
|
||||
console.error('Connect error:', error);
|
||||
set({ error: error?.message || 'Failed to connect' });
|
||||
setTimeout(() => set({ error: null }), 5000);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
disconnect: async () => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
// TODO: Call Wails Go backend method
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API call
|
||||
set({ status: 'Disconnected', connected: false });
|
||||
} catch (error: any) {
|
||||
console.error('Disconnect error:', error);
|
||||
set({ error: error?.message || 'Failed to disconnect' });
|
||||
setTimeout(() => set({ error: null }), 5000);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
// TODO: Call Wails Go backend method
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API call
|
||||
set({ status: 'Logged Out', connected: false, activeProfile: null });
|
||||
} catch (error: any) {
|
||||
console.error('Logout error:', error);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Data refresh operations (placeholder implementations with mock data)
|
||||
refreshStatus: async () => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
const mockStatus = {
|
||||
status: 'Disconnected',
|
||||
daemon: 'Connected',
|
||||
};
|
||||
set({
|
||||
status: mockStatus.status,
|
||||
connected: mockStatus.status === 'Connected',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Status refresh error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
refreshConfig: async () => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
const mockConfig: Config = {
|
||||
managementUrl: 'https://api.netbird.io:443',
|
||||
preSharedKey: '',
|
||||
interfaceName: 'wt0',
|
||||
wireguardPort: 51820,
|
||||
mtu: 1280,
|
||||
serverSSHAllowed: false,
|
||||
autoConnect: false,
|
||||
rosenpassEnabled: false,
|
||||
rosenpassPermissive: false,
|
||||
lazyConnectionEnabled: false,
|
||||
blockInbound: false,
|
||||
networkMonitor: true,
|
||||
disableDns: false,
|
||||
disableClientRoutes: false,
|
||||
disableServerRoutes: false,
|
||||
blockLanAccess: false,
|
||||
};
|
||||
set({ config: mockConfig });
|
||||
} catch (error) {
|
||||
console.error('Config refresh error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
updateConfig: async (config: Config) => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API call
|
||||
set({ config });
|
||||
} catch (error: any) {
|
||||
console.error('Config update error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
refreshNetworks: async () => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
const mockNetworks: Network[] = [];
|
||||
set({ networks: mockNetworks });
|
||||
} catch (error) {
|
||||
console.error('Networks refresh error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
toggleNetwork: async (networkId: string) => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
const networks = get().networks.map(net =>
|
||||
net.id === networkId ? { ...net, selected: !net.selected } : net
|
||||
);
|
||||
set({ networks });
|
||||
} catch (error) {
|
||||
console.error('Toggle network error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
refreshProfiles: async () => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
const mockProfiles: Profile[] = [];
|
||||
set({ profiles: mockProfiles });
|
||||
} catch (error) {
|
||||
console.error('Profiles refresh error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
switchProfile: async (profileId: string) => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API call
|
||||
const profile = get().profiles.find(p => p.id === profileId);
|
||||
if (profile) {
|
||||
set({ activeProfile: profile });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Switch profile error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
deleteProfile: async (profileId: string) => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API call
|
||||
const profiles = get().profiles.filter(p => p.id !== profileId);
|
||||
set({ profiles });
|
||||
} catch (error) {
|
||||
console.error('Delete profile error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
addProfile: async (name: string) => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API call
|
||||
await get().refreshProfiles();
|
||||
} catch (error) {
|
||||
console.error('Add profile error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
removeProfile: async (profileId: string) => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate API call
|
||||
const profiles = get().profiles.filter(p => p.id !== profileId);
|
||||
set({ profiles });
|
||||
} catch (error) {
|
||||
console.error('Remove profile error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
refreshPeers: async () => {
|
||||
try {
|
||||
// TODO: Call Wails Go backend method
|
||||
const mockPeers: Peer[] = [];
|
||||
set({ peers: mockPeers });
|
||||
} catch (error) {
|
||||
console.error('Peers refresh error:', error);
|
||||
}
|
||||
},
|
||||
}));
|
||||
26
client/ui-wails/frontend/src/style.css
Normal file
26
client/ui-wails/frontend/src/style.css
Normal file
@@ -0,0 +1,26 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
1
client/ui-wails/frontend/src/vite-env.d.ts
vendored
Normal file
1
client/ui-wails/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
39
client/ui-wails/frontend/tailwind.config.cjs
Normal file
39
client/ui-wails/frontend/tailwind.config.cjs
Normal file
@@ -0,0 +1,39 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
icy: {
|
||||
blue: '#a3d7e5',
|
||||
'blue-dark': '#8cc8d7',
|
||||
'blue-light': '#c8ebf5',
|
||||
'blue-alpha': 'rgba(163, 215, 229, 0.3)',
|
||||
},
|
||||
dark: {
|
||||
bg: '#121218',
|
||||
'bg-light': '#18181e',
|
||||
'bg-card': '#1c1c23',
|
||||
view: '#101014',
|
||||
},
|
||||
text: {
|
||||
light: '#f8f8fc',
|
||||
muted: '#a0a0aa',
|
||||
dark: '#0a0a0f',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
'glass': '12px',
|
||||
},
|
||||
boxShadow: {
|
||||
'glass': '0 8px 32px 0 rgba(163, 215, 229, 0.1)',
|
||||
'glass-hover': '0 8px 32px 0 rgba(163, 215, 229, 0.2)',
|
||||
'icy-glow': '0 0 20px rgba(163, 215, 229, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
31
client/ui-wails/frontend/tsconfig.json
Normal file
31
client/ui-wails/frontend/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
client/ui-wails/frontend/tsconfig.node.json
Normal file
11
client/ui-wails/frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
7
client/ui-wails/frontend/vite.config.ts
Normal file
7
client/ui-wails/frontend/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})
|
||||
4
client/ui-wails/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
4
client/ui-wails/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
7
client/ui-wails/frontend/wailsjs/go/main/App.js
Executable file
7
client/ui-wails/frontend/wailsjs/go/main/App.js
Executable file
@@ -0,0 +1,7 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
24
client/ui-wails/frontend/wailsjs/runtime/package.json
Normal file
24
client/ui-wails/frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
249
client/ui-wails/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
client/ui-wails/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
238
client/ui-wails/frontend/wailsjs/runtime/runtime.js
Normal file
238
client/ui-wails/frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
Reference in New Issue
Block a user