Compare commits
3 Commits
remove-bui
...
clan-autor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e18afbc5ea | ||
|
|
a04443adb8 | ||
|
|
20ad968d04 |
@@ -1,20 +0,0 @@
|
|||||||
name: Build Clan App (Darwin)
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Run every 4 hours
|
|
||||||
- cron: "0 */4 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-clan-app-darwin:
|
|
||||||
runs-on: nix
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build clan-app for x86_64-darwin
|
|
||||||
run: |
|
|
||||||
nix build .#packages.x86_64-darwin.clan-app --system x86_64-darwin --log-format bar-with-logs
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Shared script for creating pull requests in Gitea workflows
|
# Shared script for creating pull requests in Gitea workflows
|
||||||
set -eu
|
set -euo pipefail
|
||||||
|
|
||||||
# Required environment variables:
|
# Required environment variables:
|
||||||
# - CI_BOT_TOKEN: Gitea bot token for authentication
|
# - CI_BOT_TOKEN: Gitea bot token for authentication
|
||||||
@@ -9,22 +8,22 @@ set -eu
|
|||||||
# - PR_TITLE: Title of the pull request
|
# - PR_TITLE: Title of the pull request
|
||||||
# - PR_BODY: Body/description 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
|
echo "Error: CI_BOT_TOKEN is not set" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "${PR_BRANCH:-}" ]; then
|
if [[ -z "${PR_BRANCH:-}" ]]; then
|
||||||
echo "Error: PR_BRANCH is not set" >&2
|
echo "Error: PR_BRANCH is not set" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "${PR_TITLE:-}" ]; then
|
if [[ -z "${PR_TITLE:-}" ]]; then
|
||||||
echo "Error: PR_TITLE is not set" >&2
|
echo "Error: PR_TITLE is not set" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "${PR_BODY:-}" ]; then
|
if [[ -z "${PR_BODY:-}" ]]; then
|
||||||
echo "Error: PR_BODY is not set" >&2
|
echo "Error: PR_BODY is not set" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -44,12 +43,9 @@ resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
|
|||||||
}" \
|
}" \
|
||||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
|
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
|
||||||
|
|
||||||
if ! pr_number=$(echo "$resp" | jq -r '.number'); then
|
pr_number=$(echo "$resp" | jq -r '.number')
|
||||||
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 "Error creating pull request:" >&2
|
||||||
echo "$resp" | jq . >&2
|
echo "$resp" | jq . >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -68,11 +64,8 @@ while true; do
|
|||||||
"delete_branch_after_merge": true
|
"delete_branch_after_merge": true
|
||||||
}' \
|
}' \
|
||||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
|
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
|
||||||
if ! msg=$(echo "$resp" | jq -r '.message'); then
|
msg=$(echo "$resp" | jq -r '.message')
|
||||||
echo "Error parsing merge response" >&2
|
if [[ "$msg" != "Please try again later" ]]; then
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ "$msg" != "Please try again later" ]; then
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
echo "Retrying in 2 seconds..."
|
echo "Retrying in 2 seconds..."
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
recommendedOptimisation = lib.mkDefault true;
|
recommendedOptimisation = lib.mkDefault true;
|
||||||
recommendedProxySettings = lib.mkDefault true;
|
recommendedProxySettings = lib.mkDefault true;
|
||||||
recommendedTlsSettings = 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.
|
# Nginx sends all the access logs to /var/log/nginx/access.log by default.
|
||||||
# instead of going to the journal!
|
# instead of going to the journal!
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
{ ... }:
|
|
||||||
{
|
|
||||||
_class = "clan.service";
|
|
||||||
manifest.name = "clan-core/internet";
|
|
||||||
manifest.description = "direct access (or via ssh jumphost) to machines";
|
|
||||||
manifest.categories = [
|
|
||||||
"System"
|
|
||||||
"Network"
|
|
||||||
];
|
|
||||||
roles.default = {
|
|
||||||
interface =
|
|
||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
host = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = ''
|
|
||||||
ip address or hostname (domain) of the machine
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
jumphosts = lib.mkOption {
|
|
||||||
type = lib.types.listOf lib.types.str;
|
|
||||||
default = [ ];
|
|
||||||
description = ''
|
|
||||||
optional list of jumphosts to use to connect to the machine
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
perInstance =
|
|
||||||
{
|
|
||||||
roles,
|
|
||||||
lib,
|
|
||||||
settings,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
exports.networking = {
|
|
||||||
# TODO add user space network support to clan-cli
|
|
||||||
peers = lib.mapAttrs (_name: machine: {
|
|
||||||
host.plain = machine.settings.host;
|
|
||||||
SSHOptions = map (_x: "-J x") machine.settings.jumphosts;
|
|
||||||
}) roles.default.machines;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
let
|
|
||||||
module = lib.modules.importApply ./default.nix { };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
clan.modules = {
|
|
||||||
internet = module;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
{ ... }:
|
|
||||||
{
|
|
||||||
_class = "clan.service";
|
|
||||||
manifest.name = "clan-core/tor";
|
|
||||||
manifest.description = "Onion routing, use Hidden services to connect your machines";
|
|
||||||
manifest.categories = [
|
|
||||||
"System"
|
|
||||||
"Network"
|
|
||||||
];
|
|
||||||
|
|
||||||
roles.client = {
|
|
||||||
perInstance =
|
|
||||||
{
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
nixosModule =
|
|
||||||
{
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
config = {
|
|
||||||
services.tor = {
|
|
||||||
enable = true;
|
|
||||||
torsocks.enable = true;
|
|
||||||
client.enable = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
roles.server = {
|
|
||||||
# interface =
|
|
||||||
# { lib, ... }:
|
|
||||||
# {
|
|
||||||
# options = {
|
|
||||||
# OciSettings = lib.mkOption {
|
|
||||||
# type = lib.types.raw;
|
|
||||||
# default = null;
|
|
||||||
# description = "NixOS settings for virtualisation.oci-container.<name>.settings";
|
|
||||||
# };
|
|
||||||
# buildContainer = lib.mkOption {
|
|
||||||
# type = lib.types.nullOr lib.types.str;
|
|
||||||
# default = null;
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
perInstance =
|
|
||||||
{
|
|
||||||
instanceName,
|
|
||||||
roles,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
exports.networking = {
|
|
||||||
priority = lib.mkDefault 10;
|
|
||||||
# TODO add user space network support to clan-cli
|
|
||||||
module = "clan_lib.network.tor";
|
|
||||||
peers = lib.mapAttrs (name: machine: {
|
|
||||||
host.var = {
|
|
||||||
machine = name;
|
|
||||||
generator = "tor_${instanceName}";
|
|
||||||
file = "hostname";
|
|
||||||
};
|
|
||||||
}) roles.server.machines;
|
|
||||||
};
|
|
||||||
nixosModule =
|
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
config,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
config = {
|
|
||||||
services.tor = {
|
|
||||||
enable = true;
|
|
||||||
relay.onionServices."clan_${instanceName}" = {
|
|
||||||
version = 3;
|
|
||||||
# TODO get ports from instance machine config
|
|
||||||
map = [
|
|
||||||
{
|
|
||||||
port = 22;
|
|
||||||
target.port = 22;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
secretKey = config.clan.core.vars.generators."tor_${instanceName}".files.hs_ed25519_secret_key.path;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
clan.core.vars.generators."tor_${instanceName}" = {
|
|
||||||
files.hs_ed25519_secret_key = { };
|
|
||||||
files.hostname = { };
|
|
||||||
runtimeInputs = with pkgs; [
|
|
||||||
coreutils
|
|
||||||
tor
|
|
||||||
];
|
|
||||||
script = ''
|
|
||||||
mkdir -p data
|
|
||||||
echo -e "DataDirectory ./data\nSocksPort 0\nHiddenServiceDir ./hs\nHiddenServicePort 80 127.0.0.1:80" > torrc
|
|
||||||
timeout 2 tor -f torrc || :
|
|
||||||
mv hs/hs_ed25519_secret_key $out/hs_ed25519_secret_key
|
|
||||||
mv hs/hostname $out/hostname
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
let
|
|
||||||
module = lib.modules.importApply ./default.nix { };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
clan.modules = {
|
|
||||||
tor = module;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
perInstance =
|
perInstance =
|
||||||
{ instanceName, settings, ... }:
|
{ settings, ... }:
|
||||||
{
|
{
|
||||||
nixosModule =
|
nixosModule =
|
||||||
{ pkgs, config, ... }:
|
{ pkgs, config, ... }:
|
||||||
@@ -86,7 +86,7 @@ in
|
|||||||
|
|
||||||
# service to generate the environment file containing all secrets, as
|
# service to generate the environment file containing all secrets, as
|
||||||
# expected by the nixos NetworkManager-ensure-profile service
|
# expected by the nixos NetworkManager-ensure-profile service
|
||||||
systemd.services."NetworkManager-setup-secrets-${instanceName}" = {
|
systemd.services.NetworkManager-setup-secrets = {
|
||||||
description = "Generate wifi secrets for NetworkManager";
|
description = "Generate wifi secrets for NetworkManager";
|
||||||
requiredBy = [ "NetworkManager-ensure-profiles.service" ];
|
requiredBy = [ "NetworkManager-ensure-profiles.service" ];
|
||||||
partOf = [ "NetworkManager-ensure-profiles.service" ];
|
partOf = [ "NetworkManager-ensure-profiles.service" ];
|
||||||
|
|||||||
@@ -7,16 +7,8 @@
|
|||||||
inventory = {
|
inventory = {
|
||||||
|
|
||||||
machines.test = { };
|
machines.test = { };
|
||||||
machines.second = { };
|
|
||||||
|
|
||||||
instances = {
|
instances = {
|
||||||
wg-test-all = {
|
|
||||||
module.name = "@clan/wifi";
|
|
||||||
module.input = "self";
|
|
||||||
roles.default.tags.all = { };
|
|
||||||
roles.default.settings.networks.all = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
wg-test-one = {
|
wg-test-one = {
|
||||||
module.name = "@clan/wifi";
|
module.name = "@clan/wifi";
|
||||||
module.input = "self";
|
module.input = "self";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
clanOptions = self'.legacyPackages.clan-internals-docs;
|
buildClanOptions = self'.legacyPackages.clan-internals-docs;
|
||||||
# Simply evaluated options (JSON)
|
# Simply evaluated options (JSON)
|
||||||
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
|
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
|
||||||
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
|
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
# Frontmatter format for clanModules
|
# Frontmatter format for clanModules
|
||||||
export CLAN_MODULES_FRONTMATTER_DOCS=${clanModulesFrontmatter}/share/doc/nixos/options.json
|
export CLAN_MODULES_FRONTMATTER_DOCS=${clanModulesFrontmatter}/share/doc/nixos/options.json
|
||||||
|
|
||||||
export BUILD_CLAN_PATH=${clanOptions}/share/doc/nixos/options.json
|
export BUILD_CLAN_PATH=${buildClanOptions}/share/doc/nixos/options.json
|
||||||
|
|
||||||
mkdir $out
|
mkdir $out
|
||||||
|
|
||||||
|
|||||||
@@ -465,10 +465,6 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
|
|||||||
service_links: dict[str, dict[str, dict[str, Any]]] = json.load(f3)
|
service_links: dict[str, dict[str, dict[str, Any]]] = json.load(f3)
|
||||||
|
|
||||||
for module_name, module_info in service_links.items():
|
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"# {module_name}\n\n"
|
||||||
# output += f"`clan.modules.{module_name}`\n"
|
# output += f"`clan.modules.{module_name}`\n"
|
||||||
output += f"*{module_info['manifest']['description']}*\n"
|
output += f"*{module_info['manifest']['description']}*\n"
|
||||||
|
|||||||
@@ -35,37 +35,6 @@ services = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Complex Example: Multi-service Setup
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# Old format
|
|
||||||
services = {
|
|
||||||
borgbackup.production = {
|
|
||||||
roles.server.machines = [ "backup-server" ];
|
|
||||||
roles.server.config = {
|
|
||||||
directory = "/var/backup/borg";
|
|
||||||
};
|
|
||||||
roles.client.tags = [ "backup" ];
|
|
||||||
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
zerotier.company-network = {
|
|
||||||
roles.controller.machines = [ "network-controller" ];
|
|
||||||
roles.moon.machines = [ "moon-1" "moon-2" ];
|
|
||||||
roles.peer.tags = [ "nixos" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
sshd.internal = {
|
|
||||||
roles.server.tags = [ "nixos" ];
|
|
||||||
roles.client.tags = [ "nixos" ];
|
|
||||||
config.certificate.searchDomains = [
|
|
||||||
"internal.example.com"
|
|
||||||
"vpn.example.com"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ After: New `instances` Definition with `clanServices`
|
## ✅ After: New `instances` Definition with `clanServices`
|
||||||
@@ -101,56 +70,6 @@ instances = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Complex Example Migrated
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# New format
|
|
||||||
instances = {
|
|
||||||
borgbackup-production = {
|
|
||||||
module = {
|
|
||||||
name = "borgbackup";
|
|
||||||
input = "clan-core";
|
|
||||||
};
|
|
||||||
roles.server.machines."backup-server" = { };
|
|
||||||
roles.server.settings = {
|
|
||||||
directory = "/var/backup/borg";
|
|
||||||
};
|
|
||||||
roles.client.tags.backup = { };
|
|
||||||
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
|
|
||||||
};
|
|
||||||
|
|
||||||
zerotier-company-network = {
|
|
||||||
module = {
|
|
||||||
name = "zerotier";
|
|
||||||
input = "clan-core";
|
|
||||||
};
|
|
||||||
roles.controller.machines."network-controller" = { };
|
|
||||||
roles.moon.machines."moon-1".settings = {
|
|
||||||
stableEndpoints = [ "10.0.0.1" "2001:db8::1" ];
|
|
||||||
};
|
|
||||||
roles.moon.machines."moon-2".settings = {
|
|
||||||
stableEndpoints = [ "10.0.0.2" "2001:db8::2" ];
|
|
||||||
};
|
|
||||||
roles.peer.tags.nixos = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
sshd-internal = {
|
|
||||||
module = {
|
|
||||||
name = "sshd";
|
|
||||||
input = "clan-core";
|
|
||||||
};
|
|
||||||
roles.server.tags.nixos = { };
|
|
||||||
roles.client.tags.nixos = { };
|
|
||||||
roles.client.settings = {
|
|
||||||
certificate.searchDomains = [
|
|
||||||
"internal.example.com"
|
|
||||||
"vpn.example.com"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Steps to Migrate
|
## Steps to Migrate
|
||||||
@@ -212,33 +131,6 @@ roles.default.machines."test-inventory-machine".settings = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Important Type Changes
|
|
||||||
|
|
||||||
The new `instances` format uses **attribute sets** instead of **lists** for tags and machines:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# ❌ Old format (lists)
|
|
||||||
roles.client.tags = [ "backup" ];
|
|
||||||
roles.server.machines = [ "blob64" ];
|
|
||||||
|
|
||||||
# ✅ New format (attribute sets)
|
|
||||||
roles.client.tags.backup = { };
|
|
||||||
roles.server.machines.blob64 = { };
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handling Multiple Machines/Tags
|
|
||||||
|
|
||||||
When you need to assign multiple machines or tags to a role:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# ❌ Old format
|
|
||||||
roles.moon.machines = [ "eva" "eve" ];
|
|
||||||
|
|
||||||
# ✅ New format - each machine gets its own attribute
|
|
||||||
roles.moon.machines.eva = { };
|
|
||||||
roles.moon.machines.eve = { };
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
@@ -246,87 +138,6 @@ roles.moon.machines.eve = { };
|
|||||||
* `inventory.services` is no longer recommended; use `inventory.instances` instead.
|
* `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.
|
* 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
|
## Further reference
|
||||||
|
|
||||||
* [Authoring a 'clan.service' module](../authoring/clanServices/index.md)
|
* [Authoring a 'clan.service' module](../authoring/clanServices/index.md)
|
||||||
|
|||||||
20
flake.lock
generated
20
flake.lock
generated
@@ -13,11 +13,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1753067306,
|
"lastModified": 1752589312,
|
||||||
"narHash": "sha256-jyoEbaXa8/MwVQ+PajUdT63y3gYhgD9o7snO/SLaikw=",
|
"narHash": "sha256-BafZOenlzMYdumG12AzgVLhEVu+GcEa8nYNDSIYe1U0=",
|
||||||
"rev": "18dfd42bdb2cfff510b8c74206005f733e38d8b9",
|
"rev": "496bbf05a2aa7b061ef464254db5804d1c6f45b4",
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/18dfd42bdb2cfff510b8c74206005f733e38d8b9.tar.gz"
|
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/496bbf05a2aa7b061ef464254db5804d1c6f45b4.tar.gz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
@@ -31,11 +31,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1752718651,
|
"lastModified": 1752541678,
|
||||||
"narHash": "sha256-PkaR0qmyP9q/MDN3uYa+RLeBA0PjvEQiM0rTDDBXkL8=",
|
"narHash": "sha256-dyhGzkld6jPqnT/UfGV2oqe7tYn7hppAqFvF3GZTyXY=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "d5ad4485e6f2edcc06751df65c5e16572877db88",
|
"rev": "2bf3421f7fed5c84d9392b62dcb9d76ef09796a7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -181,11 +181,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1753006367,
|
"lastModified": 1752055615,
|
||||||
"narHash": "sha256-tzbhc4XttkyEhswByk5R38l+ztN9UDbnj0cTcP6Hp9A=",
|
"narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "421b56313c65a0815a52b424777f55acf0b56ddf",
|
"rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
inputs = {
|
inputs = {
|
||||||
flake-parts.follows = "flake-parts";
|
flake-parts.follows = "flake-parts";
|
||||||
nixpkgs.follows = "nixpkgs";
|
nixpkgs.follows = "nixpkgs";
|
||||||
|
systems.follows = "systems";
|
||||||
treefmt-nix.follows = "treefmt-nix";
|
treefmt-nix.follows = "treefmt-nix";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ lib.fix (
|
|||||||
{
|
{
|
||||||
|
|
||||||
inherit (buildClanLib)
|
inherit (buildClanLib)
|
||||||
|
buildClan
|
||||||
clan
|
clan
|
||||||
;
|
;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -78,87 +78,7 @@ in
|
|||||||
internal = true;
|
internal = true;
|
||||||
visible = false;
|
visible = false;
|
||||||
type = types.deferredModule;
|
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 = ''
|
description = ''
|
||||||
A module that is used to define the module of flake level exports -
|
A module that is used to define the module of flake level exports -
|
||||||
|
|
||||||
@@ -355,6 +275,8 @@ in
|
|||||||
templates = lib.mkOption { type = lib.types.raw; };
|
templates = lib.mkOption { type = lib.types.raw; };
|
||||||
|
|
||||||
machines = lib.mkOption { type = lib.types.raw; };
|
machines = lib.mkOption { type = lib.types.raw; };
|
||||||
|
|
||||||
|
clan-cli = lib.mkOption { type = lib.types.raw; };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -273,6 +273,9 @@ in
|
|||||||
|
|
||||||
# machine specifics
|
# machine specifics
|
||||||
machines = configsPerSystem;
|
machines = configsPerSystem;
|
||||||
|
|
||||||
|
# export clan-cli in clanInternals to tie the CLI to the flake
|
||||||
|
clan-cli = builtins.mapAttrs (_sys: pkgs: pkgs.clan-cli) clan-core.packages;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
clan-core,
|
clan-core,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
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;
|
||||||
|
|
||||||
clan =
|
clan =
|
||||||
{
|
{
|
||||||
self ? lib.warn "Argument: 'self' must be set" null, # Reference to the current flake
|
self ? lib.warn "Argument: 'self' must be set" null, # Reference to the current flake
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ in
|
|||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
instances = lib.mkOption {
|
instances = lib.mkOption {
|
||||||
default = { };
|
|
||||||
# instances.<instanceName>...
|
# instances.<instanceName>...
|
||||||
type = types.attrsOf (submoduleWith {
|
type = types.attrsOf (submoduleWith {
|
||||||
modules = [
|
modules = [
|
||||||
@@ -58,7 +57,6 @@ in
|
|||||||
};
|
};
|
||||||
# instances.<machineName>...
|
# instances.<machineName>...
|
||||||
machines = lib.mkOption {
|
machines = lib.mkOption {
|
||||||
default = { };
|
|
||||||
type = types.attrsOf (submoduleWith {
|
type = types.attrsOf (submoduleWith {
|
||||||
modules = [
|
modules = [
|
||||||
config.exportsModule
|
config.exportsModule
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ def _get_lib_names() -> list[str]:
|
|||||||
msg = f"Unsupported architecture: {machine}"
|
msg = f"Unsupported architecture: {machine}"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
if system == "darwin":
|
if system == "darwin":
|
||||||
|
if machine == "arm64":
|
||||||
return ["libwebview.dylib"]
|
return ["libwebview.dylib"]
|
||||||
|
msg = "Not supported"
|
||||||
|
raise RuntimeError(msg)
|
||||||
# linux
|
# linux
|
||||||
return ["libwebview.so"]
|
return ["libwebview.so"]
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,6 @@ buildNpmPackage (_finalAttrs: {
|
|||||||
mkdir -p api
|
mkdir -p api
|
||||||
cp -r ${clan-ts-api}/* api
|
cp -r ${clan-ts-api}/* api
|
||||||
cp -r ${fonts} ".fonts"
|
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
|
# 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 = {
|
const config: StorybookConfig = {
|
||||||
framework: "@kachurun/storybook-solid-vite",
|
framework: "@kachurun/storybook-solid-vite",
|
||||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"],
|
stories: ["../src/components/**/*.mdx", "../src/components/**/*.stories.tsx"],
|
||||||
addons: [
|
addons: [
|
||||||
"@storybook/addon-links",
|
"@storybook/addon-links",
|
||||||
"@storybook/addon-docs",
|
"@storybook/addon-docs",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
@@ -138,10 +138,6 @@
|
|||||||
transition: all 0.5s ease;
|
transition: all 0.5s ease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > span.typography {
|
|
||||||
@apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* button group */
|
/* button group */
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { TextInput } from "@/src/components/Form/TextInput";
|
|||||||
import { TextArea } from "@/src/components/Form/TextArea";
|
import { TextArea } from "@/src/components/Form/TextArea";
|
||||||
import { Checkbox } from "@/src/components/Form/Checkbox";
|
import { Checkbox } from "@/src/components/Form/Checkbox";
|
||||||
import { FieldProps } from "./Field";
|
import { FieldProps } from "./Field";
|
||||||
import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
|
||||||
|
|
||||||
const FieldsetExamples = (props: FieldsetProps) => (
|
const FieldsetExamples = (props: FieldsetProps) => (
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
@@ -27,7 +26,7 @@ const meta = {
|
|||||||
<div
|
<div
|
||||||
class={cx({
|
class={cx({
|
||||||
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
|
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
|
||||||
"w-[512px]": context.args.orientation == "horizontal",
|
"w-[1024px]": context.args.orientation == "horizontal",
|
||||||
"bg-inv-acc-3": context.args.inverted,
|
"bg-inv-acc-3": context.args.inverted,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@@ -64,11 +63,6 @@ export const Default: Story = {
|
|||||||
label="Bio"
|
label="Bio"
|
||||||
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
|
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} />
|
<Checkbox {...props} label="Accept Terms" required={true} />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
div.form-field.host-file {
|
div.form-field.host-file {
|
||||||
button {
|
button {
|
||||||
@apply w-fit;
|
@apply w-1/2;
|
||||||
}
|
|
||||||
|
|
||||||
&.horizontal {
|
|
||||||
button {
|
|
||||||
@apply grow max-w-[18rem];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export type Story = StoryObj<typeof meta>;
|
|||||||
export const Bare: Story = {
|
export const Bare: Story = {
|
||||||
args: {
|
args: {
|
||||||
onSelectFile: async () => {
|
onSelectFile: async () => {
|
||||||
return "/home/github/clans/my-clan/foo/bar/baz/fizz/buzz";
|
return "/home/bob/clans/my-clan";
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
placeholder: "e.g. 11/06/89",
|
placeholder: "e.g. 11/06/89",
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
|||||||
import { FieldProps } from "./Field";
|
import { FieldProps } from "./Field";
|
||||||
import { Orienter } from "./Orienter";
|
import { Orienter } from "./Orienter";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import { Tooltip } from "@kobalte/core/tooltip";
|
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
|
||||||
|
|
||||||
export type HostFileInputProps = FieldProps &
|
export type HostFileInputProps = FieldProps &
|
||||||
TextFieldRootProps & {
|
TextFieldRootProps & {
|
||||||
@@ -22,21 +20,10 @@ export type HostFileInputProps = FieldProps &
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const HostFileInput = (props: HostFileInputProps) => {
|
export const HostFileInput = (props: HostFileInputProps) => {
|
||||||
const [value, setValue] = createSignal<string>(props.value || "");
|
const [value, setValue] = createSignal<string | undefined>(undefined);
|
||||||
|
|
||||||
let actualInputElement: HTMLInputElement | undefined;
|
|
||||||
|
|
||||||
const selectFile = async () => {
|
const selectFile = async () => {
|
||||||
try {
|
|
||||||
console.log("selecting file", props.onSelectFile);
|
|
||||||
setValue(await 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 (
|
return (
|
||||||
@@ -46,65 +33,26 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
|||||||
ghost: props.ghost,
|
ghost: props.ghost,
|
||||||
})}
|
})}
|
||||||
{...props}
|
{...props}
|
||||||
|
value={value()}
|
||||||
|
onChange={setValue}
|
||||||
>
|
>
|
||||||
<Orienter
|
<Orienter orientation={props.orientation} align={"start"}>
|
||||||
orientation={props.orientation}
|
|
||||||
align={props.orientation == "horizontal" ? "center" : "start"}
|
|
||||||
>
|
|
||||||
<Label
|
<Label
|
||||||
labelComponent={TextField.Label}
|
labelComponent={TextField.Label}
|
||||||
descriptionComponent={TextField.Description}
|
descriptionComponent={TextField.Description}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField.Input
|
<TextField.Input {...props.input} hidden={true} />
|
||||||
{...props.input}
|
|
||||||
hidden={true}
|
|
||||||
value={value()}
|
|
||||||
ref={(el: HTMLInputElement) => {
|
|
||||||
actualInputElement = el; // Capture for local use
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!value() && (
|
|
||||||
<Button
|
<Button
|
||||||
hierarchy="secondary"
|
hierarchy="secondary"
|
||||||
size={props.size}
|
size={props.size}
|
||||||
startIcon="Folder"
|
startIcon="Folder"
|
||||||
onClick={selectFile}
|
onClick={selectFile}
|
||||||
disabled={props.disabled || props.readOnly}
|
|
||||||
>
|
>
|
||||||
No Selection
|
{value() ? value() : "No Selection"}
|
||||||
</Button>
|
</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>
|
</Orienter>
|
||||||
</TextField>
|
</TextField>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,3 +22,40 @@ 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,11 +1,12 @@
|
|||||||
import { Show } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
import { Tooltip as KTooltip } from "@kobalte/core/tooltip";
|
||||||
import Icon from "@/src/components/Icon/Icon";
|
import Icon from "@/src/components/Icon/Icon";
|
||||||
import { TextField } from "@kobalte/core/text-field";
|
import { TextField } from "@kobalte/core/text-field";
|
||||||
import { Checkbox } from "@kobalte/core/checkbox";
|
import { Checkbox } from "@kobalte/core/checkbox";
|
||||||
import { Combobox } from "@kobalte/core/combobox";
|
import { Combobox } from "@kobalte/core/combobox";
|
||||||
import "./Label.css";
|
import "./Label.css";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
export type Size = "default" | "s";
|
export type Size = "default" | "s";
|
||||||
|
|
||||||
@@ -48,17 +49,17 @@ export const Label = (props: LabelProps) => {
|
|||||||
{props.label}
|
{props.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
{props.tooltip && (
|
{props.tooltip && (
|
||||||
<Tooltip
|
<KTooltip placement="top">
|
||||||
placement="top"
|
<KTooltip.Trigger>
|
||||||
inverted={props.inverted}
|
|
||||||
trigger={
|
|
||||||
<Icon
|
<Icon
|
||||||
icon="Info"
|
icon="Info"
|
||||||
color="tertiary"
|
color="tertiary"
|
||||||
inverted={props.inverted}
|
inverted={props.inverted}
|
||||||
size={props.size == "default" ? "0.85em" : "0.75rem"}
|
size={props.size == "default" ? "0.85em" : "0.75rem"}
|
||||||
/>
|
/>
|
||||||
}
|
<KTooltip.Portal>
|
||||||
|
<KTooltip.Content
|
||||||
|
class={cx("tooltip-content", { inverted: props.inverted })}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
@@ -68,7 +69,11 @@ export const Label = (props: LabelProps) => {
|
|||||||
>
|
>
|
||||||
{props.tooltip}
|
{props.tooltip}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
<KTooltip.Arrow />
|
||||||
|
</KTooltip.Content>
|
||||||
|
</KTooltip.Portal>
|
||||||
|
</KTooltip.Trigger>
|
||||||
|
</KTooltip>
|
||||||
)}
|
)}
|
||||||
</props.labelComponent>
|
</props.labelComponent>
|
||||||
{props.description && (
|
{props.description && (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ div.orienter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
@apply flex-row justify-between gap-0;
|
@apply flex-row justify-start;
|
||||||
|
|
||||||
& > div.form-label {
|
& > div.form-label {
|
||||||
@apply w-1/2 shrink;
|
@apply w-1/2 shrink;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
div.modal-content {
|
div.modal-content {
|
||||||
@apply min-w-[320px] max-w-[512px];
|
@apply max-w-[512px];
|
||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
|
|
||||||
/* todo replace with a theme() color */
|
/* 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 border-def-2 rounded-tl-md rounded-tr-md;
|
||||||
@apply border-b-def-3;
|
@apply border-b-def-3;
|
||||||
|
|
||||||
& > .modal-title {
|
& > .title {
|
||||||
@apply mx-auto;
|
@apply mx-auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Dialog as KDialog } from "@kobalte/core/dialog";
|
|||||||
import "./Modal.css";
|
import "./Modal.css";
|
||||||
import { Typography } from "../Typography/Typography";
|
import { Typography } from "../Typography/Typography";
|
||||||
import Icon from "../Icon/Icon";
|
import Icon from "../Icon/Icon";
|
||||||
import cx from "classnames";
|
|
||||||
|
|
||||||
export interface ModalContext {
|
export interface ModalContext {
|
||||||
close(): void;
|
close(): void;
|
||||||
@@ -14,8 +13,6 @@ export interface ModalProps {
|
|||||||
title: string;
|
title: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
children: (ctx: ModalContext) => JSX.Element;
|
children: (ctx: ModalContext) => JSX.Element;
|
||||||
mount?: Node;
|
|
||||||
class?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Modal = (props: ModalProps) => {
|
export const Modal = (props: ModalProps) => {
|
||||||
@@ -23,33 +20,18 @@ export const Modal = (props: ModalProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<KDialog id={props.id} open={open()} modal={true}>
|
<KDialog id={props.id} open={open()} modal={true}>
|
||||||
<KDialog.Portal mount={props.mount}>
|
<KDialog.Portal>
|
||||||
<KDialog.Content class={cx("modal-content", props.class)}>
|
<KDialog.Content class="modal-content">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<Typography
|
<Typography class="title" hierarchy="label" family="mono" size="xs">
|
||||||
class="modal-title"
|
|
||||||
hierarchy="label"
|
|
||||||
family="mono"
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
{props.title}
|
{props.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<KDialog.CloseButton
|
<KDialog.CloseButton onClick={() => setOpen(false)}>
|
||||||
onClick={() => {
|
|
||||||
setOpen(false);
|
|
||||||
props.onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="Close" size="0.75rem" />
|
<Icon icon="Close" size="0.75rem" />
|
||||||
</KDialog.CloseButton>
|
</KDialog.CloseButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
{props.children({
|
{props.children({ close: () => setOpen(false) })}
|
||||||
close: () => {
|
|
||||||
setOpen(false);
|
|
||||||
props.onClose();
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</KDialog.Content>
|
</KDialog.Content>
|
||||||
</KDialog.Portal>
|
</KDialog.Portal>
|
||||||
|
|||||||
@@ -14,35 +14,35 @@ import { StoryContext } from "@kachurun/storybook-solid-vite";
|
|||||||
|
|
||||||
const sidebarNavProps: SidebarNavProps = {
|
const sidebarNavProps: SidebarNavProps = {
|
||||||
clanLinks: [
|
clanLinks: [
|
||||||
{ label: "Brian's Clan", path: "/clans/1" },
|
{ label: "Brian's Clan", path: "/clan/1" },
|
||||||
{ label: "Dave's Clan", path: "/clans/2" },
|
{ label: "Dave's Clan", path: "/clan/2" },
|
||||||
{ label: "Mic92's Clan", path: "/clans/3" },
|
{ label: "Mic92's Clan", path: "/clan/3" },
|
||||||
],
|
],
|
||||||
clanDetail: {
|
clanDetail: {
|
||||||
label: "Brian's Clan",
|
label: "Brian's Clan",
|
||||||
settingsPath: "/clans/1/settings",
|
settingsPath: "/clan/1/settings",
|
||||||
machines: [
|
machines: [
|
||||||
{
|
{
|
||||||
label: "Backup & Home",
|
label: "Backup & Home",
|
||||||
path: "/clans/1/machine/backup",
|
path: "/clan/1/machine/backup",
|
||||||
serviceCount: 3,
|
serviceCount: 3,
|
||||||
status: "Online",
|
status: "Online",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Raspberry Pi",
|
label: "Raspberry Pi",
|
||||||
path: "/clans/1/machine/pi",
|
path: "/clan/1/machine/pi",
|
||||||
serviceCount: 1,
|
serviceCount: 1,
|
||||||
status: "Offline",
|
status: "Offline",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Mom's Laptop",
|
label: "Mom's Laptop",
|
||||||
path: "/clans/1/machine/moms-laptop",
|
path: "/clan/1/machine/moms-laptop",
|
||||||
serviceCount: 2,
|
serviceCount: 2,
|
||||||
status: "Installed",
|
status: "Installed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Dad's Laptop",
|
label: "Dad's Laptop",
|
||||||
path: "/clans/1/machine/dads-laptop",
|
path: "/clan/1/machine/dads-laptop",
|
||||||
serviceCount: 4,
|
serviceCount: 4,
|
||||||
status: "Not Installed",
|
status: "Not Installed",
|
||||||
},
|
},
|
||||||
@@ -52,10 +52,10 @@ const sidebarNavProps: SidebarNavProps = {
|
|||||||
{
|
{
|
||||||
label: "Tools",
|
label: "Tools",
|
||||||
links: [
|
links: [
|
||||||
{ label: "Borgbackup", path: "/clans/1/service/borgbackup" },
|
{ label: "Borgbackup", path: "/clan/1/service/borgbackup" },
|
||||||
{ label: "Syncthing", path: "/clans/1/service/syncthing" },
|
{ label: "Syncthing", path: "/clan/1/service/syncthing" },
|
||||||
{ label: "Mumble", path: "/clans/1/service/mumble" },
|
{ label: "Mumble", path: "/clan/1/service/mumble" },
|
||||||
{ label: "Minecraft", path: "/clans/1/service/minecraft" },
|
{ label: "Minecraft", path: "/clan/1/service/minecraft" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,7 +81,7 @@ const meta: Meta<RouteSectionProps> = {
|
|||||||
component: SidebarNav,
|
component: SidebarNav,
|
||||||
render: (_: never, context: StoryContext<SidebarNavProps>) => {
|
render: (_: never, context: StoryContext<SidebarNavProps>) => {
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
history.set({ value: "/clans/1/machine/backup" });
|
history.set({ value: "/clan/1/machine/backup" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style="height: 670px;">
|
<div style="height: 670px;">
|
||||||
@@ -93,7 +93,7 @@ const meta: Meta<RouteSectionProps> = {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Route path="/clans/1/machine/backup" component={(props) => <></>} />
|
<Route path="/clan/1/machine/backup" component={(props) => <></>} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -42,7 +42,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<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.
|
* @property {() => Promise<void>} cancel - A function to cancel the API call, returning a promise that resolves when cancellation is completed.
|
||||||
*/
|
*/
|
||||||
export interface ApiCall<K extends OperationNames> {
|
interface ApiCall<K extends OperationNames> {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
result: Promise<OperationResponse<K>>;
|
result: Promise<OperationResponse<K>>;
|
||||||
cancel: () => Promise<void>;
|
cancel: () => Promise<void>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { callApi } from "@/src/hooks/api";
|
import { callApi } from "@/src/hooks/api";
|
||||||
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
||||||
import { Params, Navigator, useParams } from "@solidjs/router";
|
import { Params, Navigator } from "@solidjs/router";
|
||||||
|
|
||||||
export const selectClanFolder = async () => {
|
export const selectClanFolder = async () => {
|
||||||
const req = callApi("get_clan_folder", {});
|
const req = callApi("get_clan_folder", {});
|
||||||
@@ -21,37 +21,9 @@ export const selectClanFolder = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const navigateToClan = (navigate: Navigator, uri: string) => {
|
export const navigateToClan = (navigate: Navigator, uri: string) => {
|
||||||
navigate("/clans/" + window.btoa(uri));
|
navigate("/clan/" + window.btoa(uri));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clanURIParam = (params: Params) => {
|
export const clanURIParam = (params: Params) => {
|
||||||
return window.atob(params.clanURI);
|
return window.atob(params.clanURI);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useClanURI(opts: { force: true }): string;
|
|
||||||
export function useClanURI(opts: { force: boolean }): string | null;
|
|
||||||
export function useClanURI(
|
|
||||||
opts: { force: boolean } = { force: false },
|
|
||||||
): string | null {
|
|
||||||
const maybe = () => {
|
|
||||||
const params = useParams();
|
|
||||||
if (!params.clanURI) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const clanURI = clanURIParam(params);
|
|
||||||
if (!clanURI) {
|
|
||||||
throw new Error(
|
|
||||||
"Could not decode clan URI from params: " + params.clanURI,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return clanURI;
|
|
||||||
};
|
|
||||||
|
|
||||||
const uri = maybe();
|
|
||||||
if (!uri && opts.force) {
|
|
||||||
throw new Error(
|
|
||||||
"ClanURI is not set. Use this function only within contexts, where clanURI is guaranteed to have been set.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
import { QueryClient } from "@tanstack/solid-query";
|
||||||
import { Routes } from "@/src/routes";
|
import { Routes } from "@/src/routes";
|
||||||
import { Router } from "@solidjs/router";
|
import { Router } from "@solidjs/router";
|
||||||
import { Layout } from "@/src/routes/Layout";
|
import { Layout } from "@/src/routes/Layout";
|
||||||
@@ -22,11 +22,4 @@ if (import.meta.env.DEV) {
|
|||||||
await import("solid-devtools");
|
await import("solid-devtools");
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(() => <Router root={Layout}>{Routes}</Router>, root!);
|
||||||
() => (
|
|
||||||
<QueryClientProvider client={client}>
|
|
||||||
<Router root={Layout}>{Routes}</Router>
|
|
||||||
</QueryClientProvider>
|
|
||||||
),
|
|
||||||
root!,
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { useQuery, UseQueryResult } from "@tanstack/solid-query";
|
|
||||||
import { callApi, SuccessData } from "../hooks/api";
|
|
||||||
|
|
||||||
export type ListMachines = SuccessData<"list_machines">;
|
|
||||||
|
|
||||||
export type MachinesQueryResult = UseQueryResult<ListMachines>;
|
|
||||||
|
|
||||||
interface MachinesQueryParams {
|
|
||||||
clanURI: string | null;
|
|
||||||
}
|
|
||||||
export const useMachinesQuery = (props: MachinesQueryParams) =>
|
|
||||||
useQuery<ListMachines>(() => ({
|
|
||||||
queryKey: ["clans", props.clanURI, "machines"],
|
|
||||||
enabled: !!props.clanURI,
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!props.clanURI) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const api = callApi("list_machines", {
|
|
||||||
flake: {
|
|
||||||
identifier: props.clanURI,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result = await api.result;
|
|
||||||
if (result.status === "error") {
|
|
||||||
console.error("Error fetching machines:", result.errors);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return result.data;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
.fade-out {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-backdrop {
|
|
||||||
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50;
|
|
||||||
-webkit-backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-modal {
|
|
||||||
@apply min-w-96;
|
|
||||||
}
|
|
||||||
@@ -1,231 +1,10 @@
|
|||||||
import { RouteSectionProps } from "@solidjs/router";
|
import { RouteSectionProps, useParams } from "@solidjs/router";
|
||||||
import { Component, JSX, Show, createSignal } from "solid-js";
|
import { Component } from "solid-js";
|
||||||
import { useClanURI } from "@/src/hooks/clan";
|
import { clanURIParam } from "@/src/hooks/clan";
|
||||||
import { CubeScene } from "@/src/scene/cubes";
|
import { CubeScene } from "@/src/scene/cubes";
|
||||||
import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries";
|
|
||||||
import { callApi } from "@/src/hooks/api";
|
|
||||||
import { store, setStore } 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";
|
|
||||||
|
|
||||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||||
return (
|
const params = useParams();
|
||||||
<>
|
const clanURI = clanURIParam(params);
|
||||||
<div
|
return <CubeScene />;
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
<ClanSceneController />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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="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 = () => {
|
|
||||||
const clanURI = useClanURI({ force: true });
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SceneDataProvider clanURI={clanURI}>
|
|
||||||
{({ query }) => {
|
|
||||||
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] = {}; // Clear the entire object
|
|
||||||
// delete s.sceneData[clanURI][machineId];
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset Store
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
ghost
|
|
||||||
onClick={() => {
|
|
||||||
console.log("Refetching API");
|
|
||||||
query.refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Refetch API
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/* TODO: Add minimal display time */}
|
|
||||||
<div class={cx({ "fade-out": !query.isLoading })}>
|
|
||||||
<Splash />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CubeScene
|
|
||||||
isLoading={query.isLoading}
|
|
||||||
cubesQuery={query}
|
|
||||||
onCreate={onCreate}
|
|
||||||
sceneStore={() => {
|
|
||||||
const clanURI = useClanURI({ force: true });
|
|
||||||
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 | null;
|
|
||||||
children: (sceneData: { query: MachinesQueryResult }) => JSX.Element;
|
|
||||||
}) => {
|
|
||||||
const machinesQuery = useMachinesQuery({ clanURI: props.clanURI });
|
|
||||||
|
|
||||||
// This component can be used to provide scene data or context if needed
|
|
||||||
return props.children({ query: machinesQuery });
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,6 @@
|
|||||||
import { Component } from "solid-js";
|
import { Component } from "solid-js";
|
||||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
import { RouteSectionProps } from "@solidjs/router";
|
||||||
import { activeClanURI } from "@/src/stores/clan";
|
|
||||||
import { navigateToClan } from "@/src/hooks/clan";
|
|
||||||
|
|
||||||
export const Layout: Component<RouteSectionProps> = (props) => {
|
export const Layout: Component<RouteSectionProps> = (props) => (
|
||||||
const navigate = useNavigate();
|
<div class="size-full h-screen">{props.children}</div>
|
||||||
|
);
|
||||||
// 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>;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,663 +0,0 @@
|
|||||||
div.creating {
|
|
||||||
@apply flex flex-col items-center justify-center;
|
|
||||||
|
|
||||||
div.scene {
|
|
||||||
width: 400px;
|
|
||||||
height: 400px;
|
|
||||||
perspective: 1000px;
|
|
||||||
/*background: red;*/
|
|
||||||
|
|
||||||
& > .frame {
|
|
||||||
position: relative;
|
|
||||||
top: 100px;
|
|
||||||
left: 65px;
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
/*background: green;*/
|
|
||||||
|
|
||||||
/*transform: rotate3d(-2, -2, 1, 45deg);*/
|
|
||||||
transform: rotate3d(-1.5, -2, 0.5, 45deg);
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
|
|
||||||
& > .cube {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
|
|
||||||
.cube-face {
|
|
||||||
position: absolute;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(255, 255, 255, 0.56) 0%,
|
|
||||||
rgba(255, 255, 255, 0) 100%
|
|
||||||
);
|
|
||||||
border: 1px #10191a solid;
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&.front {
|
|
||||||
transform: rotateY(0deg) translateZ(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.right {
|
|
||||||
transform: rotateY(90deg) translateZ(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.back {
|
|
||||||
transform: rotateY(180deg) translateZ(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.left {
|
|
||||||
transform: rotateY(-90deg) translateZ(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.top {
|
|
||||||
transform: rotateX(90deg) translateZ(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom {
|
|
||||||
transform: rotateX(-90deg) translateZ(50px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.state-1 {
|
|
||||||
animation: anim-cube-1-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
||||||
infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.state-1-1 {
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
animation: anim-cube-1-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
||||||
infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.state-2 {
|
|
||||||
left: 120px;
|
|
||||||
animation: anim-cube-2-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
||||||
infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.state-2-2 {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
animation: anim-cube-2-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
||||||
infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.state-3 {
|
|
||||||
top: 120px;
|
|
||||||
animation: anim-cube-3-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
||||||
infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.state-3-3 {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
animation: anim-cube-3-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
||||||
infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.state-4 {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
animation: anim-cube-4-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
||||||
infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.state-4-4 {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
animation: anim-cube-4-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
|
||||||
infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes anim-cube-1-1 {
|
|
||||||
/* STEP 1 */
|
|
||||||
0% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
2.083% {
|
|
||||||
left: -40px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
16.666% {
|
|
||||||
left: -40px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STEP 2 */
|
|
||||||
18.749% {
|
|
||||||
left: 0;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
33.332% {
|
|
||||||
left: 0;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STEP 3 */
|
|
||||||
35.415% {
|
|
||||||
left: 0;
|
|
||||||
transform: translateZ(40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
49.998% {
|
|
||||||
left: 0;
|
|
||||||
transform: translateZ(40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STEP 4 */
|
|
||||||
52.081% {
|
|
||||||
left: 0;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
66.664% {
|
|
||||||
left: 0;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Step 5 */
|
|
||||||
68.747% {
|
|
||||||
left: -60px;
|
|
||||||
transform: translateZ(60px);
|
|
||||||
}
|
|
||||||
83.33% {
|
|
||||||
left: -60px;
|
|
||||||
transform: translateZ(60px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Step 6 */
|
|
||||||
85.413% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes anim-cube-2-1 {
|
|
||||||
/* STEP 1 */
|
|
||||||
0% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
2.083% {
|
|
||||||
left: 180px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
16.666% {
|
|
||||||
left: 180px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STEP 2 */
|
|
||||||
18.749% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
33.332% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STEP 3 */
|
|
||||||
35.415% {
|
|
||||||
left: 240px;
|
|
||||||
transform: translateZ(120px);
|
|
||||||
}
|
|
||||||
|
|
||||||
49.998% {
|
|
||||||
left: 240px;
|
|
||||||
transform: translateZ(120px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STEP 4 */
|
|
||||||
52.081% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
66.664% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Step 5 */
|
|
||||||
68.747% {
|
|
||||||
left: 60px;
|
|
||||||
transform: translateZ(60px);
|
|
||||||
}
|
|
||||||
83.33% {
|
|
||||||
left: 60px;
|
|
||||||
transform: translateZ(60px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Step 6 */
|
|
||||||
85.413% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes anim-cube-3-1 {
|
|
||||||
/* STEP 1 */
|
|
||||||
0% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
2.083% {
|
|
||||||
top: 220px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
16.666% {
|
|
||||||
top: 220px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STEP 2 */
|
|
||||||
18.749% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
33.332% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STEP 3 */
|
|
||||||
35.415% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
49.998% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STEP 4 */
|
|
||||||
52.081% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
66.664% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Step 5 */
|
|
||||||
68.747% {
|
|
||||||
top: 180px;
|
|
||||||
transform: translateZ(80px);
|
|
||||||
}
|
|
||||||
83.33% {
|
|
||||||
top: 180px;
|
|
||||||
transform: translateZ(80px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Step 6 */
|
|
||||||
85.413% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes anim-cube-4-1 {
|
|
||||||
/* STEP 1 */
|
|
||||||
0% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
2.083% {
|
|
||||||
top: 220px;
|
|
||||||
left: 180px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
16.666% {
|
|
||||||
top: 220px;
|
|
||||||
left: 180px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
/* STEP 2 */
|
|
||||||
18.749% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
33.332% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
/* STEP 3 */
|
|
||||||
35.415% {
|
|
||||||
top: 120px;
|
|
||||||
left: 240px;
|
|
||||||
transform: translateZ(120px);
|
|
||||||
}
|
|
||||||
49.998% {
|
|
||||||
top: 120px;
|
|
||||||
left: 240px;
|
|
||||||
transform: translateZ(120px);
|
|
||||||
}
|
|
||||||
/* STEP 4 */
|
|
||||||
52.081% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
66.664% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
/* Step 5 */
|
|
||||||
68.747% {
|
|
||||||
top: 180px;
|
|
||||||
left: 260px;
|
|
||||||
transform: translateZ(80px);
|
|
||||||
}
|
|
||||||
83.33% {
|
|
||||||
top: 180px;
|
|
||||||
left: 260px;
|
|
||||||
transform: translateZ(80px);
|
|
||||||
}
|
|
||||||
/* Step 6 */
|
|
||||||
85.413% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(0px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes anim-cube-1-2 {
|
|
||||||
/* STEP 1 */
|
|
||||||
0% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
2.083% {
|
|
||||||
left: -40px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
16.666% {
|
|
||||||
left: -40px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* STEP 2 */
|
|
||||||
18.749% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
33.332% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* STEP 3 */
|
|
||||||
35.415% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(-200px);
|
|
||||||
}
|
|
||||||
49.998% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(-200px);
|
|
||||||
}
|
|
||||||
/* STEP 4 */
|
|
||||||
52.081% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
66.664% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* Step 5 */
|
|
||||||
68.747% {
|
|
||||||
left: -60px;
|
|
||||||
transform: translateZ(-180px);
|
|
||||||
}
|
|
||||||
83.33% {
|
|
||||||
left: -60px;
|
|
||||||
transform: translateZ(-180px);
|
|
||||||
}
|
|
||||||
/* Step 6 */
|
|
||||||
85.413% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 0px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes anim-cube-2-2 {
|
|
||||||
/* STEP 1 */
|
|
||||||
0% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
2.083% {
|
|
||||||
left: 180px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
16.666% {
|
|
||||||
left: 180px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* STEP 2 */
|
|
||||||
18.749% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
33.332% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* STEP 3 */
|
|
||||||
35.415% {
|
|
||||||
left: 240px;
|
|
||||||
transform: translateZ(-200px);
|
|
||||||
}
|
|
||||||
49.998% {
|
|
||||||
left: 240px;
|
|
||||||
transform: translateZ(-200px);
|
|
||||||
}
|
|
||||||
/* STEP 4 */
|
|
||||||
52.081% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
66.664% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* Step 5 */
|
|
||||||
68.747% {
|
|
||||||
left: 60px;
|
|
||||||
transform: translateZ(-180px);
|
|
||||||
}
|
|
||||||
83.33% {
|
|
||||||
left: 60px;
|
|
||||||
transform: translateZ(-180px);
|
|
||||||
}
|
|
||||||
/* Step 6 */
|
|
||||||
85.413% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes anim-cube-3-2 {
|
|
||||||
/* STEP 1 */
|
|
||||||
0% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
2.083% {
|
|
||||||
top: 220px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
16.666% {
|
|
||||||
top: 220px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* STEP 2 */
|
|
||||||
18.749% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
33.332% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* STEP 3 */
|
|
||||||
35.415% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-200px);
|
|
||||||
}
|
|
||||||
49.998% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-200px);
|
|
||||||
}
|
|
||||||
/* STEP 4 */
|
|
||||||
52.081% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
66.664% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* Step 5 */
|
|
||||||
68.747% {
|
|
||||||
top: 180px;
|
|
||||||
transform: translateZ(-180px);
|
|
||||||
}
|
|
||||||
83.33% {
|
|
||||||
top: 180px;
|
|
||||||
transform: translateZ(-180px);
|
|
||||||
}
|
|
||||||
/* Step 6 */
|
|
||||||
85.413% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
top: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes anim-cube-4-2 {
|
|
||||||
/* STEP 1 */
|
|
||||||
0% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
2.083% {
|
|
||||||
top: 220px;
|
|
||||||
left: 180px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
16.666% {
|
|
||||||
top: 220px;
|
|
||||||
left: 180px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* STEP 2 */
|
|
||||||
18.749% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
33.332% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* STEP 3 */
|
|
||||||
35.415% {
|
|
||||||
top: 120px;
|
|
||||||
left: 240px;
|
|
||||||
transform: translateZ(-200px);
|
|
||||||
}
|
|
||||||
49.998% {
|
|
||||||
top: 120px;
|
|
||||||
left: 240px;
|
|
||||||
transform: translateZ(-200px);
|
|
||||||
}
|
|
||||||
/* STEP 4 */
|
|
||||||
52.081% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
66.664% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
/* Step 5 */
|
|
||||||
68.747% {
|
|
||||||
top: 180px;
|
|
||||||
left: 260px;
|
|
||||||
transform: translateZ(-180px);
|
|
||||||
}
|
|
||||||
83.33% {
|
|
||||||
top: 180px;
|
|
||||||
left: 260px;
|
|
||||||
transform: translateZ(-180px);
|
|
||||||
}
|
|
||||||
/* Step 6 */
|
|
||||||
85.413% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
top: 120px;
|
|
||||||
left: 120px;
|
|
||||||
transform: translateZ(-120px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
|
||||||
|
|
||||||
import "./Creating.css";
|
|
||||||
|
|
||||||
export const Creating = () => (
|
|
||||||
<div class="creating">
|
|
||||||
<Tooltip open={true} placement="top" trigger={<div />}>
|
|
||||||
<Typography hierarchy="body" size="xs" weight="medium" inverted={true}>
|
|
||||||
Your Clan is being created
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<div class="scene">
|
|
||||||
<div class="frame">
|
|
||||||
<div id="cube-1" class="cube state-1">
|
|
||||||
<div class="cube-face front"></div>
|
|
||||||
<div class="cube-face left"></div>
|
|
||||||
<div class="cube-face right"></div>
|
|
||||||
<div class="cube-face top"></div>
|
|
||||||
<div class="cube-face bottom"></div>
|
|
||||||
<div class="cube-face back"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cube-2" class="cube state-2">
|
|
||||||
<div class="cube-face front"></div>
|
|
||||||
<div class="cube-face left"></div>
|
|
||||||
<div class="cube-face right"></div>
|
|
||||||
<div class="cube-face top"></div>
|
|
||||||
<div class="cube-face bottom"></div>
|
|
||||||
<div class="cube-face back"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cube-3" class="cube state-3">
|
|
||||||
<div class="cube-face front"></div>
|
|
||||||
<div class="cube-face left"></div>
|
|
||||||
<div class="cube-face right"></div>
|
|
||||||
<div class="cube-face top"></div>
|
|
||||||
<div class="cube-face bottom"></div>
|
|
||||||
<div class="cube-face back"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cube-4" class="cube state-4">
|
|
||||||
<div class="cube-face front"></div>
|
|
||||||
<div class="cube-face left"></div>
|
|
||||||
<div class="cube-face right"></div>
|
|
||||||
<div class="cube-face top"></div>
|
|
||||||
<div class="cube-face bottom"></div>
|
|
||||||
<div class="cube-face back"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cube-1-1" class="cube state-1-1">
|
|
||||||
<div class="cube-face front"></div>
|
|
||||||
<div class="cube-face left"></div>
|
|
||||||
<div class="cube-face right"></div>
|
|
||||||
<div class="cube-face top"></div>
|
|
||||||
<div class="cube-face bottom"></div>
|
|
||||||
<div class="cube-face back"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cube-2-2" class="cube state-2-2">
|
|
||||||
<div class="cube-face front"></div>
|
|
||||||
<div class="cube-face left"></div>
|
|
||||||
<div class="cube-face right"></div>
|
|
||||||
<div class="cube-face top"></div>
|
|
||||||
<div class="cube-face bottom"></div>
|
|
||||||
<div class="cube-face back"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cube-3-3" class="cube state-3-3">
|
|
||||||
<div class="cube-face front"></div>
|
|
||||||
<div class="cube-face left"></div>
|
|
||||||
<div class="cube-face right"></div>
|
|
||||||
<div class="cube-face top"></div>
|
|
||||||
<div class="cube-face bottom"></div>
|
|
||||||
<div class="cube-face back"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cube-4-4" class="cube state-4-4">
|
|
||||||
<div class="cube-face front"></div>
|
|
||||||
<div class="cube-face left"></div>
|
|
||||||
<div class="cube-face right"></div>
|
|
||||||
<div class="cube-face top"></div>
|
|
||||||
<div class="cube-face bottom"></div>
|
|
||||||
<div class="cube-face back"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
@@ -54,7 +54,8 @@ main#welcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > div.container {
|
& > div.container {
|
||||||
@apply flex flex-col items-center justify-evenly gap-y-20 size-fit;
|
@apply flex flex-col items-center justify-evenly gap-y-20;
|
||||||
|
@apply size-fit;
|
||||||
|
|
||||||
& > div.welcome {
|
& > div.welcome {
|
||||||
@apply flex flex-col min-w-80 gap-y-6;
|
@apply flex flex-col min-w-80 gap-y-6;
|
||||||
@@ -65,7 +66,7 @@ main#welcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > div.setup {
|
& > div.setup {
|
||||||
@apply flex flex-col w-[33rem] gap-y-5;
|
@apply flex flex-col min-w-[520px] gap-y-5;
|
||||||
@apply pt-10 px-8 pb-8 bg-def-1 rounded-lg;
|
@apply pt-10 px-8 pb-8 bg-def-1 rounded-lg;
|
||||||
|
|
||||||
& > div.header {
|
& > div.header {
|
||||||
|
|||||||
@@ -1,29 +1,18 @@
|
|||||||
import {
|
import { Component, createSignal, Match, Setter, Show, Switch } from "solid-js";
|
||||||
Accessor,
|
|
||||||
Component,
|
|
||||||
createSignal,
|
|
||||||
Match,
|
|
||||||
Setter,
|
|
||||||
Show,
|
|
||||||
Switch,
|
|
||||||
} from "solid-js";
|
|
||||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||||
import "./Onboarding.css";
|
import "./Onboarding.css";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { Button } from "@/src/components/Button/Button";
|
import { Button } from "@/src/components/Button/Button";
|
||||||
import { Alert } from "@/src/components/Alert/Alert";
|
|
||||||
|
|
||||||
import { Divider } from "@/src/components/Divider/Divider";
|
import { Divider } from "@/src/components/Divider/Divider";
|
||||||
import { Logo } from "@/src/components/Logo/Logo";
|
import { Logo } from "@/src/components/Logo/Logo";
|
||||||
import { navigateToClan, selectClanFolder } from "@/src/hooks/clan";
|
import { navigateToClan, selectClanFolder } from "@/src/hooks/clan";
|
||||||
import { activeClanURI, addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
import { activeClanURI } from "@/src/stores/clan";
|
||||||
import {
|
import {
|
||||||
createForm,
|
createForm,
|
||||||
FormStore,
|
FormStore,
|
||||||
getError,
|
getError,
|
||||||
getErrors,
|
getErrors,
|
||||||
getValue,
|
getValue,
|
||||||
SubmitHandler,
|
|
||||||
valiForm,
|
valiForm,
|
||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
import { TextInput } from "@/src/components/Form/TextInput";
|
import { TextInput } from "@/src/components/Form/TextInput";
|
||||||
@@ -31,32 +20,23 @@ import { TextArea } from "@/src/components/Form/TextArea";
|
|||||||
import { Fieldset } from "@/src/components/Form/Fieldset";
|
import { Fieldset } from "@/src/components/Form/Fieldset";
|
||||||
import * as v from "valibot";
|
import * as v from "valibot";
|
||||||
import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
||||||
import { callApi } from "@/src/hooks/api";
|
|
||||||
import { Creating } from "./Creating";
|
|
||||||
|
|
||||||
type State = "welcome" | "setup" | "creating";
|
type State = "welcome" | "setup";
|
||||||
|
|
||||||
const SetupSchema = v.object({
|
const SetupSchema = v.object({
|
||||||
name: v.pipe(
|
name: v.pipe(v.string(), v.nonEmpty("Please enter a name.")),
|
||||||
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.")),
|
description: v.pipe(v.string(), v.nonEmpty("Please describe your clan.")),
|
||||||
directory: v.pipe(
|
directory: v.pipe(v.string(), v.nonEmpty("Please select a directory.")),
|
||||||
// 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>;
|
type SetupForm = v.InferInput<typeof SetupSchema>;
|
||||||
|
|
||||||
const background = (props: { state: State; form: FormStore<SetupForm> }) => (
|
interface backgroundProps {
|
||||||
|
state: State;
|
||||||
|
form: FormStore<SetupForm>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const background = (props: backgroundProps) => (
|
||||||
<div class="background">
|
<div class="background">
|
||||||
<div class="layer-1" />
|
<div class="layer-1" />
|
||||||
<div class="layer-2" />
|
<div class="layer-2" />
|
||||||
@@ -91,11 +71,7 @@ const background = (props: { state: State; form: FormStore<SetupForm> }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const welcome = (props: {
|
const welcome = (setState: Setter<State>) => {
|
||||||
setState: Setter<State>;
|
|
||||||
welcomeError: Accessor<string | undefined>;
|
|
||||||
setWelcomeError: Setter<string | undefined>;
|
|
||||||
}) => {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const selectFolder = async () => {
|
const selectFolder = async () => {
|
||||||
@@ -115,23 +91,7 @@ const welcome = (props: {
|
|||||||
Build your <br />
|
Build your <br />
|
||||||
own darknet
|
own darknet
|
||||||
</Typography>
|
</Typography>
|
||||||
{props.welcomeError() && (
|
<Button hierarchy="secondary" onClick={() => setState("setup")}>
|
||||||
<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
|
Start building
|
||||||
</Button>
|
</Button>
|
||||||
<div class="separator">
|
<div class="separator">
|
||||||
@@ -166,89 +126,13 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
|||||||
|
|
||||||
const [state, setState] = createSignal<State>("welcome");
|
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>({
|
const [setupForm, { Form, Field }] = createForm<SetupForm>({
|
||||||
validate: valiForm(SetupSchema),
|
validate: valiForm(SetupSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const formError = () => {
|
const metaError = () => {
|
||||||
const formErrors = getErrors(setupForm);
|
const errors = getErrors(setupForm, ["name", "description"]);
|
||||||
return (
|
return errors ? errors.name || errors.description : undefined;
|
||||||
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: {
|
|
||||||
meta: {
|
|
||||||
name: name,
|
|
||||||
description: description,
|
|
||||||
// todo it tries to 'delete' icon if it's not provided
|
|
||||||
// this logic is unexpected, and needs reviewed.
|
|
||||||
icon: null,
|
|
||||||
},
|
|
||||||
machines: {},
|
|
||||||
instances: {},
|
|
||||||
services: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
@@ -256,13 +140,7 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
|||||||
{background({ form: setupForm, state: state() })}
|
{background({ form: setupForm, state: state() })}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={state() === "welcome"}>
|
<Match when={state() === "welcome"}>{welcome(setState)}</Match>
|
||||||
{welcome({
|
|
||||||
setState,
|
|
||||||
welcomeError,
|
|
||||||
setWelcomeError,
|
|
||||||
})}
|
|
||||||
</Match>
|
|
||||||
|
|
||||||
<Match when={state() === "setup"}>
|
<Match when={state() === "setup"}>
|
||||||
<div class="setup">
|
<div class="setup">
|
||||||
@@ -277,16 +155,8 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
|||||||
Setup
|
Setup
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Form onSubmit={onSubmit}>
|
<Form>
|
||||||
{formError() && (
|
<Fieldset name="meta" error={metaError()}>
|
||||||
<Alert
|
|
||||||
type="error"
|
|
||||||
icon="Info"
|
|
||||||
title="Form error"
|
|
||||||
description={formError() || ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Fieldset name="meta">
|
|
||||||
<Field name="name">
|
<Field name="name">
|
||||||
{(field, input) => (
|
{(field, input) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -325,13 +195,15 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
|||||||
</Field>
|
</Field>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
|
|
||||||
<Fieldset name="location">
|
<Fieldset
|
||||||
|
name="location"
|
||||||
|
error={getError(setupForm, "directory")}
|
||||||
|
>
|
||||||
<Field name="directory">
|
<Field name="directory">
|
||||||
{(field, input) => (
|
{(field, input) => (
|
||||||
<HostFileInput
|
<HostFileInput
|
||||||
onSelectFile={onSelectFile}
|
onSelectFile={async () => "test"}
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value}
|
|
||||||
label="Select directory"
|
label="Select directory"
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
required={true}
|
required={true}
|
||||||
@@ -356,10 +228,6 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
|||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
<Match when={state() === "creating"}>
|
|
||||||
<Creating />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -8,41 +8,7 @@ export const Routes: RouteDefinition[] = [
|
|||||||
component: Onboarding,
|
component: Onboarding,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/clans",
|
path: "/clan/:clanURI",
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
component: () => (
|
|
||||||
<h1>
|
|
||||||
Clans (index) - (Doesnt really exist, just to keep the scene
|
|
||||||
mounted)
|
|
||||||
</h1>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/:clanURI",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
component: Clan,
|
component: Clan,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/machines",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
component: () => <h1>Machines (Index)</h1>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/:machineID",
|
|
||||||
component: (props) => (
|
|
||||||
<h1>Machine ID: {props.params.machineID}</h1>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
.cubes-scene-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-container {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10%;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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
@@ -1,45 +0,0 @@
|
|||||||
#splash {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: linear-gradient(to top, #e3e7e7, #edf1f1);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 9999;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#splash .content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
@apply h-8 mb-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
@apply h-3 w-60 mb-3;
|
|
||||||
width: 18rem;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
-45deg,
|
|
||||||
#bfd0d2 0px,
|
|
||||||
#bfd0d2 10px,
|
|
||||||
#f7f9fa 10px,
|
|
||||||
#f7f9fa 20px
|
|
||||||
);
|
|
||||||
animation: stripe-move 1s linear infinite;
|
|
||||||
background-size: 28px 28px; /* Sqrt(20^2 + 20^2) ~= 28 */
|
|
||||||
|
|
||||||
@apply border-2 border-solid rounded-[3px] border-bg-def-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stripe-move {
|
|
||||||
0% {
|
|
||||||
background-position: 0 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 28px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
|
||||||
import { Splash } from "./splash";
|
|
||||||
|
|
||||||
const meta: Meta = {
|
|
||||||
title: "scene/splash",
|
|
||||||
component: Splash,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
type Story = StoryObj;
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {},
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import Logo from "@/logos/darknet-builder-logo.svg";
|
|
||||||
import "./splash.css";
|
|
||||||
import { Typography } from "../components/Typography/Typography";
|
|
||||||
|
|
||||||
export const Splash = () => (
|
|
||||||
<div id="splash">
|
|
||||||
<div class="content">
|
|
||||||
<span class="title">
|
|
||||||
<Logo />
|
|
||||||
</span>
|
|
||||||
<div class="loader"></div>
|
|
||||||
|
|
||||||
<Typography hierarchy="label" size="xs" weight="medium">
|
|
||||||
Loading new Clan
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
import { createStore, produce } from "solid-js/store";
|
import { createStore, produce } from "solid-js/store";
|
||||||
import { makePersisted } from "@solid-primitives/storage";
|
import { makePersisted } from "@solid-primitives/storage";
|
||||||
|
|
||||||
export type SceneData = Record<string, { position: [number, number] }>;
|
interface ClanStoreType {
|
||||||
|
|
||||||
export interface ClanStoreType {
|
|
||||||
clanURIs: string[];
|
clanURIs: string[];
|
||||||
activeClanURI?: string;
|
activeClanURI?: string;
|
||||||
sceneData: Record<string, SceneData>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [store, setStore] = makePersisted(
|
const [store, setStore] = makePersisted(
|
||||||
createStore<ClanStoreType>({
|
createStore<ClanStoreType>({
|
||||||
clanURIs: [],
|
clanURIs: [],
|
||||||
sceneData: {},
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "clanStore",
|
name: "clanStore",
|
||||||
@@ -26,7 +22,7 @@ const [store, setStore] = makePersisted(
|
|||||||
* @function
|
* @function
|
||||||
* @returns {string} The URI of the active clan.
|
* @returns {string} The URI of the active clan.
|
||||||
*/
|
*/
|
||||||
const activeClanURI = () => store.activeClanURI;
|
const activeClanURI = (): string | undefined => store.activeClanURI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the active Clan URI in the store.
|
* Updates the active Clan URI in the store.
|
||||||
@@ -49,10 +45,8 @@ const clanURIs = (): string[] => store.clanURIs;
|
|||||||
* @param {string} uri - The URI of the clan to be added.
|
* @param {string} uri - The URI of the clan to be added.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const addClanURI = (uri: string) => {
|
const addClanURI = (uri: string) =>
|
||||||
setStore("clanURIs", store.clanURIs.length, uri);
|
setStore("clanURIs", store.clanURIs.length, uri);
|
||||||
setStore("sceneData", uri, {}); // Initialize empty scene data for every new clan URI
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a specified URI from the clan URI list and updates the active clan URI.
|
* Removes a specified URI from the clan URI list and updates the active clan URI.
|
||||||
@@ -86,7 +80,6 @@ const removeClanURI = (uri: string) => {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
store,
|
store,
|
||||||
setStore,
|
|
||||||
activeClanURI,
|
activeClanURI,
|
||||||
setActiveClanURI,
|
setActiveClanURI,
|
||||||
clanURIs,
|
clanURIs,
|
||||||
|
|||||||
@@ -1,18 +1,6 @@
|
|||||||
{
|
{ pkgs, ... }:
|
||||||
gtk4,
|
|
||||||
webkitgtk_6_0,
|
|
||||||
lib,
|
|
||||||
clangStdenv,
|
|
||||||
fetchFromGitea,
|
|
||||||
gnumake,
|
|
||||||
cmake,
|
|
||||||
clang-tools,
|
|
||||||
pkg-config,
|
|
||||||
stdenv,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
clangStdenv.mkDerivation {
|
pkgs.clangStdenv.mkDerivation {
|
||||||
pname = "webview";
|
pname = "webview";
|
||||||
version = "nightly";
|
version = "nightly";
|
||||||
|
|
||||||
@@ -20,7 +8,7 @@ clangStdenv.mkDerivation {
|
|||||||
# We disallow remote connections from the UI on Linux
|
# We disallow remote connections from the UI on Linux
|
||||||
# TODO: Disallow remote connections on MacOS
|
# TODO: Disallow remote connections on MacOS
|
||||||
|
|
||||||
src = fetchFromGitea {
|
src = pkgs.fetchFromGitea {
|
||||||
domain = "git.clan.lol";
|
domain = "git.clan.lol";
|
||||||
owner = "clan";
|
owner = "clan";
|
||||||
repo = "webview";
|
repo = "webview";
|
||||||
@@ -49,19 +37,23 @@ clangStdenv.mkDerivation {
|
|||||||
];
|
];
|
||||||
|
|
||||||
# Dependencies used during the build process, if any
|
# Dependencies used during the build process, if any
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = with pkgs; [
|
||||||
gnumake
|
gnumake
|
||||||
cmake
|
cmake
|
||||||
clang-tools
|
clang-tools
|
||||||
pkg-config
|
pkg-config
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = lib.optionals stdenv.isLinux [
|
buildInputs =
|
||||||
|
with pkgs;
|
||||||
|
[
|
||||||
|
]
|
||||||
|
++ pkgs.lib.optionals stdenv.isLinux [
|
||||||
webkitgtk_6_0
|
webkitgtk_6_0
|
||||||
gtk4
|
gtk4
|
||||||
];
|
];
|
||||||
|
|
||||||
meta = with lib; {
|
meta = with pkgs.lib; {
|
||||||
description = "Tiny cross-platform webview library for C/C++. Uses WebKit (GTK/Cocoa) and Edge WebView2 (Windows)";
|
description = "Tiny cross-platform webview library for C/C++. Uses WebKit (GTK/Cocoa) and Edge WebView2 (Windows)";
|
||||||
homepage = "https://github.com/webview/webview";
|
homepage = "https://github.com/webview/webview";
|
||||||
license = licenses.mit;
|
license = licenses.mit;
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from .facts import cli as facts
|
|||||||
from .flash import cli as flash_cli
|
from .flash import cli as flash_cli
|
||||||
from .hyperlink import help_hyperlink
|
from .hyperlink import help_hyperlink
|
||||||
from .machines import cli as machines
|
from .machines import cli as machines
|
||||||
from .network import cli as network_cli
|
|
||||||
from .profiler import profile
|
from .profiler import profile
|
||||||
from .ssh import deploy_info as ssh_cli
|
from .ssh import deploy_info as ssh_cli
|
||||||
from .vars import cli as vars_cli
|
from .vars import cli as vars_cli
|
||||||
@@ -429,26 +428,6 @@ Examples:
|
|||||||
)
|
)
|
||||||
select.register_parser(parser_select)
|
select.register_parser(parser_select)
|
||||||
|
|
||||||
parser_network = subparsers.add_parser(
|
|
||||||
"network",
|
|
||||||
aliases=["net"],
|
|
||||||
# TODO: Add help="Manage networks" when network code is ready
|
|
||||||
# help="Manage networks",
|
|
||||||
description="Manage networks",
|
|
||||||
epilog=(
|
|
||||||
"""
|
|
||||||
show information about configured networks
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan network list
|
|
||||||
Will list networks
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
network_cli.register_parser(parser_network)
|
|
||||||
|
|
||||||
parser_state = subparsers.add_parser(
|
parser_state = subparsers.add_parser(
|
||||||
"state",
|
"state",
|
||||||
aliases=["st"],
|
aliases=["st"],
|
||||||
@@ -483,7 +462,7 @@ For more detailed information, visit: {help_hyperlink("getting-started", "https:
|
|||||||
state.register_parser(parser_state)
|
state.register_parser(parser_state)
|
||||||
|
|
||||||
if argcomplete:
|
if argcomplete:
|
||||||
argcomplete.autocomplete(parser, exclude=["morph", "network", "net"])
|
argcomplete.autocomplete(parser, exclude=["morph"])
|
||||||
|
|
||||||
register_common_flags(parser)
|
register_common_flags(parser)
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
# !/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from .list import register_list_parser
|
|
||||||
from .overview import register_overview_parser
|
|
||||||
from .ping import register_ping_parser
|
|
||||||
|
|
||||||
|
|
||||||
# takes a (sub)parser and configures it
|
|
||||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
|
||||||
subparser = parser.add_subparsers(
|
|
||||||
title="command",
|
|
||||||
description="the command to run",
|
|
||||||
help="the command to run",
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
list_parser = subparser.add_parser(
|
|
||||||
"list",
|
|
||||||
help="list all networks",
|
|
||||||
epilog=(
|
|
||||||
"""
|
|
||||||
This subcommand allows listing all networks
|
|
||||||
```
|
|
||||||
[NETWORK1] [PRIORITY] [MODULE] [PEER1, PEER2]
|
|
||||||
[NETOWKR2] [PRIORITY] [MODULE] [PEER1, PEER2]
|
|
||||||
```
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan network list
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_list_parser(list_parser)
|
|
||||||
|
|
||||||
ping_parser = subparser.add_parser(
|
|
||||||
"ping",
|
|
||||||
help="ping a machine to check if it's online",
|
|
||||||
epilog=(
|
|
||||||
"""
|
|
||||||
This subcommand allows pinging a machine to check if it's online
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan network ping machine1
|
|
||||||
Check machine1 on all networks (in priority order)
|
|
||||||
|
|
||||||
$ clan network ping machine1 --network tor
|
|
||||||
Check machine1 only on the tor network
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_ping_parser(ping_parser)
|
|
||||||
|
|
||||||
overview_parser = subparser.add_parser(
|
|
||||||
"overview",
|
|
||||||
help="show the overview of all network and hosts",
|
|
||||||
epilog=(
|
|
||||||
"""
|
|
||||||
This command shows the complete state of all networks
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan network overview
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_overview_parser(overview_parser)
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from clan_lib.flake import Flake
|
|
||||||
from clan_lib.network.network import networks_from_flake
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def list_command(args: argparse.Namespace) -> None:
|
|
||||||
flake: Flake = args.flake
|
|
||||||
networks = networks_from_flake(flake)
|
|
||||||
|
|
||||||
if not networks:
|
|
||||||
print("No networks found")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Calculate column widths
|
|
||||||
col_network = max(12, max(len(name) for name in networks))
|
|
||||||
col_priority = 8
|
|
||||||
col_module = max(
|
|
||||||
10, max(len(net.module_name.split(".")[-1]) for net in networks.values())
|
|
||||||
)
|
|
||||||
col_running = 8
|
|
||||||
|
|
||||||
# Print header
|
|
||||||
header = f"{'Network':<{col_network}} {'Priority':<{col_priority}} {'Module':<{col_module}} {'Running':<{col_running}} {'Peers'}"
|
|
||||||
print(header)
|
|
||||||
print("-" * len(header))
|
|
||||||
|
|
||||||
# Print network entries
|
|
||||||
for network_name, network in sorted(
|
|
||||||
networks.items(), key=lambda network: -network[1].priority
|
|
||||||
):
|
|
||||||
# Extract simple module name from full module path
|
|
||||||
module_name = network.module_name.split(".")[-1]
|
|
||||||
|
|
||||||
# Create peer list with truncation
|
|
||||||
peer_names = list(network.peers.keys())
|
|
||||||
max_peers_shown = 3
|
|
||||||
|
|
||||||
if not peer_names:
|
|
||||||
peers_str = "No peers"
|
|
||||||
elif len(peer_names) <= max_peers_shown:
|
|
||||||
peers_str = ", ".join(peer_names)
|
|
||||||
else:
|
|
||||||
shown_peers = peer_names[:max_peers_shown]
|
|
||||||
remaining = len(peer_names) - max_peers_shown
|
|
||||||
peers_str = f"{', '.join(shown_peers)} ...({remaining} more)"
|
|
||||||
|
|
||||||
# Check if network is running
|
|
||||||
try:
|
|
||||||
is_running = network.is_running()
|
|
||||||
running_status = "Yes" if is_running else "No"
|
|
||||||
except Exception:
|
|
||||||
running_status = "Error"
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"{network_name:<{col_network}} {network.priority:<{col_priority}} {module_name:<{col_module}} {running_status:<{col_running}} {peers_str}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register_list_parser(parser: argparse.ArgumentParser) -> None:
|
|
||||||
parser.set_defaults(func=list_command)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from clan_lib.flake import Flake
|
|
||||||
from clan_lib.network.network import get_network_overview, networks_from_flake
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def overview_command(args: argparse.Namespace) -> None:
|
|
||||||
flake: Flake = args.flake
|
|
||||||
networks = networks_from_flake(flake)
|
|
||||||
overview = get_network_overview(networks)
|
|
||||||
for network_name, network in overview.items():
|
|
||||||
print(f"{network_name} {'[ONLINE]' if network['status'] else '[OFFLINE]'}")
|
|
||||||
for peer_name, peer in network["peers"].items():
|
|
||||||
print(f"\t{peer_name}: {'[OFFLINE]' if not peer else f'[{peer}]'}")
|
|
||||||
|
|
||||||
|
|
||||||
def register_overview_parser(parser: argparse.ArgumentParser) -> None:
|
|
||||||
parser.set_defaults(func=overview_command)
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from clan_lib.errors import ClanError
|
|
||||||
from clan_lib.flake import Flake
|
|
||||||
from clan_lib.network.network import networks_from_flake
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def ping_command(args: argparse.Namespace) -> None:
|
|
||||||
flake: Flake = args.flake
|
|
||||||
machine = args.machine
|
|
||||||
network_name = args.network
|
|
||||||
|
|
||||||
networks = networks_from_flake(flake)
|
|
||||||
|
|
||||||
if not networks:
|
|
||||||
print("No networks found in the flake")
|
|
||||||
|
|
||||||
# If network is specified, only check that network
|
|
||||||
if network_name:
|
|
||||||
networks_to_check = [(network_name, networks[network_name])]
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Sort networks by priority (highest first)
|
|
||||||
networks_to_check = sorted(networks.items(), key=lambda x: -x[1].priority)
|
|
||||||
|
|
||||||
found = False
|
|
||||||
results = []
|
|
||||||
for net_name, network in networks_to_check:
|
|
||||||
if machine in network.peers:
|
|
||||||
found = True
|
|
||||||
|
|
||||||
# Check if network technology is running
|
|
||||||
if not network.is_running():
|
|
||||||
results.append(f"{machine} ({net_name}): network not running")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if peer is online
|
|
||||||
ping = network.ping(machine)
|
|
||||||
results.append(f"{machine} ({net_name}): {ping}")
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
msg = f"Machine '{machine}' not found in any network"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
# Print all results
|
|
||||||
for result in results:
|
|
||||||
print(result)
|
|
||||||
|
|
||||||
|
|
||||||
def register_ping_parser(parser: argparse.ArgumentParser) -> None:
|
|
||||||
parser.add_argument(
|
|
||||||
"machine",
|
|
||||||
type=str,
|
|
||||||
help="Machine name to ping",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"--network",
|
|
||||||
"-n",
|
|
||||||
type=str,
|
|
||||||
help="Specific network to use for ping (if not specified, checks all networks)",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.set_defaults(func=ping_command)
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import contextlib
|
|
||||||
import importlib
|
|
||||||
import inspect
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, TypeVar, cast
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ClassSource:
|
|
||||||
module_name: str
|
|
||||||
file_path: Path
|
|
||||||
object_name: str
|
|
||||||
line_number: int | None = None
|
|
||||||
|
|
||||||
def vscode_clickable_path(self) -> str:
|
|
||||||
"""Return a VSCode-clickable path for the class source."""
|
|
||||||
return (
|
|
||||||
f"{self.module_name}.{self.object_name}: {self.file_path}:{self.line_number}"
|
|
||||||
if self.line_number is not None
|
|
||||||
else f"{self.module_name}.{self.object_name}: {self.file_path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return self.vscode_clickable_path()
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.vscode_clickable_path()
|
|
||||||
|
|
||||||
|
|
||||||
def import_with_source[T](
|
|
||||||
module_name: str,
|
|
||||||
class_name: str,
|
|
||||||
base_class: type[T],
|
|
||||||
*args: Any,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> T:
|
|
||||||
"""
|
|
||||||
Import a class from a module and instantiate it with source information.
|
|
||||||
|
|
||||||
This function dynamically imports a class and adds source location metadata
|
|
||||||
that can be used for debugging. The instantiated object will have VSCode-clickable
|
|
||||||
paths in its string representation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
module_name: The fully qualified module name to import
|
|
||||||
class_name: The name of the class to import from the module
|
|
||||||
base_class: The base class type for type checking
|
|
||||||
*args: Additional positional arguments to pass to the class constructor
|
|
||||||
**kwargs: Additional keyword arguments to pass to the class constructor
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An instance of the imported class with source information
|
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> from .network import NetworkTechnologyBase, ClassSource
|
|
||||||
>>> tech = import_with_source(
|
|
||||||
... "clan_lib.network.tor",
|
|
||||||
... "NetworkTechnology",
|
|
||||||
... NetworkTechnologyBase
|
|
||||||
... )
|
|
||||||
>>> print(tech) # Outputs: ~/Projects/clan-core/.../tor.py:7
|
|
||||||
"""
|
|
||||||
# Import the module
|
|
||||||
module = importlib.import_module(module_name)
|
|
||||||
|
|
||||||
# Get the class from the module
|
|
||||||
cls = getattr(module, class_name)
|
|
||||||
|
|
||||||
# Get the line number of the class definition
|
|
||||||
line_number = None
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
line_number = inspect.getsourcelines(cls)[1]
|
|
||||||
|
|
||||||
# Get the file path
|
|
||||||
file_path_str = module.__file__
|
|
||||||
assert file_path_str is not None, f"Module {module_name} file path cannot be None"
|
|
||||||
|
|
||||||
# Make the path relative to home for better readability
|
|
||||||
try:
|
|
||||||
file_path = Path(file_path_str).relative_to(Path.home())
|
|
||||||
file_path = Path("~", file_path)
|
|
||||||
except ValueError:
|
|
||||||
# If not under home directory, use absolute path
|
|
||||||
file_path = Path(file_path_str)
|
|
||||||
|
|
||||||
# Create source information
|
|
||||||
source = ClassSource(
|
|
||||||
module_name=module_name,
|
|
||||||
file_path=file_path,
|
|
||||||
object_name=class_name,
|
|
||||||
line_number=line_number,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Instantiate the class with source information
|
|
||||||
return cast(T, cls(source, *args, **kwargs))
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
from textwrap import dedent
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from clan_lib.import_utils import import_with_source
|
|
||||||
from clan_lib.network.network import NetworkTechnologyBase
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_with_source(tmp_path: Path) -> None:
|
|
||||||
"""Test importing a class with source information."""
|
|
||||||
# Create a temporary module file
|
|
||||||
module_dir = tmp_path / "test_module"
|
|
||||||
module_dir.mkdir()
|
|
||||||
|
|
||||||
# Create __init__.py to make it a package
|
|
||||||
(module_dir / "__init__.py").write_text("")
|
|
||||||
|
|
||||||
# Create a test module with a NetworkTechnology class
|
|
||||||
test_module_path = module_dir / "test_tech.py"
|
|
||||||
test_module_path.write_text(
|
|
||||||
dedent("""
|
|
||||||
from clan_lib.network.network import NetworkTechnologyBase
|
|
||||||
|
|
||||||
class NetworkTechnology(NetworkTechnologyBase):
|
|
||||||
def __init__(self, source):
|
|
||||||
super().__init__(source)
|
|
||||||
self.test_value = "test"
|
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
return True
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the temp directory to sys.path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, str(tmp_path))
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Import the class using import_with_source
|
|
||||||
instance = import_with_source(
|
|
||||||
"test_module.test_tech",
|
|
||||||
"NetworkTechnology",
|
|
||||||
cast(Any, NetworkTechnologyBase),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify the instance is created correctly
|
|
||||||
assert isinstance(instance, NetworkTechnologyBase)
|
|
||||||
assert instance.is_running() is True
|
|
||||||
assert hasattr(instance, "test_value")
|
|
||||||
assert instance.test_value == "test"
|
|
||||||
|
|
||||||
# Verify source information
|
|
||||||
assert instance.source.module_name == "test_module.test_tech"
|
|
||||||
assert instance.source.file_path.name == "test_tech.py"
|
|
||||||
assert instance.source.object_name == "NetworkTechnology"
|
|
||||||
assert instance.source.line_number == 4 # Line where class is defined
|
|
||||||
|
|
||||||
# Test string representations
|
|
||||||
str_repr = str(instance)
|
|
||||||
assert "test_tech.py:" in str_repr
|
|
||||||
assert "NetworkTechnology" in str_repr
|
|
||||||
assert str(instance.source.line_number) in str_repr
|
|
||||||
|
|
||||||
repr_repr = repr(instance)
|
|
||||||
assert "NetworkTechnology" in repr_repr
|
|
||||||
assert "test_tech.py:" in repr_repr
|
|
||||||
assert "test_module.test_tech.NetworkTechnology" in repr_repr
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Clean up sys.path
|
|
||||||
sys.path.remove(str(tmp_path))
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_with_source_with_args() -> None:
|
|
||||||
"""Test importing a class with additional constructor arguments."""
|
|
||||||
# Create a temporary test file
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
||||||
f.write(
|
|
||||||
dedent("""
|
|
||||||
from clan_lib.network.network import NetworkTechnologyBase
|
|
||||||
|
|
||||||
class NetworkTechnology(NetworkTechnologyBase):
|
|
||||||
def __init__(self, source, extra_arg, keyword_arg=None):
|
|
||||||
super().__init__(source)
|
|
||||||
self.extra_arg = extra_arg
|
|
||||||
self.keyword_arg = keyword_arg
|
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
return False
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
temp_file = Path(f.name)
|
|
||||||
|
|
||||||
# Import module dynamically
|
|
||||||
import importlib.util
|
|
||||||
import sys
|
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location("temp_module", temp_file)
|
|
||||||
assert spec is not None
|
|
||||||
assert spec.loader is not None
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
sys.modules["temp_module"] = module
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Import with additional arguments
|
|
||||||
instance = import_with_source(
|
|
||||||
"temp_module",
|
|
||||||
"NetworkTechnology",
|
|
||||||
cast(Any, NetworkTechnologyBase),
|
|
||||||
"extra_value",
|
|
||||||
keyword_arg="keyword_value",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify arguments were passed correctly
|
|
||||||
assert instance.extra_arg == "extra_value" # type: ignore[attr-defined]
|
|
||||||
assert instance.keyword_arg == "keyword_value" # type: ignore[attr-defined]
|
|
||||||
assert instance.source.object_name == "NetworkTechnology"
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Clean up
|
|
||||||
del sys.modules["temp_module"]
|
|
||||||
temp_file.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_with_source_module_not_found() -> None:
|
|
||||||
"""Test error handling when module is not found."""
|
|
||||||
with pytest.raises(ModuleNotFoundError):
|
|
||||||
import_with_source(
|
|
||||||
"non_existent_module", "SomeClass", cast(Any, NetworkTechnologyBase)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_with_source_class_not_found() -> None:
|
|
||||||
"""Test error handling when class is not found in module."""
|
|
||||||
with pytest.raises(AttributeError):
|
|
||||||
import_with_source(
|
|
||||||
"clan_lib.network.network",
|
|
||||||
"NonExistentClass",
|
|
||||||
cast(Any, NetworkTechnologyBase),
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from clan_lib.network.network import NetworkTechnologyBase
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkTechnology(NetworkTechnologyBase):
|
|
||||||
"""Direct network connection technology - checks SSH connectivity"""
|
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
"""Direct connections are always 'running' as they don't require a daemon"""
|
|
||||||
return True
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import logging
|
|
||||||
import textwrap
|
|
||||||
import time
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from functools import cached_property
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from clan_cli.vars.get import get_machine_var
|
|
||||||
from clan_lib.errors import ClanError
|
|
||||||
from clan_lib.flake import Flake
|
|
||||||
from clan_lib.import_utils import ClassSource, import_with_source
|
|
||||||
from clan_lib.ssh.parse import parse_ssh_uri
|
|
||||||
from clan_lib.ssh.remote import Remote, check_machine_ssh_reachable
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Peer:
|
|
||||||
_host: dict[str, str | dict[str, str]]
|
|
||||||
flake: Flake
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def host(self) -> str:
|
|
||||||
if "plain" in self._host and isinstance(self._host["plain"], str):
|
|
||||||
return self._host["plain"]
|
|
||||||
if "var" in self._host and isinstance(self._host["var"], dict):
|
|
||||||
_var: dict[str, str] = self._host["var"]
|
|
||||||
machine_name = _var["machine"]
|
|
||||||
generator = _var["generator"]
|
|
||||||
var = get_machine_var(
|
|
||||||
str(self.flake),
|
|
||||||
machine_name,
|
|
||||||
f"{generator}/{_var['file']}",
|
|
||||||
)
|
|
||||||
if not var.exists:
|
|
||||||
msg = (
|
|
||||||
textwrap.dedent(f"""
|
|
||||||
It looks like you added a networking module to your machine, but forgot
|
|
||||||
to deploy your changes. Please run "clan machines update {machine_name}"
|
|
||||||
so that the appropriate vars are generated and deployed properly.
|
|
||||||
""")
|
|
||||||
.rstrip("\n")
|
|
||||||
.lstrip("\n")
|
|
||||||
)
|
|
||||||
raise ClanError(msg)
|
|
||||||
return var.value.decode()
|
|
||||||
msg = f"Unknown Var Type {self._host}"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NetworkTechnologyBase(ABC):
|
|
||||||
source: ClassSource
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# TODO this will depend on the network implementation if we do user networking at some point, so it should be abstractmethod
|
|
||||||
def ping(self, peer: Peer) -> None | float:
|
|
||||||
if self.is_running():
|
|
||||||
try:
|
|
||||||
# Parse the peer's host address to create a Remote object, use peer here since we don't have the machine_name here
|
|
||||||
remote = parse_ssh_uri(machine_name="peer", address=peer.host)
|
|
||||||
|
|
||||||
# Use the existing SSH reachability check
|
|
||||||
now = time.time()
|
|
||||||
result = check_machine_ssh_reachable(remote)
|
|
||||||
|
|
||||||
if result.ok:
|
|
||||||
return (time.time() - now) * 1000
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.debug(f"Error checking peer {peer.host}: {e}")
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Network:
|
|
||||||
peers: dict[str, Peer]
|
|
||||||
module_name: str
|
|
||||||
priority: int = 1000
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def module(self) -> NetworkTechnologyBase:
|
|
||||||
res = import_with_source(
|
|
||||||
self.module_name,
|
|
||||||
"NetworkTechnology",
|
|
||||||
NetworkTechnologyBase, # type: ignore[type-abstract]
|
|
||||||
)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
return self.module.is_running()
|
|
||||||
|
|
||||||
def ping(self, peer: str) -> float | None:
|
|
||||||
return self.module.ping(self.peers[peer])
|
|
||||||
|
|
||||||
|
|
||||||
def networks_from_flake(flake: Flake) -> dict[str, Network]:
|
|
||||||
networks: dict[str, Network] = {}
|
|
||||||
networks_ = flake.select("clan.exports.instances.*.networking")
|
|
||||||
for network_name, network in networks_.items():
|
|
||||||
if network:
|
|
||||||
peers: dict[str, Peer] = {}
|
|
||||||
for _peer in network["peers"].values():
|
|
||||||
peers[_peer["name"]] = Peer(_host=_peer["host"], flake=flake)
|
|
||||||
networks[network_name] = Network(
|
|
||||||
peers=peers,
|
|
||||||
module_name=network["module"],
|
|
||||||
priority=network["priority"],
|
|
||||||
)
|
|
||||||
return networks
|
|
||||||
|
|
||||||
|
|
||||||
def get_best_remote(machine_name: str, networks: dict[str, Network]) -> Remote | None:
|
|
||||||
for network_name, network in sorted(
|
|
||||||
networks.items(), key=lambda network: -network[1].priority
|
|
||||||
):
|
|
||||||
if machine_name in network.peers:
|
|
||||||
if network.is_running() and network.ping(machine_name):
|
|
||||||
print(f"connecting via {network_name}")
|
|
||||||
return Remote.from_ssh_uri(
|
|
||||||
machine_name=machine_name,
|
|
||||||
address=network.peers[machine_name].host,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_network_overview(networks: dict[str, Network]) -> dict:
|
|
||||||
result: dict[str, dict[str, Any]] = {}
|
|
||||||
for network_name, network in networks.items():
|
|
||||||
result[network_name] = {}
|
|
||||||
result[network_name]["status"] = None
|
|
||||||
result[network_name]["peers"] = {}
|
|
||||||
network_online = False
|
|
||||||
module = network.module
|
|
||||||
log.debug(f"Using network module: {module}")
|
|
||||||
if module.is_running():
|
|
||||||
result[network_name]["status"] = True
|
|
||||||
network_online = True
|
|
||||||
for peer_name in network.peers:
|
|
||||||
if network_online:
|
|
||||||
try:
|
|
||||||
result[network_name]["peers"][peer_name] = network.ping(peer_name)
|
|
||||||
except ClanError:
|
|
||||||
log.warning(
|
|
||||||
f"getting host for machine: {peer_name} in network: {network_name} failed"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
result[network_name]["peers"][peer_name] = None
|
|
||||||
return result
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from clan_lib.flake import Flake
|
|
||||||
from clan_lib.network.network import Network, Peer, networks_from_flake
|
|
||||||
|
|
||||||
|
|
||||||
@patch("clan_lib.network.network.get_machine_var")
|
|
||||||
def test_networks_from_flake(mock_get_machine_var: MagicMock) -> None:
|
|
||||||
# Create a mock flake
|
|
||||||
flake = MagicMock(spec=Flake)
|
|
||||||
|
|
||||||
# Mock the var decryption
|
|
||||||
def mock_var_side_effect(flake_path: str, machine: str, var_path: str) -> Any:
|
|
||||||
if machine == "machine1" and var_path == "wireguard/address":
|
|
||||||
mock_var = MagicMock()
|
|
||||||
mock_var.value.decode.return_value = "192.168.1.10"
|
|
||||||
return mock_var
|
|
||||||
if machine == "machine2" and var_path == "wireguard/address":
|
|
||||||
mock_var = MagicMock()
|
|
||||||
mock_var.value.decode.return_value = "192.168.1.11"
|
|
||||||
return mock_var
|
|
||||||
return None
|
|
||||||
|
|
||||||
mock_get_machine_var.side_effect = mock_var_side_effect
|
|
||||||
|
|
||||||
# Define the expected return value from flake.select
|
|
||||||
mock_networking_data = {
|
|
||||||
"vpn-network": {
|
|
||||||
"peers": {
|
|
||||||
"machine1": {
|
|
||||||
"name": "machine1",
|
|
||||||
"host": {
|
|
||||||
"var": {
|
|
||||||
"machine": "machine1",
|
|
||||||
"generator": "wireguard",
|
|
||||||
"file": "address",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"machine2": {
|
|
||||||
"name": "machine2",
|
|
||||||
"host": {
|
|
||||||
"var": {
|
|
||||||
"machine": "machine2",
|
|
||||||
"generator": "wireguard",
|
|
||||||
"file": "address",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"module": "clan_lib.network.tor",
|
|
||||||
"priority": 1000,
|
|
||||||
},
|
|
||||||
"local-network": {
|
|
||||||
"peers": {
|
|
||||||
"machine1": {
|
|
||||||
"name": "machine1",
|
|
||||||
"host": {"plain": "10.0.0.10"},
|
|
||||||
},
|
|
||||||
"machine3": {
|
|
||||||
"name": "machine3",
|
|
||||||
"host": {"plain": "10.0.0.12"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"module": "clan_lib.network.direct",
|
|
||||||
"priority": 500,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mock the select method
|
|
||||||
flake.select.return_value = mock_networking_data
|
|
||||||
|
|
||||||
# Call the function
|
|
||||||
networks = networks_from_flake(flake)
|
|
||||||
|
|
||||||
# Verify the flake.select was called with the correct pattern
|
|
||||||
flake.select.assert_called_once_with("clan.exports.instances.*.networking")
|
|
||||||
|
|
||||||
# Verify the returned networks
|
|
||||||
assert len(networks) == 2
|
|
||||||
assert "vpn-network" in networks
|
|
||||||
assert "local-network" in networks
|
|
||||||
|
|
||||||
# Check vpn-network
|
|
||||||
vpn_network = networks["vpn-network"]
|
|
||||||
assert isinstance(vpn_network, Network)
|
|
||||||
assert vpn_network.module_name == "clan_lib.network.tor"
|
|
||||||
assert vpn_network.priority == 1000
|
|
||||||
assert len(vpn_network.peers) == 2
|
|
||||||
assert "machine1" in vpn_network.peers
|
|
||||||
assert "machine2" in vpn_network.peers
|
|
||||||
|
|
||||||
# Check peer details - this will call get_machine_var to decrypt the var
|
|
||||||
machine1_peer = vpn_network.peers["machine1"]
|
|
||||||
assert isinstance(machine1_peer, Peer)
|
|
||||||
assert machine1_peer.host == "192.168.1.10"
|
|
||||||
assert machine1_peer.flake == flake
|
|
||||||
|
|
||||||
# Check local-network
|
|
||||||
local_network = networks["local-network"]
|
|
||||||
assert local_network.module_name == "clan_lib.network.direct"
|
|
||||||
assert local_network.priority == 500
|
|
||||||
assert len(local_network.peers) == 2
|
|
||||||
assert "machine1" in local_network.peers
|
|
||||||
assert "machine3" in local_network.peers
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from urllib.error import HTTPError
|
|
||||||
from urllib.request import urlopen
|
|
||||||
|
|
||||||
from .network import NetworkTechnologyBase
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkTechnology(NetworkTechnologyBase):
|
|
||||||
socks_port: int
|
|
||||||
command_port: int
|
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
"""Check if Tor is running by sending HTTP request to SOCKS port."""
|
|
||||||
try:
|
|
||||||
response = urlopen("http://127.0.0.1:9050", timeout=5)
|
|
||||||
content = response.read().decode("utf-8", errors="ignore")
|
|
||||||
return "tor" in content.lower()
|
|
||||||
except HTTPError as e:
|
|
||||||
return "tor" in str(e).lower()
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
@@ -139,7 +139,7 @@ class InventoryStore:
|
|||||||
def _load_merged_inventory(self) -> InventorySnapshot:
|
def _load_merged_inventory(self) -> InventorySnapshot:
|
||||||
"""
|
"""
|
||||||
Loads the evaluated inventory.
|
Loads the evaluated inventory.
|
||||||
After all merge operations with eventual nix code in lib.clan.
|
After all merge operations with eventual nix code in buildClan.
|
||||||
|
|
||||||
Evaluates clanInternals.inventoryClass.inventory with nix. Which is performant.
|
Evaluates clanInternals.inventoryClass.inventory with nix. Which is performant.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from clan_cli.cli import create_parser
|
from clan_cli.cli import create_parser
|
||||||
|
|
||||||
hidden_subcommands = ["machine", "b", "f", "m", "se", "st", "va", "net", "network"]
|
hidden_subcommands = ["machine", "b", "f", "m", "se", "st", "va"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -13,7 +13,12 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
perSystem =
|
perSystem =
|
||||||
{ config, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
self',
|
||||||
|
...
|
||||||
|
}:
|
||||||
{
|
{
|
||||||
packages = {
|
packages = {
|
||||||
agit = pkgs.callPackage ./agit { };
|
agit = pkgs.callPackage ./agit { };
|
||||||
@@ -28,6 +33,25 @@
|
|||||||
classgen = pkgs.callPackage ./classgen { };
|
classgen = pkgs.callPackage ./classgen { };
|
||||||
zerotierone = pkgs.callPackage ./zerotierone { };
|
zerotierone = pkgs.callPackage ./zerotierone { };
|
||||||
update-clan-core-for-checks = pkgs.callPackage ./update-clan-core-for-checks { };
|
update-clan-core-for-checks = pkgs.callPackage ./update-clan-core-for-checks { };
|
||||||
|
clan-autorefresh = pkgs.symlinkJoin {
|
||||||
|
name = "clan";
|
||||||
|
paths = [
|
||||||
|
(pkgs.writeScriptBin "clan" ''
|
||||||
|
#!/bin/sh
|
||||||
|
set -efu
|
||||||
|
|
||||||
|
system=$(nix config show system)
|
||||||
|
|
||||||
|
nix \
|
||||||
|
--extra-experimental-features 'flakes nix-command' \
|
||||||
|
run ".#clanInternals.clan-cli.$system" -- "$@"
|
||||||
|
'')
|
||||||
|
self'.packages.clan-cli
|
||||||
|
];
|
||||||
|
postBuild = ''
|
||||||
|
rm -r $out/lib
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,12 @@
|
|||||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{ self, clan-core, ... }:
|
||||||
self,
|
|
||||||
clan-core,
|
|
||||||
nixpkgs,
|
|
||||||
...
|
|
||||||
}@inputs:
|
|
||||||
let
|
let
|
||||||
# Usage see: https://docs.clan.lol
|
# Usage see: https://docs.clan.lol
|
||||||
clan = clan-core.lib.clan {
|
clan = clan-core.lib.clan {
|
||||||
inherit self;
|
inherit self;
|
||||||
imports = [ ./clan.nix ];
|
imports = [ ./clan.nix ];
|
||||||
specialArgs = { inherit inputs; };
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -22,7 +16,7 @@
|
|||||||
# Add the Clan cli tool to the dev shell.
|
# Add the Clan cli tool to the dev shell.
|
||||||
# Use "nix develop" to enter the dev shell.
|
# Use "nix develop" to enter the dev shell.
|
||||||
devShells =
|
devShells =
|
||||||
nixpkgs.lib.genAttrs
|
clan-core.inputs.nixpkgs.lib.genAttrs
|
||||||
[
|
[
|
||||||
"x86_64-linux"
|
"x86_64-linux"
|
||||||
"aarch64-linux"
|
"aarch64-linux"
|
||||||
@@ -31,7 +25,9 @@
|
|||||||
]
|
]
|
||||||
(system: {
|
(system: {
|
||||||
default = clan-core.inputs.nixpkgs.legacyPackages.${system}.mkShell {
|
default = clan-core.inputs.nixpkgs.legacyPackages.${system}.mkShell {
|
||||||
packages = [ clan-core.packages.${system}.clan-cli ];
|
packages = [
|
||||||
|
clan-core.packages.${system}.clan-autorefresh
|
||||||
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
perSystem =
|
perSystem =
|
||||||
{ pkgs, inputs', ... }:
|
{ pkgs, inputs', ... }:
|
||||||
{
|
{
|
||||||
devShells.default = pkgs.mkShell { packages = [ inputs'.clan-core.packages.clan-cli ]; };
|
devShells.default = pkgs.mkShell { packages = [ inputs'.clan-core.packages.clan-autorefresh ]; };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user