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:
Jamil
2024-01-23 09:07:25 -08:00
committed by GitHub
parent 42a05e86ea
commit 086f7a85c6
26 changed files with 1822 additions and 418 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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
View 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
View 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
View 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
View 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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -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()?;

View File

@@ -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)
}

View File

@@ -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?;

View File

@@ -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

View File

@@ -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);

View 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.
*/

View File

@@ -1 +0,0 @@
const { invoke } = window.__TAURI__.tauri;

View File

@@ -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>

View File

@@ -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);

View 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();

View File

@@ -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;
}
}

View 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")],
};

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es6",
"module": "es6",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"moduleResolution": "node"
},
"include": ["./src/**/*"],
"exclude": ["node_modules"]
}