Compare commits

..

49 Commits

Author SHA1 Message Date
Jörg Thalheim
84dab9e329 waypipe: disable gpu for now 2025-07-17 12:12:41 +02:00
hsjobeki
ef02fca062 Merge pull request 'buildClan: Add deprecation warning' (#4384) from Qubasa/clan-core:migrate_away_buildClan into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4384
Reviewed-by: hsjobeki <hsjobeki@gmail.com>
2025-07-17 12:12:41 +02:00
Michael Hoang
5f90d60bd6 Merge pull request 'flake: remove unnecessary follows for data-mesher' (#4383) from push-yzqmtrtrkkzt into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4383
2025-07-17 12:12:41 +02:00
Qubasa
02ed516a15 buildClan: Add deprecation warning 2025-07-17 12:12:41 +02:00
Luis Hebendanz
14a6af1259 Merge pull request 'inventory: Add missing default value for exports.instances and exports.machines' (#4382) from Qubasa/clan-core:fix_inv_missing_default into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4382
2025-07-17 12:12:41 +02:00
Michael Hoang
fd26dcd0d0 flake: remove unnecessary follows for data-mesher 2025-07-17 12:12:41 +02:00
clan-bot
3b316cbf3e Merge pull request 'Update disko' (#4381) from update-disko into main 2025-07-17 12:12:41 +02:00
Qubasa
d9657d8617 inventory: Add missing default value for exports.instances and exports.machines 2025-07-17 12:12:41 +02:00
brianmcgee
74fb95653a Merge pull request 'chore: add a check for background.jpg' (#4380) from chore/stupid-jpg-check into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4380
2025-07-17 12:12:41 +02:00
gitea-actions[bot]
6073ab4a0b Update disko 2025-07-17 12:12:41 +02:00
hsjobeki
9e35d040da Merge pull request 'UI/cubes: extend cubes scene' (#4375) from scene-progress into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4375
2025-07-17 12:12:41 +02:00
Brian McGee
0e2904b34b chore: add a check for background.jpg 2025-07-17 12:12:41 +02:00
brianmcgee
71ba979120 Merge pull request 'feat: onboarding workflow' (#4379) from ui/onboarding-workflow into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4379
2025-07-17 12:12:41 +02:00
Johannes Kirschbauer
fb4b8f2745 ui/cubes: align with design 2025-07-17 12:12:41 +02:00
Brian McGee
0028311805 feat: onboarding workflow 2025-07-17 12:12:41 +02:00
Johannes Kirschbauer
abacd19c12 ui/cubes: init story 2025-07-17 12:12:41 +02:00
Johannes Kirschbauer
9446009738 ui/storybook: add all stories 2025-07-17 12:12:41 +02:00
Johannes Kirschbauer
dc7566951c UI/cubes: group logic to add more meshed 2025-07-17 12:12:41 +02:00
Johannes Kirschbauer
67d2e18fb8 cubes: scene extend 2025-07-17 12:12:41 +02:00
Mic92
350b55ea16 Merge pull request 'Update data-mesher' (#4370) from update-data-mesher into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4370
2025-07-17 12:12:40 +02:00
Mic92
ce50278621 Merge pull request 'clan-cli: Move flash.py to clan_lib/flash' (#4374) from Qubasa/clan-core:move_flash into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4374
2025-07-17 12:12:40 +02:00
gitea-actions[bot]
d9262e47cb Update data-mesher 2025-07-17 12:12:40 +02:00
DavHau
ce411b4784 Merge pull request 'cleanup_install' (#4373) from Qubasa/clan-core:cleanup_install into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4373
2025-07-17 12:12:40 +02:00
Qubasa
55f0da45a8 clan-cli: Move flash.py to clan_lib/flash 2025-07-17 12:12:40 +02:00
brianmcgee
dc7b10efe2 Merge pull request 'feat: ui/toolbar' (#4357) from ui/toolbar into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4357
2025-07-17 12:12:40 +02:00
Qubasa
3687fd48ce clan-cli: Reference HostKeyCheck literal instead of duplicating the list everywhere 2025-07-17 12:12:40 +02:00
hsjobeki
a4d26497f9 Merge pull request 'cli: fix dot files not copied to $out in buildPythonApplication' (#4371) from pkgs-for into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4371
2025-07-17 12:12:40 +02:00
brianmcgee
1c14033d48 Merge pull request 'onboarding workflow' (#4366) from ui/onboarding-workflow into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4366
Reviewed-by: Mic92 <joerg@thalheim.io>
2025-07-17 12:12:40 +02:00
Brian McGee
13ca0f5050 feat(ui): toolbar component 2025-07-17 12:12:40 +02:00
Qubasa
85f219ca1e clan-lib: Remove duplicate fields from installOptions and instead use them from Remote 2025-07-17 12:12:40 +02:00
hsjobeki
503bb62864 Merge pull request 'clanInternals: refactor configsPerSystem, minimize diff' (#4369) from pkgs-for into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4369
2025-07-17 12:12:40 +02:00
Johannes Kirschbauer
c0225ea757 cli: fix dot files not copied $out in buildPythonApplication
File such as .envrc, .gitignore where not copied into the package and thus missing in all templates
2025-07-17 12:12:40 +02:00
Brian McGee
d6b27d8740 wip: onboarding workflow 2025-07-17 12:12:40 +02:00
Qubasa
c09cb834fa clan-lib: Change BuildOn enum to Literal type. Literals can be translated better to typescript 2025-07-17 12:12:40 +02:00
Johannes Kirschbauer
91434044e0 clanInternals: refactor configsPerSystem, minimize diff 2025-07-17 12:12:40 +02:00
Qubasa
56a76242ca clan-cli: Fix incorrect ipv6 check in check_machine_ssh_reachable 2025-07-17 12:12:40 +02:00
Kenji Berthold
7255673440 Merge pull request 'pkgs/cli: Validate clan directory for update-hardware-config' (#4367) from kenji/ke-hardware-update-validation into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4367
2025-07-17 12:12:39 +02:00
Luis Hebendanz
5b651752ba Merge pull request 'pkgs/cli: Fix ssh logging' (#4362) from kenji/ke-ssh-remove-debug into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4362
2025-07-17 12:12:39 +02:00
hsjobeki
b1aa79b33f Merge pull request 'revert bd3861c58056a847556c459ce420968044ce1459' (#4368) from hsjobeki-patch-1 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4368
2025-07-17 12:12:39 +02:00
a-kenji
26fdfeec00 pkgs/cli: Validate clan directory for update-hardware-config 2025-07-17 12:12:39 +02:00
kenji
a0c3b6d33e Merge pull request 'pkgs/clan(templates): Add shell completions' (#4327) from kenji/ke-disko-shell into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4327
2025-07-17 12:12:39 +02:00
Mic92
8be8af9117 Merge pull request 'gitignore-images' (#4364) from gitignore-images into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4364
2025-07-17 12:12:39 +02:00
hsjobeki
338a6ad340 revert bd3861c580
revert Merge pull request 'Remove clanModules/*' (#4202) from remove-modules into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4202

See: https://git.clan.lol/clan/clan-core/issues/4365

Not all modules are migrated.
If they are not migrated, we need to write migration docs and please display the link to the migration docs
2025-07-17 12:12:39 +02:00
kenji
38fafba6d8 Merge pull request 'pkgs/cli: Add facts deprecation warning to clan facts help output' (#4329) from kenji/ke-facts-cli-warning into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4329
2025-07-17 12:12:39 +02:00
Jörg Thalheim
2dcc4bba15 update-flake-inputs: email/user doesn't need to be configured 2025-07-17 12:12:39 +02:00
clan-bot
b57897c6c9 Merge pull request 'Update sops-nix' (#4361) from update-sops-nix into main 2025-07-17 12:12:39 +02:00
Jörg Thalheim
b640be3dd2 run flake updates every 5 hours 2025-07-17 12:12:39 +02:00
pinpox
7266e4b273 Merge pull request 'Remove clanModules/*' (#4202) from remove-modules into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4202
2025-07-17 12:12:39 +02:00
pinpox
546eeb22d0 Remove clanModules 2025-07-17 12:12:39 +02:00
144 changed files with 1966 additions and 5304 deletions

View File

@@ -1,20 +0,0 @@
name: Build Clan App (Darwin)
on:
schedule:
# Run every 4 hours
- cron: "0 */4 * * *"
workflow_dispatch:
push:
branches:
- main
jobs:
build-clan-app-darwin:
runs-on: nix
steps:
- uses: actions/checkout@v4
- name: Build clan-app for x86_64-darwin
run: |
nix build .#packages.x86_64-darwin.clan-app --system x86_64-darwin --log-format bar-with-logs

View File

@@ -1,7 +1,6 @@
#!/bin/sh
#!/usr/bin/env bash
# Shared script for creating pull requests in Gitea workflows
set -eu
set -euo pipefail
# Required environment variables:
# - CI_BOT_TOKEN: Gitea bot token for authentication
@@ -9,22 +8,22 @@ set -eu
# - PR_TITLE: Title of the pull request
# - PR_BODY: Body/description of the pull request
if [ -z "${CI_BOT_TOKEN:-}" ]; then
if [[ -z "${CI_BOT_TOKEN:-}" ]]; then
echo "Error: CI_BOT_TOKEN is not set" >&2
exit 1
fi
if [ -z "${PR_BRANCH:-}" ]; then
if [[ -z "${PR_BRANCH:-}" ]]; then
echo "Error: PR_BRANCH is not set" >&2
exit 1
fi
if [ -z "${PR_TITLE:-}" ]; then
if [[ -z "${PR_TITLE:-}" ]]; then
echo "Error: PR_TITLE is not set" >&2
exit 1
fi
if [ -z "${PR_BODY:-}" ]; then
if [[ -z "${PR_BODY:-}" ]]; then
echo "Error: PR_BODY is not set" >&2
exit 1
fi
@@ -44,12 +43,9 @@ resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
}" \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
if ! pr_number=$(echo "$resp" | jq -r '.number'); then
echo "Error parsing response from pull request creation" >&2
exit 1
fi
pr_number=$(echo "$resp" | jq -r '.number')
if [ "$pr_number" = "null" ]; then
if [[ "$pr_number" == "null" ]]; then
echo "Error creating pull request:" >&2
echo "$resp" | jq . >&2
exit 1
@@ -68,15 +64,12 @@ while true; do
"delete_branch_after_merge": true
}' \
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
if ! msg=$(echo "$resp" | jq -r '.message'); then
echo "Error parsing merge response" >&2
exit 1
fi
if [ "$msg" != "Please try again later" ]; then
msg=$(echo "$resp" | jq -r '.message')
if [[ "$msg" != "Please try again later" ]]; then
break
fi
echo "Retrying in 2 seconds..."
sleep 2
done
echo "Pull request #$pr_number merge initiated"
echo "Pull request #$pr_number merge initiated"

View File

@@ -24,7 +24,7 @@ If you're new to Clan and eager to dive in, start with our quickstart guide and
In the Clan ecosystem, security is paramount. Learn how to handle secrets effectively:
- **Secrets Management**: Securely manage secrets by consulting [Vars](https://docs.clan.lol/guides/vars-backend/)<!-- [secrets.md](docs/site/guides/vars-backend.md) -->.
- **Secrets Management**: Securely manage secrets by consulting [secrets](https://docs.clan.lol/guides/getting-started/secrets/)<!-- [secrets.md](docs/site/guides/getting-started/secrets.md) -->.
### Contributing to Clan

View File

@@ -38,6 +38,7 @@
recommendedOptimisation = lib.mkDefault true;
recommendedProxySettings = lib.mkDefault true;
recommendedTlsSettings = lib.mkDefault true;
recommendedZstdSettings = lib.mkDefault true;
# Nginx sends all the access logs to /var/log/nginx/access.log by default.
# instead of going to the journal!

View File

@@ -1,47 +0,0 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/internet";
manifest.description = "direct access (or via ssh jumphost) to machines";
manifest.categories = [
"System"
"Network"
];
roles.default = {
interface =
{ lib, ... }:
{
options = {
host = lib.mkOption {
type = lib.types.str;
description = ''
ip address or hostname (domain) of the machine
'';
};
jumphosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
optional list of jumphosts to use to connect to the machine
'';
};
};
};
perInstance =
{
roles,
lib,
settings,
...
}:
{
exports.networking = {
# TODO add user space network support to clan-cli
peers = lib.mapAttrs (_name: machine: {
host.plain = machine.settings.host;
SSHOptions = map (_x: "-J x") machine.settings.jumphosts;
}) roles.default.machines;
};
};
};
}

View File

@@ -1,9 +0,0 @@
{ lib, ... }:
let
module = lib.modules.importApply ./default.nix { };
in
{
clan.modules = {
internet = module;
};
}

View File

@@ -1,110 +0,0 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/tor";
manifest.description = "Onion routing, use Hidden services to connect your machines";
manifest.categories = [
"System"
"Network"
];
roles.client = {
perInstance =
{
...
}:
{
nixosModule =
{
...
}:
{
config = {
services.tor = {
enable = true;
torsocks.enable = true;
client.enable = true;
};
};
};
};
};
roles.server = {
# interface =
# { lib, ... }:
# {
# options = {
# OciSettings = lib.mkOption {
# type = lib.types.raw;
# default = null;
# description = "NixOS settings for virtualisation.oci-container.<name>.settings";
# };
# buildContainer = lib.mkOption {
# type = lib.types.nullOr lib.types.str;
# default = null;
# };
# };
# };
perInstance =
{
instanceName,
roles,
lib,
...
}:
{
exports.networking = {
priority = lib.mkDefault 10;
# TODO add user space network support to clan-cli
module = "clan_lib.network.tor";
peers = lib.mapAttrs (name: machine: {
host.var = {
machine = name;
generator = "tor_${instanceName}";
file = "hostname";
};
}) roles.server.machines;
};
nixosModule =
{
pkgs,
config,
...
}:
{
config = {
services.tor = {
enable = true;
relay.onionServices."clan_${instanceName}" = {
version = 3;
# TODO get ports from instance machine config
map = [
{
port = 22;
target.port = 22;
}
];
secretKey = config.clan.core.vars.generators."tor_${instanceName}".files.hs_ed25519_secret_key.path;
};
};
clan.core.vars.generators."tor_${instanceName}" = {
files.hs_ed25519_secret_key = { };
files.hostname = { };
runtimeInputs = with pkgs; [
coreutils
tor
];
script = ''
mkdir -p data
echo -e "DataDirectory ./data\nSocksPort 0\nHiddenServiceDir ./hs\nHiddenServicePort 80 127.0.0.1:80" > torrc
timeout 2 tor -f torrc || :
mv hs/hs_ed25519_secret_key $out/hs_ed25519_secret_key
mv hs/hostname $out/hostname
'';
};
};
};
};
};
}

View File

@@ -1,9 +0,0 @@
{ lib, ... }:
let
module = lib.modules.importApply ./default.nix { };
in
{
clan.modules = {
tor = module;
};
}

View File

@@ -39,7 +39,7 @@ in
};
perInstance =
{ instanceName, settings, ... }:
{ settings, ... }:
{
nixosModule =
{ pkgs, config, ... }:
@@ -86,7 +86,7 @@ in
# service to generate the environment file containing all secrets, as
# expected by the nixos NetworkManager-ensure-profile service
systemd.services."NetworkManager-setup-secrets-${instanceName}" = {
systemd.services.NetworkManager-setup-secrets = {
description = "Generate wifi secrets for NetworkManager";
requiredBy = [ "NetworkManager-ensure-profiles.service" ];
partOf = [ "NetworkManager-ensure-profiles.service" ];

View File

@@ -7,16 +7,8 @@
inventory = {
machines.test = { };
machines.second = { };
instances = {
wg-test-all = {
module.name = "@clan/wifi";
module.input = "self";
roles.default.tags.all = { };
roles.default.settings.networks.all = { };
};
wg-test-one = {
module.name = "@clan/wifi";
module.input = "self";

View File

@@ -134,9 +134,9 @@
systemd.services.zerotier-inventory-autoaccept =
let
machines = uniqueStrings (
(lib.optionals (roles ? moon) (lib.attrNames roles.moon.machines))
++ (lib.optionals (roles ? controller) (lib.attrNames roles.controller.machines))
++ (lib.optionals (roles ? peer) (lib.attrNames roles.peer.machines))
(lib.attrNames roles.moon.machines)
++ (lib.attrNames roles.controller.machines)
++ (lib.attrNames roles.peer.machines)
);
networkIps = builtins.foldl' (
ips: name:

View File

@@ -32,33 +32,6 @@ let
};
};
}).config;
testFlakeNoMoon =
(clanLib.clan {
self = { };
directory = ./vm;
machines.jon = {
nixpkgs.hostPlatform = "x86_64-linux";
};
machines.sara = {
nixpkgs.hostPlatform = "x86_64-linux";
};
machines.bam = {
nixpkgs.hostPlatform = "x86_64-linux";
};
modules.zerotier = module;
inventory.instances = {
zerotier = {
module.name = "zerotier";
module.input = "self";
roles.peer.tags.all = { };
roles.controller.machines.bam = { };
};
};
}).config;
in
{
test_peers = {
@@ -100,30 +73,4 @@ in
networkName = "zerotier";
};
};
test_peers_no_moon = {
expr = {
hasNetworkIds = testFlakeNoMoon.nixosConfigurations.jon.config.services.zerotierone.joinNetworks;
isController =
testFlakeNoMoon.nixosConfigurations.jon.config.clan.core.networking.zerotier.controller.enable;
networkName = testFlakeNoMoon.nixosConfigurations.jon.config.clan.core.networking.zerotier.name;
};
expected = {
hasNetworkIds = [ "0e28cb903344475e" ];
isController = false;
networkName = "zerotier";
};
};
test_controller_no_moon = {
expr = {
hasNetworkIds = testFlakeNoMoon.nixosConfigurations.bam.config.services.zerotierone.joinNetworks;
isController =
testFlakeNoMoon.nixosConfigurations.bam.config.clan.core.networking.zerotier.controller.enable;
networkName = testFlakeNoMoon.nixosConfigurations.bam.config.clan.core.networking.zerotier.name;
};
expected = {
hasNetworkIds = [ "0e28cb903344475e" ];
isController = true;
networkName = "zerotier";
};
};
}

View File

@@ -48,25 +48,34 @@ nav:
- Home: index.md
- Guides:
- Getting Started:
- Creating Your First Clan: guides/getting-started/index.md
- Create USB Installer: guides/getting-started/installer.md
- Add Machines: guides/getting-started/add-machines.md
- Add User: guides/getting-started/add-user.md
- Add Services: guides/getting-started/add-services.md
- Deploy Machine: guides/getting-started/deploy.md
- Continuous Integration: guides/getting-started/check.md
- Inventory: guides/inventory.md
- Using Services: guides/clanServices.md
- Backup & Restore: guides/backups.md
- 🚀 Creating Your First Clan: guides/getting-started/index.md
- 📀 Create USB Installer (optional): guides/getting-started/installer.md
- ⚙️ Add Machines: guides/getting-started/add-machines.md
- ⚙️ Add User: guides/getting-started/add-user.md
- ⚙️ Add Services: guides/getting-started/add-services.md
- 🔐 Secrets & Facts: guides/getting-started/secrets.md
- 🚢 Deploy Machine: guides/getting-started/deploy.md
- 🧪 Continuous Integration: guides/getting-started/check.md
- clanServices: guides/clanServices.md
- Disk Encryption: guides/disk-encryption.md
- Vars: guides/vars-backend.md
- Age Plugins: guides/age-plugins.md
- Advanced Secrets: guides/secrets.md
- Machine Autoincludes: guides/more-machines.md
- Mesh VPN: guides/mesh-vpn.md
- Backup & Restore: guides/backups.md
- Vars Backend: guides/vars-backend.md
- Facts Backend: guides/secrets.md
- Adding more machines: guides/more-machines.md
- Target Host: guides/target-host.md
- Zerotier VPN: guides/mesh-vpn.md
- Inventory:
- Inventory: guides/inventory.md
- Secure Boot: guides/secure-boot.md
- Flake-parts: guides/flake-parts.md
- Authoring:
- clanService: guides/authoring/clanServices/index.md
- Disk Template: guides/authoring/templates/disk/disko-templates.md
- clanModule: guides/authoring/clanModules/index.md
- Contributing:
- Contribute: guides/contributing/CONTRIBUTING.md
- Debugging: guides/contributing/debugging.md
- Testing: guides/contributing/testing.md
- Migrations:
- Migrate existing Flakes: guides/migrations/migration-guide.md
- Migrate inventory Services: guides/migrations/migrate-inventory-services.md
@@ -76,84 +85,66 @@ nav:
- Reference:
- Overview: reference/index.md
- Services:
- List:
- Overview: reference/clanServices/index.md
- reference/clanServices/admin.md
- reference/clanServices/borgbackup.md
- reference/clanServices/data-mesher.md
- reference/clanServices/emergency-access.md
- reference/clanServices/garage.md
- reference/clanServices/hello-world.md
- reference/clanServices/importer.md
- reference/clanServices/mycelium.md
- reference/clanServices/packages.md
- reference/clanServices/sshd.md
- reference/clanServices/state-version.md
- reference/clanServices/trusted-nix-caches.md
- reference/clanServices/users.md
- reference/clanServices/wifi.md
- reference/clanServices/zerotier.md
- API: reference/clanServices/clan-service-author-interface.md
- Writing a Service Module: developer/extensions/clanServices/index.md
- Overview: reference/clanServices/index.md
- reference/clanServices/admin.md
- reference/clanServices/borgbackup.md
- reference/clanServices/data-mesher.md
- reference/clanServices/emergency-access.md
- reference/clanServices/garage.md
- reference/clanServices/hello-world.md
- reference/clanServices/importer.md
- reference/clanServices/mycelium.md
- reference/clanServices/packages.md
- reference/clanServices/sshd.md
- reference/clanServices/state-version.md
- reference/clanServices/trusted-nix-caches.md
- reference/clanServices/users.md
- reference/clanServices/wifi.md
- reference/clanServices/zerotier.md
- Interface for making Services: reference/clanServices/clan-service-author-interface.md
- Modules:
- List:
- Overview: reference/clanModules/index.md
- reference/clanModules/frontmatter/index.md
# TODO: display the docs of the clan.service modules
- reference/clanModules/admin.md
# This is the module overview and should stay at the top
- reference/clanModules/borgbackup-static.md
- reference/clanModules/data-mesher.md
- reference/clanModules/borgbackup.md
- reference/clanModules/deltachat.md
- reference/clanModules/disk-id.md
- reference/clanModules/dyndns.md
- reference/clanModules/ergochat.md
- reference/clanModules/garage.md
- reference/clanModules/heisenbridge.md
- reference/clanModules/importer.md
- reference/clanModules/iwd.md
- reference/clanModules/localbackup.md
- reference/clanModules/localsend.md
- reference/clanModules/matrix-synapse.md
- reference/clanModules/moonlight.md
- reference/clanModules/mumble.md
- reference/clanModules/mycelium.md
- reference/clanModules/nginx.md
- reference/clanModules/packages.md
- reference/clanModules/postgresql.md
- reference/clanModules/root-password.md
- reference/clanModules/single-disk.md
- reference/clanModules/sshd.md
- reference/clanModules/state-version.md
- reference/clanModules/static-hosts.md
- reference/clanModules/sunshine.md
- reference/clanModules/syncthing-static-peers.md
- reference/clanModules/syncthing.md
- reference/clanModules/thelounge.md
- reference/clanModules/trusted-nix-caches.md
- reference/clanModules/user-password.md
- reference/clanModules/auto-upgrade.md
- reference/clanModules/vaultwarden.md
- reference/clanModules/xfce.md
- reference/clanModules/zerotier-static-peers.md
- reference/clanModules/zerotier.md
- reference/clanModules/zt-tcp-relay.md
- Writing a Clan Module: developer/extensions/clanModules/index.md
- Nix API:
- inputs.clan-core.lib.clan: reference/nix-api/clan.md
- config.clan.core:
- Overview: reference/clan.core/index.md
- reference/clan.core/backups.md
- reference/clan.core/deployment.md
- reference/clan.core/facts.md
- reference/clan.core/networking.md
- reference/clan.core/settings.md
- reference/clan.core/sops.md
- reference/clan.core/state.md
- reference/clan.core/vars.md
- Inventory: reference/nix-api/inventory.md
- Overview: reference/clanModules/index.md
- reference/clanModules/frontmatter/index.md
# TODO: display the docs of the clan.service modules
- reference/clanModules/admin.md
# This is the module overview and should stay at the top
- reference/clanModules/borgbackup-static.md
- reference/clanModules/data-mesher.md
- reference/clanModules/borgbackup.md
- reference/clanModules/deltachat.md
- reference/clanModules/disk-id.md
- reference/clanModules/dyndns.md
- reference/clanModules/ergochat.md
- reference/clanModules/garage.md
- reference/clanModules/heisenbridge.md
- reference/clanModules/importer.md
- reference/clanModules/iwd.md
- reference/clanModules/localbackup.md
- reference/clanModules/localsend.md
- reference/clanModules/matrix-synapse.md
- reference/clanModules/moonlight.md
- reference/clanModules/mumble.md
- reference/clanModules/mycelium.md
- reference/clanModules/nginx.md
- reference/clanModules/packages.md
- reference/clanModules/postgresql.md
- reference/clanModules/root-password.md
- reference/clanModules/single-disk.md
- reference/clanModules/sshd.md
- reference/clanModules/state-version.md
- reference/clanModules/static-hosts.md
- reference/clanModules/sunshine.md
- reference/clanModules/syncthing-static-peers.md
- reference/clanModules/syncthing.md
- reference/clanModules/thelounge.md
- reference/clanModules/trusted-nix-caches.md
- reference/clanModules/user-password.md
- reference/clanModules/auto-upgrade.md
- reference/clanModules/vaultwarden.md
- reference/clanModules/xfce.md
- reference/clanModules/zerotier-static-peers.md
- reference/clanModules/zerotier.md
- reference/clanModules/zt-tcp-relay.md
- CLI:
- Overview: reference/cli/index.md
@@ -170,7 +161,21 @@ nav:
- reference/cli/templates.md
- reference/cli/vars.md
- reference/cli/vms.md
- NixOS Modules:
- clan.core:
- Overview: reference/clan.core/index.md
- reference/clan.core/backups.md
- reference/clan.core/deployment.md
- reference/clan.core/facts.md
- reference/clan.core/networking.md
- reference/clan.core/settings.md
- reference/clan.core/sops.md
- reference/clan.core/state.md
- reference/clan.core/vars.md
- Nix API:
- clan: reference/nix-api/clan.md
- Inventory: reference/nix-api/inventory.md
- Glossary: reference/glossary.md
- Decisions:
- Architecture Decisions: decisions/README.md
@@ -182,14 +187,8 @@ nav:
- Template: decisions/_template.md
- Options: options.md
- Developer:
- Introduction: developer/index.md
- Dev Setup: developer/contributing/CONTRIBUTING.md
- Writing a Service Module: developer/extensions/clanServices/index.md
- Writing a Clan Module: developer/extensions/clanModules/index.md
- Writing a Disko Template: developer/extensions/templates/disk/disko-templates.md
- Debugging: developer/contributing/debugging.md
- Testing: developer/contributing/testing.md
- Python API: developer/api.md
- Introduction: intern/index.md
- API: intern/api.md
docs_dir: site
site_dir: out
@@ -247,6 +246,3 @@ plugins:
- search
- macros
- redoc-tag
- redirects:
redirect_maps:
guides/getting-started/secrets.md: guides/vars-backend.md

View File

@@ -40,7 +40,6 @@ pkgs.stdenv.mkDerivation {
mkdocs-material
mkdocs-macros
mkdocs-redoc-tag
mkdocs-redirects
]);
configurePhase = ''
pushd docs

View File

@@ -114,6 +114,9 @@
in
{
options = {
_ = mkOption {
type = types.raw;
};
instances.${name} = lib.mkOption {
inherit description;
type = types.submodule {
@@ -146,29 +149,20 @@
};
};
docModules = [
{
inherit self;
}
self.modules.clan.default
{
options.inventory = lib.mkOption {
type = types.submoduleWith {
modules = [
{ noInstanceOptions = true; }
] ++ mapAttrsToList fakeInstanceOptions serviceModules;
};
};
}
];
mkScope = name: modules: {
inherit name;
modules = [
{
_module.args = { inherit clanLib; };
_file = "docs mkScope";
}
{ noInstanceOptions = true; }
../../../lib/modules/inventoryClass/interface.nix
] ++ mapAttrsToList fakeInstanceOptions modules;
urlPrefix = "https://github.com/nix-community/dream2nix/blob/main/";
};
in
{
# Uncomment for debugging
# legacyPackages.docModules = lib.evalModules {
# modules = docModules;
# };
packages = lib.optionalAttrs ((privateInputs ? nuschtos) || (inputs ? nuschtos)) {
docs-options =
(privateInputs.nuschtos or inputs.nuschtos)
@@ -177,13 +171,7 @@
inherit baseHref;
title = "Clan Options";
# scopes = mapAttrsToList mkScope serviceModules;
scopes = [
{
name = "Clan";
modules = docModules;
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
}
];
scopes = [ (mkScope "Clan Inventory" serviceModules) ];
};
};
};

View File

@@ -465,10 +465,6 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
service_links: dict[str, dict[str, dict[str, Any]]] = json.load(f3)
for module_name, module_info in service_links.items():
# Skip specific modules that are not ready for documentation
if module_name in ["internet", "tor"]:
continue
output = f"# {module_name}\n\n"
# output += f"`clan.modules.{module_name}`\n"
output += f"*{module_info['manifest']['description']}*\n"
@@ -805,7 +801,7 @@ Typically needed by module authors to define roles, behavior and metadata for di
!!! Note
This is not a user-facing documentation, but rather meant as a reference for *module authors*
See: [clanService Authoring Guide](../../developer/extensions/clanServices/index.md)
See: [clanService Authoring Guide](../../guides/authoring/clanServices/index.md)
"""
# Inventory options are already included under the clan attribute
# We just omitted them in the clan docs, because we want a separate output for the inventory model

View File

@@ -1,59 +0,0 @@
## Using Age Plugins
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
You must **precede your secret key with a comment that contains its corresponding recipient**.
This is usually output as part of the generation process
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
Here is an example:
```title="~/.config/sops/age/keys.txt"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
!!! note
The comment that precedes the plugin secret key need only contain the recipient.
Any other text is ignored.
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
just `# age1zdy....`
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
are loaded when using Clan:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add Yubikey and FIDO2 HMAC plugins
# Note: the plugins listed here must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# elided for brevity
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# elided for brevity
};
}
```

View File

@@ -267,5 +267,5 @@ The benefit of this approach is that downstream users can override the value of
## Further
- [Reference Documentation for Service Authors](../../../reference/clanServices/clan-service-author-interface.md)
- [Migration Guide from ClanModules to ClanServices](../../../guides/migrations/migrate-inventory-services.md)
- [Migration Guide from ClanModules to ClanServices](../../migrations/migrate-inventory-services.md)
- [Decision that lead to ClanServices](../../../decisions/01-ClanModules.md)

View File

@@ -138,7 +138,7 @@ You can use services exposed by Clans core module library, `clan-core`.
You can also author your own `clanService` modules.
🔗 Learn how to write your own service: [Authoring a clanService](../developer/extensions/clanServices/index.md)
🔗 Learn how to write your own service: [Authoring a clanService](../guides/authoring/clanServices/index.md)
You might expose your service module from your flake — this makes it easy for other people to also use your module in their clan.
@@ -154,6 +154,6 @@ You might expose your service module from your flake — this makes it easy for
## Whats Next?
* [Author your own clanService →](../developer/extensions/clanServices/index.md)
* [Author your own clanService →](../guides/authoring/clanServices/index.md)
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
<!-- TODO: * [Understand the architecture →](../explanation/clan-architecture.md) -->

View File

@@ -41,7 +41,7 @@ To learn more: [Guide about clanService](../clanServices.md)
```
1. See [reference/clanServices](../../reference/clanServices/index.md) for all available services and how to configure them.
Or read [authoring/clanServices](../../developer/extensions/clanServices/index.md) if you want to bring your own
Or read [authoring/clanServices](../authoring/clanServices/index.md) if you want to bring your own
2. Replace `__YOUR_CONTROLLER_` with the *name* of your machine.

View File

@@ -57,7 +57,7 @@ For more information see [clanService/users](../../reference/clanServices/users.
Some people like to define a `users` folder in their repository root.
That allows to bind all user specific logic to a single place (`default.nix`)
Which can be imported into individual machines to make the user available on that machine.
Which can be imported into individual machines to make the user avilable on that machine.
```bash
.
@@ -107,7 +107,7 @@ We can use this property of clan services to bind a nixosModule to the user, whi
}
```
1. Type `path` or `string`: Must point to a separate file. Inlining a module is not possible
1. Type `path` or `string`: Must point to a seperate file. Inlining a module is not possible
!!! Note "This is inspiration"
Our community might come up with better solutions soon.

View File

@@ -8,6 +8,7 @@ Now that you have created a machines, added some services and setup secrets. Thi
- [x] RAM > 2GB
- [x] **Two Computers**: You need one computer that you're getting ready (we'll call this the Target Computer) and another one to set it up from (we'll call this the Setup Computer). Make sure both can talk to each other over the network using SSH.
- [x] **Machine configuration**: See our basic [adding and configuring machine guide](./add-machines.md)
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
## Physical Hardware
@@ -17,7 +18,7 @@ Steps:
- Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md).
- Boot the target machine and connect it to a network that makes it reachable from your setup computer.
- Note down a reachable ip address (*ipv4*, *ipv6* or *tor*)
- Note down a reachable ip adress (*ipv4*, *ipv6* or *tor*)
---
@@ -168,7 +169,7 @@ Re-run the command with the correct disk:
clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368"
```
Should now be successful
Should now be succesfull
```shellSession
Applied disk template 'single-disk' to machine 'jon'

View File

@@ -59,7 +59,7 @@ Enter a *name*, confirm with *enter*. A directory with that name will be created
## Explore the Project Structure
Take a look at all project files:
Take a lookg at all project files:
```bash
cd my-clan
@@ -125,10 +125,11 @@ To change the name of your clan edit `meta.name` in the `clan.nix` or `flake.nix
You can continue with **any** of the following steps at your own pace:
- [x] [Install Nix & Clan CLI](./index.md)
- [x] [Initialize Clan](./index.md#add-clan-cli-to-your-shell)
- [x] [Initialize Clan](./index.md#initialize-your-project)
- [ ] [Create USB Installer (optional)](./installer.md)
- [ ] [Add Machines](./add-machines.md)
- [ ] [Add a User](./add-user.md)
- [ ] [Add Services](./add-services.md)
- [ ] [Configure Secrets](./secrets.md)
- [ ] [Deploy](./deploy.md) - Requires configured secrets
- [ ] [Setup CI (optional)](./check.md)

View File

@@ -0,0 +1,179 @@
Setting up secrets is **Required** for any *machine deployments* or *vm runs* - You need to complete the steps: [Create Admin Keypair](#create-your-admin-keypair) and [Add Your Public Key(s)](#add-your-public-keys)
---
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
By default, Clan uses the [sops](https://github.com/getsops/sops) format
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
Clan can also be configured to be used with other secret store [backends](../../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
This guide will walk you through:
- **Creating a Keypair for Your User**: Learn how to generate a keypair for `$USER` to securely control all secrets.
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
## Create Your Admin Keypair
To get started, you'll need to create **your admin keypair**.
!!! info
Don't worry — if you've already made one before, this step won't change or overwrite it.
```bash
clan secrets key generate
```
**Output**:
```{.console, .no-copy}
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
```
!!! warning
Make sure to keep a safe backup of the private key you've just created.
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
```title="~/.config/sops/age/keys.txt"
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
```
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
using `SOPS_AGE_KEY_FILE`.
For more information see the [SOPS] guide on [encrypting with age].
!!! note
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
### Add Your Public Key(s)
```console
clan secrets users add $USER --age-key <your_public_key>
```
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
Once run this will create the following files:
```{.console, .no-copy}
sops/
└── users/
└── <your_username>/
└── key.json
```
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
!!! note
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
```console
clan secrets users add $USER \
--age-key <your_public_key_1> \
--age-key <your_public_key_2> \
...
```
### Manage Your Public Key(s)
You can list keys for your user with `clan secrets users get $USER`:
```console
clan secrets users get alice
[
{
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
"type": "age",
"username": "alice"
},
{
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
"type": "age",
"username": "alice"
}
]
```
To add a new key to your user:
```console
clan secrets users add-key $USER --age-key <your_public_key>
```
To remove a key from your user:
```console
clan secrets users remove-key $USER --age-key <your_public_key>
```
[age]: https://github.com/FiloSottile/age
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
[sops]: https://github.com/getsops/sops
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
## Further: Using Age Plugins
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
You must **precede your secret key with a comment that contains its corresponding recipient**.
This is usually output as part of the generation process
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
Here is an example:
```title="~/.config/sops/age/keys.txt"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
!!! note
The comment that precedes the plugin secret key need only contain the recipient.
Any other text is ignored.
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
just `# age1zdy....`
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
are loaded when using Clan:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add Yubikey and FIDO2 HMAC plugins
# Note: the plugins listed here must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# elided for brevity
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# elided for brevity
};
}
```

View File

@@ -1,7 +1,7 @@
# Migrating from using `clanModules` to `clanServices`
**Audience**: This is a guide for **people using `clanModules`**.
If you are a **module author** and need to migrate your modules please consult our **new** [clanServices authoring guide](../../developer/extensions/clanServices/index.md)
If you are a **module author** and need to migrate your modules please consult our **new** [clanServices authoring guide](../authoring/clanServices/index.md)
## What's Changing?
@@ -35,37 +35,6 @@ services = {
};
```
### Complex Example: Multi-service Setup
```nix
# Old format
services = {
borgbackup.production = {
roles.server.machines = [ "backup-server" ];
roles.server.config = {
directory = "/var/backup/borg";
};
roles.client.tags = [ "backup" ];
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
};
zerotier.company-network = {
roles.controller.machines = [ "network-controller" ];
roles.moon.machines = [ "moon-1" "moon-2" ];
roles.peer.tags = [ "nixos" ];
};
sshd.internal = {
roles.server.tags = [ "nixos" ];
roles.client.tags = [ "nixos" ];
config.certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
```
---
## ✅ After: New `instances` Definition with `clanServices`
@@ -101,56 +70,6 @@ instances = {
};
```
### Complex Example Migrated
```nix
# New format
instances = {
borgbackup-production = {
module = {
name = "borgbackup";
input = "clan-core";
};
roles.server.machines."backup-server" = { };
roles.server.settings = {
directory = "/var/backup/borg";
};
roles.client.tags.backup = { };
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
};
zerotier-company-network = {
module = {
name = "zerotier";
input = "clan-core";
};
roles.controller.machines."network-controller" = { };
roles.moon.machines."moon-1".settings = {
stableEndpoints = [ "10.0.0.1" "2001:db8::1" ];
};
roles.moon.machines."moon-2".settings = {
stableEndpoints = [ "10.0.0.2" "2001:db8::2" ];
};
roles.peer.tags.nixos = { };
};
sshd-internal = {
module = {
name = "sshd";
input = "clan-core";
};
roles.server.tags.nixos = { };
roles.client.tags.nixos = { };
roles.client.settings = {
certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
};
```
---
## Steps to Migrate
@@ -212,33 +131,6 @@ roles.default.machines."test-inventory-machine".settings = {
};
```
### Important Type Changes
The new `instances` format uses **attribute sets** instead of **lists** for tags and machines:
```nix
# ❌ Old format (lists)
roles.client.tags = [ "backup" ];
roles.server.machines = [ "blob64" ];
# ✅ New format (attribute sets)
roles.client.tags.backup = { };
roles.server.machines.blob64 = { };
```
### Handling Multiple Machines/Tags
When you need to assign multiple machines or tags to a role:
```nix
# ❌ Old format
roles.moon.machines = [ "eva" "eve" ];
# ✅ New format - each machine gets its own attribute
roles.moon.machines.eva = { };
roles.moon.machines.eve = { };
```
---
!!! Warning
@@ -246,89 +138,8 @@ roles.moon.machines.eve = { };
* `inventory.services` is no longer recommended; use `inventory.instances` instead.
* Module authors should begin exporting service modules under the `clan.modules` attribute of their flake.
## Troubleshooting Common Migration Errors
### Error: "not of type `attribute set of (submodule)`"
This error occurs when using lists instead of attribute sets for tags or machines:
```
error: A definition for option `flake.clan.inventory.instances.borgbackup-blob64.roles.client.tags' is not of type `attribute set of (submodule)'.
```
**Solution**: Convert lists to attribute sets as shown in the "Important Type Changes" section above.
### Error: "unsupported attribute `module`"
This error indicates the module structure is incorrect:
```
error: Module ':anon-4:anon-1' has an unsupported attribute `module'.
```
**Solution**: Ensure the `module` attribute has exactly two fields: `name` and `input`.
### Error: "attribute 'pkgs' missing"
This suggests the instance configuration is trying to use imports incorrectly:
```
error: attribute 'pkgs' missing
```
**Solution**: Use the `module = { name = "..."; input = "..."; }` format instead of `imports`.
### Removed Features
The following features from the old `services` format are no longer supported in `instances`:
- Top-level `config` attribute (use `roles.<role>.settings` instead)
- Direct module imports (use the `module` declaration instead)
### extraModules Support
The `extraModules` attribute is still supported in the new instances format! The key change is how modules are specified:
**Old format (string paths relative to clan root):**
```nix
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
```
**New format (NixOS modules):**
```nix
# Direct module reference
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
# Or using self
roles.client.extraModules = [ self.nixosModules.borgbackup ];
# Or inline module definition
roles.client.extraModules = [
{ config, ... }: {
# Your module configuration here
}
];
```
The `extraModules` now expects actual **NixOS modules** rather than string paths. This provides better type checking and more flexibility in how modules are specified.
**Alternative: Using @clan/importer**
For scenarios where you need to import modules with specific tag-based targeting, you can also use the dedicated `@clan/importer` service:
```nix
instances = {
my-importer = {
module.name = "@clan/importer";
module.input = "clan-core";
roles.default.tags.my-tag = { };
roles.default.extraModules = [ self.nixosModules.myModule ];
};
};
```
## Further reference
* [Authoring a 'clan.service' module](../../developer/extensions/clanServices/index.md)
* [Authoring a 'clan.service' module](../authoring/clanServices/index.md)
* [ClanServices](../clanServices.md)
* [Inventory Reference](../../reference/nix-api/inventory.md)
* [Inventory Reference](../../reference/nix-api/inventory.md)

View File

@@ -1,141 +1,25 @@
This article provides an overview over the underlying secrets system which is used by [Vars](../guides/vars-backend.md).
Under most circumstances you should use [Vars](../guides/vars-backend.md) directly instead.
If you want to know more about how to save and share passwords in your clan read further!
Consider using `clan secrets` only for managing admin users and groups, as well as a debugging tool.
Manually interacting with secrets via `clan secrets [set|remove]`, etc may break the integrity of your `Vars` state.
---
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
By default, Clan uses the [sops](https://github.com/getsops/sops) format
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
Clan can also be configured to be used with other secret store [backends](../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
## Create Your Admin Keypair
To get started, you'll need to create **your admin keypair**.
!!! info
Don't worry — if you've already made one before, this step won't change or overwrite it.
```bash
clan secrets key generate
```
**Output**:
```{.console, .no-copy}
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
```
!!! warning
Make sure to keep a safe backup of the private key you've just created.
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
```title="~/.config/sops/age/keys.txt"
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
```
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
using `SOPS_AGE_KEY_FILE`.
For more information see the [SOPS] guide on [encrypting with age].
!!! note
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
## Add Your Public Key(s)
```console
clan secrets users add $USER --age-key <your_public_key>
```
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
Once run this will create the following files:
```{.console, .no-copy}
sops/
└── users/
└── <your_username>/
└── key.json
```
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
!!! note
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
```console
clan secrets users add $USER \
--age-key <your_public_key_1> \
--age-key <your_public_key_2> \
...
```
## Manage Your Public Key(s)
You can list keys for your user with `clan secrets users get $USER`:
```console
clan secrets users get alice
[
{
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
"type": "age",
"username": "alice"
},
{
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
"type": "age",
"username": "alice"
}
]
```
To add a new key to your user:
```console
clan secrets users add-key $USER --age-key <your_public_key>
```
To remove a key from your user:
```console
clan secrets users remove-key $USER --age-key <your_public_key>
```
[age]: https://github.com/FiloSottile/age
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
[sops]: https://github.com/getsops/sops
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
## Adding a Secret
### Adding a Secret
```shellSession
clan secrets set mysecret
Paste your secret:
```
## Retrieving a Stored Secret
### Retrieving a Stored Secret
```bash
clan secrets get mysecret
```
## List all Secrets
### List all Secrets
```bash
clan secrets list
```
## NixOS integration
### NixOS integration
A NixOS machine will automatically import all secrets that are encrypted for the
current machine. At runtime it will use the host key to decrypt all secrets into
@@ -153,7 +37,7 @@ In your nixos configuration you can get a path to secrets like this `config.sops
}
```
## Assigning Access
### Assigning Access
When using `clan secrets set <secret>` without arguments, secrets are encrypted for the key of the user named like your current $USER.

View File

@@ -4,21 +4,7 @@ hide:
- toc
---
# :material-home: What is Clan?
[Clan](https://clan.lol/) is a peer-to-peer computer management framework that
empowers you to reclaim control over your digital computing experience. Built on
NixOS, Clan provides a unified interface for managing networks of machines with
automated [secret management](./guides/secrets.md), secure [mesh VPN
connectivity](./guides/mesh-vpn.md), and customizable installation images. Whether
you're running a homelab or building decentralized computing infrastructure,
Clan simplifies configuration management while restoring your independence from
closed computing ecosystems.
At the heart of Clan are [Clan Services](./reference/clanServices/index.md) - the core
concept that enables you to add functionality across multiple machines in your
network. While Clan ships with essential core services, you can [create custom
services](./guides/clanServices.md) tailored to your specific needs.
# :material-home: Welcome to **Clan**'s documentation
[Getting Started](./guides/getting-started/index.md){ .md-button }
@@ -52,7 +38,7 @@ services](./guides/clanServices.md) tailored to your specific needs.
Use Clan with [https://flake.parts/]()
- [Contribute](./developer/contributing/CONTRIBUTING.md)
- [Contribute](./guides/contributing/CONTRIBUTING.md)
---

View File

@@ -2,7 +2,6 @@
font-family: "Roboto";
src: url(./Roboto-Regular.ttf) format("truetype");
}
@font-face {
font-family: "Fira Code";
src: url(./FiraCode-VF.ttf) format("truetype");
@@ -21,9 +20,3 @@
.md-nav__item.md-nav__item--section > label > span {
color: var(--md-typeset-a-color);
}
.md-typeset h4 {
margin: 3em 0 0.5em;
font-weight: bold;
color: #7ebae4;
}

26
flake.lock generated
View File

@@ -13,11 +13,11 @@
]
},
"locked": {
"lastModified": 1753067306,
"narHash": "sha256-jyoEbaXa8/MwVQ+PajUdT63y3gYhgD9o7snO/SLaikw=",
"rev": "18dfd42bdb2cfff510b8c74206005f733e38d8b9",
"lastModified": 1752589312,
"narHash": "sha256-BafZOenlzMYdumG12AzgVLhEVu+GcEa8nYNDSIYe1U0=",
"rev": "496bbf05a2aa7b061ef464254db5804d1c6f45b4",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/18dfd42bdb2cfff510b8c74206005f733e38d8b9.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/496bbf05a2aa7b061ef464254db5804d1c6f45b4.tar.gz"
},
"original": {
"type": "tarball",
@@ -31,11 +31,11 @@
]
},
"locked": {
"lastModified": 1753140376,
"narHash": "sha256-7lrVrE0jSvZHrxEzvnfHFE/Wkk9DDqb+mYCodI5uuB8=",
"lastModified": 1752718651,
"narHash": "sha256-PkaR0qmyP9q/MDN3uYa+RLeBA0PjvEQiM0rTDDBXkL8=",
"owner": "nix-community",
"repo": "disko",
"rev": "545aba02960caa78a31bd9a8709a0ad4b6320a5c",
"rev": "d5ad4485e6f2edcc06751df65c5e16572877db88",
"type": "github"
},
"original": {
@@ -51,11 +51,11 @@
]
},
"locked": {
"lastModified": 1753121425,
"narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=",
"lastModified": 1751413152,
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "644e0fc48951a860279da645ba77fe4a6e814c5e",
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
"type": "github"
},
"original": {
@@ -181,11 +181,11 @@
]
},
"locked": {
"lastModified": 1753006367,
"narHash": "sha256-tzbhc4XttkyEhswByk5R38l+ztN9UDbnj0cTcP6Hp9A=",
"lastModified": 1752055615,
"narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "421b56313c65a0815a52b424777f55acf0b56ddf",
"rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9",
"type": "github"
},
"original": {

View File

@@ -78,87 +78,7 @@ in
internal = true;
visible = false;
type = types.deferredModule;
default = {
options.networking = lib.mkOption {
default = null;
type = lib.types.nullOr (
lib.types.submodule {
options = {
priority = lib.mkOption {
type = lib.types.int;
default = 1000;
description = ''
priority with which this network should be tried.
higher priority means it gets used earlier in the chain
'';
};
module = lib.mkOption {
# type = lib.types.enum [
# "clan_lib.network.direct"
# "clan_lib.network.tor"
# ];
type = lib.types.str;
default = "clan_lib.network.direct";
description = ''
the technology this network uses to connect to the target
This is used for userspace networking with socks proxies.
'';
};
# should we call this machines? hosts?
peers = lib.mkOption {
# <name>
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
};
SSHOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
host = lib.mkOption {
description = '''';
type = lib.types.attrTag {
plain = lib.mkOption {
type = lib.types.str;
description = ''
a plain value, which can be read directly from the config
'';
};
var = lib.mkOption {
type = lib.types.submodule {
options = {
machine = lib.mkOption {
type = lib.types.str;
example = "jon";
};
generator = lib.mkOption {
type = lib.types.str;
example = "tor-ssh";
};
file = lib.mkOption {
type = lib.types.str;
example = "hostname";
};
};
};
};
};
};
};
}
)
);
};
};
}
);
};
};
default = { };
description = ''
A module that is used to define the module of flake level exports -
@@ -229,8 +149,8 @@ in
};
inventory = lib.mkOption {
type = types.submoduleWith {
modules = [
type = types.submodule {
imports = [
{
_module.args = { inherit clanLib; };
_file = "clan interface";

View File

@@ -247,7 +247,7 @@ in
{
distributedServices = clanLib.inventory.mapInstances {
inherit (clanConfig) inventory exportsModule;
inherit flakeInputs directory;
inherit flakeInputs;
clanCoreModules = clan-core.clan.modules;
prefix = [ "distributedServices" ];
};

View File

@@ -1,8 +1,4 @@
# Wraps all services in one fixed point module
{
# TODO: consume directly from clan.config
directory,
}:
{
lib,
config,
@@ -33,8 +29,6 @@ in
{
_module.args._ctx = [ name ];
_module.args.exports' = config.exports;
_module.args.directory = directory;
}
)
./service-module.nix
@@ -77,5 +71,8 @@ in
};
default = { };
};
debug = mkOption {
default = lib.mapAttrsToList (_: service: service.exports) config.mappedServices;
};
};
}

View File

@@ -24,7 +24,6 @@ in
flakeInputs,
# The clan inventory
inventory,
directory,
clanCoreModules,
prefix ? [ ],
exportsModule,
@@ -129,7 +128,7 @@ in
_ctx = prefix;
};
modules = [
(import ./all-services-wrapper.nix { inherit directory; })
./all-services-wrapper.nix
] ++ modules;
};

View File

@@ -2,7 +2,6 @@
lib,
config,
_ctx,
directory,
...
}:
let
@@ -213,7 +212,7 @@ in
options.extraModules = lib.mkOption {
default = [ ];
type = types.listOf (types.either types.deferredModule types.str);
type = types.listOf (types.deferredModule);
};
})
];
@@ -756,14 +755,10 @@ in
instanceRes
// {
nixosModule = {
imports =
[
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
instanceRes.nixosModule
]
++ (map (
s: if builtins.typeOf s == "string" then "${directory}/${s}" else s
) instanceCfg.roles.${roleName}.extraModules);
imports = [
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
instanceRes.nixosModule
] ++ instanceCfg.roles.${roleName}.extraModules;
};
}

View File

@@ -45,7 +45,6 @@ let
};
in
clanLib.inventory.mapInstances {
directory = ./.;
clanCoreModules = { };
flakeInputs = flakeInputsFixture;
inherit inventory;
@@ -53,7 +52,6 @@ let
};
in
{
extraModules = import ./extraModules.nix { inherit clanLib; };
exports = import ./exports.nix { inherit lib clanLib; };
resolve_module_spec = import ./import_module_spec.nix { inherit lib callInventoryAdapter; };
test_simple =

View File

@@ -1,33 +0,0 @@
{ clanLib }:
let
clan = clanLib.clan {
self = { };
directory = ./.;
machines.jon = {
nixpkgs.hostPlatform = "x86_64-linux";
};
# A module that adds exports perMachine
modules.A = {
manifest.name = "A";
roles.peer = { };
};
inventory = {
instances.A = {
module.input = "self";
roles.peer.tags.all = { };
roles.peer.extraModules = [ ./oneOption.nix ];
};
};
};
in
{
test_1 = {
inherit clan;
expr = clan.config.nixosConfigurations.jon.config.testDebug;
expected = 42;
};
}

View File

@@ -1,6 +0,0 @@
{ lib, ... }:
{
options.testDebug = lib.mkOption {
default = 42;
};
}

View File

@@ -142,7 +142,7 @@ in
- The module MUST have at least `features = [ "inventory" ]` in the frontmatter section.
- The module MUST have a subfolder `roles` with at least one `{roleName}.nix` file.
For further information see: [Module Authoring Guide](../../developer/extensions/clanServices/index.md).
For further information see: [Module Authoring Guide](../../guides/authoring/clanServices/index.md).
???+ example
```nix
@@ -179,8 +179,8 @@ in
map (m: "'${m}'") (lib.attrNames (lib.filterAttrs (n: _v: !builtins.elem n allowedNames) moduleSet))
)}
See: https://docs.clan.lol/developer/extensions/clanServices/
And: https://docs.clan.lol/developer/extensions/clanServices/
See: https://docs.clan.lol/guides/clanServices/
And: https://docs.clan.lol/guides/authoring/clanServices/
'' moduleSet;
};

View File

@@ -313,18 +313,6 @@ class Machine:
command = f"nc -z {shlex.quote(addr)} {port}"
self.wait_until_succeeds(command, timeout=timeout)
def wait_for_file(self, filename: str, timeout: int = 30) -> None:
"""
Waits until the file exists in the machine's file system.
"""
def check_file(_last_try: bool) -> bool:
result = self.execute(f"test -e {filename}")
return result.returncode == 0
with self.nested(f"waiting for file '{filename}'"):
retry(check_file, timeout)
def wait_for_unit(self, unit: str, timeout: int = 900) -> None:
"""
Wait for a systemd unit to get into "active" state.
@@ -419,14 +407,6 @@ def setup_filesystems(container: ContainerInfo) -> None:
Path("/etc/os-release").touch()
Path("/etc/machine-id").write_text("a5ea3f98dedc0278b6f3cc8c37eeaeac")
container.nix_store_dir.mkdir(parents=True)
# Recreate symlinks
for file in Path("/nix/store").iterdir():
if file.is_symlink():
target = file.readlink()
sym = container.nix_store_dir / file.name
os.symlink(target, sym)
# Read /proc/mounts and replicate every bind mount
with Path("/proc/self/mounts").open() as f:
for line in f:

View File

@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any
from clan_lib.api import ApiResponse
from clan_lib.api.tasks import WebThread
from clan_lib.async_run import set_current_thread_opkey, set_should_cancel
from clan_lib.async_run import set_should_cancel
if TYPE_CHECKING:
from .middleware import Middleware
@@ -98,7 +98,7 @@ class ApiBridge(ABC):
*,
thread_name: str = "ApiBridgeThread",
wait_for_completion: bool = False,
timeout: float = 60.0 * 60, # 1 hour default timeout
timeout: float = 60.0,
) -> None:
"""Process an API request in a separate thread with cancellation support.
@@ -112,7 +112,6 @@ class ApiBridge(ABC):
def thread_task(stop_event: threading.Event) -> None:
set_should_cancel(lambda: stop_event.is_set())
set_current_thread_opkey(op_key)
try:
log.debug(
f"Processing {request.method_name} with args {request.args} "

View File

@@ -9,7 +9,6 @@ gi.require_version("Gtk", "4.0")
from clan_lib.api import ApiError, ErrorDataClass, SuccessDataClass
from clan_lib.api.directory import FileRequest
from clan_lib.async_run import get_current_thread_opkey
from clan_lib.clan.check import check_clan_valid
from clan_lib.flake import Flake
from gi.repository import Gio, GLib, Gtk
@@ -25,7 +24,7 @@ def remove_none(_list: list) -> list:
RESULT: dict[str, SuccessDataClass[list[str] | None] | ErrorDataClass] = {}
def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
def get_clan_folder(*, op_key: str) -> SuccessDataClass[Flake] | ErrorDataClass:
"""
Opens the clan folder using the GTK file dialog.
Returns the path to the clan folder or an error if it fails.
@@ -35,10 +34,7 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
title="Select Clan Folder",
initial_folder=str(Path.home()),
)
response = get_system_file(file_request)
op_key = response.op_key
response = get_system_file(file_request, op_key=op_key)
if isinstance(response, ErrorDataClass):
return response
@@ -74,13 +70,8 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
def get_system_file(
file_request: FileRequest,
file_request: FileRequest, *, op_key: str
) -> SuccessDataClass[list[str] | None] | ErrorDataClass:
op_key = get_current_thread_opkey()
if not op_key:
msg = "No operation key found in the current thread context."
raise RuntimeError(msg)
GLib.idle_add(gtk_open_file, file_request, op_key)
while RESULT.get(op_key) is None:

View File

@@ -21,12 +21,18 @@ class ArgumentParsingMiddleware(Middleware):
# Convert dictionary arguments to dataclass instances
reconciled_arguments = {}
for k, v in context.request.args.items():
if k == "op_key":
continue
# Get the expected argument type from the API
arg_class = self.api.get_method_argtype(context.request.method_name, k)
# Convert dictionary to dataclass instance
reconciled_arguments[k] = from_dict(arg_class, v)
# Add op_key to arguments
reconciled_arguments["op_key"] = context.request.op_key
# Create a new request with reconciled arguments
updated_request = BackendRequest(

View File

@@ -1,22 +1,13 @@
import json
import logging
import threading
import uuid
from http.server import BaseHTTPRequestHandler
from pathlib import Path
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse
from clan_lib.api import (
MethodRegistry,
SuccessDataClass,
dataclass_to_dict,
)
from clan_lib.api import MethodRegistry, SuccessDataClass, dataclass_to_dict
from clan_lib.api.tasks import WebThread
from clan_lib.async_run import (
set_current_thread_opkey,
set_should_cancel,
)
from clan_app.api.api_bridge import ApiBridge, BackendRequest, BackendResponse
@@ -333,34 +324,17 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
msg = f"Operation key '{op_key}' is already in use. Please try again."
raise ValueError(msg)
def process_request_in_thread(
self,
request: BackendRequest,
*,
thread_name: str = "ApiBridgeThread",
wait_for_completion: bool = False,
timeout: float = 60.0 * 60, # 1 hour default timeout
) -> None:
pass
def _process_api_request_in_thread(
self, api_request: BackendRequest, method_name: str
) -> None:
"""Process the API request in a separate thread."""
stop_event = threading.Event()
request = api_request
op_key = request.op_key or "unknown"
set_should_cancel(lambda: stop_event.is_set())
set_current_thread_opkey(op_key)
curr_thread = threading.current_thread()
self.threads[op_key] = WebThread(thread=curr_thread, stop_event=stop_event)
log.debug(
f"Processing {request.method_name} with args {request.args} "
f"and header {request.header}"
# Use the inherited thread processing method
self.process_request_in_thread(
api_request,
thread_name="HttpThread",
wait_for_completion=True,
timeout=60.0,
)
self.process_request(request)
def log_message(self, format: str, *args: Any) -> None: # noqa: A002
"""Override default logging to use our logger."""

View File

@@ -29,7 +29,10 @@ def _get_lib_names() -> list[str]:
msg = f"Unsupported architecture: {machine}"
raise RuntimeError(msg)
if system == "darwin":
return ["libwebview.dylib"]
if machine == "arm64":
return ["libwebview.dylib"]
msg = "Not supported"
raise RuntimeError(msg)
# linux
return ["libwebview.so"]

View File

@@ -0,0 +1,39 @@
version: "0.5"
processes:
# App Dev
clan-app-ui:
namespace: "app"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
npm install
vite
ready_log_line: "VITE"
clan-app:
namespace: "app"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app
./bin/clan-app --debug --content-uri http://localhost:3000
depends_on:
clan-app-ui:
condition: "process_log_ready"
is_foreground: true
ready_log_line: "Debug mode enabled"
# Storybook Dev
storybook:
namespace: "storybook"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
npm run storybook-dev -- --ci
ready_log_line: "started"
luakit:
namespace: "storybook"
command: "luakit http://localhost:6006"
depends_on:
storybook:
condition: "process_log_ready"

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -17,7 +17,6 @@
"@solidjs/router": "^0.15.3",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"@tanstack/solid-query-devtools": "^5.83.0",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",
@@ -54,6 +53,7 @@
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
"solid-devtools": "^0.34.0",
"storybook": "^9.0.8",
"swagger-ui-dist": "^5.26.2",
"tailwindcss": "^3.4.3",
@@ -360,6 +360,22 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-typescript": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
"integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
@@ -1536,6 +1552,13 @@
"node": ">= 8"
}
},
"node_modules/@nothing-but/utils": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@nothing-but/utils/-/utils-0.17.0.tgz",
"integrity": "sha512-TuCHcHLOqDL0SnaAxACfuRHBNRgNJcNn9X0GiH5H3YSDBVquCr3qEIG3FOQAuMyZCbu9w8nk2CHhOsn7IvhIwQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@oxc-resolver/binding-darwin-arm64": {
"version": "11.5.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.5.0.tgz",
@@ -1790,6 +1813,64 @@
"@sinonjs/commons": "^3.0.1"
}
},
"node_modules/@solid-devtools/debugger": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@solid-devtools/debugger/-/debugger-0.28.1.tgz",
"integrity": "sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nothing-but/utils": "~0.17.0",
"@solid-devtools/shared": "^0.20.0",
"@solid-primitives/bounds": "^0.1.1",
"@solid-primitives/event-listener": "^2.4.1",
"@solid-primitives/keyboard": "^1.3.1",
"@solid-primitives/rootless": "^1.5.1",
"@solid-primitives/scheduled": "^1.5.1",
"@solid-primitives/static-store": "^0.1.1",
"@solid-primitives/utils": "^6.3.1"
},
"peerDependencies": {
"solid-js": "^1.9.0"
}
},
"node_modules/@solid-devtools/shared": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@solid-devtools/shared/-/shared-0.20.0.tgz",
"integrity": "sha512-o5TACmUOQsxpzpOKCjbQqGk8wL8PMi+frXG9WNu4Lh3PQVUB6hs95Kl/S8xc++zwcMguUKZJn8h5URUiMOca6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nothing-but/utils": "~0.17.0",
"@solid-primitives/event-listener": "^2.4.1",
"@solid-primitives/media": "^2.3.1",
"@solid-primitives/refs": "^1.1.1",
"@solid-primitives/rootless": "^1.5.1",
"@solid-primitives/scheduled": "^1.5.1",
"@solid-primitives/static-store": "^0.1.1",
"@solid-primitives/styles": "^0.1.1",
"@solid-primitives/utils": "^6.3.1"
},
"peerDependencies": {
"solid-js": "^1.9.0"
}
},
"node_modules/@solid-primitives/bounds": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/bounds/-/bounds-0.1.3.tgz",
"integrity": "sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/event-listener": "^2.4.3",
"@solid-primitives/resize-observer": "^2.1.3",
"@solid-primitives/static-store": "^0.1.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/event-listener": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/event-listener/-/event-listener-2.4.3.tgz",
@@ -1802,6 +1883,21 @@
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/keyboard": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/keyboard/-/keyboard-1.3.3.tgz",
"integrity": "sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/event-listener": "^2.4.3",
"@solid-primitives/rootless": "^1.5.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/keyed": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/keyed/-/keyed-1.5.2.tgz",
@@ -1889,6 +1985,16 @@
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/scheduled": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/scheduled/-/scheduled-1.5.2.tgz",
"integrity": "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/static-store": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/static-store/-/static-store-0.1.2.tgz",
@@ -1922,6 +2028,20 @@
}
}
},
"node_modules/@solid-primitives/styles": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/styles/-/styles-0.1.2.tgz",
"integrity": "sha512-7iX5K+J5b1PRrbgw3Ki92uvU2LgQ0Kd/QMsrAZxDg5dpUBwMyTijZkA3bbs1ikZsT1oQhS41bTyKbjrXeU0Awg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/rootless": "^1.5.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/trigger": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/trigger/-/trigger-1.2.2.tgz",
@@ -2161,19 +2281,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-devtools": {
"version": "5.81.2",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz",
"integrity": "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==",
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz",
"integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2181,12 +2291,12 @@
}
},
"node_modules/@tanstack/solid-query": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.83.0.tgz",
"integrity": "sha512-RF8Tv9+6+Kmzj+EafbTzvzzPq+J5SzHtc1Tz3D2MZ/EvlZTH+GL5q4HNnWK3emg7CB6WzyGnTuERmmWJaZs8/w==",
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.81.5.tgz",
"integrity": "sha512-VqVXaxiJIsKA6B45uApF+RUD3g8Roj/vdAuGpHMjR+RyHqlyQ+hOwgmALkzlbkbIaWCQi8CJOvrbU6WOBuMOxA==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.83.0"
"@tanstack/query-core": "5.81.5"
},
"funding": {
"type": "github",
@@ -2196,23 +2306,6 @@
"solid-js": "^1.6.0"
}
},
"node_modules/@tanstack/solid-query-devtools": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-devtools/-/solid-query-devtools-5.83.0.tgz",
"integrity": "sha512-Z0wQlAWXz/U2bJ/paMRBTDhMoPnB9Te6GmA21sXnI+nDnAAPZRcPxFBiCgYJS3eFsvbkdRGJwoUSQrdIgy0shg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-devtools": "5.81.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/solid-query": "^5.83.0",
"solid-js": "^1.6.0"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -6903,6 +6996,29 @@
"url": "https://github.com/sponsors/cyyynthia"
}
},
"node_modules/solid-devtools": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/solid-devtools/-/solid-devtools-0.34.3.tgz",
"integrity": "sha512-ZQua959n+Zu3sLbm9g0IRjYUb1YYlYbu83PWLRoKbSsq0a3ItQNhnS2OBU7rQNmOKZiMexNo9Z3izas9BcOKDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@babel/plugin-syntax-typescript": "^7.27.1",
"@babel/types": "^7.27.6",
"@solid-devtools/debugger": "^0.28.1",
"@solid-devtools/shared": "^0.20.0"
},
"peerDependencies": {
"solid-js": "^1.9.0",
"vite": "^2.2.3 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/solid-js": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz",

View File

@@ -52,6 +52,7 @@
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
"solid-devtools": "^0.34.0",
"storybook": "^9.0.8",
"swagger-ui-dist": "^5.26.2",
"tailwindcss": "^3.4.3",
@@ -72,7 +73,6 @@
"@solidjs/router": "^0.15.3",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"@tanstack/solid-query-devtools": "^5.83.0",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",

View File

@@ -9,7 +9,6 @@ import { TextInput } from "@/src/components/Form/TextInput";
import { TextArea } from "@/src/components/Form/TextArea";
import { Checkbox } from "@/src/components/Form/Checkbox";
import { FieldProps } from "./Field";
import { HostFileInput } from "@/src/components/Form/HostFileInput";
const FieldsetExamples = (props: FieldsetProps) => (
<div class="flex flex-col gap-8">
@@ -27,7 +26,7 @@ const meta = {
<div
class={cx({
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
"w-[512px]": context.args.orientation == "horizontal",
"w-[1024px]": context.args.orientation == "horizontal",
"bg-inv-acc-3": context.args.inverted,
})}
>
@@ -64,11 +63,6 @@ export const Default: Story = {
label="Bio"
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
/>
<HostFileInput
{...props}
label="Profile pic"
onSelectFile={async () => "/home/foo/bar/baz/fizz/buzz/bla/bizz"}
/>
<Checkbox {...props} label="Accept Terms" required={true} />
</>
),

View File

@@ -1,11 +1,5 @@
div.form-field.host-file {
button {
@apply w-fit;
}
&.horizontal {
button {
@apply grow max-w-[18rem];
}
@apply w-1/2;
}
}

View File

@@ -47,10 +47,7 @@ export const HostFileInput = (props: HostFileInputProps) => {
})}
{...props}
>
<Orienter
orientation={props.orientation}
align={props.orientation == "horizontal" ? "center" : "start"}
>
<Orienter orientation={props.orientation} align={"start"}>
<Label
labelComponent={TextField.Label}
descriptionComponent={TextField.Description}

View File

@@ -5,7 +5,7 @@ div.orienter {
}
&.horizontal {
@apply flex-row justify-between gap-0;
@apply flex-row justify-start;
& > div.form-label {
@apply w-1/2 shrink;

View File

@@ -1,5 +1,5 @@
div.modal-content {
@apply min-w-[320px] max-w-[512px];
@apply max-w-[512px];
@apply rounded-md;
/* todo replace with a theme() color */
@@ -12,7 +12,7 @@ div.modal-content {
@apply border border-def-2 rounded-tl-md rounded-tr-md;
@apply border-b-def-3;
& > .modal-title {
& > .title {
@apply mx-auto;
}
}

View File

@@ -3,7 +3,6 @@ import { Dialog as KDialog } from "@kobalte/core/dialog";
import "./Modal.css";
import { Typography } from "../Typography/Typography";
import Icon from "../Icon/Icon";
import cx from "classnames";
export interface ModalContext {
close(): void;
@@ -14,8 +13,6 @@ export interface ModalProps {
title: string;
onClose: () => void;
children: (ctx: ModalContext) => JSX.Element;
mount?: Node;
class?: string;
}
export const Modal = (props: ModalProps) => {
@@ -23,33 +20,18 @@ export const Modal = (props: ModalProps) => {
return (
<KDialog id={props.id} open={open()} modal={true}>
<KDialog.Portal mount={props.mount}>
<KDialog.Content class={cx("modal-content", props.class)}>
<KDialog.Portal>
<KDialog.Content class="modal-content">
<div class="header">
<Typography
class="modal-title"
hierarchy="label"
family="mono"
size="xs"
>
<Typography class="title" hierarchy="label" family="mono" size="xs">
{props.title}
</Typography>
<KDialog.CloseButton
onClick={() => {
setOpen(false);
props.onClose();
}}
>
<KDialog.CloseButton onClick={() => setOpen(false)}>
<Icon icon="Close" size="0.75rem" />
</KDialog.CloseButton>
</div>
<div class="body">
{props.children({
close: () => {
setOpen(false);
props.onClose();
},
})}
{props.children({ close: () => setOpen(false) })}
</div>
</KDialog.Content>
</KDialog.Portal>

View File

@@ -1,157 +0,0 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
createMemoryHistory,
MemoryRouter,
Route,
RouteSectionProps,
} from "@solidjs/router";
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
import { Suspense } from "solid-js";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { addClanURI, resetStore } from "@/src/stores/clan";
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
import { encodeBase64 } from "@/src/hooks/clan";
const defaultClanURI = "/home/brian/clans/my-clan";
const queryData = {
"/home/brian/clans/my-clan": {
details: {
name: "Brian's Clan",
uri: "/home/brian/clans/my-clan",
},
machines: {
europa: {
name: "Europa",
machineClass: "nixos",
},
ganymede: {
name: "Ganymede",
machineClass: "nixos",
},
},
},
"/home/brian/clans/davhau": {
details: {
name: "Dave's Clan",
uri: "/home/brian/clans/davhau",
},
machines: {
callisto: {
name: "Callisto",
machineClass: "nixos",
},
amalthea: {
name: "Amalthea",
machineClass: "nixos",
},
},
},
"/home/brian/clans/mic92": {
details: {
name: "Mic92's Clan",
uri: "/home/brian/clans/mic92",
},
machines: {
thebe: {
name: "Thebe",
machineClass: "nixos",
},
sponde: {
name: "Sponde",
machineClass: "nixos",
},
},
},
};
const staticSections = [
{
title: "Links",
links: [
{ label: "GitHub", path: "https://github.com/brian-the-dev" },
{ label: "Twitter", path: "https://twitter.com/brian_the_dev" },
{
label: "LinkedIn",
path: "https://www.linkedin.com/in/brian-the-dev/",
},
{
label: "Instagram",
path: "https://www.instagram.com/brian_the_dev/",
},
],
},
];
const meta: Meta<RouteSectionProps> = {
title: "Components/Sidebar",
component: Sidebar,
render: () => {
// set history to point to our test clan
const history = createMemoryHistory();
history.set({ value: `/clans/${encodeBase64(defaultClanURI)}` });
// reset local storage and then add each clan
resetStore();
Object.keys(queryData).forEach((uri) => addClanURI(uri));
return (
<div style="height: 670px;">
<MemoryRouter
history={history}
root={(props) => <Suspense>{props.children}</Suspense>}
>
<Route
path="/clans/:clanURI"
component={() => <Sidebar staticSections={staticSections} />}
>
<Route path="/" />
<Route
path="/machines/:machineID"
component={() => <h1>Machine</h1>}
/>
</Route>
</MemoryRouter>
<SolidQueryDevtools initialIsOpen={true} />
</div>
);
},
};
export default meta;
type Story = StoryObj<RouteSectionProps>;
export const Default: Story = {
args: {},
decorators: [
(Story: StoryObj) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Infinity,
},
},
});
Object.entries(queryData).forEach(([clanURI, clan]) => {
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "details"],
clan.details,
);
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "machines"],
clan.machines || {},
);
});
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
);
},
],
};

View File

@@ -1,28 +0,0 @@
import "./Sidebar.css";
import { SidebarHeader } from "@/src/components/Sidebar/SidebarHeader";
import { SidebarBody } from "@/src/components/Sidebar/SidebarBody";
export interface LinkProps {
path: string;
label?: string;
}
export interface SectionProps {
title: string;
links: LinkProps[];
}
export interface SidebarProps {
staticSections?: SectionProps[];
}
export const Sidebar = (props: SidebarProps) => {
return (
<>
<div class="sidebar">
<SidebarHeader />
<SidebarBody {...props} />
</div>
</>
);
};

View File

@@ -1,107 +0,0 @@
import "./SidebarHeader.css";
import Icon from "@/src/components/Icon/Icon";
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router";
import { Typography } from "../Typography/Typography";
import { createSignal, For, Suspense } from "solid-js";
import { useClanListQuery } from "@/src/queries/queries";
import { navigateToClan, useClanURI } from "@/src/hooks/clan";
import { clanURIs } from "@/src/stores/clan";
export const SidebarHeader = () => {
const navigate = useNavigate();
const [open, setOpen] = createSignal(false);
// get information about the current active clan
const clanURI = useClanURI();
const allClans = useClanListQuery(clanURIs());
const activeClan = () => allClans.find(({ data }) => data?.uri === clanURI);
return (
<div class="sidebar-header">
<Suspense fallback={"Loading..."}>
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger">
<div class="clan-label">
<div class="clan-icon">
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={true}
>
{activeClan()?.data?.name.charAt(0).toUpperCase()}
</Typography>
</div>
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={!open()}
>
{activeClan()?.data?.name}
</Typography>
</div>
<DropdownMenu.Icon>
<Icon icon={"CaretDown"} inverted={!open()} size="0.75rem" />
</DropdownMenu.Icon>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content">
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigateToClan(navigate, clanURI)}
>
<Icon
icon="Settings"
size="0.75rem"
inverted={true}
color="tertiary"
/>
<Typography hierarchy="label" size="xs" weight="medium">
Settings
</Typography>
</DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group">
<DropdownMenu.GroupLabel class="dropdown-group-label">
<Typography
hierarchy="label"
family="mono"
size="xs"
color="tertiary"
>
YOUR CLANS
</Typography>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={allClans}>
{(clan) => (
<Suspense fallback={"Loading..."}>
<DropdownMenu.Item
class="dropdown-item"
onSelect={() =>
navigateToClan(navigate, clan.data!.uri)
}
>
<Typography
hierarchy="label"
size="xs"
weight="medium"
>
{clan.data?.name}
</Typography>
</DropdownMenu.Item>
</Suspense>
)}
</For>
</div>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</Suspense>
</div>
);
};

