Compare commits
134 Commits
vars-new
...
ui-version
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f9ab3de19 | ||
|
|
9739a5ae2b | ||
|
|
54446d751f | ||
|
|
7bc8e091a5 | ||
|
|
3462d458ac | ||
|
|
bd42d67b0c | ||
|
|
d99ca36f9f | ||
|
|
57f9cd9eee | ||
|
|
a9ec94b0df | ||
|
|
c64dbceceb | ||
|
|
5d924e0c98 | ||
|
|
6a6688019b | ||
|
|
f33172fa73 | ||
|
|
00914311a4 | ||
|
|
ceeb40d9ac | ||
|
|
afab33056e | ||
|
|
a5183f4b4c | ||
|
|
a686d7523b | ||
|
|
56b784992d | ||
|
|
5f723dc376 | ||
|
|
1609989734 | ||
|
|
0c07d5cfe0 | ||
|
|
9c37ef4cbe | ||
|
|
783b6a8b06 | ||
|
|
4f13049ee2 | ||
|
|
2f4f303048 | ||
|
|
d02868b950 | ||
|
|
4f7d82671f | ||
|
|
0dce3fc7ec | ||
|
|
a635f9c6fe | ||
|
|
a8ed1c30e4 | ||
|
|
c0c41d52bd | ||
|
|
bb236bb543 | ||
|
|
d7cf79faa7 | ||
|
|
dab11cb020 | ||
|
|
f2cb6fef41 | ||
|
|
655b87ad04 | ||
|
|
d462ae501e | ||
|
|
59a8c402ba | ||
|
|
3b309ea74b | ||
|
|
508cd3c784 | ||
|
|
2bff7403df | ||
|
|
b5a6e809d0 | ||
|
|
ec28c5c307 | ||
|
|
10f9e5d11b | ||
|
|
b8ba8b79ca | ||
|
|
fd07d02d2d | ||
|
|
2a3d1efc6f | ||
|
|
947e0a5488 | ||
|
|
57b5520143 | ||
|
|
9fd1031f4d | ||
|
|
c382e8f1f3 | ||
|
|
cf92303f31 | ||
|
|
80d0dc9805 | ||
|
|
4e2cbb188c | ||
|
|
eb6460fb40 | ||
|
|
155bd36d2b | ||
|
|
40ea5bf591 | ||
|
|
0cd9c84de0 | ||
|
|
e1ea44a2cc | ||
|
|
7c4865e8b0 | ||
|
|
b032cd4a29 | ||
|
|
61edc1e06f | ||
|
|
c369f3b5d1 | ||
|
|
0cc1f072f7 | ||
|
|
a2a011a47f | ||
|
|
e1796e19e4 | ||
|
|
972adc7a7c | ||
|
|
e1b4f296e3 | ||
|
|
1cb2156d87 | ||
|
|
84703fa293 | ||
|
|
0e10122d54 | ||
|
|
ecd731024c | ||
|
|
e0da575201 | ||
|
|
3577c689bd | ||
|
|
885103bfa4 | ||
|
|
afc1ca37bd | ||
|
|
4aa536a1bf | ||
|
|
c61dfbf8dd | ||
|
|
e6785fa1d0 | ||
|
|
89ea01fd04 | ||
|
|
a8a08e21e4 | ||
|
|
700f571598 | ||
|
|
08c15b3d9b | ||
|
|
2848b6d5d6 | ||
|
|
ddc1059799 | ||
|
|
b690515dd7 | ||
|
|
e9cef9c7c1 | ||
|
|
ca69864a20 | ||
|
|
5436f284fb | ||
|
|
00df032635 | ||
|
|
a2c016718a | ||
|
|
d1abebf068 | ||
|
|
9635fb03b7 | ||
|
|
f48c596617 | ||
|
|
0589c71601 | ||
|
|
a2c2d73e49 | ||
|
|
99b22dfcbf | ||
|
|
cd04686663 | ||
|
|
2b3e847c28 | ||
|
|
d0ec4fd8e6 | ||
|
|
bb5c523ac8 | ||
|
|
4df4f5220b | ||
|
|
a082fd2ed9 | ||
|
|
3161c10aa8 | ||
|
|
7ad8ed1af0 | ||
|
|
94919dc9b8 | ||
|
|
1502cfa4a7 | ||
|
|
cce0207225 | ||
|
|
38f98645ac | ||
|
|
74d2ae0619 | ||
|
|
c122201ff2 | ||
|
|
e72795904d | ||
|
|
32ddb4ffa7 | ||
|
|
db6220b57b | ||
|
|
e929f36f80 | ||
|
|
f71460c4f9 | ||
|
|
8302f3ffde | ||
|
|
bd82de6001 | ||
|
|
06613de825 | ||
|
|
76af63ee1c | ||
|
|
3baa43fd87 | ||
|
|
a6b8ca06ab | ||
|
|
f7faf2cd63 | ||
|
|
bff3908bb1 | ||
|
|
d0613b4030 | ||
|
|
52b711667e | ||
|
|
13d6db98d1 | ||
|
|
195134dd5e | ||
|
|
0670f0ad32 | ||
|
|
daf843eeab | ||
|
|
291b742fd7 | ||
|
|
f7d6c23aaa | ||
|
|
1f26135381 |
75
.gitea/workflows/create-pr.sh
Executable file
75
.gitea/workflows/create-pr.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared script for creating pull requests in Gitea workflows
|
||||
set -euo pipefail
|
||||
|
||||
# Required environment variables:
|
||||
# - CI_BOT_TOKEN: Gitea bot token for authentication
|
||||
# - PR_BRANCH: Branch name for the pull request
|
||||
# - PR_TITLE: Title of the pull request
|
||||
# - PR_BODY: Body/description of the pull request
|
||||
|
||||
if [[ -z "${CI_BOT_TOKEN:-}" ]]; then
|
||||
echo "Error: CI_BOT_TOKEN is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${PR_BRANCH:-}" ]]; then
|
||||
echo "Error: PR_BRANCH is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${PR_TITLE:-}" ]]; then
|
||||
echo "Error: PR_TITLE is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${PR_BODY:-}" ]]; then
|
||||
echo "Error: PR_BODY is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Push the branch
|
||||
git push origin "+HEAD:${PR_BRANCH}"
|
||||
|
||||
# Create pull request
|
||||
resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
|
||||
-H "Authorization: token $CI_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"head\": \"${PR_BRANCH}\",
|
||||
\"base\": \"main\",
|
||||
\"title\": \"${PR_TITLE}\",
|
||||
\"body\": \"${PR_BODY}\"
|
||||
}" \
|
||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
|
||||
|
||||
pr_number=$(echo "$resp" | jq -r '.number')
|
||||
|
||||
if [[ "$pr_number" == "null" ]]; then
|
||||
echo "Error creating pull request:" >&2
|
||||
echo "$resp" | jq . >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Created pull request #$pr_number"
|
||||
|
||||
# Merge when checks succeed
|
||||
while true; do
|
||||
resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
|
||||
-H "Authorization: token $CI_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Do": "merge",
|
||||
"merge_when_checks_succeed": true,
|
||||
"delete_branch_after_merge": true
|
||||
}' \
|
||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
|
||||
msg=$(echo "$resp" | jq -r '.message')
|
||||
if [[ "$msg" != "Please try again later" ]]; then
|
||||
break
|
||||
fi
|
||||
echo "Retrying in 2 seconds..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Pull request #$pr_number merge initiated"
|
||||
@@ -19,35 +19,10 @@ jobs:
|
||||
run: |
|
||||
export GIT_AUTHOR_NAME=clan-bot GIT_AUTHOR_EMAIL=clan-bot@clan.lol GIT_COMMITTER_NAME=clan-bot GIT_COMMITTER_EMAIL=clan-bot@clan.lol
|
||||
git commit -am "Update pinned clan-core for checks"
|
||||
git push origin +HEAD:update-clan-core-for-checks
|
||||
set -x
|
||||
resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
|
||||
-H "Authorization: token $CI_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"head": "update-clan-core-for-checks",
|
||||
"base": "main",
|
||||
"title": "Update Clan Core for Checks",
|
||||
"body": "This PR updates the pinned clan-core flake input that is used for checks."
|
||||
}' \
|
||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
|
||||
pr_number=$(echo "$resp" | jq -r '.number')
|
||||
|
||||
# Merge when succeed
|
||||
while true; do
|
||||
resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
|
||||
-H "Authorization: token $CI_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Do": "merge",
|
||||
"merge_when_checks_succeed": true,
|
||||
"delete_branch_after_merge": true
|
||||
}' \
|
||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
|
||||
msg=$(echo $resp | jq -r '.message')
|
||||
if [[ "$msg" != "Please try again later" ]]; then
|
||||
break
|
||||
fi
|
||||
echo "Retrying in 2 seconds..."
|
||||
sleep 2
|
||||
done
|
||||
# Use shared PR creation script
|
||||
export PR_BRANCH="update-clan-core-for-checks"
|
||||
export PR_TITLE="Update Clan Core for Checks"
|
||||
export PR_BODY="This PR updates the pinned clan-core flake input that is used for checks."
|
||||
|
||||
./.gitea/workflows/create-pr.sh
|
||||
|
||||
40
.gitea/workflows/update-private-flake-inputs.yml
Normal file
40
.gitea/workflows/update-private-flake-inputs.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
name: "Update private flake inputs"
|
||||
on:
|
||||
repository_dispatch:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 3 * * *" # Run daily at 3 AM
|
||||
jobs:
|
||||
update-private-flake:
|
||||
runs-on: nix
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Update private flake inputs
|
||||
run: |
|
||||
# Update the private flake lock file
|
||||
cd devFlake/private
|
||||
nix flake update
|
||||
cd ../..
|
||||
|
||||
# Update the narHash
|
||||
bash ./devFlake/update-private-narhash
|
||||
- name: Create pull request
|
||||
env:
|
||||
CI_BOT_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
|
||||
run: |
|
||||
export GIT_AUTHOR_NAME=clan-bot GIT_AUTHOR_EMAIL=clan-bot@clan.lol GIT_COMMITTER_NAME=clan-bot GIT_COMMITTER_EMAIL=clan-bot@clan.lol
|
||||
|
||||
# Check if there are any changes
|
||||
if ! git diff --quiet; then
|
||||
git add devFlake/private/flake.lock devFlake/private.narHash
|
||||
git commit -m "Update dev flake"
|
||||
|
||||
# Use shared PR creation script
|
||||
export PR_BRANCH="update-dev-flake"
|
||||
export PR_TITLE="Update dev flake"
|
||||
export PR_BODY="This PR updates the dev flake inputs and corresponding narHash."
|
||||
else
|
||||
echo "No changes detected in dev flake inputs"
|
||||
fi
|
||||
@@ -19,10 +19,11 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
dependencies = [
|
||||
self
|
||||
pkgs.stdenv.drvPath
|
||||
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
|
||||
dependencies =
|
||||
[
|
||||
pkgs.stdenv.drvPath
|
||||
]
|
||||
++ builtins.map (i: i.outPath) (builtins.attrValues (builtins.removeAttrs self.inputs [ "self" ]));
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
in
|
||||
{
|
||||
|
||||
@@ -47,14 +47,6 @@ nixosLib.runTest (
|
||||
|
||||
clientone =
|
||||
{ config, pkgs, ... }:
|
||||
let
|
||||
dependencies = [
|
||||
clan-core
|
||||
pkgs.stdenv.drvPath
|
||||
] ++ builtins.map (i: i.outPath) (builtins.attrValues clan-core.inputs);
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
|
||||
in
|
||||
{
|
||||
|
||||
services.openssh.enable = true;
|
||||
@@ -65,15 +57,6 @@ nixosLib.runTest (
|
||||
|
||||
environment.systemPackages = [ clan-core.packages.${pkgs.system}.clan-cli ];
|
||||
|
||||
environment.etc.install-closure.source = "${closureInfo}/store-paths";
|
||||
nix.settings = {
|
||||
substituters = pkgs.lib.mkForce [ ];
|
||||
hashed-mirrors = null;
|
||||
connect-timeout = pkgs.lib.mkForce 3;
|
||||
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
||||
};
|
||||
system.extraDependencies = dependencies;
|
||||
|
||||
clan.core.state.test-backups.folders = [ "/var/test-backups" ];
|
||||
};
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ in
|
||||
pkgs.xkcdpass
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -f "$out"/borgbackup.ssh
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/borgbackup.ssh
|
||||
xkcdpass -n 4 -d - > "$out"/borgbackup.repokey
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ The importer module allows users to configure importing modules in a flexible an
|
||||
|
||||
It exposes the `extraModules` functionality of the inventory, without any added configuration.
|
||||
|
||||
## Usage:
|
||||
## Usage
|
||||
|
||||
```nix
|
||||
inventory.services = {
|
||||
|
||||
@@ -54,7 +54,7 @@ in
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -f "$out"/ssh.id_ed25519
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/ssh.id_ed25519
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -74,7 +74,7 @@ in
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t rsa -b 4096 -N "" -f "$out"/ssh.id_rsa
|
||||
ssh-keygen -t rsa -b 4096 -N "" -C "" -f "$out"/ssh.id_rsa
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -f "$out"/id_ed25519
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
pkgs.xkcdpass
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -f "$out"/borgbackup.ssh
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/borgbackup.ssh
|
||||
xkcdpass -n 4 -d - > "$out"/borgbackup.repokey
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -41,14 +41,6 @@
|
||||
clan-core,
|
||||
...
|
||||
}:
|
||||
let
|
||||
dependencies = [
|
||||
clan-core
|
||||
pkgs.stdenv.drvPath
|
||||
] ++ builtins.map (i: i.outPath) (builtins.attrValues clan-core.inputs);
|
||||
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
|
||||
|
||||
in
|
||||
{
|
||||
|
||||
services.openssh.enable = true;
|
||||
@@ -59,15 +51,6 @@
|
||||
|
||||
environment.systemPackages = [ clan-core.packages.${pkgs.system}.clan-cli ];
|
||||
|
||||
environment.etc.install-closure.source = "${closureInfo}/store-paths";
|
||||
nix.settings = {
|
||||
substituters = pkgs.lib.mkForce [ ];
|
||||
hashed-mirrors = null;
|
||||
connect-timeout = pkgs.lib.mkForce 3;
|
||||
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
||||
};
|
||||
system.extraDependencies = dependencies;
|
||||
|
||||
clan.core.state.test-backups.folders = [ "/var/test-backups" ];
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,13 @@ in
|
||||
unit-test-module = (
|
||||
self.clanLib.test.flakeModules.makeEvalChecks {
|
||||
inherit module;
|
||||
inherit self inputs;
|
||||
inherit inputs;
|
||||
fileset = lib.fileset.unions [
|
||||
# The hello-world service being tested
|
||||
../../clanServices/hello-world
|
||||
# Required modules
|
||||
../../nixosModules/clanCore
|
||||
];
|
||||
testName = "hello-world";
|
||||
tests = ./tests/eval-tests.nix;
|
||||
# Optional arguments passed to the test
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
The importer module allows users to configure importing modules in a flexible and structured way.
|
||||
It exposes the `extraModules` functionality of the inventory, without any added configuration.
|
||||
|
||||
## Usage:
|
||||
## Usage
|
||||
|
||||
```nix
|
||||
inventory.instances = {
|
||||
|
||||
36
clanServices/sshd/README.md
Normal file
36
clanServices/sshd/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
The `sshd` Clan service manages SSH to make it easy to securely access your machines over the internet. The service uses `vars` to store the SSH host keys for each machine to ensure they remain stable across deployments.
|
||||
|
||||
`sshd` also generates SSH certificates for both servers and clients allowing for certificate-based authentication for SSH.
|
||||
|
||||
The service also disables password-based authentication over SSH, to access your machines you'll need to use public key authentication or certificate-based authentication.
|
||||
|
||||
## Usage
|
||||
|
||||
```nix
|
||||
{
|
||||
inventory.instances = {
|
||||
# By default this service only generates ed25519 host keys
|
||||
sshd-basic = {
|
||||
module = {
|
||||
name = "sshd";
|
||||
input = "clan-core";
|
||||
};
|
||||
roles.server.tags.all = { };
|
||||
roles.client.tags.all = { };
|
||||
};
|
||||
|
||||
# Also generate RSA host keys for all servers
|
||||
sshd-with-rsa = {
|
||||
module = {
|
||||
name = "sshd";
|
||||
input = "clan-core";
|
||||
};
|
||||
roles.server.tags.all = { };
|
||||
roles.server.settings = {
|
||||
hostKeys.rsa.enable = true;
|
||||
};
|
||||
roles.client.tags.all = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/sshd";
|
||||
manifest.description = "Enables secure remote access to the machine over ssh.";
|
||||
manifest.description = "Enables secure remote access to the machine over SSH";
|
||||
manifest.categories = [
|
||||
"System"
|
||||
"Network"
|
||||
@@ -49,7 +49,7 @@
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -f "$out"/id_ed25519
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -f "$out"/id_ed25519
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t rsa -b 4096 -N "" -f "$out"/ssh.id_rsa
|
||||
ssh-keygen -t rsa -b 4096 -N "" -C "" -f "$out"/ssh.id_rsa
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -f "$out"/ssh.id_ed25519
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/ssh.id_ed25519
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
## Usage
|
||||
|
||||
```
|
||||
inventory.instances = {
|
||||
|
||||
# Deploy user alice on all machines. Don't prompt for password (will be
|
||||
# auto-generated).
|
||||
|
||||
user-alice = {
|
||||
module = {
|
||||
name = "users";
|
||||
input = "clan";
|
||||
```nix
|
||||
{
|
||||
inventory.instances = {
|
||||
# Deploy user alice on all machines. Don't prompt for password (will be
|
||||
# auto-generated).
|
||||
user-alice = {
|
||||
module = {
|
||||
name = "users";
|
||||
input = "clan";
|
||||
};
|
||||
roles.default.tags.all = { };
|
||||
roles.default.settings = {
|
||||
user = "alice";
|
||||
prompt = false;
|
||||
};
|
||||
};
|
||||
roles.default.tags.all = { };
|
||||
roles.default.settings = {
|
||||
user = "alice";
|
||||
prompt = false;
|
||||
|
||||
# Deploy user bob only on his laptop. Prompt for a password.
|
||||
user-bob = {
|
||||
module = {
|
||||
name = "users";
|
||||
input = "clan";
|
||||
};
|
||||
roles.default.machines.bobs-laptop = { };
|
||||
roles.default.settings.user = "bob";
|
||||
};
|
||||
};
|
||||
|
||||
# Deploy user bob only on his laptop. Prompt for a password.
|
||||
user-bob = {
|
||||
module = {
|
||||
name = "users";
|
||||
input = "clan";
|
||||
};
|
||||
roles.default.machines.bobs-laptop = { };
|
||||
roles.default.settings.user = "bob";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -15,7 +15,15 @@ in
|
||||
unit-test-module = (
|
||||
self.clanLib.test.flakeModules.makeEvalChecks {
|
||||
inherit module;
|
||||
inherit self inputs;
|
||||
inherit inputs;
|
||||
fileset = lib.fileset.unions [
|
||||
# The zerotier service being tested
|
||||
../../clanServices/zerotier
|
||||
# Required modules
|
||||
../../nixosModules/clanCore
|
||||
# Dependencies like clan-cli
|
||||
../../pkgs/clan-cli
|
||||
];
|
||||
testName = "zerotier";
|
||||
tests = ./tests/eval-tests.nix;
|
||||
testArgs = { };
|
||||
|
||||
1
devFlake/private.narHash
Normal file
1
devFlake/private.narHash
Normal file
@@ -0,0 +1 @@
|
||||
sha256-pFUj3KhQ4FkzZT19t+FHBru8u8Lspax0rS2cv7nXIgM=
|
||||
165
devFlake/private/flake.lock
generated
Normal file
165
devFlake/private/flake.lock
generated
Normal file
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": [
|
||||
"systems"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"ixx": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"nuschtos",
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nuschtos",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1748294338,
|
||||
"narHash": "sha256-FVO01jdmUNArzBS7NmaktLdGA5qA3lUMJ4B7a05Iynw=",
|
||||
"owner": "NuschtOS",
|
||||
"repo": "ixx",
|
||||
"rev": "cc5f390f7caf265461d4aab37e98d2292ebbdb85",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NuschtOS",
|
||||
"ref": "v0.0.8",
|
||||
"repo": "ixx",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-dev": {
|
||||
"locked": {
|
||||
"lastModified": 1751867001,
|
||||
"narHash": "sha256-3I49W0s3WVEDBO5S1RxYr74E2LLG7X8Wuvj9AmU0RDk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "73feb5e20ec7259e280ca6f424ba165059b3bb6b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable-small",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nuschtos": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"ixx": "ixx",
|
||||
"nixpkgs": [
|
||||
"nixpkgs-dev"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1749730855,
|
||||
"narHash": "sha256-L3x2nSlFkXkM6tQPLJP3oCBMIsRifhIDPMQQdHO5xWo=",
|
||||
"owner": "NuschtOS",
|
||||
"repo": "search",
|
||||
"rev": "8dfe5879dd009ff4742b668d9c699bc4b9761742",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NuschtOS",
|
||||
"repo": "search",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs-dev": "nixpkgs-dev",
|
||||
"nuschtos": "nuschtos",
|
||||
"systems": "systems_2",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": []
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1750931469,
|
||||
"narHash": "sha256-0IEdQB1nS+uViQw4k3VGUXntjkDp7aAlqcxdewb/hAc=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "ac8e6f32e11e9c7f153823abc3ab007f2a65d3e1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
19
devFlake/private/flake.nix
Normal file
19
devFlake/private/flake.nix
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
description = "private dev inputs";
|
||||
|
||||
# Dev dependencies
|
||||
inputs.nixpkgs-dev.url = "github:NixOS/nixpkgs/nixos-unstable-small";
|
||||
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
inputs.flake-utils.inputs.systems.follows = "systems";
|
||||
|
||||
inputs.nuschtos.url = "github:NuschtOS/search";
|
||||
inputs.nuschtos.inputs.nixpkgs.follows = "nixpkgs-dev";
|
||||
|
||||
inputs.treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
inputs.treefmt-nix.inputs.nixpkgs.follows = "";
|
||||
|
||||
inputs.systems.url = "github:nix-systems/default";
|
||||
|
||||
outputs = _: { };
|
||||
}
|
||||
12
devFlake/update-private-narhash
Executable file
12
devFlake/update-private-narhash
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Used to update the private dev flake hash reference.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "Updating $PWD/private.narHash" >&2
|
||||
|
||||
nix --extra-experimental-features 'flakes nix-command' flake lock ./private
|
||||
nix --extra-experimental-features 'flakes nix-command' hash path ./private >./private.narHash
|
||||
|
||||
echo OK
|
||||
@@ -62,6 +62,7 @@ nav:
|
||||
- Vars Backend: guides/vars-backend.md
|
||||
- Facts Backend: guides/secrets.md
|
||||
- Adding more machines: guides/more-machines.md
|
||||
- Target Host: guides/target-host.md
|
||||
- Inventory:
|
||||
- Inventory: guides/inventory.md
|
||||
- Secure Boot: guides/secure-boot.md
|
||||
@@ -154,6 +155,7 @@ nav:
|
||||
- reference/cli/show.md
|
||||
- reference/cli/ssh.md
|
||||
- reference/cli/state.md
|
||||
- reference/cli/templates.md
|
||||
- reference/cli/vars.md
|
||||
- reference/cli/vms.md
|
||||
- NixOS Modules:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
clan-core,
|
||||
pkgs,
|
||||
module-docs,
|
||||
clan-cli-docs,
|
||||
@@ -19,7 +18,17 @@ pkgs.stdenv.mkDerivation {
|
||||
|
||||
# Points to repository root.
|
||||
# so that we can access directories outside of docs to include code snippets
|
||||
src = clan-core;
|
||||
src = pkgs.lib.fileset.toSource {
|
||||
root = ../..;
|
||||
fileset = pkgs.lib.fileset.unions [
|
||||
# Docs directory
|
||||
../../docs
|
||||
# Icons needed for the build
|
||||
../../pkgs/clan-app/ui/icons
|
||||
# Any other directories that might be referenced for code snippets
|
||||
# Add them here as needed based on what mkdocs actually uses
|
||||
];
|
||||
};
|
||||
|
||||
nativeBuildInputs =
|
||||
[
|
||||
|
||||
@@ -82,10 +82,9 @@
|
||||
}
|
||||
''
|
||||
export CLAN_CORE_PATH=${
|
||||
self.filter {
|
||||
include = [
|
||||
"clanModules"
|
||||
];
|
||||
inputs.nixpkgs.lib.fileset.toSource {
|
||||
root = ../..;
|
||||
fileset = ../../clanModules;
|
||||
}
|
||||
}
|
||||
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
||||
@@ -126,7 +125,6 @@
|
||||
});
|
||||
packages = {
|
||||
docs = pkgs.python3.pkgs.callPackage ./default.nix {
|
||||
clan-core = self;
|
||||
inherit (self'.packages)
|
||||
clan-cli-docs
|
||||
docs-options
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
{ self, config, ... }:
|
||||
{
|
||||
self,
|
||||
config,
|
||||
inputs,
|
||||
privateInputs ? { },
|
||||
...
|
||||
}:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
inputs',
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -157,11 +163,16 @@
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.docs-options = inputs'.nuschtos.packages.mkMultiSearch {
|
||||
inherit baseHref;
|
||||
title = "Clan Options";
|
||||
# scopes = mapAttrsToList mkScope serviceModules;
|
||||
scopes = [ (mkScope "Clan Inventory" serviceModules) ];
|
||||
packages = lib.optionalAttrs ((privateInputs ? nuschtos) || (inputs ? nuschtos)) {
|
||||
docs-options =
|
||||
(privateInputs.nuschtos or inputs.nuschtos)
|
||||
.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch
|
||||
{
|
||||
inherit baseHref;
|
||||
title = "Clan Options";
|
||||
# scopes = mapAttrsToList mkScope serviceModules;
|
||||
scopes = [ (mkScope "Clan Inventory" serviceModules) ];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ Benefits:
|
||||
* Caching mechanism is very simple.
|
||||
|
||||
|
||||
### Method 2: Direct access:
|
||||
### Method 2: Direct access
|
||||
|
||||
Directly calling the evaluator / build sandbox via `nix build` and `nix eval`within the Python code
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ git+file:///home/lhebendanz/Projects/clan-core
|
||||
│ ├───editor omitted (use '--all-systems' to show)
|
||||
└───templates
|
||||
├───default: template: Initialize a new clan flake
|
||||
└───new-clan: template: Initialize a new clan flake
|
||||
└───default: template: Initialize a new clan flake
|
||||
```
|
||||
|
||||
You can execute every test separately by following the tree path `nix run .#checks.x86_64-linux.clan-pytest -L` for example.
|
||||
|
||||
@@ -122,8 +122,8 @@ CTRL+D
|
||||
4. Locally generate ssh host keys. You only need to generate ones for the algorithms you're using in `authorizedKeys`.
|
||||
|
||||
```bash
|
||||
ssh-keygen -q -N "" -t ed25519 -f ./initrd_host_ed25519_key
|
||||
ssh-keygen -q -N "" -t rsa -b 4096 -f ./initrd_host_rsa_key
|
||||
ssh-keygen -q -N "" -C "" -t ed25519 -f ./initrd_host_ed25519_key
|
||||
ssh-keygen -q -N "" -C "" -t rsa -b 4096 -f ./initrd_host_rsa_key
|
||||
```
|
||||
|
||||
5. Securely copy your local initrd ssh host keys to the installer's `/mnt` directory:
|
||||
|
||||
82
docs/site/guides/target-host.md
Normal file
82
docs/site/guides/target-host.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# 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
|
||||
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
|
||||
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.
|
||||
92
flake.lock
generated
92
flake.lock
generated
@@ -16,11 +16,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1751413887,
|
||||
"narHash": "sha256-+ut7DrSwamExIvaCFdiTYD88NTSYJFG2CEOvCha59vI=",
|
||||
"rev": "246f0d66547d073af6249e4f7852466197e871ed",
|
||||
"lastModified": 1751846468,
|
||||
"narHash": "sha256-h0mpWZIOIAKj4fmLNyI2HDG+c0YOkbYmyJXSj/bQ9s0=",
|
||||
"rev": "a2166c13b0cb3febdaf36391cd2019aa2ccf4366",
|
||||
"type": "tarball",
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/246f0d66547d073af6249e4f7852466197e871ed.tar.gz"
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/a2166c13b0cb3febdaf36391cd2019aa2ccf4366.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -34,11 +34,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1751607816,
|
||||
"narHash": "sha256-5PtrwjqCIJ4DKQhzYdm8RFePBuwb+yTzjV52wWoGSt4=",
|
||||
"lastModified": 1751854533,
|
||||
"narHash": "sha256-U/OQFplExOR1jazZY4KkaQkJqOl59xlh21HP9mI79Vc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "da6109c917b48abc1f76dd5c9bf3901c8c80f662",
|
||||
"rev": "16b74a1e304197248a1bc663280f2548dbfcae3c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -67,52 +67,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": [
|
||||
"systems"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"ixx": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"nuschtos",
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nuschtos",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1748294338,
|
||||
"narHash": "sha256-FVO01jdmUNArzBS7NmaktLdGA5qA3lUMJ4B7a05Iynw=",
|
||||
"owner": "NuschtOS",
|
||||
"repo": "ixx",
|
||||
"rev": "cc5f390f7caf265461d4aab37e98d2292ebbdb85",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NuschtOS",
|
||||
"ref": "v0.0.8",
|
||||
"repo": "ixx",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-darwin": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -164,51 +118,25 @@
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 315532800,
|
||||
"narHash": "sha256-0HRxGUoOMtOYnwlMWY0AkuU88WHaI3Q5GEILmsWpI8U=",
|
||||
"rev": "a48741b083d4f36dd79abd9f760c84da6b4dc0e5",
|
||||
"narHash": "sha256-mUlYenGbsUFP0A3EhfKJXmUl5+MQGJLhoEop2t3g5p4=",
|
||||
"rev": "ceb24d94c6feaa4e8737a8e2bd3cf71c3a7eaaa0",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre823094.a48741b083d4/nixexprs.tar.xz"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre826033.ceb24d94c6fe/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"nuschtos": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"ixx": "ixx",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1749730855,
|
||||
"narHash": "sha256-L3x2nSlFkXkM6tQPLJP3oCBMIsRifhIDPMQQdHO5xWo=",
|
||||
"owner": "NuschtOS",
|
||||
"repo": "search",
|
||||
"rev": "8dfe5879dd009ff4742b668d9c699bc4b9761742",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NuschtOS",
|
||||
"repo": "search",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"data-mesher": "data-mesher",
|
||||
"disko": "disko",
|
||||
"flake-parts": "flake-parts",
|
||||
"flake-utils": "flake-utils",
|
||||
"nix-darwin": "nix-darwin",
|
||||
"nix-select": "nix-select",
|
||||
"nixos-facter-modules": "nixos-facter-modules",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nuschtos": "nuschtos",
|
||||
"sops-nix": "sops-nix",
|
||||
"systems": "systems",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
|
||||
23
flake.nix
23
flake.nix
@@ -35,19 +35,13 @@
|
||||
};
|
||||
};
|
||||
|
||||
# dependencies needed for nuschtos
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
flake-utils.inputs.systems.follows = "systems";
|
||||
nuschtos.url = "github:NuschtOS/search";
|
||||
nuschtos.inputs.nixpkgs.follows = "nixpkgs";
|
||||
nuschtos.inputs.flake-utils.follows = "flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{
|
||||
flake-parts,
|
||||
nixpkgs,
|
||||
systems,
|
||||
flake-parts,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -56,10 +50,25 @@
|
||||
optional
|
||||
pathExists
|
||||
;
|
||||
|
||||
loadDevFlake =
|
||||
path:
|
||||
let
|
||||
flakeHash = nixpkgs.lib.fileContents "${toString path}.narHash";
|
||||
flakePath = "path:${toString path}?narHash=${flakeHash}";
|
||||
in
|
||||
builtins.getFlake (builtins.unsafeDiscardStringContext flakePath);
|
||||
|
||||
devFlake = builtins.tryEval (loadDevFlake ./devFlake/private);
|
||||
|
||||
privateInputs = if devFlake.success then devFlake.value.inputs else { };
|
||||
in
|
||||
flake-parts.lib.mkFlake { inherit inputs; } (
|
||||
{ ... }:
|
||||
{
|
||||
_module.args = {
|
||||
inherit privateInputs;
|
||||
};
|
||||
clan = {
|
||||
meta.name = "clan-core";
|
||||
inventory = {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
perSystem =
|
||||
{ self', pkgs, ... }:
|
||||
{
|
||||
treefmt.projectRootFile = ".git/config";
|
||||
treefmt.projectRootFile = "LICENSE.md";
|
||||
treefmt.programs.shellcheck.enable = true;
|
||||
|
||||
treefmt.programs.mypy.enable = true;
|
||||
|
||||
@@ -37,6 +37,7 @@ lib.fix (
|
||||
inventory = clanLib.callLib ./modules/inventory { };
|
||||
modules = clanLib.callLib ./modules/inventory/frontmatter { };
|
||||
test = clanLib.callLib ./test { };
|
||||
flake-inputs = clanLib.callLib ./flake-inputs.nix { };
|
||||
# Custom types
|
||||
types = clanLib.callLib ./types { };
|
||||
|
||||
|
||||
18
lib/flake-inputs.nix
Normal file
18
lib/flake-inputs.nix
Normal file
@@ -0,0 +1,18 @@
|
||||
{ ... }:
|
||||
{
|
||||
/**
|
||||
Generate nix-unit input overrides for tests
|
||||
|
||||
# Example
|
||||
```nix
|
||||
inputOverrides = clanLib.flake-inputs.getOverrides inputs;
|
||||
```
|
||||
*/
|
||||
getOverrides =
|
||||
inputs:
|
||||
builtins.concatStringsSep " " (
|
||||
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (
|
||||
builtins.filter (name: name != "self") (builtins.attrNames inputs)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
{ self, inputs, ... }:
|
||||
let
|
||||
inputOverrides = builtins.concatStringsSep " " (
|
||||
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
|
||||
);
|
||||
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
|
||||
in
|
||||
{
|
||||
perSystem =
|
||||
|
||||
@@ -229,8 +229,6 @@ in
|
||||
clanInternals = {
|
||||
inventoryClass =
|
||||
let
|
||||
localModuleSet =
|
||||
lib.filterAttrs (n: _: !inventory._legacyModules ? ${n}) inventory.modules // config.modules;
|
||||
flakeInputs = config.self.inputs;
|
||||
in
|
||||
{
|
||||
@@ -240,7 +238,7 @@ in
|
||||
imports = [
|
||||
../inventoryClass/builder/default.nix
|
||||
(lib.modules.importApply ../inventoryClass/service-list-from-inputs.nix {
|
||||
inherit flakeInputs clanLib localModuleSet;
|
||||
inherit flakeInputs clanLib;
|
||||
})
|
||||
{
|
||||
inherit inventory directory;
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
inputOverrides = builtins.concatStringsSep " " (
|
||||
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
|
||||
);
|
||||
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{ self, inputs, ... }:
|
||||
let
|
||||
inputOverrides = builtins.concatStringsSep " " (
|
||||
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
|
||||
);
|
||||
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
|
||||
in
|
||||
{
|
||||
perSystem =
|
||||
@@ -12,6 +10,23 @@ in
|
||||
system,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# Common filtered source for inventory tests
|
||||
inventoryTestsSrc = lib.fileset.toSource {
|
||||
root = ../../../..;
|
||||
fileset = lib.fileset.unions [
|
||||
../../../../flake.nix
|
||||
../../../../flake.lock
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../../..)
|
||||
../../../../flakeModules
|
||||
../../../../lib
|
||||
../../../../nixosModules/clanCore
|
||||
../../../../clanModules/borgbackup
|
||||
../../../../machines
|
||||
../../../../inventory.json
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.<attrName>
|
||||
legacyPackages.evalTests-distributedServices = import ./tests {
|
||||
@@ -29,7 +44,7 @@ in
|
||||
--extra-experimental-features flakes \
|
||||
--show-trace \
|
||||
${inputOverrides} \
|
||||
--flake ${self}#legacyPackages.${system}.evalTests-distributedServices
|
||||
--flake ${inventoryTestsSrc}#legacyPackages.${system}.evalTests-distributedServices
|
||||
|
||||
touch $out
|
||||
'';
|
||||
@@ -39,7 +54,7 @@ in
|
||||
--extra-experimental-features flakes \
|
||||
--show-trace \
|
||||
${inputOverrides} \
|
||||
--flake ${self}#legacyPackages.${system}.eval-tests-resolve-module
|
||||
--flake ${inventoryTestsSrc}#legacyPackages.${system}.eval-tests-resolve-module
|
||||
|
||||
touch $out
|
||||
'';
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
inputOverrides = builtins.concatStringsSep " " (
|
||||
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
|
||||
);
|
||||
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
@@ -70,12 +68,18 @@ in
|
||||
--show-trace \
|
||||
${inputOverrides} \
|
||||
--flake ${
|
||||
self.filter {
|
||||
include = [
|
||||
"flakeModules"
|
||||
"lib"
|
||||
"clanModules/flake-module.nix"
|
||||
"clanModules/borgbackup"
|
||||
lib.fileset.toSource {
|
||||
root = ../../..;
|
||||
fileset = lib.fileset.unions [
|
||||
../../../flake.nix
|
||||
../../../flake.lock
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../..)
|
||||
../../../flakeModules
|
||||
../../../lib
|
||||
../../../nixosModules/clanCore
|
||||
../../../clanModules/borgbackup
|
||||
../../../machines
|
||||
../../../inventory.json
|
||||
];
|
||||
}
|
||||
}#legacyPackages.${system}.evalTests-inventory
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
{
|
||||
flakeInputs,
|
||||
clanLib,
|
||||
localModuleSet,
|
||||
}:
|
||||
{ lib, config, ... }:
|
||||
|
||||
let
|
||||
|
||||
inspectModule =
|
||||
inputName: moduleName: module:
|
||||
let
|
||||
@@ -28,16 +25,30 @@ in
|
||||
{
|
||||
options.modulesPerSource = lib.mkOption {
|
||||
# { sourceName :: { moduleName :: {} }}
|
||||
readOnly = true;
|
||||
type = lib.types.raw;
|
||||
default =
|
||||
let
|
||||
inputsWithModules = lib.filterAttrs (_inputName: v: v ? clan.modules) flakeInputs;
|
||||
|
||||
in
|
||||
lib.mapAttrs (
|
||||
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
|
||||
) inputsWithModules;
|
||||
};
|
||||
options.localModules = lib.mkOption {
|
||||
default = lib.mapAttrs (inspectModule "self") localModuleSet;
|
||||
readOnly = true;
|
||||
type = lib.types.raw;
|
||||
default = config.modulesPerSource.self;
|
||||
};
|
||||
options.templatesPerSource = lib.mkOption {
|
||||
# { sourceName :: { moduleName :: {} }}
|
||||
readOnly = true;
|
||||
type = lib.types.raw;
|
||||
default =
|
||||
let
|
||||
inputsWithTemplates = lib.filterAttrs (_inputName: v: v ? clan.templates) flakeInputs;
|
||||
in
|
||||
lib.mapAttrs (_inputName: v: lib.mapAttrs (_n: t: t) v.clan.templates) inputsWithTemplates;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
makeEvalChecks =
|
||||
{
|
||||
self,
|
||||
fileset,
|
||||
inputs,
|
||||
testName,
|
||||
tests,
|
||||
@@ -24,9 +24,7 @@
|
||||
testArgs ? { },
|
||||
}:
|
||||
let
|
||||
inputOverrides = builtins.concatStringsSep " " (
|
||||
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
|
||||
);
|
||||
inputOverrides = clanLib.flake-inputs.getOverrides inputs;
|
||||
attrName = "eval-tests-${testName}";
|
||||
in
|
||||
{
|
||||
@@ -41,16 +39,44 @@
|
||||
}
|
||||
// testArgs
|
||||
);
|
||||
checks.${attrName} = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
|
||||
export HOME="$(realpath .)"
|
||||
checks.${attrName} =
|
||||
let
|
||||
# The root is two directories up from where this file is located
|
||||
root = ../..;
|
||||
|
||||
nix-unit --eval-store "$HOME" \
|
||||
--extra-experimental-features flakes \
|
||||
--show-trace \
|
||||
${inputOverrides} \
|
||||
--flake ${self}#legacyPackages.${system}.${attrName}
|
||||
touch $out
|
||||
'';
|
||||
# Combine the user-provided fileset with all flake-module.nix files
|
||||
# and other essential files
|
||||
src = lib.fileset.toSource {
|
||||
inherit root;
|
||||
fileset = lib.fileset.unions [
|
||||
# Core flake files
|
||||
(root + "/flake.nix")
|
||||
(root + "/flake.lock")
|
||||
|
||||
# All flake-module.nix files anywhere in the tree
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") root)
|
||||
|
||||
# The flakeModules/clan.nix if it exists
|
||||
(lib.fileset.maybeMissing (root + "/flakeModules/clan.nix"))
|
||||
|
||||
# Core libraries
|
||||
(root + "/lib")
|
||||
|
||||
# User-provided fileset
|
||||
fileset
|
||||
];
|
||||
};
|
||||
in
|
||||
pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
|
||||
export HOME="$(realpath .)"
|
||||
|
||||
nix-unit --eval-store "$HOME" \
|
||||
--extra-experimental-features flakes \
|
||||
--show-trace \
|
||||
${inputOverrides} \
|
||||
--flake ${src}#legacyPackages.${system}.${attrName}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{ self, inputs, ... }:
|
||||
{
|
||||
self,
|
||||
inputs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
perSystem =
|
||||
{ ... }:
|
||||
@@ -10,7 +15,11 @@
|
||||
test-types-module = (
|
||||
self.clanLib.test.flakeModules.makeEvalChecks {
|
||||
module = throw "";
|
||||
inherit self inputs;
|
||||
inherit inputs;
|
||||
fileset = lib.fileset.unions [
|
||||
# Only lib is needed for type tests
|
||||
../../lib
|
||||
];
|
||||
testName = "types";
|
||||
tests = ./tests.nix;
|
||||
# Optional arguments passed to the test
|
||||
|
||||
@@ -40,6 +40,18 @@ in
|
||||
};
|
||||
|
||||
config = {
|
||||
# Check for removed passBackend option usage
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.clan.core.vars.settings.passBackend == null;
|
||||
message = ''
|
||||
The option `clan.core.vars.settings.passBackend' has been removed.
|
||||
Use clan.core.vars.password-store.passPackage instead.
|
||||
Set it to pkgs.pass for GPG or pkgs.passage for age encryption.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
# check all that all non-secret files have no owner/group/mode set
|
||||
warnings = lib.foldl' (
|
||||
warnings: generator:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ lib, pkgs, ... }:
|
||||
{ lib, pkgs }:
|
||||
let
|
||||
eval =
|
||||
module:
|
||||
|
||||
@@ -5,18 +5,14 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
inputOverrides = builtins.concatStringsSep " " (
|
||||
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
|
||||
);
|
||||
inputOverrides = self.clanLib.flake-inputs.getOverrides inputs;
|
||||
in
|
||||
{
|
||||
perSystem =
|
||||
{ system, pkgs, ... }:
|
||||
{
|
||||
legacyPackages.evalTests-module-clan-vars = import ./eval-tests {
|
||||
inherit lib;
|
||||
clan-core = self;
|
||||
pkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||
inherit lib pkgs;
|
||||
};
|
||||
checks.eval-module-clan-vars = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
|
||||
export HOME="$(realpath .)"
|
||||
@@ -26,11 +22,15 @@ in
|
||||
--show-trace \
|
||||
${inputOverrides} \
|
||||
--flake ${
|
||||
self.filter {
|
||||
include = [
|
||||
"flakeModules"
|
||||
"nixosModules"
|
||||
"lib"
|
||||
lib.fileset.toSource {
|
||||
root = ../../..;
|
||||
fileset = lib.fileset.unions [
|
||||
../../../flake.nix
|
||||
../../../flake.lock
|
||||
(lib.fileset.fileFilter (file: file.name == "flake-module.nix") ../../..)
|
||||
../../../flakeModules/clan.nix
|
||||
../../../lib
|
||||
../../../nixosModules/clanCore/vars
|
||||
];
|
||||
}
|
||||
}#legacyPackages.${system}.evalTests-module-clan-vars
|
||||
|
||||
@@ -54,7 +54,7 @@ in
|
||||
{
|
||||
_class = "nixos";
|
||||
|
||||
options.clan.vars.password-store = {
|
||||
options.clan.core.vars.password-store = {
|
||||
secretLocation = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = "/etc/secret-vars";
|
||||
@@ -62,6 +62,13 @@ in
|
||||
location where the tarball with the password-store secrets will be uploaded to and the manifest
|
||||
'';
|
||||
};
|
||||
passPackage = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.pass;
|
||||
description = ''
|
||||
Password store package to use. Can be pkgs.pass for GPG-based storage or pkgs.passage for age-based storage.
|
||||
'';
|
||||
};
|
||||
};
|
||||
config = {
|
||||
clan.core.vars.settings =
|
||||
@@ -76,7 +83,7 @@ in
|
||||
else if file.config.neededFor == "services" then
|
||||
"/run/secrets/${file.config.generatorName}/${file.config.name}"
|
||||
else if file.config.neededFor == "activation" then
|
||||
"${config.clan.password-store.secretLocation}/activation/${file.config.generatorName}/${file.config.name}"
|
||||
"${config.clan.core.vars.password-store.secretLocation}/activation/${file.config.generatorName}/${file.config.name}"
|
||||
else if file.config.neededFor == "partitioning" then
|
||||
"/run/partitioning-secrets/${file.config.generatorName}/${file.config.name}"
|
||||
else
|
||||
@@ -95,7 +102,7 @@ in
|
||||
]
|
||||
''
|
||||
[ -e /run/current-system ] || echo setting up secrets...
|
||||
${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets_for_users.tar.gz /run/user-secrets
|
||||
${installSecretTarball}/bin/install-secret-tarball ${config.clan.core.vars.password-store.secretLocation}/secrets_for_users.tar.gz /run/user-secrets
|
||||
''
|
||||
// lib.optionalAttrs (config.system ? dryActivationScript) {
|
||||
supportsDryActivation = true;
|
||||
@@ -111,7 +118,7 @@ in
|
||||
]
|
||||
''
|
||||
[ -e /run/current-system ] || echo setting up secrets...
|
||||
${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets.tar.gz /run/secrets
|
||||
${installSecretTarball}/bin/install-secret-tarball ${config.clan.core.vars.password-store.secretLocation}/secrets.tar.gz /run/secrets
|
||||
''
|
||||
// lib.optionalAttrs (config.system ? dryActivationScript) {
|
||||
supportsDryActivation = true;
|
||||
@@ -129,7 +136,7 @@ in
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = [
|
||||
"${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets_for_users.tar.gz /run/user-secrets"
|
||||
"${installSecretTarball}/bin/install-secret-tarball ${config.clan.core.vars.password-store.secretLocation}/secrets_for_users.tar.gz /run/user-secrets"
|
||||
];
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
@@ -142,7 +149,7 @@ in
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = [
|
||||
"${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets.tar.gz /run/secrets"
|
||||
"${installSecretTarball}/bin/install-secret-tarball ${config.clan.core.vars.password-store.secretLocation}/secrets.tar.gz /run/secrets"
|
||||
];
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
|
||||
@@ -15,17 +15,6 @@
|
||||
'';
|
||||
};
|
||||
|
||||
passBackend = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"passage"
|
||||
"pass"
|
||||
];
|
||||
default = "pass";
|
||||
description = ''
|
||||
password-store backend to use. Valid options are `pass` and `passage`
|
||||
'';
|
||||
};
|
||||
|
||||
secretModule = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
internal = true;
|
||||
@@ -65,4 +54,15 @@
|
||||
the python import path to the public module
|
||||
'';
|
||||
};
|
||||
|
||||
# Legacy option that guides migration
|
||||
passBackend = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
visible = false;
|
||||
description = ''
|
||||
DEPRECATED: This option has been removed. Use clan.vars.password-store.passPackage instead.
|
||||
Set it to pkgs.pass for GPG or pkgs.passage for age encryption.
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ const handleCancel = async <K extends OperationNames>(
|
||||
orig_task: Promise<BackendReturnType<K>>,
|
||||
) => {
|
||||
console.log("Canceling operation: ", ops_key);
|
||||
const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key });
|
||||
const { promise, op_key } = _callApi("delete_task", { task_id: ops_key });
|
||||
promise.catch((error) => {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
|
||||
@@ -75,7 +75,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
||||
}
|
||||
|
||||
setInstalling(true);
|
||||
await callApi("install_machine", {
|
||||
await callApi("run_machine_install", {
|
||||
opts: {
|
||||
machine: {
|
||||
name: name,
|
||||
@@ -163,7 +163,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
||||
}
|
||||
|
||||
await callApi(
|
||||
"deploy_machine",
|
||||
"run_machine_deploy",
|
||||
{
|
||||
machine: {
|
||||
name: name,
|
||||
|
||||
@@ -13,7 +13,7 @@ export const clanMetaQuery = (uri: string | undefined = undefined) =>
|
||||
queryFn: async () => {
|
||||
console.log("fetching clan meta", clanURI);
|
||||
|
||||
const result = await callApi("show_clan_meta", {
|
||||
const result = await callApi("get_clan_details", {
|
||||
flake: { identifier: clanURI! },
|
||||
}).promise;
|
||||
|
||||
|
||||
@@ -33,27 +33,6 @@ export const createModulesQuery = (
|
||||
},
|
||||
}));
|
||||
|
||||
export const tagsQuery = (uri: string | undefined) =>
|
||||
useQuery<string[]>(() => ({
|
||||
queryKey: [uri, "tags"],
|
||||
placeholderData: [],
|
||||
queryFn: async () => {
|
||||
if (!uri) return [];
|
||||
|
||||
const response = await callApi("get_inventory", {
|
||||
flake: { identifier: uri },
|
||||
}).promise;
|
||||
if (response.status === "error") {
|
||||
console.error("Failed to fetch data");
|
||||
} else {
|
||||
const machines = response.data.machines || {};
|
||||
const tags = Object.values(machines).flatMap((m) => m.tags || []);
|
||||
return tags;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}));
|
||||
|
||||
export const machinesQuery = (uri: string | undefined) =>
|
||||
useQuery<string[]>(() => ({
|
||||
queryKey: [uri, "machines"],
|
||||
@@ -61,7 +40,7 @@ export const machinesQuery = (uri: string | undefined) =>
|
||||
queryFn: async () => {
|
||||
if (!uri) return [];
|
||||
|
||||
const response = await callApi("get_inventory", {
|
||||
const response = await callApi("list_machines", {
|
||||
flake: { identifier: uri },
|
||||
}).promise;
|
||||
if (response.status === "error") {
|
||||
|
||||
@@ -66,7 +66,7 @@ export const CreateClan = () => {
|
||||
}
|
||||
|
||||
// Will generate a key if it doesn't exist, and add a user to the clan
|
||||
const k = await callApi("keygen", {
|
||||
const k = await callApi("create_secrets_user", {
|
||||
flake_dir: target_dir[0],
|
||||
}).promise;
|
||||
|
||||
@@ -203,6 +203,6 @@ export const CreateClan = () => {
|
||||
};
|
||||
|
||||
type Meta = Extract<
|
||||
OperationResponse<"show_clan_meta">,
|
||||
OperationResponse<"get_clan_details">,
|
||||
{ status: "success" }
|
||||
>["data"];
|
||||
|
||||
@@ -23,7 +23,7 @@ const EditClanForm = (props: EditClanFormProps) => {
|
||||
const handleSubmit: SubmitHandler<GeneralData> = async (values, event) => {
|
||||
await toast.promise(
|
||||
(async () => {
|
||||
await callApi("update_clan_meta", {
|
||||
await callApi("set_clan_details", {
|
||||
options: {
|
||||
flake: { identifier: props.directory },
|
||||
meta: values,
|
||||
@@ -128,7 +128,7 @@ const EditClanForm = (props: EditClanFormProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
type GeneralData = SuccessQuery<"show_clan_meta">["data"];
|
||||
type GeneralData = SuccessQuery<"get_clan_details">["data"];
|
||||
|
||||
export const ClanDetails = () => {
|
||||
const params = useParams();
|
||||
|
||||
@@ -100,7 +100,7 @@ export const Flash = () => {
|
||||
const deviceQuery = createQuery(() => ({
|
||||
queryKey: ["block_devices"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_block_devices", {}).promise;
|
||||
const result = await callApi("list_block_devices", {}).promise;
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
@@ -110,7 +110,7 @@ export const Flash = () => {
|
||||
const keymapQuery = createQuery(() => ({
|
||||
queryKey: ["list_keymaps"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("list_possible_keymaps", {}).promise;
|
||||
const result = await callApi("list_keymaps", {}).promise;
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
@@ -120,7 +120,7 @@ export const Flash = () => {
|
||||
const langQuery = createQuery(() => ({
|
||||
queryKey: ["list_languages"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("list_possible_languages", {}).promise;
|
||||
const result = await callApi("list_languages", {}).promise;
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
@@ -157,7 +157,7 @@ export const Flash = () => {
|
||||
console.log("Confirmed flash:", values);
|
||||
try {
|
||||
await toast.promise(
|
||||
callApi("flash_machine", {
|
||||
callApi("run_machine_flash", {
|
||||
machine: {
|
||||
name: values.machine.devicePath,
|
||||
flake: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "../../components/Button/Button";
|
||||
import Icon from "@/src/components/icon";
|
||||
|
||||
type ServiceModel = Extract<
|
||||
OperationResponse<"show_mdns">,
|
||||
OperationResponse<"list_mdns_services">,
|
||||
{ status: "success" }
|
||||
>["data"]["services"];
|
||||
|
||||
@@ -16,7 +16,7 @@ export const HostList: Component = () => {
|
||||
<div class="" data-tip="Refresh install targets">
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => callApi("show_mdns", {})}
|
||||
onClick={() => callApi("list_mdns_services", {})}
|
||||
startIcon={<Icon icon="Update" />}
|
||||
></Button>
|
||||
</div>
|
||||
|
||||
@@ -120,7 +120,7 @@ export function InstallMachine(props: InstallMachineProps) {
|
||||
throw new Error("No target host found for the machine");
|
||||
}
|
||||
|
||||
const installPromise = callApi("install_machine", {
|
||||
const installPromise = callApi("run_machine_install", {
|
||||
opts: {
|
||||
machine: {
|
||||
name: props.name,
|
||||
|
||||
@@ -149,7 +149,7 @@ export function MachineForm(props: MachineFormProps) {
|
||||
|
||||
setIsUpdating(true);
|
||||
const r = await callApi(
|
||||
"deploy_machine",
|
||||
"run_machine_deploy",
|
||||
{
|
||||
machine: {
|
||||
name: machine,
|
||||
|
||||
@@ -71,7 +71,7 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
||||
const hwReportQuery = useQuery(() => ({
|
||||
queryKey: [props.dir, props.machine_id, "hw_report"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_machine_hardware_config", {
|
||||
const result = await callApi("get_machine_hardware_summary", {
|
||||
machine: {
|
||||
flake: {
|
||||
identifier: props.dir,
|
||||
@@ -127,7 +127,7 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const r = await callApi("generate_machine_hardware_info", {
|
||||
const r = await callApi("run_machine_hardware_info", {
|
||||
opts: {
|
||||
machine: {
|
||||
name: props.machine_id,
|
||||
|
||||
@@ -173,7 +173,7 @@ export const VarsStep = (props: VarsStepProps) => {
|
||||
toast.error("Error fetching data");
|
||||
return;
|
||||
}
|
||||
const result = await callApi("generate_vars_for_machine", {
|
||||
const result = await callApi("run_generators", {
|
||||
machine_name: props.machine_id,
|
||||
base_dir: props.dir,
|
||||
generators: generatorsQuery.data.map((generator) => generator.name),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BackButton } from "@/src/components/BackButton";
|
||||
import { createModulesQuery, machinesQuery, tagsQuery } from "@/src/queries";
|
||||
import { createModulesQuery, machinesQuery } from "@/src/queries";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { For, Match, Switch } from "solid-js";
|
||||
import { ModuleInfo } from "./list";
|
||||
@@ -34,28 +34,11 @@ interface AddModuleProps {
|
||||
|
||||
const AddModule = (props: AddModuleProps) => {
|
||||
const { activeClanURI } = useClanContext();
|
||||
const tags = tagsQuery(activeClanURI());
|
||||
const machines = machinesQuery(activeClanURI());
|
||||
return (
|
||||
<div>
|
||||
<div>Add to your clan</div>
|
||||
<Switch fallback="loading">
|
||||
<Match when={tags.data}>
|
||||
{(tags) => (
|
||||
<For each={Object.keys(props.data.roles)}>
|
||||
{(role) => (
|
||||
<>
|
||||
<div class="text-neutral-600">{role}s</div>
|
||||
<RoleForm
|
||||
avilableTags={tags()}
|
||||
availableMachines={machines.data || []}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
<Switch fallback="loading">Removed</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -62,7 +62,6 @@ const Details = (props: DetailsProps) => {
|
||||
navigate(`/modules/add/${props.id}`);
|
||||
// const uri = activeURI();
|
||||
// if (!uri) return;
|
||||
// const res = await callApi("get_inventory", { base_path: uri });
|
||||
// if (res.status === "error") {
|
||||
// toast.error("Failed to fetch inventory");
|
||||
// return;
|
||||
|
||||
16
pkgs/clan-app/ui/package-lock.json
generated
16
pkgs/clan-app/ui/package-lock.json
generated
@@ -48,7 +48,7 @@
|
||||
"jsdom": "^26.1.0",
|
||||
"knip": "^5.61.2",
|
||||
"markdown-to-jsx": "^7.7.10",
|
||||
"playwright": "~1.52.0",
|
||||
"playwright": "~1.53.2",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
@@ -6189,13 +6189,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
||||
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
||||
"version": "1.53.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
|
||||
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.52.0"
|
||||
"playwright-core": "1.53.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -6208,9 +6208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
||||
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
||||
"version": "1.53.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
|
||||
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"jsdom": "^26.1.0",
|
||||
"knip": "^5.61.2",
|
||||
"markdown-to-jsx": "^7.7.10",
|
||||
"playwright": "~1.52.0",
|
||||
"playwright": "~1.53.2",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
|
||||
@@ -90,7 +90,7 @@ const handleCancel = async <K extends OperationNames>(
|
||||
orig_task: Promise<BackendReturnType<K>>,
|
||||
) => {
|
||||
console.log("Canceling operation: ", ops_key);
|
||||
const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key });
|
||||
const { promise, op_key } = _callApi("delete_task", { task_id: ops_key });
|
||||
promise.catch((error) => {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
|
||||
39
pkgs/clan-app/ui/src/components/v2/Alert/Alert.css
Normal file
39
pkgs/clan-app/ui/src/components/v2/Alert/Alert.css
Normal file
@@ -0,0 +1,39 @@
|
||||
div.alert {
|
||||
@apply flex gap-2.5 px-6 py-4 size-full rounded-md items-start;
|
||||
|
||||
&.has-icon {
|
||||
@apply pl-4;
|
||||
|
||||
svg.icon {
|
||||
@apply relative top-0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-dismiss {
|
||||
@apply pr-4;
|
||||
}
|
||||
|
||||
& > div.content {
|
||||
@apply flex flex-col gap-2 size-full;
|
||||
}
|
||||
|
||||
&.info {
|
||||
@apply bg-semantic-info-1 border border-semantic-info-3 fg-semantic-info-3;
|
||||
}
|
||||
|
||||
&.error {
|
||||
@apply bg-semantic-error-2 border border-semantic-error-3 fg-semantic-error-3;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply bg-semantic-warning-2 border border-semantic-warning-3 fg-semantic-warning-3;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply bg-semantic-success-1 border border-semantic-success-3 fg-semantic-success-3;
|
||||
}
|
||||
|
||||
& > button.dismiss-trigger {
|
||||
@apply relative top-0.5;
|
||||
}
|
||||
}
|
||||
138
pkgs/clan-app/ui/src/components/v2/Alert/Alert.stories.tsx
Normal file
138
pkgs/clan-app/ui/src/components/v2/Alert/Alert.stories.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Alert, AlertProps } from "@/src/components/v2/Alert/Alert";
|
||||
import { expect, fn } from "storybook/test";
|
||||
import { StoryContext } from "@kachurun/storybook-solid-vite";
|
||||
|
||||
const meta: Meta<AlertProps> = {
|
||||
title: "Components/Alert",
|
||||
component: Alert,
|
||||
decorators: [
|
||||
(Story: StoryObj) => (
|
||||
<div class="w-72">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<AlertProps>;
|
||||
|
||||
export const Info: Story = {
|
||||
args: {
|
||||
type: "info",
|
||||
title: "Headline",
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.",
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
...Info.args,
|
||||
type: "error",
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
...Info.args,
|
||||
type: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
...Info.args,
|
||||
type: "success",
|
||||
},
|
||||
};
|
||||
|
||||
export const InfoIcon: Story = {
|
||||
args: {
|
||||
...Info.args,
|
||||
icon: "Info",
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorIcon: Story = {
|
||||
args: {
|
||||
...Error.args,
|
||||
icon: "WarningFilled",
|
||||
},
|
||||
};
|
||||
|
||||
export const WarningIcon: Story = {
|
||||
args: {
|
||||
...Warning.args,
|
||||
icon: "WarningFilled",
|
||||
},
|
||||
};
|
||||
|
||||
export const SuccessIcon: Story = {
|
||||
args: {
|
||||
...Success.args,
|
||||
icon: "Checkmark",
|
||||
},
|
||||
};
|
||||
|
||||
export const InfoDismiss: Story = {
|
||||
args: {
|
||||
...Info.args,
|
||||
onDismiss: fn(),
|
||||
play: async ({ canvas, step, userEvent, args }: StoryContext) => {
|
||||
await userEvent.click(canvas.getByRole("button"));
|
||||
await expect(args.onDismiss).toHaveBeenCalled();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorDismiss: Story = {
|
||||
args: {
|
||||
...InfoDismiss.args,
|
||||
type: "error",
|
||||
},
|
||||
};
|
||||
|
||||
export const WarningDismiss: Story = {
|
||||
args: {
|
||||
...InfoDismiss.args,
|
||||
type: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const SuccessDismiss: Story = {
|
||||
args: {
|
||||
...InfoDismiss.args,
|
||||
type: "success",
|
||||
},
|
||||
};
|
||||
|
||||
export const InfoIconDismiss: Story = {
|
||||
args: {
|
||||
...InfoDismiss.args,
|
||||
icon: "Info",
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorIconDismiss: Story = {
|
||||
args: {
|
||||
...ErrorDismiss.args,
|
||||
icon: "WarningFilled",
|
||||
},
|
||||
};
|
||||
|
||||
export const WarningIconDismiss: Story = {
|
||||
args: {
|
||||
...WarningDismiss.args,
|
||||
icon: "WarningFilled",
|
||||
},
|
||||
};
|
||||
|
||||
export const SuccessIconDismiss: Story = {
|
||||
args: {
|
||||
...SuccessDismiss.args,
|
||||
icon: "Checkmark",
|
||||
},
|
||||
};
|
||||
43
pkgs/clan-app/ui/src/components/v2/Alert/Alert.tsx
Normal file
43
pkgs/clan-app/ui/src/components/v2/Alert/Alert.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import "./Alert.css";
|
||||
import cx from "classnames";
|
||||
import Icon, { IconVariant } from "@/src/components/v2/Icon/Icon";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { Alert as KAlert } from "@kobalte/core/alert";
|
||||
|
||||
export interface AlertProps {
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: IconVariant;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
export const Alert = (props: AlertProps) => (
|
||||
<KAlert
|
||||
class={cx("alert", props.type, {
|
||||
"has-icon": props.icon,
|
||||
"has-dismiss": props.onDismiss,
|
||||
})}
|
||||
>
|
||||
{props.icon && <Icon icon={props.icon} color="inherit" size="1rem" />}
|
||||
<div class="content">
|
||||
<Typography hierarchy="body" size="default" weight="bold" color="inherit">
|
||||
{props.title}
|
||||
</Typography>
|
||||
<Typography hierarchy="body" size="xs" color="inherit">
|
||||
{props.description}
|
||||
</Typography>
|
||||
</div>
|
||||
{props.onDismiss && (
|
||||
<Button
|
||||
name="dismiss-alert"
|
||||
class="dismiss-trigger"
|
||||
onClick={props.onDismiss}
|
||||
aria-label={`Dismiss ${props.type} alert`}
|
||||
>
|
||||
<Icon icon="Close" color="primary" size="0.75rem" />
|
||||
</Button>
|
||||
)}
|
||||
</KAlert>
|
||||
);
|
||||
@@ -1,15 +1,15 @@
|
||||
div.divider {
|
||||
@apply bg-inv-2;
|
||||
hr {
|
||||
@apply border-none outline-none bg-inv-2;
|
||||
|
||||
&.inverted {
|
||||
@apply bg-def-3;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
&[data-orientation="horizontal"] {
|
||||
@apply w-full h-px;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
&[data-orientation="vertical"] {
|
||||
@apply h-full w-px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import "./Divider.css";
|
||||
import cx from "classnames";
|
||||
import { Separator, SeparatorRootProps } from "@kobalte/core/separator";
|
||||
|
||||
export interface DividerProps {
|
||||
export interface DividerProps extends Pick<SeparatorRootProps, "orientation"> {
|
||||
inverted?: boolean;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
export const Divider = (props: DividerProps) => {
|
||||
const inverted = props.inverted || false;
|
||||
const orientation = () => props.orientation || "horizontal";
|
||||
|
||||
return <div class={cx("divider", orientation(), { inverted: inverted })} />;
|
||||
return (
|
||||
<Separator
|
||||
class={cx({ inverted: inverted })}
|
||||
orientation={props.orientation}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,10 @@ div.form-field {
|
||||
&[data-invalid] {
|
||||
@apply border-semantic-error-4;
|
||||
}
|
||||
|
||||
&[data-readonly] {
|
||||
@apply cursor-default bg-inherit border-none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +36,10 @@ div.form-field {
|
||||
&[data-disabled] {
|
||||
@apply bg-def-4 border-none;
|
||||
}
|
||||
|
||||
&[data-readonly] {
|
||||
@apply bg-inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,15 @@ export const Disabled: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ReadOnly: Story = {
|
||||
export const ReadOnlyUnchecked: Story = {
|
||||
args: {
|
||||
...Tooltip.args,
|
||||
readOnly: true,
|
||||
defaultChecked: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const ReadOnlyChecked: Story = {
|
||||
args: {
|
||||
...Tooltip.args,
|
||||
readOnly: true,
|
||||
|
||||
@@ -11,37 +11,64 @@ import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import "./Checkbox.css";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
import { Show } from "solid-js";
|
||||
|
||||
export type CheckboxProps = FieldProps &
|
||||
KCheckboxRootProps & {
|
||||
input?: PolymorphicProps<"input", KCheckboxInputProps<"input">>;
|
||||
};
|
||||
|
||||
export const Checkbox = (props: CheckboxProps) => (
|
||||
<KCheckbox
|
||||
class={cx("form-field", "checkbox", props.size, props.orientation, {
|
||||
inverted: props.inverted,
|
||||
ghost: props.ghost,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={"start"}>
|
||||
<Label
|
||||
labelComponent={KCheckbox.Label}
|
||||
descriptionComponent={KCheckbox.Description}
|
||||
{...props}
|
||||
/>
|
||||
<KCheckbox.Input {...props.input} />
|
||||
<KCheckbox.Control class="checkbox-control">
|
||||
<KCheckbox.Indicator>
|
||||
<Icon
|
||||
icon="Checkmark"
|
||||
inverted={props.inverted}
|
||||
color="secondary"
|
||||
size="100%"
|
||||
/>
|
||||
</KCheckbox.Indicator>
|
||||
</KCheckbox.Control>
|
||||
</Orienter>
|
||||
</KCheckbox>
|
||||
);
|
||||
export const Checkbox = (props: CheckboxProps) => {
|
||||
const alignment = () =>
|
||||
(props.orientation || "vertical") == "vertical" ? "start" : "center";
|
||||
|
||||
const iconChecked = (
|
||||
<Icon
|
||||
icon="Checkmark"
|
||||
inverted={props.inverted}
|
||||
color="secondary"
|
||||
size="100%"
|
||||
/>
|
||||
);
|
||||
|
||||
const iconUnchecked = (
|
||||
<Icon
|
||||
icon="Close"
|
||||
inverted={props.inverted}
|
||||
color="secondary"
|
||||
size="100%"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<KCheckbox
|
||||
class={cx("form-field", "checkbox", props.size, props.orientation, {
|
||||
inverted: props.inverted,
|
||||
ghost: props.ghost,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={alignment()}>
|
||||
<Label
|
||||
labelComponent={KCheckbox.Label}
|
||||
descriptionComponent={KCheckbox.Description}
|
||||
{...props}
|
||||
/>
|
||||
<KCheckbox.Input {...props.input} />
|
||||
<KCheckbox.Control class="checkbox-control">
|
||||
{props.readOnly && (
|
||||
<Show
|
||||
when={props.checked || props.defaultChecked}
|
||||
fallback={iconUnchecked}
|
||||
>
|
||||
{iconChecked}
|
||||
</Show>
|
||||
)}
|
||||
{!props.readOnly && (
|
||||
<KCheckbox.Indicator>{iconChecked}</KCheckbox.Indicator>
|
||||
)}
|
||||
</KCheckbox.Control>
|
||||
</Orienter>
|
||||
</KCheckbox>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
div.form-field.combobox {
|
||||
div.control {
|
||||
@apply flex flex-col w-full gap-2;
|
||||
@apply flex flex-col size-full gap-2;
|
||||
|
||||
div.selected-options {
|
||||
@apply flex flex-wrap gap-1 w-full min-h-5;
|
||||
@apply flex flex-wrap gap-1 size-full min-h-5;
|
||||
}
|
||||
|
||||
div.input-container {
|
||||
@@ -44,7 +44,8 @@ div.form-field.combobox {
|
||||
}
|
||||
|
||||
&[data-readonly] {
|
||||
@apply outline-def-2 cursor-not-allowed;
|
||||
@apply outline-none border-none bg-inherit;
|
||||
@apply p-0 resize-none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +77,10 @@ div.form-field.combobox {
|
||||
& > input {
|
||||
@apply px-1.5 py-1;
|
||||
font-size: 0.75rem;
|
||||
|
||||
&[data-readonly] {
|
||||
@apply p-0;
|
||||
}
|
||||
}
|
||||
|
||||
& > button.trigger {
|
||||
@@ -111,6 +116,10 @@ div.form-field.combobox {
|
||||
&[data-invalid] {
|
||||
@apply outline-semantic-error-4;
|
||||
}
|
||||
|
||||
&[data-readonly] {
|
||||
@apply outline-none border-none bg-inherit cursor-auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ export const ReadOnly: Story = {
|
||||
args: {
|
||||
...Tooltip.args,
|
||||
readOnly: true,
|
||||
defaultValue: "foo",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -83,14 +83,18 @@ export const DefaultItemControl = <Option,>(
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="input-container">
|
||||
<KCombobox.Input />
|
||||
<KCombobox.Trigger class="trigger">
|
||||
<KCombobox.Icon class="icon">
|
||||
<Icon icon="Expand" inverted={props.inverted} size="100%" />
|
||||
</KCombobox.Icon>
|
||||
</KCombobox.Trigger>
|
||||
</div>
|
||||
{!(props.readOnly && props.multiple) && (
|
||||
<div class="input-container">
|
||||
<KCombobox.Input />
|
||||
{!props.readOnly && (
|
||||
<KCombobox.Trigger class="trigger">
|
||||
<KCombobox.Icon class="icon">
|
||||
<Icon icon="Expand" inverted={props.inverted} size="100%" />
|
||||
</KCombobox.Icon>
|
||||
</KCombobox.Trigger>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -101,7 +105,13 @@ export const Combobox = <Option, OptGroup = never>(
|
||||
const itemControl = () => props.itemControl || DefaultItemControl;
|
||||
const itemComponent = () => props.itemComponent || DefaultItemComponent;
|
||||
|
||||
const align = () => (props.orientation === "horizontal" ? "start" : "center");
|
||||
const align = () => {
|
||||
if (props.readOnly) {
|
||||
return "center";
|
||||
} else {
|
||||
return props.orientation === "horizontal" ? "start" : "center";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<KCombobox
|
||||
|
||||
@@ -40,7 +40,7 @@ export type Story = StoryObj<typeof meta>;
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
legend: "Signup",
|
||||
fields: (props: FieldProps) => (
|
||||
children: (props: FieldProps) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...props}
|
||||
@@ -90,7 +90,7 @@ export const Error: Story = {
|
||||
args: {
|
||||
legend: "Signup",
|
||||
error: "You must enter a First Name",
|
||||
fields: (props: FieldProps) => (
|
||||
children: (props: FieldProps) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...props}
|
||||
|
||||
@@ -1,41 +1,57 @@
|
||||
import "./Fieldset.css";
|
||||
import { JSX } from "solid-js";
|
||||
import { JSX, splitProps } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import { FieldProps } from "./Field";
|
||||
|
||||
export interface FieldsetProps extends FieldProps {
|
||||
legend: string;
|
||||
disabled: boolean;
|
||||
export type FieldsetFieldProps = Pick<
|
||||
FieldProps,
|
||||
"orientation" | "inverted"
|
||||
> & {
|
||||
error?: string;
|
||||
fields: (props: FieldProps) => JSX.Element;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export interface FieldsetProps
|
||||
extends Pick<FieldProps, "orientation" | "inverted"> {
|
||||
legend?: string;
|
||||
disabled?: boolean;
|
||||
error?: string;
|
||||
children: (props: FieldsetFieldProps) => JSX.Element;
|
||||
}
|
||||
|
||||
export const Fieldset = (props: FieldsetProps) => {
|
||||
const orientation = () => props.orientation || "vertical";
|
||||
|
||||
const [fieldProps] = splitProps(props, [
|
||||
"orientation",
|
||||
"inverted",
|
||||
"disabled",
|
||||
"error",
|
||||
]);
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
role="group"
|
||||
class={cx(orientation(), { inverted: props.inverted })}
|
||||
disabled={props.disabled}
|
||||
class={cx({ inverted: props.inverted })}
|
||||
disabled={props.disabled || false}
|
||||
>
|
||||
<legend>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="default"
|
||||
weight="normal"
|
||||
color="tertiary"
|
||||
transform="uppercase"
|
||||
inverted={props.inverted}
|
||||
>
|
||||
{props.legend}
|
||||
</Typography>
|
||||
</legend>
|
||||
<div class="fields">
|
||||
{props.fields({ ...props, orientation: orientation() })}
|
||||
</div>
|
||||
{props.legend && (
|
||||
<legend>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="default"
|
||||
weight="normal"
|
||||
color="tertiary"
|
||||
transform="uppercase"
|
||||
inverted={props.inverted}
|
||||
>
|
||||
{props.legend}
|
||||
</Typography>
|
||||
</legend>
|
||||
)}
|
||||
<div class="fields">{props.children(fieldProps)}</div>
|
||||
{props.error && (
|
||||
<div class="error" role="alert">
|
||||
<Typography
|
||||
|
||||
@@ -12,7 +12,7 @@ div.form-label {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
|
||||
& > label[data-required] {
|
||||
& > label[data-required] & not(label[data-readonly]) {
|
||||
span.typography::after {
|
||||
@apply fg-def-4 ml-1;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface LabelProps {
|
||||
tooltip?: string;
|
||||
icon?: string;
|
||||
inverted?: boolean;
|
||||
readOnly?: boolean;
|
||||
validationState?: "valid" | "invalid";
|
||||
}
|
||||
|
||||
@@ -42,7 +43,7 @@ export const Label = (props: LabelProps) => {
|
||||
hierarchy="label"
|
||||
size={props.size || "default"}
|
||||
color={props.validationState == "invalid" ? "error" : "primary"}
|
||||
weight="bold"
|
||||
weight={props.readOnly ? "normal" : "bold"}
|
||||
inverted={props.inverted}
|
||||
>
|
||||
{props.label}
|
||||
|
||||
@@ -5,7 +5,7 @@ div.orienter {
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
@apply flex-row gap-2 justify-between;
|
||||
@apply flex-row justify-start;
|
||||
|
||||
& > div.form-label {
|
||||
@apply w-1/2 shrink;
|
||||
|
||||
@@ -34,7 +34,13 @@ div.form-field {
|
||||
}
|
||||
|
||||
&[data-readonly] {
|
||||
@apply outline-def-2 cursor-not-allowed;
|
||||
@apply outline-none border-none bg-inherit p-0 cursor-auto resize-none;
|
||||
}
|
||||
}
|
||||
|
||||
&.textarea textarea {
|
||||
&[data-readonly] {
|
||||
@apply overflow-y-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +78,10 @@ div.form-field {
|
||||
&.textarea textarea {
|
||||
@apply px-1.5 py-1;
|
||||
font-size: 0.75rem;
|
||||
|
||||
&[data-readonly] {
|
||||
@apply p-0;
|
||||
}
|
||||
}
|
||||
|
||||
&.text div.input-container {
|
||||
@@ -114,6 +124,11 @@ div.form-field {
|
||||
&[data-invalid] {
|
||||
@apply outline-semantic-error-4;
|
||||
}
|
||||
|
||||
&[data-readonly] {
|
||||
@apply outline-def-2 cursor-auto;
|
||||
@apply outline-none border-none bg-inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export const TextInput = (props: TextInputProps) => (
|
||||
{...props}
|
||||
/>
|
||||
<div class="input-container">
|
||||
{props.icon && (
|
||||
{props.icon && !props.readOnly && (
|
||||
<Icon
|
||||
icon={props.icon}
|
||||
inverted={props.inverted}
|
||||
@@ -42,7 +42,7 @@ export const TextInput = (props: TextInputProps) => (
|
||||
)}
|
||||
<TextField.Input
|
||||
{...props.input}
|
||||
classList={{ "has-icon": props.icon }}
|
||||
classList={{ "has-icon": props.icon && !props.readOnly }}
|
||||
/>
|
||||
</div>
|
||||
</Orienter>
|
||||
|
||||
24
pkgs/clan-app/ui/src/components/v2/Modal/Modal.css
Normal file
24
pkgs/clan-app/ui/src/components/v2/Modal/Modal.css
Normal file
@@ -0,0 +1,24 @@
|
||||
div.modal-content {
|
||||
@apply max-w-[512px];
|
||||
@apply rounded-md;
|
||||
|
||||
/* todo replace with a theme() color */
|
||||
box-shadow: 0.1875rem 0.1875rem 0 0 rgba(145, 172, 175, 0.32);
|
||||
|
||||
& > div.header {
|
||||
@apply flex items-center justify-center;
|
||||
@apply w-full px-2 py-1.5;
|
||||
@apply bg-def-3;
|
||||
@apply border border-def-2 rounded-tl-md rounded-tr-md;
|
||||
@apply border-b-def-3;
|
||||
|
||||
& > .title {
|
||||
@apply mx-auto;
|
||||
}
|
||||
}
|
||||
|
||||
& > div.body {
|
||||
@apply p-6 bg-def-1;
|
||||
@apply border border-def-2 rounded-bl-md rounded-br-md;
|
||||
}
|
||||
}
|
||||
74
pkgs/clan-app/ui/src/components/v2/Modal/Modal.stories.tsx
Normal file
74
pkgs/clan-app/ui/src/components/v2/Modal/Modal.stories.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { TagProps } from "@/src/components/v2/Tag/Tag";
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { fn } from "storybook/test";
|
||||
import {
|
||||
Modal,
|
||||
ModalContext,
|
||||
ModalProps,
|
||||
} from "@/src/components/v2/Modal/Modal";
|
||||
import { Fieldset } from "@/src/components/v2/Form/Fieldset";
|
||||
import { TextInput } from "@/src/components/v2/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/v2/Form/TextArea";
|
||||
import { Checkbox } from "@/src/components/v2/Form/Checkbox";
|
||||
import { Button } from "../Button/Button";
|
||||
|
||||
const meta: Meta<ModalProps> = {
|
||||
title: "Components/Modal",
|
||||
component: Modal,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<TagProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: "Example Modal",
|
||||
onClose: fn(),
|
||||
children: ({ close }: ModalContext) => (
|
||||
<form class="flex flex-col gap-5">
|
||||
<Fieldset legend="General">
|
||||
{(props) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...props}
|
||||
label="First Name"
|
||||
size="s"
|
||||
required={true}
|
||||
input={{ placeholder: "Ron" }}
|
||||
/>
|
||||
<TextInput
|
||||
{...props}
|
||||
label="Last Name"
|
||||
size="s"
|
||||
required={true}
|
||||
input={{ placeholder: "Burgundy" }}
|
||||
/>
|
||||
<TextArea
|
||||
{...props}
|
||||
label="Bio"
|
||||
size="s"
|
||||
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
|
||||
/>
|
||||
<Checkbox
|
||||
{...props}
|
||||
size="s"
|
||||
label="Accept Terms"
|
||||
required={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Fieldset>
|
||||
|
||||
<div class="flex w-full items-center justify-end gap-4">
|
||||
<Button size="s" hierarchy="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="s" type="submit" hierarchy="primary" onClick={close}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
),
|
||||
},
|
||||
};
|
||||
40
pkgs/clan-app/ui/src/components/v2/Modal/Modal.tsx
Normal file
40
pkgs/clan-app/ui/src/components/v2/Modal/Modal.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { createSignal, JSX } from "solid-js";
|
||||
import { Dialog as KDialog } from "@kobalte/core/dialog";
|
||||
import "./Modal.css";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
import Icon from "../Icon/Icon";
|
||||
|
||||
export interface ModalContext {
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export interface ModalProps {
|
||||
id?: string;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: (ctx: ModalContext) => JSX.Element;
|
||||
}
|
||||
|
||||
export const Modal = (props: ModalProps) => {
|
||||
const [open, setOpen] = createSignal(true);
|
||||
|
||||
return (
|
||||
<KDialog id={props.id} open={open()} modal={true}>
|
||||
<KDialog.Portal>
|
||||
<KDialog.Content class="modal-content">
|
||||
<div class="header">
|
||||
<Typography class="title" hierarchy="label" family="mono" size="xs">
|
||||
{props.title}
|
||||
</Typography>
|
||||
<KDialog.CloseButton onClick={() => setOpen(false)}>
|
||||
<Icon icon="Close" size="0.75rem" />
|
||||
</KDialog.CloseButton>
|
||||
</div>
|
||||
<div class="body">
|
||||
{props.children({ close: () => setOpen(false) })}
|
||||
</div>
|
||||
</KDialog.Content>
|
||||
</KDialog.Portal>
|
||||
</KDialog>
|
||||
);
|
||||
};
|
||||
@@ -4,8 +4,8 @@ div.sidebar-header {
|
||||
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
theme(colors.bg.inv.3) 0%,
|
||||
theme(colors.bg.inv.4) 0%
|
||||
theme(colors.bg.inv.2) 0%,
|
||||
theme(colors.bg.inv.3) 0%
|
||||
);
|
||||
|
||||
& > .dropdown-trigger {
|
||||
|
||||
33
pkgs/clan-app/ui/src/components/v2/Sidebar/SidebarPane.css
Normal file
33
pkgs/clan-app/ui/src/components/v2/Sidebar/SidebarPane.css
Normal file
@@ -0,0 +1,33 @@
|
||||
div.sidebar-pane {
|
||||
@apply h-full w-auto max-w-60 border-none;
|
||||
|
||||
& > div.header {
|
||||
@apply flex items-center justify-between px-3 py-2 rounded-t-[0.5rem];
|
||||
@apply border-t-[1px] border-t-bg-inv-3
|
||||
border-r-[1px] border-r-bg-inv-3
|
||||
border-b-2 border-b-bg-inv-4
|
||||
border-l-[1px] border-l-bg-inv-3;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
theme(colors.bg.inv.3) 0%,
|
||||
theme(colors.bg.inv.4) 100%
|
||||
);
|
||||
}
|
||||
|
||||
& > div.body {
|
||||
@apply flex flex-col gap-4 px-2 pt-4 pb-3 w-full h-full;
|
||||
@apply backdrop-blur-md;
|
||||
@apply rounded-b-[0.5rem]
|
||||
border-r-[1px] border-r-bg-inv-3
|
||||
border-b-2 border-b-bg-inv-4
|
||||
border-l-[1px] border-l-bg-inv-3;
|
||||
|
||||
background:
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
theme(colors.bg.inv.2) 0%,
|
||||
theme(colors.bg.inv.3) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import {
|
||||
SidebarPane,
|
||||
SidebarPaneProps,
|
||||
} from "@/src/components/v2/Sidebar/SidebarPane";
|
||||
import { SidebarSection } from "./SidebarSection";
|
||||
import { Divider } from "@/src/components/v2/Divider/Divider";
|
||||
import { TextInput } from "@/src/components/v2/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/v2/Form/TextArea";
|
||||
import { Checkbox } from "@/src/components/v2/Form/Checkbox";
|
||||
import { Combobox } from "../Form/Combobox";
|
||||
|
||||
const meta: Meta<SidebarPaneProps> = {
|
||||
title: "Components/Sidebar/Pane",
|
||||
component: SidebarPane,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<SidebarPaneProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: "Neptune",
|
||||
onClose: () => {
|
||||
console.log("closing");
|
||||
},
|
||||
children: (
|
||||
<>
|
||||
<SidebarSection
|
||||
title="General"
|
||||
onSave={async () => {
|
||||
console.log("saving general");
|
||||
}}
|
||||
>
|
||||
{(editing) => (
|
||||
<form class="flex flex-col gap-3">
|
||||
<TextInput
|
||||
label="First Name"
|
||||
size="s"
|
||||
inverted={true}
|
||||
required={true}
|
||||
readOnly={!editing}
|
||||
orientation="horizontal"
|
||||
input={{ value: "Ron" }}
|
||||
/>
|
||||
<Divider />
|
||||
<TextInput
|
||||
label="Last Name"
|
||||
size="s"
|
||||
inverted={true}
|
||||
required={true}
|
||||
readOnly={!editing}
|
||||
orientation="horizontal"
|
||||
input={{ value: "Burgundy" }}
|
||||
/>
|
||||
<Divider />
|
||||
<TextArea
|
||||
label="Bio"
|
||||
size="s"
|
||||
inverted={true}
|
||||
readOnly={!editing}
|
||||
orientation="horizontal"
|
||||
input={{
|
||||
value:
|
||||
"It's actually an optical illusion, it's the pattern on the pants.",
|
||||
rows: 4,
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
<Checkbox
|
||||
size="s"
|
||||
label="Share Profile"
|
||||
required={true}
|
||||
inverted={true}
|
||||
readOnly={!editing}
|
||||
checked={true}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
</SidebarSection>
|
||||
<SidebarSection
|
||||
title="Tags"
|
||||
onSave={async () => {
|
||||
console.log("saving general");
|
||||
}}
|
||||
>
|
||||
{(editing) => (
|
||||
<form class="flex flex-col gap-3">
|
||||
<Combobox
|
||||
size="s"
|
||||
inverted={true}
|
||||
required={true}
|
||||
readOnly={!editing}
|
||||
orientation="horizontal"
|
||||
multiple={true}
|
||||
options={["All", "Home Server", "Backup", "Random"]}
|
||||
defaultValue={["All", "Home Server", "Backup", "Random"]}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
</SidebarSection>
|
||||
<SidebarSection
|
||||
title="Advanced Settings"
|
||||
onSave={async () => {
|
||||
console.log("saving general");
|
||||
}}
|
||||
>
|
||||
{(editing) => <></>}
|
||||
</SidebarSection>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
27
pkgs/clan-app/ui/src/components/v2/Sidebar/SidebarPane.tsx
Normal file
27
pkgs/clan-app/ui/src/components/v2/Sidebar/SidebarPane.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { JSX } from "solid-js";
|
||||
import "./SidebarPane.css";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import Icon from "../Icon/Icon";
|
||||
import { Button as KButton } from "@kobalte/core/button";
|
||||
|
||||
export interface SidebarPaneProps {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const SidebarPane = (props: SidebarPaneProps) => {
|
||||
return (
|
||||
<div class="sidebar-pane">
|
||||
<div class="header">
|
||||
<Typography hierarchy="body" size="s" weight="bold" inverted={true}>
|
||||
{props.title}
|
||||
</Typography>
|
||||
<KButton onClick={props.onClose}>
|
||||
<Icon icon="Close" color="primary" size="0.75rem" inverted={true} />
|
||||
</KButton>
|
||||
</div>
|
||||
<div class="body">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
div.sidebar-section {
|
||||
@apply flex flex-col gap-2 w-full h-full;
|
||||
|
||||
& > div.header {
|
||||
@apply flex items-center justify-between px-1.5;
|
||||
|
||||
& > div.controls {
|
||||
@apply flex justify-end gap-2;
|
||||
}
|
||||
}
|
||||
|
||||
& > div.content {
|
||||
@apply w-full h-full px-1.5 py-3 rounded-md bg-inv-4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createSignal, JSX } from "solid-js";
|
||||
import "./SidebarSection.css";
|
||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||
import { Button as KButton } from "@kobalte/core/button";
|
||||
import Icon from "../Icon/Icon";
|
||||
|
||||
export interface SidebarSectionProps {
|
||||
title: string;
|
||||
onSave: () => Promise<void>;
|
||||
children: (editing: boolean) => JSX.Element;
|
||||
}
|
||||
|
||||
export const SidebarSection = (props: SidebarSectionProps) => {
|
||||
const [editing, setEditing] = createSignal(false);
|
||||
|
||||
const save = async () => {
|
||||
// todo how do we surface errors?
|
||||
await props.onSave();
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="sidebar-section">
|
||||
<div class="header">
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="xs"
|
||||
family="mono"
|
||||
weight="light"
|
||||
transform="uppercase"
|
||||
color="tertiary"
|
||||
inverted={true}
|
||||
>
|
||||
{props.title}
|
||||
</Typography>
|
||||
<div class="controls">
|
||||
{editing() && (
|
||||
<KButton>
|
||||
<Icon
|
||||
icon="Checkmark"
|
||||
color="tertiary"
|
||||
size="0.75rem"
|
||||
inverted={true}
|
||||
onClick={save}
|
||||
/>
|
||||
</KButton>
|
||||
)}
|
||||
<KButton onClick={() => setEditing(!editing())}>
|
||||
<Icon
|
||||
icon={editing() ? "Close" : "Edit"}
|
||||
color="tertiary"
|
||||
size="0.75rem"
|
||||
inverted={true}
|
||||
/>
|
||||
</KButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">{props.children(editing())}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,9 @@
|
||||
/* Body */
|
||||
.typography {
|
||||
&.weight-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
&.weight-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Color, fgClass } from "@/src/components/v2/colors";
|
||||
|
||||
export type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
|
||||
export type Hierarchy = "body" | "title" | "headline" | "label" | "teaser";
|
||||
export type Weight = "normal" | "medium" | "bold";
|
||||
export type Weight = "normal" | "medium" | "bold" | "light";
|
||||
export type Family = "regular" | "condensed" | "mono";
|
||||
export type Transform = "uppercase" | "lowercase" | "capitalize";
|
||||
|
||||
@@ -80,9 +80,10 @@ const defaultFamilyMap: Record<Hierarchy, Family> = {
|
||||
};
|
||||
|
||||
const weightMap: Record<Weight, string> = {
|
||||
normal: cx("weight-normal"),
|
||||
medium: cx("weight-medium"),
|
||||
bold: cx("weight-bold"),
|
||||
normal: "weight-normal",
|
||||
medium: "weight-medium",
|
||||
bold: "weight-bold",
|
||||
light: "weight-light",
|
||||
};
|
||||
|
||||
interface _TypographyProps<H extends Hierarchy> {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const clanMetaQuery = (uri: string | undefined = undefined) =>
|
||||
queryFn: async () => {
|
||||
console.log("fetching clan meta", clanURI);
|
||||
|
||||
const result = await callApi("show_clan_meta", {
|
||||
const result = await callApi("get_clan_details", {
|
||||
flake: { identifier: clanURI! },
|
||||
}).promise;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export const CreateClan = () => {
|
||||
const r = await callApi("create_clan", {
|
||||
opts: {
|
||||
dest: target_dir[0],
|
||||
template_name: template,
|
||||
template: template,
|
||||
initial: {
|
||||
meta,
|
||||
services: {},
|
||||
@@ -65,7 +65,7 @@ export const CreateClan = () => {
|
||||
}
|
||||
|
||||
// Will generate a key if it doesn't exist, and add a user to the clan
|
||||
const k = await callApi("keygen", {
|
||||
const k = await callApi("create_secrets_user", {
|
||||
flake_dir: target_dir[0],
|
||||
}).promise;
|
||||
|
||||
@@ -202,6 +202,6 @@ export const CreateClan = () => {
|
||||
};
|
||||
|
||||
type Meta = Extract<
|
||||
OperationResponse<"show_clan_meta">,
|
||||
OperationResponse<"get_clan_details">,
|
||||
{ status: "success" }
|
||||
>["data"];
|
||||
|
||||
@@ -23,7 +23,7 @@ const EditClanForm = (props: EditClanFormProps) => {
|
||||
const handleSubmit: SubmitHandler<GeneralData> = async (values, event) => {
|
||||
await toast.promise(
|
||||
(async () => {
|
||||
await callApi("update_clan_meta", {
|
||||
await callApi("set_clan_details", {
|
||||
options: {
|
||||
flake: { identifier: props.directory },
|
||||
meta: values,
|
||||
@@ -128,7 +128,7 @@ const EditClanForm = (props: EditClanFormProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
type GeneralData = SuccessQuery<"show_clan_meta">["data"];
|
||||
type GeneralData = SuccessQuery<"get_clan_details">["data"];
|
||||
|
||||
export const ClanDetails = () => {
|
||||
const params = useParams();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "../../components/Button/Button";
|
||||
import Icon from "@/src/components/icon";
|
||||
|
||||
type ServiceModel = Extract<
|
||||
OperationResponse<"show_mdns">,
|
||||
OperationResponse<"list_mdns_services">,
|
||||
{ status: "success" }
|
||||
>["data"]["services"];
|
||||
|
||||
@@ -16,7 +16,7 @@ export const HostList: Component = () => {
|
||||
<div class="" data-tip="Refresh install targets">
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => callApi("show_mdns", {})}
|
||||
onClick={() => callApi("list_mdns_services", {})}
|
||||
startIcon={<Icon icon="Update" />}
|
||||
></Button>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ from . import (
|
||||
clan,
|
||||
secrets,
|
||||
select,
|
||||
templates,
|
||||
state,
|
||||
vms,
|
||||
)
|
||||
@@ -195,6 +196,13 @@ For more detailed information, visit: {help_hyperlink("getting-started", "https:
|
||||
|
||||
clan.register_parser(parser_flake)
|
||||
|
||||
parser_templates = subparsers.add_parser(
|
||||
"templates",
|
||||
help="Subcommands to interact with templates",
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
templates.register_parser(parser_templates)
|
||||
|
||||
parser_flash = subparsers.add_parser(
|
||||
"flash",
|
||||
help="Flashes your machine to an USB drive",
|
||||
|
||||
@@ -4,7 +4,6 @@ import argparse
|
||||
from clan_cli.clan.inspect import register_inspect_parser
|
||||
|
||||
from .create import register_create_parser
|
||||
from .list import register_list_parser
|
||||
|
||||
|
||||
# takes a (sub)parser and configures it
|
||||
@@ -19,5 +18,3 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
register_create_parser(create_parser)
|
||||
inspect_parser = subparser.add_parser("inspect", help="Inspect a clan ")
|
||||
register_inspect_parser(inspect_parser)
|
||||
list_parser = subparser.add_parser("list", help="List clan templates")
|
||||
register_list_parser(list_parser)
|
||||
|
||||
@@ -4,36 +4,17 @@ import logging
|
||||
from pathlib import Path
|
||||
|
||||
from clan_lib.clan.create import CreateOptions, create_clan
|
||||
from clan_lib.templates import (
|
||||
InputPrio,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_create_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--input",
|
||||
type=str,
|
||||
help="""Flake input name to use as template source
|
||||
can be specified multiple times, inputs are tried in order of definition
|
||||
Example: --input clan --input clan-core
|
||||
""",
|
||||
action="append",
|
||||
default=[],
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no-self",
|
||||
help="Do not look into own flake for templates",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--template",
|
||||
type=str,
|
||||
help="Clan template name",
|
||||
help="""Reference to the template to use for the clan. default="default". In the format '<flake_ref>#template_name' Where <flake_ref> is a flake reference (e.g. github:org/repo) or a local path (e.g. '.' ).
|
||||
Omitting '<flake_ref>#' will use the builtin templates (e.g. just 'default' from clan-core ).
|
||||
""",
|
||||
default="default",
|
||||
)
|
||||
|
||||
@@ -59,19 +40,10 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
|
||||
)
|
||||
|
||||
def create_flake_command(args: argparse.Namespace) -> None:
|
||||
if len(args.input) == 0:
|
||||
args.input = ["clan", "clan-core"]
|
||||
|
||||
if args.no_self:
|
||||
input_prio = InputPrio.try_inputs(tuple(args.input))
|
||||
else:
|
||||
input_prio = InputPrio.try_self_then_inputs(tuple(args.input))
|
||||
|
||||
create_clan(
|
||||
CreateOptions(
|
||||
input_prio=input_prio,
|
||||
dest=args.path,
|
||||
template_name=args.template,
|
||||
template=args.template,
|
||||
setup_git=not args.no_git,
|
||||
src_flake=args.flake,
|
||||
update_clan=not args.no_update,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user