Compare commits
180 Commits
serve-json
...
fix-typogr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1e10b2c0a | ||
|
|
289732ad20 | ||
|
|
a50b6f7bc7 | ||
|
|
cdd241d8ff | ||
|
|
0803d9c864 | ||
|
|
7171864a5e | ||
|
|
7aa9a34168 | ||
|
|
0ec2c32ff8 | ||
|
|
ea2d6aab65 | ||
|
|
4101ebc45b | ||
|
|
45c7c42634 | ||
|
|
8baf4fcedd | ||
|
|
a41e0ba80f | ||
|
|
798d445f3e | ||
|
|
00bd003be4 | ||
|
|
5841432b6f | ||
|
|
1fb91ec161 | ||
|
|
fc16879336 | ||
|
|
290510ae74 | ||
|
|
7b926d43dc | ||
|
|
d91a44c7c5 | ||
|
|
a47ed71bb7 | ||
|
|
18f9df29da | ||
|
|
2438dc09a2 | ||
|
|
420412e60c | ||
|
|
aee6bc335b | ||
|
|
6ae679fb3d | ||
|
|
b40a13b4c5 | ||
|
|
dd2aa70efd | ||
|
|
2a9c9f7f2c | ||
|
|
82001544fd | ||
|
|
9f352aa362 | ||
|
|
35177ead40 | ||
|
|
1931c17513 | ||
|
|
b12debf373 | ||
|
|
0b3d362357 | ||
|
|
d8119f2308 | ||
|
|
ce36894ab1 | ||
|
|
c5f4f2e1d6 | ||
|
|
c861ffe07b | ||
|
|
6df980bc57 | ||
|
|
9d1d07b0ca | ||
|
|
24a774b5d6 | ||
|
|
442f673128 | ||
|
|
8905b5c5f1 | ||
|
|
3eff656dfa | ||
|
|
79e6f34c9e | ||
|
|
9c6e8f7735 | ||
|
|
cc4fd1369e | ||
|
|
7f32d6f81a | ||
|
|
a450ca10b8 | ||
|
|
06fbf32691 | ||
|
|
d4bd297439 | ||
|
|
acc8043f26 | ||
|
|
35e5d0daab | ||
|
|
e51c9ef1ad | ||
|
|
cdcbe3359a | ||
|
|
e5b51e6a2b | ||
|
|
694ebc5b30 | ||
|
|
ff2555cc4a | ||
|
|
016255459c | ||
|
|
14f03bcab0 | ||
|
|
4dc90b3d39 | ||
|
|
8cdce6c0c8 | ||
|
|
8904cf27a4 | ||
|
|
493194c124 | ||
|
|
5d1600a077 | ||
|
|
7daaacbddf | ||
|
|
30e18bbc66 | ||
|
|
16dffa99c0 | ||
|
|
58ad50b749 | ||
|
|
bc25074f5b | ||
|
|
c79916d06c | ||
|
|
4d53542f79 | ||
|
|
d3ef03aeb3 | ||
|
|
9949fac5ea | ||
|
|
6d236a6282 | ||
|
|
6e6a920796 | ||
|
|
99092a6ef2 | ||
|
|
1897b7bb06 | ||
|
|
878789cf38 | ||
|
|
8a59cf7ea3 | ||
|
|
7ade9cd222 | ||
|
|
447f619ecc | ||
|
|
657a55517b | ||
|
|
16a5b34ddf | ||
|
|
23f303b6ba | ||
|
|
84bf9f3bc5 | ||
|
|
48736011de | ||
|
|
cf5675b7f3 | ||
|
|
f0bbdad9ef | ||
|
|
5f83fe02a1 | ||
|
|
8cb92e143d | ||
|
|
73f5f887f3 | ||
|
|
db4e6c0be5 | ||
|
|
c24892f865 | ||
|
|
6badc14936 | ||
|
|
3d1fb401fd | ||
|
|
f2cdac75e2 | ||
|
|
5d6e35832c | ||
|
|
9aa9ba500e | ||
|
|
2934269279 | ||
|
|
1c7323c90a | ||
|
|
e667e03832 | ||
|
|
7f227b232c | ||
|
|
9d887805a8 | ||
|
|
244e1c7447 | ||
|
|
78911063a6 | ||
|
|
d86509e97b | ||
|
|
6de431df2c | ||
|
|
cda49b5b20 | ||
|
|
678841e64c | ||
|
|
74549164e4 | ||
|
|
6afe8695de | ||
|
|
460800b6fb | ||
|
|
5558bf3b9a | ||
|
|
62701f7730 | ||
|
|
a2f3e2e513 | ||
|
|
4867d467de | ||
|
|
d9685acc37 | ||
|
|
1aaa157f20 | ||
|
|
9a0ad4182f | ||
|
|
65d194af58 | ||
|
|
1f2f71ab03 | ||
|
|
f985187999 | ||
|
|
396a8d1e5e | ||
|
|
651f630080 | ||
|
|
21de41f1c0 | ||
|
|
98e5987e22 | ||
|
|
a77af2d379 | ||
|
|
ccde9e0ba6 | ||
|
|
6f6f582fe3 | ||
|
|
29a3140702 | ||
|
|
465eda24bc | ||
|
|
2888907109 | ||
|
|
f770f600c6 | ||
|
|
729f1673b3 | ||
|
|
7c95cb0177 | ||
|
|
b7f159aea3 | ||
|
|
06a0062311 | ||
|
|
aa840d9758 | ||
|
|
d1e6da0779 | ||
|
|
e6981ddd72 | ||
|
|
101c52f7c2 | ||
|
|
a83f301e59 | ||
|
|
5120d90b85 | ||
|
|
ea1e470502 | ||
|
|
f4d6edc501 | ||
|
|
cbbc235570 | ||
|
|
56d9256c02 | ||
|
|
e131d3d036 | ||
|
|
7f5b7b5057 | ||
|
|
c27fa9f56e | ||
|
|
1a1addb19d | ||
|
|
349da24b29 | ||
|
|
717f66b613 | ||
|
|
dcbc8c9a50 | ||
|
|
9834f413cc | ||
|
|
fb5645ae33 | ||
|
|
dc311d78e2 | ||
|
|
f0b1d8b2af | ||
|
|
7f0d55ef74 | ||
|
|
6e8860b3a0 | ||
|
|
5a5ec468c7 | ||
|
|
fbc2b889b5 | ||
|
|
fb094e8f3b | ||
|
|
e2eb26345f | ||
|
|
6f1a94e825 | ||
|
|
05951ffdb9 | ||
|
|
69de5f10c0 | ||
|
|
c01a191f3a | ||
|
|
dfe1a3e67f | ||
|
|
e975b67fad | ||
|
|
5c08893db0 | ||
|
|
cb679dbee2 | ||
|
|
f339ca0d85 | ||
|
|
547ba4276e | ||
|
|
cae63cc45d | ||
|
|
527b4b2e40 | ||
|
|
448c22c280 |
@@ -8,7 +8,7 @@ Our mission is simple: to democratize computing by providing tools that empower
|
||||
|
||||
## Features of Clan
|
||||
|
||||
- **Full-Stack System Deployment:** Utilize Clan’s toolkit alongside Nix's reliability to build and manage systems effortlessly.
|
||||
- **Full-Stack System Deployment:** Utilize Clan's toolkit alongside Nix's reliability to build and manage systems effortlessly.
|
||||
- **Overlay Networks:** Secure, private communication channels between devices.
|
||||
- **Virtual Machine Integration:** Seamless operation of VM applications within the main operating system.
|
||||
- **Robust Backup Management:** Long-term, self-hosted data preservation.
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"-i", ssh_conn.ssh_key,
|
||||
"--option", "store", os.environ['CLAN_TEST_STORE'],
|
||||
"--update-hardware-config", "nixos-facter",
|
||||
"--no-persist-state",
|
||||
]
|
||||
|
||||
subprocess.run(clan_cmd, check=True)
|
||||
@@ -275,7 +276,7 @@
|
||||
"${self.checks.x86_64-linux.clan-core-for-checks}",
|
||||
"${closureInfo}"
|
||||
)
|
||||
|
||||
|
||||
# Set up SSH connection
|
||||
ssh_conn = setup_ssh_connection(
|
||||
target,
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
jsonpath = /tmp/telegraf.json;
|
||||
jsonpath = "/tmp/telegraf.json";
|
||||
auth_user = "prometheus";
|
||||
in
|
||||
{
|
||||
|
||||
@@ -19,18 +20,24 @@
|
||||
builtins.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value.allowedTCPPorts = [ 9273 9990 ];
|
||||
value.allowedTCPPorts = [
|
||||
9273
|
||||
9990
|
||||
];
|
||||
}) settings.interfaces
|
||||
)
|
||||
);
|
||||
|
||||
systemd.services.telegsaf-json.script = "${pkgs.miniserve}/bin/miniserve -p 9990 ${jsonpath}";
|
||||
networking.firewall.allowedTCPPorts = lib.mkIf (settings.allowAllInterfaces == true) [
|
||||
9273
|
||||
9990
|
||||
];
|
||||
|
||||
networking.firewall.allowedTCPPorts = lib.mkIf (settings.allowAllInterfaces == true) [ 9273 ];
|
||||
clan.core.vars.generators."telegraf" = {
|
||||
|
||||
clan.core.vars.generators."telegraf-password" = {
|
||||
files.telegraf-password.neededFor = "users";
|
||||
files.telegraf-password.restartUnits = [ "telegraf.service" ];
|
||||
files.password.restartUnits = [ "telegraf.service" ];
|
||||
files.password-env.restartUnits = [ "telegraf.service" ];
|
||||
files.miniserve-auth.restartUnits = [ "telegraf.service" ];
|
||||
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
@@ -40,16 +47,22 @@
|
||||
|
||||
script = ''
|
||||
PASSWORD=$(xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n")
|
||||
echo "BASIC_AUTH_PWD=$PASSWORD" > "$out"/telegraf-password
|
||||
echo "BASIC_AUTH_PWD=$PASSWORD" > "$out"/password-env
|
||||
echo "${auth_user}:$PASSWORD" > "$out"/miniserve-auth
|
||||
echo "$PASSWORD" | tr -d "\n" > "$out"/password
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.telegraf-json = {
|
||||
enable = true;
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
script = "${pkgs.miniserve}/bin/miniserve -p 9990 ${jsonpath} --auth-file ${config.clan.core.vars.generators.telegraf.files.miniserve-auth.path}";
|
||||
};
|
||||
|
||||
services.telegraf = {
|
||||
enable = true;
|
||||
environmentFiles = [
|
||||
(builtins.toString
|
||||
config.clan.core.vars.generators."telegraf-password".files.telegraf-password.path
|
||||
)
|
||||
(builtins.toString config.clan.core.vars.generators.telegraf.files.password-env.path)
|
||||
];
|
||||
extraConfig = {
|
||||
agent.interval = "60s";
|
||||
@@ -64,32 +77,35 @@
|
||||
|
||||
exec =
|
||||
let
|
||||
currentSystemScript = pkgs.writeShellScript "current-system" ''
|
||||
printf "current_system,path=%s present=0\n" $(readlink /run/current-system)
|
||||
nixosSystems = pkgs.writeShellScript "current-system" ''
|
||||
printf "nixos_systems,current_system=%s,booted_system=%s,current_kernel=%s,booted_kernel=%s present=0\n" \
|
||||
"$(readlink /run/current-system)" "$(readlink /run/booted-system)" \
|
||||
"$(basename $(echo /run/current-system/kernel-modules/lib/modules/*))" \
|
||||
"$(basename $(echo /run/booted-system/kernel-modules/lib/modules/*))"
|
||||
'';
|
||||
in
|
||||
[
|
||||
{
|
||||
# Expose the path to current-system as metric. We use
|
||||
# this to check if the machine is up-to-date.
|
||||
commands = [ currentSystemScript ];
|
||||
commands = [ nixosSystems ];
|
||||
data_format = "influx";
|
||||
}
|
||||
];
|
||||
};
|
||||
# sadly there doesn'T seem to exist a telegraf http_client output plugin
|
||||
outputs.prometheus_client = {
|
||||
listen = ":9273";
|
||||
metric_version = 2;
|
||||
basic_username = "${auth_user}";
|
||||
basic_password = "$${BASIC_AUTH_PWD}";
|
||||
};
|
||||
|
||||
outputs.file = {
|
||||
files = [ jsonpath ];
|
||||
data_format = "json";
|
||||
json_timestamp_units = "1s";
|
||||
};
|
||||
|
||||
outputs.prometheus_client = {
|
||||
listen = ":9273";
|
||||
metric_version = 2;
|
||||
basic_username = "prometheus";
|
||||
basic_password = "$${BASIC_AUTH_PWD}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,6 +17,20 @@
|
||||
};
|
||||
};
|
||||
|
||||
# Deploy user Carol on all machines. Prompt only once and use the
|
||||
# same password on all machines. (`share = true`)
|
||||
user-carol = {
|
||||
module = {
|
||||
name = "users";
|
||||
input = "clan";
|
||||
};
|
||||
roles.default.tags.all = { };
|
||||
roles.default.settings = {
|
||||
user = "carol";
|
||||
share = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Deploy user bob only on his laptop. Prompt for a password.
|
||||
user-bob = {
|
||||
module = {
|
||||
@@ -29,3 +43,44 @@
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from `root-password` module
|
||||
|
||||
The deprecated `clan.root-password` module has been replaced by the `users` module. Here's how to migrate:
|
||||
|
||||
### 1. Update your flake configuration
|
||||
|
||||
Replace the `root-password` module import with a `users` service instance:
|
||||
|
||||
```nix
|
||||
# OLD - Remove this from your nixosModules:
|
||||
imports = [
|
||||
self.inputs.clan-core.clanModules.root-password
|
||||
];
|
||||
|
||||
# NEW - Add to inventory.instances or machines/flake-module.nix:
|
||||
instances = {
|
||||
users-root = {
|
||||
module.name = "users";
|
||||
module.input = "clan-core";
|
||||
roles.default.tags.nixos = { };
|
||||
roles.default.settings = {
|
||||
user = "root";
|
||||
prompt = false; # Set to true if you want to be prompted
|
||||
groups = [ ];
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Migrate vars
|
||||
|
||||
The vars structure has changed from `root-password` to `user-password-root`:
|
||||
|
||||
```bash
|
||||
# For each machine, rename the vars directories:
|
||||
cd vars/per-machine/<machine-name>/
|
||||
mv root-password user-password-root
|
||||
mv user-password-root/password-hash user-password-root/user-password-hash
|
||||
mv user-password-root/password user-password-root/user-password
|
||||
```
|
||||
|
||||
@@ -59,6 +59,17 @@
|
||||
- "input" - Allows the user to access input devices.
|
||||
'';
|
||||
};
|
||||
share = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Weather the user should have the same password on all machines.
|
||||
|
||||
By default, you will be prompted for a new password for every host.
|
||||
Unless `generate` is set to `true`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,7 +93,6 @@
|
||||
};
|
||||
|
||||
clan.core.vars.generators."user-password-${settings.user}" = {
|
||||
|
||||
files.user-password-hash.neededFor = "users";
|
||||
files.user-password-hash.restartUnits = lib.optional (config.services.userborn.enable) "userborn.service";
|
||||
files.user-password.deploy = false;
|
||||
@@ -107,6 +117,8 @@
|
||||
pkgs.mkpasswd
|
||||
];
|
||||
|
||||
share = settings.share;
|
||||
|
||||
script =
|
||||
(
|
||||
if settings.prompt then
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IPv6 address allocator for WireGuard networks.
|
||||
"""IPv6 address allocator for WireGuard networks.
|
||||
|
||||
Network layout:
|
||||
- Base network: /40 ULA prefix (fd00::/8 + 32 bits from hash)
|
||||
@@ -20,8 +19,7 @@ def hash_string(s: str) -> str:
|
||||
|
||||
|
||||
def generate_ula_prefix(instance_name: str) -> ipaddress.IPv6Network:
|
||||
"""
|
||||
Generate a /40 ULA prefix from instance name.
|
||||
"""Generate a /40 ULA prefix from instance name.
|
||||
|
||||
Format: fd{32-bit hash}/40
|
||||
This gives us fd00:0000:0000::/40 through fdff:ffff:ff00::/40
|
||||
@@ -46,10 +44,10 @@ def generate_ula_prefix(instance_name: str) -> ipaddress.IPv6Network:
|
||||
|
||||
|
||||
def generate_controller_subnet(
|
||||
base_network: ipaddress.IPv6Network, controller_name: str
|
||||
base_network: ipaddress.IPv6Network,
|
||||
controller_name: str,
|
||||
) -> ipaddress.IPv6Network:
|
||||
"""
|
||||
Generate a /56 subnet for a controller from the base /40 network.
|
||||
"""Generate a /56 subnet for a controller from the base /40 network.
|
||||
|
||||
We have 16 bits (40 to 56) to allocate controller subnets.
|
||||
This allows for 65,536 possible controller subnets.
|
||||
@@ -68,8 +66,7 @@ def generate_controller_subnet(
|
||||
|
||||
|
||||
def generate_peer_suffix(peer_name: str) -> str:
|
||||
"""
|
||||
Generate a unique 64-bit host suffix for a peer.
|
||||
"""Generate a unique 64-bit host suffix for a peer.
|
||||
|
||||
This suffix will be used in all controller subnets to create unique addresses.
|
||||
Format: :xxxx:xxxx:xxxx:xxxx (64 bits)
|
||||
@@ -86,7 +83,7 @@ def generate_peer_suffix(peer_name: str) -> str:
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 4:
|
||||
print(
|
||||
"Usage: ipv6_allocator.py <output_dir> <instance_name> <controller|peer> <machine_name>"
|
||||
"Usage: ipv6_allocator.py <output_dir> <instance_name> <controller|peer> <machine_name>",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
18
devFlake/flake.lock
generated
@@ -3,10 +3,10 @@
|
||||
"clan-core-for-checks": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1755649112,
|
||||
"narHash": "sha256-Tk/Sjlb7W+5usS4upH0la4aGYs2velkHADnhhn2xO88=",
|
||||
"lastModified": 1756081310,
|
||||
"narHash": "sha256-wj1H5Pr6w4AsB+nG3K07SgSIDZ7jDCkGnh5XXWLdtk8=",
|
||||
"ref": "main",
|
||||
"rev": "bdab3e23af4a9715dfa6346f344f1bd428508672",
|
||||
"rev": "7b926d43dc361cd8d3ad3c14a2e7e75375b7d215",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "https://git.clan.lol/clan/clan-core"
|
||||
@@ -84,11 +84,11 @@
|
||||
},
|
||||
"nixpkgs-dev": {
|
||||
"locked": {
|
||||
"lastModified": 1755628699,
|
||||
"narHash": "sha256-IAM29K+Cz9pu90lDDkJpmOpWzQ9Ed0FNkRJcereY+rM=",
|
||||
"lastModified": 1756050191,
|
||||
"narHash": "sha256-lMtTT4rv5On7D0P4Z+k7UkvbAKKuVGRbJi/VJeRCQwI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "01b1b3809cfa3b0fbca8d75c9cf4b2efdb8182cf",
|
||||
"rev": "759dcc6981cd4aa222d36069f78fe7064d563305",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -165,11 +165,11 @@
|
||||
"nixpkgs": []
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1754847726,
|
||||
"narHash": "sha256-2vX8QjO5lRsDbNYvN9hVHXLU6oMl+V/PsmIiJREG4rE=",
|
||||
"lastModified": 1755934250,
|
||||
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "7d81f6fb2e19bf84f1c65135d1060d829fae2408",
|
||||
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
2
docs/.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
/site/reference
|
||||
/site/static
|
||||
/site/options-page
|
||||
/site/options
|
||||
/site/openapi.json
|
||||
!/site/static/extra.css
|
||||
|
||||
@@ -6,7 +6,7 @@ edit_uri: _edit/main/docs/docs/
|
||||
|
||||
validation:
|
||||
omitted_files: warn
|
||||
absolute_links: warn
|
||||
absolute_links: ignore
|
||||
unrecognized_links: warn
|
||||
|
||||
markdown_extensions:
|
||||
@@ -64,7 +64,7 @@ nav:
|
||||
- Disk Encryption: guides/disk-encryption.md
|
||||
- Age Plugins: guides/age-plugins.md
|
||||
- Secrets management: guides/secrets.md
|
||||
- Target Host: guides/target-host.md
|
||||
- Networking: guides/networking.md
|
||||
- Zerotier VPN: guides/mesh-vpn.md
|
||||
- Secure Boot: guides/secure-boot.md
|
||||
- Flake-parts: guides/flake-parts.md
|
||||
@@ -78,7 +78,7 @@ nav:
|
||||
- Writing a Disko Template: guides/disko-templates/community.md
|
||||
- Migrations:
|
||||
- Migrate existing Flakes: guides/migrations/migration-guide.md
|
||||
- Migrate inventory Services: guides/migrations/migrate-inventory-services.md
|
||||
- Migrate from clan modules to services: guides/migrations/migrate-inventory-services.md
|
||||
- Facts Vars Migration: guides/migrations/migration-facts-vars.md
|
||||
- Disk id: guides/migrations/disk-id.md
|
||||
- Concepts:
|
||||
@@ -88,7 +88,7 @@ nav:
|
||||
- Templates: concepts/templates.md
|
||||
- Reference:
|
||||
- Overview: reference/index.md
|
||||
- Clan Options: options.md
|
||||
- Browse Options: "/options"
|
||||
- Services:
|
||||
- Overview:
|
||||
- reference/clanServices/index.md
|
||||
@@ -155,6 +155,7 @@ nav:
|
||||
- 05-deployment-parameters: decisions/05-deployment-parameters.md
|
||||
- Template: decisions/_template.md
|
||||
- Glossary: reference/glossary.md
|
||||
- Browse Options: "/options"
|
||||
|
||||
docs_dir: site
|
||||
site_dir: out
|
||||
|
||||
@@ -54,9 +54,9 @@ pkgs.stdenv.mkDerivation {
|
||||
chmod -R +w ./site/reference
|
||||
echo "Generated API documentation in './site/reference/' "
|
||||
|
||||
rm -r ./site/options-page || true
|
||||
cp -r ${docs-options} ./site/options-page
|
||||
chmod -R +w ./site/options-page
|
||||
rm -rf ./site/options
|
||||
cp -r ${docs-options} ./site/options
|
||||
chmod -R +w ./site/options
|
||||
|
||||
mkdir -p ./site/static/asciinema-player
|
||||
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
serviceModules = self.clan.modules;
|
||||
|
||||
baseHref = "/options-page/";
|
||||
baseHref = "/options/";
|
||||
|
||||
getRoles =
|
||||
module:
|
||||
@@ -126,7 +126,7 @@
|
||||
nestedSettingsOption = mkOption {
|
||||
type = types.raw;
|
||||
description = ''
|
||||
See [instances.${name}.roles.${roleName}.settings](${baseHref}?option_scope=0&option=instances.${name}.roles.${roleName}.settings)
|
||||
See [instances.${name}.roles.${roleName}.settings](${baseHref}?option_scope=0&option=inventory.instances.${name}.roles.${roleName}.settings)
|
||||
'';
|
||||
};
|
||||
settingsOption = mkOption {
|
||||
@@ -161,6 +161,42 @@
|
||||
}
|
||||
];
|
||||
|
||||
baseModule =
|
||||
# Module
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = (import (pkgs.path + "/nixos/modules/module-list.nix"));
|
||||
nixpkgs.pkgs = pkgs;
|
||||
clan.core.name = "dummy";
|
||||
system.stateVersion = config.system.nixos.release;
|
||||
# Set this to work around a bug where `clan.core.settings.machine.name`
|
||||
# is forced due to `networking.interfaces` being forced
|
||||
# somewhere in the nixpkgs options
|
||||
facter.detected.dhcp.enable = lib.mkForce false;
|
||||
};
|
||||
|
||||
evalClanModules =
|
||||
let
|
||||
evaled = lib.evalModules {
|
||||
class = "nixos";
|
||||
modules = [
|
||||
baseModule
|
||||
{
|
||||
clan.core.settings.directory = self;
|
||||
}
|
||||
self.nixosModules.clanCore
|
||||
];
|
||||
};
|
||||
in
|
||||
evaled;
|
||||
|
||||
coreOptions =
|
||||
(pkgs.nixosOptionsDoc {
|
||||
options = (evalClanModules.options).clan.core or { };
|
||||
warningsAreErrors = true;
|
||||
transformOptions = self.clanLib.docs.stripStorePathsFromDeclarations;
|
||||
}).optionsJSON;
|
||||
|
||||
in
|
||||
{
|
||||
# Uncomment for debugging
|
||||
@@ -175,10 +211,17 @@
|
||||
# scopes = mapAttrsToList mkScope serviceModules;
|
||||
scopes = [
|
||||
{
|
||||
name = "Clan";
|
||||
inherit baseHref;
|
||||
name = "Flake Options (clan.nix file)";
|
||||
modules = docModules;
|
||||
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
|
||||
}
|
||||
{
|
||||
name = "Machine Options (clan.core NixOS options)";
|
||||
optionsJSON = "${coreOptions}/share/doc/nixos/options.json";
|
||||
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
|
||||
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ from typing import Any
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.services.modules import (
|
||||
CategoryInfo,
|
||||
Frontmatter,
|
||||
ModuleManifest,
|
||||
)
|
||||
|
||||
# Get environment variables
|
||||
@@ -66,8 +66,7 @@ def render_option_header(name: str) -> str:
|
||||
|
||||
|
||||
def join_lines_with_indentation(lines: list[str], indent: int = 4) -> str:
|
||||
"""
|
||||
Joins multiple lines with a specified number of whitespace characters as indentation.
|
||||
"""Joins multiple lines with a specified number of whitespace characters as indentation.
|
||||
|
||||
Args:
|
||||
lines (list of str): The lines of text to join.
|
||||
@@ -75,6 +74,7 @@ def join_lines_with_indentation(lines: list[str], indent: int = 4) -> str:
|
||||
|
||||
Returns:
|
||||
str: The indented and concatenated string.
|
||||
|
||||
"""
|
||||
# Create the indentation string (e.g., four spaces)
|
||||
indent_str = " " * indent
|
||||
@@ -161,7 +161,10 @@ def render_option(
|
||||
|
||||
|
||||
def print_options(
|
||||
options_file: str, head: str, no_options: str, replace_prefix: str | None = None
|
||||
options_file: str,
|
||||
head: str,
|
||||
no_options: str,
|
||||
replace_prefix: str | None = None,
|
||||
) -> str:
|
||||
res = ""
|
||||
with (Path(options_file) / "share/doc/nixos/options.json").open() as f:
|
||||
@@ -176,9 +179,8 @@ def print_options(
|
||||
return res
|
||||
|
||||
|
||||
def module_header(module_name: str, has_inventory_feature: bool = False) -> str:
|
||||
indicator = " 🔹" if has_inventory_feature else ""
|
||||
return f"# {module_name}{indicator}\n\n"
|
||||
def module_header(module_name: str) -> str:
|
||||
return f"# {module_name}\n\n"
|
||||
|
||||
|
||||
clan_core_descr = """
|
||||
@@ -236,7 +238,7 @@ def produce_clan_core_docs() -> None:
|
||||
for submodule_name, split_options in split.items():
|
||||
outfile = f"{module_name}/{submodule_name}.md"
|
||||
print(
|
||||
f"[clan_core.{submodule_name}] Rendering option of: {submodule_name}... {outfile}"
|
||||
f"[clan_core.{submodule_name}] Rendering option of: {submodule_name}... {outfile}",
|
||||
)
|
||||
init_level = 1
|
||||
root = options_to_tree(split_options, debug=True)
|
||||
@@ -271,56 +273,9 @@ def produce_clan_core_docs() -> None:
|
||||
of.write(output)
|
||||
|
||||
|
||||
def render_roles(roles: list[str] | None, module_name: str) -> str:
|
||||
if roles:
|
||||
roles_list = "\n".join([f"- `{r}`" for r in roles])
|
||||
return (
|
||||
f"""
|
||||
### Roles
|
||||
|
||||
This module can be used via predefined roles
|
||||
|
||||
{roles_list}
|
||||
"""
|
||||
"""
|
||||
Every role has its own configuration options, which are each listed below.
|
||||
|
||||
For more information, see the [inventory guide](../../concepts/inventory.md).
|
||||
|
||||
??? Example
|
||||
For example the `admin` module adds the following options globally to all machines where it is used.
|
||||
|
||||
`clan.admin.allowedkeys`
|
||||
|
||||
```nix
|
||||
clan-core.lib.clan {
|
||||
inventory.services = {
|
||||
admin.me = {
|
||||
roles.default.machines = [ "jon" ];
|
||||
config.allowedkeys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQD..." ];
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
"""
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
clan_modules_descr = """
|
||||
Clan modules are [NixOS modules](https://wiki.nixos.org/wiki/NixOS_modules)
|
||||
which have been enhanced with additional features provided by Clan, with
|
||||
certain option types restricted to enable configuration through a graphical
|
||||
interface.
|
||||
|
||||
!!! note "🔹"
|
||||
Modules with this indicator support the [inventory](../../concepts/inventory.md) feature.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def render_categories(
|
||||
categories: list[str], categories_info: dict[str, CategoryInfo]
|
||||
categories: list[str],
|
||||
categories_info: dict[str, CategoryInfo],
|
||||
) -> str:
|
||||
res = """<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">"""
|
||||
for cat in categories:
|
||||
@@ -385,10 +340,10 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
|
||||
# output += f"`clan.modules.{module_name}`\n"
|
||||
output += f"*{module_info['manifest']['description']}*\n"
|
||||
|
||||
fm = Frontmatter("")
|
||||
# output += "## Categories\n\n"
|
||||
output += render_categories(
|
||||
module_info["manifest"]["categories"], fm.categories_info
|
||||
module_info["manifest"]["categories"],
|
||||
ModuleManifest.categories_info(),
|
||||
)
|
||||
|
||||
output += f"{module_info['manifest']['readme']}\n"
|
||||
@@ -397,7 +352,7 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
|
||||
|
||||
output += f"The {module_name} module has the following roles:\n\n"
|
||||
|
||||
for role_name, _ in module_info["roles"].items():
|
||||
for role_name in module_info["roles"]:
|
||||
output += f"- {role_name}\n"
|
||||
|
||||
for role_name, role_filename in module_info["roles"].items():
|
||||
@@ -417,35 +372,8 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
|
||||
of.write(output)
|
||||
|
||||
|
||||
def build_option_card(module_name: str, frontmatter: Frontmatter) -> str:
|
||||
"""
|
||||
Build the overview index card for each reference target option.
|
||||
"""
|
||||
|
||||
def indent_all(text: str, indent_size: int = 4) -> str:
|
||||
"""
|
||||
Indent all lines in a string.
|
||||
"""
|
||||
indent = " " * indent_size
|
||||
lines = text.split("\n")
|
||||
indented_text = indent + ("\n" + indent).join(lines)
|
||||
return indented_text
|
||||
|
||||
def to_md_li(module_name: str, frontmatter: Frontmatter) -> str:
|
||||
md_li = (
|
||||
f"""- **[{module_name}](./{"-".join(module_name.split(" "))}.md)**\n\n"""
|
||||
)
|
||||
md_li += f"""{indent_all("---", 4)}\n\n"""
|
||||
fmd = f"\n{frontmatter.description.strip()}" if frontmatter.description else ""
|
||||
md_li += f"""{indent_all(fmd, 4)}"""
|
||||
return md_li
|
||||
|
||||
return f"{to_md_li(module_name, frontmatter)}\n\n"
|
||||
|
||||
|
||||
def split_options_by_root(options: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
"""
|
||||
Split the flat dictionary of options into a dict of which each entry will construct complete option trees.
|
||||
"""Split the flat dictionary of options into a dict of which each entry will construct complete option trees.
|
||||
{
|
||||
"a": { Data }
|
||||
"a.b": { Data }
|
||||
@@ -529,9 +457,7 @@ def option_short_name(option_name: str) -> str:
|
||||
|
||||
|
||||
def options_to_tree(options: dict[str, Any], debug: bool = False) -> Option:
|
||||
"""
|
||||
Convert the options dictionary to a tree structure.
|
||||
"""
|
||||
"""Convert the options dictionary to a tree structure."""
|
||||
|
||||
# Helper function to create nested structure
|
||||
def add_to_tree(path_parts: list[str], info: Any, current_node: Option) -> None:
|
||||
@@ -583,22 +509,24 @@ def options_to_tree(options: dict[str, Any], debug: bool = False) -> Option:
|
||||
|
||||
|
||||
def options_docs_from_tree(
|
||||
root: Option, init_level: int = 1, prefix: list[str] | None = None
|
||||
root: Option,
|
||||
init_level: int = 1,
|
||||
prefix: list[str] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
eender the options from the tree structure.
|
||||
"""Eender the options from the tree structure.
|
||||
|
||||
Args:
|
||||
root (Option): The root option node.
|
||||
init_level (int): The initial level of indentation.
|
||||
prefix (list str): Will be printed as common prefix of all attribute names.
|
||||
|
||||
"""
|
||||
|
||||
def render_tree(option: Option, level: int = init_level) -> str:
|
||||
output = ""
|
||||
|
||||
should_render = not option.name.startswith("<") and not option.name.startswith(
|
||||
"_"
|
||||
"_",
|
||||
)
|
||||
if should_render:
|
||||
# short_name = option_short_name(option.name)
|
||||
@@ -623,7 +551,7 @@ def options_docs_from_tree(
|
||||
return md
|
||||
|
||||
|
||||
if __name__ == "__main__": #
|
||||
if __name__ == "__main__":
|
||||
produce_clan_core_docs()
|
||||
|
||||
produce_clan_service_author_docs()
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
# Auto-included Files
|
||||
|
||||
Clan automatically imports the following files from a directory and registers them.
|
||||
Clan automatically imports specific files from each machine directory and registers them, reducing the need for manual configuration.
|
||||
|
||||
## Machine registration
|
||||
## Machine Registration
|
||||
|
||||
Every folder `machines/{machineName}` will be registered automatically as a Clan machine.
|
||||
Every folder under `machines/{machineName}` is automatically registered as a Clan machine.
|
||||
|
||||
!!! info "Automatically loaded files"
|
||||
!!! info "Files loaded automatically for each machine"
|
||||
|
||||
The following files are loaded automatically for each Clan machine:
|
||||
The following files are detected and imported for every Clan machine:
|
||||
|
||||
- [x] `machines/{machineName}/configuration.nix`
|
||||
- [x] `machines/{machineName}/hardware-configuration.nix`
|
||||
- [x] `machines/{machineName}/facter.json` Automatically configured, for further information see [nixos-facter](https://clan.lol/blog/nixos-facter/)
|
||||
- [x] `machines/{machineName}/disko.nix` Automatically loaded, for further information see the [disko docs](https://github.com/nix-community/disko/blob/master/docs/quickstart.md).
|
||||
- [x] `machines/{machineName}/configuration.nix`
|
||||
Main configuration file for the machine.
|
||||
|
||||
- [x] `machines/{machineName}/hardware-configuration.nix`
|
||||
Hardware-specific configuration generated by NixOS.
|
||||
|
||||
- [x] `machines/{machineName}/facter.json`
|
||||
Contains system facts. Automatically generated — see [nixos-facter](https://clan.lol/blog/nixos-facter/) for details.
|
||||
|
||||
- [x] `machines/{machineName}/disko.nix`
|
||||
Disk layout configuration. See the [disko quickstart](https://github.com/nix-community/disko/blob/master/docs/quickstart.md) for more info.
|
||||
|
||||
## Other Auto-included Files
|
||||
|
||||
* **`inventory.json`**
|
||||
Managed by Clan's API.
|
||||
Merges with `clan.inventory` to extend the inventory.
|
||||
|
||||
* **`.clan-flake`**
|
||||
Sentinel file to be used to locate the root of a Clan repository.
|
||||
Falls back to `.git`, `.hg`, `.svn`, or `flake.nix` if not found.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Using `clanServices`
|
||||
|
||||
Clan’s `clanServices` system is a composable way to define and deploy services across machines.
|
||||
Clan's `clanServices` system is a composable way to define and deploy services across machines.
|
||||
|
||||
This guide shows how to **instantiate** a `clanService`, explains how service definitions are structured in your inventory, and how to pick or create services from modules exposed by flakes.
|
||||
|
||||
@@ -130,7 +130,7 @@ inventory.instances = {
|
||||
|
||||
## Picking a clanService
|
||||
|
||||
You can use services exposed by Clan’s core module library, `clan-core`.
|
||||
You can use services exposed by Clan's core module library, `clan-core`.
|
||||
|
||||
🔗 See: [List of Available Services in clan-core](../reference/clanServices/index.md)
|
||||
|
||||
@@ -152,7 +152,7 @@ You might expose your service module from your flake — this makes it easy for
|
||||
|
||||
---
|
||||
|
||||
## What’s Next?
|
||||
## What's Next?
|
||||
|
||||
* [Author your own clanService →](../guides/services/community.md)
|
||||
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
|
||||
|
||||
@@ -27,7 +27,7 @@ inputs = {
|
||||
|
||||
## Import the Clan flake-parts Module
|
||||
|
||||
After updating your flake inputs, the next step is to import the Clan flake-parts module. This will make the [Clan options](../options.md) available within `mkFlake`.
|
||||
After updating your flake inputs, the next step is to import the Clan flake-parts module. This will make the [Clan options](/options) available within `mkFlake`.
|
||||
|
||||
```nix
|
||||
{
|
||||
|
||||
@@ -271,7 +271,7 @@ The following table shows the migration status of each deprecated clanModule:
|
||||
| `nginx` | ❌ Removed | |
|
||||
| `packages` | ✅ [Migrated](../../reference/clanServices/packages.md) | |
|
||||
| `postgresql` | ❌ Removed | Now an [option](../../reference/clan.core/settings.md) |
|
||||
| `root-password` | ✅ [Migrated](../../reference/clanServices/users.md) | |
|
||||
| `root-password` | ✅ [Migrated](../../reference/clanServices/users.md) | See [migration guide](../../reference/clanServices/users.md#migration-from-root-password-module) |
|
||||
| `single-disk` | ❌ Removed | |
|
||||
| `sshd` | ✅ [Migrated](../../reference/clanServices/sshd.md) | |
|
||||
| `state-version` | ✅ [Migrated](../../reference/clanServices/state-version.md) | |
|
||||
|
||||
184
docs/site/guides/networking.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Connecting to Your Machines
|
||||
|
||||
Clan provides automatic networking with fallback mechanisms to reliably connect to your machines.
|
||||
|
||||
## Option 1: Automatic Networking with Fallback (Recommended)
|
||||
|
||||
Clan's networking module automatically manages connections through various network technologies with intelligent fallback. When you run `clan ssh` or `clan machines update`, Clan tries each configured network by priority until one succeeds.
|
||||
|
||||
### Basic Setup with Internet Service
|
||||
|
||||
For machines with public IPs or DNS names, use the `internet` service to configure direct SSH while keeping fallback options:
|
||||
|
||||
```{.nix title="flake.nix" hl_lines="7-10 14-16"}
|
||||
{
|
||||
outputs = { self, clan-core, ... }:
|
||||
let
|
||||
clan = clan-core.lib.clan {
|
||||
inventory.instances = {
|
||||
# Direct SSH with fallback support
|
||||
internet = {
|
||||
roles.default.machines.server1 = {
|
||||
settings.address = "server1.example.com";
|
||||
};
|
||||
roles.default.machines.server2 = {
|
||||
settings.address = "192.168.1.100";
|
||||
};
|
||||
};
|
||||
|
||||
# Fallback: Secure connections via Tor
|
||||
tor = {
|
||||
roles.server.tags.nixos = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan.config) nixosConfigurations;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Setup with Multiple Networks
|
||||
|
||||
```{.nix title="flake.nix" hl_lines="7-10 13-16 19-21"}
|
||||
{
|
||||
outputs = { self, clan-core, ... }:
|
||||
let
|
||||
clan = clan-core.lib.clan {
|
||||
inventory.instances = {
|
||||
# Priority 1: Try direct connection first
|
||||
internet = {
|
||||
roles.default.machines.publicserver = {
|
||||
settings.address = "public.example.com";
|
||||
};
|
||||
};
|
||||
|
||||
# Priority 2: VPN for internal machines
|
||||
zerotier = {
|
||||
roles.controller.machines."controller" = { };
|
||||
roles.peer.tags.nixos = { };
|
||||
};
|
||||
|
||||
# Priority 3: Tor as universal fallback
|
||||
tor = {
|
||||
roles.server.tags.nixos = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan.config) nixosConfigurations;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
Clan automatically tries networks in order of priority:
|
||||
1. Direct internet connections (if configured)
|
||||
2. VPN networks (ZeroTier, Tailscale, etc.)
|
||||
3. Tor hidden services
|
||||
4. Any other configured networks
|
||||
|
||||
If one network fails, Clan automatically tries the next.
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# View all configured networks and their status
|
||||
clan network list
|
||||
|
||||
# Test connectivity through all networks
|
||||
clan network ping machine1
|
||||
|
||||
# Show complete network topology
|
||||
clan network overview
|
||||
```
|
||||
|
||||
## Option 2: Manual targetHost (Bypasses Fallback!)
|
||||
|
||||
!!! warning
|
||||
Setting `targetHost` directly **disables all automatic networking and fallback**. Only use this if you need complete control and don't want Clan's intelligent connection management.
|
||||
|
||||
### Using Inventory (For Static Addresses)
|
||||
|
||||
Use inventory-level `targetHost` when the address is **static** and doesn't depend on NixOS configuration:
|
||||
|
||||
```{.nix title="flake.nix" hl_lines="8"}
|
||||
{
|
||||
outputs = { self, clan-core, ... }:
|
||||
let
|
||||
clan = clan-core.lib.clan {
|
||||
inventory.machines.server = {
|
||||
# WARNING: This bypasses all networking modules!
|
||||
# Use for: Static IPs, DNS names, known hostnames
|
||||
deploy.targetHost = "root@192.168.1.100";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan.config) nixosConfigurations;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**When to use inventory-level:**
|
||||
- Static IP addresses: `"root@192.168.1.100"`
|
||||
- DNS names: `"user@server.example.com"`
|
||||
- Any address that doesn't change based on machine configuration
|
||||
|
||||
### Using NixOS Configuration (For Dynamic Addresses)
|
||||
|
||||
Use machine-level `targetHost` when you need to **interpolate values from the NixOS configuration**:
|
||||
|
||||
```{.nix title="flake.nix" hl_lines="7"}
|
||||
{
|
||||
outputs = { self, clan-core, ... }:
|
||||
let
|
||||
clan = clan-core.lib.clan {
|
||||
machines.server = { config, ... }: {
|
||||
# WARNING: This also bypasses all networking modules!
|
||||
# REQUIRED for: Addresses that depend on NixOS config
|
||||
clan.core.networking.targetHost = "root@${config.networking.hostName}.local";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan.config) nixosConfigurations;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**When to use machine-level (NixOS config):**
|
||||
- Using hostName from config: `"root@${config.networking.hostName}.local"`
|
||||
- Building from multiple config values: `"${config.users.users.deploy.name}@${config.networking.hostName}"`
|
||||
- Any address that depends on evaluated NixOS configuration
|
||||
|
||||
!!! info "Key Difference"
|
||||
**Inventory-level** (`deploy.targetHost`) is evaluated immediately and works with static strings.
|
||||
**Machine-level** (`clan.core.networking.targetHost`) is evaluated after NixOS configuration and can access `config.*` values.
|
||||
|
||||
## Quick Decision Guide
|
||||
|
||||
| Scenario | Recommended Approach | Why |
|
||||
|----------|---------------------|-----|
|
||||
| Public servers | `internet` service | Keeps fallback options |
|
||||
| Mixed infrastructure | Multiple networks | Automatic failover |
|
||||
| Machines behind NAT | ZeroTier/Tor | NAT traversal with fallback |
|
||||
| Testing/debugging | Manual targetHost | Full control, no magic |
|
||||
| Single static machine | Manual targetHost | Simple, no overhead |
|
||||
|
||||
## Command-Line Override
|
||||
|
||||
The `--target-host` flag bypasses ALL networking configuration:
|
||||
|
||||
```bash
|
||||
# Emergency access - ignores all networking config
|
||||
clan machines update server --target-host root@backup-ip.com
|
||||
|
||||
# Direct SSH - no fallback attempted
|
||||
clan ssh laptop --target-host user@10.0.0.5
|
||||
```
|
||||
|
||||
Use this for debugging or emergency access when automatic networking isn't working.
|
||||
@@ -1,84 +0,0 @@
|
||||
# How to Set `targetHost` for a Machine
|
||||
|
||||
The `targetHost` defines where the machine can be reached for operations like SSH or deployment. You can set it in two ways, depending on your use case.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Option 1: Use the Inventory (Recommended for Static Hosts)
|
||||
|
||||
If the hostname is **static**, like `server.example.com`, set it in the **inventory**:
|
||||
|
||||
```{.nix title="flake.nix" hl_lines="8"}
|
||||
{
|
||||
# edlided
|
||||
outputs =
|
||||
{ self, clan-core, ... }:
|
||||
let
|
||||
# Sometimes this attribute set is defined in clan.nix
|
||||
clan = clan-core.lib.clan {
|
||||
inventory.machines.jon = {
|
||||
deploy.targetHost = "root@server.example.com";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
|
||||
# elided
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This is fast, simple and explicit, and doesn’t require evaluating the NixOS config. We can also displayed it in the clan-cli or clan-app.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Option 2: Use NixOS (Only for Dynamic Hosts)
|
||||
|
||||
If your target host depends on a **dynamic expression** (like using the machine’s evaluated FQDN), set it inside the NixOS module:
|
||||
|
||||
```{.nix title="flake.nix" hl_lines="8"}
|
||||
{
|
||||
# edlided
|
||||
outputs =
|
||||
{ self, clan-core, ... }:
|
||||
let
|
||||
# Sometimes this attribute set is defined in clan.nix
|
||||
clan = clan-core.lib.clan {
|
||||
machines.jon = {config, ...}: {
|
||||
clan.core.networking.targetHost = "jon@${config.networking.fqdn}";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
|
||||
# elided
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Use this **only if the value cannot be made static**, because it’s slower and won't be displayed in the clan-cli or clan-app yet.
|
||||
|
||||
---
|
||||
|
||||
## 📝 TL;DR
|
||||
|
||||
| Use Case | Use Inventory? | Example |
|
||||
| ------------------------- | -------------- | -------------------------------- |
|
||||
| Static hostname | ✅ Yes | `root@server.example.com` |
|
||||
| Dynamic config expression | ❌ No | `jon@${config.networking.fqdn}` |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Coming Soon: Unified Networking Module
|
||||
|
||||
We’re working on a new networking module that will automatically do all of this for you.
|
||||
|
||||
- Easier to use
|
||||
- Sane defaults: You’ll always be able to reach the machine — no need to worry about hostnames.
|
||||
- ✨ Migration from **either method** will be supported and simple.
|
||||
|
||||
## Summary
|
||||
|
||||
- Ask: *Does this hostname dynamically change based on NixOS config?*
|
||||
- If **no**, use the inventory.
|
||||
- If **yes**, then use NixOS config.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
template: options.html
|
||||
---
|
||||
|
||||
|
||||
<iframe src="/options-page/" height="1000" width="100%"></iframe>
|
||||
@@ -4,7 +4,7 @@ This section of the site provides an overview of available options and commands
|
||||
|
||||
---
|
||||
|
||||
- [Clan Configuration Option](../options.md) - for defining a Clan
|
||||
- [Clan Configuration Option](/options) - for defining a Clan
|
||||
- Learn how to use the [Clan CLI](./cli/index.md)
|
||||
- Explore available [services](./clanServices/index.md)
|
||||
- [NixOS Configuration Options](./clan.core/index.md) - Additional options avilable on a NixOS machine.
|
||||
|
||||
28
flake.lock
generated
@@ -13,11 +13,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753067306,
|
||||
"narHash": "sha256-jyoEbaXa8/MwVQ+PajUdT63y3gYhgD9o7snO/SLaikw=",
|
||||
"rev": "18dfd42bdb2cfff510b8c74206005f733e38d8b9",
|
||||
"lastModified": 1756091210,
|
||||
"narHash": "sha256-oEUEAZnLbNHi8ti4jY8x10yWcIkYoFc5XD+2hjmOS04=",
|
||||
"rev": "eb831bca21476fa8f6df26cb39e076842634700d",
|
||||
"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/eb831bca21476fa8f6df26cb39e076842634700d.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -71,11 +71,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755275010,
|
||||
"narHash": "sha256-lEApCoWUEWh0Ifc3k1JdVjpMtFFXeL2gG1qvBnoRc2I=",
|
||||
"lastModified": 1755825449,
|
||||
"narHash": "sha256-XkiN4NM9Xdy59h69Pc+Vg4PxkSm9EWl6u7k6D5FZ5cM=",
|
||||
"owner": "nix-darwin",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "7220b01d679e93ede8d7b25d6f392855b81dd475",
|
||||
"rev": "8df64f819698c1fee0c2969696f54a843b2231e8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -86,11 +86,11 @@
|
||||
},
|
||||
"nix-select": {
|
||||
"locked": {
|
||||
"lastModified": 1745005516,
|
||||
"narHash": "sha256-IVaoOGDIvAa/8I0sdiiZuKptDldrkDWUNf/+ezIRhyc=",
|
||||
"rev": "69d8bf596194c5c35a4e90dd02c52aa530caddf8",
|
||||
"lastModified": 1755887746,
|
||||
"narHash": "sha256-lzWbpHKX0WAn/jJDoCijIDss3rqYIPawe46GDaE6U3g=",
|
||||
"rev": "92c2574c5e113281591be01e89bb9ddb31d19156",
|
||||
"type": "tarball",
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/nix-select/archive/69d8bf596194c5c35a4e90dd02c52aa530caddf8.tar.gz"
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/nix-select/archive/92c2574c5e113281591be01e89bb9ddb31d19156.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -181,11 +181,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1754847726,
|
||||
"narHash": "sha256-2vX8QjO5lRsDbNYvN9hVHXLU6oMl+V/PsmIiJREG4rE=",
|
||||
"lastModified": 1755934250,
|
||||
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "7d81f6fb2e19bf84f1c65135d1060d829fae2408",
|
||||
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -328,7 +328,7 @@ rec {
|
||||
# To get the type of a Deferred modules we need to know the interface of the place where it is evaluated.
|
||||
# i.e. in case of a clan.service this is the interface of the service which dynamically changes depending on the service
|
||||
# We assign "type" = []
|
||||
# This means any value is valid — or like TypeScript’s unknown.
|
||||
# This means any value is valid — or like TypeScript's unknown.
|
||||
# We can assign the type later, when we know the exact interface.
|
||||
# tsType = "unknown" is a type that we preload for json2ts, such that it gets the correct type in typescript
|
||||
(option.type.name == "deferredModule")
|
||||
|
||||
@@ -32,11 +32,15 @@ def init_test_environment() -> None:
|
||||
|
||||
# Set up network bridge
|
||||
subprocess.run(
|
||||
["ip", "link", "add", "br0", "type", "bridge"], check=True, text=True
|
||||
["ip", "link", "add", "br0", "type", "bridge"],
|
||||
check=True,
|
||||
text=True,
|
||||
)
|
||||
subprocess.run(["ip", "link", "set", "br0", "up"], check=True, text=True)
|
||||
subprocess.run(
|
||||
["ip", "addr", "add", "192.168.1.254/24", "dev", "br0"], check=True, text=True
|
||||
["ip", "addr", "add", "192.168.1.254/24", "dev", "br0"],
|
||||
check=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# Set up minimal passwd file for unprivileged operations
|
||||
@@ -111,8 +115,7 @@ def mount(
|
||||
mountflags: int = 0,
|
||||
data: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
A Python wrapper for the mount system call.
|
||||
"""A Python wrapper for the mount system call.
|
||||
|
||||
:param source: The source of the file system (e.g., device name, remote filesystem).
|
||||
:param target: The mount point (an existing directory).
|
||||
@@ -129,7 +132,11 @@ def mount(
|
||||
|
||||
# Call the mount system call
|
||||
result = libc.mount(
|
||||
source_c, target_c, fstype_c, ctypes.c_ulong(mountflags), data_c
|
||||
source_c,
|
||||
target_c,
|
||||
fstype_c,
|
||||
ctypes.c_ulong(mountflags),
|
||||
data_c,
|
||||
)
|
||||
|
||||
if result != 0:
|
||||
@@ -145,7 +152,7 @@ def prepare_machine_root(machinename: str, root: Path) -> None:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
root.joinpath("etc").mkdir(parents=True, exist_ok=True)
|
||||
root.joinpath(".env").write_text(
|
||||
"\n".join(f"{k}={v}" for k, v in os.environ.items())
|
||||
"\n".join(f"{k}={v}" for k, v in os.environ.items()),
|
||||
)
|
||||
|
||||
|
||||
@@ -157,7 +164,6 @@ def retry(fn: Callable, timeout: int = 900) -> None:
|
||||
"""Call the given function repeatedly, with 1 second intervals,
|
||||
until it returns True or a timeout is reached.
|
||||
"""
|
||||
|
||||
for _ in range(timeout):
|
||||
if fn(False):
|
||||
return
|
||||
@@ -284,8 +290,7 @@ class Machine:
|
||||
check_output: bool = True,
|
||||
timeout: int | None = 900,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Execute a shell command, returning a list `(status, stdout)`.
|
||||
"""Execute a shell command, returning a list `(status, stdout)`.
|
||||
|
||||
Commands are run with `set -euo pipefail` set:
|
||||
|
||||
@@ -316,7 +321,6 @@ class Machine:
|
||||
`timeout` parameter, e.g., `execute(cmd, timeout=10)` or
|
||||
`execute(cmd, timeout=None)`. The default is 900 seconds.
|
||||
"""
|
||||
|
||||
# Always run command with shell opts
|
||||
command = f"set -eo pipefail; source /etc/profile; set -xu; {command}"
|
||||
|
||||
@@ -330,7 +334,9 @@ class Machine:
|
||||
return proc
|
||||
|
||||
def nested(
|
||||
self, msg: str, attrs: dict[str, str] | None = None
|
||||
self,
|
||||
msg: str,
|
||||
attrs: dict[str, str] | None = None,
|
||||
) -> _GeneratorContextManager:
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
@@ -339,8 +345,7 @@ class Machine:
|
||||
return self.logger.nested(msg, my_attrs)
|
||||
|
||||
def systemctl(self, q: str) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Runs `systemctl` commands with optional support for
|
||||
"""Runs `systemctl` commands with optional support for
|
||||
`systemctl --user`
|
||||
|
||||
```py
|
||||
@@ -355,8 +360,7 @@ class Machine:
|
||||
return self.execute(f"systemctl {q}")
|
||||
|
||||
def wait_until_succeeds(self, command: str, timeout: int = 900) -> str:
|
||||
"""
|
||||
Repeat a shell command with 1-second intervals until it succeeds.
|
||||
"""Repeat a shell command with 1-second intervals until it succeeds.
|
||||
Has a default timeout of 900 seconds which can be modified, e.g.
|
||||
`wait_until_succeeds(cmd, timeout=10)`. See `execute` for details on
|
||||
command execution.
|
||||
@@ -374,18 +378,17 @@ class Machine:
|
||||
return output
|
||||
|
||||
def wait_for_open_port(
|
||||
self, port: int, addr: str = "localhost", timeout: int = 900
|
||||
self,
|
||||
port: int,
|
||||
addr: str = "localhost",
|
||||
timeout: int = 900,
|
||||
) -> None:
|
||||
"""
|
||||
Wait for a port to be open on the given address.
|
||||
"""
|
||||
"""Wait for a port to be open on the given address."""
|
||||
command = f"nc -z {shlex.quote(addr)} {port}"
|
||||
self.wait_until_succeeds(command, timeout=timeout)
|
||||
|
||||
def wait_for_file(self, filename: str, timeout: int = 30) -> None:
|
||||
"""
|
||||
Waits until the file exists in the machine's file system.
|
||||
"""
|
||||
"""Waits until the file exists in the machine's file system."""
|
||||
|
||||
def check_file(_last_try: bool) -> bool:
|
||||
result = self.execute(f"test -e {filename}")
|
||||
@@ -395,8 +398,7 @@ class Machine:
|
||||
retry(check_file, timeout)
|
||||
|
||||
def wait_for_unit(self, unit: str, timeout: int = 900) -> None:
|
||||
"""
|
||||
Wait for a systemd unit to get into "active" state.
|
||||
"""Wait for a systemd unit to get into "active" state.
|
||||
Throws exceptions on "failed" and "inactive" states as well as after
|
||||
timing out.
|
||||
"""
|
||||
@@ -441,9 +443,7 @@ class Machine:
|
||||
return res.stdout
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Shut down the machine, waiting for the VM to exit.
|
||||
"""
|
||||
"""Shut down the machine, waiting for the VM to exit."""
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process.wait()
|
||||
@@ -557,7 +557,7 @@ class Driver:
|
||||
rootdir=tempdir_path / container.name,
|
||||
out_dir=self.out_dir,
|
||||
logger=self.logger,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def start_all(self) -> None:
|
||||
@@ -581,7 +581,7 @@ class Driver:
|
||||
)
|
||||
|
||||
print(
|
||||
f"To attach to container {machine.name} run on the same machine that runs the test:"
|
||||
f"To attach to container {machine.name} run on the same machine that runs the test:",
|
||||
)
|
||||
print(
|
||||
" ".join(
|
||||
@@ -603,8 +603,8 @@ class Driver:
|
||||
"-c",
|
||||
"bash",
|
||||
Style.RESET_ALL,
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def test_symbols(self) -> dict[str, Any]:
|
||||
@@ -623,7 +623,7 @@ class Driver:
|
||||
"additionally exposed symbols:\n "
|
||||
+ ", ".join(m.name for m in self.machines)
|
||||
+ ",\n "
|
||||
+ ", ".join(list(general_symbols.keys()))
|
||||
+ ", ".join(list(general_symbols.keys())),
|
||||
)
|
||||
return {**general_symbols, **machine_symbols}
|
||||
|
||||
|
||||
@@ -25,14 +25,18 @@ class AbstractLogger(ABC):
|
||||
@abstractmethod
|
||||
@contextmanager
|
||||
def subtest(
|
||||
self, name: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
name: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@contextmanager
|
||||
def nested(
|
||||
self, message: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
message: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
pass
|
||||
|
||||
@@ -66,7 +70,7 @@ class JunitXMLLogger(AbstractLogger):
|
||||
|
||||
def __init__(self, outfile: Path) -> None:
|
||||
self.tests: dict[str, JunitXMLLogger.TestCaseState] = {
|
||||
"main": self.TestCaseState()
|
||||
"main": self.TestCaseState(),
|
||||
}
|
||||
self.currentSubtest = "main"
|
||||
self.outfile: Path = outfile
|
||||
@@ -78,7 +82,9 @@ class JunitXMLLogger(AbstractLogger):
|
||||
|
||||
@contextmanager
|
||||
def subtest(
|
||||
self, name: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
name: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
old_test = self.currentSubtest
|
||||
self.tests.setdefault(name, self.TestCaseState())
|
||||
@@ -90,7 +96,9 @@ class JunitXMLLogger(AbstractLogger):
|
||||
|
||||
@contextmanager
|
||||
def nested(
|
||||
self, message: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
message: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
self.log(message)
|
||||
yield
|
||||
@@ -144,7 +152,9 @@ class CompositeLogger(AbstractLogger):
|
||||
|
||||
@contextmanager
|
||||
def subtest(
|
||||
self, name: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
name: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
with ExitStack() as stack:
|
||||
for logger in self.logger_list:
|
||||
@@ -153,7 +163,9 @@ class CompositeLogger(AbstractLogger):
|
||||
|
||||
@contextmanager
|
||||
def nested(
|
||||
self, message: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
message: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
with ExitStack() as stack:
|
||||
for logger in self.logger_list:
|
||||
@@ -200,19 +212,24 @@ class TerminalLogger(AbstractLogger):
|
||||
|
||||
@contextmanager
|
||||
def subtest(
|
||||
self, name: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
name: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
with self.nested("subtest: " + name, attributes):
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def nested(
|
||||
self, message: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
message: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
self._eprint(
|
||||
self.maybe_prefix(
|
||||
Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL, attributes
|
||||
)
|
||||
Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL,
|
||||
attributes,
|
||||
),
|
||||
)
|
||||
|
||||
tic = time.time()
|
||||
@@ -259,7 +276,9 @@ class XMLLogger(AbstractLogger):
|
||||
return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C")
|
||||
|
||||
def maybe_prefix(
|
||||
self, message: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
message: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> str:
|
||||
if attributes and "machine" in attributes:
|
||||
return f"{attributes['machine']}: {message}"
|
||||
@@ -309,14 +328,18 @@ class XMLLogger(AbstractLogger):
|
||||
|
||||
@contextmanager
|
||||
def subtest(
|
||||
self, name: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
name: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
with self.nested("subtest: " + name, attributes):
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def nested(
|
||||
self, message: str, attributes: dict[str, str] | None = None
|
||||
self,
|
||||
message: str,
|
||||
attributes: dict[str, str] | None = None,
|
||||
) -> Iterator[None]:
|
||||
if attributes is None:
|
||||
attributes = {}
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
{ ... }:
|
||||
{
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.machine-id = {
|
||||
perSystem.clan.nixosTests.machine-id = {
|
||||
|
||||
name = "service-machine-id";
|
||||
name = "service-machine-id";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
# Workaround until we can use nodes.server = { };
|
||||
modules."@clan/importer" = ../../../../clanServices/importer;
|
||||
|
||||
inventory = {
|
||||
machines.server = { };
|
||||
instances.importer = {
|
||||
module.name = "@clan/importer";
|
||||
module.input = "self";
|
||||
roles.default.tags.all = { };
|
||||
roles.default.extraModules = [
|
||||
{
|
||||
# Test machine ID generation
|
||||
clan.core.settings.machine-id.enable = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# TODO: Broken. Use instead of importer after fixing.
|
||||
# nodes.server = { };
|
||||
|
||||
# This is not an actual vm test, this is a workaround to
|
||||
# generate the needed vars for the eval test.
|
||||
testScript = "";
|
||||
clan = {
|
||||
directory = ./.;
|
||||
machines.server = {
|
||||
clan.core.settings.machine-id.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
# This is not an actual vm test, this is a workaround to
|
||||
# generate the needed vars for the eval test.
|
||||
testScript = "";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,30 +10,14 @@
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
# Workaround until we can use nodes.machine = { };
|
||||
modules."@clan/importer" = ../../../../clanServices/importer;
|
||||
|
||||
inventory = {
|
||||
machines.machine = { };
|
||||
instances.importer = {
|
||||
module.name = "@clan/importer";
|
||||
module.input = "self";
|
||||
roles.default.tags.all = { };
|
||||
roles.default.extraModules = [
|
||||
{
|
||||
clan.core.postgresql.enable = true;
|
||||
clan.core.postgresql.users.test = { };
|
||||
clan.core.postgresql.databases.test.create.options.OWNER = "test";
|
||||
clan.core.settings.directory = ./.;
|
||||
}
|
||||
];
|
||||
};
|
||||
machines.machine = {
|
||||
clan.core.postgresql.enable = true;
|
||||
clan.core.postgresql.users.test = { };
|
||||
clan.core.postgresql.databases.test.create.options.OWNER = "test";
|
||||
clan.core.settings.directory = ./.;
|
||||
};
|
||||
};
|
||||
|
||||
# TODO: Broken. Use instead of importer after fixing.
|
||||
# nodes.machine = { };
|
||||
|
||||
testScript =
|
||||
let
|
||||
runpg = "runuser -u postgres -- /run/current-system/sw/bin/psql";
|
||||
|
||||
@@ -304,6 +304,15 @@ in
|
||||
description = "The unix file mode of the file. Must be a 4-digit octal number.";
|
||||
default = "0400";
|
||||
};
|
||||
exists = mkOption {
|
||||
description = ''
|
||||
Returns true if the file exists, This is used to guard against reading not set value in evaluation.
|
||||
This currently only works for non secret files.
|
||||
'';
|
||||
type = bool;
|
||||
default = if file.config.secret then throw "Cannot determine existance of secret file" else false;
|
||||
defaultText = "Throws error because the existance of a secret file cannot be determined";
|
||||
};
|
||||
value =
|
||||
mkOption {
|
||||
description = ''
|
||||
|
||||
@@ -25,7 +25,7 @@ in
|
||||
);
|
||||
value = mkIf (file.config.secret == false) (
|
||||
# dynamically adjust priority to allow overriding with mkDefault in case the file is not found
|
||||
if (pathExists file.config.flakePath) then
|
||||
if file.config.exists then
|
||||
# if the file is found it should have normal priority
|
||||
readFile file.config.flakePath
|
||||
else
|
||||
@@ -34,6 +34,7 @@ in
|
||||
throw "Please run `clan vars generate ${config.clan.core.settings.machine.name}` as file was not found: ${file.config.path}"
|
||||
)
|
||||
);
|
||||
exists = mkIf (file.config.secret == false) (pathExists file.config.flakePath);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ def compute_zerotier_ip(network_id: str, identity: Identity) -> ipaddress.IPv6Ad
|
||||
(node_id >> 16) & 0xFF,
|
||||
(node_id >> 8) & 0xFF,
|
||||
(node_id) & 0xFF,
|
||||
]
|
||||
],
|
||||
)
|
||||
return ipaddress.IPv6Address(bytes(addr_parts))
|
||||
|
||||
@@ -203,7 +203,10 @@ def compute_zerotier_ip(network_id: str, identity: Identity) -> ipaddress.IPv6Ad
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--mode", choices=["network", "identity"], required=True, type=str
|
||||
"--mode",
|
||||
choices=["network", "identity"],
|
||||
required=True,
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument("--ip", type=Path, required=True)
|
||||
parser.add_argument("--identity-secret", type=Path, required=True)
|
||||
|
||||
@@ -17,7 +17,7 @@ def main() -> None:
|
||||
|
||||
moon_json = json.loads(Path(moon_json_path).read_text())
|
||||
moon_json["roots"][0]["stableEndpoints"] = json.loads(
|
||||
Path(endpoint_config).read_text()
|
||||
Path(endpoint_config).read_text(),
|
||||
)
|
||||
|
||||
with NamedTemporaryFile("w") as f:
|
||||
|
||||
@@ -38,8 +38,7 @@ def get_gitea_api_url(remote: str = "origin") -> str:
|
||||
host_and_path = remote_url.split("@")[1] # git.clan.lol:clan/clan-core.git
|
||||
host = host_and_path.split(":")[0] # git.clan.lol
|
||||
repo_path = host_and_path.split(":")[1] # clan/clan-core.git
|
||||
if repo_path.endswith(".git"):
|
||||
repo_path = repo_path[:-4] # clan/clan-core
|
||||
repo_path = repo_path.removesuffix(".git") # clan/clan-core
|
||||
elif remote_url.startswith("https://"):
|
||||
# HTTPS format: https://git.clan.lol/clan/clan-core.git
|
||||
url_parts = remote_url.replace("https://", "").split("/")
|
||||
@@ -86,7 +85,10 @@ def get_repo_info_from_api_url(api_url: str) -> tuple[str, str]:
|
||||
|
||||
|
||||
def fetch_pr_statuses(
|
||||
repo_owner: str, repo_name: str, commit_sha: str, host: str
|
||||
repo_owner: str,
|
||||
repo_name: str,
|
||||
commit_sha: str,
|
||||
host: str,
|
||||
) -> list[dict]:
|
||||
"""Fetch CI statuses for a specific commit SHA."""
|
||||
status_url = (
|
||||
@@ -183,7 +185,7 @@ def run_git_command(command: list) -> tuple[int, str, str]:
|
||||
|
||||
def get_current_branch_name() -> str:
|
||||
exit_code, branch_name, error = run_git_command(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"]
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
)
|
||||
|
||||
if exit_code != 0:
|
||||
@@ -196,7 +198,7 @@ def get_current_branch_name() -> str:
|
||||
def get_latest_commit_info() -> tuple[str, str]:
|
||||
"""Get the title and body of the latest commit."""
|
||||
exit_code, commit_msg, error = run_git_command(
|
||||
["git", "log", "-1", "--pretty=format:%B"]
|
||||
["git", "log", "-1", "--pretty=format:%B"],
|
||||
)
|
||||
|
||||
if exit_code != 0:
|
||||
@@ -225,7 +227,7 @@ def get_commits_since_main() -> list[tuple[str, str]]:
|
||||
"main..HEAD",
|
||||
"--no-merges",
|
||||
"--pretty=format:%s|%b|---END---",
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
if exit_code != 0:
|
||||
@@ -263,7 +265,9 @@ def open_editor_for_pr() -> tuple[str, str]:
|
||||
commits_since_main = get_commits_since_main()
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w+", suffix="COMMIT_EDITMSG", delete=False
|
||||
mode="w+",
|
||||
suffix="COMMIT_EDITMSG",
|
||||
delete=False,
|
||||
) as temp_file:
|
||||
temp_file.flush()
|
||||
temp_file_path = temp_file.name
|
||||
@@ -280,7 +284,7 @@ def open_editor_for_pr() -> tuple[str, str]:
|
||||
temp_file.write("# The first line will be used as the PR title.\n")
|
||||
temp_file.write("# Everything else will be used as the PR description.\n")
|
||||
temp_file.write(
|
||||
"# To abort creation of the PR, close editor with an error code.\n"
|
||||
"# To abort creation of the PR, close editor with an error code.\n",
|
||||
)
|
||||
temp_file.write("# In vim for example you can use :cq!\n")
|
||||
temp_file.write("#\n")
|
||||
@@ -373,7 +377,7 @@ def create_agit_push(
|
||||
print(
|
||||
f" Description: {description[:50]}..."
|
||||
if len(description) > 50
|
||||
else f" Description: {description}"
|
||||
else f" Description: {description}",
|
||||
)
|
||||
print()
|
||||
|
||||
@@ -530,19 +534,26 @@ Examples:
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"-t", "--topic", help="Set PR topic (default: current branch name)"
|
||||
"-t",
|
||||
"--topic",
|
||||
help="Set PR topic (default: current branch name)",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"--title", help="Set the PR title (default: last commit title)"
|
||||
"--title",
|
||||
help="Set the PR title (default: last commit title)",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"--description", help="Override the PR description (default: commit body)"
|
||||
"--description",
|
||||
help="Override the PR description (default: commit body)",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
"-f", "--force", action="store_true", help="Force push the changes"
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Force push the changes",
|
||||
)
|
||||
|
||||
create_parser.add_argument(
|
||||
|
||||
@@ -13,7 +13,9 @@ log = logging.getLogger(__name__)
|
||||
def main(argv: list[str] = sys.argv) -> int:
|
||||
parser = argparse.ArgumentParser(description="Clan App")
|
||||
parser.add_argument(
|
||||
"--content-uri", type=str, help="The URI of the content to display"
|
||||
"--content-uri",
|
||||
type=str,
|
||||
help="The URI of the content to display",
|
||||
)
|
||||
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
||||
parser.add_argument(
|
||||
|
||||
@@ -56,18 +56,23 @@ class ApiBridge(ABC):
|
||||
for middleware in self.middleware_chain:
|
||||
try:
|
||||
log.debug(
|
||||
f"{middleware.__class__.__name__} => {request.method_name}"
|
||||
f"{middleware.__class__.__name__} => {request.method_name}",
|
||||
)
|
||||
middleware.process(context)
|
||||
except Exception as e:
|
||||
# If middleware fails, handle error
|
||||
self.send_api_error_response(
|
||||
request.op_key or "unknown", str(e), ["middleware_error"]
|
||||
request.op_key or "unknown",
|
||||
str(e),
|
||||
["middleware_error"],
|
||||
)
|
||||
return
|
||||
|
||||
def send_api_error_response(
|
||||
self, op_key: str, error_message: str, location: list[str]
|
||||
self,
|
||||
op_key: str,
|
||||
error_message: str,
|
||||
location: list[str],
|
||||
) -> None:
|
||||
"""Send an error response."""
|
||||
from clan_lib.api import ApiError, ErrorDataClass
|
||||
@@ -80,7 +85,7 @@ class ApiBridge(ABC):
|
||||
message="An internal error occured",
|
||||
description=error_message,
|
||||
location=location,
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -107,6 +112,7 @@ class ApiBridge(ABC):
|
||||
thread_name: Name for the thread (for debugging)
|
||||
wait_for_completion: Whether to wait for the thread to complete
|
||||
timeout: Timeout in seconds when waiting for completion
|
||||
|
||||
"""
|
||||
op_key = request.op_key or "unknown"
|
||||
|
||||
@@ -116,7 +122,7 @@ class ApiBridge(ABC):
|
||||
try:
|
||||
log.debug(
|
||||
f"Processing {request.method_name} with args {request.args} "
|
||||
f"and header {request.header} in thread {thread_name}"
|
||||
f"and header {request.header} in thread {thread_name}",
|
||||
)
|
||||
self.process_request(request)
|
||||
finally:
|
||||
@@ -124,7 +130,9 @@ class ApiBridge(ABC):
|
||||
|
||||
stop_event = threading.Event()
|
||||
thread = threading.Thread(
|
||||
target=thread_task, args=(stop_event,), name=thread_name
|
||||
target=thread_task,
|
||||
args=(stop_event,),
|
||||
name=thread_name,
|
||||
)
|
||||
thread.start()
|
||||
|
||||
@@ -138,5 +146,7 @@ class ApiBridge(ABC):
|
||||
if thread.is_alive():
|
||||
stop_event.set() # Cancel the thread
|
||||
self.send_api_error_response(
|
||||
op_key, "Request timeout", ["api_bridge", request.method_name]
|
||||
op_key,
|
||||
"Request timeout",
|
||||
["api_bridge", request.method_name],
|
||||
)
|
||||
|
||||
@@ -26,8 +26,7 @@ RESULT: dict[str, SuccessDataClass[list[str] | None] | ErrorDataClass] = {}
|
||||
|
||||
|
||||
def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
|
||||
"""
|
||||
Opens the clan folder using the GTK file dialog.
|
||||
"""Opens the clan folder using the GTK file dialog.
|
||||
Returns the path to the clan folder or an error if it fails.
|
||||
"""
|
||||
file_request = FileRequest(
|
||||
@@ -52,7 +51,7 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
|
||||
message="No folder selected",
|
||||
description="You must select a folder to open.",
|
||||
location=["get_clan_folder"],
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -66,7 +65,7 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
|
||||
message="Invalid clan folder",
|
||||
description=f"The selected folder '{clan_folder}' is not a valid clan folder.",
|
||||
location=["get_clan_folder"],
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -102,8 +101,10 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||
selected_path = remove_none([gfile.get_path()])
|
||||
returns(
|
||||
SuccessDataClass(
|
||||
op_key=op_key, data=selected_path, status="success"
|
||||
)
|
||||
op_key=op_key,
|
||||
data=selected_path,
|
||||
status="success",
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception("Error opening file")
|
||||
@@ -116,9 +117,9 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||
message=e.__class__.__name__,
|
||||
description=str(e),
|
||||
location=["get_system_file"],
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def on_file_select_multiple(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||
@@ -128,8 +129,10 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||
selected_paths = remove_none([gfile.get_path() for gfile in gfiles])
|
||||
returns(
|
||||
SuccessDataClass(
|
||||
op_key=op_key, data=selected_paths, status="success"
|
||||
)
|
||||
op_key=op_key,
|
||||
data=selected_paths,
|
||||
status="success",
|
||||
),
|
||||
)
|
||||
else:
|
||||
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
||||
@@ -144,9 +147,9 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||
message=e.__class__.__name__,
|
||||
description=str(e),
|
||||
location=["get_system_file"],
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||
@@ -156,8 +159,10 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||
selected_path = remove_none([gfile.get_path()])
|
||||
returns(
|
||||
SuccessDataClass(
|
||||
op_key=op_key, data=selected_path, status="success"
|
||||
)
|
||||
op_key=op_key,
|
||||
data=selected_path,
|
||||
status="success",
|
||||
),
|
||||
)
|
||||
else:
|
||||
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
||||
@@ -172,9 +177,9 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||
message=e.__class__.__name__,
|
||||
description=str(e),
|
||||
location=["get_system_file"],
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def on_save_finish(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||
@@ -184,8 +189,10 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||
selected_path = remove_none([gfile.get_path()])
|
||||
returns(
|
||||
SuccessDataClass(
|
||||
op_key=op_key, data=selected_path, status="success"
|
||||
)
|
||||
op_key=op_key,
|
||||
data=selected_path,
|
||||
status="success",
|
||||
),
|
||||
)
|
||||
else:
|
||||
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
||||
@@ -200,9 +207,9 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
||||
message=e.__class__.__name__,
|
||||
description=str(e),
|
||||
location=["get_system_file"],
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
dialog = Gtk.FileDialog()
|
||||
|
||||
@@ -39,7 +39,7 @@ class ArgumentParsingMiddleware(Middleware):
|
||||
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
f"Error while parsing arguments for {context.request.method_name}"
|
||||
f"Error while parsing arguments for {context.request.method_name}",
|
||||
)
|
||||
context.bridge.send_api_error_response(
|
||||
context.request.op_key or "unknown",
|
||||
|
||||
@@ -23,7 +23,9 @@ class Middleware(ABC):
|
||||
"""Process the request through this middleware."""
|
||||
|
||||
def register_context_manager(
|
||||
self, context: MiddlewareContext, cm: AbstractContextManager[Any]
|
||||
self,
|
||||
context: MiddlewareContext,
|
||||
cm: AbstractContextManager[Any],
|
||||
) -> Any:
|
||||
"""Register a context manager with the exit stack."""
|
||||
return context.exit_stack.enter_context(cm)
|
||||
|
||||
@@ -25,23 +25,26 @@ class LoggingMiddleware(Middleware):
|
||||
try:
|
||||
# Handle log group configuration
|
||||
log_group: list[str] | None = context.request.header.get("logging", {}).get(
|
||||
"group_path", None
|
||||
"group_path",
|
||||
None,
|
||||
)
|
||||
if log_group is not None:
|
||||
if not isinstance(log_group, list):
|
||||
msg = f"Expected log_group to be a list, got {type(log_group)}"
|
||||
raise TypeError(msg) # noqa: TRY301
|
||||
log.warning(
|
||||
f"Using log group {log_group} for {context.request.method_name} with op_key {context.request.op_key}"
|
||||
f"Using log group {log_group} for {context.request.method_name} with op_key {context.request.op_key}",
|
||||
)
|
||||
# Create log file
|
||||
log_file = self.log_manager.create_log_file(
|
||||
method, op_key=context.request.op_key or "unknown", group_path=log_group
|
||||
method,
|
||||
op_key=context.request.op_key or "unknown",
|
||||
group_path=log_group,
|
||||
).get_file_path()
|
||||
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
f"Error while handling request header of {context.request.method_name}"
|
||||
f"Error while handling request header of {context.request.method_name}",
|
||||
)
|
||||
context.bridge.send_api_error_response(
|
||||
context.request.op_key or "unknown",
|
||||
@@ -76,7 +79,8 @@ class LoggingMiddleware(Middleware):
|
||||
line_buffering=True,
|
||||
)
|
||||
self.handler = setup_logging(
|
||||
log.getEffectiveLevel(), log_file=handler_stream
|
||||
log.getEffectiveLevel(),
|
||||
log_file=handler_stream,
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
@@ -32,7 +32,7 @@ class MethodExecutionMiddleware(Middleware):
|
||||
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
f"Error while handling result of {context.request.method_name}"
|
||||
f"Error while handling result of {context.request.method_name}",
|
||||
)
|
||||
context.bridge.send_api_error_response(
|
||||
context.request.op_key or "unknown",
|
||||
|
||||
@@ -48,7 +48,7 @@ def app_run(app_opts: ClanAppOptions) -> int:
|
||||
# Add a log group ["clans", <dynamic_name>, "machines", <dynamic_name>]
|
||||
log_manager = LogManager(base_dir=user_data_dir() / "clan-app" / "logs")
|
||||
clan_log_group = LogGroupConfig("clans", "Clans").add_child(
|
||||
LogGroupConfig("machines", "Machines")
|
||||
LogGroupConfig("machines", "Machines"),
|
||||
)
|
||||
log_manager = log_manager.add_root_group_config(clan_log_group)
|
||||
# Init LogManager global in log_manager_api module
|
||||
@@ -89,7 +89,7 @@ def app_run(app_opts: ClanAppOptions) -> int:
|
||||
# HTTP-only mode - keep the server running
|
||||
log.info("HTTP API server running...")
|
||||
log.info(
|
||||
f"Swagger: http://{app_opts.http_host}:{app_opts.http_port}/api/swagger"
|
||||
f"Swagger: http://{app_opts.http_host}:{app_opts.http_port}/api/swagger",
|
||||
)
|
||||
|
||||
log.info("Press Ctrl+C to stop the server")
|
||||
|
||||
@@ -63,7 +63,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
def _send_json_response_with_status(
|
||||
self, data: dict[str, Any], status_code: int = 200
|
||||
self,
|
||||
data: dict[str, Any],
|
||||
status_code: int = 200,
|
||||
) -> None:
|
||||
"""Send a JSON response with the given status code."""
|
||||
try:
|
||||
@@ -82,11 +84,13 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||
response_dict = dataclass_to_dict(response)
|
||||
self._send_json_response_with_status(response_dict, 200)
|
||||
log.debug(
|
||||
f"HTTP response for {response._op_key}: {json.dumps(response_dict, indent=2)}" # noqa: SLF001
|
||||
f"HTTP response for {response._op_key}: {json.dumps(response_dict, indent=2)}", # noqa: SLF001
|
||||
)
|
||||
|
||||
def _create_success_response(
|
||||
self, op_key: str, data: dict[str, Any]
|
||||
self,
|
||||
op_key: str,
|
||||
data: dict[str, Any],
|
||||
) -> BackendResponse:
|
||||
"""Create a successful API response."""
|
||||
return BackendResponse(
|
||||
@@ -98,14 +102,16 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||
def _send_info_response(self) -> None:
|
||||
"""Send server information response."""
|
||||
response = self._create_success_response(
|
||||
"info", {"message": "Clan API Server", "version": "1.0.0"}
|
||||
"info",
|
||||
{"message": "Clan API Server", "version": "1.0.0"},
|
||||
)
|
||||
self.send_api_response(response)
|
||||
|
||||
def _send_methods_response(self) -> None:
|
||||
"""Send available API methods response."""
|
||||
response = self._create_success_response(
|
||||
"methods", {"methods": list(self.api.functions.keys())}
|
||||
"methods",
|
||||
{"methods": list(self.api.functions.keys())},
|
||||
)
|
||||
self.send_api_response(response)
|
||||
|
||||
@@ -179,7 +185,7 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||
json_data = json.loads(file_data.decode("utf-8"))
|
||||
server_address = getattr(self.server, "server_address", ("localhost", 80))
|
||||
json_data["servers"] = [
|
||||
{"url": f"http://{server_address[0]}:{server_address[1]}/api/v1/"}
|
||||
{"url": f"http://{server_address[0]}:{server_address[1]}/api/v1/"},
|
||||
]
|
||||
file_data = json.dumps(json_data, indent=2).encode("utf-8")
|
||||
|
||||
@@ -213,7 +219,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||
# Validate API path
|
||||
if not path.startswith("/api/v1/"):
|
||||
self.send_api_error_response(
|
||||
"post", f"Path not found: {path}", ["http_bridge", "POST"]
|
||||
"post",
|
||||
f"Path not found: {path}",
|
||||
["http_bridge", "POST"],
|
||||
)
|
||||
return
|
||||
|
||||
@@ -221,7 +229,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||
method_name = path[len("/api/v1/") :]
|
||||
if not method_name:
|
||||
self.send_api_error_response(
|
||||
"post", "Method name required", ["http_bridge", "POST"]
|
||||
"post",
|
||||
"Method name required",
|
||||
["http_bridge", "POST"],
|
||||
)
|
||||
return
|
||||
|
||||
@@ -289,19 +299,26 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||
|
||||
# Create API request
|
||||
api_request = BackendRequest(
|
||||
method_name=method_name, args=body, header=header, op_key=op_key
|
||||
method_name=method_name,
|
||||
args=body,
|
||||
header=header,
|
||||
op_key=op_key,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.send_api_error_response(
|
||||
gen_op_key, str(e), ["http_bridge", method_name]
|
||||
gen_op_key,
|
||||
str(e),
|
||||
["http_bridge", method_name],
|
||||
)
|
||||
return
|
||||
|
||||
self._process_api_request_in_thread(api_request, method_name)
|
||||
|
||||
def _parse_request_data(
|
||||
self, request_data: dict[str, Any], gen_op_key: str
|
||||
self,
|
||||
request_data: dict[str, Any],
|
||||
gen_op_key: str,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], str]:
|
||||
"""Parse and validate request data components."""
|
||||
header = request_data.get("header", {})
|
||||
@@ -344,7 +361,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||
pass
|
||||
|
||||
def _process_api_request_in_thread(
|
||||
self, api_request: BackendRequest, method_name: str
|
||||
self,
|
||||
api_request: BackendRequest,
|
||||
method_name: str,
|
||||
) -> None:
|
||||
"""Process the API request in a separate thread."""
|
||||
stop_event = threading.Event()
|
||||
@@ -358,7 +377,7 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
||||
|
||||
log.debug(
|
||||
f"Processing {request.method_name} with args {request.args} "
|
||||
f"and header {request.header}"
|
||||
f"and header {request.header}",
|
||||
)
|
||||
self.process_request(request)
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ def mock_log_manager() -> Mock:
|
||||
|
||||
@pytest.fixture
|
||||
def http_bridge(
|
||||
mock_api: MethodRegistry, mock_log_manager: Mock
|
||||
mock_api: MethodRegistry,
|
||||
mock_log_manager: Mock,
|
||||
) -> tuple[MethodRegistry, tuple]:
|
||||
"""Create HTTP bridge dependencies for testing."""
|
||||
middleware_chain = (
|
||||
@@ -256,7 +257,9 @@ class TestIntegration:
|
||||
"""Integration tests for HTTP API components."""
|
||||
|
||||
def test_full_request_flow(
|
||||
self, mock_api: MethodRegistry, mock_log_manager: Mock
|
||||
self,
|
||||
mock_api: MethodRegistry,
|
||||
mock_log_manager: Mock,
|
||||
) -> None:
|
||||
"""Test complete request flow from server to bridge to middleware."""
|
||||
server: HttpApiServer = HttpApiServer(
|
||||
@@ -301,7 +304,9 @@ class TestIntegration:
|
||||
server.stop()
|
||||
|
||||
def test_blocking_task(
|
||||
self, mock_api: MethodRegistry, mock_log_manager: Mock
|
||||
self,
|
||||
mock_api: MethodRegistry,
|
||||
mock_log_manager: Mock,
|
||||
) -> None:
|
||||
shared_threads: dict[str, tasks.WebThread] = {}
|
||||
tasks.BAKEND_THREADS = shared_threads
|
||||
|
||||
@@ -21,7 +21,7 @@ def _get_lib_names() -> list[str]:
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if system == "windows":
|
||||
if machine == "amd64" or machine == "x86_64":
|
||||
if machine in {"amd64", "x86_64"}:
|
||||
return ["webview.dll", "WebView2Loader.dll"]
|
||||
if machine == "arm64":
|
||||
msg = "arm64 is not supported on Windows"
|
||||
@@ -36,7 +36,6 @@ def _get_lib_names() -> list[str]:
|
||||
|
||||
def _be_sure_libraries() -> list[Path] | None:
|
||||
"""Ensure libraries exist and return paths."""
|
||||
|
||||
lib_dir = os.environ.get("WEBVIEW_LIB_DIR")
|
||||
if not lib_dir:
|
||||
msg = "WEBVIEW_LIB_DIR environment variable is not set"
|
||||
|
||||
@@ -144,7 +144,9 @@ class Webview:
|
||||
)
|
||||
else:
|
||||
bridge = WebviewBridge(
|
||||
webview=self, middleware_chain=tuple(self._middleware), threads={}
|
||||
webview=self,
|
||||
middleware_chain=tuple(self._middleware),
|
||||
threads={},
|
||||
)
|
||||
self._bridge = bridge
|
||||
|
||||
@@ -154,7 +156,10 @@ class Webview:
|
||||
def set_size(self, value: Size) -> None:
|
||||
"""Set the webview size (legacy compatibility)."""
|
||||
_webview_lib.webview_set_size(
|
||||
self.handle, value.width, value.height, value.hint
|
||||
self.handle,
|
||||
value.width,
|
||||
value.height,
|
||||
value.hint,
|
||||
)
|
||||
|
||||
def set_title(self, value: str) -> None:
|
||||
@@ -194,7 +199,10 @@ class Webview:
|
||||
|
||||
self._callbacks[name] = c_callback
|
||||
_webview_lib.webview_bind(
|
||||
self.handle, _encode_c_string(name), c_callback, None
|
||||
self.handle,
|
||||
_encode_c_string(name),
|
||||
c_callback,
|
||||
None,
|
||||
)
|
||||
|
||||
def bind(self, name: str, callback: Callable[..., Any]) -> None:
|
||||
@@ -219,7 +227,10 @@ class Webview:
|
||||
|
||||
def return_(self, seq: str, status: int, result: str) -> None:
|
||||
_webview_lib.webview_return(
|
||||
self.handle, _encode_c_string(seq), status, _encode_c_string(result)
|
||||
self.handle,
|
||||
_encode_c_string(seq),
|
||||
status,
|
||||
_encode_c_string(result),
|
||||
)
|
||||
|
||||
def eval(self, source: str) -> None:
|
||||
|
||||
@@ -26,7 +26,9 @@ class WebviewBridge(ApiBridge):
|
||||
def send_api_response(self, response: BackendResponse) -> None:
|
||||
"""Send response back to the webview client."""
|
||||
serialized = json.dumps(
|
||||
dataclass_to_dict(response), indent=4, ensure_ascii=False
|
||||
dataclass_to_dict(response),
|
||||
indent=4,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
log.debug(f"Sending response: {serialized}")
|
||||
@@ -40,7 +42,6 @@ class WebviewBridge(ApiBridge):
|
||||
arg: int,
|
||||
) -> None:
|
||||
"""Handle a call from webview's JavaScript bridge."""
|
||||
|
||||
try:
|
||||
op_key = op_key_bytes.decode()
|
||||
raw_args = json.loads(request_data.decode())
|
||||
@@ -68,7 +69,10 @@ class WebviewBridge(ApiBridge):
|
||||
|
||||
# Create API request
|
||||
api_request = BackendRequest(
|
||||
method_name=method_name, args=args, header=header, op_key=op_key
|
||||
method_name=method_name,
|
||||
args=args,
|
||||
header=header,
|
||||
op_key=op_key,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -77,7 +81,9 @@ class WebviewBridge(ApiBridge):
|
||||
)
|
||||
log.exception(msg)
|
||||
self.send_api_error_response(
|
||||
op_key, str(e), ["webview_bridge", method_name]
|
||||
op_key,
|
||||
str(e),
|
||||
["webview_bridge", method_name],
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@@ -54,8 +54,7 @@ class Command:
|
||||
|
||||
@pytest.fixture
|
||||
def command() -> Iterator[Command]:
|
||||
"""
|
||||
Starts a background command. The process is automatically terminated in the end.
|
||||
"""Starts a background command. The process is automatically terminated in the end.
|
||||
>>> p = command.run(["some", "daemon"])
|
||||
>>> print(p.pid)
|
||||
"""
|
||||
|
||||
@@ -2,12 +2,15 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from clan_lib.custom_logger import setup_logging
|
||||
from clan_lib.nix import nix_shell
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
pytest_plugins = [
|
||||
"temporary_dir",
|
||||
"root",
|
||||
|
||||
@@ -13,23 +13,17 @@ else:
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def project_root() -> Path:
|
||||
"""
|
||||
Root directory the clan-cli
|
||||
"""
|
||||
"""Root directory the clan-cli"""
|
||||
return PROJECT_ROOT
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_root() -> Path:
|
||||
"""
|
||||
Root directory of the tests
|
||||
"""
|
||||
"""Root directory of the tests"""
|
||||
return TEST_ROOT
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def clan_core() -> Path:
|
||||
"""
|
||||
Directory of the clan-core flake
|
||||
"""
|
||||
"""Directory of the clan-core flake"""
|
||||
return CLAN_CORE
|
||||
|
||||
@@ -24,7 +24,11 @@ def app() -> Generator[GtkProc]:
|
||||
cmd = [sys.executable, "-m", "clan_app"]
|
||||
print(f"Running: {cmd}")
|
||||
rapp = Popen(
|
||||
cmd, text=True, stdout=sys.stdout, stderr=sys.stderr, start_new_session=True
|
||||
cmd,
|
||||
text=True,
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
start_new_session=True,
|
||||
)
|
||||
yield GtkProc(rapp)
|
||||
# Cleanup: Terminate your application
|
||||
|
||||
1
pkgs/clan-app/ui/icons/address.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M9.223 38.777h8.444V43H5V30.333h4.223zM43 43h-4.223v-8.444h-8.444V43h-4.222V21.889H43zM30.333 30.333h8.444v-4.222h-8.444zM17.667 9.223H9.223v4.221h8.444v4.223H9.223v4.222h8.444v4.222H5V5h12.667zm4.222 12.666h-4.222v-4.222h4.222zM43 17.667h-4.223V9.223h-8.444V5H43zm-21.111-4.223h-4.222V9.223h4.222z"/></svg>
|
||||
|
After Width: | Height: | Size: 399 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
|
||||
<path d="M27 38H6V17H10V13H13.5V9H37.5V13H41V24H27V27H34V31H30.5V34.5H27V38ZM16.5 20.5H20V17H16.5V20.5Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M27 38H6V17h4v-4h3.5V9h24v4H41v11H27v3h7v4h-3.5v3.5H27zM16.5 20.5H20V17h-3.5z"/></svg>
|
||||
|
Before Width: | Height: | Size: 221 B After Width: | Height: | Size: 178 B |
1
pkgs/clan-app/ui/icons/check-solid.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M46 46H2V2h44zM16.667 33.777h4.889V28.89h-4.889zm-4.89-4.888h4.89V24h-4.89zm9.779 0h4.888V24h-4.888zM26.444 24h4.889v-4.889h-4.889zm4.889-9.777v4.888h4.89v-4.888z"/></svg>
|
||||
|
After Width: | Height: | Size: 263 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor">
|
||||
<path d="M36.888 11H41.3v4.413h-4.412zm-4.413 8.825v-4.412h4.413v4.412zm-4.413 4.413v-4.413h4.413v4.413zM23.65 28.65h4.413v-4.412H23.65zm-4.412 4.413h4.412V28.65h-4.412zm-4.413 0v4.412h4.413v-4.413zm-4.412-4.413h4.412v4.413h-4.412zm0 0H6v-4.412h4.413z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M36.888 11H41.3v4.413h-4.412zm-4.413 8.825v-4.412h4.413v4.412zm-4.413 4.413v-4.413h4.413v4.413zM23.65 28.65h4.413v-4.412H23.65zm-4.412 4.413h4.412V28.65h-4.412zm-4.413 0v4.412h4.413v-4.413zm-4.412-4.413h4.412v4.413h-4.412zm0 0H6v-4.412h4.413z"/></svg>
|
||||
|
Before Width: | Height: | Size: 349 B After Width: | Height: | Size: 343 B |
@@ -1,10 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="89" fill="currentColor">
|
||||
<g clip-path="url(#a)">
|
||||
<path d="M57.709 20.105H68.62c1.157 0 2.099-.94 2.099-2.095V9.632a2.1 2.1 0 0 0-2.099-2.095h-3.439c-1.111 0-2.014-.9-2.014-2.01V2.095A2.1 2.1 0 0 0 61.07 0H30.02a2.1 2.1 0 0 0-2.098 2.095v3.432c0 1.11-.903 2.01-2.014 2.01H22.47c-1.157 0-2.098.94-2.098 2.095v3.432c0 1.11-.903 2.01-2.014 2.01h-3.439c-1.157 0-2.099.94-2.099 2.094 0 0-.503-1.272-.503 22.493 0 21.247.503 19.38.503 19.38 0 1.156.942 2.096 2.1 2.096h3.438c1.111 0 2.014.9 2.014 2.01v3.517c0 1.109.902 2.01 2.013 2.01h3.524c1.111 0 2.014.9 2.014 2.01v3.432a2.1 2.1 0 0 0 2.098 2.094h30.211c1.157 0 2.099-.94 2.099-2.094v-3.433c0-1.11.902-2.01 2.013-2.01h5.557c1.158 0 2.099-.94 2.099-2.094v-9.984a2.1 2.1 0 0 0-2.099-2.095h-13.03c-1.157 0-2.098.94-2.098 2.095v5.044c0 1.11-.902 2.01-2.014 2.01H37.488c-1.111 0-2.013-.9-2.013-2.01v-5.11a2.1 2.1 0 0 0-2.099-2.094h-5.119c-1.111 0-1.739.163-2.014-2.01-.085-.698-.13-1.553-.196-2.695-.163-2.878-.307-1.723-.307-10.369 0-12.085.314-15.563.503-17.24.19-1.677.903-2.01 2.014-2.01h5.12c1.156 0 2.098-.94 2.098-2.094v-3.433c0-1.109.902-2.01 2.013-2.01h16.116c1.111 0 2.014.901 2.014 2.01v3.433c0 1.155.94 2.094 2.098 2.094zM18.626 73.757h-2.478a.87.87 0 0 1-.87-.868v-2.473c0-.96-.777-1.743-1.745-1.743H6.838c-.96 0-1.745.777-1.745 1.743v2.473a.87.87 0 0 1-.87.868H1.746c-.961 0-1.746.776-1.746 1.742v6.682c0 .96.778 1.742 1.746 1.742h2.477c.484 0 .87.392.87.868v2.473c0 .96.778 1.743 1.745 1.743h6.695c.961 0 1.746-.777 1.746-1.743v-2.473c0-.483.392-.868.87-.868h2.477c.961 0 1.746-.776 1.746-1.742v-6.682c0-.96-.778-1.742-1.746-1.742"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path d="M0 0h72v89H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="89" fill="currentColor"><g clip-path="url(#a)"><path d="M57.709 20.105H68.62c1.157 0 2.099-.94 2.099-2.095V9.632a2.1 2.1 0 0 0-2.099-2.095h-3.439c-1.111 0-2.014-.9-2.014-2.01V2.095A2.1 2.1 0 0 0 61.07 0H30.02a2.1 2.1 0 0 0-2.098 2.095v3.432c0 1.11-.903 2.01-2.014 2.01H22.47c-1.157 0-2.098.94-2.098 2.095v3.432c0 1.11-.903 2.01-2.014 2.01h-3.439c-1.157 0-2.099.94-2.099 2.094 0 0-.503-1.272-.503 22.493 0 21.247.503 19.38.503 19.38 0 1.156.942 2.096 2.1 2.096h3.438c1.111 0 2.014.9 2.014 2.01v3.517c0 1.109.902 2.01 2.013 2.01h3.524c1.111 0 2.014.9 2.014 2.01v3.432a2.1 2.1 0 0 0 2.098 2.094h30.211c1.157 0 2.099-.94 2.099-2.094v-3.433c0-1.11.902-2.01 2.013-2.01h5.557c1.158 0 2.099-.94 2.099-2.094v-9.984a2.1 2.1 0 0 0-2.099-2.095h-13.03c-1.157 0-2.098.94-2.098 2.095v5.044c0 1.11-.902 2.01-2.014 2.01H37.488c-1.111 0-2.013-.9-2.013-2.01v-5.11a2.1 2.1 0 0 0-2.099-2.094h-5.119c-1.111 0-1.739.163-2.014-2.01-.085-.698-.13-1.553-.196-2.695-.163-2.878-.307-1.723-.307-10.369 0-12.085.314-15.563.503-17.24.19-1.677.903-2.01 2.014-2.01h5.12c1.156 0 2.098-.94 2.098-2.094v-3.433c0-1.109.902-2.01 2.013-2.01h16.116c1.111 0 2.014.901 2.014 2.01v3.433c0 1.155.94 2.094 2.098 2.094zM18.626 73.757h-2.478a.87.87 0 0 1-.87-.868v-2.473c0-.96-.777-1.743-1.745-1.743H6.838c-.96 0-1.745.777-1.745 1.743v2.473a.87.87 0 0 1-.87.868H1.746c-.961 0-1.746.776-1.746 1.742v6.682c0 .96.778 1.742 1.746 1.742h2.477c.484 0 .87.392.87.868v2.473c0 .96.778 1.743 1.745 1.743h6.695c.961 0 1.746-.777 1.746-1.743v-2.473c0-.483.392-.868.87-.868h2.477c.961 0 1.746-.776 1.746-1.742v-6.682c0-.96-.778-1.742-1.746-1.742"/></g><defs><clipPath id="a"><path d="M0 0h72v89H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
1
pkgs/clan-app/ui/icons/clan-logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="223" height="89" fill="currentColor"><g clip-path="url(#a)"><path d="M55.503 18.696h10.104a1.946 1.946 0 0 0 1.943-1.948v-7.79c0-1.075-.87-1.947-1.943-1.947h-3.186a1.863 1.863 0 0 1-1.866-1.87V1.947C60.555.872 59.685 0 58.612 0h-27.98a1.946 1.946 0 0 0-1.944 1.947v3.194c0 1.036-.832 1.87-1.865 1.87h-3.187a1.946 1.946 0 0 0-1.943 1.947v3.194c0 1.036-.832 1.87-1.866 1.87h-3.186a1.946 1.946 0 0 0-1.943 1.947s-.467 1.153-.467 23.253c0 19.763.467 21.913.467 21.913 0 1.075.87 1.948 1.943 1.948h3.186c1.034 0 1.866.833 1.866 1.87v3.271c0 1.036.831 1.87 1.865 1.87h3.265c1.033 0 1.865.833 1.865 1.87v3.193c0 1.075.87 1.948 1.943 1.948h27.981a1.946 1.946 0 0 0 1.943-1.948v-3.194c0-1.036.832-1.87 1.866-1.87h5.145a1.946 1.946 0 0 0 1.943-1.947v-9.285c0-1.075-.87-1.948-1.943-1.948H55.503a1.946 1.946 0 0 0-1.943 1.948v4.69c0 1.035-.832 1.869-1.866 1.869H37.55a1.863 1.863 0 0 1-1.866-1.87v-4.752c0-1.075-.87-1.947-1.943-1.947H29c-1.034 0-1.609.148-1.865-1.87-.078-.646-.125-1.44-.18-2.508-.147-2.68-.287-5.5-.287-13.539 0-11.24.288-16.81.466-18.369.18-1.558.832-1.87 1.866-1.87h4.741a1.946 1.946 0 0 0 1.943-1.947v-3.193c0-1.037.832-1.87 1.866-1.87h14.145c1.034 0 1.866.833 1.866 1.87v3.193c0 1.075.87 1.948 1.943 1.948M20.247 74.822h-2.293a.814.814 0 0 1-.808-.81v-2.298c0-.896-.723-1.62-1.617-1.62H9.327c-.894 0-1.617.724-1.617 1.62v2.298c0 .444-.365.81-.808.81H4.609c-.894 0-1.617.725-1.617 1.62v6.217c0 .896.723 1.62 1.617 1.62h2.293c.443 0 .808.366.808.81v2.299c0 .895.723 1.62 1.617 1.62h6.202c.894 0 1.617-.725 1.617-1.62v-2.299c0-.444.365-.81.808-.81h2.293c.894 0 1.617-.724 1.617-1.62v-6.216c0-.896-.723-1.62-1.617-1.62M221.135 35.04h-1.71a1.863 1.863 0 0 1-1.866-1.87v-3.272c0-1.036-.831-1.87-1.865-1.87h-3.265a1.863 1.863 0 0 1-1.865-1.87v-3.271c0-1.036-.832-1.87-1.865-1.87h-20.971a1.863 1.863 0 0 0-1.865 1.87v3.965c0 .514-.42.935-.933.935h-3.559c-.513 0-.84-.32-.933-.935l-.622-3.918c-.148-1.099-.676-1.777-1.788-1.777l-3.653-.14h-2.052a3.736 3.736 0 0 0-3.73 3.74V61.68a3.736 3.736 0 0 1-3.731 3.739h-8.394a1.863 1.863 0 0 1-1.866-1.87V36.714c0-11.825-7.461-18.813-22.556-18.813-13.718 0-20.325 5.04-21.203 14.443-.109 1.153.552 1.815 1.702 1.815l7.757.569c1.143.1 1.594-.554 1.811-1.652.77-3.74 4.174-5.827 9.933-5.827 7.081 0 10.042 3.358 10.042 9.076v3.014c0 1.036-.831 1.87-1.865 1.87l-.342-.024h-9.715c-15.421 0-22.984 5.983-22.984 17.956 0 3.802.778 7.058 2.254 9.738h-.59c-1.765-1.27-2.457-2.236-3.055-2.93-.256-.295-.653-.537-1.345-.537h-1.717l-5.993.008h-3.264a3.736 3.736 0 0 1-3.731-3.74V1.769C89.74.654 89.072 0 87.969 0H79.55c-1.034 0-1.865.732-1.865 1.768l-.024 54.304v13.554c0 4.13 3.343 7.479 7.462 7.479h50.84c8.448-.429 8.604-3.42 9.436-4.542.645 3.56 1.865 4.347 4.71 4.518 8.137.117 18.343.032 18.49.024h4.975c4.119 0 6.684-3.35 6.684-7.479l.777-27.264c0-1.036.832-1.87 1.866-1.87h2.021a1.56 1.56 0 0 0 1.554-1.558v-3.583c0-1.036.832-1.87 1.866-1.87h11.868a3.37 3.37 0 0 1 3.366 3.373v3.249c0 1.075.87 1.947 1.943 1.947h4.119c.513 0 .933.42.933.935v32.25c0 1.036.831 1.87 1.865 1.87h6.84a3.736 3.736 0 0 0 3.731-3.74V36.91c0-1.036-.832-1.87-1.866-1.87zM142.64 54.225c0 8.927-6.132 14.715-15.335 14.715-6.606 0-9.793-2.953-9.793-8.748 0-6.442 3.832-9.636 11.62-9.636h13.508v3.669"/></g><defs><clipPath id="a"><path d="M0 0h223v89H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
1
pkgs/clan-app/ui/icons/close-circle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M35.667 7.667h4.666v4.666H45v23.334h-4.667v4.666h-4.666V45H12.333v-4.667H7.667v-4.666H3V12.333h4.667V7.667h4.666V3h23.334zM15 29.4V33h3.6v-3.6zm14.4 0V33H33v-3.6zm-10.8-3.6v3.6h3.6v-3.6zm7.2 0v3.6h3.6v-3.6zm-3.6-3.6v3.6h3.6v-3.6zm-3.6-3.6v3.6h3.6v-3.6zm7.2 0v3.6h3.6v-3.6zM15 15v3.6h3.6V15zm14.4 0v3.6H33V15z"/></svg>
|
||||
|
After Width: | Height: | Size: 409 B |
1
pkgs/clan-app/ui/icons/code.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M15.6 9h4.2v4.286h-4.2zM11.4 13.286h4.2v4.285h-4.2zM7.2 17.571h4.2v4.286H7.2zM3 21.857h4.2v4.286H3zM7.2 26.143h4.2v4.286H7.2zM11.4 30.429h4.2v4.285h-4.2zM15.6 34.714h4.2V39h-4.2zM32.4 9h-4.2v4.286h4.2zM36.6 13.286h-4.2v4.285h4.2zM40.8 17.571h-4.2v4.286h4.2zM45 21.857h-4.2v4.286H45zM40.8 26.143h-4.2v4.286h4.2z"/><path d="M36.6 30.429h-4.2v4.285h4.2zM32.4 34.714h-4.2V39h4.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 476 B |
@@ -1,25 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="42" viewBox="0 0 35 42" fill="currentColor">
|
||||
<rect y="6" width="6" height="6"/>
|
||||
<rect x="6" y="6" width="6" height="6"/>
|
||||
<rect x="12" y="6" width="6" height="6"/>
|
||||
<rect x="6" y="12" width="6" height="6"/>
|
||||
<rect x="18" y="18" width="6" height="6"/>
|
||||
<rect x="18" y="12" width="6" height="6"/>
|
||||
<rect x="12" y="24" width="6" height="6"/>
|
||||
<rect x="12" y="18" width="6" height="6"/>
|
||||
<rect x="12" y="12" width="6" height="6"/>
|
||||
<rect width="6" height="6"/>
|
||||
<rect x="6" width="6" height="6"/>
|
||||
<rect x="24" y="18" width="6" height="6"/>
|
||||
<rect y="12" width="6" height="6"/>
|
||||
<rect x="6" y="18" width="6" height="6"/>
|
||||
<rect y="18" width="6" height="6"/>
|
||||
<rect y="24" width="6" height="6"/>
|
||||
<rect y="30" width="6" height="6"/>
|
||||
<rect y="36" width="6" height="6"/>
|
||||
<rect x="6" y="24" width="6" height="6"/>
|
||||
<rect x="18" y="24" width="6" height="6"/>
|
||||
<rect x="24" y="24" width="6" height="6"/>
|
||||
<rect x="29" y="24" width="6" height="6"/>
|
||||
<rect x="6" y="30" width="6" height="6"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M8 9h6v6H8zM14 9h6v6h-6zM20 9h6v6h-6zM14 15h6v6h-6zM26 21h6v6h-6zM26 15h6v6h-6zM20 27h6v6h-6zM20 21h6v6h-6zM20 15h6v6h-6zM8 3h6v6H8zM14 3h6v6h-6zM32 21h6v6h-6zM8 15h6v6H8zM14 21h6v6h-6zM8 21h6v6H8zM8 27h6v6H8zM8 33h6v6H8zM8 39h6v6H8zM14 27h6v6h-6zM26 27h6v6h-6zM32 27h6v6h-6z"/><path d="M37 27h6v6h-6zM14 33h6v6h-6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 416 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path fill-rule="evenodd" d="M8.7 9h11.1v5.625H42v3.75H19.8V24H8.7v-5.625H5v-3.75h3.7zm3.7 3.75v7.5h3.7v-7.5zM27.2 24h11.1v5.625H42v3.75h-3.7V39H27.2v-5.625H5v-3.75h22.2zm3.7 3.75v7.5h3.7v-7.5z" clip-rule="evenodd"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path fill-rule="evenodd" d="M11.7 9h8.1v5.625H42v3.75H19.8V24h-8.1v-5.625H5v-3.75h6.7zm15.5 15h8.1v5.625H42v3.75h-6.7V39h-8.1v-5.625H5v-3.75h22.2z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 259 B |
1
pkgs/clan-app/ui/icons/general.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M34.25 9.3H45v34.4H2.001v-4.3H2V13.6h.001V9.3H12.75V5h21.5zM19.201 30.8v4.3h8.6v-4.3zm-4.3-4.3v4.3h4.3v-4.3zm12.9 0v4.3h4.3v-4.3zm-12.9-8.6v4.3h4.3v-4.3zm12.9 0v4.3h4.3v-4.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
|
||||
<path d="M20.2002 12.7998H23V15.5996H25.8008V14.2002H28.6006V10H34.2002V12.7998H37V15.5996H39.8008V24H37V26.7998H34.2002V29.5996H31.4004V32.4004H28.6006V35.2002H25.8008V38H23V35.2002H20.2002V32.4004H17.4004V29.5996H14.6006V26.7998H11.8008V24H9V15.5996H11.8008V12.7998H14.6006V10H20.2002V12.7998Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M20.2 12.8H23v2.8h2.8v-1.4h2.8V10h5.6v2.8H37v2.8h2.8V24H37v2.8h-2.8v2.8h-2.8v2.8h-2.8v2.8h-2.8V38H23v-2.8h-2.8v-2.8h-2.8v-2.8h-2.8v-2.8h-2.8V24H9v-8.4h2.8v-2.8h2.8V10h5.6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 272 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
|
||||
<path d="M38.666 5V39.667H38.667V5H43V44H4V5H38.666ZM12.666 35.334H16.999V31H12.666V35.334ZM29.999 35.334H34.333V31H29.999V35.334ZM21.333 26.667H25.666V22.334H21.333V26.667ZM12.666 18H16.999V13.667H12.666V18ZM29.999 18H34.333V13.667H29.999V18Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M38.666 5v34.667h.001V5H43v39H4V5zm-26 30.334h4.333V31h-4.333zm17.333 0h4.334V31h-4.334zm-8.666-8.667h4.333v-4.333h-4.333zM12.666 18h4.333v-4.333h-4.333zm17.333 0h4.334v-4.333h-4.334z"/></svg>
|
||||
|
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 284 B |
1
pkgs/clan-app/ui/icons/minimize.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M7.667 45H3v-4.667h4.667zM17 45h-4.667v-4.667H17zm9.333 0h-4.666v-4.667h4.666zm9.334 0H31v-4.667h4.667zM45 45h-4.667v-4.667H45zM7.667 35.667H3V31h4.667zm37.333 0h-4.667V31H45zM7.667 26.333H3v-4.666h4.667zm37.333 0h-4.667V7.667H21.667V3H45zM7.667 17H3v-4.667h4.667zm0-9.333H3V3h4.667zm9.333 0h-4.667V3H17z"/></svg>
|
||||
|
After Width: | Height: | Size: 405 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
|
||||
<path d="M38 42H10V38H6V10H10V6H38V10H42V38H38V42ZM18 32H30V28H18V32ZM14 28H18V24H14V28ZM30 28H34V24H30V28ZM16 20H20V16H16V20ZM28 20H32V16H28V20Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M38 42H10v-4H6V10h4V6h28v4h4v28h-4zM18 32h12v-4H18zm-4-4h4v-4h-4zm16 0h4v-4h-4zm-14-8h4v-4h-4zm12 0h4v-4h-4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 209 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
|
||||
<path d="M42 42H14V38H38V14H42V42ZM34 6V34H6V6H34ZM18 18H14V22H18V26H22V22H26V18H22V14H18V18Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M42 42H14v-4h24V14h4zM34 6v28H6V6zM18 18h-4v4h4v4h4v-4h4v-4h-4v-4h-4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 211 B After Width: | Height: | Size: 170 B |
@@ -1,13 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="27" viewBox="0 0 38 27" fill="currentColor">
|
||||
<rect x="4.46155" y="4.15381" width="4.15385" height="4.15385"/>
|
||||
<rect x="29.3846" y="4.15381" width="4.15385" height="4.15385"/>
|
||||
<rect x="8.61539" width="4.15385" height="4.15385"/>
|
||||
<rect x="33.5385" width="4.15385" height="4.15385"/>
|
||||
<rect x="0.307678" width="4.15385" height="4.15385"/>
|
||||
<rect x="25.2308" width="4.15385" height="4.15385"/>
|
||||
<rect x="0.307678" y="8.30762" width="4.15385" height="4.15385"/>
|
||||
<rect x="25.2308" y="8.30762" width="4.15385" height="4.15385"/>
|
||||
<rect x="8.61539" y="8.30762" width="4.15385" height="4.15385"/>
|
||||
<rect x="33.5385" y="8.30762" width="4.15385" height="4.15385"/>
|
||||
<rect x="4" y="23" width="30" height="4"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M39.247 38H8.753v-4.148h30.494zM9.223 22.923H5v-4.308h4.223zm8.444 0h-4.223v-4.308h4.223zm16.889 0h-4.223v-4.308h4.223zm8.444 0h-4.223v-4.308H43zm-29.556-4.308H9.223v-4.307h4.221zm25.333 0h-4.221v-4.307h4.221zM9.223 14.308H5V10h4.223zm8.444 0h-4.223V10h4.223zm16.889 0h-4.223V10h4.223zm8.444 0h-4.223V10H43z"/></svg>
|
||||
|
Before Width: | Height: | Size: 801 B After Width: | Height: | Size: 408 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
|
||||
<path d="M39.2002 39.2002H43V43H39.2002V39.2021H35.3994V35.4014H39.2002V39.2002ZM27.7998 8.80078H31.5996V31.6016H27.7998V35.4004H12.6006V12.6016H20.2002V8.80078H12.6006V5H27.7998V8.80078ZM35.4004 35.4004H31.6006V31.6006H35.4004V35.4004ZM12.5996 12.5996H8.7998V20.2002H12.5996V31.6016H8.7998V27.8008H5V12.5996H8.7998V8.80078H12.5996V12.5996ZM35.4004 27.8008H31.6006V12.5996H35.4004V27.8008Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M39.2 39.2H43V43h-3.8v-3.798h-3.8v-3.8h3.8zM27.8 8.8h3.8v22.802h-3.8V35.4H12.6V12.602h7.6V8.8h-7.6V5h15.2zm7.6 26.6h-3.8v-3.8h3.8zM12.6 12.6H8.8v7.6h3.8v11.402H8.8V27.8H5V12.6h3.8V8.8h3.8zm22.8 15.2h-3.8V12.6h3.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 507 B After Width: | Height: | Size: 314 B |
1
pkgs/clan-app/ui/icons/services.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M43 43H5V20.2h38zm-3.8-26.6H8.8v-3.8h30.4zm-3.8-7.6H12.6V5h22.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 165 B |
@@ -1,8 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="23" viewBox="0 0 36 23" fill="currentColor">
|
||||
<rect x="27" width="22.5" height="4.5" transform="rotate(90 27 0)"/>
|
||||
<rect x="31.5" y="4.5" width="13.5" height="4.5" transform="rotate(90 31.5 4.5)"/>
|
||||
<rect x="36" y="9" width="4.5" height="4.5" transform="rotate(90 36 9)"/>
|
||||
<rect width="22.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 9 0)"/>
|
||||
<rect width="13.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 4.5 4.5)"/>
|
||||
<rect width="4.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 0 9)"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M33 13v22.5h-4.5V13zM37.5 17.5V31H33V17.5zM42 22v4.5h-4.5V22zM15 13v22.5h4.5V13zM10.5 17.5V31H15V17.5zM6 22v4.5h4.5V22z"/></svg>
|
||||
|
Before Width: | Height: | Size: 624 B After Width: | Height: | Size: 220 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
|
||||
<path d="M39.4004 31.2002H35.7998V34.7998H32.2002V38.3994H28.5996V42H25V38.3994H21.4004V34.7998H17.7998V31.2002H14.2002V27.6006H39.4004V31.2002ZM28.5996 13.2002H32.2002V16.7998H35.7998V20.3994H39.4004V24H43V27.5996H10.5996V24H7V9.60059H28.5996V13.2002ZM14.2002 13.2002V16.7998H17.7998V13.2002H14.2002ZM25 9.59961H7V6H25V9.59961Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M39.4 31.2h-3.6v3.6h-3.6v3.6h-3.6V42H25v-3.6h-3.6v-3.6h-3.6v-3.6h-3.6v-3.6h25.2zm-10.8-18h3.6v3.6h3.6v3.6h3.6V24H43v3.6H10.6V24H7V9.6h21.6zm-14.4 0v3.6h3.6v-3.6zM25 9.6H7V6h18z"/></svg>
|
||||
|
Before Width: | Height: | Size: 446 B After Width: | Height: | Size: 277 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8 0H28.4V9.6H18.8V0ZM11.6 12H35.6V16.8H30.8V33.6V48H26V33.6H21.2V48H16.4V33.6V16.8H11.6V12ZM6.8 7.2V12H11.6V7.2H6.8ZM6.8 7.2L2 7.2V2.4H6.8V7.2ZM40.4 7.2V12H35.6V7.2H40.4ZM40.4 7.2L40.4 2.4H45.2V7.2L40.4 7.2Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path fill-rule="evenodd" d="M18.8 0h9.6v9.6h-9.6zm-7.2 12h24v4.8h-4.8V48H26V33.6h-4.8V48h-4.8V16.8h-4.8zM6.8 7.2V12h4.8V7.2zm0 0H2V2.4h4.8zm33.6 0V12h-4.8V7.2zm0 0V2.4h4.8v4.8z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 289 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path fill-rule="evenodd" d="M42 6H5v37h37zm-20.555 8.222h4.111v12.334h-4.111zm0 16.445h4.111v4.11h-4.111z" clip-rule="evenodd"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor"><path d="M26 6h4v4h4v4h4v4h4v4h4v4h-4v4h-4v4h-4v4h-4v4h-4v4h-4v-4h-4v-4h-4v-4h-4v-4H6v-4H2v-4h4v-4h4v-4h4v-4h4V6h4V2h4zm-4 24v4h4v-4zm0-16v12h4V14z"/></svg>
|
||||
|
Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 239 B |
272
pkgs/clan-app/ui/package-lock.json
generated
@@ -15,9 +15,11 @@
|
||||
"@modular-forms/solid": "^0.25.1",
|
||||
"@solid-primitives/storage": "^4.3.2",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@tanstack/eslint-plugin-query": "^5.51.12",
|
||||
"@tanstack/solid-query": "^5.76.0",
|
||||
"@tanstack/solid-query-devtools": "^5.83.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||
"@tanstack/solid-query": "^5.85.5",
|
||||
"@tanstack/solid-query-devtools": "^5.85.5",
|
||||
"@tanstack/solid-query-persist-client": "^5.85.5",
|
||||
"@tanstack/solid-virtual": "^3.13.12",
|
||||
"solid-js": "^1.9.7",
|
||||
"solid-toast": "^0.5.0",
|
||||
"three": "^0.176.0",
|
||||
@@ -2487,12 +2489,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query": {
|
||||
"version": "5.81.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.81.2.tgz",
|
||||
"integrity": "sha512-h4k6P6fm5VhKP5NkK+0TTVpGGyKQdx6tk7NYYG7J7PkSu7ClpLgBihw7yzK8N3n5zPaF3IMyErxfoNiXWH/3/A==",
|
||||
"version": "5.83.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.83.1.tgz",
|
||||
"integrity": "sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^8.18.1"
|
||||
"@typescript-eslint/utils": "^8.37.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2502,10 +2504,169 @@
|
||||
"eslint": "^8.57.0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz",
|
||||
"integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.40.0",
|
||||
"@typescript-eslint/types": "^8.40.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz",
|
||||
"integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz",
|
||||
"integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz",
|
||||
"integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz",
|
||||
"integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.40.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/visitor-keys": "8.40.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz",
|
||||
"integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.40.0",
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"@typescript-eslint/typescript-estree": "8.40.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz",
|
||||
"integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.40.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.83.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
|
||||
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
|
||||
"version": "5.85.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz",
|
||||
"integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2513,22 +2674,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-devtools": {
|
||||
"version": "5.81.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz",
|
||||
"integrity": "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==",
|
||||
"version": "5.84.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz",
|
||||
"integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/solid-query": {
|
||||
"version": "5.83.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.83.0.tgz",
|
||||
"integrity": "sha512-RF8Tv9+6+Kmzj+EafbTzvzzPq+J5SzHtc1Tz3D2MZ/EvlZTH+GL5q4HNnWK3emg7CB6WzyGnTuERmmWJaZs8/w==",
|
||||
"node_modules/@tanstack/query-persist-client-core": {
|
||||
"version": "5.85.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.5.tgz",
|
||||
"integrity": "sha512-2JQiyiTVaaUu8pwPqOp6tjNa64ZN+0T9eZ3lfksV4le1VuG99fTcAYmZFIydvzwWlSM7GEF/1kpl5bwW2Y1qfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.83.0"
|
||||
"@tanstack/query-core": "5.85.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/solid-query": {
|
||||
"version": "5.85.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.85.5.tgz",
|
||||
"integrity": "sha512-0o0Ibk9wqydm4JatbIjmvDu1+MofeZ1bU9BKwAbpt7HYjrLVCeddpW6zGmp41nN7t/mHJyR+ctW9oiNumCkEfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.85.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2539,22 +2713,65 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/solid-query-devtools": {
|
||||
"version": "5.83.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-devtools/-/solid-query-devtools-5.83.0.tgz",
|
||||
"integrity": "sha512-Z0wQlAWXz/U2bJ/paMRBTDhMoPnB9Te6GmA21sXnI+nDnAAPZRcPxFBiCgYJS3eFsvbkdRGJwoUSQrdIgy0shg==",
|
||||
"version": "5.85.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-devtools/-/solid-query-devtools-5.85.5.tgz",
|
||||
"integrity": "sha512-9rC22wILlV9Lcsi4xKPmzRkNio1NOxNT36diIS+HjpOmhsEP/aI8XkNKQa/KPhhaSN2naYaTCJamh7eBAQ0Ymg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.81.2"
|
||||
"@tanstack/query-devtools": "5.84.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/solid-query": "^5.83.0",
|
||||
"@tanstack/solid-query": "^5.85.5",
|
||||
"solid-js": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/solid-query-persist-client": {
|
||||
"version": "5.85.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-persist-client/-/solid-query-persist-client-5.85.5.tgz",
|
||||
"integrity": "sha512-2aG7UnLZlfE3R4XKqYuIeXVKjJOghjsjq4EU2Ifp915FTBZcZo61sEw1zRqRlrDjEFYAs4kJUZwqViDSJYyX2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-persist-client-core": "5.85.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/solid-query": "^5.85.5",
|
||||
"solid-js": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/solid-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/solid-virtual/-/solid-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-0dS8GkBTmbuM9cUR6Jni0a45eJbd32CAEbZj8HrZMWIj3lu974NpGz5ywcomOGJ9GdeHuDaRzlwtonBbKV1ihQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
@@ -2894,6 +3111,7 @@
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
|
||||
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.36.0",
|
||||
@@ -2915,6 +3133,7 @@
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
|
||||
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
@@ -2932,6 +3151,7 @@
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
|
||||
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2972,6 +3192,7 @@
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
|
||||
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2985,6 +3206,7 @@
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
|
||||
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.36.0",
|
||||
@@ -3013,6 +3235,7 @@
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -3025,6 +3248,7 @@
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
|
||||
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
@@ -3048,6 +3272,7 @@
|
||||
"version": "8.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
|
||||
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.36.0",
|
||||
@@ -3065,6 +3290,7 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
@@ -72,9 +72,11 @@
|
||||
"@modular-forms/solid": "^0.25.1",
|
||||
"@solid-primitives/storage": "^4.3.2",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@tanstack/eslint-plugin-query": "^5.51.12",
|
||||
"@tanstack/solid-query": "^5.76.0",
|
||||
"@tanstack/solid-query-devtools": "^5.83.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||
"@tanstack/solid-query": "^5.85.5",
|
||||
"@tanstack/solid-query-devtools": "^5.85.5",
|
||||
"@tanstack/solid-query-persist-client": "^5.85.5",
|
||||
"@tanstack/solid-virtual": "^3.13.12",
|
||||
"solid-js": "^1.9.7",
|
||||
"solid-toast": "^0.5.0",
|
||||
"three": "^0.176.0",
|
||||
|
||||
@@ -5,3 +5,14 @@
|
||||
.horizontal_button {
|
||||
@apply grow max-w-[18rem];
|
||||
}
|
||||
|
||||
/* Vendored from tooltip */
|
||||
.tooltipContent {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
{value() && (
|
||||
<Tooltip placement="top">
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content class="tooltip-content">
|
||||
<Tooltip.Content class={styles.tooltipContent}>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
|
||||
@@ -55,23 +55,14 @@ export const Label = (props: LabelProps) => {
|
||||
<Tooltip
|
||||
placement="top"
|
||||
inverted={props.inverted}
|
||||
trigger={
|
||||
<Icon
|
||||
icon="Info"
|
||||
color="tertiary"
|
||||
inverted={props.inverted}
|
||||
size={props.size == "default" ? "0.85em" : "0.75rem"}
|
||||
/>
|
||||
}
|
||||
description={props.tooltip}
|
||||
>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={!props.inverted}
|
||||
>
|
||||
{props.tooltip}
|
||||
</Typography>
|
||||
<Icon
|
||||
icon="Info"
|
||||
color="tertiary"
|
||||
inverted={props.inverted}
|
||||
size={props.size == "default" ? "0.85em" : "0.75rem"}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</props.labelComponent>
|
||||
|
||||
@@ -27,7 +27,7 @@ div.form-field.machine-tags {
|
||||
@apply bg-def-acc-1 outline-def-acc-2;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&:not(:read-only):focus-visible {
|
||||
@apply bg-def-1 outline-def-acc-3;
|
||||
|
||||
box-shadow:
|
||||
@@ -106,7 +106,7 @@ div.form-field.machine-tags {
|
||||
@apply bg-inv-acc-2 outline-inv-acc-2;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&:not(:read-only):focus-visible {
|
||||
@apply bg-inv-acc-4;
|
||||
box-shadow:
|
||||
0 0 0 0.125rem theme(colors.bg.inv.1),
|
||||
|
||||
@@ -17,7 +17,7 @@ div.form-field {
|
||||
@apply bg-def-acc-1 outline-def-acc-2;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&:not(:read-only):focus-visible {
|
||||
@apply bg-def-1 outline-def-acc-3;
|
||||
|
||||
box-shadow:
|
||||
@@ -119,7 +119,7 @@ div.form-field {
|
||||
@apply bg-inv-acc-2 outline-inv-acc-2;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&:not(:read-only):focus-visible {
|
||||
@apply bg-inv-acc-4;
|
||||
box-shadow:
|
||||
0 0 0 0.125rem theme(colors.bg.inv.1),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import cx from "classnames";
|
||||
import { Component, JSX, splitProps } from "solid-js";
|
||||
|
||||
import Address from "@/icons/address.svg";
|
||||
import AI from "@/icons/ai.svg";
|
||||
import ArrowBottom from "@/icons/arrow-bottom.svg";
|
||||
import ArrowLeft from "@/icons/arrow-left.svg";
|
||||
import ArrowRight from "@/icons/arrow-right.svg";
|
||||
@@ -10,9 +13,12 @@ import CaretLeft from "@/icons/caret-left.svg";
|
||||
import CaretRight from "@/icons/caret-right.svg";
|
||||
import CaretUp from "@/icons/caret-up.svg";
|
||||
import Checkmark from "@/icons/checkmark.svg";
|
||||
import CheckSolid from "@/icons/check-solid.svg";
|
||||
import ClanIcon from "@/icons/clan-icon.svg";
|
||||
import Cursor from "@/icons/cursor.svg";
|
||||
import Close from "@/icons/close.svg";
|
||||
import CloseCircle from "@/icons/close-circle.svg";
|
||||
import Code from "@/icons/code.svg";
|
||||
import Cursor from "@/icons/cursor.svg";
|
||||
import Download from "@/icons/download.svg";
|
||||
import Edit from "@/icons/edit.svg";
|
||||
import Expand from "@/icons/expand.svg";
|
||||
@@ -21,35 +27,39 @@ import EyeOpen from "@/icons/eye-open.svg";
|
||||
import Filter from "@/icons/filter.svg";
|
||||
import Flash from "@/icons/flash.svg";
|
||||
import Folder from "@/icons/folder.svg";
|
||||
import General from "@/icons/general.svg";
|
||||
import Grid from "@/icons/grid.svg";
|
||||
import Heart from "@/icons/heart.svg";
|
||||
import Info from "@/icons/info.svg";
|
||||
import List from "@/icons/list.svg";
|
||||
import Load from "@/icons/load.svg";
|
||||
import Machine from "@/icons/machine.svg";
|
||||
import Minimize from "@/icons/minimize.svg";
|
||||
import Modules from "@/icons/modules.svg";
|
||||
import More from "@/icons/more.svg";
|
||||
import NewMachine from "@/icons/new-machine.svg";
|
||||
import Offline from "@/icons/offline.svg";
|
||||
import Paperclip from "@/icons/paperclip.svg";
|
||||
import Plus from "@/icons/plus.svg";
|
||||
import Reload from "@/icons/reload.svg";
|
||||
import Report from "@/icons/report.svg";
|
||||
import Search from "@/icons/search.svg";
|
||||
import Settings from "@/icons/settings.svg";
|
||||
import Trash from "@/icons/trash.svg";
|
||||
import Update from "@/icons/update.svg";
|
||||
import WarningFilled from "@/icons/warning-filled.svg";
|
||||
import Modules from "@/icons/modules.svg";
|
||||
import NewMachine from "@/icons/new-machine.svg";
|
||||
import AI from "@/icons/ai.svg";
|
||||
import User from "@/icons/user.svg";
|
||||
import Heart from "@/icons/heart.svg";
|
||||
import SearchFilled from "@/icons/search-filled.svg";
|
||||
import Offline from "@/icons/offline.svg";
|
||||
import Services from "@/icons/services.svg";
|
||||
import Settings from "@/icons/settings.svg";
|
||||
import Switch from "@/icons/switch.svg";
|
||||
import Tag from "@/icons/tag.svg";
|
||||
import Machine from "@/icons/machine.svg";
|
||||
import Trash from "@/icons/trash.svg";
|
||||
import Update from "@/icons/update.svg";
|
||||
import User from "@/icons/user.svg";
|
||||
import WarningFilled from "@/icons/warning-filled.svg";
|
||||
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
import { Color, fgClass } from "../colors";
|
||||
|
||||
const icons = {
|
||||
Address,
|
||||
AI,
|
||||
ArrowBottom,
|
||||
ArrowLeft,
|
||||
@@ -61,8 +71,11 @@ const icons = {
|
||||
CaretRight,
|
||||
CaretUp,
|
||||
Checkmark,
|
||||
CheckSolid,
|
||||
ClanIcon,
|
||||
Close,
|
||||
CloseCircle,
|
||||
Code,
|
||||
Cursor,
|
||||
Download,
|
||||
Edit,
|
||||
@@ -72,12 +85,14 @@ const icons = {
|
||||
Filter,
|
||||
Flash,
|
||||
Folder,
|
||||
General,
|
||||
Grid,
|
||||
Heart,
|
||||
Info,
|
||||
List,
|
||||
Load,
|
||||
Machine,
|
||||
Minimize,
|
||||
Modules,
|
||||
More,
|
||||
NewMachine,
|
||||
@@ -88,6 +103,7 @@ const icons = {
|
||||
Report,
|
||||
Search,
|
||||
SearchFilled,
|
||||
Services,
|
||||
Settings,
|
||||
Switch,
|
||||
Tag,
|
||||
@@ -101,8 +117,6 @@ export type IconVariant = keyof typeof icons;
|
||||
|
||||
const viewBoxes: Partial<Record<IconVariant, string>> = {
|
||||
ClanIcon: "0 0 72 89",
|
||||
Offline: "0 0 38 27",
|
||||
Cursor: "0 0 35 42",
|
||||
};
|
||||
|
||||
export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.modal {
|
||||
@apply w-screen max-w-2xl h-fit flex flex-col;
|
||||
|
||||
.content {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply flex items-center justify-between;
|
||||
}
|
||||
|
||||
.clans {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Modal } from "../../components/Modal/Modal";
|
||||
import cx from "classnames";
|
||||
import styles from "./ListClansModal.module.css";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { navigateToClan, navigateToOnboarding } from "@/src/hooks/clan";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { For, Show } from "solid-js";
|
||||
import { activeClanURI, clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { useClanListQuery } from "@/src/hooks/queries";
|
||||
import { Alert } from "@/src/components/Alert/Alert";
|
||||
import { NavSection } from "../NavSection/NavSection";
|
||||
|
||||
export interface ListClansModalProps {
|
||||
onClose?: () => void;
|
||||
error?: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ListClansModal = (props: ListClansModalProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const query = useClanListQuery(clanURIs());
|
||||
|
||||
// we only want clans we could interrogate successfully
|
||||
// todo how to surface the ones that failed to users?
|
||||
const clanList = () => query.filter((it) => it.isSuccess);
|
||||
|
||||
const selectClan = (uri: string) => () => {
|
||||
if (uri == activeClanURI()) {
|
||||
navigateToClan(navigate, uri);
|
||||
} else {
|
||||
setActiveClanURI(uri);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Select Clan"
|
||||
open
|
||||
onClose={props.onClose}
|
||||
class={cx(styles.modal)}
|
||||
>
|
||||
<div class={cx(styles.content)}>
|
||||
<Show when={props.error}>
|
||||
<Alert
|
||||
type="error"
|
||||
title={props.error?.title || ""}
|
||||
description={props.error?.description}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<div class={cx(styles.header)}>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="xs"
|
||||
color="tertiary"
|
||||
transform="uppercase"
|
||||
>
|
||||
Your Clans
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
ghost
|
||||
size="s"
|
||||
startIcon="Plus"
|
||||
onClick={() => {
|
||||
props.onClose?.();
|
||||
navigateToOnboarding(navigate, true);
|
||||
}}
|
||||
>
|
||||
Add Clan
|
||||
</Button>
|
||||
</div>
|
||||
<ul class={cx(styles.clans)}>
|
||||
<For each={clanList()}>
|
||||
{(clan) => (
|
||||
<li>
|
||||
<NavSection
|
||||
label={clan.data.name}
|
||||
description={clan.data.description ?? undefined}
|
||||
onClick={selectClan(clan.data.uri)}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -37,8 +37,8 @@
|
||||
}
|
||||
|
||||
.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);
|
||||
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center z-50;
|
||||
@apply bg-inv-4 opacity-70;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
|
||||
@@ -13,9 +13,9 @@ import Icon from "../Icon/Icon";
|
||||
import cx from "classnames";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
export type ModalContextType = {
|
||||
export interface ModalContextType {
|
||||
portalRef: HTMLDivElement;
|
||||
};
|
||||
}
|
||||
|
||||
const ModalContext = createContext<unknown>();
|
||||
|
||||
@@ -30,7 +30,7 @@ export const useModalContext = () => {
|
||||
export interface ModalProps {
|
||||
id?: string;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
children: JSX.Element;
|
||||
mount?: Node;
|
||||
class?: string;
|
||||
@@ -57,13 +57,11 @@ export const Modal = (props: ModalProps) => {
|
||||
>
|
||||
{props.title}
|
||||
</Typography>
|
||||
<KDialog.CloseButton
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
<Icon icon="Close" size="0.75rem" />
|
||||
</KDialog.CloseButton>
|
||||
<Show when={props.onClose}>
|
||||
<KDialog.CloseButton onClick={props.onClose}>
|
||||
<Icon icon="Close" size="0.75rem" />
|
||||
</KDialog.CloseButton>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={props.metaHeader}>
|
||||
{(metaHeader) => (
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.navSection {
|
||||
@apply w-full flex gap-1.5 items-center justify-between;
|
||||
@apply rounded-md px-4 py-5 bg-def-2;
|
||||
|
||||
.meta {
|
||||
@apply flex flex-col gap-1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-def-3 cursor-pointer;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-def-4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import {
|
||||
NavSection,
|
||||
NavSectionProps,
|
||||
} from "@/src/components/NavSection/NavSection";
|
||||
|
||||
const meta: Meta<NavSectionProps> = {
|
||||
title: "Components/NavSection",
|
||||
component: NavSection,
|
||||
decorators: [
|
||||
(Story: StoryObj) => (
|
||||
<div class="w-96">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<NavSectionProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: "My Clan",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDescription: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
description:
|
||||
"This is my Clan. There are many Clans like it, but this one is mine",
|
||||
},
|
||||
};
|
||||
35
pkgs/clan-app/ui/src/components/NavSection/NavSection.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import cx from "classnames";
|
||||
import styles from "./NavSection.module.css";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
import { Show } from "solid-js";
|
||||
|
||||
export interface NavSectionProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const NavSection = (props: NavSectionProps) => {
|
||||
return (
|
||||
<Button class={cx(styles.navSection)} onClick={props.onClick}>
|
||||
<div class={cx(styles.meta)}>
|
||||
<Typography hierarchy="label" size="default" weight="bold">
|
||||
{props.label}
|
||||
</Typography>
|
||||
<Show when={props.description}>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="s"
|
||||
weight="normal"
|
||||
color="secondary"
|
||||
>
|
||||
{props.description}
|
||||
</Typography>
|
||||
</Show>
|
||||
</div>
|
||||
<Icon icon="CaretRight" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
102
pkgs/clan-app/ui/src/components/Search/Search.module.css
Normal file
@@ -0,0 +1,102 @@
|
||||
.searchInput {
|
||||
@apply w-full bg-inv-4 fg-inv-1;
|
||||
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
font-family: "Archivo", sans-serif;
|
||||
line-height: 132%;
|
||||
|
||||
&::placeholder {
|
||||
@apply fg-def-4;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
&[data-invalid] {
|
||||
@apply outline-semantic-error-4;
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
@apply outline-def-2 fg-def-4 cursor-not-allowed;
|
||||
}
|
||||
|
||||
&[data-readonly] {
|
||||
@apply outline-none border-none bg-inherit p-0 cursor-auto resize-none;
|
||||
}
|
||||
}
|
||||
|
||||
.searchHeader {
|
||||
@apply bg-inv-3 flex gap-2 items-center p-2 rounded-md z-50;
|
||||
@apply px-3 pt-3 pb-2;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
@apply flex items-center gap-2 bg-inv-4 rounded-md px-1 w-full;
|
||||
|
||||
:has :focus-visible {
|
||||
@apply bg-def-1;
|
||||
}
|
||||
}
|
||||
|
||||
.searchItem {
|
||||
@apply flex py-1 px-2 pr-4 gap-2 justify-between items-center rounded-md;
|
||||
|
||||
& [role="option"] {
|
||||
@apply flex flex-col w-full;
|
||||
}
|
||||
|
||||
/* Icon */
|
||||
& [role="complementary"] {
|
||||
@apply size-8 flex items-center justify-center bg-white rounded-md;
|
||||
}
|
||||
|
||||
&[data-highlighted],
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
@apply bg-inv-acc-2;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-inv-acc-3;
|
||||
}
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
@apply bg-gradient-to-b from-bg-inv-3 to-bg-inv-4;
|
||||
|
||||
@apply h-[14.5rem] rounded-lg;
|
||||
|
||||
border: 1px solid #2b4647;
|
||||
|
||||
background:
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.18) 0%, rgba(0, 0, 0, 0.18) 100%),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
var(--clr-bg-inv-3, rgba(43, 70, 71, 0.79)) 0%,
|
||||
var(--clr-bg-inv-4, rgba(32, 54, 55, 0.79)) 100%
|
||||
);
|
||||
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.searchContent {
|
||||
@apply px-3;
|
||||
height: calc(14.5rem - 4rem);
|
||||
}
|
||||
|
||||
@keyframes contentHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
75
pkgs/clan-app/ui/src/components/Search/Search.stories.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
|
||||
import { Search, SearchProps, Module } from "./Search";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Search",
|
||||
component: Search,
|
||||
} satisfies Meta<SearchProps>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<SearchProps>;
|
||||
|
||||
// To test the virtualizer, we can generate a list of modules
|
||||
function generateModules(count: number): Module[] {
|
||||
const greek = [
|
||||
"alpha",
|
||||
"beta",
|
||||
"gamma",
|
||||
"delta",
|
||||
"epsilon",
|
||||
"zeta",
|
||||
"eta",
|
||||
"theta",
|
||||
"iota",
|
||||
"kappa",
|
||||
"lambda",
|
||||
"mu",
|
||||
"nu",
|
||||
"xi",
|
||||
"omicron",
|
||||
"pi",
|
||||
"rho",
|
||||
"sigma",
|
||||
"tau",
|
||||
"upsilon",
|
||||
"phi",
|
||||
"chi",
|
||||
"psi",
|
||||
"omega",
|
||||
];
|
||||
|
||||
const modules: Module[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
modules.push({
|
||||
value: `lolcat/module-${i + 1}`,
|
||||
name: `Module ${i + 1}`,
|
||||
description: `${greek[i % greek.length]}#${i + 1}`,
|
||||
input: "lolcat",
|
||||
});
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// Test with lots of modules
|
||||
options: generateModules(1000),
|
||||
},
|
||||
render: (args: SearchProps) => {
|
||||
return (
|
||||
<div class="absolute bottom-1/3 w-3/4 px-3">
|
||||
<Search
|
||||
{...args}
|
||||
onChange={(module) => {
|
||||
// Go to the module configuration
|
||||
console.log("Selected module:", module);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
210
pkgs/clan-app/ui/src/components/Search/Search.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Button } from "../Button/Button";
|
||||
import styles from "./Search.module.css";
|
||||
import { Combobox } from "@kobalte/core/combobox";
|
||||
import { createMemo, createSignal, For } from "solid-js";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
import { createVirtualizer } from "@tanstack/solid-virtual";
|
||||
import { CollectionNode } from "@kobalte/core/*";
|
||||
|
||||
export interface Module {
|
||||
value: string;
|
||||
name: string;
|
||||
input: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SearchProps {
|
||||
onChange: (value: Module | null) => void;
|
||||
options: Module[];
|
||||
}
|
||||
export function Search(props: SearchProps) {
|
||||
// Controlled input value, to allow resetting the input itself
|
||||
const [value, setValue] = createSignal<Module | null>(null);
|
||||
const [inputValue, setInputValue] = createSignal<string>("");
|
||||
|
||||
let inputEl: HTMLInputElement;
|
||||
|
||||
let listboxRef: HTMLUListElement;
|
||||
|
||||
// const [currentItems, setItems] = createSignal(ALL_MODULES_OPTIONS_FLAT);
|
||||
const [comboboxItems, setComboboxItems] = createSignal<
|
||||
CollectionNode<Module>[]
|
||||
>(
|
||||
props.options.map((item) => ({
|
||||
rawValue: item,
|
||||
})) as CollectionNode<Module>[],
|
||||
);
|
||||
|
||||
// Create a reactive virtualizer that updates when items change
|
||||
const virtualizer = createMemo(() => {
|
||||
const items = comboboxItems();
|
||||
|
||||
const newVirtualizer = createVirtualizer({
|
||||
count: items.length,
|
||||
getScrollElement: () => listboxRef,
|
||||
getItemKey: (index) => {
|
||||
const item = items[index];
|
||||
return item?.rawValue?.value || `item-${index}`;
|
||||
},
|
||||
estimateSize: () => 42,
|
||||
gap: 6,
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
return newVirtualizer;
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox<Module>
|
||||
value={value()}
|
||||
onChange={(value) => {
|
||||
setValue(value);
|
||||
setInputValue(value ? value.name : "");
|
||||
props.onChange(value);
|
||||
}}
|
||||
class={styles.searchContainer}
|
||||
placement="bottom-start"
|
||||
options={props.options}
|
||||
optionValue="value"
|
||||
optionTextValue="name"
|
||||
optionLabel="name"
|
||||
placeholder="Search a service"
|
||||
sameWidth={true}
|
||||
open={true}
|
||||
gutter={7}
|
||||
modal={false}
|
||||
flip={false}
|
||||
virtualized={true}
|
||||
allowsEmptyCollection={true}
|
||||
closeOnSelection={false}
|
||||
triggerMode="manual"
|
||||
noResetInputOnBlur={true}
|
||||
>
|
||||
<Combobox.Control<Module> class={styles.searchHeader}>
|
||||
{(state) => (
|
||||
<div class={styles.inputContainer}>
|
||||
<Icon icon="Search" color="quaternary" />
|
||||
<Combobox.Input
|
||||
ref={(el) => {
|
||||
inputEl = el;
|
||||
}}
|
||||
class={styles.searchInput}
|
||||
placeholder={"Search a service"}
|
||||
value={inputValue()}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="reset"
|
||||
hierarchy="primary"
|
||||
size="s"
|
||||
ghost
|
||||
icon="CloseCircle"
|
||||
onClick={() => {
|
||||
state.clear();
|
||||
setInputValue("");
|
||||
|
||||
// Dispatch an input event to notify combobox listeners
|
||||
inputEl.dispatchEvent(
|
||||
new Event("input", { bubbles: true, cancelable: true }),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Control>
|
||||
<Combobox.Portal>
|
||||
<Combobox.Content class={styles.searchContent} tabIndex={-1}>
|
||||
<Combobox.Listbox<Module>
|
||||
ref={(el) => {
|
||||
listboxRef = el;
|
||||
}}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
overflow: "auto",
|
||||
"overflow-y": "auto",
|
||||
}}
|
||||
scrollToItem={(key) => {
|
||||
const idx = comboboxItems().findIndex(
|
||||
(option) => option.rawValue.value === key,
|
||||
);
|
||||
virtualizer().scrollToIndex(idx);
|
||||
}}
|
||||
>
|
||||
{(items) => {
|
||||
// Update the virtualizer with the filtered items
|
||||
const arr = Array.from(items());
|
||||
setComboboxItems(arr);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer().getTotalSize()}px`,
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<For each={virtualizer().getVirtualItems()}>
|
||||
{(virtualRow) => {
|
||||
const item: CollectionNode<Module> | undefined =
|
||||
items().getItem(virtualRow.key as string);
|
||||
|
||||
if (!item) {
|
||||
console.warn("Item not found for key:", virtualRow.key);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Combobox.Item
|
||||
item={item}
|
||||
class={styles.searchItem}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<div role="complementary">
|
||||
<Icon icon="Code" />
|
||||
</div>
|
||||
<div role="option">
|
||||
<Combobox.ItemLabel class="flex">
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="s"
|
||||
weight="medium"
|
||||
inverted
|
||||
>
|
||||
{item.rawValue.name}
|
||||
</Typography>
|
||||
</Combobox.ItemLabel>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xxs"
|
||||
weight="normal"
|
||||
color="quaternary"
|
||||
inverted
|
||||
class="flex justify-between"
|
||||
>
|
||||
<span>{item.rawValue.description}</span>
|
||||
<span>by {item.rawValue.input}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
</Combobox.Item>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Combobox.Listbox>
|
||||
</Combobox.Content>
|
||||
</Combobox.Portal>
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||
import { addClanURI, resetStore } from "@/src/stores/clan";
|
||||
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
|
||||
import { encodeBase64 } from "@/src/hooks/clan";
|
||||
import { ApiClientProvider } from "@/src/hooks/ApiClient";
|
||||
import {
|
||||
ApiCall,
|
||||
OperationArgs,
|
||||
OperationNames,
|
||||
OperationResponse,
|
||||
} from "@/src/hooks/api";
|
||||
|
||||
const defaultClanURI = "/home/brian/clans/my-clan";
|
||||
|
||||
@@ -24,10 +31,16 @@ const queryData = {
|
||||
europa: {
|
||||
name: "Europa",
|
||||
machineClass: "nixos",
|
||||
state: {
|
||||
status: "online",
|
||||
},
|
||||
},
|
||||
ganymede: {
|
||||
name: "Ganymede",
|
||||
machineClass: "nixos",
|
||||
state: {
|
||||
status: "out_of_sync",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -40,10 +53,16 @@ const queryData = {
|
||||
callisto: {
|
||||
name: "Callisto",
|
||||
machineClass: "nixos",
|
||||
state: {
|
||||
status: "not_installed",
|
||||
},
|
||||
},
|
||||
amalthea: {
|
||||
name: "Amalthea",
|
||||
machineClass: "nixos",
|
||||
state: {
|
||||
status: "offline",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -56,10 +75,16 @@ const queryData = {
|
||||
thebe: {
|
||||
name: "Thebe",
|
||||
machineClass: "nixos",
|
||||
state: {
|
||||
status: "online",
|
||||
},
|
||||
},
|
||||
sponde: {
|
||||
name: "Sponde",
|
||||
machineClass: "nixos",
|
||||
state: {
|
||||
status: "online",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,6 +148,18 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<RouteSectionProps>;
|
||||
|
||||
const mockFetcher = <K extends OperationNames>(
|
||||
method: K,
|
||||
args: OperationArgs<K>,
|
||||
) =>
|
||||
({
|
||||
uuid: "mock",
|
||||
result: Promise.reject<OperationResponse<K>>("not implemented"),
|
||||
cancel: async () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
}) satisfies ApiCall<K>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
decorators: [
|
||||
@@ -141,16 +178,28 @@ export const Default: Story = {
|
||||
["clans", encodeBase64(clanURI), "details"],
|
||||
clan.details,
|
||||
);
|
||||
|
||||
const machines = clan.machines || {};
|
||||
|
||||
queryClient.setQueryData(
|
||||
["clans", encodeBase64(clanURI), "machines"],
|
||||
clan.machines || {},
|
||||
machines,
|
||||
);
|
||||
|
||||
Object.entries(machines).forEach(([name, machine]) => {
|
||||
queryClient.setQueryData(
|
||||
["clans", encodeBase64(clanURI), "machine", name, "state"],
|
||||
machine.state,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
<ApiClientProvider client={{ fetch: mockFetcher }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
</ApiClientProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3,11 +3,12 @@ import { A } from "@solidjs/router";
|
||||
import { Accordion } from "@kobalte/core/accordion";
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { For } from "solid-js";
|
||||
import { For, useContext } from "solid-js";
|
||||
import { MachineStatus } from "@/src/components/MachineStatus/MachineStatus";
|
||||
import { buildMachinePath, useClanURI } from "@/src/hooks/clan";
|
||||
import { useMachinesQuery, useMachineStateQuery } from "@/src/hooks/queries";
|
||||
import { useMachineStateQuery } from "@/src/hooks/queries";
|
||||
import { SidebarProps } from "./Sidebar";
|
||||
import { ClanContext } from "@/src/routes/Clan/Clan";
|
||||
|
||||
interface MachineProps {
|
||||
clanURI: string;
|
||||
@@ -56,7 +57,11 @@ const MachineRoute = (props: MachineProps) => {
|
||||
|
||||
export const SidebarBody = (props: SidebarProps) => {
|
||||
const clanURI = useClanURI();
|
||||
const machineList = useMachinesQuery(clanURI);
|
||||
|
||||
const ctx = useContext(ClanContext);
|
||||
if (!ctx) {
|
||||
throw new Error("ClanContext not found");
|
||||
}
|
||||
|
||||
const sectionLabels = (props.staticSections || []).map(
|
||||
(section) => section.title,
|
||||
@@ -96,7 +101,7 @@ export const SidebarBody = (props: SidebarProps) => {
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="content">
|
||||
<nav>
|
||||
<For each={Object.entries(machineList.data || {})}>
|
||||
<For each={Object.entries(ctx.machinesQuery.data || {})}>
|
||||
{([id, machine]) => (
|
||||
<MachineRoute
|
||||
clanURI={clanURI}
|
||||
|
||||
@@ -39,7 +39,7 @@ div.sidebar-header {
|
||||
}
|
||||
|
||||
.sidebar-dropdown-content {
|
||||
@apply flex flex-col w-full px-2 py-1.5 z-10;
|
||||
@apply flex flex-col w-full px-2 py-1.5 z-10 gap-3;
|
||||
@apply bg-def-1 rounded-bl-md rounded-br-md;
|
||||
@apply border border-def-2;
|
||||
|
||||
@@ -58,6 +58,7 @@ div.sidebar-header {
|
||||
@apply px-1;
|
||||
|
||||
.dropdown-group-label {
|
||||
@apply flex items-baseline justify-between w-full;
|
||||
}
|
||||
|
||||
.dropdown-group-items {
|
||||
|
||||
@@ -3,10 +3,15 @@ import Icon from "@/src/components/Icon/Icon";
|
||||
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
import { createSignal, For, Suspense } from "solid-js";
|
||||
import { useClanListQuery } from "@/src/hooks/queries";
|
||||
import { navigateToClan, useClanURI } from "@/src/hooks/clan";
|
||||
import { clanURIs } from "@/src/stores/clan";
|
||||
import { createSignal, For, Suspense, useContext } from "solid-js";
|
||||
import {
|
||||
navigateToClan,
|
||||
navigateToOnboarding,
|
||||
useClanURI,
|
||||
} from "@/src/hooks/clan";
|
||||
import { setActiveClanURI } from "@/src/stores/clan";
|
||||
import { Button } from "../Button/Button";
|
||||
import { ClanContext } from "@/src/routes/Clan/Clan";
|
||||
|
||||
export const SidebarHeader = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -14,10 +19,23 @@ export const SidebarHeader = () => {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
|
||||
// get information about the current active clan
|
||||
const clanURI = useClanURI();
|
||||
const allClans = useClanListQuery(clanURIs());
|
||||
const ctx = useContext(ClanContext);
|
||||
|
||||
const activeClan = () => allClans.find(({ data }) => data?.uri === clanURI);
|
||||
if (!ctx) {
|
||||
throw new Error("SidebarContext not found");
|
||||
}
|
||||
|
||||
const clanURI = useClanURI();
|
||||
|
||||
const clanChar = () =>
|
||||
ctx?.activeClanQuery?.data?.name.charAt(0).toUpperCase();
|
||||
const clanName = () => ctx?.activeClanQuery?.data?.name;
|
||||
|
||||
const clanList = () =>
|
||||
ctx.allClansQueries
|
||||
.filter((it) => it.isSuccess)
|
||||
.map((it) => it.data!)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<div class="sidebar-header">
|
||||
@@ -32,7 +50,7 @@ export const SidebarHeader = () => {
|
||||
weight="bold"
|
||||
inverted={true}
|
||||
>
|
||||
{activeClan()?.data?.name.charAt(0).toUpperCase()}
|
||||
{clanChar()}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography
|
||||
@@ -41,7 +59,7 @@ export const SidebarHeader = () => {
|
||||
weight="bold"
|
||||
inverted={!open()}
|
||||
>
|
||||
{activeClan()?.data?.name}
|
||||
{clanName()}
|
||||
</Typography>
|
||||
</div>
|
||||
<DropdownMenu.Icon>
|
||||
@@ -71,26 +89,36 @@ export const SidebarHeader = () => {
|
||||
family="mono"
|
||||
size="xs"
|
||||
color="tertiary"
|
||||
transform="uppercase"
|
||||
>
|
||||
YOUR CLANS
|
||||
Your Clans
|
||||
</Typography>
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
ghost
|
||||
size="xs"
|
||||
startIcon="Plus"
|
||||
onClick={() => navigateToOnboarding(navigate, true)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</DropdownMenu.GroupLabel>
|
||||
<div class="dropdown-group-items">
|
||||
<For each={allClans}>
|
||||
<For each={clanList()}>
|
||||
{(clan) => (
|
||||
<Suspense fallback={"Loading..."}>
|
||||
<DropdownMenu.Item
|
||||
class="dropdown-item"
|
||||
onSelect={() =>
|
||||
navigateToClan(navigate, clan.data!.uri)
|
||||
}
|
||||
onSelect={() => {
|
||||
setActiveClanURI(clan.uri);
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
>
|
||||
{clan.data?.name}
|
||||
{clan.name}
|
||||
</Typography>
|
||||
</DropdownMenu.Item>
|
||||
</Suspense>
|
||||
|
||||
@@ -5,7 +5,7 @@ div.sidebar-section {
|
||||
@apply flex items-center justify-between px-1.5;
|
||||
|
||||
& > div.controls {
|
||||
@apply flex justify-end gap-2;
|
||||
@apply flex items-center justify-end;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
import { OperationNames, SuccessData } from "@/src/hooks/api";
|
||||
import { GenericSchema, GenericSchemaAsync } from "valibot";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button as KButton } from "@kobalte/core/button";
|
||||
import Icon from "@/src/components/Icon/Icon";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
|
||||
import "./SidebarSection.css";
|
||||
import { Loader } from "../../components/Loader/Loader";
|
||||
@@ -83,24 +82,24 @@ export function SidebarSectionForm<
|
||||
</Typography>
|
||||
<div class="controls h-4">
|
||||
{editing() && !formStore.submitting && (
|
||||
<KButton type="submit">
|
||||
<Icon
|
||||
icon="Checkmark"
|
||||
color="tertiary"
|
||||
size="0.75rem"
|
||||
inverted
|
||||
/>
|
||||
</KButton>
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
size="xs"
|
||||
startIcon="Checkmark"
|
||||
ghost
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
{editing() && formStore.submitting && <Loader />}
|
||||
<KButton onClick={editOrClose}>
|
||||
<Icon
|
||||
icon={editing() ? "Close" : "Edit"}
|
||||
color="tertiary"
|
||||
size="0.75rem"
|
||||
inverted
|
||||
/>
|
||||
</KButton>
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
ghost
|
||||
size="xs"
|
||||
icon={editing() ? "Close" : "Edit"}
|
||||
onClick={editOrClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Toolbar, ToolbarProps } from "@/src/components/Toolbar/Toolbar";
|
||||
import { Divider } from "@/src/components/Divider/Divider";
|
||||
import { ToolbarButton } from "./ToolbarButton";
|
||||
import { Tooltip } from "../Tooltip/Tooltip";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
|
||||
const meta: Meta<ToolbarProps> = {
|
||||
title: "Components/Toolbar",
|
||||
@@ -18,11 +16,24 @@ export const Default: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<ToolbarButton name="select" icon="Cursor" />
|
||||
<ToolbarButton name="new-machine" icon="NewMachine" />
|
||||
<ToolbarButton
|
||||
name="select"
|
||||
icon="Cursor"
|
||||
description="Select my thing"
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="new-machine"
|
||||
icon="NewMachine"
|
||||
description="Select this thing"
|
||||
/>
|
||||
<Divider orientation="vertical" />
|
||||
<ToolbarButton name="modules" icon="Modules" selected={true} />
|
||||
<ToolbarButton name="ai" icon="AI" />
|
||||
<ToolbarButton
|
||||
name="modules"
|
||||
icon="Modules"
|
||||
selected={true}
|
||||
description="Add service"
|
||||
/>
|
||||
<ToolbarButton name="ai" icon="AI" description="Call your AI Manager" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -40,49 +51,22 @@ export const WithTooltip: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<Tooltip
|
||||
trigger={<ToolbarButton name="select" icon="Cursor" />}
|
||||
placement="top"
|
||||
>
|
||||
<div class="mb-1 p-1 text-fg-inv-1">
|
||||
<Typography hierarchy="label" size="s" color="inherit">
|
||||
Select an object
|
||||
</Typography>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Divider orientation="vertical" />
|
||||
<Tooltip
|
||||
trigger={<ToolbarButton name="new-machine" icon="NewMachine" />}
|
||||
placement="top"
|
||||
>
|
||||
<div class="mb-1 p-1 text-fg-inv-1">
|
||||
<Typography hierarchy="label" size="s" color="inherit">
|
||||
Create a new machine
|
||||
</Typography>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
trigger={
|
||||
<ToolbarButton name="modules" icon="Modules" selected={true} />
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<div class="mb-1 p-1 text-fg-inv-1">
|
||||
<Typography hierarchy="label" size="s" color="inherit">
|
||||
Manage Services
|
||||
</Typography>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
trigger={<ToolbarButton name="ai" icon="AI" />}
|
||||
placement="top"
|
||||
>
|
||||
<div class="mb-1 p-1 text-fg-inv-1">
|
||||
<Typography hierarchy="label" size="s" color="inherit">
|
||||
Chat with AI
|
||||
</Typography>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<ToolbarButton name="select" icon="Cursor" description="Select" />
|
||||
|
||||
<ToolbarButton
|
||||
name="new-machine"
|
||||
icon="NewMachine"
|
||||
description="Select"
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
name="modules"
|
||||
icon="Modules"
|
||||
selected={true}
|
||||
description="Select"
|
||||
/>
|
||||
|
||||
<ToolbarButton name="ai" icon="AI" description="Select" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -3,22 +3,26 @@ import cx from "classnames";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
|
||||
import type { JSX } from "solid-js";
|
||||
import { Tooltip } from "../Tooltip/Tooltip";
|
||||
|
||||
export interface ToolbarButtonProps
|
||||
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon: IconVariant;
|
||||
description: JSX.Element;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export const ToolbarButton = (props: ToolbarButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
class={cx(styles.toolbar_button, {
|
||||
[styles["selected"]]: props.selected,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Icon icon={props.icon} inverted={!props.selected} />
|
||||
</Button>
|
||||
<Tooltip description={props.description} gutter={10} placement="top">
|
||||
<Button
|
||||
class={cx(styles.toolbar_button, {
|
||||
[styles["selected"]]: props.selected,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Icon icon={props.icon} inverted={!props.selected} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
div.tooltip-content {
|
||||
.tooltipContent {
|
||||
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
|
||||
|
||||
max-width: min(calc(100vw - 16px), 380px);
|
||||
@@ -1,31 +1,40 @@
|
||||
import "./Tooltip.css";
|
||||
import {
|
||||
Tooltip as KTooltip,
|
||||
TooltipRootProps as KTooltipRootProps,
|
||||
} from "@kobalte/core/tooltip";
|
||||
import cx from "classnames";
|
||||
import { JSX } from "solid-js";
|
||||
import styles from "./Tooltip.module.css";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
|
||||
export interface TooltipProps extends KTooltipRootProps {
|
||||
inverted?: boolean;
|
||||
trigger: JSX.Element;
|
||||
children: JSX.Element;
|
||||
description: JSX.Element;
|
||||
animation?: "bounce";
|
||||
}
|
||||
|
||||
export const Tooltip = (props: TooltipProps) => {
|
||||
return (
|
||||
<KTooltip {...props}>
|
||||
<KTooltip.Trigger>{props.trigger}</KTooltip.Trigger>
|
||||
<KTooltip.Trigger>{props.children}</KTooltip.Trigger>
|
||||
<KTooltip.Portal>
|
||||
<KTooltip.Content
|
||||
class={cx("tooltip-content", {
|
||||
inverted: props.inverted,
|
||||
class={cx(styles.tooltipContent, {
|
||||
[styles.inverted]: props.inverted,
|
||||
"animate-bounce": props.animation == "bounce",
|
||||
})}
|
||||
>
|
||||
{props.placement == "bottom" && <KTooltip.Arrow />}
|
||||
{props.children}
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="s"
|
||||
weight="medium"
|
||||
color="primary"
|
||||
inverted={!props.inverted}
|
||||
>
|
||||
{props.description}
|
||||
</Typography>
|
||||
{props.placement == "top" && <KTooltip.Arrow />}
|
||||
</KTooltip.Content>
|
||||
</KTooltip.Portal>
|
||||
|
||||
@@ -36,6 +36,9 @@ export const navigateToClan = (navigate: Navigator, clanURI: string) => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
export const navigateToOnboarding = (navigate: Navigator, addClan: boolean) =>
|
||||
navigate(`/${addClan ? "?addClan=true" : ""}`);
|
||||
|
||||
export const navigateToMachine = (
|
||||
navigate: Navigator,
|
||||
clanURI: string,
|
||||
|
||||