View File

@@ -1,5 +1,5 @@
div.sidebar {
@apply w-60 border-none;
@apply h-full w-auto max-w-60 border-none;
& > div.header {
}

View File

@@ -0,0 +1,109 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
createMemoryHistory,
MemoryRouter,
Route,
RouteSectionProps,
} from "@solidjs/router";
import {
SidebarNav,
SidebarNavProps,
} from "@/src/components/Sidebar/SidebarNav";
import { Suspense } from "solid-js";
import { StoryContext } from "@kachurun/storybook-solid-vite";
const sidebarNavProps: SidebarNavProps = {
clanLinks: [
{ label: "Brian's Clan", path: "/clan/1" },
{ label: "Dave's Clan", path: "/clan/2" },
{ label: "Mic92's Clan", path: "/clan/3" },
],
clanDetail: {
label: "Brian's Clan",
settingsPath: "/clan/1/settings",
machines: [
{
label: "Backup & Home",
path: "/clan/1/machine/backup",
serviceCount: 3,
status: "Online",
},
{
label: "Raspberry Pi",
path: "/clan/1/machine/pi",
serviceCount: 1,
status: "Offline",
},
{
label: "Mom's Laptop",
path: "/clan/1/machine/moms-laptop",
serviceCount: 2,
status: "Installed",
},
{
label: "Dad's Laptop",
path: "/clan/1/machine/dads-laptop",
serviceCount: 4,
status: "Not Installed",
},
],
},
extraSections: [
{
label: "Tools",
links: [
{ label: "Borgbackup", path: "/clan/1/service/borgbackup" },
{ label: "Syncthing", path: "/clan/1/service/syncthing" },
{ label: "Mumble", path: "/clan/1/service/mumble" },
{ label: "Minecraft", path: "/clan/1/service/minecraft" },
],
},
{
label: "Links",
links: [
{ label: "GitHub", path: "https://github.com/brian-the-dev" },
{ label: "Twitter", path: "https://twitter.com/brian_the_dev" },
{
label: "LinkedIn",
path: "https://www.linkedin.com/in/brian-the-dev/",
},
{
label: "Instagram",
path: "https://www.instagram.com/brian_the_dev/",
},
],
},
],
};
const meta: Meta<RouteSectionProps> = {
title: "Components/Sidebar/Nav",
component: SidebarNav,
render: (_: never, context: StoryContext<SidebarNavProps>) => {
const history = createMemoryHistory();
history.set({ value: "/clan/1/machine/backup" });
return (
<div style="height: 670px;">
<MemoryRouter
history={history}
root={(props) => (
<Suspense>
<SidebarNav {...sidebarNavProps} />
</Suspense>
)}
>
<Route path="/clan/1/machine/backup" component={(props) => <></>} />
</MemoryRouter>
</div>
);
},
};
export default meta;
type Story = StoryObj<RouteSectionProps>;
export const Default: Story = {
args: {},
};

