Compare commits
174 Commits
nim65s-mul
...
ui/reduce-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30edcacce5 | ||
|
|
5d431094bb | ||
|
|
694059d3ce | ||
|
|
2299feb809 | ||
|
|
59105bd1da | ||
|
|
9018ffce7a | ||
|
|
94662b722d | ||
|
|
0ffad32657 | ||
|
|
50803c2e25 | ||
|
|
ebdd3e8413 | ||
|
|
ffe58fc189 | ||
|
|
7065464227 | ||
|
|
5f567e2473 | ||
|
|
46ffcdf182 | ||
|
|
9afeec5683 | ||
|
|
329047e865 | ||
|
|
5c7e6b3830 | ||
|
|
1e51439414 | ||
|
|
a472f7f696 | ||
|
|
29c764773f | ||
|
|
af056f2355 | ||
|
|
6803f3c6f5 | ||
|
|
6b9ce0da66 | ||
|
|
38d62af1ba | ||
|
|
c880ab7cc1 | ||
|
|
613a1fb553 | ||
|
|
14f255c2d5 | ||
|
|
eaa5a9a204 | ||
|
|
34ccbcc13d | ||
|
|
f58a120db1 | ||
|
|
5b59cfbc34 | ||
|
|
cc69892e3b | ||
|
|
c94330ee9c | ||
|
|
377056e80c | ||
|
|
1dbaff7b61 | ||
|
|
bf416f1b5f | ||
|
|
d83bcf638f | ||
|
|
acfe3b0a04 | ||
|
|
04f36a4cb1 | ||
|
|
41a0138c16 | ||
|
|
f1be729206 | ||
|
|
cacd853374 | ||
|
|
07caa6890f | ||
|
|
9706285474 | ||
|
|
1510b4014b | ||
|
|
d5e0f7e505 | ||
|
|
b9e5cf1220 | ||
|
|
f4eb59c373 | ||
|
|
09b92084c8 | ||
|
|
06257d044a | ||
|
|
34ca7a4a7b | ||
|
|
ce70be5ca3 | ||
|
|
dd3051d62b | ||
|
|
5f290fed7f | ||
|
|
a34ec8ed22 | ||
|
|
4597b207e7 | ||
|
|
9257cb02ee | ||
|
|
cd8a1d9a32 | ||
|
|
ee9ae21bd2 | ||
|
|
bd1451ce18 | ||
|
|
a94cc4b7f7 | ||
|
|
cf2ccd7e14 | ||
|
|
69ab00b34b | ||
|
|
0043870882 | ||
|
|
0ea42ae541 | ||
|
|
ad50cfbcbb | ||
|
|
cf65ae81cf | ||
|
|
19ca7d9a77 | ||
|
|
0b2ee45526 | ||
|
|
28e39ada84 | ||
|
|
fb52b955cc | ||
|
|
77f75b916d | ||
|
|
97022ba873 | ||
|
|
aee71b3fd6 | ||
|
|
76535852e4 | ||
|
|
a694e8d122 | ||
|
|
93fee8263f | ||
|
|
28859641eb | ||
|
|
3a2be243c0 | ||
|
|
9fdf41813a | ||
|
|
04f3a9480f | ||
|
|
f7762b3119 | ||
|
|
634e4116cf | ||
|
|
015c09b0e5 | ||
|
|
6e0a43c777 | ||
|
|
7fc527b649 | ||
|
|
2f0ba0782a | ||
|
|
bc3b6c792f | ||
|
|
b5a3d617fd | ||
|
|
579492f071 | ||
|
|
0ed02da28f | ||
|
|
4abfbb05a2 | ||
|
|
6126cccbcc | ||
|
|
9e77d16e6d | ||
|
|
53752d4a69 | ||
|
|
38955f763f | ||
|
|
bd97896899 | ||
|
|
d6efeb3295 | ||
|
|
e3247d9c36 | ||
|
|
4055508588 | ||
|
|
ff65dfc883 | ||
|
|
1f5ef04a61 | ||
|
|
89f0e90910 | ||
|
|
137aa71529 | ||
|
|
4b5273fbc1 | ||
|
|
aed48be645 | ||
|
|
5fdc9823d1 | ||
|
|
f6284a7ac2 | ||
|
|
72473746ff | ||
|
|
4b36b3e07c | ||
|
|
5a63eeed4e | ||
|
|
ac96d67f09 | ||
|
|
d01342aa79 | ||
|
|
2d404254da | ||
|
|
71b69c1010 | ||
|
|
f155c68efe | ||
|
|
e57741b60c | ||
|
|
c9cacfcf62 | ||
|
|
2d937b80b1 | ||
|
|
e8b91e63bc | ||
|
|
a9d6fa7712 | ||
|
|
65a23983c2 | ||
|
|
c181400267 | ||
|
|
e8ff0d1ad4 | ||
|
|
f9f8a947e2 | ||
|
|
c5b0154af7 | ||
|
|
864742f05f | ||
|
|
38b043f625 | ||
|
|
174e66ef95 | ||
|
|
315049de20 | ||
|
|
2e577dbd1e | ||
|
|
a9b457e063 | ||
|
|
4281770ec7 | ||
|
|
1bd950fa39 | ||
|
|
e37b61240b | ||
|
|
23d2975bb5 | ||
|
|
d441d4c1c1 | ||
|
|
840cb7e2cb | ||
|
|
cf232e1002 | ||
|
|
7414dc6e7e | ||
|
|
d97f997349 | ||
|
|
0621ae1ca6 | ||
|
|
992048e1b2 | ||
|
|
261cad7674 | ||
|
|
a012e4b1af | ||
|
|
158b98ee05 | ||
|
|
14d367e50f | ||
|
|
48c575699e | ||
|
|
60768cc537 | ||
|
|
c26dff282b | ||
|
|
5022f6f26c | ||
|
|
94b93074bc | ||
|
|
d962033236 | ||
|
|
a548851245 | ||
|
|
b32e61bb6d | ||
|
|
e731322af3 | ||
|
|
fd21c6b4ee | ||
|
|
5a86862f47 | ||
|
|
1d1a2563c3 | ||
|
|
4bc57980ff | ||
|
|
3afd0c0971 | ||
|
|
e6a6cb27ec | ||
|
|
dcd78c5d84 | ||
|
|
2a1ad66292 | ||
|
|
5d0d4404b8 | ||
|
|
7b369c77b5 | ||
|
|
06b70a982b | ||
|
|
c9b1b0fb94 | ||
|
|
66bdbb0959 | ||
|
|
752f030d03 | ||
|
|
8c7e93c92e | ||
|
|
579885a6e2 | ||
|
|
45f7ebc0c9 | ||
|
|
997d675f8c |
20
.gitea/workflows/build-clan-app-darwin.yml
Normal file
20
.gitea/workflows/build-clan-app-darwin.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
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
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/sh
|
||||
|
||||
# Shared script for creating pull requests in Gitea workflows
|
||||
set -euo pipefail
|
||||
set -eu
|
||||
|
||||
# Required environment variables:
|
||||
# - CI_BOT_TOKEN: Gitea bot token for authentication
|
||||
@@ -8,22 +9,22 @@ set -euo pipefail
|
||||
# - 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
|
||||
@@ -43,9 +44,12 @@ resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
|
||||
}" \
|
||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
|
||||
|
||||
pr_number=$(echo "$resp" | jq -r '.number')
|
||||
if ! pr_number=$(echo "$resp" | jq -r '.number'); then
|
||||
echo "Error parsing response from pull request creation" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$pr_number" == "null" ]]; then
|
||||
if [ "$pr_number" = "null" ]; then
|
||||
echo "Error creating pull request:" >&2
|
||||
echo "$resp" | jq . >&2
|
||||
exit 1
|
||||
@@ -64,12 +68,15 @@ while true; do
|
||||
"delete_branch_after_merge": true
|
||||
}' \
|
||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
|
||||
msg=$(echo "$resp" | jq -r '.message')
|
||||
if [[ "$msg" != "Please try again later" ]]; then
|
||||
if ! msg=$(echo "$resp" | jq -r '.message'); then
|
||||
echo "Error parsing merge response" >&2
|
||||
exit 1
|
||||
fi
|
||||
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"
|
||||
|
||||
@@ -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 [secrets](https://docs.clan.lol/guides/getting-started/secrets/)<!-- [secrets.md](docs/site/guides/getting-started/secrets.md) -->.
|
||||
- **Secrets Management**: Securely manage secrets by consulting [Vars](https://docs.clan.lol/guides/vars-backend/)<!-- [secrets.md](docs/site/guides/vars-backend.md) -->.
|
||||
|
||||
### Contributing to Clan
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
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!
|
||||
|
||||
47
clanServices/internet/default.nix
Normal file
47
clanServices/internet/default.nix
Normal file
@@ -0,0 +1,47 @@
|
||||
{ ... }:
|
||||
{
|
||||
_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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
9
clanServices/internet/flake-module.nix
Normal file
9
clanServices/internet/flake-module.nix
Normal file
@@ -0,0 +1,9 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
internet = module;
|
||||
};
|
||||
}
|
||||
110
clanServices/tor/default.nix
Normal file
110
clanServices/tor/default.nix
Normal file
@@ -0,0 +1,110 @@
|
||||
{ ... }:
|
||||
{
|
||||
_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
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
9
clanServices/tor/flake-module.nix
Normal file
9
clanServices/tor/flake-module.nix
Normal file
@@ -0,0 +1,9 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
tor = module;
|
||||
};
|
||||
}
|
||||
@@ -134,9 +134,9 @@
|
||||
systemd.services.zerotier-inventory-autoaccept =
|
||||
let
|
||||
machines = uniqueStrings (
|
||||
(lib.attrNames roles.moon.machines)
|
||||
++ (lib.attrNames roles.controller.machines)
|
||||
++ (lib.attrNames roles.peer.machines)
|
||||
(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))
|
||||
);
|
||||
networkIps = builtins.foldl' (
|
||||
ips: name:
|
||||
|
||||
@@ -32,6 +32,33 @@ 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 = {
|
||||
@@ -73,4 +100,30 @@ 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";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
202
docs/mkdocs.yml
202
docs/mkdocs.yml
@@ -48,34 +48,25 @@ nav:
|
||||
- Home: index.md
|
||||
- Guides:
|
||||
- Getting Started:
|
||||
- 🚀 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
|
||||
- Mesh VPN: guides/mesh-vpn.md
|
||||
- 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
|
||||
- Vars Backend: guides/vars-backend.md
|
||||
- Facts Backend: guides/secrets.md
|
||||
- Adding more machines: guides/more-machines.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
|
||||
- Target Host: guides/target-host.md
|
||||
- Inventory:
|
||||
- Inventory: guides/inventory.md
|
||||
- Zerotier VPN: guides/mesh-vpn.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
|
||||
@@ -85,66 +76,84 @@ nav:
|
||||
- Reference:
|
||||
- Overview: reference/index.md
|
||||
- Services:
|
||||
- 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
|
||||
- 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
|
||||
- Modules:
|
||||
- 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
|
||||
- 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
|
||||
- CLI:
|
||||
- Overview: reference/cli/index.md
|
||||
|
||||
@@ -161,21 +170,7 @@ 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
|
||||
@@ -187,8 +182,14 @@ nav:
|
||||
- Template: decisions/_template.md
|
||||
- Options: options.md
|
||||
- Developer:
|
||||
- Introduction: intern/index.md
|
||||
- API: intern/api.md
|
||||
- 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
|
||||
|
||||
docs_dir: site
|
||||
site_dir: out
|
||||
@@ -246,3 +247,6 @@ plugins:
|
||||
- search
|
||||
- macros
|
||||
- redoc-tag
|
||||
- redirects:
|
||||
redirect_maps:
|
||||
guides/getting-started/secrets.md: guides/vars-backend.md
|
||||
|
||||
@@ -40,6 +40,7 @@ pkgs.stdenv.mkDerivation {
|
||||
mkdocs-material
|
||||
mkdocs-macros
|
||||
mkdocs-redoc-tag
|
||||
mkdocs-redirects
|
||||
]);
|
||||
configurePhase = ''
|
||||
pushd docs
|
||||
|
||||
@@ -114,9 +114,6 @@
|
||||
in
|
||||
{
|
||||
options = {
|
||||
_ = mkOption {
|
||||
type = types.raw;
|
||||
};
|
||||
instances.${name} = lib.mkOption {
|
||||
inherit description;
|
||||
type = types.submodule {
|
||||
@@ -149,20 +146,29 @@
|
||||
};
|
||||
};
|
||||
|
||||
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/";
|
||||
};
|
||||
docModules = [
|
||||
{
|
||||
inherit self;
|
||||
}
|
||||
self.modules.clan.default
|
||||
{
|
||||
options.inventory = lib.mkOption {
|
||||
type = types.submoduleWith {
|
||||
modules = [
|
||||
{ noInstanceOptions = true; }
|
||||
] ++ mapAttrsToList fakeInstanceOptions serviceModules;
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
in
|
||||
{
|
||||
# Uncomment for debugging
|
||||
# legacyPackages.docModules = lib.evalModules {
|
||||
# modules = docModules;
|
||||
# };
|
||||
|
||||
packages = lib.optionalAttrs ((privateInputs ? nuschtos) || (inputs ? nuschtos)) {
|
||||
docs-options =
|
||||
(privateInputs.nuschtos or inputs.nuschtos)
|
||||
@@ -171,7 +177,13 @@
|
||||
inherit baseHref;
|
||||
title = "Clan Options";
|
||||
# scopes = mapAttrsToList mkScope serviceModules;
|
||||
scopes = [ (mkScope "Clan Inventory" serviceModules) ];
|
||||
scopes = [
|
||||
{
|
||||
name = "Clan";
|
||||
modules = docModules;
|
||||
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -465,6 +465,10 @@ 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"
|
||||
@@ -801,7 +805,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](../../guides/authoring/clanServices/index.md)
|
||||
See: [clanService Authoring Guide](../../developer/extensions/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
|
||||
|
||||
@@ -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](../../migrations/migrate-inventory-services.md)
|
||||
- [Migration Guide from ClanModules to ClanServices](../../../guides/migrations/migrate-inventory-services.md)
|
||||
- [Decision that lead to ClanServices](../../../decisions/01-ClanModules.md)
|
||||
59
docs/site/guides/age-plugins.md
Normal file
59
docs/site/guides/age-plugins.md
Normal file
@@ -0,0 +1,59 @@
|
||||
## 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
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -138,7 +138,7 @@ You can use services exposed by Clan’s core module library, `clan-core`.
|
||||
|
||||
You can also author your own `clanService` modules.
|
||||
|
||||
🔗 Learn how to write your own service: [Authoring a clanService](../guides/authoring/clanServices/index.md)
|
||||
🔗 Learn how to write your own service: [Authoring a clanService](../developer/extensions/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
|
||||
|
||||
## What’s Next?
|
||||
|
||||
* [Author your own clanService →](../guides/authoring/clanServices/index.md)
|
||||
* [Author your own clanService →](../developer/extensions/clanServices/index.md)
|
||||
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
|
||||
<!-- TODO: * [Understand the architecture →](../explanation/clan-architecture.md) -->
|
||||
|
||||
@@ -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](../authoring/clanServices/index.md) if you want to bring your own
|
||||
Or read [authoring/clanServices](../../developer/extensions/clanServices/index.md) if you want to bring your own
|
||||
|
||||
2. Replace `__YOUR_CONTROLLER_` with the *name* of your machine.
|
||||
|
||||
|
||||
@@ -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 avilable on that machine.
|
||||
Which can be imported into individual machines to make the user available 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 seperate file. Inlining a module is not possible
|
||||
1. Type `path` or `string`: Must point to a separate file. Inlining a module is not possible
|
||||
|
||||
!!! Note "This is inspiration"
|
||||
Our community might come up with better solutions soon.
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
|
||||
@@ -18,7 +17,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 adress (*ipv4*, *ipv6* or *tor*)
|
||||
- Note down a reachable ip address (*ipv4*, *ipv6* or *tor*)
|
||||
|
||||
---
|
||||
|
||||
@@ -169,7 +168,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 succesfull
|
||||
Should now be successful
|
||||
|
||||
```shellSession
|
||||
Applied disk template 'single-disk' to machine 'jon'
|
||||
|
||||
@@ -59,7 +59,7 @@ Enter a *name*, confirm with *enter*. A directory with that name will be created
|
||||
|
||||
## Explore the Project Structure
|
||||
|
||||
Take a lookg at all project files:
|
||||
Take a look at all project files:
|
||||
|
||||
```bash
|
||||
cd my-clan
|
||||
@@ -125,11 +125,10 @@ 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#initialize-your-project)
|
||||
- [x] [Initialize Clan](./index.md#add-clan-cli-to-your-shell)
|
||||
- [ ] [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)
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -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](../authoring/clanServices/index.md)
|
||||
If you are a **module author** and need to migrate your modules please consult our **new** [clanServices authoring guide](../../developer/extensions/clanServices/index.md)
|
||||
|
||||
## What's Changing?
|
||||
|
||||
@@ -35,6 +35,37 @@ 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`
|
||||
@@ -70,6 +101,56 @@ 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
|
||||
@@ -131,6 +212,33 @@ 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
|
||||
@@ -138,8 +246,89 @@ roles.default.machines."test-inventory-machine".settings = {
|
||||
* `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](../authoring/clanServices/index.md)
|
||||
* [Authoring a 'clan.service' module](../../developer/extensions/clanServices/index.md)
|
||||
* [ClanServices](../clanServices.md)
|
||||
* [Inventory Reference](../../reference/nix-api/inventory.md)
|
||||
* [Inventory Reference](../../reference/nix-api/inventory.md)
|
||||
|
||||
@@ -1,25 +1,141 @@
|
||||
If you want to know more about how to save and share passwords in your clan read further!
|
||||
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.
|
||||
|
||||
### Adding a Secret
|
||||
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
|
||||
|
||||
```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
|
||||
@@ -37,7 +153,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.
|
||||
|
||||
|
||||
@@ -4,7 +4,21 @@ hide:
|
||||
- toc
|
||||
---
|
||||
|
||||
# :material-home: Welcome to **Clan**'s documentation
|
||||
# :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.
|
||||
|
||||
[Getting Started](./guides/getting-started/index.md){ .md-button }
|
||||
|
||||
@@ -38,7 +52,7 @@ hide:
|
||||
|
||||
Use Clan with [https://flake.parts/]()
|
||||
|
||||
- [Contribute](./guides/contributing/CONTRIBUTING.md)
|
||||
- [Contribute](./developer/contributing/CONTRIBUTING.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
font-family: "Roboto";
|
||||
src: url(./Roboto-Regular.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: url(./FiraCode-VF.ttf) format("truetype");
|
||||
@@ -20,3 +21,9 @@
|
||||
.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
26
flake.lock
generated
@@ -13,11 +13,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752589312,
|
||||
"narHash": "sha256-BafZOenlzMYdumG12AzgVLhEVu+GcEa8nYNDSIYe1U0=",
|
||||
"rev": "496bbf05a2aa7b061ef464254db5804d1c6f45b4",
|
||||
"lastModified": 1753067306,
|
||||
"narHash": "sha256-jyoEbaXa8/MwVQ+PajUdT63y3gYhgD9o7snO/SLaikw=",
|
||||
"rev": "18dfd42bdb2cfff510b8c74206005f733e38d8b9",
|
||||
"type": "tarball",
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/496bbf05a2aa7b061ef464254db5804d1c6f45b4.tar.gz"
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/18dfd42bdb2cfff510b8c74206005f733e38d8b9.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -31,11 +31,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752541678,
|
||||
"narHash": "sha256-dyhGzkld6jPqnT/UfGV2oqe7tYn7hppAqFvF3GZTyXY=",
|
||||
"lastModified": 1753140376,
|
||||
"narHash": "sha256-7lrVrE0jSvZHrxEzvnfHFE/Wkk9DDqb+mYCodI5uuB8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "2bf3421f7fed5c84d9392b62dcb9d76ef09796a7",
|
||||
"rev": "545aba02960caa78a31bd9a8709a0ad4b6320a5c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -51,11 +51,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1751413152,
|
||||
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
|
||||
"lastModified": 1753121425,
|
||||
"narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
|
||||
"rev": "644e0fc48951a860279da645ba77fe4a6e814c5e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -181,11 +181,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752055615,
|
||||
"narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=",
|
||||
"lastModified": 1753006367,
|
||||
"narHash": "sha256-tzbhc4XttkyEhswByk5R38l+ztN9UDbnj0cTcP6Hp9A=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9",
|
||||
"rev": "421b56313c65a0815a52b424777f55acf0b56ddf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
inputs = {
|
||||
flake-parts.follows = "flake-parts";
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
systems.follows = "systems";
|
||||
treefmt-nix.follows = "treefmt-nix";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -78,7 +78,87 @@ in
|
||||
internal = true;
|
||||
visible = false;
|
||||
type = types.deferredModule;
|
||||
default = { };
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
A module that is used to define the module of flake level exports -
|
||||
|
||||
@@ -149,8 +229,8 @@ in
|
||||
};
|
||||
|
||||
inventory = lib.mkOption {
|
||||
type = types.submodule {
|
||||
imports = [
|
||||
type = types.submoduleWith {
|
||||
modules = [
|
||||
{
|
||||
_module.args = { inherit clanLib; };
|
||||
_file = "clan interface";
|
||||
|
||||
@@ -247,7 +247,7 @@ in
|
||||
{
|
||||
distributedServices = clanLib.inventory.mapInstances {
|
||||
inherit (clanConfig) inventory exportsModule;
|
||||
inherit flakeInputs;
|
||||
inherit flakeInputs directory;
|
||||
clanCoreModules = clan-core.clan.modules;
|
||||
prefix = [ "distributedServices" ];
|
||||
};
|
||||
|
||||
@@ -7,8 +7,29 @@
|
||||
}:
|
||||
rec {
|
||||
buildClan =
|
||||
# TODO: Once all templates and docs are migrated add: lib.warn "'buildClan' is deprecated. Use 'clan-core.lib.clan' instead"
|
||||
module: (clan module).config;
|
||||
module:
|
||||
lib.warn ''
|
||||
==================== DEPRECATION NOTICE ====================
|
||||
Please migrate
|
||||
from: 'clan = inputs.<clan-core>.lib.buildClan'
|
||||
to : 'clan = inputs.<clan-core>.lib.clan'
|
||||
in your flake.nix.
|
||||
|
||||
Please also migrate
|
||||
from: 'inherit (clan) nixosConfigurations clanInternals; '
|
||||
to : "
|
||||
inherit (clan.config) nixosConfigurations clanInternals;
|
||||
clan = clan.config;
|
||||
"
|
||||
in your flake.nix.
|
||||
|
||||
Reason:
|
||||
- Improves consistency between flake-parts and non-flake-parts users.
|
||||
|
||||
- It also allows us to use the top level attribute 'clan' to expose
|
||||
attributes that can be used for cross-clan functionality.
|
||||
============================================================
|
||||
'' (clan module).config;
|
||||
|
||||
clan =
|
||||
{
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# Wraps all services in one fixed point module
|
||||
{
|
||||
# TODO: consume directly from clan.config
|
||||
directory,
|
||||
}:
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
@@ -29,6 +33,8 @@ in
|
||||
{
|
||||
_module.args._ctx = [ name ];
|
||||
_module.args.exports' = config.exports;
|
||||
_module.args.directory = directory;
|
||||
|
||||
}
|
||||
)
|
||||
./service-module.nix
|
||||
@@ -48,6 +54,7 @@ in
|
||||
{
|
||||
options = {
|
||||
instances = lib.mkOption {
|
||||
default = { };
|
||||
# instances.<instanceName>...
|
||||
type = types.attrsOf (submoduleWith {
|
||||
modules = [
|
||||
@@ -57,6 +64,7 @@ in
|
||||
};
|
||||
# instances.<machineName>...
|
||||
machines = lib.mkOption {
|
||||
default = { };
|
||||
type = types.attrsOf (submoduleWith {
|
||||
modules = [
|
||||
config.exportsModule
|
||||
@@ -69,8 +77,5 @@ in
|
||||
};
|
||||
default = { };
|
||||
};
|
||||
debug = mkOption {
|
||||
default = lib.mapAttrsToList (_: service: service.exports) config.mappedServices;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ in
|
||||
flakeInputs,
|
||||
# The clan inventory
|
||||
inventory,
|
||||
directory,
|
||||
clanCoreModules,
|
||||
prefix ? [ ],
|
||||
exportsModule,
|
||||
@@ -128,7 +129,7 @@ in
|
||||
_ctx = prefix;
|
||||
};
|
||||
modules = [
|
||||
./all-services-wrapper.nix
|
||||
(import ./all-services-wrapper.nix { inherit directory; })
|
||||
] ++ modules;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
lib,
|
||||
config,
|
||||
_ctx,
|
||||
directory,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -212,7 +213,7 @@ in
|
||||
|
||||
options.extraModules = lib.mkOption {
|
||||
default = [ ];
|
||||
type = types.listOf (types.deferredModule);
|
||||
type = types.listOf (types.either types.deferredModule types.str);
|
||||
};
|
||||
})
|
||||
];
|
||||
@@ -755,10 +756,14 @@ in
|
||||
instanceRes
|
||||
// {
|
||||
nixosModule = {
|
||||
imports = [
|
||||
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
|
||||
instanceRes.nixosModule
|
||||
] ++ instanceCfg.roles.${roleName}.extraModules;
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ let
|
||||
};
|
||||
in
|
||||
clanLib.inventory.mapInstances {
|
||||
directory = ./.;
|
||||
clanCoreModules = { };
|
||||
flakeInputs = flakeInputsFixture;
|
||||
inherit inventory;
|
||||
@@ -52,6 +53,7 @@ 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 =
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{ 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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.testDebug = lib.mkOption {
|
||||
default = 42;
|
||||
};
|
||||
}
|
||||
@@ -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](../../guides/authoring/clanServices/index.md).
|
||||
For further information see: [Module Authoring Guide](../../developer/extensions/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/guides/clanServices/
|
||||
And: https://docs.clan.lol/guides/authoring/clanServices/
|
||||
See: https://docs.clan.lol/developer/extensions/clanServices/
|
||||
And: https://docs.clan.lol/developer/extensions/clanServices/
|
||||
'' moduleSet;
|
||||
};
|
||||
|
||||
|
||||
@@ -313,6 +313,18 @@ 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.
|
||||
@@ -407,6 +419,14 @@ 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:
|
||||
|
||||
@@ -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_should_cancel
|
||||
from clan_lib.async_run import set_current_thread_opkey, 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,
|
||||
timeout: float = 60.0 * 60, # 1 hour default timeout
|
||||
) -> None:
|
||||
"""Process an API request in a separate thread with cancellation support.
|
||||
|
||||
@@ -112,6 +112,7 @@ 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} "
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
@@ -24,7 +25,7 @@ def remove_none(_list: list) -> list:
|
||||
RESULT: dict[str, SuccessDataClass[list[str] | None] | ErrorDataClass] = {}
|
||||
|
||||
|
||||
def get_clan_folder(*, op_key: str) -> SuccessDataClass[Flake] | ErrorDataClass:
|
||||
def get_clan_folder() -> 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.
|
||||
@@ -34,7 +35,10 @@ def get_clan_folder(*, op_key: str) -> SuccessDataClass[Flake] | ErrorDataClass:
|
||||
title="Select Clan Folder",
|
||||
initial_folder=str(Path.home()),
|
||||
)
|
||||
response = get_system_file(file_request, op_key=op_key)
|
||||
|
||||
response = get_system_file(file_request)
|
||||
|
||||
op_key = response.op_key
|
||||
|
||||
if isinstance(response, ErrorDataClass):
|
||||
return response
|
||||
@@ -70,8 +74,13 @@ def get_clan_folder(*, op_key: str) -> SuccessDataClass[Flake] | ErrorDataClass:
|
||||
|
||||
|
||||
def get_system_file(
|
||||
file_request: FileRequest, *, op_key: str
|
||||
file_request: FileRequest,
|
||||
) -> 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:
|
||||
|
||||
@@ -21,18 +21,12 @@ 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(
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
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
|
||||
|
||||
@@ -324,17 +333,34 @@ 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."""
|
||||
# Use the inherited thread processing method
|
||||
self.process_request_in_thread(
|
||||
api_request,
|
||||
thread_name="HttpThread",
|
||||
wait_for_completion=True,
|
||||
timeout=60.0,
|
||||
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}"
|
||||
)
|
||||
self.process_request(request)
|
||||
|
||||
def log_message(self, format: str, *args: Any) -> None: # noqa: A002
|
||||
"""Override default logging to use our logger."""
|
||||
|
||||
@@ -29,10 +29,7 @@ def _get_lib_names() -> list[str]:
|
||||
msg = f"Unsupported architecture: {machine}"
|
||||
raise RuntimeError(msg)
|
||||
if system == "darwin":
|
||||
if machine == "arm64":
|
||||
return ["libwebview.dylib"]
|
||||
msg = "Not supported"
|
||||
raise RuntimeError(msg)
|
||||
return ["libwebview.dylib"]
|
||||
# linux
|
||||
return ["libwebview.so"]
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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"
|
||||
@@ -21,6 +21,12 @@ buildNpmPackage (_finalAttrs: {
|
||||
mkdir -p api
|
||||
cp -r ${clan-ts-api}/* api
|
||||
cp -r ${fonts} ".fonts"
|
||||
|
||||
# only needed for the next couple weeks to make sure this file doesn't make it back into the git history
|
||||
if [[ -f "${./ui}/src/routes/Onboarding/background.jpg" ]]; then
|
||||
echo "background.jpg found, exiting"
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
|
||||
# todo figure out why this fails only inside of Nix
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { StorybookConfig } from "@kachurun/storybook-solid-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
framework: "@kachurun/storybook-solid-vite",
|
||||
stories: ["../src/components/**/*.mdx", "../src/components/**/*.stories.tsx"],
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-docs",
|
||||
|
||||
1
pkgs/clan-app/ui/logos/darknet-builder-logo.svg
Normal file
1
pkgs/clan-app/ui/logos/darknet-builder-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
186
pkgs/clan-app/ui/package-lock.json
generated
186
pkgs/clan-app/ui/package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"@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",
|
||||
@@ -53,7 +54,6 @@
|
||||
"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,22 +360,6 @@
|
||||
"@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",
|
||||
@@ -1552,13 +1536,6 @@
|
||||
"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",
|
||||
@@ -1813,64 +1790,6 @@
|
||||
"@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",
|
||||
@@ -1883,21 +1802,6 @@
|
||||
"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",
|
||||
@@ -1985,16 +1889,6 @@
|
||||
"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",
|
||||
@@ -2028,20 +1922,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -2281,9 +2161,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.81.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz",
|
||||
"integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2291,12 +2181,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/solid-query": {
|
||||
"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==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.81.5"
|
||||
"@tanstack/query-core": "5.83.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2306,6 +2196,23 @@
|
||||
"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",
|
||||
@@ -6996,29 +6903,6 @@
|
||||
"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",
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"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",
|
||||
@@ -73,6 +72,7 @@
|
||||
"@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",
|
||||
|
||||
@@ -138,6 +138,10 @@
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
& > span.typography {
|
||||
@apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
/* button group */
|
||||
|
||||
@@ -9,6 +9,7 @@ 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">
|
||||
@@ -26,7 +27,7 @@ const meta = {
|
||||
<div
|
||||
class={cx({
|
||||
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
|
||||
"w-[1024px]": context.args.orientation == "horizontal",
|
||||
"w-[512px]": context.args.orientation == "horizontal",
|
||||
"bg-inv-acc-3": context.args.inverted,
|
||||
})}
|
||||
>
|
||||
@@ -63,6 +64,11 @@ 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} />
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
div.form-field.host-file {
|
||||
button {
|
||||
@apply w-1/2;
|
||||
@apply w-fit;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
button {
|
||||
@apply grow max-w-[18rem];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export type Story = StoryObj<typeof meta>;
|
||||
export const Bare: Story = {
|
||||
args: {
|
||||
onSelectFile: async () => {
|
||||
return "/home/bob/clans/my-clan";
|
||||
return "/home/github/clans/my-clan/foo/bar/baz/fizz/buzz";
|
||||
},
|
||||
input: {
|
||||
placeholder: "e.g. 11/06/89",
|
||||
|
||||
@@ -12,6 +12,8 @@ import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
import { createSignal } from "solid-js";
|
||||
import { Tooltip } from "@kobalte/core/tooltip";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
|
||||
export type HostFileInputProps = FieldProps &
|
||||
TextFieldRootProps & {
|
||||
@@ -20,10 +22,21 @@ export type HostFileInputProps = FieldProps &
|
||||
};
|
||||
|
||||
export const HostFileInput = (props: HostFileInputProps) => {
|
||||
const [value, setValue] = createSignal<string | undefined>(undefined);
|
||||
const [value, setValue] = createSignal<string>(props.value || "");
|
||||
|
||||
let actualInputElement: HTMLInputElement | undefined;
|
||||
|
||||
const selectFile = async () => {
|
||||
setValue(await props.onSelectFile());
|
||||
try {
|
||||
console.log("selecting file", props.onSelectFile);
|
||||
setValue(await props.onSelectFile());
|
||||
actualInputElement?.dispatchEvent(
|
||||
new Event("input", { bubbles: true, cancelable: true }),
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error selecting file", error);
|
||||
// todo work out how to display the error
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -33,26 +46,65 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
ghost: props.ghost,
|
||||
})}
|
||||
{...props}
|
||||
value={value()}
|
||||
onChange={setValue}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={"start"}>
|
||||
<Orienter
|
||||
orientation={props.orientation}
|
||||
align={props.orientation == "horizontal" ? "center" : "start"}
|
||||
>
|
||||
<Label
|
||||
labelComponent={TextField.Label}
|
||||
descriptionComponent={TextField.Description}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<TextField.Input {...props.input} hidden={true} />
|
||||
<TextField.Input
|
||||
{...props.input}
|
||||
hidden={true}
|
||||
value={value()}
|
||||
ref={(el: HTMLInputElement) => {
|
||||
actualInputElement = el; // Capture for local use
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
size={props.size}
|
||||
startIcon="Folder"
|
||||
onClick={selectFile}
|
||||
>
|
||||
{value() ? value() : "No Selection"}
|
||||
</Button>
|
||||
{!value() && (
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
size={props.size}
|
||||
startIcon="Folder"
|
||||
onClick={selectFile}
|
||||
disabled={props.disabled || props.readOnly}
|
||||
>
|
||||
No Selection
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{value() && (
|
||||
<Tooltip placement="top">
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content class="tooltip-content">
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={!props.inverted}
|
||||
>
|
||||
{value()}
|
||||
</Typography>
|
||||
<Tooltip.Arrow />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
<Tooltip.Trigger
|
||||
as={Button}
|
||||
hierarchy="secondary"
|
||||
size={props.size}
|
||||
startIcon="Folder"
|
||||
onClick={selectFile}
|
||||
disabled={props.disabled || props.readOnly}
|
||||
>
|
||||
{value()}
|
||||
</Tooltip.Trigger>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Orienter>
|
||||
</TextField>
|
||||
);
|
||||
|
||||
@@ -22,40 +22,3 @@ div.form-label {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.tooltip-content {
|
||||
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
|
||||
|
||||
max-width: min(calc(100vw - 16px), 380px);
|
||||
transform-origin: var(--kb-tooltip-content-transform-origin);
|
||||
animation: tooltipHide 250ms ease-in forwards;
|
||||
|
||||
&[data-expanded] {
|
||||
animation: tooltipShow 250ms ease-out;
|
||||
}
|
||||
|
||||
&.inverted {
|
||||
@apply bg-def-2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tooltipShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes tooltipHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Show } from "solid-js";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Tooltip as KTooltip } from "@kobalte/core/tooltip";
|
||||
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
||||
import Icon from "@/src/components/Icon/Icon";
|
||||
import { TextField } from "@kobalte/core/text-field";
|
||||
import { Checkbox } from "@kobalte/core/checkbox";
|
||||
import { Combobox } from "@kobalte/core/combobox";
|
||||
import "./Label.css";
|
||||
import cx from "classnames";
|
||||
|
||||
export type Size = "default" | "s";
|
||||
|
||||
@@ -49,31 +48,27 @@ export const Label = (props: LabelProps) => {
|
||||
{props.label}
|
||||
</Typography>
|
||||
{props.tooltip && (
|
||||
<KTooltip placement="top">
|
||||
<KTooltip.Trigger>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
inverted={props.inverted}
|
||||
trigger={
|
||||
<Icon
|
||||
icon="Info"
|
||||
color="tertiary"
|
||||
inverted={props.inverted}
|
||||
size={props.size == "default" ? "0.85em" : "0.75rem"}
|
||||
/>
|
||||
<KTooltip.Portal>
|
||||
<KTooltip.Content
|
||||
class={cx("tooltip-content", { inverted: props.inverted })}
|
||||
>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={!props.inverted}
|
||||
>
|
||||
{props.tooltip}
|
||||
</Typography>
|
||||
<KTooltip.Arrow />
|
||||
</KTooltip.Content>
|
||||
</KTooltip.Portal>
|
||||
</KTooltip.Trigger>
|
||||
</KTooltip>
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={!props.inverted}
|
||||
>
|
||||
{props.tooltip}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)}
|
||||
</props.labelComponent>
|
||||
{props.description && (
|
||||
|
||||
@@ -5,7 +5,7 @@ div.orienter {
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
@apply flex-row justify-start;
|
||||
@apply flex-row justify-between gap-0;
|
||||
|
||||
& > div.form-label {
|
||||
@apply w-1/2 shrink;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
div.modal-content {
|
||||
@apply max-w-[512px];
|
||||
@apply min-w-[320px] 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;
|
||||
|
||||
& > .title {
|
||||
& > .modal-title {
|
||||
@apply mx-auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -13,6 +14,8 @@ export interface ModalProps {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: (ctx: ModalContext) => JSX.Element;
|
||||
mount?: Node;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const Modal = (props: ModalProps) => {
|
||||
@@ -20,18 +23,33 @@ export const Modal = (props: ModalProps) => {
|
||||
|
||||
return (
|
||||
<KDialog id={props.id} open={open()} modal={true}>
|
||||
<KDialog.Portal>
|
||||
<KDialog.Content class="modal-content">
|
||||
<KDialog.Portal mount={props.mount}>
|
||||
<KDialog.Content class={cx("modal-content", props.class)}>
|
||||
<div class="header">
|
||||
<Typography class="title" hierarchy="label" family="mono" size="xs">
|
||||
<Typography
|
||||
class="modal-title"
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="xs"
|
||||
>
|
||||
{props.title}
|
||||
</Typography>
|
||||
<KDialog.CloseButton onClick={() => setOpen(false)}>
|
||||
<KDialog.CloseButton
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
<Icon icon="Close" size="0.75rem" />
|
||||
</KDialog.CloseButton>
|
||||
</div>
|
||||
<div class="body">
|
||||
{props.children({ close: () => setOpen(false) })}
|
||||
{props.children({
|
||||
close: () => {
|
||||
setOpen(false);
|
||||
props.onClose();
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</KDialog.Content>
|
||||
</KDialog.Portal>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
div.sidebar {
|
||||
@apply h-full w-auto max-w-60 border-none;
|
||||
@apply w-60 border-none;
|
||||
|
||||
& > div.header {
|
||||
}
|
||||
157
pkgs/clan-app/ui/src/components/Sidebar/Sidebar.stories.tsx
Normal file
157
pkgs/clan-app/ui/src/components/Sidebar/Sidebar.stories.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
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>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
28
pkgs/clan-app/ui/src/components/Sidebar/Sidebar.tsx
Normal file
28
pkgs/clan-app/ui/src/components/Sidebar/Sidebar.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,46 +1,60 @@
|
||||
import "./SidebarNavBody.css";
|
||||
import "./SidebarBody.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) => (
|
||||
<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} />
|
||||
<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>
|
||||
<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>
|
||||
</A>
|
||||
);
|
||||
|
||||
export const SidebarNavBody = (props: SidebarNavProps) => {
|
||||
const sectionLabels = props.extraSections.map((section) => section.label);
|
||||
export const SidebarBody = (props: SidebarProps) => {
|
||||
const clanURI = useClanURI();
|
||||
const machineList = useMachinesQuery(clanURI);
|
||||
|
||||
const sectionLabels = (props.staticSections || []).map(
|
||||
(section) => section.title,
|
||||
);
|
||||
|
||||
// controls which sections are open by default
|
||||
// we want them all to be open by default
|
||||
@@ -76,20 +90,24 @@ export const SidebarNavBody = (props: SidebarNavProps) => {
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="content">
|
||||
<nav>
|
||||
<For each={props.clanDetail.machines}>
|
||||
{(machine) => (
|
||||
<A href={machine.path}>
|
||||
<MachineRoute {...machine} />
|
||||
</A>
|
||||
<For each={Object.entries(machineList.data || {})}>
|
||||
{([id, machine]) => (
|
||||
<MachineRoute
|
||||
clanURI={clanURI}
|
||||
machineID={id}
|
||||
name={machine.name || id}
|
||||
status="Not Installed"
|
||||
serviceCount={0}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</nav>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
|
||||
<For each={props.extraSections}>
|
||||
<For each={props.staticSections}>
|
||||
{(section) => (
|
||||
<Accordion.Item class="item" value={section.label}>
|
||||
<Accordion.Item class="item" value={section.title}>
|
||||
<Accordion.Header class="header">
|
||||
<Accordion.Trigger class="trigger">
|
||||
<Typography
|
||||
@@ -100,7 +118,7 @@ export const SidebarNavBody = (props: SidebarNavProps) => {
|
||||
inverted={true}
|
||||
color="tertiary"
|
||||
>
|
||||
{section.label}
|
||||
{section.title}
|
||||
</Typography>
|
||||
<Icon
|
||||
icon="CaretDown"
|
||||
@@ -15,10 +15,11 @@ div.sidebar-header {
|
||||
|
||||
transition: all 250ms ease-in-out;
|
||||
|
||||
div.title {
|
||||
div.clan-label {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
107
pkgs/clan-app/ui/src/components/Sidebar/SidebarHeader.tsx
Normal file
107
pkgs/clan-app/ui/src/components/Sidebar/SidebarHeader.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
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: {},
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
div.sidebar-pane {
|
||||
@apply h-full w-auto max-w-60 border-none;
|
||||
@apply w-full max-w-60 border-none;
|
||||
|
||||
& > div.header {
|
||||
@apply flex items-center justify-between px-3 py-2 rounded-t-[0.5rem];
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Checkbox } from "@/src/components/Form/Checkbox";
|
||||
import { Combobox } from "../Form/Combobox";
|
||||
|
||||
const meta: Meta<SidebarPaneProps> = {
|
||||
title: "Components/Sidebar/Pane",
|
||||
title: "Components/SidebarPane",
|
||||
component: SidebarPane,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ div.sidebar-section {
|
||||
}
|
||||
|
||||
& > div.content {
|
||||
@apply w-full h-full px-1.5 py-3 rounded-md bg-inv-4;
|
||||
@apply w-full h-full px-1.5 py-3 rounded-md bg-inv-4 opacity-60;
|
||||
}
|
||||
|
||||
&.editing > div.content {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -20,7 +21,7 @@ export const SidebarSection = (props: SidebarSectionProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="sidebar-section">
|
||||
<div class={cx("sidebar-section", { editing: editing() })}>
|
||||
<div class="header">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
|
||||
9
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.css
Normal file
9
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.css
Normal file
@@ -0,0 +1,9 @@
|
||||
div.tooltip-content {
|
||||
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
|
||||
|
||||
max-width: min(calc(100vw - 16px), 380px);
|
||||
|
||||
&.inverted {
|
||||
@apply bg-def-2;
|
||||
}
|
||||
}
|
||||
40
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.stories.tsx
Normal file
40
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.stories.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Tooltip, TooltipProps } from "@/src/components/Tooltip/Tooltip";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
|
||||
const meta: Meta<TooltipProps> = {
|
||||
title: "Components/Tooltip",
|
||||
component: Tooltip,
|
||||
decorators: [
|
||||
(Story: StoryObj<TooltipProps>) => (
|
||||
<div class="p-16">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<TooltipProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placement: "top",
|
||||
inverted: false,
|
||||
trigger: <Button hierarchy="primary">Trigger</Button>,
|
||||
children: (
|
||||
<Typography hierarchy="body" size="xs" inverted={true} weight="medium">
|
||||
Your Clan is being created
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const AnimateBounce: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
animation: "bounce",
|
||||
},
|
||||
};
|
||||
34
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.tsx
Normal file
34
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import "./Tooltip.css";
|
||||
import {
|
||||
Tooltip as KTooltip,
|
||||
TooltipRootProps as KTooltipRootProps,
|
||||
} from "@kobalte/core/tooltip";
|
||||
import cx from "classnames";
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
export interface TooltipProps extends KTooltipRootProps {
|
||||
inverted?: boolean;
|
||||
trigger: JSX.Element;
|
||||
children: JSX.Element;
|
||||
animation?: "bounce";
|
||||
}
|
||||
|
||||
export const Tooltip = (props: TooltipProps) => {
|
||||
return (
|
||||
<KTooltip {...props}>
|
||||
<KTooltip.Trigger>{props.trigger}</KTooltip.Trigger>
|
||||
<KTooltip.Portal>
|
||||
<KTooltip.Content
|
||||
class={cx("tooltip-content", {
|
||||
inverted: props.inverted,
|
||||
"animate-bounce": props.animation == "bounce",
|
||||
})}
|
||||
>
|
||||
{props.placement == "bottom" && <KTooltip.Arrow />}
|
||||
{props.children}
|
||||
{props.placement == "top" && <KTooltip.Arrow />}
|
||||
</KTooltip.Content>
|
||||
</KTooltip.Portal>
|
||||
</KTooltip>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,7 @@ 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>;
|
||||
@@ -42,7 +43,7 @@ interface BackendReturnType<K extends OperationNames> {
|
||||
* @property {Promise<BackendReturnType<K>>} result - A promise that resolves to the return type of the backend operation.
|
||||
* @property {() => Promise<void>} cancel - A function to cancel the API call, returning a promise that resolves when cancellation is completed.
|
||||
*/
|
||||
interface ApiCall<K extends OperationNames> {
|
||||
export interface ApiCall<K extends OperationNames> {
|
||||
uuid: string;
|
||||
result: Promise<OperationResponse<K>>;
|
||||
cancel: () => Promise<void>;
|
||||
@@ -64,9 +65,14 @@ export const callApi = <K extends OperationNames>(
|
||||
};
|
||||
}
|
||||
|
||||
const op_key = backendOpts?.op_key ?? crypto.randomUUID();
|
||||
|
||||
const req: BackendSendType<OperationNames> = {
|
||||
body: args,
|
||||
header: backendOpts,
|
||||
header: {
|
||||
...backendOpts,
|
||||
op_key,
|
||||
},
|
||||
};
|
||||
|
||||
const result = (
|
||||
@@ -78,9 +84,6 @@ 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),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { Params, Navigator } from "@solidjs/router";
|
||||
import { Params, Navigator, useParams } from "@solidjs/router";
|
||||
|
||||
export const encodeBase64 = (value: string) => window.btoa(value);
|
||||
export const decodeBase64 = (value: string) => window.atob(value);
|
||||
|
||||
export const selectClanFolder = async () => {
|
||||
const req = callApi("get_clan_folder", {});
|
||||
@@ -20,10 +23,51 @@ export const selectClanFolder = async () => {
|
||||
throw new Error("Illegal state exception");
|
||||
};
|
||||
|
||||
export const navigateToClan = (navigate: Navigator, uri: string) => {
|
||||
navigate("/clan/" + window.btoa(uri));
|
||||
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 clanURIParam = (params: Params) => {
|
||||
return window.atob(params.clanURI);
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
import "./index.css";
|
||||
import { QueryClient } from "@tanstack/solid-query";
|
||||
import { QueryClient, QueryClientProvider } 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();
|
||||
|
||||
@@ -18,8 +19,14 @@ 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(() => <Router root={Layout}>{Routes}</Router>, root!);
|
||||
render(
|
||||
() => (
|
||||
<QueryClientProvider client={client}>
|
||||
{import.meta.env.DEV && <SolidQueryDevtools initialIsOpen={true} />}
|
||||
<Router root={Layout}>{Routes}</Router>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
root!,
|
||||
);
|
||||
|
||||
80
pkgs/clan-app/ui/src/queries/queries.ts
Normal file
80
pkgs/clan-app/ui/src/queries/queries.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
})),
|
||||
}));
|
||||
24
pkgs/clan-app/ui/src/routes/Clan/Clan.css
Normal file
24
pkgs/clan-app/ui/src/routes/Clan/Clan.css
Normal file
@@ -0,0 +1,24 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,10 +1,309 @@
|
||||
import { RouteSectionProps, useParams } from "@solidjs/router";
|
||||
import { Component } from "solid-js";
|
||||
import { clanURIParam } from "@/src/hooks/clan";
|
||||
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 { 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) => {
|
||||
const params = useParams();
|
||||
const clanURI = clanURIParam(params);
|
||||
return <CubeScene />;
|
||||
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 });
|
||||
};
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { Component } from "solid-js";
|
||||
import { RouteSectionProps } from "@solidjs/router";
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
import { activeClanURI } from "@/src/stores/clan";
|
||||
import { navigateToClan } from "@/src/hooks/clan";
|
||||
|
||||
export const Layout: Component<RouteSectionProps> = (props) => (
|
||||
<div class="size-full h-screen">{props.children}</div>
|
||||
);
|
||||
export const Layout: Component<RouteSectionProps> = (props) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// check for an active clan uri and redirect to it on first load
|
||||
const activeURI = activeClanURI();
|
||||
if (!props.location.pathname.startsWith("/clans/") && activeURI) {
|
||||
navigateToClan(navigate, activeURI);
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
return <div class="size-full h-screen">{props.children}</div>;
|
||||
};
|
||||
|
||||
19
pkgs/clan-app/ui/src/routes/Machine/Machine.tsx
Normal file
19
pkgs/clan-app/ui/src/routes/Machine/Machine.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
663
pkgs/clan-app/ui/src/routes/Onboarding/Creating.css
Normal file
663
pkgs/clan-app/ui/src/routes/Onboarding/Creating.css
Normal file
@@ -0,0 +1,663 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
90
pkgs/clan-app/ui/src/routes/Onboarding/Creating.tsx
Normal file
90
pkgs/clan-app/ui/src/routes/Onboarding/Creating.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
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>
|
||||
);
|
||||
@@ -54,11 +54,10 @@ main#welcome {
|
||||
}
|
||||
|
||||
& > div.container {
|
||||
@apply flex flex-col items-center justify-evenly gap-y-20;
|
||||
@apply size-fit;
|
||||
@apply flex flex-col items-center justify-evenly gap-y-20 size-fit;
|
||||
|
||||
& > div.welcome {
|
||||
@apply flex flex-col min-w-80 gap-y-6;
|
||||
@apply flex flex-col w-80 gap-y-6;
|
||||
|
||||
& > div.separator {
|
||||
@apply grid grid-cols-3 grid-rows-1 gap-x-4 items-center;
|
||||
@@ -66,7 +65,7 @@ main#welcome {
|
||||
}
|
||||
|
||||
& > div.setup {
|
||||
@apply flex flex-col min-w-[520px] gap-y-5;
|
||||
@apply flex flex-col w-[33rem] gap-y-5;
|
||||
@apply pt-10 px-8 pb-8 bg-def-1 rounded-lg;
|
||||
|
||||
& > div.header {
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { Component, createSignal, Match, Setter, Show, Switch } from "solid-js";
|
||||
import {
|
||||
Accessor,
|
||||
Component,
|
||||
createSignal,
|
||||
Match,
|
||||
Setter,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
import "./Onboarding.css";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Alert } from "@/src/components/Alert/Alert";
|
||||
|
||||
import { Divider } from "@/src/components/Divider/Divider";
|
||||
import { Logo } from "@/src/components/Logo/Logo";
|
||||
import { navigateToClan, selectClanFolder } from "@/src/hooks/clan";
|
||||
import { activeClanURI } from "@/src/stores/clan";
|
||||
import { activeClanURI, addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
||||
import {
|
||||
createForm,
|
||||
FormStore,
|
||||
getError,
|
||||
getErrors,
|
||||
getValue,
|
||||
SubmitHandler,
|
||||
valiForm,
|
||||
} from "@modular-forms/solid";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
@@ -20,23 +31,32 @@ import { TextArea } from "@/src/components/Form/TextArea";
|
||||
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";
|
||||
type State = "welcome" | "setup" | "creating";
|
||||
|
||||
const SetupSchema = v.object({
|
||||
name: v.pipe(v.string(), v.nonEmpty("Please enter a name.")),
|
||||
name: v.pipe(
|
||||
v.string(),
|
||||
v.nonEmpty("Please enter a name."),
|
||||
v.regex(
|
||||
new RegExp("^[a-zA-Z0-9_\\-]+$"),
|
||||
"Name must be alphanumeric and can contain underscores and dashes, without spaces.",
|
||||
),
|
||||
),
|
||||
description: v.pipe(v.string(), v.nonEmpty("Please describe your clan.")),
|
||||
directory: v.pipe(v.string(), v.nonEmpty("Please select a directory.")),
|
||||
directory: v.pipe(
|
||||
// initial value is undefined, and I can't see how to handle this better in valibot, so for now when the type
|
||||
// is incorrect we treat it as empty
|
||||
v.string("Please select a directory."),
|
||||
v.nonEmpty("Please select a directory."),
|
||||
),
|
||||
});
|
||||
|
||||
type SetupForm = v.InferInput<typeof SetupSchema>;
|
||||
|
||||
interface backgroundProps {
|
||||
state: State;
|
||||
form: FormStore<SetupForm>;
|
||||
}
|
||||
|
||||
const background = (props: backgroundProps) => (
|
||||
const background = (props: { state: State; form: FormStore<SetupForm> }) => (
|
||||
<div class="background">
|
||||
<div class="layer-1" />
|
||||
<div class="layer-2" />
|
||||
@@ -71,7 +91,11 @@ const background = (props: backgroundProps) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const welcome = (setState: Setter<State>) => {
|
||||
const welcome = (props: {
|
||||
setState: Setter<State>;
|
||||
welcomeError: Accessor<string | undefined>;
|
||||
setWelcomeError: Setter<string | undefined>;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const selectFolder = async () => {
|
||||
@@ -91,7 +115,23 @@ const welcome = (setState: Setter<State>) => {
|
||||
Build your <br />
|
||||
own darknet
|
||||
</Typography>
|
||||
<Button hierarchy="secondary" onClick={() => setState("setup")}>
|
||||
{props.welcomeError() && (
|
||||
<Alert
|
||||
type="error"
|
||||
icon="Info"
|
||||
title="Your Clan creation failed"
|
||||
description={props.welcomeError() || ""}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
onClick={() => {
|
||||
// reset welcome error
|
||||
props.setWelcomeError(undefined);
|
||||
// move to next step
|
||||
props.setState("setup");
|
||||
}}
|
||||
>
|
||||
Start building
|
||||
</Button>
|
||||
<div class="separator">
|
||||
@@ -126,13 +166,81 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
|
||||
const [state, setState] = createSignal<State>("welcome");
|
||||
|
||||
// used to display an error in the welcome screen in the event of a failed
|
||||
// clan creation
|
||||
const [welcomeError, setWelcomeError] = createSignal<string | undefined>();
|
||||
|
||||
//
|
||||
const [setupForm, { Form, Field }] = createForm<SetupForm>({
|
||||
validate: valiForm(SetupSchema),
|
||||
});
|
||||
|
||||
const metaError = () => {
|
||||
const errors = getErrors(setupForm, ["name", "description"]);
|
||||
return errors ? errors.name || errors.description : undefined;
|
||||
const formError = () => {
|
||||
const formErrors = getErrors(setupForm);
|
||||
return (
|
||||
formErrors.name ||
|
||||
formErrors.description ||
|
||||
formErrors.directory ||
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
||||
const onSelectFile = async () => {
|
||||
const req = callApi("get_system_file", {
|
||||
file_request: {
|
||||
mode: "select_folder",
|
||||
title: "Select a folder for you new Clan",
|
||||
},
|
||||
});
|
||||
|
||||
const resp = await req.result;
|
||||
|
||||
if (resp.status === "error") {
|
||||
// just throw the first error, I can't imagine why there would be multiple
|
||||
// errors for this call
|
||||
throw new Error(resp.errors[0].message);
|
||||
}
|
||||
|
||||
if (resp.status === "success" && resp.data) {
|
||||
return resp.data[0];
|
||||
}
|
||||
|
||||
throw new Error("No data returned from api call");
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<SetupForm> = async (
|
||||
{ name, description, directory },
|
||||
event,
|
||||
) => {
|
||||
const path = `${directory}/${name}`;
|
||||
|
||||
const req = callApi("create_clan", {
|
||||
opts: {
|
||||
dest: path,
|
||||
// todo allow users to select a template
|
||||
template: "minimal",
|
||||
initial: {
|
||||
name,
|
||||
description,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setState("creating");
|
||||
|
||||
const resp = await req.result;
|
||||
|
||||
if (resp.status === "error") {
|
||||
setWelcomeError(resp.errors[0].message);
|
||||
setState("welcome");
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp.status === "success") {
|
||||
addClanURI(path);
|
||||
setActiveClanURI(path);
|
||||
navigateToClan(navigate, path);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -140,7 +248,13 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
{background({ form: setupForm, state: state() })}
|
||||
<div class="container">
|
||||
<Switch>
|
||||
<Match when={state() === "welcome"}>{welcome(setState)}</Match>
|
||||
<Match when={state() === "welcome"}>
|
||||
{welcome({
|
||||
setState,
|
||||
welcomeError,
|
||||
setWelcomeError,
|
||||
})}
|
||||
</Match>
|
||||
|
||||
<Match when={state() === "setup"}>
|
||||
<div class="setup">
|
||||
@@ -155,8 +269,16 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
Setup
|
||||
</Typography>
|
||||
</div>
|
||||
<Form>
|
||||
<Fieldset name="meta" error={metaError()}>
|
||||
<Form onSubmit={onSubmit}>
|
||||
{formError() && (
|
||||
<Alert
|
||||
type="error"
|
||||
icon="Info"
|
||||
title="Form error"
|
||||
description={formError() || ""}
|
||||
/>
|
||||
)}
|
||||
<Fieldset name="meta">
|
||||
<Field name="name">
|
||||
{(field, input) => (
|
||||
<TextInput
|
||||
@@ -195,15 +317,13 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
</Field>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset
|
||||
name="location"
|
||||
error={getError(setupForm, "directory")}
|
||||
>
|
||||
<Fieldset name="location">
|
||||
<Field name="directory">
|
||||
{(field, input) => (
|
||||
<HostFileInput
|
||||
onSelectFile={async () => "test"}
|
||||
onSelectFile={onSelectFile}
|
||||
{...field}
|
||||
value={field.value}
|
||||
label="Select directory"
|
||||
orientation="horizontal"
|
||||
required={true}
|
||||
@@ -228,6 +348,10 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
</Form>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={state() === "creating"}>
|
||||
<Creating />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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[] = [
|
||||
{
|
||||
@@ -8,7 +9,30 @@ export const Routes: RouteDefinition[] = [
|
||||
component: Onboarding,
|
||||
},
|
||||
{
|
||||
path: "/clan/:clanURI",
|
||||
component: Clan,
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
10
pkgs/clan-app/ui/src/scene/cubes.css
Normal file
10
pkgs/clan-app/ui/src/scene/cubes.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.cubes-scene-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
@apply absolute bottom-8 z-10 w-full;
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
15
pkgs/clan-app/ui/src/scene/cubes.stories.tsx
Normal file
15
pkgs/clan-app/ui/src/scene/cubes.stories.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { CubeScene } from "./cubes";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "scene/cubes",
|
||||
component: CubeScene,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user