Compare commits
264 Commits
remove-mod
...
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 | ||
|
|
de0b1b2d70 | ||
|
|
6996a6340a | ||
|
|
3c433da8f5 | ||
|
|
ef2a2bdb67 | ||
|
|
7b61a668e9 | ||
|
|
bdab3e23af | ||
|
|
2b068928a2 | ||
|
|
ec798f89fd | ||
|
|
9efee40477 | ||
|
|
448c22c280 | ||
|
|
6c6e30ae60 | ||
|
|
b27ff67a14 | ||
|
|
c0ffb17e00 | ||
|
|
e9ccf157b6 | ||
|
|
451f2427fe | ||
|
|
1676cdd9a4 | ||
|
|
109e6473ab | ||
|
|
55acff50d0 | ||
|
|
eee1bd1ae0 | ||
|
|
e46d5870ff | ||
|
|
f6ec32a5d1 | ||
|
|
e336d1b19c | ||
|
|
7399f59652 | ||
|
|
088abe396e | ||
|
|
26b31e24a3 | ||
|
|
099f4c2b8b | ||
|
|
b43605c168 | ||
|
|
899dba5a08 | ||
|
|
d2b94ced5a | ||
|
|
cdf9fa1753 | ||
|
|
d1e7e2993d | ||
|
|
e05d85c759 | ||
|
|
53873411a6 | ||
|
|
39e0ab21bd | ||
|
|
8269d869c3 | ||
|
|
e19d1c8122 | ||
|
|
0cd4ff1b12 | ||
|
|
9aebf02f05 | ||
|
|
ffb7b91da7 | ||
|
|
2d264a8e5e | ||
|
|
abf6893714 | ||
|
|
699c56c721 | ||
|
|
2ce5388a75 | ||
|
|
3e664255d6 | ||
|
|
5b1a9d6848 | ||
|
|
1850abdd0d | ||
|
|
ed503f64da | ||
|
|
4074a184b2 | ||
|
|
6fe2b06f09 | ||
|
|
8fe7cb1b3d | ||
|
|
815c6c9438 | ||
|
|
9ce563aa08 | ||
|
|
c25844dd07 | ||
|
|
a167e70e63 | ||
|
|
dd96fe6b73 | ||
|
|
40d35d37e2 | ||
|
|
071f0f8034 | ||
|
|
81d88fe253 | ||
|
|
ab274ce932 | ||
|
|
ba1e598a76 | ||
|
|
b5d29bd301 | ||
|
|
e174e8e029 | ||
|
|
453d2b4a0a | ||
|
|
aadc8a1d63 | ||
|
|
aaca8f4763 | ||
|
|
0a1a63dfdd | ||
|
|
ee87f20471 | ||
|
|
43febe5f33 | ||
|
|
c63bbabceb | ||
|
|
8f1b270b59 | ||
|
|
da0af8bd53 | ||
|
|
f82d18d649 | ||
|
|
287a303484 | ||
|
|
1213608f30 | ||
|
|
fa1693e8c0 | ||
|
|
ed3ed7cb2a | ||
|
|
b2e88fb3fa | ||
|
|
d6ca50218a | ||
|
|
7d1f0956d6 | ||
|
|
d150c80854 | ||
|
|
2d1828d088 | ||
|
|
f7f897a311 | ||
|
|
683ffbdc76 | ||
|
|
480ad3a5f1 | ||
|
|
16361f03e9 |
@@ -1,9 +0,0 @@
|
|||||||
name: checks
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
jobs:
|
|
||||||
checks-impure:
|
|
||||||
runs-on: nix
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- run: nix run .#impure-checks
|
|
||||||
20
CODEOWNERS
@@ -0,0 +1,20 @@
|
|||||||
|
clanServices/.* @pinpox @kenji
|
||||||
|
|
||||||
|
lib/test/container-test-driver/.* @DavHau @mic92
|
||||||
|
lib/modules/inventory/.* @hsjobeki
|
||||||
|
lib/modules/inventoryClass/.* @hsjobeki
|
||||||
|
|
||||||
|
pkgs/clan-app/ui/.* @hsjobeki @brianmcgee
|
||||||
|
pkgs/clan-app/clan_app/.* @qubasa @hsjobeki
|
||||||
|
|
||||||
|
pkgs/clan-cli/clan_cli/.* @lassulus @mic92 @kenji
|
||||||
|
pkgs/clan-cli/clan_cli/(secrets|vars)/.* @DavHau @lassulus
|
||||||
|
|
||||||
|
pkgs/clan-cli/clan_lib/log_machines/.* @Qubasa
|
||||||
|
pkgs/clan-cli/clan_lib/ssh/.* @Qubasa @Mic92 @lassulus
|
||||||
|
pkgs/clan-cli/clan_lib/tags/.* @hsjobeki
|
||||||
|
pkgs/clan-cli/clan_lib/persist/.* @hsjobeki
|
||||||
|
pkgs/clan-cli/clan_lib/flake/.* @lassulus
|
||||||
|
|
||||||
|
pkgs/clan-cli/api.py @hsjobeki
|
||||||
|
pkgs/clan-cli/openapi.py @hsjobeki
|
||||||
@@ -8,7 +8,7 @@ Our mission is simple: to democratize computing by providing tools that empower
|
|||||||
|
|
||||||
## Features of Clan
|
## 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.
|
- **Overlay Networks:** Secure, private communication channels between devices.
|
||||||
- **Virtual Machine Integration:** Seamless operation of VM applications within the main operating system.
|
- **Virtual Machine Integration:** Seamless operation of VM applications within the main operating system.
|
||||||
- **Robust Backup Management:** Long-term, self-hosted data preservation.
|
- **Robust Backup Management:** Long-term, self-hosted data preservation.
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ in
|
|||||||
++ filter pathExists [
|
++ filter pathExists [
|
||||||
./devshell/flake-module.nix
|
./devshell/flake-module.nix
|
||||||
./flash/flake-module.nix
|
./flash/flake-module.nix
|
||||||
./impure/flake-module.nix
|
|
||||||
./installation/flake-module.nix
|
./installation/flake-module.nix
|
||||||
./update/flake-module.nix
|
./update/flake-module.nix
|
||||||
./morph/flake-module.nix
|
./morph/flake-module.nix
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
perSystem =
|
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
self',
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
# a script that executes all other checks
|
|
||||||
packages.impure-checks = pkgs.writeShellScriptBin "impure-checks" ''
|
|
||||||
#!${pkgs.bash}/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
unset CLAN_DIR
|
|
||||||
|
|
||||||
export PATH="${
|
|
||||||
lib.makeBinPath (
|
|
||||||
[
|
|
||||||
pkgs.gitMinimal
|
|
||||||
pkgs.nix
|
|
||||||
pkgs.coreutils
|
|
||||||
pkgs.rsync # needed to have rsync installed on the dummy ssh server
|
|
||||||
]
|
|
||||||
++ self'.packages.clan-cli-full.runtimeDependencies
|
|
||||||
)
|
|
||||||
}"
|
|
||||||
ROOT=$(git rev-parse --show-toplevel)
|
|
||||||
cd "$ROOT/pkgs/clan-cli"
|
|
||||||
|
|
||||||
# Set up custom git configuration for tests
|
|
||||||
export GIT_CONFIG_GLOBAL=$(mktemp)
|
|
||||||
git config --file "$GIT_CONFIG_GLOBAL" user.name "Test User"
|
|
||||||
git config --file "$GIT_CONFIG_GLOBAL" user.email "test@example.com"
|
|
||||||
export GIT_CONFIG_SYSTEM=/dev/null
|
|
||||||
|
|
||||||
# this disables dynamic dependency loading in clan-cli
|
|
||||||
export CLAN_NO_DYNAMIC_DEPS=1
|
|
||||||
|
|
||||||
jobs=$(nproc)
|
|
||||||
# Spawning worker in pytest is relatively slow, so we limit the number of jobs to 13
|
|
||||||
# (current number of impure tests)
|
|
||||||
jobs="$((jobs > 6 ? 6 : jobs))"
|
|
||||||
|
|
||||||
nix develop "$ROOT#clan-cli" -c bash -c "TMPDIR=/tmp python -m pytest -n $jobs -m impure ./clan_cli $@"
|
|
||||||
|
|
||||||
# Clean up temporary git config
|
|
||||||
rm -f "$GIT_CONFIG_GLOBAL"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -232,6 +232,7 @@
|
|||||||
"-i", ssh_conn.ssh_key,
|
"-i", ssh_conn.ssh_key,
|
||||||
"--option", "store", os.environ['CLAN_TEST_STORE'],
|
"--option", "store", os.environ['CLAN_TEST_STORE'],
|
||||||
"--update-hardware-config", "nixos-facter",
|
"--update-hardware-config", "nixos-facter",
|
||||||
|
"--no-persist-state",
|
||||||
]
|
]
|
||||||
|
|
||||||
subprocess.run(clan_cmd, check=True)
|
subprocess.run(clan_cmd, check=True)
|
||||||
@@ -275,7 +276,7 @@
|
|||||||
"${self.checks.x86_64-linux.clan-core-for-checks}",
|
"${self.checks.x86_64-linux.clan-core-for-checks}",
|
||||||
"${closureInfo}"
|
"${closureInfo}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set up SSH connection
|
# Set up SSH connection
|
||||||
ssh_conn = setup_ssh_connection(
|
ssh_conn = setup_ssh_connection(
|
||||||
target,
|
target,
|
||||||
|
|||||||
@@ -10,22 +10,34 @@
|
|||||||
lib,
|
lib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
jsonpath = "/tmp/telegraf.json";
|
||||||
|
auth_user = "prometheus";
|
||||||
|
in
|
||||||
{
|
{
|
||||||
|
|
||||||
networking.firewall.interfaces = lib.mkIf (settings.allowAllInterfaces == false) (
|
networking.firewall.interfaces = lib.mkIf (settings.allowAllInterfaces == false) (
|
||||||
builtins.listToAttrs (
|
builtins.listToAttrs (
|
||||||
map (name: {
|
map (name: {
|
||||||
inherit name;
|
inherit name;
|
||||||
value.allowedTCPPorts = [ 9273 ];
|
value.allowedTCPPorts = [
|
||||||
|
9273
|
||||||
|
9990
|
||||||
|
];
|
||||||
}) settings.interfaces
|
}) settings.interfaces
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = lib.mkIf (settings.allowAllInterfaces == true) [ 9273 ];
|
networking.firewall.allowedTCPPorts = lib.mkIf (settings.allowAllInterfaces == true) [
|
||||||
|
9273
|
||||||
|
9990
|
||||||
|
];
|
||||||
|
|
||||||
clan.core.vars.generators."telegraf-password" = {
|
clan.core.vars.generators."telegraf" = {
|
||||||
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 = [
|
runtimeInputs = [
|
||||||
pkgs.coreutils
|
pkgs.coreutils
|
||||||
@@ -35,16 +47,22 @@
|
|||||||
|
|
||||||
script = ''
|
script = ''
|
||||||
PASSWORD=$(xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n")
|
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 = {
|
services.telegraf = {
|
||||||
enable = true;
|
enable = true;
|
||||||
environmentFiles = [
|
environmentFiles = [
|
||||||
(builtins.toString
|
(builtins.toString config.clan.core.vars.generators.telegraf.files.password-env.path)
|
||||||
config.clan.core.vars.generators."telegraf-password".files.telegraf-password.path
|
|
||||||
)
|
|
||||||
];
|
];
|
||||||
extraConfig = {
|
extraConfig = {
|
||||||
agent.interval = "60s";
|
agent.interval = "60s";
|
||||||
@@ -59,25 +77,35 @@
|
|||||||
|
|
||||||
exec =
|
exec =
|
||||||
let
|
let
|
||||||
currentSystemScript = pkgs.writeShellScript "current-system" ''
|
nixosSystems = pkgs.writeShellScript "current-system" ''
|
||||||
printf "current_system,path=%s present=0\n" $(readlink /run/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
|
in
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
# Expose the path to current-system as metric. We use
|
# Expose the path to current-system as metric. We use
|
||||||
# this to check if the machine is up-to-date.
|
# this to check if the machine is up-to-date.
|
||||||
commands = [ currentSystemScript ];
|
commands = [ nixosSystems ];
|
||||||
data_format = "influx";
|
data_format = "influx";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
# sadly there doesn'T seem to exist a telegraf http_client output plugin
|
||||||
outputs.prometheus_client = {
|
outputs.prometheus_client = {
|
||||||
listen = ":9273";
|
listen = ":9273";
|
||||||
metric_version = 2;
|
metric_version = 2;
|
||||||
basic_username = "prometheus";
|
basic_username = "${auth_user}";
|
||||||
basic_password = "$${BASIC_AUTH_PWD}";
|
basic_password = "$${BASIC_AUTH_PWD}";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
outputs.file = {
|
||||||
|
files = [ jsonpath ];
|
||||||
|
data_format = "json";
|
||||||
|
json_timestamp_units = "1s";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
|
# Deploy user bob only on his laptop. Prompt for a password.
|
||||||
user-bob = {
|
user-bob = {
|
||||||
module = {
|
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.
|
- "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}" = {
|
clan.core.vars.generators."user-password-${settings.user}" = {
|
||||||
|
|
||||||
files.user-password-hash.neededFor = "users";
|
files.user-password-hash.neededFor = "users";
|
||||||
files.user-password-hash.restartUnits = lib.optional (config.services.userborn.enable) "userborn.service";
|
files.user-password-hash.restartUnits = lib.optional (config.services.userborn.enable) "userborn.service";
|
||||||
files.user-password.deploy = false;
|
files.user-password.deploy = false;
|
||||||
@@ -107,6 +117,8 @@
|
|||||||
pkgs.mkpasswd
|
pkgs.mkpasswd
|
||||||
];
|
];
|
||||||
|
|
||||||
|
share = settings.share;
|
||||||
|
|
||||||
script =
|
script =
|
||||||
(
|
(
|
||||||
if settings.prompt then
|
if settings.prompt then
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""IPv6 address allocator for WireGuard networks.
|
||||||
IPv6 address allocator for WireGuard networks.
|
|
||||||
|
|
||||||
Network layout:
|
Network layout:
|
||||||
- Base network: /40 ULA prefix (fd00::/8 + 32 bits from hash)
|
- 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:
|
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
|
Format: fd{32-bit hash}/40
|
||||||
This gives us fd00:0000:0000::/40 through fdff:ffff:ff00::/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(
|
def generate_controller_subnet(
|
||||||
base_network: ipaddress.IPv6Network, controller_name: str
|
base_network: ipaddress.IPv6Network,
|
||||||
|
controller_name: str,
|
||||||
) -> ipaddress.IPv6Network:
|
) -> 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.
|
We have 16 bits (40 to 56) to allocate controller subnets.
|
||||||
This allows for 65,536 possible 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:
|
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.
|
This suffix will be used in all controller subnets to create unique addresses.
|
||||||
Format: :xxxx:xxxx:xxxx:xxxx (64 bits)
|
Format: :xxxx:xxxx:xxxx:xxxx (64 bits)
|
||||||
@@ -86,7 +83,7 @@ def generate_peer_suffix(peer_name: str) -> str:
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
if len(sys.argv) < 4:
|
if len(sys.argv) < 4:
|
||||||
print(
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|||||||
24
devFlake/flake.lock
generated
@@ -3,10 +3,10 @@
|
|||||||
"clan-core-for-checks": {
|
"clan-core-for-checks": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755093452,
|
"lastModified": 1756081310,
|
||||||
"narHash": "sha256-NKBss7QtNnOqYVyJmYCgaCvYZK0mpQTQc9fLgE1mGyk=",
|
"narHash": "sha256-wj1H5Pr6w4AsB+nG3K07SgSIDZ7jDCkGnh5XXWLdtk8=",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"rev": "7e97734797f0c6bd3c2d3a51cf54a2a6b371c222",
|
"rev": "7b926d43dc361cd8d3ad3c14a2e7e75375b7d215",
|
||||||
"shallow": true,
|
"shallow": true,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.clan.lol/clan/clan-core"
|
"url": "https://git.clan.lol/clan/clan-core"
|
||||||
@@ -84,11 +84,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-dev": {
|
"nixpkgs-dev": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755375481,
|
"lastModified": 1756050191,
|
||||||
"narHash": "sha256-43PgCQFgFD1nM/7dncytV0c5heNHe/gXrEud18ZWcZU=",
|
"narHash": "sha256-lMtTT4rv5On7D0P4Z+k7UkvbAKKuVGRbJi/VJeRCQwI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "35f1742e4f1470817ff8203185e2ce0359947f12",
|
"rev": "759dcc6981cd4aa222d36069f78fe7064d563305",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -107,11 +107,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1754869408,
|
"lastModified": 1755555503,
|
||||||
"narHash": "sha256-G1zNuxiCDfqNQVoL9j5v+ZYfUER7AI158ev98/JC8LI=",
|
"narHash": "sha256-WiOO7GUOsJ4/DoMy2IC5InnqRDSo2U11la48vCCIjjY=",
|
||||||
"owner": "NuschtOS",
|
"owner": "NuschtOS",
|
||||||
"repo": "search",
|
"repo": "search",
|
||||||
"rev": "2f5478267557a0f7a70d953b6c0867a5b4282739",
|
"rev": "6f3efef888b92e6520f10eae15b86ff537e1d2ea",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -165,11 +165,11 @@
|
|||||||
"nixpkgs": []
|
"nixpkgs": []
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1754847726,
|
"lastModified": 1755934250,
|
||||||
"narHash": "sha256-2vX8QjO5lRsDbNYvN9hVHXLU6oMl+V/PsmIiJREG4rE=",
|
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "7d81f6fb2e19bf84f1c65135d1060d829fae2408",
|
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
2
docs/.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
/site/reference
|
/site/reference
|
||||||
/site/static
|
/site/static
|
||||||
/site/options-page
|
/site/options
|
||||||
/site/openapi.json
|
/site/openapi.json
|
||||||
!/site/static/extra.css
|
!/site/static/extra.css
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ edit_uri: _edit/main/docs/docs/
|
|||||||
|
|
||||||
validation:
|
validation:
|
||||||
omitted_files: warn
|
omitted_files: warn
|
||||||
absolute_links: warn
|
absolute_links: ignore
|
||||||
unrecognized_links: warn
|
unrecognized_links: warn
|
||||||
|
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
@@ -64,7 +64,7 @@ nav:
|
|||||||
- Disk Encryption: guides/disk-encryption.md
|
- Disk Encryption: guides/disk-encryption.md
|
||||||
- Age Plugins: guides/age-plugins.md
|
- Age Plugins: guides/age-plugins.md
|
||||||
- Secrets management: guides/secrets.md
|
- Secrets management: guides/secrets.md
|
||||||
- Target Host: guides/target-host.md
|
- Networking: guides/networking.md
|
||||||
- Zerotier VPN: guides/mesh-vpn.md
|
- Zerotier VPN: guides/mesh-vpn.md
|
||||||
- Secure Boot: guides/secure-boot.md
|
- Secure Boot: guides/secure-boot.md
|
||||||
- Flake-parts: guides/flake-parts.md
|
- Flake-parts: guides/flake-parts.md
|
||||||
@@ -78,7 +78,7 @@ nav:
|
|||||||
- Writing a Disko Template: guides/disko-templates/community.md
|
- Writing a Disko Template: guides/disko-templates/community.md
|
||||||
- Migrations:
|
- Migrations:
|
||||||
- Migrate existing Flakes: guides/migrations/migration-guide.md
|
- 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
|
- Facts Vars Migration: guides/migrations/migration-facts-vars.md
|
||||||
- Disk id: guides/migrations/disk-id.md
|
- Disk id: guides/migrations/disk-id.md
|
||||||
- Concepts:
|
- Concepts:
|
||||||
@@ -88,7 +88,7 @@ nav:
|
|||||||
- Templates: concepts/templates.md
|
- Templates: concepts/templates.md
|
||||||
- Reference:
|
- Reference:
|
||||||
- Overview: reference/index.md
|
- Overview: reference/index.md
|
||||||
- Clan Options: options.md
|
- Browse Options: "/options"
|
||||||
- Services:
|
- Services:
|
||||||
- Overview:
|
- Overview:
|
||||||
- reference/clanServices/index.md
|
- reference/clanServices/index.md
|
||||||
@@ -155,6 +155,7 @@ nav:
|
|||||||
- 05-deployment-parameters: decisions/05-deployment-parameters.md
|
- 05-deployment-parameters: decisions/05-deployment-parameters.md
|
||||||
- Template: decisions/_template.md
|
- Template: decisions/_template.md
|
||||||
- Glossary: reference/glossary.md
|
- Glossary: reference/glossary.md
|
||||||
|
- Browse Options: "/options"
|
||||||
|
|
||||||
docs_dir: site
|
docs_dir: site
|
||||||
site_dir: out
|
site_dir: out
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ pkgs.stdenv.mkDerivation {
|
|||||||
chmod -R +w ./site/reference
|
chmod -R +w ./site/reference
|
||||||
echo "Generated API documentation in './site/reference/' "
|
echo "Generated API documentation in './site/reference/' "
|
||||||
|
|
||||||
rm -r ./site/options-page || true
|
rm -rf ./site/options
|
||||||
cp -r ${docs-options} ./site/options-page
|
cp -r ${docs-options} ./site/options
|
||||||
chmod -R +w ./site/options-page
|
chmod -R +w ./site/options
|
||||||
|
|
||||||
mkdir -p ./site/static/asciinema-player
|
mkdir -p ./site/static/asciinema-player
|
||||||
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
|
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
serviceModules = self.clan.modules;
|
serviceModules = self.clan.modules;
|
||||||
|
|
||||||
baseHref = "/options-page/";
|
baseHref = "/options/";
|
||||||
|
|
||||||
getRoles =
|
getRoles =
|
||||||
module:
|
module:
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
nestedSettingsOption = mkOption {
|
nestedSettingsOption = mkOption {
|
||||||
type = types.raw;
|
type = types.raw;
|
||||||
description = ''
|
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 {
|
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
|
in
|
||||||
{
|
{
|
||||||
# Uncomment for debugging
|
# Uncomment for debugging
|
||||||
@@ -175,10 +211,17 @@
|
|||||||
# scopes = mapAttrsToList mkScope serviceModules;
|
# scopes = mapAttrsToList mkScope serviceModules;
|
||||||
scopes = [
|
scopes = [
|
||||||
{
|
{
|
||||||
name = "Clan";
|
inherit baseHref;
|
||||||
|
name = "Flake Options (clan.nix file)";
|
||||||
modules = docModules;
|
modules = docModules;
|
||||||
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
|
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.errors import ClanError
|
||||||
from clan_lib.services.modules import (
|
from clan_lib.services.modules import (
|
||||||
CategoryInfo,
|
CategoryInfo,
|
||||||
Frontmatter,
|
ModuleManifest,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get environment variables
|
# 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:
|
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:
|
Args:
|
||||||
lines (list of str): The lines of text to join.
|
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:
|
Returns:
|
||||||
str: The indented and concatenated string.
|
str: The indented and concatenated string.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Create the indentation string (e.g., four spaces)
|
# Create the indentation string (e.g., four spaces)
|
||||||
indent_str = " " * indent
|
indent_str = " " * indent
|
||||||
@@ -161,7 +161,10 @@ def render_option(
|
|||||||
|
|
||||||
|
|
||||||
def print_options(
|
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:
|
) -> str:
|
||||||
res = ""
|
res = ""
|
||||||
with (Path(options_file) / "share/doc/nixos/options.json").open() as f:
|
with (Path(options_file) / "share/doc/nixos/options.json").open() as f:
|
||||||
@@ -176,9 +179,8 @@ def print_options(
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def module_header(module_name: str, has_inventory_feature: bool = False) -> str:
|
def module_header(module_name: str) -> str:
|
||||||
indicator = " 🔹" if has_inventory_feature else ""
|
return f"# {module_name}\n\n"
|
||||||
return f"# {module_name}{indicator}\n\n"
|
|
||||||
|
|
||||||
|
|
||||||
clan_core_descr = """
|
clan_core_descr = """
|
||||||
@@ -236,7 +238,7 @@ def produce_clan_core_docs() -> None:
|
|||||||
for submodule_name, split_options in split.items():
|
for submodule_name, split_options in split.items():
|
||||||
outfile = f"{module_name}/{submodule_name}.md"
|
outfile = f"{module_name}/{submodule_name}.md"
|
||||||
print(
|
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
|
init_level = 1
|
||||||
root = options_to_tree(split_options, debug=True)
|
root = options_to_tree(split_options, debug=True)
|
||||||
@@ -271,56 +273,9 @@ def produce_clan_core_docs() -> None:
|
|||||||
of.write(output)
|
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(
|
def render_categories(
|
||||||
categories: list[str], categories_info: dict[str, CategoryInfo]
|
categories: list[str],
|
||||||
|
categories_info: dict[str, CategoryInfo],
|
||||||
) -> str:
|
) -> str:
|
||||||
res = """<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">"""
|
res = """<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">"""
|
||||||
for cat in categories:
|
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"`clan.modules.{module_name}`\n"
|
||||||
output += f"*{module_info['manifest']['description']}*\n"
|
output += f"*{module_info['manifest']['description']}*\n"
|
||||||
|
|
||||||
fm = Frontmatter("")
|
|
||||||
# output += "## Categories\n\n"
|
# output += "## Categories\n\n"
|
||||||
output += render_categories(
|
output += render_categories(
|
||||||
module_info["manifest"]["categories"], fm.categories_info
|
module_info["manifest"]["categories"],
|
||||||
|
ModuleManifest.categories_info(),
|
||||||
)
|
)
|
||||||
|
|
||||||
output += f"{module_info['manifest']['readme']}\n"
|
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"
|
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"
|
output += f"- {role_name}\n"
|
||||||
|
|
||||||
for role_name, role_filename in module_info["roles"].items():
|
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)
|
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]]:
|
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": { Data }
|
||||||
"a.b": { 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:
|
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
|
# Helper function to create nested structure
|
||||||
def add_to_tree(path_parts: list[str], info: Any, current_node: Option) -> None:
|
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(
|
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:
|
) -> str:
|
||||||
"""
|
"""Eender the options from the tree structure.
|
||||||
eender the options from the tree structure.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
root (Option): The root option node.
|
root (Option): The root option node.
|
||||||
init_level (int): The initial level of indentation.
|
init_level (int): The initial level of indentation.
|
||||||
prefix (list str): Will be printed as common prefix of all attribute names.
|
prefix (list str): Will be printed as common prefix of all attribute names.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def render_tree(option: Option, level: int = init_level) -> str:
|
def render_tree(option: Option, level: int = init_level) -> str:
|
||||||
output = ""
|
output = ""
|
||||||
|
|
||||||
should_render = not option.name.startswith("<") and not option.name.startswith(
|
should_render = not option.name.startswith("<") and not option.name.startswith(
|
||||||
"_"
|
"_",
|
||||||
)
|
)
|
||||||
if should_render:
|
if should_render:
|
||||||
# short_name = option_short_name(option.name)
|
# short_name = option_short_name(option.name)
|
||||||
@@ -623,7 +551,7 @@ def options_docs_from_tree(
|
|||||||
return md
|
return md
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": #
|
if __name__ == "__main__":
|
||||||
produce_clan_core_docs()
|
produce_clan_core_docs()
|
||||||
|
|
||||||
produce_clan_service_author_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}/configuration.nix`
|
||||||
- [x] `machines/{machineName}/hardware-configuration.nix`
|
Main configuration file for the machine.
|
||||||
- [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}/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`
|
# 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.
|
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
|
## 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)
|
🔗 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)
|
* [Author your own clanService →](../guides/services/community.md)
|
||||||
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
|
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
|
||||||
|
|||||||
@@ -90,13 +90,10 @@ export CLAN_DEBUG_COMMANDS=1
|
|||||||
These options help you pinpoint the source and context of print messages and debug logs during development.
|
These options help you pinpoint the source and context of print messages and debug logs during development.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Analyzing Performance
|
## Analyzing Performance
|
||||||
|
|
||||||
To understand what's causing slow performance, set the environment variable `export CLAN_CLI_PERF=1`. When you complete a clan command, you'll see a summary of various performance metrics, helping you identify what's taking up time.
|
To understand what's causing slow performance, set the environment variable `export CLAN_CLI_PERF=1`. When you complete a clan command, you'll see a summary of various performance metrics, helping you identify what's taking up time.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## See all possible packages and tests
|
## See all possible packages and tests
|
||||||
|
|
||||||
To quickly show all possible packages and tests execute:
|
To quickly show all possible packages and tests execute:
|
||||||
@@ -155,28 +152,16 @@ To test the CLI locally in a development environment and set breakpoints for deb
|
|||||||
|
|
||||||
## Test Locally in a Nix Sandbox
|
## Test Locally in a Nix Sandbox
|
||||||
|
|
||||||
To run tests in a Nix sandbox, you have two options depending on whether your test functions have been marked as impure or not:
|
To run tests in a Nix sandbox:
|
||||||
|
|
||||||
### Running Tests Marked as Impure
|
|
||||||
|
|
||||||
If your test functions need to execute `nix build` and have been marked as impure because you can't execute `nix build` inside a Nix sandbox, use the following command:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix run .#impure-checks -L
|
nix build .#checks.x86_64-linux.clan-pytest-with-core
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will run the impure test functions.
|
|
||||||
|
|
||||||
### Running Pure Tests
|
|
||||||
|
|
||||||
For test functions that have not been marked as impure and don't require executing `nix build`, you can use the following command:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix build .#checks.x86_64-linux.clan-pytest --rebuild
|
nix build .#checks.x86_64-linux.clan-pytest-without-core
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will run all pure test functions.
|
|
||||||
|
|
||||||
### Inspecting the Nix Sandbox
|
### Inspecting the Nix Sandbox
|
||||||
|
|
||||||
If you need to inspect the Nix sandbox while running tests, follow these steps:
|
If you need to inspect the Nix sandbox while running tests, follow these steps:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ inputs = {
|
|||||||
|
|
||||||
## Import the Clan flake-parts Module
|
## 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
|
```nix
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ The following table shows the migration status of each deprecated clanModule:
|
|||||||
| `nginx` | ❌ Removed | |
|
| `nginx` | ❌ Removed | |
|
||||||
| `packages` | ✅ [Migrated](../../reference/clanServices/packages.md) | |
|
| `packages` | ✅ [Migrated](../../reference/clanServices/packages.md) | |
|
||||||
| `postgresql` | ❌ Removed | Now an [option](../../reference/clan.core/settings.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 | |
|
| `single-disk` | ❌ Removed | |
|
||||||
| `sshd` | ✅ [Migrated](../../reference/clanServices/sshd.md) | |
|
| `sshd` | ✅ [Migrated](../../reference/clanServices/sshd.md) | |
|
||||||
| `state-version` | ✅ [Migrated](../../reference/clanServices/state-version.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)
|
- Learn how to use the [Clan CLI](./cli/index.md)
|
||||||
- Explore available [services](./clanServices/index.md)
|
- Explore available [services](./clanServices/index.md)
|
||||||
- [NixOS Configuration Options](./clan.core/index.md) - Additional options avilable on a NixOS machine.
|
- [NixOS Configuration Options](./clan.core/index.md) - Additional options avilable on a NixOS machine.
|
||||||
|
|||||||
46
flake.lock
generated
@@ -13,11 +13,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1753067306,
|
"lastModified": 1756091210,
|
||||||
"narHash": "sha256-jyoEbaXa8/MwVQ+PajUdT63y3gYhgD9o7snO/SLaikw=",
|
"narHash": "sha256-oEUEAZnLbNHi8ti4jY8x10yWcIkYoFc5XD+2hjmOS04=",
|
||||||
"rev": "18dfd42bdb2cfff510b8c74206005f733e38d8b9",
|
"rev": "eb831bca21476fa8f6df26cb39e076842634700d",
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/18dfd42bdb2cfff510b8c74206005f733e38d8b9.tar.gz"
|
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/eb831bca21476fa8f6df26cb39e076842634700d.tar.gz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
@@ -31,11 +31,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1754971456,
|
"lastModified": 1755519972,
|
||||||
"narHash": "sha256-p04ZnIBGzerSyiY2dNGmookCldhldWAu03y0s3P8CB0=",
|
"narHash": "sha256-bU4nqi3IpsUZJeyS8Jk85ytlX61i4b0KCxXX9YcOgVc=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "8246829f2e675a46919718f9a64b71afe3bfb22d",
|
"rev": "4073ff2f481f9ef3501678ff479ed81402caae6d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -71,11 +71,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755275010,
|
"lastModified": 1755825449,
|
||||||
"narHash": "sha256-lEApCoWUEWh0Ifc3k1JdVjpMtFFXeL2gG1qvBnoRc2I=",
|
"narHash": "sha256-XkiN4NM9Xdy59h69Pc+Vg4PxkSm9EWl6u7k6D5FZ5cM=",
|
||||||
"owner": "nix-darwin",
|
"owner": "nix-darwin",
|
||||||
"repo": "nix-darwin",
|
"repo": "nix-darwin",
|
||||||
"rev": "7220b01d679e93ede8d7b25d6f392855b81dd475",
|
"rev": "8df64f819698c1fee0c2969696f54a843b2231e8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -86,11 +86,11 @@
|
|||||||
},
|
},
|
||||||
"nix-select": {
|
"nix-select": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1745005516,
|
"lastModified": 1755887746,
|
||||||
"narHash": "sha256-IVaoOGDIvAa/8I0sdiiZuKptDldrkDWUNf/+ezIRhyc=",
|
"narHash": "sha256-lzWbpHKX0WAn/jJDoCijIDss3rqYIPawe46GDaE6U3g=",
|
||||||
"rev": "69d8bf596194c5c35a4e90dd02c52aa530caddf8",
|
"rev": "92c2574c5e113281591be01e89bb9ddb31d19156",
|
||||||
"type": "tarball",
|
"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": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
@@ -99,11 +99,11 @@
|
|||||||
},
|
},
|
||||||
"nixos-facter-modules": {
|
"nixos-facter-modules": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1750412875,
|
"lastModified": 1755504238,
|
||||||
"narHash": "sha256-uP9Xxw5XcFwjX9lNoYRpybOnIIe1BHfZu5vJnnPg3Jc=",
|
"narHash": "sha256-mw7q5DPdmz/1au8mY0u1DztRgVyJToGJfJszxjKSNes=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixos-facter-modules",
|
"repo": "nixos-facter-modules",
|
||||||
"rev": "14df13c84552a7d1f33c1cd18336128fbc43f920",
|
"rev": "354ed498c9628f32383c3bf5b6668a17cdd72a28",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -115,10 +115,10 @@
|
|||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 315532800,
|
"lastModified": 315532800,
|
||||||
"narHash": "sha256-moy1MfcGj+Pd+lU3PHYQUJq9OP0Evv9me8MjtmHlnRM=",
|
"narHash": "sha256-h8Sx4S+/0FpodZji6W9lHzwY5BcuUG85Aj3GfhvGC2o=",
|
||||||
"rev": "32f313e49e42f715491e1ea7b306a87c16fe0388",
|
"rev": "a650b5d0de99158323597f048667c4d914243224",
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre844992.32f313e49e42/nixexprs.tar.xz"
|
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre845298.a650b5d0de99/nixexprs.tar.xz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
@@ -181,11 +181,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1754847726,
|
"lastModified": 1755934250,
|
||||||
"narHash": "sha256-2vX8QjO5lRsDbNYvN9hVHXLU6oMl+V/PsmIiJREG4rE=",
|
"narHash": "sha256-CsDojnMgYsfshQw3t4zjRUkmMmUdZGthl16bXVWgRYU=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "7d81f6fb2e19bf84f1c65135d1060d829fae2408",
|
"rev": "74e1a52d5bd9430312f8d1b8b0354c92c17453e5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
./nixosModules/flake-module.nix
|
./nixosModules/flake-module.nix
|
||||||
./pkgs/flake-module.nix
|
./pkgs/flake-module.nix
|
||||||
./templates/flake-module.nix
|
./templates/flake-module.nix
|
||||||
|
./pkgs/clan-cli/clan_cli/tests/flake-module.nix
|
||||||
]
|
]
|
||||||
++ [
|
++ [
|
||||||
(if pathExists ./flakeModules/clan.nix then import ./flakeModules/clan.nix inputs.self else { })
|
(if pathExists ./flakeModules/clan.nix then import ./flakeModules/clan.nix inputs.self else { })
|
||||||
|
|||||||
@@ -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.
|
# 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
|
# 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" = []
|
# 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.
|
# 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
|
# tsType = "unknown" is a type that we preload for json2ts, such that it gets the correct type in typescript
|
||||||
(option.type.name == "deferredModule")
|
(option.type.name == "deferredModule")
|
||||||
|
|||||||
@@ -255,6 +255,16 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
installedAt = lib.mkOption {
|
||||||
|
type = types.nullOr types.int;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Indicates when the machine was first installed.
|
||||||
|
|
||||||
|
Timestamp is in unix time (seconds since epoch).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
tags = lib.mkOption {
|
tags = lib.mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
List of tags for the machine.
|
List of tags for the machine.
|
||||||
|
|||||||
@@ -32,11 +32,15 @@ def init_test_environment() -> None:
|
|||||||
|
|
||||||
# Set up network bridge
|
# Set up network bridge
|
||||||
subprocess.run(
|
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", "link", "set", "br0", "up"], check=True, text=True)
|
||||||
subprocess.run(
|
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
|
# Set up minimal passwd file for unprivileged operations
|
||||||
@@ -111,8 +115,7 @@ def mount(
|
|||||||
mountflags: int = 0,
|
mountflags: int = 0,
|
||||||
data: str | None = None,
|
data: str | None = 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 source: The source of the file system (e.g., device name, remote filesystem).
|
||||||
:param target: The mount point (an existing directory).
|
:param target: The mount point (an existing directory).
|
||||||
@@ -129,7 +132,11 @@ def mount(
|
|||||||
|
|
||||||
# Call the mount system call
|
# Call the mount system call
|
||||||
result = libc.mount(
|
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:
|
if result != 0:
|
||||||
@@ -145,7 +152,7 @@ def prepare_machine_root(machinename: str, root: Path) -> None:
|
|||||||
root.mkdir(parents=True, exist_ok=True)
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
root.joinpath("etc").mkdir(parents=True, exist_ok=True)
|
root.joinpath("etc").mkdir(parents=True, exist_ok=True)
|
||||||
root.joinpath(".env").write_text(
|
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,
|
"""Call the given function repeatedly, with 1 second intervals,
|
||||||
until it returns True or a timeout is reached.
|
until it returns True or a timeout is reached.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for _ in range(timeout):
|
for _ in range(timeout):
|
||||||
if fn(False):
|
if fn(False):
|
||||||
return
|
return
|
||||||
@@ -284,8 +290,7 @@ class Machine:
|
|||||||
check_output: bool = True,
|
check_output: bool = True,
|
||||||
timeout: int | None = 900,
|
timeout: int | None = 900,
|
||||||
) -> subprocess.CompletedProcess:
|
) -> 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:
|
Commands are run with `set -euo pipefail` set:
|
||||||
|
|
||||||
@@ -316,7 +321,6 @@ class Machine:
|
|||||||
`timeout` parameter, e.g., `execute(cmd, timeout=10)` or
|
`timeout` parameter, e.g., `execute(cmd, timeout=10)` or
|
||||||
`execute(cmd, timeout=None)`. The default is 900 seconds.
|
`execute(cmd, timeout=None)`. The default is 900 seconds.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Always run command with shell opts
|
# Always run command with shell opts
|
||||||
command = f"set -eo pipefail; source /etc/profile; set -xu; {command}"
|
command = f"set -eo pipefail; source /etc/profile; set -xu; {command}"
|
||||||
|
|
||||||
@@ -330,7 +334,9 @@ class Machine:
|
|||||||
return proc
|
return proc
|
||||||
|
|
||||||
def nested(
|
def nested(
|
||||||
self, msg: str, attrs: dict[str, str] | None = None
|
self,
|
||||||
|
msg: str,
|
||||||
|
attrs: dict[str, str] | None = None,
|
||||||
) -> _GeneratorContextManager:
|
) -> _GeneratorContextManager:
|
||||||
if attrs is None:
|
if attrs is None:
|
||||||
attrs = {}
|
attrs = {}
|
||||||
@@ -339,8 +345,7 @@ class Machine:
|
|||||||
return self.logger.nested(msg, my_attrs)
|
return self.logger.nested(msg, my_attrs)
|
||||||
|
|
||||||
def systemctl(self, q: str) -> subprocess.CompletedProcess:
|
def systemctl(self, q: str) -> subprocess.CompletedProcess:
|
||||||
"""
|
"""Runs `systemctl` commands with optional support for
|
||||||
Runs `systemctl` commands with optional support for
|
|
||||||
`systemctl --user`
|
`systemctl --user`
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -355,8 +360,7 @@ class Machine:
|
|||||||
return self.execute(f"systemctl {q}")
|
return self.execute(f"systemctl {q}")
|
||||||
|
|
||||||
def wait_until_succeeds(self, command: str, timeout: int = 900) -> str:
|
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.
|
Has a default timeout of 900 seconds which can be modified, e.g.
|
||||||
`wait_until_succeeds(cmd, timeout=10)`. See `execute` for details on
|
`wait_until_succeeds(cmd, timeout=10)`. See `execute` for details on
|
||||||
command execution.
|
command execution.
|
||||||
@@ -374,18 +378,17 @@ class Machine:
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
def wait_for_open_port(
|
def wait_for_open_port(
|
||||||
self, port: int, addr: str = "localhost", timeout: int = 900
|
self,
|
||||||
|
port: int,
|
||||||
|
addr: str = "localhost",
|
||||||
|
timeout: int = 900,
|
||||||
) -> None:
|
) -> 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}"
|
command = f"nc -z {shlex.quote(addr)} {port}"
|
||||||
self.wait_until_succeeds(command, timeout=timeout)
|
self.wait_until_succeeds(command, timeout=timeout)
|
||||||
|
|
||||||
def wait_for_file(self, filename: str, timeout: int = 30) -> None:
|
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:
|
def check_file(_last_try: bool) -> bool:
|
||||||
result = self.execute(f"test -e {filename}")
|
result = self.execute(f"test -e {filename}")
|
||||||
@@ -395,8 +398,7 @@ class Machine:
|
|||||||
retry(check_file, timeout)
|
retry(check_file, timeout)
|
||||||
|
|
||||||
def wait_for_unit(self, unit: str, timeout: int = 900) -> None:
|
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
|
Throws exceptions on "failed" and "inactive" states as well as after
|
||||||
timing out.
|
timing out.
|
||||||
"""
|
"""
|
||||||
@@ -441,9 +443,7 @@ class Machine:
|
|||||||
return res.stdout
|
return res.stdout
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
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:
|
if self.process:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
self.process.wait()
|
self.process.wait()
|
||||||
@@ -557,7 +557,7 @@ class Driver:
|
|||||||
rootdir=tempdir_path / container.name,
|
rootdir=tempdir_path / container.name,
|
||||||
out_dir=self.out_dir,
|
out_dir=self.out_dir,
|
||||||
logger=self.logger,
|
logger=self.logger,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def start_all(self) -> None:
|
def start_all(self) -> None:
|
||||||
@@ -581,7 +581,7 @@ class Driver:
|
|||||||
)
|
)
|
||||||
|
|
||||||
print(
|
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(
|
print(
|
||||||
" ".join(
|
" ".join(
|
||||||
@@ -603,8 +603,8 @@ class Driver:
|
|||||||
"-c",
|
"-c",
|
||||||
"bash",
|
"bash",
|
||||||
Style.RESET_ALL,
|
Style.RESET_ALL,
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_symbols(self) -> dict[str, Any]:
|
def test_symbols(self) -> dict[str, Any]:
|
||||||
@@ -623,7 +623,7 @@ class Driver:
|
|||||||
"additionally exposed symbols:\n "
|
"additionally exposed symbols:\n "
|
||||||
+ ", ".join(m.name for m in self.machines)
|
+ ", ".join(m.name for m in self.machines)
|
||||||
+ ",\n "
|
+ ",\n "
|
||||||
+ ", ".join(list(general_symbols.keys()))
|
+ ", ".join(list(general_symbols.keys())),
|
||||||
)
|
)
|
||||||
return {**general_symbols, **machine_symbols}
|
return {**general_symbols, **machine_symbols}
|
||||||
|
|
||||||
|
|||||||
@@ -25,14 +25,18 @@ class AbstractLogger(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def subtest(
|
def subtest(
|
||||||
self, name: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
name: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def nested(
|
def nested(
|
||||||
self, message: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
message: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -66,7 +70,7 @@ class JunitXMLLogger(AbstractLogger):
|
|||||||
|
|
||||||
def __init__(self, outfile: Path) -> None:
|
def __init__(self, outfile: Path) -> None:
|
||||||
self.tests: dict[str, JunitXMLLogger.TestCaseState] = {
|
self.tests: dict[str, JunitXMLLogger.TestCaseState] = {
|
||||||
"main": self.TestCaseState()
|
"main": self.TestCaseState(),
|
||||||
}
|
}
|
||||||
self.currentSubtest = "main"
|
self.currentSubtest = "main"
|
||||||
self.outfile: Path = outfile
|
self.outfile: Path = outfile
|
||||||
@@ -78,7 +82,9 @@ class JunitXMLLogger(AbstractLogger):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def subtest(
|
def subtest(
|
||||||
self, name: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
name: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
old_test = self.currentSubtest
|
old_test = self.currentSubtest
|
||||||
self.tests.setdefault(name, self.TestCaseState())
|
self.tests.setdefault(name, self.TestCaseState())
|
||||||
@@ -90,7 +96,9 @@ class JunitXMLLogger(AbstractLogger):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def nested(
|
def nested(
|
||||||
self, message: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
message: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
self.log(message)
|
self.log(message)
|
||||||
yield
|
yield
|
||||||
@@ -144,7 +152,9 @@ class CompositeLogger(AbstractLogger):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def subtest(
|
def subtest(
|
||||||
self, name: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
name: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for logger in self.logger_list:
|
for logger in self.logger_list:
|
||||||
@@ -153,7 +163,9 @@ class CompositeLogger(AbstractLogger):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def nested(
|
def nested(
|
||||||
self, message: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
message: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for logger in self.logger_list:
|
for logger in self.logger_list:
|
||||||
@@ -200,19 +212,24 @@ class TerminalLogger(AbstractLogger):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def subtest(
|
def subtest(
|
||||||
self, name: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
name: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
with self.nested("subtest: " + name, attributes):
|
with self.nested("subtest: " + name, attributes):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def nested(
|
def nested(
|
||||||
self, message: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
message: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
self._eprint(
|
self._eprint(
|
||||||
self.maybe_prefix(
|
self.maybe_prefix(
|
||||||
Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL, attributes
|
Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL,
|
||||||
)
|
attributes,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
tic = time.time()
|
tic = time.time()
|
||||||
@@ -259,7 +276,9 @@ class XMLLogger(AbstractLogger):
|
|||||||
return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C")
|
return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C")
|
||||||
|
|
||||||
def maybe_prefix(
|
def maybe_prefix(
|
||||||
self, message: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
message: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
if attributes and "machine" in attributes:
|
if attributes and "machine" in attributes:
|
||||||
return f"{attributes['machine']}: {message}"
|
return f"{attributes['machine']}: {message}"
|
||||||
@@ -309,14 +328,18 @@ class XMLLogger(AbstractLogger):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def subtest(
|
def subtest(
|
||||||
self, name: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
name: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
with self.nested("subtest: " + name, attributes):
|
with self.nested("subtest: " + name, attributes):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def nested(
|
def nested(
|
||||||
self, message: str, attributes: dict[str, str] | None = None
|
self,
|
||||||
|
message: str,
|
||||||
|
attributes: dict[str, str] | None = None,
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
if attributes is None:
|
if attributes is None:
|
||||||
attributes = {}
|
attributes = {}
|
||||||
|
|||||||
@@ -1,40 +1,17 @@
|
|||||||
{ ... }:
|
|
||||||
{
|
{
|
||||||
perSystem =
|
perSystem.clan.nixosTests.machine-id = {
|
||||||
{ ... }:
|
|
||||||
{
|
|
||||||
clan.nixosTests.machine-id = {
|
|
||||||
|
|
||||||
name = "service-machine-id";
|
name = "service-machine-id";
|
||||||
|
|
||||||
clan = {
|
clan = {
|
||||||
directory = ./.;
|
directory = ./.;
|
||||||
|
machines.server = {
|
||||||
# Workaround until we can use nodes.server = { };
|
clan.core.settings.machine-id.enable = true;
|
||||||
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 = "";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# 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 = {
|
clan = {
|
||||||
directory = ./.;
|
directory = ./.;
|
||||||
|
|
||||||
# Workaround until we can use nodes.machine = { };
|
machines.machine = {
|
||||||
modules."@clan/importer" = ../../../../clanServices/importer;
|
clan.core.postgresql.enable = true;
|
||||||
|
clan.core.postgresql.users.test = { };
|
||||||
inventory = {
|
clan.core.postgresql.databases.test.create.options.OWNER = "test";
|
||||||
machines.machine = { };
|
clan.core.settings.directory = ./.;
|
||||||
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 = ./.;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# TODO: Broken. Use instead of importer after fixing.
|
|
||||||
# nodes.machine = { };
|
|
||||||
|
|
||||||
testScript =
|
testScript =
|
||||||
let
|
let
|
||||||
runpg = "runuser -u postgres -- /run/current-system/sw/bin/psql";
|
runpg = "runuser -u postgres -- /run/current-system/sw/bin/psql";
|
||||||
|
|||||||
@@ -290,9 +290,11 @@ in
|
|||||||
};
|
};
|
||||||
owner = mkOption {
|
owner = mkOption {
|
||||||
description = "The user name or id that will own the file.";
|
description = "The user name or id that will own the file.";
|
||||||
|
type = str;
|
||||||
default = "root";
|
default = "root";
|
||||||
};
|
};
|
||||||
group = mkOption {
|
group = mkOption {
|
||||||
|
type = str;
|
||||||
description = "The group name or id that will own the file.";
|
description = "The group name or id that will own the file.";
|
||||||
default = if _class == "darwin" then "wheel" else "root";
|
default = if _class == "darwin" then "wheel" else "root";
|
||||||
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
|
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
|
||||||
@@ -302,6 +304,15 @@ in
|
|||||||
description = "The unix file mode of the file. Must be a 4-digit octal number.";
|
description = "The unix file mode of the file. Must be a 4-digit octal number.";
|
||||||
default = "0400";
|
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 =
|
value =
|
||||||
mkOption {
|
mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ in
|
|||||||
);
|
);
|
||||||
value = mkIf (file.config.secret == false) (
|
value = mkIf (file.config.secret == false) (
|
||||||
# dynamically adjust priority to allow overriding with mkDefault in case the file is not found
|
# 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
|
# if the file is found it should have normal priority
|
||||||
readFile file.config.flakePath
|
readFile file.config.flakePath
|
||||||
else
|
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}"
|
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);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
116
nixosModules/clanCore/vm-base.nix
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Standalone VM base module that can be imported independently
|
||||||
|
# This module contains the core VM configuration without the system extension
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
modulesPath,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
# Flatten the list of state folders into a single list
|
||||||
|
stateFolders = lib.flatten (
|
||||||
|
lib.mapAttrsToList (_item: attrs: attrs.folders) config.clan.core.state
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/virtualisation/qemu-vm.nix")
|
||||||
|
./serial.nix
|
||||||
|
./waypipe.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
clan.core.state.HOME.folders = [ "/home" ];
|
||||||
|
|
||||||
|
clan.services.waypipe = {
|
||||||
|
inherit (config.clan.core.vm.inspect.waypipe) enable command;
|
||||||
|
};
|
||||||
|
|
||||||
|
# required for issuing shell commands via qga
|
||||||
|
services.qemuGuest.enable = true;
|
||||||
|
|
||||||
|
# required to react to system_powerdown qmp command
|
||||||
|
# Some desktop managers like xfce override the poweroff signal and therefore
|
||||||
|
# make it impossible to handle it via 'logind' directly.
|
||||||
|
services.acpid.enable = true;
|
||||||
|
services.acpid.handlers.power.event = "button/power.*";
|
||||||
|
services.acpid.handlers.power.action = "poweroff";
|
||||||
|
|
||||||
|
# only works on x11
|
||||||
|
services.spice-vdagentd.enable = config.services.xserver.enable;
|
||||||
|
|
||||||
|
boot.initrd.systemd.enable = true;
|
||||||
|
|
||||||
|
boot.initrd.systemd.storePaths = [
|
||||||
|
pkgs.util-linux
|
||||||
|
pkgs.e2fsprogs
|
||||||
|
];
|
||||||
|
boot.initrd.systemd.emergencyAccess = true;
|
||||||
|
|
||||||
|
# userborn would be faster because it doesn't need perl, but it cannot create normal users
|
||||||
|
services.userborn.enable = true;
|
||||||
|
users.mutableUsers = false;
|
||||||
|
users.allowNoPasswordLogin = true;
|
||||||
|
|
||||||
|
boot.initrd.kernelModules = [ "virtiofs" ];
|
||||||
|
virtualisation.writableStore = false;
|
||||||
|
virtualisation.fileSystems = lib.mkForce (
|
||||||
|
{
|
||||||
|
"/nix/store" = {
|
||||||
|
device = "nix-store";
|
||||||
|
options = [
|
||||||
|
"x-systemd.requires=systemd-modules-load.service"
|
||||||
|
"ro"
|
||||||
|
];
|
||||||
|
fsType = "virtiofs";
|
||||||
|
};
|
||||||
|
|
||||||
|
"/" = {
|
||||||
|
device = "/dev/vda";
|
||||||
|
fsType = "ext4";
|
||||||
|
options = [
|
||||||
|
"defaults"
|
||||||
|
"x-systemd.makefs"
|
||||||
|
"nobarrier"
|
||||||
|
"noatime"
|
||||||
|
"nodiratime"
|
||||||
|
"data=writeback"
|
||||||
|
"discard"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
"/vmstate" = {
|
||||||
|
device = "/dev/vdb";
|
||||||
|
options = [
|
||||||
|
"x-systemd.makefs"
|
||||||
|
"noatime"
|
||||||
|
"nodiratime"
|
||||||
|
"discard"
|
||||||
|
];
|
||||||
|
noCheck = true;
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
|
||||||
|
${config.clan.core.facts.secretUploadDirectory} = {
|
||||||
|
device = "secrets";
|
||||||
|
fsType = "9p";
|
||||||
|
neededForBoot = true;
|
||||||
|
options = [
|
||||||
|
"trans=virtio"
|
||||||
|
"version=9p2000.L"
|
||||||
|
"cache=loose"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.listToAttrs (
|
||||||
|
map (
|
||||||
|
folder:
|
||||||
|
lib.nameValuePair folder {
|
||||||
|
device = "/vmstate${folder}";
|
||||||
|
fsType = "none";
|
||||||
|
options = [ "bind" ];
|
||||||
|
}
|
||||||
|
) stateFolders
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,116 +4,11 @@
|
|||||||
pkgs,
|
pkgs,
|
||||||
options,
|
options,
|
||||||
extendModules,
|
extendModules,
|
||||||
modulesPath,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# Flatten the list of state folders into a single list
|
# Import the standalone VM base module
|
||||||
stateFolders = lib.flatten (
|
vmModule = import ./vm-base.nix;
|
||||||
lib.mapAttrsToList (_item: attrs: attrs.folders) config.clan.core.state
|
|
||||||
);
|
|
||||||
|
|
||||||
vmModule = {
|
|
||||||
imports = [
|
|
||||||
(modulesPath + "/virtualisation/qemu-vm.nix")
|
|
||||||
./serial.nix
|
|
||||||
./waypipe.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
clan.core.state.HOME.folders = [ "/home" ];
|
|
||||||
|
|
||||||
clan.services.waypipe = {
|
|
||||||
inherit (config.clan.core.vm.inspect.waypipe) enable command;
|
|
||||||
};
|
|
||||||
|
|
||||||
# required for issuing shell commands via qga
|
|
||||||
services.qemuGuest.enable = true;
|
|
||||||
|
|
||||||
# required to react to system_powerdown qmp command
|
|
||||||
# Some desktop managers like xfce override the poweroff signal and therefore
|
|
||||||
# make it impossible to handle it via 'logind' directly.
|
|
||||||
services.acpid.enable = true;
|
|
||||||
services.acpid.handlers.power.event = "button/power.*";
|
|
||||||
services.acpid.handlers.power.action = "poweroff";
|
|
||||||
|
|
||||||
# only works on x11
|
|
||||||
services.spice-vdagentd.enable = config.services.xserver.enable;
|
|
||||||
|
|
||||||
boot.initrd.systemd.enable = true;
|
|
||||||
|
|
||||||
boot.initrd.systemd.storePaths = [
|
|
||||||
pkgs.util-linux
|
|
||||||
pkgs.e2fsprogs
|
|
||||||
];
|
|
||||||
boot.initrd.systemd.emergencyAccess = true;
|
|
||||||
|
|
||||||
# userborn would be faster because it doesn't need perl, but it cannot create normal users
|
|
||||||
services.userborn.enable = true;
|
|
||||||
users.mutableUsers = false;
|
|
||||||
users.allowNoPasswordLogin = true;
|
|
||||||
|
|
||||||
boot.initrd.kernelModules = [ "virtiofs" ];
|
|
||||||
virtualisation.writableStore = false;
|
|
||||||
virtualisation.fileSystems = lib.mkForce (
|
|
||||||
{
|
|
||||||
"/nix/store" = {
|
|
||||||
device = "nix-store";
|
|
||||||
options = [
|
|
||||||
"x-systemd.requires=systemd-modules-load.service"
|
|
||||||
"ro"
|
|
||||||
];
|
|
||||||
fsType = "virtiofs";
|
|
||||||
};
|
|
||||||
|
|
||||||
"/" = {
|
|
||||||
device = "/dev/vda";
|
|
||||||
fsType = "ext4";
|
|
||||||
options = [
|
|
||||||
"defaults"
|
|
||||||
"x-systemd.makefs"
|
|
||||||
"nobarrier"
|
|
||||||
"noatime"
|
|
||||||
"nodiratime"
|
|
||||||
"data=writeback"
|
|
||||||
"discard"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
"/vmstate" = {
|
|
||||||
device = "/dev/vdb";
|
|
||||||
options = [
|
|
||||||
"x-systemd.makefs"
|
|
||||||
"noatime"
|
|
||||||
"nodiratime"
|
|
||||||
"discard"
|
|
||||||
];
|
|
||||||
noCheck = true;
|
|
||||||
fsType = "ext4";
|
|
||||||
};
|
|
||||||
|
|
||||||
${config.clan.core.facts.secretUploadDirectory} = {
|
|
||||||
device = "secrets";
|
|
||||||
fsType = "9p";
|
|
||||||
neededForBoot = true;
|
|
||||||
options = [
|
|
||||||
"trans=virtio"
|
|
||||||
"version=9p2000.L"
|
|
||||||
"cache=loose"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// lib.listToAttrs (
|
|
||||||
map (
|
|
||||||
folder:
|
|
||||||
lib.nameValuePair folder {
|
|
||||||
device = "/vmstate${folder}";
|
|
||||||
fsType = "none";
|
|
||||||
options = [ "bind" ];
|
|
||||||
}
|
|
||||||
) stateFolders
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
# We cannot simply merge the VM config into the current system config, because
|
# We cannot simply merge the VM config into the current system config, because
|
||||||
# it is not necessarily a VM.
|
# it is not necessarily a VM.
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ def compute_zerotier_ip(network_id: str, identity: Identity) -> ipaddress.IPv6Ad
|
|||||||
(node_id >> 16) & 0xFF,
|
(node_id >> 16) & 0xFF,
|
||||||
(node_id >> 8) & 0xFF,
|
(node_id >> 8) & 0xFF,
|
||||||
(node_id) & 0xFF,
|
(node_id) & 0xFF,
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
return ipaddress.IPv6Address(bytes(addr_parts))
|
return ipaddress.IPv6Address(bytes(addr_parts))
|
||||||
|
|
||||||
@@ -203,7 +203,10 @@ def compute_zerotier_ip(network_id: str, identity: Identity) -> ipaddress.IPv6Ad
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
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("--ip", type=Path, required=True)
|
||||||
parser.add_argument("--identity-secret", 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 = json.loads(Path(moon_json_path).read_text())
|
||||||
moon_json["roots"][0]["stableEndpoints"] = json.loads(
|
moon_json["roots"][0]["stableEndpoints"] = json.loads(
|
||||||
Path(endpoint_config).read_text()
|
Path(endpoint_config).read_text(),
|
||||||
)
|
)
|
||||||
|
|
||||||
with NamedTemporaryFile("w") as f:
|
with NamedTemporaryFile("w") as f:
|
||||||
|
|||||||
@@ -34,4 +34,7 @@ in
|
|||||||
|
|
||||||
flake.nixosModules.clanCore = clanCore;
|
flake.nixosModules.clanCore = clanCore;
|
||||||
flake.darwinModules.clanCore = clanCore;
|
flake.darwinModules.clanCore = clanCore;
|
||||||
|
|
||||||
|
# Standalone VM base module that can be imported for VM testing
|
||||||
|
flake.nixosModules.clan-vm-base = ./clanCore/vm-base.nix;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_and_path = remote_url.split("@")[1] # git.clan.lol:clan/clan-core.git
|
||||||
host = host_and_path.split(":")[0] # git.clan.lol
|
host = host_and_path.split(":")[0] # git.clan.lol
|
||||||
repo_path = host_and_path.split(":")[1] # clan/clan-core.git
|
repo_path = host_and_path.split(":")[1] # clan/clan-core.git
|
||||||
if repo_path.endswith(".git"):
|
repo_path = repo_path.removesuffix(".git") # clan/clan-core
|
||||||
repo_path = repo_path[:-4] # clan/clan-core
|
|
||||||
elif remote_url.startswith("https://"):
|
elif remote_url.startswith("https://"):
|
||||||
# HTTPS format: https://git.clan.lol/clan/clan-core.git
|
# HTTPS format: https://git.clan.lol/clan/clan-core.git
|
||||||
url_parts = remote_url.replace("https://", "").split("/")
|
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(
|
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]:
|
) -> list[dict]:
|
||||||
"""Fetch CI statuses for a specific commit SHA."""
|
"""Fetch CI statuses for a specific commit SHA."""
|
||||||
status_url = (
|
status_url = (
|
||||||
@@ -183,7 +185,7 @@ def run_git_command(command: list) -> tuple[int, str, str]:
|
|||||||
|
|
||||||
def get_current_branch_name() -> str:
|
def get_current_branch_name() -> str:
|
||||||
exit_code, branch_name, error = run_git_command(
|
exit_code, branch_name, error = run_git_command(
|
||||||
["git", "rev-parse", "--abbrev-ref", "HEAD"]
|
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
@@ -196,7 +198,7 @@ def get_current_branch_name() -> str:
|
|||||||
def get_latest_commit_info() -> tuple[str, str]:
|
def get_latest_commit_info() -> tuple[str, str]:
|
||||||
"""Get the title and body of the latest commit."""
|
"""Get the title and body of the latest commit."""
|
||||||
exit_code, commit_msg, error = run_git_command(
|
exit_code, commit_msg, error = run_git_command(
|
||||||
["git", "log", "-1", "--pretty=format:%B"]
|
["git", "log", "-1", "--pretty=format:%B"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
@@ -225,7 +227,7 @@ def get_commits_since_main() -> list[tuple[str, str]]:
|
|||||||
"main..HEAD",
|
"main..HEAD",
|
||||||
"--no-merges",
|
"--no-merges",
|
||||||
"--pretty=format:%s|%b|---END---",
|
"--pretty=format:%s|%b|---END---",
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
@@ -263,7 +265,9 @@ def open_editor_for_pr() -> tuple[str, str]:
|
|||||||
commits_since_main = get_commits_since_main()
|
commits_since_main = get_commits_since_main()
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(
|
||||||
mode="w+", suffix="COMMIT_EDITMSG", delete=False
|
mode="w+",
|
||||||
|
suffix="COMMIT_EDITMSG",
|
||||||
|
delete=False,
|
||||||
) as temp_file:
|
) as temp_file:
|
||||||
temp_file.flush()
|
temp_file.flush()
|
||||||
temp_file_path = temp_file.name
|
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("# 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("# Everything else will be used as the PR description.\n")
|
||||||
temp_file.write(
|
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("# In vim for example you can use :cq!\n")
|
||||||
temp_file.write("#\n")
|
temp_file.write("#\n")
|
||||||
@@ -373,7 +377,7 @@ def create_agit_push(
|
|||||||
print(
|
print(
|
||||||
f" Description: {description[:50]}..."
|
f" Description: {description[:50]}..."
|
||||||
if len(description) > 50
|
if len(description) > 50
|
||||||
else f" Description: {description}"
|
else f" Description: {description}",
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
@@ -530,19 +534,26 @@ Examples:
|
|||||||
)
|
)
|
||||||
|
|
||||||
create_parser.add_argument(
|
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(
|
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(
|
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(
|
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(
|
create_parser.add_argument(
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ log = logging.getLogger(__name__)
|
|||||||
def main(argv: list[str] = sys.argv) -> int:
|
def main(argv: list[str] = sys.argv) -> int:
|
||||||
parser = argparse.ArgumentParser(description="Clan App")
|
parser = argparse.ArgumentParser(description="Clan App")
|
||||||
parser.add_argument(
|
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("--debug", action="store_true", help="Enable debug mode")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|||||||
@@ -56,18 +56,23 @@ class ApiBridge(ABC):
|
|||||||
for middleware in self.middleware_chain:
|
for middleware in self.middleware_chain:
|
||||||
try:
|
try:
|
||||||
log.debug(
|
log.debug(
|
||||||
f"{middleware.__class__.__name__} => {request.method_name}"
|
f"{middleware.__class__.__name__} => {request.method_name}",
|
||||||
)
|
)
|
||||||
middleware.process(context)
|
middleware.process(context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If middleware fails, handle error
|
# If middleware fails, handle error
|
||||||
self.send_api_error_response(
|
self.send_api_error_response(
|
||||||
request.op_key or "unknown", str(e), ["middleware_error"]
|
request.op_key or "unknown",
|
||||||
|
str(e),
|
||||||
|
["middleware_error"],
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
def send_api_error_response(
|
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:
|
) -> None:
|
||||||
"""Send an error response."""
|
"""Send an error response."""
|
||||||
from clan_lib.api import ApiError, ErrorDataClass
|
from clan_lib.api import ApiError, ErrorDataClass
|
||||||
@@ -80,7 +85,7 @@ class ApiBridge(ABC):
|
|||||||
message="An internal error occured",
|
message="An internal error occured",
|
||||||
description=error_message,
|
description=error_message,
|
||||||
location=location,
|
location=location,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,6 +112,7 @@ class ApiBridge(ABC):
|
|||||||
thread_name: Name for the thread (for debugging)
|
thread_name: Name for the thread (for debugging)
|
||||||
wait_for_completion: Whether to wait for the thread to complete
|
wait_for_completion: Whether to wait for the thread to complete
|
||||||
timeout: Timeout in seconds when waiting for completion
|
timeout: Timeout in seconds when waiting for completion
|
||||||
|
|
||||||
"""
|
"""
|
||||||
op_key = request.op_key or "unknown"
|
op_key = request.op_key or "unknown"
|
||||||
|
|
||||||
@@ -116,7 +122,7 @@ class ApiBridge(ABC):
|
|||||||
try:
|
try:
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Processing {request.method_name} with args {request.args} "
|
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)
|
self.process_request(request)
|
||||||
finally:
|
finally:
|
||||||
@@ -124,7 +130,9 @@ class ApiBridge(ABC):
|
|||||||
|
|
||||||
stop_event = threading.Event()
|
stop_event = threading.Event()
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=thread_task, args=(stop_event,), name=thread_name
|
target=thread_task,
|
||||||
|
args=(stop_event,),
|
||||||
|
name=thread_name,
|
||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
@@ -138,5 +146,7 @@ class ApiBridge(ABC):
|
|||||||
if thread.is_alive():
|
if thread.is_alive():
|
||||||
stop_event.set() # Cancel the thread
|
stop_event.set() # Cancel the thread
|
||||||
self.send_api_error_response(
|
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:
|
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.
|
Returns the path to the clan folder or an error if it fails.
|
||||||
"""
|
"""
|
||||||
file_request = FileRequest(
|
file_request = FileRequest(
|
||||||
@@ -52,7 +51,7 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
|
|||||||
message="No folder selected",
|
message="No folder selected",
|
||||||
description="You must select a folder to open.",
|
description="You must select a folder to open.",
|
||||||
location=["get_clan_folder"],
|
location=["get_clan_folder"],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
|
|||||||
message="Invalid clan folder",
|
message="Invalid clan folder",
|
||||||
description=f"The selected folder '{clan_folder}' is not a valid clan folder.",
|
description=f"The selected folder '{clan_folder}' is not a valid clan folder.",
|
||||||
location=["get_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()])
|
selected_path = remove_none([gfile.get_path()])
|
||||||
returns(
|
returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_path, status="success"
|
op_key=op_key,
|
||||||
)
|
data=selected_path,
|
||||||
|
status="success",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("Error opening file")
|
log.exception("Error opening file")
|
||||||
@@ -116,9 +117,9 @@ def gtk_open_file(file_request: FileRequest, op_key: str) -> bool:
|
|||||||
message=e.__class__.__name__,
|
message=e.__class__.__name__,
|
||||||
description=str(e),
|
description=str(e),
|
||||||
location=["get_system_file"],
|
location=["get_system_file"],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_file_select_multiple(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
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])
|
selected_paths = remove_none([gfile.get_path() for gfile in gfiles])
|
||||||
returns(
|
returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_paths, status="success"
|
op_key=op_key,
|
||||||
)
|
data=selected_paths,
|
||||||
|
status="success",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
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__,
|
message=e.__class__.__name__,
|
||||||
description=str(e),
|
description=str(e),
|
||||||
location=["get_system_file"],
|
location=["get_system_file"],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
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()])
|
selected_path = remove_none([gfile.get_path()])
|
||||||
returns(
|
returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_path, status="success"
|
op_key=op_key,
|
||||||
)
|
data=selected_path,
|
||||||
|
status="success",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
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__,
|
message=e.__class__.__name__,
|
||||||
description=str(e),
|
description=str(e),
|
||||||
location=["get_system_file"],
|
location=["get_system_file"],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_save_finish(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
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()])
|
selected_path = remove_none([gfile.get_path()])
|
||||||
returns(
|
returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_path, status="success"
|
op_key=op_key,
|
||||||
)
|
data=selected_path,
|
||||||
|
status="success",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
returns(SuccessDataClass(op_key=op_key, data=None, status="success"))
|
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__,
|
message=e.__class__.__name__,
|
||||||
description=str(e),
|
description=str(e),
|
||||||
location=["get_system_file"],
|
location=["get_system_file"],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
dialog = Gtk.FileDialog()
|
dialog = Gtk.FileDialog()
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class ArgumentParsingMiddleware(Middleware):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(
|
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.bridge.send_api_error_response(
|
||||||
context.request.op_key or "unknown",
|
context.request.op_key or "unknown",
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ class Middleware(ABC):
|
|||||||
"""Process the request through this middleware."""
|
"""Process the request through this middleware."""
|
||||||
|
|
||||||
def register_context_manager(
|
def register_context_manager(
|
||||||
self, context: MiddlewareContext, cm: AbstractContextManager[Any]
|
self,
|
||||||
|
context: MiddlewareContext,
|
||||||
|
cm: AbstractContextManager[Any],
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Register a context manager with the exit stack."""
|
"""Register a context manager with the exit stack."""
|
||||||
return context.exit_stack.enter_context(cm)
|
return context.exit_stack.enter_context(cm)
|
||||||
|
|||||||
@@ -25,23 +25,26 @@ class LoggingMiddleware(Middleware):
|
|||||||
try:
|
try:
|
||||||
# Handle log group configuration
|
# Handle log group configuration
|
||||||
log_group: list[str] | None = context.request.header.get("logging", {}).get(
|
log_group: list[str] | None = context.request.header.get("logging", {}).get(
|
||||||
"group_path", None
|
"group_path",
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
if log_group is not None:
|
if log_group is not None:
|
||||||
if not isinstance(log_group, list):
|
if not isinstance(log_group, list):
|
||||||
msg = f"Expected log_group to be a list, got {type(log_group)}"
|
msg = f"Expected log_group to be a list, got {type(log_group)}"
|
||||||
raise TypeError(msg) # noqa: TRY301
|
raise TypeError(msg) # noqa: TRY301
|
||||||
log.warning(
|
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
|
# Create log file
|
||||||
log_file = self.log_manager.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()
|
).get_file_path()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(
|
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.bridge.send_api_error_response(
|
||||||
context.request.op_key or "unknown",
|
context.request.op_key or "unknown",
|
||||||
@@ -76,7 +79,8 @@ class LoggingMiddleware(Middleware):
|
|||||||
line_buffering=True,
|
line_buffering=True,
|
||||||
)
|
)
|
||||||
self.handler = setup_logging(
|
self.handler = setup_logging(
|
||||||
log.getEffectiveLevel(), log_file=handler_stream
|
log.getEffectiveLevel(),
|
||||||
|
log_file=handler_stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class MethodExecutionMiddleware(Middleware):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(
|
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.bridge.send_api_error_response(
|
||||||
context.request.op_key or "unknown",
|
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>]
|
# Add a log group ["clans", <dynamic_name>, "machines", <dynamic_name>]
|
||||||
log_manager = LogManager(base_dir=user_data_dir() / "clan-app" / "logs")
|
log_manager = LogManager(base_dir=user_data_dir() / "clan-app" / "logs")
|
||||||
clan_log_group = LogGroupConfig("clans", "Clans").add_child(
|
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)
|
log_manager = log_manager.add_root_group_config(clan_log_group)
|
||||||
# Init LogManager global in log_manager_api module
|
# 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
|
# HTTP-only mode - keep the server running
|
||||||
log.info("HTTP API server running...")
|
log.info("HTTP API server running...")
|
||||||
log.info(
|
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")
|
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")
|
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
def _send_json_response_with_status(
|
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:
|
) -> None:
|
||||||
"""Send a JSON response with the given status code."""
|
"""Send a JSON response with the given status code."""
|
||||||
try:
|
try:
|
||||||
@@ -82,11 +84,13 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
response_dict = dataclass_to_dict(response)
|
response_dict = dataclass_to_dict(response)
|
||||||
self._send_json_response_with_status(response_dict, 200)
|
self._send_json_response_with_status(response_dict, 200)
|
||||||
log.debug(
|
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(
|
def _create_success_response(
|
||||||
self, op_key: str, data: dict[str, Any]
|
self,
|
||||||
|
op_key: str,
|
||||||
|
data: dict[str, Any],
|
||||||
) -> BackendResponse:
|
) -> BackendResponse:
|
||||||
"""Create a successful API response."""
|
"""Create a successful API response."""
|
||||||
return BackendResponse(
|
return BackendResponse(
|
||||||
@@ -98,14 +102,16 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
def _send_info_response(self) -> None:
|
def _send_info_response(self) -> None:
|
||||||
"""Send server information response."""
|
"""Send server information response."""
|
||||||
response = self._create_success_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)
|
self.send_api_response(response)
|
||||||
|
|
||||||
def _send_methods_response(self) -> None:
|
def _send_methods_response(self) -> None:
|
||||||
"""Send available API methods response."""
|
"""Send available API methods response."""
|
||||||
response = self._create_success_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)
|
self.send_api_response(response)
|
||||||
|
|
||||||
@@ -179,7 +185,7 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
json_data = json.loads(file_data.decode("utf-8"))
|
json_data = json.loads(file_data.decode("utf-8"))
|
||||||
server_address = getattr(self.server, "server_address", ("localhost", 80))
|
server_address = getattr(self.server, "server_address", ("localhost", 80))
|
||||||
json_data["servers"] = [
|
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")
|
file_data = json.dumps(json_data, indent=2).encode("utf-8")
|
||||||
|
|
||||||
@@ -213,7 +219,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
# Validate API path
|
# Validate API path
|
||||||
if not path.startswith("/api/v1/"):
|
if not path.startswith("/api/v1/"):
|
||||||
self.send_api_error_response(
|
self.send_api_error_response(
|
||||||
"post", f"Path not found: {path}", ["http_bridge", "POST"]
|
"post",
|
||||||
|
f"Path not found: {path}",
|
||||||
|
["http_bridge", "POST"],
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -221,7 +229,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
method_name = path[len("/api/v1/") :]
|
method_name = path[len("/api/v1/") :]
|
||||||
if not method_name:
|
if not method_name:
|
||||||
self.send_api_error_response(
|
self.send_api_error_response(
|
||||||
"post", "Method name required", ["http_bridge", "POST"]
|
"post",
|
||||||
|
"Method name required",
|
||||||
|
["http_bridge", "POST"],
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -289,19 +299,26 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
# Create API request
|
# Create API request
|
||||||
api_request = BackendRequest(
|
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:
|
except Exception as e:
|
||||||
self.send_api_error_response(
|
self.send_api_error_response(
|
||||||
gen_op_key, str(e), ["http_bridge", method_name]
|
gen_op_key,
|
||||||
|
str(e),
|
||||||
|
["http_bridge", method_name],
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._process_api_request_in_thread(api_request, method_name)
|
self._process_api_request_in_thread(api_request, method_name)
|
||||||
|
|
||||||
def _parse_request_data(
|
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]:
|
) -> tuple[dict[str, Any], dict[str, Any], str]:
|
||||||
"""Parse and validate request data components."""
|
"""Parse and validate request data components."""
|
||||||
header = request_data.get("header", {})
|
header = request_data.get("header", {})
|
||||||
@@ -344,7 +361,9 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _process_api_request_in_thread(
|
def _process_api_request_in_thread(
|
||||||
self, api_request: BackendRequest, method_name: str
|
self,
|
||||||
|
api_request: BackendRequest,
|
||||||
|
method_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Process the API request in a separate thread."""
|
"""Process the API request in a separate thread."""
|
||||||
stop_event = threading.Event()
|
stop_event = threading.Event()
|
||||||
@@ -358,7 +377,7 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Processing {request.method_name} with args {request.args} "
|
f"Processing {request.method_name} with args {request.args} "
|
||||||
f"and header {request.header}"
|
f"and header {request.header}",
|
||||||
)
|
)
|
||||||
self.process_request(request)
|
self.process_request(request)
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ def mock_log_manager() -> Mock:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def http_bridge(
|
def http_bridge(
|
||||||
mock_api: MethodRegistry, mock_log_manager: Mock
|
mock_api: MethodRegistry,
|
||||||
|
mock_log_manager: Mock,
|
||||||
) -> tuple[MethodRegistry, tuple]:
|
) -> tuple[MethodRegistry, tuple]:
|
||||||
"""Create HTTP bridge dependencies for testing."""
|
"""Create HTTP bridge dependencies for testing."""
|
||||||
middleware_chain = (
|
middleware_chain = (
|
||||||
@@ -256,7 +257,9 @@ class TestIntegration:
|
|||||||
"""Integration tests for HTTP API components."""
|
"""Integration tests for HTTP API components."""
|
||||||
|
|
||||||
def test_full_request_flow(
|
def test_full_request_flow(
|
||||||
self, mock_api: MethodRegistry, mock_log_manager: Mock
|
self,
|
||||||
|
mock_api: MethodRegistry,
|
||||||
|
mock_log_manager: Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test complete request flow from server to bridge to middleware."""
|
"""Test complete request flow from server to bridge to middleware."""
|
||||||
server: HttpApiServer = HttpApiServer(
|
server: HttpApiServer = HttpApiServer(
|
||||||
@@ -301,7 +304,9 @@ class TestIntegration:
|
|||||||
server.stop()
|
server.stop()
|
||||||
|
|
||||||
def test_blocking_task(
|
def test_blocking_task(
|
||||||
self, mock_api: MethodRegistry, mock_log_manager: Mock
|
self,
|
||||||
|
mock_api: MethodRegistry,
|
||||||
|
mock_log_manager: Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
shared_threads: dict[str, tasks.WebThread] = {}
|
shared_threads: dict[str, tasks.WebThread] = {}
|
||||||
tasks.BAKEND_THREADS = shared_threads
|
tasks.BAKEND_THREADS = shared_threads
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def _get_lib_names() -> list[str]:
|
|||||||
machine = platform.machine().lower()
|
machine = platform.machine().lower()
|
||||||
|
|
||||||
if system == "windows":
|
if system == "windows":
|
||||||
if machine == "amd64" or machine == "x86_64":
|
if machine in {"amd64", "x86_64"}:
|
||||||
return ["webview.dll", "WebView2Loader.dll"]
|
return ["webview.dll", "WebView2Loader.dll"]
|
||||||
if machine == "arm64":
|
if machine == "arm64":
|
||||||
msg = "arm64 is not supported on Windows"
|
msg = "arm64 is not supported on Windows"
|
||||||
@@ -36,7 +36,6 @@ def _get_lib_names() -> list[str]:
|
|||||||
|
|
||||||
def _be_sure_libraries() -> list[Path] | None:
|
def _be_sure_libraries() -> list[Path] | None:
|
||||||
"""Ensure libraries exist and return paths."""
|
"""Ensure libraries exist and return paths."""
|
||||||
|
|
||||||
lib_dir = os.environ.get("WEBVIEW_LIB_DIR")
|
lib_dir = os.environ.get("WEBVIEW_LIB_DIR")
|
||||||
if not lib_dir:
|
if not lib_dir:
|
||||||
msg = "WEBVIEW_LIB_DIR environment variable is not set"
|
msg = "WEBVIEW_LIB_DIR environment variable is not set"
|
||||||
|
|||||||
@@ -144,7 +144,9 @@ class Webview:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
bridge = WebviewBridge(
|
bridge = WebviewBridge(
|
||||||
webview=self, middleware_chain=tuple(self._middleware), threads={}
|
webview=self,
|
||||||
|
middleware_chain=tuple(self._middleware),
|
||||||
|
threads={},
|
||||||
)
|
)
|
||||||
self._bridge = bridge
|
self._bridge = bridge
|
||||||
|
|
||||||
@@ -154,7 +156,10 @@ class Webview:
|
|||||||
def set_size(self, value: Size) -> None:
|
def set_size(self, value: Size) -> None:
|
||||||
"""Set the webview size (legacy compatibility)."""
|
"""Set the webview size (legacy compatibility)."""
|
||||||
_webview_lib.webview_set_size(
|
_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:
|
def set_title(self, value: str) -> None:
|
||||||
@@ -194,7 +199,10 @@ class Webview:
|
|||||||
|
|
||||||
self._callbacks[name] = c_callback
|
self._callbacks[name] = c_callback
|
||||||
_webview_lib.webview_bind(
|
_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:
|
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:
|
def return_(self, seq: str, status: int, result: str) -> None:
|
||||||
_webview_lib.webview_return(
|
_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:
|
def eval(self, source: str) -> None:
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ class WebviewBridge(ApiBridge):
|
|||||||
def send_api_response(self, response: BackendResponse) -> None:
|
def send_api_response(self, response: BackendResponse) -> None:
|
||||||
"""Send response back to the webview client."""
|
"""Send response back to the webview client."""
|
||||||
serialized = json.dumps(
|
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}")
|
log.debug(f"Sending response: {serialized}")
|
||||||
@@ -40,7 +42,6 @@ class WebviewBridge(ApiBridge):
|
|||||||
arg: int,
|
arg: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle a call from webview's JavaScript bridge."""
|
"""Handle a call from webview's JavaScript bridge."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
op_key = op_key_bytes.decode()
|
op_key = op_key_bytes.decode()
|
||||||
raw_args = json.loads(request_data.decode())
|
raw_args = json.loads(request_data.decode())
|
||||||
@@ -68,7 +69,10 @@ class WebviewBridge(ApiBridge):
|
|||||||
|
|
||||||
# Create API request
|
# Create API request
|
||||||
api_request = BackendRequest(
|
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:
|
except Exception as e:
|
||||||
@@ -77,7 +81,9 @@ class WebviewBridge(ApiBridge):
|
|||||||
)
|
)
|
||||||
log.exception(msg)
|
log.exception(msg)
|
||||||
self.send_api_error_response(
|
self.send_api_error_response(
|
||||||
op_key, str(e), ["webview_bridge", method_name]
|
op_key,
|
||||||
|
str(e),
|
||||||
|
["webview_bridge", method_name],
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ class Command:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def command() -> Iterator[Command]:
|
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"])
|
>>> p = command.run(["some", "daemon"])
|
||||||
>>> print(p.pid)
|
>>> print(p.pid)
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from clan_lib.custom_logger import setup_logging
|
from clan_lib.custom_logger import setup_logging
|
||||||
from clan_lib.nix import nix_shell
|
from clan_lib.nix import nix_shell
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
pytest_plugins = [
|
pytest_plugins = [
|
||||||
"temporary_dir",
|
"temporary_dir",
|
||||||
"root",
|
"root",
|
||||||
|
|||||||
@@ -13,23 +13,17 @@ else:
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def project_root() -> Path:
|
def project_root() -> Path:
|
||||||
"""
|
"""Root directory the clan-cli"""
|
||||||
Root directory the clan-cli
|
|
||||||
"""
|
|
||||||
return PROJECT_ROOT
|
return PROJECT_ROOT
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def test_root() -> Path:
|
def test_root() -> Path:
|
||||||
"""
|
"""Root directory of the tests"""
|
||||||
Root directory of the tests
|
|
||||||
"""
|
|
||||||
return TEST_ROOT
|
return TEST_ROOT
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def clan_core() -> Path:
|
def clan_core() -> Path:
|
||||||
"""
|
"""Directory of the clan-core flake"""
|
||||||
Directory of the clan-core flake
|
|
||||||
"""
|
|
||||||
return CLAN_CORE
|
return CLAN_CORE
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ def app() -> Generator[GtkProc]:
|
|||||||
cmd = [sys.executable, "-m", "clan_app"]
|
cmd = [sys.executable, "-m", "clan_app"]
|
||||||
print(f"Running: {cmd}")
|
print(f"Running: {cmd}")
|
||||||
rapp = Popen(
|
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)
|
yield GtkProc(rapp)
|
||||||
# Cleanup: Terminate your application
|
# Cleanup: Terminate your application
|
||||||
|
|||||||
3
pkgs/clan-app/ui/.gitignore
vendored
@@ -2,4 +2,5 @@ app/api
|
|||||||
app/.fonts
|
app/.fonts
|
||||||
|
|
||||||
.vite
|
.vite
|
||||||
storybook-static
|
storybook-static
|
||||||
|
*.css.d.ts
|
||||||
6223
pkgs/clan-app/ui/api/Inventory.ts
Normal file
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">
|
<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>
|
||||||
<path d="M27 38H6V17H10V13H13.5V9H37.5V13H41V24H27V27H34V31H30.5V34.5H27V38ZM16.5 20.5H20V17H16.5V20.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">
|
<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>
|
||||||
<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">
|
<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>
|
||||||
<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">
|
<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>
|
||||||
<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>
|
|
||||||
|
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">
|
<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>
|
||||||
<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>
|
|
||||||
|
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">
|
<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>
|
||||||
<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>
|
|
||||||
|
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">
|
<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>
|
||||||
<path d="M38 42H10V38H6V10H10V6H38V10H42V38H38V42ZM18 32H30V28H18V32ZM14 28H18V24H14V28ZM30 28H34V24H30V28ZM16 20H20V16H16V20ZM28 20H32V16H28V20Z"/>
|
|
||||||
</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">
|
<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>
|
||||||
<path d="M42 42H14V38H38V14H42V42ZM34 6V34H6V6H34ZM18 18H14V22H18V26H22V22H26V18H22V14H18V18Z"/>
|
|
||||||
</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">
|
<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>
|
||||||
<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>
|
|
||||||
|
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">
|
<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>
|
||||||
<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>
|
|
||||||
|
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">
|
<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>
|
||||||
<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>
|
|
||||||
|
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">
|
<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>
|
||||||
<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>
|
|
||||||
|
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">
|
<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>
|
||||||
<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>
|
|
||||||
|
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 |
1324
pkgs/clan-app/ui/package-lock.json
generated
@@ -57,7 +57,9 @@
|
|||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"typescript-eslint": "^8.32.1",
|
"typescript-eslint": "^8.32.1",
|
||||||
|
"typescript-plugin-css-modules": "^5.2.0",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
|
"vite-css-modules": "^1.10.0",
|
||||||
"vite-plugin-solid": "^2.8.2",
|
"vite-plugin-solid": "^2.8.2",
|
||||||
"vite-plugin-solid-svg": "^0.8.1",
|
"vite-plugin-solid-svg": "^0.8.1",
|
||||||
"vitest": "^3.2.3",
|
"vitest": "^3.2.3",
|
||||||
@@ -70,9 +72,11 @@
|
|||||||
"@modular-forms/solid": "^0.25.1",
|
"@modular-forms/solid": "^0.25.1",
|
||||||
"@solid-primitives/storage": "^4.3.2",
|
"@solid-primitives/storage": "^4.3.2",
|
||||||
"@solidjs/router": "^0.15.3",
|
"@solidjs/router": "^0.15.3",
|
||||||
"@tanstack/eslint-plugin-query": "^5.51.12",
|
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||||
"@tanstack/solid-query": "^5.76.0",
|
"@tanstack/solid-query": "^5.85.5",
|
||||||
"@tanstack/solid-query-devtools": "^5.83.0",
|
"@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-js": "^1.9.7",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
"three": "^0.176.0",
|
"three": "^0.176.0",
|
||||||
|
|||||||
@@ -1,30 +1,21 @@
|
|||||||
div.alert {
|
.alert {
|
||||||
@apply flex flex-row gap-2.5 p-4 rounded-md items-start;
|
@apply flex flex-row gap-2.5 p-4 rounded-md items-start;
|
||||||
|
|
||||||
&.has-icon {
|
&.hasIcon {
|
||||||
@apply pl-3;
|
@apply pl-3;
|
||||||
|
|
||||||
svg.icon {
|
|
||||||
@apply relative top-0.5;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-dismiss {
|
&.hasIcon svg.icon {
|
||||||
@apply pr-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > button.dismiss-trigger {
|
|
||||||
@apply relative top-0.5;
|
@apply relative top-0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div.content {
|
&.hasDismiss {
|
||||||
@apply flex flex-col size-full gap-1;
|
@apply pr-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.info {
|
&.info {
|
||||||
@apply bg-semantic-info-1 border border-semantic-info-3 fg-semantic-info-3;
|
@apply bg-semantic-info-1 border border-semantic-info-3 fg-semantic-info-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
@apply bg-semantic-error-2 border border-semantic-error-3 fg-semantic-error-3;
|
@apply bg-semantic-error-2 border border-semantic-error-3 fg-semantic-error-3;
|
||||||
}
|
}
|
||||||
@@ -38,6 +29,18 @@ div.alert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.transparent {
|
&.transparent {
|
||||||
@apply bg-transparent border-none p-0;
|
@apply bg-transparent border-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.noPadding {
|
||||||
|
@apply p-0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alertContent {
|
||||||
|
@apply flex flex-col size-full gap-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dismissTrigger {
|
||||||
|
@apply relative top-0.5;
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import "./Alert.css";
|
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
|
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
|
||||||
import { Typography } from "@/src/components/Typography/Typography";
|
import { Typography } from "@/src/components/Typography/Typography";
|
||||||
import { Button } from "@kobalte/core/button";
|
import { Button } from "@kobalte/core/button";
|
||||||
import { Alert as KAlert } from "@kobalte/core/alert";
|
import { Alert as KAlert } from "@kobalte/core/alert";
|
||||||
import { Show } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
|
import styles from "./Alert.module.css";
|
||||||
|
|
||||||
export interface AlertProps {
|
export interface AlertProps {
|
||||||
icon?: IconVariant;
|
icon?: IconVariant;
|
||||||
@@ -13,6 +13,7 @@ export interface AlertProps {
|
|||||||
title: string;
|
title: string;
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
|
dense?: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,16 +25,17 @@ export const Alert = (props: AlertProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<KAlert
|
<KAlert
|
||||||
class={cx("alert", props.type, {
|
class={cx(styles.alert, styles[props.type], {
|
||||||
"has-icon": props.icon,
|
[styles.hasIcon]: props.icon,
|
||||||
"has-dismiss": props.onDismiss,
|
[styles.hasDismiss]: props.onDismiss,
|
||||||
transparent: props.transparent,
|
[styles.transparent]: props.transparent,
|
||||||
|
[styles.noPadding]: props.dense,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{props.icon && (
|
{props.icon && (
|
||||||
<Icon icon={props.icon} color="inherit" size={iconSize()} />
|
<Icon icon={props.icon} color="inherit" size={iconSize()} />
|
||||||
)}
|
)}
|
||||||
<div class="content">
|
<div class={styles.alertContent}>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
family="condensed"
|
family="condensed"
|
||||||
@@ -57,7 +59,7 @@ export const Alert = (props: AlertProps) => {
|
|||||||
{props.onDismiss && (
|
{props.onDismiss && (
|
||||||
<Button
|
<Button
|
||||||
name="dismiss-alert"
|
name="dismiss-alert"
|
||||||
class="dismiss-trigger"
|
class={styles.dismissTrigger}
|
||||||
onClick={props.onDismiss}
|
onClick={props.onDismiss}
|
||||||
aria-label={`Dismiss ${props.type} alert`}
|
aria-label={`Dismiss ${props.type} alert`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
.button {
|
.button {
|
||||||
@apply flex gap-2 shrink-0 items-center justify-center;
|
@apply flex gap-2 shrink-0 items-center justify-center;
|
||||||
@apply px-4 py-2;
|
@apply h-[2.125rem] px-4 py-2 rounded-[0.1875rem];
|
||||||
|
|
||||||
height: theme(height.9);
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
/* Add transition for smooth width animation */
|
/* Add transition for smooth width animation */
|
||||||
transition: width 0.5s ease 0.1s;
|
transition: width 0.5s ease 0.1s;
|
||||||
|
|
||||||
&.s {
|
&.s {
|
||||||
@apply px-3 py-1.5;
|
@apply h-[1.625rem] px-3 py-1.5 rounded-[0.125rem];
|
||||||
height: theme(height.7);
|
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
&:has(> .icon-start):has(> .label) {
|
&:has(> .icon-start):has(> .label) {
|
||||||
@apply pl-2;
|
@apply pl-2;
|
||||||
@@ -22,6 +17,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.xs {
|
||||||
|
@apply h-[1.125rem] gap-0.5 p-2 rounded-[0.125rem];
|
||||||
|
|
||||||
|
&:has(> .icon-start):has(> .label) {
|
||||||
|
@apply pl-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(> .icon-end):has(> .label) {
|
||||||
|
@apply pr-1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
@apply bg-inv-acc-4 fg-inv-1;
|
@apply bg-inv-acc-4 fg-inv-1;
|
||||||
@apply border border-solid border-inv-4;
|
@apply border border-solid border-inv-4;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
|
|||||||
|
|
||||||
const ButtonExamples: Component<ButtonProps> = (props) => (
|
const ButtonExamples: Component<ButtonProps> = (props) => (
|
||||||
<>
|
<>
|
||||||
<div class="grid w-fit grid-cols-4 gap-8">
|
<div class="grid w-fit grid-cols-6 gap-8">
|
||||||
<div>
|
<div>
|
||||||
<Button data-testid="default" {...props}>
|
<Button data-testid="default" {...props}>
|
||||||
Label
|
Label
|
||||||
@@ -19,6 +19,11 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
|||||||
Label
|
Label
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button data-testid="xsmall" size="xs" {...props}>
|
||||||
|
Label
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button data-testid="default-disabled" {...props} disabled={true}>
|
<Button data-testid="default-disabled" {...props} disabled={true}>
|
||||||
Disabled
|
Disabled
|
||||||
@@ -35,6 +40,17 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
data-testid="xsmall-disabled"
|
||||||
|
{...props}
|
||||||
|
disabled={true}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button data-testid="default-start-icon" {...props} startIcon="Flash">
|
<Button data-testid="default-start-icon" {...props} startIcon="Flash">
|
||||||
Label
|
Label
|
||||||
@@ -50,6 +66,16 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
|||||||
Label
|
Label
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
data-testid="xsmall-start-icon"
|
||||||
|
{...props}
|
||||||
|
startIcon="Flash"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Label
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
data-testid="default-disabled-start-icon"
|
data-testid="default-disabled-start-icon"
|
||||||
@@ -72,6 +98,18 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
data-testid="xsmall-disabled-start-icon"
|
||||||
|
{...props}
|
||||||
|
startIcon="Flash"
|
||||||
|
size="xs"
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button data-testid="default-end-icon" {...props} endIcon="Flash">
|
<Button data-testid="default-end-icon" {...props} endIcon="Flash">
|
||||||
Label
|
Label
|
||||||
@@ -87,6 +125,16 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
|||||||
Label
|
Label
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
data-testid="xsmall-end-icon"
|
||||||
|
{...props}
|
||||||
|
endIcon="Flash"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Label
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
data-testid="default-disabled-end-icon"
|
data-testid="default-disabled-end-icon"
|
||||||
@@ -108,12 +156,27 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
|||||||
Disabled
|
Disabled
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
data-testid="xsmall-disabled-end-icon"
|
||||||
|
{...props}
|
||||||
|
endIcon="Flash"
|
||||||
|
size="xs"
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button data-testid="default-icon" {...props} icon="Flash" />
|
<Button data-testid="default-icon" {...props} icon="Flash" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button data-testid="small-icon" {...props} icon="Flash" size="s" />
|
<Button data-testid="small-icon" {...props} icon="Flash" size="s" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button data-testid="xsmall-icon" {...props} icon="Flash" size="xs" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
data-testid="default-disabled-icon"
|
data-testid="default-disabled-icon"
|
||||||
@@ -131,6 +194,15 @@ const ButtonExamples: Component<ButtonProps> = (props) => (
|
|||||||
size="s"
|
size="s"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
data-testid="xsmall-disabled-icon"
|
||||||
|
{...props}
|
||||||
|
icon="Flash"
|
||||||
|
disabled={true}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import "./Button.css";
|
|||||||
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
|
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
|
||||||
import { Loader } from "@/src/components/Loader/Loader";
|
import { Loader } from "@/src/components/Loader/Loader";
|
||||||
|
|
||||||
export type Size = "default" | "s";
|
export type Size = "default" | "s" | "xs";
|
||||||
export type Hierarchy = "primary" | "secondary";
|
export type Hierarchy = "primary" | "secondary";
|
||||||
|
|
||||||
export type Action = () => Promise<void>;
|
export type Action = () => Promise<void>;
|
||||||
@@ -28,6 +28,7 @@ export interface ButtonProps
|
|||||||
const iconSizes: Record<Size, string> = {
|
const iconSizes: Record<Size, string> = {
|
||||||
default: "1rem",
|
default: "1rem",
|
||||||
s: "0.8125rem",
|
s: "0.8125rem",
|
||||||
|
xs: "0.625rem",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Button = (props: ButtonProps) => {
|
export const Button = (props: ButtonProps) => {
|
||||||
|
|||||||
@@ -5,3 +5,14 @@
|
|||||||
.horizontal_button {
|
.horizontal_button {
|
||||||
@apply grow max-w-[18rem];
|
@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() && (
|
{value() && (
|
||||||
<Tooltip placement="top">
|
<Tooltip placement="top">
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
<Tooltip.Content class="tooltip-content">
|
<Tooltip.Content class={styles.tooltipContent}>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export interface LabelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Label = (props: LabelProps) => {
|
export const Label = (props: LabelProps) => {
|
||||||
const descriptionSize = () => (props.size == "default" ? "xs" : "xxs");
|
const descriptionSize = () => (props.size == "default" ? "s" : "xs");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.label}>
|
<Show when={props.label}>
|
||||||
@@ -55,23 +55,14 @@ export const Label = (props: LabelProps) => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
placement="top"
|
placement="top"
|
||||||
inverted={props.inverted}
|
inverted={props.inverted}
|
||||||
trigger={
|
description={props.tooltip}
|
||||||
<Icon
|
|
||||||
icon="Info"
|
|
||||||
color="tertiary"
|
|
||||||
inverted={props.inverted}
|
|
||||||
size={props.size == "default" ? "0.85em" : "0.75rem"}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Typography
|
<Icon
|
||||||
hierarchy="body"
|
icon="Info"
|
||||||
size="xs"
|
color="tertiary"
|
||||||
weight="medium"
|
inverted={props.inverted}
|
||||||
inverted={!props.inverted}
|
size={props.size == "default" ? "0.85em" : "0.75rem"}
|
||||||
>
|
/>
|
||||||
{props.tooltip}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</props.labelComponent>
|
</props.labelComponent>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ div.form-field.machine-tags {
|
|||||||
@apply bg-def-acc-1 outline-def-acc-2;
|
@apply bg-def-acc-1 outline-def-acc-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:not(:read-only):focus-visible {
|
||||||
@apply bg-def-1 outline-def-acc-3;
|
@apply bg-def-1 outline-def-acc-3;
|
||||||
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -106,7 +106,7 @@ div.form-field.machine-tags {
|
|||||||
@apply bg-inv-acc-2 outline-inv-acc-2;
|
@apply bg-inv-acc-2 outline-inv-acc-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:not(:read-only):focus-visible {
|
||||||
@apply bg-inv-acc-4;
|
@apply bg-inv-acc-4;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 0.125rem theme(colors.bg.inv.1),
|
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;
|
@apply bg-def-acc-1 outline-def-acc-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:not(:read-only):focus-visible {
|
||||||
@apply bg-def-1 outline-def-acc-3;
|
@apply bg-def-1 outline-def-acc-3;
|
||||||
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -119,7 +119,7 @@ div.form-field {
|
|||||||
@apply bg-inv-acc-2 outline-inv-acc-2;
|
@apply bg-inv-acc-2 outline-inv-acc-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:not(:read-only):focus-visible {
|
||||||
@apply bg-inv-acc-4;
|
@apply bg-inv-acc-4;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 0.125rem theme(colors.bg.inv.1),
|
0 0 0 0.125rem theme(colors.bg.inv.1),
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { Component, JSX, splitProps } from "solid-js";
|
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 ArrowBottom from "@/icons/arrow-bottom.svg";
|
||||||
import ArrowLeft from "@/icons/arrow-left.svg";
|
import ArrowLeft from "@/icons/arrow-left.svg";
|
||||||
import ArrowRight from "@/icons/arrow-right.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 CaretRight from "@/icons/caret-right.svg";
|
||||||
import CaretUp from "@/icons/caret-up.svg";
|
import CaretUp from "@/icons/caret-up.svg";
|
||||||
import Checkmark from "@/icons/checkmark.svg";
|
import Checkmark from "@/icons/checkmark.svg";
|
||||||
|
import CheckSolid from "@/icons/check-solid.svg";
|
||||||
import ClanIcon from "@/icons/clan-icon.svg";
|
import ClanIcon from "@/icons/clan-icon.svg";
|
||||||
import Cursor from "@/icons/cursor.svg";
|
|
||||||
import Close from "@/icons/close.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 Download from "@/icons/download.svg";
|
||||||
import Edit from "@/icons/edit.svg";
|
import Edit from "@/icons/edit.svg";
|
||||||
import Expand from "@/icons/expand.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 Filter from "@/icons/filter.svg";
|
||||||
import Flash from "@/icons/flash.svg";
|
import Flash from "@/icons/flash.svg";
|
||||||
import Folder from "@/icons/folder.svg";
|
import Folder from "@/icons/folder.svg";
|
||||||
|
import General from "@/icons/general.svg";
|
||||||
import Grid from "@/icons/grid.svg";
|
import Grid from "@/icons/grid.svg";
|
||||||
|
import Heart from "@/icons/heart.svg";
|
||||||
import Info from "@/icons/info.svg";
|
import Info from "@/icons/info.svg";
|
||||||
import List from "@/icons/list.svg";
|
import List from "@/icons/list.svg";
|
||||||
import Load from "@/icons/load.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 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 Paperclip from "@/icons/paperclip.svg";
|
||||||
import Plus from "@/icons/plus.svg";
|
import Plus from "@/icons/plus.svg";
|
||||||
import Reload from "@/icons/reload.svg";
|
import Reload from "@/icons/reload.svg";
|
||||||
import Report from "@/icons/report.svg";
|
import Report from "@/icons/report.svg";
|
||||||
import Search from "@/icons/search.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 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 Switch from "@/icons/switch.svg";
|
||||||
import Tag from "@/icons/tag.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 { Dynamic } from "solid-js/web";
|
||||||
|
|
||||||
import { Color, fgClass } from "../colors";
|
import { Color, fgClass } from "../colors";
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
|
Address,
|
||||||
AI,
|
AI,
|
||||||
ArrowBottom,
|
ArrowBottom,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -61,8 +71,11 @@ const icons = {
|
|||||||
CaretRight,
|
CaretRight,
|
||||||
CaretUp,
|
CaretUp,
|
||||||
Checkmark,
|
Checkmark,
|
||||||
|
CheckSolid,
|
||||||
ClanIcon,
|
ClanIcon,
|
||||||
Close,
|
Close,
|
||||||
|
CloseCircle,
|
||||||
|
Code,
|
||||||
Cursor,
|
Cursor,
|
||||||
Download,
|
Download,
|
||||||
Edit,
|
Edit,
|
||||||
@@ -72,12 +85,14 @@ const icons = {
|
|||||||
Filter,
|
Filter,
|
||||||
Flash,
|
Flash,
|
||||||
Folder,
|
Folder,
|
||||||
|
General,
|
||||||
Grid,
|
Grid,
|
||||||
Heart,
|
Heart,
|
||||||
Info,
|
Info,
|
||||||
List,
|
List,
|
||||||
Load,
|
Load,
|
||||||
Machine,
|
Machine,
|
||||||
|
Minimize,
|
||||||
Modules,
|
Modules,
|
||||||
More,
|
More,
|
||||||
NewMachine,
|
NewMachine,
|
||||||
@@ -88,6 +103,7 @@ const icons = {
|
|||||||
Report,
|
Report,
|
||||||
Search,
|
Search,
|
||||||
SearchFilled,
|
SearchFilled,
|
||||||
|
Services,
|
||||||
Settings,
|
Settings,
|
||||||
Switch,
|
Switch,
|
||||||
Tag,
|
Tag,
|
||||||
@@ -101,8 +117,6 @@ export type IconVariant = keyof typeof icons;
|
|||||||
|
|
||||||
const viewBoxes: Partial<Record<IconVariant, string>> = {
|
const viewBoxes: Partial<Record<IconVariant, string>> = {
|
||||||
ClanIcon: "0 0 72 89",
|
ClanIcon: "0 0 72 89",
|
||||||
Offline: "0 0 38 27",
|
|
||||||
Cursor: "0 0 35 42",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
span.machine-status {
|
span.machine-status {
|
||||||
@apply flex items-center gap-1;
|
@apply flex items-center gap-1.5;
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
@apply w-1.5 h-1.5 rounded-full m-1.5;
|
@apply w-1.5 h-1.5 rounded-full m-1.5;
|
||||||
@@ -13,7 +13,7 @@ span.machine-status {
|
|||||||
background-color: theme(colors.fg.semantic.error.1);
|
background-color: theme(colors.fg.semantic.error.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.installed > .indicator {
|
&.out-of-sync > .indicator {
|
||||||
background-color: theme(colors.fg.inv.3);
|
background-color: theme(colors.fg.inv.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,27 +20,38 @@ export default meta;
|
|||||||
|
|
||||||
type Story = StoryObj<MachineStatusProps>;
|
type Story = StoryObj<MachineStatusProps>;
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
|
|
||||||
export const Online: Story = {
|
export const Online: Story = {
|
||||||
args: {
|
args: {
|
||||||
status: "Online",
|
status: "online",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Offline: Story = {
|
export const Offline: Story = {
|
||||||
args: {
|
args: {
|
||||||
status: "Offline",
|
status: "offline",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Installed: Story = {
|
export const OutOfSync: Story = {
|
||||||
args: {
|
args: {
|
||||||
status: "Installed",
|
status: "out_of_sync",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotInstalled: Story = {
|
export const NotInstalled: Story = {
|
||||||
args: {
|
args: {
|
||||||
status: "Not Installed",
|
status: "not_installed",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LoadingWithLabel: Story = {
|
||||||
|
args: {
|
||||||
|
...Loading.args,
|
||||||
|
label: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,7 +71,7 @@ export const OfflineWithLabel: Story = {
|
|||||||
|
|
||||||
export const InstalledWithLabel: Story = {
|
export const InstalledWithLabel: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Installed.args,
|
...OutOfSync.args,
|
||||||
label: true,
|
label: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||