View File

@@ -0,0 +1,47 @@
import "./SidebarNav.css";
import { SidebarNavHeader } from "@/src/components/Sidebar/SidebarNavHeader";
import { SidebarNavBody } from "@/src/components/Sidebar/SidebarNavBody";
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
export interface LinkProps {
path: string;
label?: string;
}
export interface SectionProps {
label: string;
links: LinkProps[];
}
export interface MachineProps {
label: string;
path: string;
status: MachineStatus;
serviceCount: number;
}
export interface ClanLinkProps {
label: string;
path: string;
}
export interface ClanProps {
label: string;
settingsPath: string;
machines: MachineProps[];
}
export interface SidebarNavProps {
clanDetail: ClanProps;
clanLinks: ClanLinkProps[];
extraSections: SectionProps[];
}
export const SidebarNav = (props: SidebarNavProps) => {
return (
<div class="sidebar">
<SidebarNavHeader {...props} />
<SidebarNavBody {...props} />
</div>
);
};

View File

@@ -1,60 +1,46 @@
import "./SidebarBody.css";
import "./SidebarNavBody.css";
import { A } from "@solidjs/router";
import { Accordion } from "@kobalte/core/accordion";
import Icon from "../Icon/Icon";
import { Typography } from "@/src/components/Typography/Typography";
import {
MachineProps,
SidebarNavProps,
} from "@/src/components/Sidebar/SidebarNav";
import { For } from "solid-js";
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
import { useMachinesQuery } from "@/src/queries/queries";
import { SidebarProps } from "./Sidebar";
interface MachineProps {
clanURI: string;
machineID: string;
name: string;
status: MachineStatus;
serviceCount: number;
}
const MachineRoute = (props: MachineProps) => (
<A href={buildMachinePath(props.clanURI, props.machineID)}>
<div class="flex w-full flex-col gap-2">
<div class="flex flex-row items-center justify-between">
<Typography
hierarchy="label"
size="xs"
weight="bold"
color="primary"
inverted={true}
>
{props.name}
</Typography>
<MachineStatus status={props.status} />
</div>
<div class="flex w-full flex-row items-center gap-1">
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
<Typography
hierarchy="label"
family="mono"
size="s"
inverted={true}
color="primary"
>
{props.serviceCount}
</Typography>
</div>
<div class="flex w-full flex-col gap-2">
<div class="flex flex-row items-center justify-between">
<Typography
hierarchy="label"
size="xs"
weight="bold"
color="primary"
inverted={true}
>
{props.label}
</Typography>
<MachineStatus status={props.status} />
</div>
</A>
<div class="flex w-full flex-row items-center gap-1">
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
<Typography
hierarchy="label"
family="mono"
size="s"
inverted={true}
color="primary"
>
{props.serviceCount}
</Typography>
</div>
</div>
);
export const SidebarBody = (props: SidebarProps) => {
const clanURI = useClanURI();
const machineList = useMachinesQuery(clanURI);
const sectionLabels = (props.staticSections || []).map(
(section) => section.title,
);
export const SidebarNavBody = (props: SidebarNavProps) => {
const sectionLabels = props.extraSections.map((section) => section.label);
// controls which sections are open by default
// we want them all to be open by default
@@ -90,24 +76,20 @@ export const SidebarBody = (props: SidebarProps) => {
</Accordion.Header>
<Accordion.Content class="content">
<nav>
<For each={Object.entries(machineList.data || {})}>
{([id, machine]) => (
<MachineRoute
clanURI={clanURI}
machineID={id}
name={machine.name || id}
status="Not Installed"
serviceCount={0}
/>
<For each={props.clanDetail.machines}>
{(machine) => (
<A href={machine.path}>
<MachineRoute {...machine} />
</A>
)}
</For>
</nav>
</Accordion.Content>
</Accordion.Item>
<For each={props.staticSections}>
<For each={props.extraSections}>
{(section) => (
<Accordion.Item class="item" value={section.title}>
<Accordion.Item class="item" value={section.label}>
<Accordion.Header class="header">
<Accordion.Trigger class="trigger">
<Typography
@@ -118,7 +100,7 @@ export const SidebarBody = (props: SidebarProps) => {
inverted={true}
color="tertiary"
>
{section.title}
{section.label}
</Typography>
<Icon
icon="CaretDown"

View File

@@ -15,11 +15,10 @@ div.sidebar-header {
transition: all 250ms ease-in-out;
div.clan-label {
div.title {
@apply flex items-center gap-2 justify-start;
& > .clan-icon {
@apply flex justify-center items-center;
@apply rounded-full bg-inv-4 w-7 h-7;
}
}

View File

@@ -0,0 +1,96 @@
import "./SidebarNavHeader.css";
import Icon from "@/src/components/Icon/Icon";
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router";
import { Typography } from "../Typography/Typography";
import { createSignal, For } from "solid-js";
import { ClanLinkProps, ClanProps } from "@/src/components/Sidebar/SidebarNav";
export interface SidebarHeaderProps {
clanDetail: ClanProps;
clanLinks: ClanLinkProps[];
}
export const SidebarNavHeader = (props: SidebarHeaderProps) => {
const navigate = useNavigate();
const [open, setOpen] = createSignal(false);
const firstChar = props.clanDetail.label.charAt(0);
return (
<div class="sidebar-header">
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger">
<div class="title">
<div class="clan-icon">
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={true}
>
{firstChar.toUpperCase()}
</Typography>
</div>
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={!open()}
>
{props.clanDetail.label}
</Typography>
</div>
<DropdownMenu.Icon>
<Icon icon={"CaretDown"} inverted={!open()} size="0.75rem" />
</DropdownMenu.Icon>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content">
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigate(props.clanDetail.settingsPath)}
>
<Icon
icon="Settings"
size="0.75rem"
inverted={true}
color="tertiary"
/>
<Typography hierarchy="label" size="xs" weight="medium">
Settings
</Typography>
</DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group">
<DropdownMenu.GroupLabel class="dropdown-group-label">
<Typography
hierarchy="label"
family="mono"
size="xs"
color="tertiary"
>
YOUR CLANS
</Typography>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={props.clanLinks}>
{(clan) => (
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigate(clan.path)}
>
<Typography hierarchy="label" size="xs" weight="medium">
{clan.label}
</Typography>
</DropdownMenu.Item>
)}
</For>
</div>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
);
};

View File

@@ -1,5 +1,5 @@
div.sidebar-pane {
@apply w-full max-w-60 border-none;
@apply h-full w-auto max-w-60 border-none;
& > div.header {
@apply flex items-center justify-between px-3 py-2 rounded-t-[0.5rem];

View File

@@ -11,7 +11,7 @@ import { Checkbox } from "@/src/components/Form/Checkbox";
import { Combobox } from "../Form/Combobox";
const meta: Meta<SidebarPaneProps> = {
title: "Components/SidebarPane",
title: "Components/Sidebar/Pane",
component: SidebarPane,
};

View File

@@ -10,10 +10,6 @@ div.sidebar-section {
}
& > div.content {
@apply w-full h-full px-1.5 py-3 rounded-md bg-inv-4 opacity-60;
}
&.editing > div.content {
@apply opacity-100;
@apply w-full h-full px-1.5 py-3 rounded-md bg-inv-4;
}
}

View File

@@ -3,7 +3,6 @@ import "./SidebarSection.css";
import { Typography } from "@/src/components/Typography/Typography";
import { Button as KButton } from "@kobalte/core/button";
import Icon from "../Icon/Icon";
import cx from "classnames";
export interface SidebarSectionProps {
title: string;
@@ -21,7 +20,7 @@ export const SidebarSection = (props: SidebarSectionProps) => {
};
return (
<div class={cx("sidebar-section", { editing: editing() })}>
<div class="sidebar-section">
<div class="header">
<Typography
hierarchy="label"

View File

@@ -20,7 +20,6 @@ export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
interface SendHeaderType {
logging?: { group_path: string[] };
op_key?: string;
}
interface BackendSendType<K extends OperationNames> {
body: OperationArgs<K>;
@@ -65,14 +64,9 @@ export const callApi = <K extends OperationNames>(
};
}
const op_key = backendOpts?.op_key ?? crypto.randomUUID();
const req: BackendSendType<OperationNames> = {
body: args,
header: {
...backendOpts,
op_key,
},
header: backendOpts,
};
const result = (
@@ -84,6 +78,9 @@ export const callApi = <K extends OperationNames>(
>
)[method](req) as Promise<BackendReturnType<K>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const op_key = (result as any)._webviewMessageId as string;
return {
uuid: op_key,
result: result.then(({ body }) => body),

View File

@@ -1,9 +1,6 @@
import { callApi } from "@/src/hooks/api";
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
import { Params, Navigator, useParams } from "@solidjs/router";
export const encodeBase64 = (value: string) => window.btoa(value);
export const decodeBase64 = (value: string) => window.atob(value);
import { Params, Navigator } from "@solidjs/router";
export const selectClanFolder = async () => {
const req = callApi("get_clan_folder", {});
@@ -23,51 +20,10 @@ export const selectClanFolder = async () => {
throw new Error("Illegal state exception");
};
export const buildClanPath = (clanURI: string) => {
return "/clans/" + encodeBase64(clanURI);
};
export const buildMachinePath = (clanURI: string, machineID: string) => {
return (
"/clans/" + encodeBase64(clanURI) + "/machines/" + encodeBase64(machineID)
);
};
export const navigateToClan = (navigate: Navigator, clanURI: string) => {
const path = buildClanPath(clanURI);
console.log("Navigating to clan", clanURI, path);
navigate(path);
};
export const navigateToMachine = (
navigate: Navigator,
clanURI: string,
machineID: string,
) => {
const path = buildMachinePath(clanURI, machineID);
console.log("Navigating to machine", clanURI, machineID, path);
navigate(path);
export const navigateToClan = (navigate: Navigator, uri: string) => {
navigate("/clan/" + window.btoa(uri));
};
export const clanURIParam = (params: Params) => {
return decodeBase64(params.clanURI);
};
export const useClanURI = () => clanURIParam(useParams());
export const machineIDParam = (params: Params) => {
return decodeBase64(params.machineID);
};
export const useMachineID = (): string => {
const params = useParams();
return machineIDParam(params);
};
export const maybeUseMachineID = (): string | null => {
const params = useParams();
if (params.machineID === undefined) {
return null;
}
return machineIDParam(params);
return window.atob(params.clanURI);
};

View File

@@ -2,11 +2,10 @@
import { render } from "solid-js/web";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { QueryClient } from "@tanstack/solid-query";
import { Routes } from "@/src/routes";
import { Router } from "@solidjs/router";
import { Layout } from "@/src/routes/Layout";
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
export const client = new QueryClient();
@@ -19,14 +18,8 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
}
if (import.meta.env.DEV) {
console.log("Development mode");
// Load the debugger in development mode
await import("solid-devtools");
}
render(
() => (
<QueryClientProvider client={client}>
{import.meta.env.DEV && <SolidQueryDevtools initialIsOpen={true} />}
<Router root={Layout}>{Routes}</Router>
</QueryClientProvider>
),
root!,
);
render(() => <Router root={Layout}>{Routes}</Router>, root!);

View File

@@ -1,80 +0,0 @@
import { useQueries, useQuery, UseQueryResult } from "@tanstack/solid-query";
import { callApi, SuccessData } from "../hooks/api";
import { encodeBase64 } from "@/src/hooks/clan";
export type ClanDetails = SuccessData<"get_clan_details">;
export type ClanDetailsWithURI = ClanDetails & { uri: string };
export type ListMachines = SuccessData<"list_machines">;
export type MachinesQueryResult = UseQueryResult<ListMachines>;
export type ClanListQueryResult = UseQueryResult<ClanDetailsWithURI>[];
export const useMachinesQuery = (clanURI: string) =>
useQuery<ListMachines>(() => ({
queryKey: ["clans", encodeBase64(clanURI), "machines"],
queryFn: async () => {
const api = callApi("list_machines", {
flake: {
identifier: clanURI,
},
});
const result = await api.result;
if (result.status === "error") {
console.error("Error fetching machines:", result.errors);
return {};
}
return result.data;
},
}));
export const useClanDetailsQuery = (clanURI: string) =>
useQuery<ClanDetails>(() => ({
queryKey: ["clans", encodeBase64(clanURI), "details"],
queryFn: async () => {
const call = callApi("get_clan_details", {
flake: {
identifier: clanURI,
},
});
const result = await call.result;
if (result.status === "error") {
// todo should we create some specific error types?
console.error("Error fetching clan details:", result.errors);
throw new Error(result.errors[0].message);
}
return {
uri: clanURI,
...result.data,
};
},
}));
export const useClanListQuery = (clanURIs: string[]): ClanListQueryResult =>
useQueries(() => ({
queries: clanURIs.map((clanURI) => ({
queryKey: ["clans", encodeBase64(clanURI), "details"],
enabled: !!clanURI,
queryFn: async () => {
const call = callApi("get_clan_details", {
flake: {
identifier: clanURI,
},
});
const result = await call.result;
if (result.status === "error") {
// todo should we create some specific error types?
console.error("Error fetching clan details:", result.errors);
throw new Error(result.errors[0].message);
}
return {
uri: clanURI,
...result.data,
};
},
})),
}));

View File

@@ -1,24 +0,0 @@
.fade-out {
opacity: 0;
transition: opacity 0.5s ease;
}
.create-backdrop {
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50;
-webkit-backdrop-filter: blur(4px);
}
.create-modal {
@apply min-w-96;
}
.sidebar-container {
}
div.sidebar {
@apply absolute top-10 bottom-20 left-4 w-60;
}
div.sidebar-pane {
@apply absolute top-12 bottom-20 left-[16.5rem] w-64;
}

View File

@@ -1,309 +1,10 @@
import { RouteSectionProps } from "@solidjs/router";
import {
Component,
JSX,
Show,
createEffect,
createMemo,
createSignal,
on,
onMount,
} from "solid-js";
import {
buildMachinePath,
maybeUseMachineID,
useClanURI,
} from "@/src/hooks/clan";
import { RouteSectionProps, useParams } from "@solidjs/router";
import { Component } from "solid-js";
import { clanURIParam } from "@/src/hooks/clan";
import { CubeScene } from "@/src/scene/cubes";
import {
ClanListQueryResult,
MachinesQueryResult,
useClanListQuery,
useMachinesQuery,
} from "@/src/queries/queries";
import { callApi } from "@/src/hooks/api";
import { store, setStore, clanURIs } from "@/src/stores/clan";
import { produce } from "solid-js/store";
import { Button } from "@/src/components/Button/Button";
import { Splash } from "@/src/scene/splash";
import cx from "classnames";
import "./Clan.css";
import { Modal } from "@/src/components/Modal/Modal";
import { TextInput } from "@/src/components/Form/TextInput";
import { createForm, FieldValues, reset } from "@modular-forms/solid";
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
import { useNavigate } from "@solidjs/router";
export const Clan: Component<RouteSectionProps> = (props) => {
return (
<>
<Sidebar />
{props.children}
<ClanSceneController {...props} />
</>
);
};
interface CreateFormValues extends FieldValues {
name: string;
}
interface MockProps {
onClose: () => void;
onSubmit: (formValues: CreateFormValues) => void;
}
const MockCreateMachine = (props: MockProps) => {
let container: Node;
const [form, { Form, Field, FieldArray }] = createForm<CreateFormValues>();
return (
<div ref={(el) => (container = el)} class="create-backdrop">
<Modal
mount={container!}
onClose={() => {
reset(form);
props.onClose();
}}
class="create-modal"
title="Create Machine"
>
{() => (
<Form class="flex flex-col" onSubmit={props.onSubmit}>
<Field name="name">
{(field, props) => (
<>
<TextInput
{...field}
label="Name"
size="s"
required={true}
input={{ ...props, placeholder: "name" }}
/>
</>
)}
</Field>
<div class="mt-4 flex w-full items-center justify-end gap-4">
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
Cancel
</Button>
<Button
size="s"
type="submit"
hierarchy="primary"
onClick={close}
>
Create
</Button>
</div>
</Form>
)}
</Modal>
</div>
);
};
const ClanSceneController = (props: RouteSectionProps) => {
const clanURI = useClanURI();
const navigate = useNavigate();
const [dialogHandlers, setDialogHandlers] = createSignal<{
resolve: ({ id }: { id: string }) => void;
reject: (err: unknown) => void;
} | null>(null);
const onCreate = async (): Promise<{ id: string }> => {
return new Promise((resolve, reject) => {
setShowModal(true);
setDialogHandlers({ resolve, reject });
});
};
const sendCreate = async (values: CreateFormValues) => {
const api = callApi("create_machine", {
opts: {
clan_dir: {
identifier: clanURI,
},
machine: {
name: values.name,
},
},
});
const res = await api.result;
if (res.status === "error") {
// TODO: Handle displaying errors
console.error("Error creating machine:");
// Important: rejects the promise
throw new Error(res.errors[0].message);
}
return { id: values.name };
};
const [showModal, setShowModal] = createSignal(false);
const [loadingCooldown, setLoadingCooldown] = createSignal(false);
onMount(() => {
setTimeout(() => {
setLoadingCooldown(true);
}, 1500);
});
const [selectedIds, setSelectedIds] = createSignal<Set<string>>(new Set());
const onMachineSelect = (ids: Set<string>) => {
// Get the first selected ID and navigate to its machine details
const selected = ids.values().next().value;
if (selected) {
navigate(buildMachinePath(clanURI, selected));
}
};
const machine = createMemo(() => maybeUseMachineID());
createEffect(
on(machine, (machineId) => {
if (machineId) {
setSelectedIds(() => {
const res = new Set<string>();
res.add(machineId);
return res;
});
} else {
setSelectedIds(new Set<string>());
}
}),
);
return (
<SceneDataProvider clanURI={clanURI}>
{({ clansQuery, machinesQuery }) => {
// a combination of the individual clan details query status and the machines query status
// the cube scene needs the machines query, the sidebar needs the clans query and machines query results
// so we wait on both before removing the loader to avoid any loading artefacts
const isLoading = (): boolean => {
// check the machines query first
if (machinesQuery.isLoading) {
return true;
}
// otherwise iterate the clans query and return early if we find a queries that is still loading
for (const query of clansQuery) {
if (query.isLoading) {
return true;
}
}
return false;
};
return (
<>
<Show when={showModal()}>
<MockCreateMachine
onClose={() => {
setShowModal(false);
dialogHandlers()?.reject(new Error("User cancelled"));
}}
onSubmit={async (values) => {
try {
const result = await sendCreate(values);
dialogHandlers()?.resolve(result);
setShowModal(false);
} catch (err) {
dialogHandlers()?.reject(err);
setShowModal(false);
}
}}
/>
</Show>
<div
class="flex flex-row"
style={{ position: "absolute", top: "10px", left: "10px" }}
>
<Button
ghost
onClick={() => {
setStore(
produce((s) => {
for (const machineId in s.sceneData[clanURI]) {
// Reset the position of each machine to [0, 0]
s.sceneData[clanURI] = {};
}
}),
);
}}
>
Reset Store
</Button>
<Button
ghost
onClick={() => {
console.log("Refetching API");
machinesQuery.refetch();
}}
>
Refetch API
</Button>
</div>
{/* TODO: Add minimal display time */}
<div
class={cx({
"fade-out": !machinesQuery.isLoading && loadingCooldown(),
})}
>
<Splash />
</div>
<CubeScene
selectedIds={selectedIds}
onSelect={onMachineSelect}
isLoading={isLoading()}
cubesQuery={machinesQuery}
onCreate={onCreate}
sceneStore={() => {
const clanURI = useClanURI();
return store.sceneData?.[clanURI];
}}
setMachinePos={(machineId: string, pos: [number, number]) => {
console.log("calling setStore", machineId, pos);
setStore(
produce((s) => {
if (!s.sceneData) {
s.sceneData = {};
}
if (!s.sceneData[clanURI]) {
s.sceneData[clanURI] = {};
}
if (!s.sceneData[clanURI][machineId]) {
s.sceneData[clanURI][machineId] = { position: pos };
} else {
s.sceneData[clanURI][machineId].position = pos;
}
}),
);
}}
/>
</>
);
}}
</SceneDataProvider>
);
};
const SceneDataProvider = (props: {
clanURI: string;
children: (sceneData: {
clansQuery: ClanListQueryResult;
machinesQuery: MachinesQueryResult;
}) => JSX.Element;
}) => {
const clansQuery = useClanListQuery(clanURIs());
const machinesQuery = useMachinesQuery(props.clanURI);
// This component can be used to provide scene data or context if needed
return props.children({ clansQuery, machinesQuery });
const params = useParams();
const clanURI = clanURIParam(params);
return <CubeScene />;
};

View File

@@ -8,7 +8,7 @@ export const Layout: Component<RouteSectionProps> = (props) => {
// check for an active clan uri and redirect to it on first load
const activeURI = activeClanURI();
if (!props.location.pathname.startsWith("/clans/") && activeURI) {
if (!props.location.pathname.startsWith("/clan/") && activeURI) {
navigateToClan(navigate, activeURI);
} else {
navigate("/");

View File

@@ -1,19 +0,0 @@
import { RouteSectionProps, useNavigate } from "@solidjs/router";
import { SidebarPane } from "@/src/components/Sidebar/SidebarPane";
import { navigateToClan, useClanURI, useMachineID } from "@/src/hooks/clan";
export const Machine = (props: RouteSectionProps) => {
const navigate = useNavigate();
const clanURI = useClanURI();
const onClose = () => {
// go back to clan route
navigateToClan(navigate, clanURI);
};
return (
<SidebarPane title={useMachineID()} onClose={onClose}>
<h1>Hello world</h1>
</SidebarPane>
);
};

View File

@@ -1,663 +0,0 @@
div.creating {
@apply flex flex-col items-center justify-center;
div.scene {
width: 400px;
height: 400px;
perspective: 1000px;
/*background: red;*/
& > .frame {
position: relative;
top: 100px;
left: 65px;
width: 200px;
height: 200px;
/*background: green;*/
/*transform: rotate3d(-2, -2, 1, 45deg);*/
transform: rotate3d(-1.5, -2, 0.5, 45deg);
transform-style: preserve-3d;
& > .cube {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
transform-style: preserve-3d;
.cube-face {
position: absolute;
width: 100px;
height: 100px;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.56) 0%,
rgba(255, 255, 255, 0) 100%
);
border: 1px #10191a solid;
opacity: 1;
&.front {
transform: rotateY(0deg) translateZ(50px);
}
&.right {
transform: rotateY(90deg) translateZ(50px);
}
&.back {
transform: rotateY(180deg) translateZ(50px);
}
&.left {
transform: rotateY(-90deg) translateZ(50px);
}
&.top {
transform: rotateX(90deg) translateZ(50px);
}
&.bottom {
transform: rotateX(-90deg) translateZ(50px);
}
}
&.state-1 {
animation: anim-cube-1-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-1-1 {
transform: translateZ(-120px);
animation: anim-cube-1-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-2 {
left: 120px;
animation: anim-cube-2-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-2-2 {
left: 120px;
transform: translateZ(-120px);
animation: anim-cube-2-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-3 {
top: 120px;
animation: anim-cube-3-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-3-3 {
top: 120px;
transform: translateZ(-120px);
animation: anim-cube-3-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-4 {
top: 120px;
left: 120px;
animation: anim-cube-4-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
&.state-4-4 {
top: 120px;
left: 120px;
transform: translateZ(-120px);
animation: anim-cube-4-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
}
}
}
}
@keyframes anim-cube-1-1 {
/* STEP 1 */
0% {
left: 0px;
transform: translateZ(0px);
}
2.083% {
left: -40px;
transform: translateZ(0px);
}
16.666% {
left: -40px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
left: 0;
transform: translateZ(0px);
}
33.332% {
left: 0;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
left: 0;
transform: translateZ(40px);
}
49.998% {
left: 0;
transform: translateZ(40px);
}
/* STEP 4 */
52.081% {
left: 0;
transform: translateZ(0);
}
66.664% {
left: 0;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
left: -60px;
transform: translateZ(60px);
}
83.33% {
left: -60px;
transform: translateZ(60px);
}
/* Step 6 */
85.413% {
left: 0px;
transform: translateZ(0px);
}
100% {
left: 0px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-2-1 {
/* STEP 1 */
0% {
left: 120px;
transform: translateZ(0px);
}
2.083% {
left: 180px;
transform: translateZ(0px);
}
16.666% {
left: 180px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
left: 120px;
transform: translateZ(0px);
}
33.332% {
left: 120px;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
left: 240px;
transform: translateZ(120px);
}
49.998% {
left: 240px;
transform: translateZ(120px);
}
/* STEP 4 */
52.081% {
left: 120px;
transform: translateZ(0);
}
66.664% {
left: 120px;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
left: 60px;
transform: translateZ(60px);
}
83.33% {
left: 60px;
transform: translateZ(60px);
}
/* Step 6 */
85.413% {
left: 120px;
transform: translateZ(0px);
}
100% {
left: 120px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-3-1 {
/* STEP 1 */
0% {
top: 120px;
transform: translateZ(0px);
}
2.083% {
top: 220px;
transform: translateZ(0px);
}
16.666% {
top: 220px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
top: 120px;
transform: translateZ(0px);
}
33.332% {
top: 120px;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
top: 120px;
transform: translateZ(40px);
}
49.998% {
top: 120px;
transform: translateZ(40px);
}
/* STEP 4 */
52.081% {
top: 120px;
transform: translateZ(0);
}
66.664% {
top: 120px;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
top: 180px;
transform: translateZ(80px);
}
83.33% {
top: 180px;
transform: translateZ(80px);
}
/* Step 6 */
85.413% {
top: 120px;
transform: translateZ(0px);
}
100% {
top: 120px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-4-1 {
/* STEP 1 */
0% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
2.083% {
top: 220px;
left: 180px;
transform: translateZ(0px);
}
16.666% {
top: 220px;
left: 180px;
transform: translateZ(0px);
}
/* STEP 2 */
18.749% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
33.332% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
/* STEP 3 */
35.415% {
top: 120px;
left: 240px;
transform: translateZ(120px);
}
49.998% {
top: 120px;
left: 240px;
transform: translateZ(120px);
}
/* STEP 4 */
52.081% {
top: 120px;
left: 120px;
transform: translateZ(0);
}
66.664% {
top: 120px;
left: 120px;
transform: translateZ(0);
}
/* Step 5 */
68.747% {
top: 180px;
left: 260px;
transform: translateZ(80px);
}
83.33% {
top: 180px;
left: 260px;
transform: translateZ(80px);
}
/* Step 6 */
85.413% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
100% {
top: 120px;
left: 120px;
transform: translateZ(0px);
}
}
@keyframes anim-cube-1-2 {
/* STEP 1 */
0% {
left: 0px;
transform: translateZ(-120px);
}
2.083% {
left: -40px;
transform: translateZ(-120px);
}
16.666% {
left: -40px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
left: 0px;
transform: translateZ(-120px);
}
33.332% {
left: 0px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
left: 0px;
transform: translateZ(-200px);
}
49.998% {
left: 0px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
left: 0px;
transform: translateZ(-120px);
}
66.664% {
left: 0px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
left: -60px;
transform: translateZ(-180px);
}
83.33% {
left: -60px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
left: 0px;
transform: translateZ(-120px);
}
100% {
left: 0px;
transform: translateZ(-120px);
}
}
@keyframes anim-cube-2-2 {
/* STEP 1 */
0% {
left: 120px;
transform: translateZ(-120px);
}
2.083% {
left: 180px;
transform: translateZ(-120px);
}
16.666% {
left: 180px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
left: 120px;
transform: translateZ(-120px);
}
33.332% {
left: 120px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
left: 240px;
transform: translateZ(-200px);
}
49.998% {
left: 240px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
left: 120px;
transform: translateZ(-120px);
}
66.664% {
left: 120px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
left: 60px;
transform: translateZ(-180px);
}
83.33% {
left: 60px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
left: 120px;
transform: translateZ(-120px);
}
100% {
left: 120px;
transform: translateZ(-120px);
}
}
@keyframes anim-cube-3-2 {
/* STEP 1 */
0% {
top: 120px;
transform: translateZ(-120px);
}
2.083% {
top: 220px;
transform: translateZ(-120px);
}
16.666% {
top: 220px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
top: 120px;
transform: translateZ(-120px);
}
33.332% {
top: 120px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
top: 120px;
transform: translateZ(-200px);
}
49.998% {
top: 120px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
top: 120px;
transform: translateZ(-120px);
}
66.664% {
top: 120px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
top: 180px;
transform: translateZ(-180px);
}
83.33% {
top: 180px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
top: 120px;
transform: translateZ(-120px);
}
100% {
top: 120px;
transform: translateZ(-120px);
}
}
@keyframes anim-cube-4-2 {
/* STEP 1 */
0% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
2.083% {
top: 220px;
left: 180px;
transform: translateZ(-120px);
}
16.666% {
top: 220px;
left: 180px;
transform: translateZ(-120px);
}
/* STEP 2 */
18.749% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
33.332% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
/* STEP 3 */
35.415% {
top: 120px;
left: 240px;
transform: translateZ(-200px);
}
49.998% {
top: 120px;
left: 240px;
transform: translateZ(-200px);
}
/* STEP 4 */
52.081% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
66.664% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
/* Step 5 */
68.747% {
top: 180px;
left: 260px;
transform: translateZ(-180px);
}
83.33% {
top: 180px;
left: 260px;
transform: translateZ(-180px);
}
/* Step 6 */
85.413% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
100% {
top: 120px;
left: 120px;
transform: translateZ(-120px);
}
}

View File

@@ -1,90 +0,0 @@
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
import { Typography } from "@/src/components/Typography/Typography";
import "./Creating.css";
export const Creating = () => (
<div class="creating">
<Tooltip open={true} placement="top" trigger={<div />}>
<Typography hierarchy="body" size="xs" weight="medium" inverted={true}>
Your Clan is being created
</Typography>
</Tooltip>
<div class="scene">
<div class="frame">
<div id="cube-1" class="cube state-1">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-2" class="cube state-2">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-3" class="cube state-3">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-4" class="cube state-4">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-1-1" class="cube state-1-1">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-2-2" class="cube state-2-2">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-3-3" class="cube state-3-3">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
<div id="cube-4-4" class="cube state-4-4">
<div class="cube-face front"></div>
<div class="cube-face left"></div>
<div class="cube-face right"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
<div class="cube-face back"></div>
</div>
</div>
</div>
</div>
);

View File

@@ -54,10 +54,11 @@ main#welcome {
}
& > div.container {
@apply flex flex-col items-center justify-evenly gap-y-20 size-fit;
@apply flex flex-col items-center justify-evenly gap-y-20;
@apply size-fit;
& > div.welcome {
@apply flex flex-col w-80 gap-y-6;
@apply flex flex-col min-w-80 gap-y-6;
& > div.separator {
@apply grid grid-cols-3 grid-rows-1 gap-x-4 items-center;
@@ -65,7 +66,7 @@ main#welcome {
}
& > div.setup {
@apply flex flex-col w-[33rem] gap-y-5;
@apply flex flex-col min-w-[520px] gap-y-5;
@apply pt-10 px-8 pb-8 bg-def-1 rounded-lg;
& > div.header {
@@ -80,5 +81,10 @@ main#welcome {
}
}
}
div.creating {
@apply w-[17.0625rem] h-[20.4375rem];
background: url(./cube.svg) center / cover no-repeat;
}
}
}

View File

@@ -11,6 +11,7 @@ import { RouteSectionProps, useNavigate } from "@solidjs/router";
import "./Onboarding.css";
import { Typography } from "@/src/components/Typography/Typography";
import { Button } from "@/src/components/Button/Button";
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
import { Alert } from "@/src/components/Alert/Alert";
import { Divider } from "@/src/components/Divider/Divider";
@@ -32,7 +33,6 @@ import { Fieldset } from "@/src/components/Form/Fieldset";
import * as v from "valibot";
import { HostFileInput } from "@/src/components/Form/HostFileInput";
import { callApi } from "@/src/hooks/api";
import { Creating } from "./Creating";
type State = "welcome" | "setup" | "creating";
@@ -154,6 +154,21 @@ const welcome = (props: {
);
};
const creating = () => (
<div class="animate-pulse">
<Tooltip
open={true}
placement="top"
animation="bounce"
trigger={<div class="creating" />}
>
<Typography hierarchy="body" size="xs" weight="medium" inverted={true}>
Your Clan is being created
</Typography>
</Tooltip>
</div>
);
export const Onboarding: Component<RouteSectionProps> = (props) => {
const navigate = useNavigate();
@@ -220,8 +235,16 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
// todo allow users to select a template
template: "minimal",
initial: {
name,
description,
meta: {
name: name,
description: description,
// todo it tries to 'delete' icon if it's not provided
// this logic is unexpected, and needs reviewed.
icon: null,
},
machines: {},
instances: {},
services: {},
},
},
});
@@ -349,9 +372,7 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
</div>
</Match>
<Match when={state() === "creating"}>
<Creating />
</Match>
<Match when={state() === "creating"}>{creating()}</Match>
</Switch>
</div>
</main>

View File

@@ -0,0 +1,316 @@
<svg width="273" height="327" viewBox="0 0 273 327" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 137.399 89.7148)" fill="url(#paint0_linear_4542_12187)" stroke="url(#paint1_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.8281 125.194)" fill="url(#paint2_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.8281 125.194)" fill="url(#paint3_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.8281 125.194)" stroke="url(#paint4_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.326 160.673)" fill="url(#paint5_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.326 160.673)" fill="url(#paint6_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.326 160.673)" stroke="url(#paint7_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 62.5708 142.85)" fill="url(#paint8_linear_4542_12187)" stroke="url(#paint9_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 178.329)" fill="url(#paint10_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 178.329)" fill="url(#paint11_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 178.329)" stroke="url(#paint12_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 213.808)" fill="url(#paint13_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 213.808)" fill="url(#paint14_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 213.808)" stroke="url(#paint15_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499715 -0.86619 0.499715 210.502 137.882)" fill="url(#paint16_linear_4542_12187)" stroke="url(#paint17_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 173.361)" fill="url(#paint18_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 173.361)" fill="url(#paint19_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 173.361)" stroke="url(#paint20_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 208.84)" fill="url(#paint21_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 208.84)" fill="url(#paint22_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 208.84)" stroke="url(#paint23_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 137.158 185.084)" fill="url(#paint24_linear_4542_12187)" stroke="url(#paint25_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 220.563)" fill="url(#paint26_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 220.563)" fill="url(#paint27_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 220.563)" stroke="url(#paint28_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 256.042)" fill="url(#paint29_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 256.042)" fill="url(#paint30_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 256.042)" stroke="url(#paint31_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 135.915 0)" fill="url(#paint32_linear_4542_12187)" stroke="url(#paint33_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 74.3442 35.479)" fill="url(#paint34_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 74.3442 35.479)" fill="url(#paint35_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 74.3442 35.479)" stroke="url(#paint36_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 135.842 70.9579)" fill="url(#paint37_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 135.842 70.9579)" fill="url(#paint38_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 135.842 70.9579)" stroke="url(#paint39_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 62.5708 47.2027)" fill="url(#paint40_linear_4542_12187)" stroke="url(#paint41_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 82.6817)" fill="url(#paint42_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 82.6817)" fill="url(#paint43_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 82.6817)" stroke="url(#paint44_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 118.161)" fill="url(#paint45_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 118.161)" fill="url(#paint46_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 118.161)" stroke="url(#paint47_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499715 -0.86619 0.499715 210.502 42.234)" fill="url(#paint48_linear_4542_12187)" stroke="url(#paint49_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 77.713)" fill="url(#paint50_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 77.713)" fill="url(#paint51_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 77.713)" stroke="url(#paint52_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 113.192)" fill="url(#paint53_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 113.192)" fill="url(#paint54_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 113.192)" stroke="url(#paint55_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 137.158 89.4367)" fill="url(#paint56_linear_4542_12187)" stroke="url(#paint57_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 124.916)" fill="url(#paint58_linear_4542_12187)" fill-opacity="0.1"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 124.916)" fill="url(#paint59_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 124.916)" stroke="url(#paint60_linear_4542_12187)" stroke-width="0.64"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 160.395)" fill="url(#paint61_linear_4542_12187)"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 160.395)" fill="url(#paint62_linear_4542_12187)" fill-opacity="0.24"/>
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 160.395)" stroke="url(#paint63_linear_4542_12187)" stroke-width="0.64"/>
<defs>
<linearGradient id="paint0_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.94"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint2_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint4_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint5_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.81"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint6_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint7_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint8_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.94"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint9_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint10_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint11_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint12_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint13_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.81"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint14_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint15_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint16_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.94"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint17_linear_4542_12187" x1="73.129" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint18_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint19_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42737" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint20_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint21_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.81"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint22_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint23_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint24_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.94"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint25_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint26_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint27_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint28_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint29_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.81"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint30_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1743" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint31_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint32_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.94"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint33_linear_4542_12187" x1="73.129" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint34_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint35_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint36_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint37_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.81"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint38_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint39_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint40_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.94"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint41_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint42_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint43_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint44_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint45_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.81"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint46_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint47_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint48_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.94"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint49_linear_4542_12187" x1="73.129" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint50_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint51_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint52_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint53_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.81"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint54_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint55_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint56_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.94"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint57_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint58_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint59_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint60_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
<linearGradient id="paint61_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.81"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint62_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
<stop stop-color="#40A2A6"/>
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint63_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
<stop stop-color="#10191A"/>
<stop offset="1" stop-color="#2C4547"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,7 +1,6 @@
import type { RouteDefinition } from "@solidjs/router/dist/types";
import { Onboarding } from "@/src/routes/Onboarding/Onboarding";
import { Clan } from "@/src/routes/Clan/Clan";
import { Machine } from "@/src/routes/Machine/Machine";
export const Routes: RouteDefinition[] = [
{
@@ -9,30 +8,7 @@ export const Routes: RouteDefinition[] = [
component: Onboarding,
},
{
path: "/clans",
children: [
{
path: "/",
component: () => (
<h1>
Clans (index) - (Doesnt really exist, just to keep the scene
mounted)
</h1>
),
},
{
path: "/:clanURI",
component: Clan,
children: [
{
path: "/",
},
{
path: "/machines/:machineID",
component: Machine,
},
],
},
],
path: "/clan/:clanURI",
component: Clan,
},
];

View File

@@ -5,6 +5,11 @@
}
.toolbar-container {
@apply absolute bottom-8 z-10 w-full;
@apply flex justify-center items-center;
position: absolute;
bottom: 10%;
width: 100%;
z-index: 10;
display: flex;
justify-content: center;
align-items: center;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +0,0 @@
#splash {
position: fixed;
inset: 0;
background: linear-gradient(to top, #e3e7e7, #edf1f1);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
pointer-events: none;
}
#splash .content {
display: flex;
flex-direction: column;
align-items: center;
}
.title {
@apply h-8 mb-8;
}
.loader {
@apply h-3 w-60 mb-3;
width: 18rem;
background: repeating-linear-gradient(
-45deg,
#bfd0d2 0px,
#bfd0d2 10px,
#f7f9fa 10px,
#f7f9fa 20px
);
animation: stripe-move 1s linear infinite;
background-size: 28px 28px; /* Sqrt(20^2 + 20^2) ~= 28 */
@apply border-2 border-solid rounded-[3px] border-bg-def-1;
}
@keyframes stripe-move {
0% {
background-position: 0 0;
}
100% {
background-position: 28px 0;
}
}

View File

@@ -1,15 +0,0 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Splash } from "./splash";
const meta: Meta = {
title: "scene/splash",
component: Splash,
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
args: {},
};

View File

@@ -1,18 +0,0 @@
import Logo from "@/logos/darknet-builder-logo.svg";
import "./splash.css";
import { Typography } from "../components/Typography/Typography";
export const Splash = () => (
<div id="splash">
<div class="content">
<span class="title">
<Logo />
</span>
<div class="loader"></div>
<Typography hierarchy="label" size="xs" weight="medium">
Loading new Clan
</Typography>
</div>
</div>
);

View File

@@ -1,18 +1,14 @@
import { createStore, produce } from "solid-js/store";
import { makePersisted } from "@solid-primitives/storage";
export type SceneData = Record<string, { position: [number, number] }>;
export interface ClanStoreType {
interface ClanStoreType {
clanURIs: string[];
activeClanURI?: string;
sceneData: Record<string, SceneData>;
}
const [store, setStore] = makePersisted(
createStore<ClanStoreType>({
clanURIs: [],
sceneData: {},
}),
{
name: "clanStore",
@@ -20,21 +16,13 @@ const [store, setStore] = makePersisted(
},
);
const resetStore = () => {
setStore({
clanURIs: [],
activeClanURI: undefined,
sceneData: {},
});
};
/**
* Retrieves the active clan URI from the store.
*
* @function
* @returns {string} The URI of the active clan.
*/
const activeClanURI = () => store.activeClanURI;
const activeClanURI = (): string | undefined => store.activeClanURI;
/**
* Updates the active Clan URI in the store.
@@ -57,10 +45,8 @@ const clanURIs = (): string[] => store.clanURIs;
* @param {string} uri - The URI of the clan to be added.
*
*/
const addClanURI = (uri: string) => {
const addClanURI = (uri: string) =>
setStore("clanURIs", store.clanURIs.length, uri);
setStore("sceneData", uri, {}); // Initialize empty scene data for every new clan URI
};
/**
* Removes a specified URI from the clan URI list and updates the active clan URI.
@@ -94,11 +80,9 @@ const removeClanURI = (uri: string) => {
export {
store,
setStore,
activeClanURI,
setActiveClanURI,
clanURIs,
addClanURI,
removeClanURI,
resetStore,
};

View File

@@ -1,6 +1,7 @@
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";
import solidSvg from "vite-plugin-solid-svg";
import devtools from "solid-devtools/vite";
import path from "node:path";
import { exec } from "child_process";
@@ -37,6 +38,7 @@ export default defineConfig({
Uncomment the following line to enable solid-devtools.
For more info see https://github.com/thetarnav/solid-devtools/tree/main/packages/extension#readme
*/
devtools(),
solidPlugin(),
solidSvg(),
regenPythonApiOnFileChange(),

View File

@@ -1,18 +1,6 @@
{
gtk4,
webkitgtk_6_0,
lib,
clangStdenv,
fetchFromGitea,
gnumake,
cmake,
clang-tools,
pkg-config,
stdenv,
...
}:
{ pkgs, ... }:
clangStdenv.mkDerivation {
pkgs.clangStdenv.mkDerivation {
pname = "webview";
version = "nightly";
@@ -20,7 +8,7 @@ clangStdenv.mkDerivation {
# We disallow remote connections from the UI on Linux
# TODO: Disallow remote connections on MacOS
src = fetchFromGitea {
src = pkgs.fetchFromGitea {
domain = "git.clan.lol";
owner = "clan";
repo = "webview";
@@ -49,19 +37,23 @@ clangStdenv.mkDerivation {
];
# Dependencies used during the build process, if any
nativeBuildInputs = [
nativeBuildInputs = with pkgs; [
gnumake
cmake
clang-tools
pkg-config
];
buildInputs = lib.optionals stdenv.isLinux [
webkitgtk_6_0
gtk4
];
buildInputs =
with pkgs;
[
]
++ pkgs.lib.optionals stdenv.isLinux [
webkitgtk_6_0
gtk4
];
meta = with lib; {
meta = with pkgs.lib; {
description = "Tiny cross-platform webview library for C/C++. Uses WebKit (GTK/Cocoa) and Edge WebView2 (Windows)";
homepage = "https://github.com/webview/webview";
license = licenses.mit;

View File

@@ -6,14 +6,11 @@ from pathlib import Path
from clan_lib.clan.create import CreateOptions, create_clan
from clan_lib.errors import ClanError
from clan_cli.completions import add_dynamic_completer, complete_templates_clan
from clan_cli.vars.keygen import create_secrets_user_auto
log = logging.getLogger(__name__)
def register_create_parser(parser: argparse.ArgumentParser) -> None:
template_action = parser.add_argument(
parser.add_argument(
"--template",
type=str,
help="""Reference to the template to use for the clan. default="default". In the format '<flake_ref>#template_name' Where <flake_ref> is a flake reference (e.g. github:org/repo) or a local path (e.g. '.' ).
@@ -21,7 +18,6 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
""",
default="default",
)
add_dynamic_completer(template_action, complete_templates_clan)
parser.add_argument(
"--no-git",
@@ -44,12 +40,6 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
default=False,
)
parser.add_argument(
"--user",
help="The user to generate the keys for. Default: logged-in OS username (e.g. from $LOGNAME or system)",
default=None,
)
def create_flake_command(args: argparse.Namespace) -> None:
# Ask for a path interactively if none provided
if args.name is None:
@@ -69,10 +59,5 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
update_clan=not args.no_update,
)
)
create_secrets_user_auto(
flake_dir=Path(args.name).resolve(),
user=args.user,
force=True,
)
parser.set_defaults(func=create_flake_command)

View File

@@ -25,7 +25,6 @@ from .facts import cli as facts
from .flash import cli as flash_cli
from .hyperlink import help_hyperlink
from .machines import cli as machines
from .network import cli as network_cli
from .profiler import profile
from .ssh import deploy_info as ssh_cli
from .vars import cli as vars_cli
@@ -285,7 +284,7 @@ Examples:
$ clan secrets get [SECRET]
Will display the content of the specified secret.
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
@@ -324,7 +323,7 @@ Examples:
This is especially useful for resetting certain passwords while leaving the rest
of the facts for a machine in place.
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
@@ -362,7 +361,7 @@ Examples:
This is especially useful for resetting certain passwords while leaving the rest
of the vars for a machine in place.
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
@@ -429,26 +428,6 @@ Examples:
)
select.register_parser(parser_select)
parser_network = subparsers.add_parser(
"network",
aliases=["net"],
# TODO: Add help="Manage networks" when network code is ready
# help="Manage networks",
description="Manage networks",
epilog=(
"""
show information about configured networks
Examples:
$ clan network list
Will list networks
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
network_cli.register_parser(parser_network)
parser_state = subparsers.add_parser(
"state",
aliases=["st"],
@@ -482,10 +461,10 @@ For more detailed information, visit: {help_hyperlink("getting-started", "https:
)
state.register_parser(parser_state)
register_common_flags(parser)
if argcomplete:
argcomplete.autocomplete(parser, exclude=["morph", "network", "net"])
argcomplete.autocomplete(parser, exclude=["morph"])
register_common_flags(parser)
return parser

View File

@@ -29,8 +29,6 @@ COMPLETION_TIMEOUT: int = 3
def clan_dir(flake: str | None) -> str | None:
if flake is not None:
return flake
from clan_lib.dirs import get_clan_flake_toplevel_or_env
path_result = get_clan_flake_toplevel_or_env()
@@ -47,9 +45,7 @@ def complete_machines(
def run_cmd() -> None:
try:
if (
clan_dir_result := clan_dir(getattr(parsed_args, "flake", None))
) is not None:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
@@ -83,9 +79,7 @@ def complete_services_for_machine(
def run_cmd() -> None:
try:
if (
clan_dir_result := clan_dir(getattr(parsed_args, "flake", None))
) is not None:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
@@ -127,9 +121,7 @@ def complete_backup_providers_for_machine(
def run_cmd() -> None:
try:
if (
clan_dir_result := clan_dir(getattr(parsed_args, "flake", None))
) is not None:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
@@ -171,9 +163,7 @@ def complete_state_services_for_machine(
def run_cmd() -> None:
try:
if (
clan_dir_result := clan_dir(getattr(parsed_args, "flake", None))
) is not None:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
@@ -214,12 +204,7 @@ def complete_secrets(
from .secrets.secrets import list_secrets
flake = (
clan_dir_result
if (clan_dir_result := clan_dir(getattr(parsed_args, "flake", None)))
is not None
else "."
)
flake = clan_dir_result if (clan_dir_result := clan_dir(None)) is not None else "."
secrets = list_secrets(Flake(flake).path)
@@ -237,12 +222,7 @@ def complete_users(
from .secrets.users import list_users
flake = (
clan_dir_result
if (clan_dir_result := clan_dir(getattr(parsed_args, "flake", None)))
is not None
else "."
)
flake = clan_dir_result if (clan_dir_result := clan_dir(None)) is not None else "."
users = list_users(Path(flake))
@@ -260,12 +240,7 @@ def complete_groups(
from .secrets.groups import list_groups
flake = (
clan_dir_result
if (clan_dir_result := clan_dir(getattr(parsed_args, "flake", None)))
is not None
else "."
)
flake = clan_dir_result if (clan_dir_result := clan_dir(None)) is not None else "."
groups_list = list_groups(Path(flake))
groups = [group.name for group in groups_list]
@@ -283,12 +258,7 @@ def complete_templates_disko(
from clan_lib.templates import list_templates
flake = (
clan_dir_result
if (clan_dir_result := clan_dir(getattr(parsed_args, "flake", None)))
is not None
else "."
)
flake = clan_dir_result if (clan_dir_result := clan_dir(None)) is not None else "."
list_all_templates = list_templates(Flake(flake))
disko_template_list = list_all_templates.builtins.get("disko")
@@ -299,74 +269,6 @@ def complete_templates_disko(
return []
def complete_templates_clan(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for clan templates
"""
from clan_lib.templates import list_templates
flake = (
clan_dir_result
if (clan_dir_result := clan_dir(getattr(parsed_args, "flake", None)))
is not None
else "."
)
list_all_templates = list_templates(Flake(flake))
clan_template_list = list_all_templates.builtins.get("clan")
if clan_template_list:
clan_templates = list(clan_template_list)
clan_dict = dict.fromkeys(clan_templates, "clan")
return clan_dict
return []
def complete_vars_for_machine(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for variable names for a specific machine.
Only completes vars that already exist in the vars directory on disk.
This is fast as it only scans the filesystem without any evaluation.
"""
from pathlib import Path
machine_name = getattr(parsed_args, "machine", None)
if not machine_name:
return []
if (clan_dir_result := clan_dir(getattr(parsed_args, "flake", None))) is not None:
flake_path = Path(clan_dir_result)
else:
flake_path = Path()
vars_dir = flake_path / "vars" / "per-machine" / machine_name
vars_list: list[str] = []
if vars_dir.exists() and vars_dir.is_dir():
try:
for generator_dir in vars_dir.iterdir():
if not generator_dir.is_dir():
continue
generator_name = generator_dir.name
for var_dir in generator_dir.iterdir():
if var_dir.is_dir():
var_name = var_dir.name
var_id = f"{generator_name}/{var_name}"
vars_list.append(var_id)
except Exception:
pass
vars_dict = dict.fromkeys(vars_list, "var")
return vars_dict
def complete_target_host(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
@@ -378,9 +280,7 @@ def complete_target_host(
def run_cmd() -> None:
try:
if (
clan_dir_result := clan_dir(getattr(parsed_args, "flake", None))
) is not None:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
@@ -420,9 +320,7 @@ def complete_tags(
def run_computed_tags_cmd() -> None:
try:
if (
clan_dir_result := clan_dir(getattr(parsed_args, "flake", None))
) is not None:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
@@ -438,9 +336,7 @@ def complete_tags(
def run_machines_tags_cmd() -> None:
machine_tags: list[str] = []
try:
if (
clan_dir_result := clan_dir(getattr(parsed_args, "flake", None))
) is not None:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."

Some files were not shown because too many files have changed in this diff Show More