mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
refactor(windows): Windows UI polish (#3338)
- Add Tailwind and Flowbite for consistent UI --------- Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com> Co-authored-by: Reactor Scram <ReactorScram@users.noreply.github.com>
This commit is contained in:
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -68,6 +68,10 @@ updates:
|
||||
directory: website/
|
||||
schedule:
|
||||
interval: monthly
|
||||
- package-ecosystem: npm
|
||||
directory: rust/windows-client/
|
||||
schedule:
|
||||
interval: monthly
|
||||
- package-ecosystem: npm
|
||||
directory: elixir/apps/web/assets/
|
||||
schedule:
|
||||
|
||||
2
.github/workflows/_rust.yml
vendored
2
.github/workflows/_rust.yml
vendored
@@ -103,7 +103,7 @@ jobs:
|
||||
|
||||
# Used for release artifact
|
||||
# In release mode the name comes from tauri.conf.json
|
||||
cp "target/release/Firezone Windows Client.exe" "${{ env.BINARY_DEST_PATH }}-x64.exe"
|
||||
cp "target/release/Firezone.exe" "${{ env.BINARY_DEST_PATH }}-x64.exe"
|
||||
cp "target/release/bundle/msi/*.msi" "${{ env.BINARY_DEST_PATH }}-x64.msi"
|
||||
|
||||
Get-FileHash ${{ env.BINARY_DEST_PATH }}-x64.exe -Algorithm SHA256 | Select-Object Hash > ${{ env.BINARY_DEST_PATH }}-x64.exe.sha256sum.txt
|
||||
|
||||
2
.github/workflows/cd.yml
vendored
2
.github/workflows/cd.yml
vendored
@@ -269,7 +269,7 @@ jobs:
|
||||
|
||||
# Used for release artifact
|
||||
# This should match 'build-tauri' in _rust.yml
|
||||
cp "target/release/Firezone Windows Client.exe" "${{ env.BINARY_DEST_PATH }}-x64.exe"
|
||||
cp "target/release/Firezone.exe" "${{ env.BINARY_DEST_PATH }}-x64.exe"
|
||||
cp "target/release/bundle/msi/*.msi" "${{ env.BINARY_DEST_PATH }}-x64.msi"
|
||||
|
||||
Get-FileHash ${{ env.BINARY_DEST_PATH }}-x64.exe -Algorithm SHA256 | Select-Object Hash > ${{ env.BINARY_DEST_PATH }}-x64.exe.sha256sum.txt
|
||||
|
||||
6
rust/windows-client/.gitignore
vendored
6
rust/windows-client/.gitignore
vendored
@@ -20,3 +20,9 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# CSS output from the Tailwind compiler
|
||||
src/output.css
|
||||
|
||||
# JS output from the TypeScript compiler
|
||||
src/**/*.js
|
||||
|
||||
@@ -7,7 +7,7 @@ This crate houses a Windows GUI client.
|
||||
This is the minimal toolchain needed to compile natively for x86_64 Windows:
|
||||
|
||||
1. [Install rustup](https://win.rustup.rs/x86_64) for Windows.
|
||||
1. Install Tauri tooling: `cargo install tauri-cli`
|
||||
1. Install [pnpm](https://pnpm.io/installation) for your platform.
|
||||
|
||||
### Recommended IDE Setup
|
||||
|
||||
@@ -19,15 +19,20 @@ This is the minimal toolchain needed to compile natively for x86_64 Windows:
|
||||
|
||||
## Building
|
||||
|
||||
Builds are best started from the frontend tool `pnpm`. This ensures typescript
|
||||
and css is compiled properly before bundling the application.
|
||||
|
||||
See the [`package.json`](./package.json) script for more details as to what's
|
||||
going on under the hood.
|
||||
|
||||
```powershell
|
||||
# Builds a release exe
|
||||
cargo tauri build
|
||||
pnpm build
|
||||
|
||||
# The release exe, MSI, and NSIS installer should be up in the workspace.
|
||||
# The release exe and MSI installer should be up in the workspace.
|
||||
# The exe can run without being installed
|
||||
stat ../target/release/firezone-windows-client.exe
|
||||
stat ../target/release/bundle/msi/firezone-windows-client_0.0.0_x64_en-US.msi
|
||||
stat ../target/release/bundle/nsis/firezone-windows-client_0.0.0_x64-setup.exe
|
||||
stat ../target/release/Firezone.exe
|
||||
stat ../target/release/bundle/msi/Firezone_0.0.0_x64_en-US.msi
|
||||
```
|
||||
|
||||
## Running
|
||||
@@ -35,16 +40,15 @@ stat ../target/release/bundle/nsis/firezone-windows-client_0.0.0_x64-setup.exe
|
||||
From this dir:
|
||||
|
||||
```powershell
|
||||
# Tauri has some hot-reloading features. If the Rust code changes it will even recompile
|
||||
# and restart the program for you.
|
||||
RUST_LOG=info,firezone_windows_client=debug cargo tauri dev
|
||||
# This will start the frontend tools in watch mode and then run `tauri dev`
|
||||
pnpm dev
|
||||
|
||||
# You can call debug subcommands on the exe from this directory too
|
||||
# e.g. this is equivalent to `cargo run -- debug hostname`
|
||||
cargo tauri dev -- -- debug hostname
|
||||
|
||||
# The exe is up in the workspace
|
||||
stat ../target/debug/firezone-windows-client.exe
|
||||
stat ../target/debug/Firezone.exe
|
||||
```
|
||||
|
||||
The app's config and logs will be stored at
|
||||
@@ -56,6 +60,10 @@ Tauri says it should work on Windows 10, Version 1803 and up. Older versions may
|
||||
work if you
|
||||
[manually install WebView2](https://tauri.app/v1/guides/getting-started/prerequisites#2-webview2)
|
||||
|
||||
`x86_64` architecture is supported at this time. See
|
||||
[this issue](https://github.com/firezone/firezone/issues/2992) for `aarch64`
|
||||
support.
|
||||
|
||||
## Threat model
|
||||
|
||||
We can split this to its own doc or generalize it to the whole project if
|
||||
|
||||
14
rust/windows-client/build-debug.bat
Executable file
14
rust/windows-client/build-debug.bat
Executable file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Copy frontend dependencies
|
||||
copy "node_modules\flowbite\dist\flowbite.min.js" "src\"
|
||||
|
||||
REM Compile TypeScript
|
||||
call pnpm tsc
|
||||
|
||||
REM Compile CSS
|
||||
call pnpm tailwindcss -i src\input.css -o src\output.css
|
||||
|
||||
REM Compile Rust and bundle
|
||||
call tauri build --debug --bundles none
|
||||
14
rust/windows-client/build.bat
Executable file
14
rust/windows-client/build.bat
Executable file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Copy frontend dependencies
|
||||
copy "node_modules\flowbite\dist\flowbite.min.js" "src\"
|
||||
|
||||
REM Compile TypeScript
|
||||
call pnpm tsc
|
||||
|
||||
REM Compile CSS
|
||||
call pnpm tailwindcss -i src\input.css -o src\output.css
|
||||
|
||||
REM Compile Rust and bundle
|
||||
call tauri build
|
||||
18
rust/windows-client/build.sh
Executable file
18
rust/windows-client/build.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
# The Windows client obviously doesn't build for *nix, but this
|
||||
# script is helpful for doing UI work on those platforms for the
|
||||
# Windows client.
|
||||
set -e
|
||||
|
||||
# Copy frontend dependencies
|
||||
cp node_modules/flowbite/dist/flowbite.min.js src/
|
||||
|
||||
# Compile TypeScript
|
||||
tsc
|
||||
|
||||
# Compile CSS
|
||||
tailwindcss -i src/input.css -o src/output.css
|
||||
|
||||
# Compile Rust and bundle
|
||||
tauri build
|
||||
14
rust/windows-client/dev.bat
Executable file
14
rust/windows-client/dev.bat
Executable file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Copy frontend dependencies
|
||||
copy "node_modules\flowbite\dist\flowbite.min.js" "src\"
|
||||
|
||||
REM Compile TypeScript in watch mode
|
||||
start tsc --watch
|
||||
|
||||
REM Compile CSS in watch mode
|
||||
start call npx tailwindcss -i src\input.css -o src\output.css --watch
|
||||
|
||||
REM Start Tauri hot-reloading
|
||||
tauri dev
|
||||
24
rust/windows-client/dev.sh
Executable file
24
rust/windows-client/dev.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
# The Windows client obviously doesn't build for *nix, but this
|
||||
# script is helpful for doing UI work on those platforms for the
|
||||
# Windows client.
|
||||
set -e
|
||||
|
||||
# Fixes exiting with Ctrl-C
|
||||
stop() {
|
||||
kill $(jobs -p)
|
||||
}
|
||||
trap stop SIGINT SIGTERM
|
||||
|
||||
# Copy frontend dependencies
|
||||
cp node_modules/flowbite/dist/flowbite.min.js src/
|
||||
|
||||
# Compile TypeScript
|
||||
tsc --watch &
|
||||
|
||||
# Compile CSS
|
||||
tailwindcss -i src/input.css -o src/output.css --watch &
|
||||
|
||||
# Start Tauri hot-reloading: Not applicable for Windows
|
||||
# tauri dev
|
||||
25
rust/windows-client/package.json
Normal file
25
rust/windows-client/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "run-script-os",
|
||||
"build:win32": "call build.bat",
|
||||
"build:darwin:linux": "./build.sh",
|
||||
"build-debug": "run-script-os",
|
||||
"build-debug:win32": "call build-debug.bat",
|
||||
"dev": "run-script-os",
|
||||
"dev:win32": "call dev.bat",
|
||||
"dev:darwin:linux": "./dev.sh",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.0",
|
||||
"@tauri-apps/cli": "^1.0",
|
||||
"flowbite": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18",
|
||||
"http-server": "^14.1.1",
|
||||
"run-script-os": "^1.1.6",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
1168
rust/windows-client/pnpm-lock.yaml
generated
Normal file
1168
rust/windows-client/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
name = "firezone-windows-client"
|
||||
# mark:automatic-version
|
||||
version = "1.0.0"
|
||||
description = "Firezone Windows Client"
|
||||
description = "Firezone"
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -138,9 +138,10 @@ pub(crate) fn run(params: client::GuiParams) -> Result<()> {
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
logging::clear_logs,
|
||||
logging::count_logs,
|
||||
logging::export_logs,
|
||||
logging::start_stop_log_counting,
|
||||
settings::apply_advanced_settings,
|
||||
settings::reset_advanced_settings,
|
||||
settings::get_advanced_settings,
|
||||
])
|
||||
.system_tray(tray)
|
||||
@@ -273,7 +274,6 @@ pub(crate) enum ControllerRequest {
|
||||
Quit,
|
||||
SchemeRequest(url::Url),
|
||||
SignIn,
|
||||
StartStopLogCounting(bool),
|
||||
SignOut,
|
||||
TunnelReady,
|
||||
}
|
||||
@@ -350,7 +350,6 @@ struct Controller {
|
||||
/// The UUIDv4 device ID persisted to disk
|
||||
/// Sent verbatim to Session::connect
|
||||
device_id: String,
|
||||
log_counting_task: Option<tokio::task::JoinHandle<Result<()>>>,
|
||||
logging_handles: client::logging::Handles,
|
||||
/// Tells us when to wake up and look for a new resource list. Tokio docs say that memory reads and writes are synchronized when notifying, so we don't need an extra mutex on the resources.
|
||||
notify_controller: Arc<Notify>,
|
||||
@@ -380,7 +379,6 @@ impl Controller {
|
||||
ctlr_tx,
|
||||
session: None,
|
||||
device_id,
|
||||
log_counting_task: None,
|
||||
logging_handles,
|
||||
notify_controller,
|
||||
tunnel_ready: false,
|
||||
@@ -584,19 +582,6 @@ async fn run_controller(
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Req::StartStopLogCounting(enable) => {
|
||||
if enable {
|
||||
if controller.log_counting_task.is_none() {
|
||||
let app = app.clone();
|
||||
controller.log_counting_task = Some(tokio::spawn(logging::count_logs(app)));
|
||||
tracing::debug!("started log counting");
|
||||
}
|
||||
} else if let Some(t) = controller.log_counting_task {
|
||||
t.abort();
|
||||
controller.log_counting_task = None;
|
||||
tracing::debug!("cancelled log counting");
|
||||
}
|
||||
}
|
||||
Req::TunnelReady => {
|
||||
controller.tunnel_ready = true;
|
||||
controller.refresh_system_tray_menu()?;
|
||||
|
||||
@@ -10,7 +10,6 @@ use std::{
|
||||
result::Result as StdResult,
|
||||
str::FromStr,
|
||||
};
|
||||
use tauri::Manager;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::subscriber::set_global_default;
|
||||
use tracing_log::LogTracer;
|
||||
@@ -42,27 +41,8 @@ pub(crate) fn setup(log_filter: &str) -> Result<Handles> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn start_stop_log_counting(
|
||||
managed: tauri::State<'_, Managed>,
|
||||
enable: bool,
|
||||
) -> StdResult<(), String> {
|
||||
// Delegate this to Controller so that it can decide whether to obey.
|
||||
// e.g. if there's already a count task running, it may refuse to start another.
|
||||
managed
|
||||
.ctlr_tx
|
||||
.send(ControllerRequest::StartStopLogCounting(enable))
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn clear_logs(
|
||||
app: tauri::AppHandle,
|
||||
managed: tauri::State<'_, Managed>,
|
||||
) -> StdResult<(), String> {
|
||||
clear_logs_inner(app, &managed)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
pub(crate) async fn clear_logs(managed: tauri::State<'_, Managed>) -> StdResult<(), String> {
|
||||
clear_logs_inner(&managed).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -72,18 +52,27 @@ pub(crate) async fn export_logs(managed: tauri::State<'_, Managed>) -> StdResult
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize)]
|
||||
pub(crate) struct FileCount {
|
||||
bytes: u64,
|
||||
files: u64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn count_logs() -> StdResult<FileCount, String> {
|
||||
count_logs_inner().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Delete all files in the logs directory.
|
||||
///
|
||||
/// This includes the current log file, so we won't write any more logs to disk
|
||||
/// until the file rolls over or the app restarts.
|
||||
pub(crate) async fn clear_logs_inner(app: tauri::AppHandle, managed: &Managed) -> Result<()> {
|
||||
pub(crate) async fn clear_logs_inner(managed: &Managed) -> Result<()> {
|
||||
let mut dir = tokio::fs::read_dir("logs").await?;
|
||||
while let Some(entry) = dir.next_entry().await? {
|
||||
tokio::fs::remove_file(entry.path()).await?;
|
||||
}
|
||||
|
||||
count_logs(app).await?;
|
||||
|
||||
managed.fault_msleep(5000).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -146,23 +135,16 @@ pub(crate) async fn export_logs_to(path: PathBuf, stem: PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize)]
|
||||
struct FileCount {
|
||||
bytes: u64,
|
||||
files: u64,
|
||||
}
|
||||
|
||||
pub(crate) async fn count_logs(app: tauri::AppHandle) -> Result<()> {
|
||||
/// Count log files and their sizes
|
||||
pub(crate) async fn count_logs_inner() -> Result<FileCount> {
|
||||
let mut dir = tokio::fs::read_dir("logs").await?;
|
||||
let mut file_count = FileCount::default();
|
||||
// Zero out the GUI
|
||||
app.emit_all("file_count_progress", None::<FileCount>)?;
|
||||
|
||||
while let Some(entry) = dir.next_entry().await? {
|
||||
let md = entry.metadata().await?;
|
||||
file_count.files += 1;
|
||||
file_count.bytes += md.len();
|
||||
}
|
||||
// Show the result on the GUI
|
||||
app.emit_all("file_count_progress", Some(&file_count))?;
|
||||
Ok(())
|
||||
|
||||
Ok(file_count)
|
||||
}
|
||||
|
||||
@@ -53,11 +53,24 @@ pub(crate) async fn apply_advanced_settings(
|
||||
managed: tauri::State<'_, Managed>,
|
||||
settings: AdvancedSettings,
|
||||
) -> StdResult<(), String> {
|
||||
apply_advanced_settings_inner(managed.inner(), settings)
|
||||
apply_advanced_settings_inner(managed.inner(), &settings)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn reset_advanced_settings(
|
||||
managed: tauri::State<'_, Managed>,
|
||||
) -> StdResult<AdvancedSettings, String> {
|
||||
let settings = AdvancedSettings::default();
|
||||
|
||||
apply_advanced_settings_inner(managed.inner(), &settings)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn get_advanced_settings(
|
||||
managed: tauri::State<'_, Managed>,
|
||||
@@ -75,7 +88,7 @@ pub(crate) async fn get_advanced_settings(
|
||||
|
||||
pub(crate) async fn apply_advanced_settings_inner(
|
||||
managed: &Managed,
|
||||
settings: AdvancedSettings,
|
||||
settings: &AdvancedSettings,
|
||||
) -> Result<()> {
|
||||
let DirAndPath { dir, path } = advanced_settings_path()?;
|
||||
tokio::fs::create_dir_all(&dir).await?;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"build": {
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"devPath": "../src",
|
||||
@@ -7,7 +7,7 @@
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
"productName": "Firezone Windows Client"
|
||||
"productName": "Firezone"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
@@ -25,7 +25,7 @@
|
||||
"icons/firezone.ico"
|
||||
],
|
||||
"publisher": "Firezone",
|
||||
"shortDescription": "Firezone Windows client"
|
||||
"shortDescription": "Firezone"
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="stylesheet" href="output.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>About Firezone</title>
|
||||
<script type="module" src="/main.js" defer></script>
|
||||
<script src="../node_modules/flowbite/dist/flowbite.min.js" defer></script>
|
||||
<style>
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #ffe21c);
|
||||
|
||||
9
rust/windows-client/src/input.css
Normal file
9
rust/windows-client/src/input.css
Normal file
@@ -0,0 +1,9 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/*
|
||||
* This file serves as the main entry point for your Tailwind CSS installation.
|
||||
* Generate the CSS from this with `pnpm tailwindcss -i src/input.css -o src/output.css`
|
||||
* This has been added to the build script in package.json for convenience.
|
||||
*/
|
||||
@@ -1 +0,0 @@
|
||||
const { invoke } = window.__TAURI__.tauri;
|
||||
@@ -2,70 +2,180 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="stylesheet" href="output.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Settings</title>
|
||||
<script src="settings.js"></script>
|
||||
<style>
|
||||
.logo.vanilla:hover {
|
||||
filter: drop-shadow(0 0 2em #ffe21c);
|
||||
}
|
||||
|
||||
.tab button.active {
|
||||
background-color: orange;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.tabcontent {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="settings.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row tab">
|
||||
<button class="tablinks" onclick="openTab(event, 'tab_advanced')">Advanced</button>
|
||||
<button class="tablinks" onclick="openTab(event, 'tab_logs')">Diagnostic Logs</button>
|
||||
<body class="bg-netural-100 text-neutral-900">
|
||||
<div class="container mx-auto">
|
||||
<div class="mb-4 border-b border-neutral-300">
|
||||
<ul
|
||||
class="justify-center flex flex-wrap -mb-px text-sm font-medium text-center"
|
||||
id="tabs"
|
||||
data-tabs-toggle="#tab-content"
|
||||
role="tablist"
|
||||
>
|
||||
<li class="me-2" role="presentation">
|
||||
<button
|
||||
class="inline-block p-4 border-b-2 rounded-t-lg"
|
||||
id="advanced-tab"
|
||||
data-tabs-target="#advanced"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="advanced"
|
||||
aria-selected="false"
|
||||
>
|
||||
Advanced
|
||||
</button>
|
||||
</li>
|
||||
<li class="me-2" role="presentation">
|
||||
<button
|
||||
class="inline-block p-4 border-b-2 rounded-t-lg hover:text-neutral-700 hover:border-neutral-400"
|
||||
id="logs-tab"
|
||||
data-tabs-target="#logs"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="logs"
|
||||
aria-selected="false"
|
||||
>
|
||||
Diagnostic Logs
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
|
||||
<div id="tab_logs" class="tabcontent">
|
||||
<div class="row">
|
||||
<button type="button" id="export-logs-btn">Export Logs</button>
|
||||
<button type="button" id="clear-logs-btn">Clear Logs</button>
|
||||
<div id="tab-content">
|
||||
<div
|
||||
class="hidden p-4 rounded-lg bg-neutral-50"
|
||||
id="advanced"
|
||||
role="tabpanel"
|
||||
aria-labelledby="advanced-tab"
|
||||
>
|
||||
<p class="mx-8 text-neutral-900">
|
||||
<strong>WARNING</strong>: These settings are intended for internal
|
||||
debug purposes <strong>only</strong>. Changing these is not
|
||||
supported and will disrupt access to your resources.
|
||||
</p>
|
||||
<form id="advanced-settings-form" class="max-w-md mt-12 mx-auto">
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input
|
||||
name="auth-base-url"
|
||||
id="auth-base-url-input"
|
||||
class="block py-2.5 px-0 w-full text-sm text-neutral-900 bg-transparent border-0 border-b-2 border-neutral-300 appearance-none focus:outline-none focus:ring-0 focus:border-accent-600 peer"
|
||||
placeholder=" "
|
||||
required
|
||||
/>
|
||||
<label
|
||||
for="auth-base-url"
|
||||
class="peer-focus:font-medium absolute text-sm text-neutral-600 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto peer-focus:text-accent-600 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
|
||||
>Auth Base URL</label
|
||||
>
|
||||
</div>
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input
|
||||
name="api-url"
|
||||
id="api-url-input"
|
||||
class="block py-2.5 px-0 w-full text-sm text-neutral-900 bg-transparent border-0 border-b-2 border-neutral-300 appearance-none focus:outline-none focus:ring-0 focus:border-accent-600 peer"
|
||||
placeholder=" "
|
||||
required
|
||||
/>
|
||||
<label
|
||||
for="api-url"
|
||||
class="peer-focus:font-medium absolute text-sm text-neutral-600 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-accent-600 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
|
||||
>API URL</label
|
||||
>
|
||||
</div>
|
||||
<div class="relative z-0 w-full mb-5 group">
|
||||
<input
|
||||
name="log-filter"
|
||||
id="log-filter-input"
|
||||
class="block py-2.5 px-0 w-full text-sm text-neutral-900 bg-transparent border-0 border-b-2 border-neutral-300 appearance-none focus:outline-none focus:ring-0 focus:border-accent-600 peer"
|
||||
placeholder=" "
|
||||
required
|
||||
/>
|
||||
<label
|
||||
for="log_filter"
|
||||
class="peer-focus:font-medium absolute text-sm text-neutral-600 duration-300 transform -translate-y-6 scale-75 top-3 -z-10 origin-[0] peer-focus:start-0 rtl:peer-focus:translate-x-1/4 peer-focus:text-accent-600 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-6"
|
||||
>Log Filter</label
|
||||
>
|
||||
</div>
|
||||
<div class="inline-flex w-full justify-between">
|
||||
<button
|
||||
id="reset-advanced-settings-btn"
|
||||
type="button"
|
||||
class="bg-neutral-200 text-neutral-900 hover:bg-neutral-300 font-medium rounded text-sm w-full sm:w-auto px-5 py-2.5 text-center"
|
||||
>
|
||||
Reset to Defaults
|
||||
</button>
|
||||
<button
|
||||
id="apply-advanced-settings-btn"
|
||||
type="submit"
|
||||
class="text-white bg-accent-450 hover:bg-accent-700 font-medium rounded text-sm w-full sm:w-auto px-5 py-2.5 text-center"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p id="log-count-output"></p>
|
||||
<div
|
||||
class="hidden p-4 rounded-lg bg-neutral-50"
|
||||
id="logs"
|
||||
role="tabpanel"
|
||||
aria-labelledby="logs-tab"
|
||||
>
|
||||
<div class="mt-16 flex justify-center">
|
||||
<p class="mr-1">Log directory size:</p>
|
||||
<p id="log-count-output">Calculating...</p>
|
||||
</div>
|
||||
<div class="mt-8 flex justify-center">
|
||||
<button
|
||||
id="export-logs-btn"
|
||||
type="button"
|
||||
class="mr-4 inline-flex items-center bg-neutral-200 text-neutral-900 hover:bg-neutral-300 font-medium rounded text-sm px-5 py-2.5 text-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4 mr-2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
Export Logs
|
||||
</button>
|
||||
<button
|
||||
id="clear-logs-btn"
|
||||
type="button"
|
||||
class="inline-flex items-center bg-neutral-200 text-neutral-900 hover:bg-neutral-300 font-medium rounded text-sm px-5 py-2.5 text-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4 mr-2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab_advanced" class="tabcontent">
|
||||
<form id="advanced-settings-form">
|
||||
<div class="row">
|
||||
<label for="auth-base-url-input">Auth Base URL:</label>
|
||||
<input id="auth-base-url-input"></input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="api-url-input">API URL:</label>
|
||||
<input id="api-url-input"></input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="log-filter-input">Log Filter:</label>
|
||||
<input id="log-filter-input"></input>
|
||||
</div>
|
||||
|
||||
<p><b>WARNING</b>: These settings are intended for internal debug purposes <b>only</b>. Changing these is not supported and will disrupt access to your resources.</p>
|
||||
|
||||
<div class="row">
|
||||
<button type="button" id="reset-advanced-settings-btn">Reset to Defaults</button>
|
||||
<button type="submit" id="apply-advanced-settings-btn">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./flowbite.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
let auth_base_url_input;
|
||||
let api_url_input;
|
||||
let log_count_output;
|
||||
let log_filter_input;
|
||||
let reset_advanced_settings_btn;
|
||||
let apply_advanced_settings_btn;
|
||||
let export_logs_btn;
|
||||
let clear_logs_btn;
|
||||
|
||||
const querySel = function(id) {
|
||||
return document.querySelector(id);
|
||||
};
|
||||
|
||||
const { invoke } = window.__TAURI__.tauri;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
|
||||
// Lock the UI when we're saving to disk, since disk writes are technically async.
|
||||
// Parameters:
|
||||
// - locked - Boolean, true to lock the UI, false to unlock it.
|
||||
function lock_advanced_settings_form(locked) {
|
||||
auth_base_url_input.disabled = locked;
|
||||
api_url_input.disabled = locked;
|
||||
log_filter_input.disabled = locked;
|
||||
|
||||
if (locked) {
|
||||
reset_advanced_settings_btn.textContent = "...";
|
||||
apply_advanced_settings_btn.textContent = "...";
|
||||
}
|
||||
else {
|
||||
reset_advanced_settings_btn.textContent = "Reset to Defaults";
|
||||
apply_advanced_settings_btn.textContent = "Apply";
|
||||
}
|
||||
|
||||
reset_advanced_settings_btn.disabled = locked;
|
||||
apply_advanced_settings_btn.disabled = locked;
|
||||
}
|
||||
|
||||
function lock_logs_form(locked) {
|
||||
export_logs_btn.disabled = locked;
|
||||
clear_logs_btn.disabled = locked;
|
||||
}
|
||||
|
||||
async function apply_advanced_settings() {
|
||||
lock_advanced_settings_form(true);
|
||||
|
||||
// Invoke Rust
|
||||
// TODO: Why doesn't JS' await syntax work here?
|
||||
invoke("apply_advanced_settings", {
|
||||
"settings": {
|
||||
"auth_base_url": auth_base_url_input.value,
|
||||
"api_url": api_url_input.value,
|
||||
"log_filter": log_filter_input.value
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
lock_advanced_settings_form(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
lock_advanced_settings_form(false);
|
||||
});
|
||||
}
|
||||
|
||||
async function get_advanced_settings() {
|
||||
lock_advanced_settings_form(true);
|
||||
|
||||
invoke("get_advanced_settings")
|
||||
.then((settings) => {
|
||||
auth_base_url_input.value = settings["auth_base_url"];
|
||||
api_url_input.value = settings["api_url"];
|
||||
log_filter_input.value = settings["log_filter"];
|
||||
lock_advanced_settings_form(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
lock_advanced_settings_form(false);
|
||||
});
|
||||
}
|
||||
|
||||
async function export_logs() {
|
||||
lock_logs_form(true);
|
||||
|
||||
invoke("export_logs")
|
||||
.then(() => {
|
||||
lock_logs_form(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
lock_logs_form(false);
|
||||
});
|
||||
}
|
||||
|
||||
async function clear_logs() {
|
||||
lock_logs_form(true);
|
||||
|
||||
invoke("clear_logs")
|
||||
.then(() => {
|
||||
lock_logs_form(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
lock_logs_form(false);
|
||||
});
|
||||
}
|
||||
|
||||
function openTab(evt, tabName) {
|
||||
let tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (let i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
|
||||
let tablinks = document.getElementsByClassName("tablinks");
|
||||
for (let i = 0; i < tablinks.length; i++) {
|
||||
// TODO: There's a better way to change classes on an element
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
// TODO: There's a better way to do this
|
||||
evt.currentTarget.className += " active";
|
||||
|
||||
invoke("start_stop_log_counting", {"enable": tabName == "tab_logs"})
|
||||
.then(() => {
|
||||
// Good
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
// Advanced tab
|
||||
auth_base_url_input = querySel("#auth-base-url-input");
|
||||
api_url_input = querySel("#api-url-input");
|
||||
log_count_output = querySel("#log-count-output");
|
||||
log_filter_input = querySel("#log-filter-input");
|
||||
reset_advanced_settings_btn = querySel("#reset-advanced-settings-btn");
|
||||
apply_advanced_settings_btn = querySel("#apply-advanced-settings-btn");
|
||||
|
||||
querySel("#advanced-settings-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
apply_advanced_settings();
|
||||
});
|
||||
|
||||
// Logs tab
|
||||
export_logs_btn = querySel("#export-logs-btn");
|
||||
clear_logs_btn = querySel("#clear-logs-btn");
|
||||
|
||||
export_logs_btn.addEventListener("click", (e) => {
|
||||
export_logs();
|
||||
});
|
||||
clear_logs_btn.addEventListener("click", (e) => {
|
||||
clear_logs();
|
||||
});
|
||||
|
||||
await listen("file_count_progress", (event) => {
|
||||
const pl = event.payload;
|
||||
|
||||
let s = "Calculating...";
|
||||
if (!! pl) {
|
||||
const megabytes = Math.round(pl.bytes / 100000) / 10;
|
||||
s = `${pl.files} files, ${megabytes} MB`;
|
||||
}
|
||||
log_count_output.innerText = s;
|
||||
});
|
||||
|
||||
// TODO: Why doesn't this open the Advanced tab by default?
|
||||
querySel("#tab_advanced").click();
|
||||
|
||||
get_advanced_settings().await;
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded",setup);
|
||||
225
rust/windows-client/src/settings.ts
Normal file
225
rust/windows-client/src/settings.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
// Purpose: TypeScript file for the settings page.
|
||||
|
||||
// Custom types
|
||||
interface Settings {
|
||||
auth_base_url: string;
|
||||
api_url: string;
|
||||
log_filter: string;
|
||||
}
|
||||
|
||||
interface TauriEvent {
|
||||
type: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
interface FileCount {
|
||||
files: number;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
// Stub Tauri API for TypeScript. Helpful when developing without Tauri running.
|
||||
export {};
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI__: {
|
||||
tauri: {
|
||||
invoke: (cmd: string, args?: any) => Promise<any>;
|
||||
};
|
||||
event: {
|
||||
listen: (cmd: string, callback: (event: TauriEvent) => void) => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
window.__TAURI__ = window.__TAURI__ || {
|
||||
tauri: {
|
||||
invoke: (_cmd: string, _args?: any) => {
|
||||
return Promise.reject("Tauri API not initialized");
|
||||
},
|
||||
},
|
||||
event: {
|
||||
listen: (_cmd: string, _callback: (event: TauriEvent) => void) => {
|
||||
console.error("Tauri API not initialized");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { invoke } = window.__TAURI__.tauri;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
|
||||
// DOM elements
|
||||
const form = <HTMLFormElement>document.getElementById("advanced-settings-form");
|
||||
const authBaseUrlInput = <HTMLInputElement>(
|
||||
document.getElementById("auth-base-url-input")
|
||||
);
|
||||
const apiUrlInput = <HTMLInputElement>document.getElementById("api-url-input");
|
||||
const logFilterInput = <HTMLInputElement>(
|
||||
document.getElementById("log-filter-input")
|
||||
);
|
||||
const logCountOutput = <HTMLParagraphElement>(
|
||||
document.getElementById("log-count-output")
|
||||
);
|
||||
const resetAdvancedSettingsBtn = <HTMLButtonElement>(
|
||||
document.getElementById("reset-advanced-settings-btn")
|
||||
);
|
||||
const applyAdvancedSettingsBtn = <HTMLButtonElement>(
|
||||
document.getElementById("apply-advanced-settings-btn")
|
||||
);
|
||||
const exportLogsBtn = <HTMLButtonElement>(
|
||||
document.getElementById("export-logs-btn")
|
||||
);
|
||||
const clearLogsBtn = <HTMLButtonElement>(
|
||||
document.getElementById("clear-logs-btn")
|
||||
);
|
||||
const logsTabBtn = <HTMLButtonElement>document.getElementById("logs-tab");
|
||||
|
||||
// Rust bridge functions
|
||||
|
||||
// Lock the UI when we're saving to disk, since disk writes are technically async.
|
||||
function lockAdvancedSettingsForm() {
|
||||
authBaseUrlInput.disabled = true;
|
||||
apiUrlInput.disabled = true;
|
||||
logFilterInput.disabled = true;
|
||||
resetAdvancedSettingsBtn.disabled = true;
|
||||
applyAdvancedSettingsBtn.disabled = true;
|
||||
|
||||
resetAdvancedSettingsBtn.textContent = "Updating...";
|
||||
applyAdvancedSettingsBtn.textContent = "Updating...";
|
||||
}
|
||||
|
||||
function unlockAdvancedSettingsForm() {
|
||||
authBaseUrlInput.disabled = false;
|
||||
apiUrlInput.disabled = false;
|
||||
logFilterInput.disabled = false;
|
||||
resetAdvancedSettingsBtn.disabled = false;
|
||||
applyAdvancedSettingsBtn.disabled = false;
|
||||
|
||||
resetAdvancedSettingsBtn.textContent = "Reset to Defaults";
|
||||
applyAdvancedSettingsBtn.textContent = "Apply";
|
||||
}
|
||||
|
||||
function lockLogsForm() {
|
||||
exportLogsBtn.disabled = true;
|
||||
clearLogsBtn.disabled = true;
|
||||
}
|
||||
|
||||
function unlockLogsForm() {
|
||||
exportLogsBtn.disabled = false;
|
||||
clearLogsBtn.disabled = false;
|
||||
}
|
||||
|
||||
async function applyAdvancedSettings() {
|
||||
console.log("Applying advanced settings");
|
||||
lockAdvancedSettingsForm();
|
||||
|
||||
invoke("apply_advanced_settings", {
|
||||
settings: {
|
||||
auth_base_url: authBaseUrlInput.value,
|
||||
api_url: apiUrlInput.value,
|
||||
log_filter: logFilterInput.value,
|
||||
},
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
unlockAdvancedSettingsForm();
|
||||
});
|
||||
}
|
||||
|
||||
async function resetAdvancedSettings() {
|
||||
console.log("Resetting advanced settings");
|
||||
lockAdvancedSettingsForm();
|
||||
|
||||
invoke("reset_advanced_settings")
|
||||
.then((settings: Settings) => {
|
||||
authBaseUrlInput.value = settings.auth_base_url;
|
||||
apiUrlInput.value = settings.api_url;
|
||||
logFilterInput.value = settings.log_filter;
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
unlockAdvancedSettingsForm();
|
||||
});
|
||||
}
|
||||
|
||||
async function getAdvancedSettings() {
|
||||
console.log("Getting advanced settings");
|
||||
lockAdvancedSettingsForm();
|
||||
|
||||
invoke("get_advanced_settings")
|
||||
.then((settings: Settings) => {
|
||||
authBaseUrlInput.value = settings.auth_base_url;
|
||||
apiUrlInput.value = settings.api_url;
|
||||
logFilterInput.value = settings.log_filter;
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
unlockAdvancedSettingsForm();
|
||||
});
|
||||
}
|
||||
|
||||
async function exportLogs() {
|
||||
console.log("Exporting logs");
|
||||
lockLogsForm();
|
||||
|
||||
invoke("export_logs")
|
||||
.catch((e: Error) => {
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
unlockLogsForm();
|
||||
});
|
||||
}
|
||||
|
||||
async function clearLogs() {
|
||||
console.log("Clearing logs");
|
||||
lockLogsForm();
|
||||
|
||||
invoke("clear_logs")
|
||||
.catch((e: Error) => {
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
logCountOutput.innerText = "0 files, 0 MB";
|
||||
unlockLogsForm();
|
||||
});
|
||||
}
|
||||
|
||||
async function countLogs() {
|
||||
invoke("count_logs")
|
||||
.then((fileCount) => {
|
||||
console.log(fileCount);
|
||||
const megabytes = Math.round(fileCount.bytes / 100000) / 10;
|
||||
logCountOutput.innerText = `${fileCount.files} files, ${megabytes} MB`;
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
console.error(e);
|
||||
logCountOutput.innerText = `Error counting logs: ${e.message}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
form.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
applyAdvancedSettings();
|
||||
});
|
||||
resetAdvancedSettingsBtn.addEventListener("click", (e) => {
|
||||
resetAdvancedSettings();
|
||||
});
|
||||
exportLogsBtn.addEventListener("click", (e) => {
|
||||
exportLogs();
|
||||
});
|
||||
clearLogsBtn.addEventListener("click", (e) => {
|
||||
clearLogs();
|
||||
});
|
||||
logsTabBtn.addEventListener("click", (e) => {
|
||||
countLogs();
|
||||
});
|
||||
|
||||
// Load settings
|
||||
getAdvancedSettings();
|
||||
@@ -1,114 +0,0 @@
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:disabled,
|
||||
button:disabled {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
||||
61
rust/windows-client/tailwind.config.js
Normal file
61
rust/windows-client/tailwind.config.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const firezoneColors = {
|
||||
// See our brand palette in Figma.
|
||||
// These have been reversed to match Tailwind's default order.
|
||||
|
||||
// primary: orange
|
||||
"heat-wave": {
|
||||
50: "#fff9f5",
|
||||
100: "#fff1e5",
|
||||
200: "#ffddc2",
|
||||
300: "#ffbc85",
|
||||
400: "#ff9a47",
|
||||
450: "#ff7300",
|
||||
500: "#ff7605",
|
||||
600: "#c25700",
|
||||
700: "#7f3900",
|
||||
800: "#5c2900",
|
||||
900: "#331700",
|
||||
},
|
||||
// accent: violet
|
||||
"electric-violet": {
|
||||
50: "#f8f5ff",
|
||||
100: "#ece5ff",
|
||||
200: "#d2c2ff",
|
||||
300: "#a585ff",
|
||||
400: "#7847ff",
|
||||
450: "#5e00d6",
|
||||
500: "#4805ff",
|
||||
600: "#3400c2",
|
||||
700: "#37007f",
|
||||
800: "#28005c",
|
||||
900: "#160033",
|
||||
},
|
||||
// neutral: night-rider
|
||||
"night-rider": {
|
||||
50: "#fcfcfc",
|
||||
100: "#f8f7f7",
|
||||
200: "#ebebea",
|
||||
300: "#dfdedd",
|
||||
400: "#c7c4c2",
|
||||
500: "#a7a3a0",
|
||||
600: "#90867f",
|
||||
700: "#766a60",
|
||||
800: "#4c3e33",
|
||||
900: "#1b140e",
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{html,ts,js}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: firezoneColors["heat-wave"],
|
||||
accent: firezoneColors["electric-violet"],
|
||||
neutral: firezoneColors["night-rider"],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("flowbite/plugin")],
|
||||
};
|
||||
12
rust/windows-client/tsconfig.json
Normal file
12
rust/windows-client/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "es6",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user