1
0
forked from clan/clan-core

Compare commits

..

2 Commits

65 changed files with 362 additions and 1309 deletions

View File

@ -10,7 +10,6 @@ in
hostPkgs = pkgs; hostPkgs = pkgs;
# speed-up evaluation # speed-up evaluation
defaults = { defaults = {
nix.package = pkgs.nixVersions.latest;
documentation.enable = lib.mkDefault false; documentation.enable = lib.mkDefault false;
boot.isContainer = true; boot.isContainer = true;

View File

@ -10,7 +10,6 @@ in
defaults = { defaults = {
documentation.enable = lib.mkDefault false; documentation.enable = lib.mkDefault false;
nix.settings.min-free = 0; nix.settings.min-free = 0;
nix.package = pkgs.nixVersions.latest;
}; };
# to accept external dependencies such as disko # to accept external dependencies such as disko

View File

@ -6,6 +6,15 @@
}: }:
let let
cfg = config.clan.matrix-synapse; cfg = config.clan.matrix-synapse;
nginx-vhost = "matrix.${config.clan.matrix-synapse.domain}";
element-web =
pkgs.runCommand "element-web-with-config" { nativeBuildInputs = [ pkgs.buildPackages.jq ]; } ''
cp -r ${pkgs.element-web} $out
chmod -R u+w $out
jq '."default_server_config"."m.homeserver" = { "base_url": "https://${nginx-vhost}:443", "server_name": "${config.clan.matrix-synapse.domain}" }' \
> $out/config.json < ${pkgs.element-web}/config.json
ln -s $out/config.json $out/config.${nginx-vhost}.json
'';
in in
{ {
options.clan.matrix-synapse = { options.clan.matrix-synapse = {
@ -13,6 +22,7 @@ in
domain = lib.mkOption { domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = "The domain name of the matrix server"; description = "The domain name of the matrix server";
example = "example.com";
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@ -49,14 +59,40 @@ in
} }
]; ];
}; };
extraConfigFiles = [ "/var/lib/matrix-synapse/registration_shared_secret.yaml" ]; extraConfigFiles = [ "/run/synapse-registration-shared-secret.yaml" ];
}; };
systemd.tmpfiles.settings."synapse" = {
"/run/synapse-registration-shared-secret.yaml" = {
C.argument = config.clanCore.facts.services.matrix-synapse.secret.synapse-registration_shared_secret.path;
z = {
mode = "0400";
user = "matrix-synapse";
};
};
};
systemd.services.matrix-synapse.serviceConfig.ExecStartPre = [ systemd.services.matrix-synapse.serviceConfig.ExecStartPre = [
"+${pkgs.writeScript "copy_registration_shared_secret" '' "+${pkgs.writeShellScript "create-matrix-synapse-db" ''
#!/bin/sh export PATH=${
cp ${config.clanCore.facts.services.matrix-synapse.secret.synapse-registration_shared_secret.path} /var/lib/matrix-synapse/registration_shared_secret.yaml lib.makeBinPath [
chown matrix-synapse:matrix-synapse /var/lib/matrix-synapse/registration_shared_secret.yaml config.services.postgresql.package
chmod 600 /var/lib/matrix-synapse/registration_shared_secret.yaml pkgs.util-linux
pkgs.gnugrep
]
}
psql() { runuser -u postgres -- psql "$@"; }
# wait for postgres to be ready
while ! runuser -u postgres pg_isready; do
sleep 1
done
if ! psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'matrix-synapse'" | grep -q 1; then
psql -c "CREATE DATABASE \"matrix-synapse\" TEMPLATE template0 LC_COLLATE = 'C' LC_CTYPE = 'C'"
fi
# create user if it doesn't exist and make it owner of the database
if ! psql -tAc "SELECT 1 FROM pg_user WHERE usename = 'matrix-synapse'" | grep -q 1; then
psql -c "CREATE USER \"matrix-synapse\""
psql -c "ALTER DATABASE \"matrix-synapse\" OWNER TO \"matrix-synapse\""
fi
''}" ''}"
]; ];
@ -72,22 +108,6 @@ in
}; };
services.postgresql.enable = true; services.postgresql.enable = true;
# we need to use both ensusureDatabases and initialScript, because the former runs everytime but with the wrong collation
services.postgresql = {
ensureDatabases = [ "matrix-synapse" ];
ensureUsers = [
{
name = "matrix-synapse";
ensureDBOwnership = true;
}
];
initialScript = pkgs.writeText "synapse-init.sql" ''
CREATE DATABASE "matrix-synapse"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
'';
};
services.nginx = { services.nginx = {
enable = true; enable = true;
virtualHosts = { virtualHosts = {
@ -102,7 +122,7 @@ in
return 200 '${ return 200 '${
builtins.toJSON { builtins.toJSON {
"m.homeserver" = { "m.homeserver" = {
"base_url" = "https://matrix.${cfg.domain}"; "base_url" = "https://${nginx-vhost}";
}; };
"m.identity_server" = { "m.identity_server" = {
"base_url" = "https://vector.im"; "base_url" = "https://vector.im";
@ -111,15 +131,12 @@ in
}'; }';
''; '';
}; };
"matrix.${cfg.domain}" = { ${nginx-vhost} = {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
locations."/_matrix" = { locations."/_matrix".proxyPass = "http://localhost:8008";
proxyPass = "http://localhost:8008"; locations."/_synapse".proxyPass = "http://localhost:8008";
}; locations."/".root = element-web;
locations."/test".extraConfig = ''
return 200 "Hello, world!";
'';
}; };
}; };
}; };

View File

@ -3,15 +3,9 @@
options.clan.static-hosts = { options.clan.static-hosts = {
excludeHosts = lib.mkOption { excludeHosts = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = default = [ config.clanCore.machineName ];
if config.clan.static-hosts.topLevelDomain != "" then [ ] else [ config.clanCore.machineName ];
description = "Hosts that should be excluded"; description = "Hosts that should be excluded";
}; };
topLevelDomain = lib.mkOption {
type = lib.types.str;
default = "";
description = "Top level domain to reach hosts";
};
}; };
config.networking.hosts = config.networking.hosts =
@ -30,15 +24,7 @@
let let
path = zerotierIpMachinePath machine; path = zerotierIpMachinePath machine;
in in
if builtins.pathExists path then if builtins.pathExists path then lib.nameValuePair (builtins.readFile path) [ machine ] else null
lib.nameValuePair (builtins.readFile path) (
if (config.clan.static-hosts.topLevelDomain == "") then
[ machine ]
else
[ "${machine}.${config.clan.static-hosts.topLevelDomain}" ]
)
else
null
) filteredMachines ) filteredMachines
); );
} }

View File

@ -20,7 +20,7 @@ let
if builtins.pathExists fullPath then builtins.readFile fullPath else null if builtins.pathExists fullPath then builtins.readFile fullPath else null
) machines; ) machines;
networkIds = lib.filter (machine: machine != null) networkIdsUnchecked; networkIds = lib.filter (machine: machine != null) networkIdsUnchecked;
networkId = if builtins.length networkIds == 0 then null else builtins.elemAt networkIds 0; networkId = builtins.elemAt networkIds 0;
in in
#TODO:trace on multiple found network-ids #TODO:trace on multiple found network-ids
#TODO:trace on no single found networkId #TODO:trace on no single found networkId
@ -38,7 +38,7 @@ in
machines = builtins.readDir machineDir; machines = builtins.readDir machineDir;
zerotierIpMachinePath = machines: machineDir + machines + "/facts/zerotier-ip"; zerotierIpMachinePath = machines: machineDir + machines + "/facts/zerotier-ip";
filteredMachines = lib.filterAttrs ( filteredMachines = lib.filterAttrs (
name: _: !(lib.elem name config.clan.zerotier-static-peers.excludeHosts) name: _: !(lib.elem name config.clan.static-hosts.excludeHosts)
) machines; ) machines;
hosts = lib.mapAttrsToList (host: _: host) ( hosts = lib.mapAttrsToList (host: _: host) (
lib.mapAttrs' ( lib.mapAttrs' (

View File

@ -27,8 +27,7 @@
packages = [ packages = [
select-shell select-shell
pkgs.tea pkgs.tea
# Better error messages than nix 2.18 pkgs.nix
pkgs.nixVersions.latest
self'.packages.tea-create-pr self'.packages.tea-create-pr
self'.packages.merge-after-ci self'.packages.merge-after-ci
self'.packages.pending-reviews self'.packages.pending-reviews

View File

@ -15,74 +15,56 @@ Let's get your development environment up and running:
1. **Install Nix Package Manager**: 1. **Install Nix Package Manager**:
- You can install the Nix package manager by either [downloading the Nix installer](https://github.com/DeterminateSystems/nix-installer/releases) or running this command: - You can install the Nix package manager by either [downloading the Nix installer](https://github.com/DeterminateSystems/nix-installer/releases) or running this command:
```bash ```bash
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
``` ```
2. **Install direnv**: 2. **Install direnv**:
- To automatically setup a devshell on entering the directory - To automatically setup a devshell on entering the directory
```bash ```bash
nix profile install nixpkgs#nix-direnv-flakes nix profile install nixpkgs#nix-direnv-flakes
``` ```
3. **Add direnv to your shell**: 3. **Add direnv to your shell**:
- Direnv needs to [hook into your shell](https://direnv.net/docs/hook.html) to work. - Direnv needs to [hook into your shell](https://direnv.net/docs/hook.html) to work.
You can do this by executing following command. The example below will setup direnv for `zsh` and `bash` You can do this by executing following command. The example below will setup direnv for `zsh` and `bash`
```bash ```bash
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL" echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL"
``` ```
4. **Create a Gitea Account**: 4. **Create a Gitea Account**:
- Register an account on https://git.clan.lol - Register an account on https://git.clan.lol
- Fork the [clan-core](https://git.clan.lol/clan/clan-core) repository - Fork the [clan-core](https://git.clan.lol/clan/clan-core) repository
- Clone the repository and navigate to it - Clone the repository and navigate to it
- Add a new remote called upstream: - Add a new remote called upstream:
```bash ```bash
git remote add upstream gitea@git.clan.lol:clan/clan-core.git git remote add upstream gitea@git.clan.lol:clan/clan-core.git
```
5. **Register Your Gitea Account Locally**:
- Execute the following command to add your Gitea account locally:
```bash
tea login add
```
- Fill out the prompt as follows:
- URL of Gitea instance: `https://git.clan.lol`
- Name of new Login [git.clan.lol]:
- Do you have an access token? No
- Username: YourUsername
- Password: YourPassword
- Set Optional settings: No
6. **Allow .envrc**:
- When you enter the directory, you'll receive an error message like this:
```bash
direnv: error .envrc is blocked. Run `direnv allow` to approve its content
```
- Execute `direnv allow` to automatically execute the shell script `.envrc` when entering the directory.
7. **(Optional) Install Git Hooks**:
- To syntax check your code you can run:
```bash
nix fmt
```
- To make this automatic install the git hooks
```bash
./scripts/pre-commit
```
8. **Open a Pull Request**:
- To automatically open up a pull request you can use our tool called:
``` ```
merge-after-ci --reviewers Mic92 Lassulus Qubasa
5. **Allow .envrc**:
- When you enter the directory, you'll receive an error message like this:
```bash
direnv: error .envrc is blocked. Run `direnv allow` to approve its content
```
- Execute `direnv allow` to automatically execute the shell script `.envrc` when entering the directory.
6. **(Optional) Install Git Hooks**:
- To syntax check your code you can run:
```bash
nix fmt
``` ```
- To make this automatic install the git hooks
```bash
./scripts/pre-commit
```
7. **Open a Pull Request**:
- Go to the webinterface and open up a pull request
# Debugging # Debugging

View File

@ -16,26 +16,15 @@ def define_env(env: Any) -> None:
@env.macro @env.macro
def asciinema(name: str) -> str: def asciinema(name: str) -> str:
return f"""<div id="{name}"> return f"""<div id="{name}">
<script src="{asciinema_dir}/asciinema-player.min.js"></script>
<script> <script>
// Function to load the script and then create the Asciinema player AsciinemaPlayer.create('{video_dir + name}',
function loadAsciinemaPlayer() {{ document.getElementById("{name}"), {{
var script = document.createElement('script'); loop: true,
script.src = "{asciinema_dir}/asciinema-player.min.js"; autoPlay: true,
script.onload = function() {{ controls: false,
AsciinemaPlayer.create('{video_dir + name}', document.getElementById("{name}"), {{ speed: 1.5,
loop: true, theme: "solarized-light"
autoPlay: true, }});
controls: false,
speed: 1.5,
theme: "solarized-light"
}});
}};
document.head.appendChild(script);
}}
// Load the Asciinema player script
loadAsciinemaPlayer();
</script> </script>
<link rel="stylesheet" type="text/css" href="{asciinema_dir}/asciinema-player.css" />
</div>""" </div>"""

View File

@ -1,4 +1,4 @@
site_name: Clan Documentation site_name: Clan Docs
site_url: https://docs.clan.lol site_url: https://docs.clan.lol
repo_url: https://git.clan.lol/clan/clan-core/ repo_url: https://git.clan.lol/clan/clan-core/
repo_name: clan-core repo_name: clan-core
@ -96,7 +96,7 @@ site_dir: out
theme: theme:
font: false font: false
logo: https://clan.lol/static/logo/clan-white.png logo: https://clan.lol/static/logo/clan-white.png
favicon: https://clan.lol/static/dark-favicon/128x128.png favicon: https://clan.lol/static/logo/clan-dark.png
name: material name: material
features: features:
- navigation.instant - navigation.instant
@ -105,8 +105,7 @@ theme:
- content.code.copy - content.code.copy
- content.tabs.link - content.tabs.link
icon: icon:
repo: fontawesome/brands/git-alt repo: fontawesome/brands/git
custom_dir: overrides
palette: palette:
# Palette toggle for light mode # Palette toggle for light mode
@ -129,6 +128,8 @@ theme:
extra_css: extra_css:
- static/extra.css - static/extra.css
- static/asciinema-player/custom-theme.css
- static/asciinema-player/asciinema-player.css
extra: extra:
social: social:

View File

@ -1,12 +0,0 @@
{% extends "base.html" %}
{% block extrahead %}
<meta property="og:title" content="Clan - Documentation, Blog & Getting Started Guide" />
<meta property="og:description" content="Documentation for Clan. The peer-to-peer machine deployment framework." />
<meta property="og:image" content="https://clan.lol/static/dark-favicon/128x128.png" />
<meta property="og:url" content="https://docs.clan.lol" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Clan" />
<meta property="og:locale" content="en_US" />
{% endblock %}

View File

@ -54,9 +54,9 @@ sudo umount /dev/sdb1
flash-installer flash-installer
``` ```
The `--ssh-pubkey`, `--language` and `--keymap` are optional. The `--ssh-pubkey`, `--language` and `--keymap` are optional.
Replace `$HOME/.ssh/id_ed25519.pub` with a path to your SSH public key. Replace `$HOME/.ssh/id_ed25519.pub` with a path to your SSH public key.
If you do not have an ssh key yet, you can generate one with `ssh-keygen -t ed25519` command. If you do not have an ssh key yet, you can generate one with `ssh-keygen -t ed25519` command.
!!! Danger "Specifying the wrong device can lead to unrecoverable data loss." !!! Danger "Specifying the wrong device can lead to unrecoverable data loss."

View File

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717177033, "lastModified": 1716773194,
"narHash": "sha256-G3CZJafCO8WDy3dyA2EhpUJEmzd5gMJ2IdItAg0Hijw=", "narHash": "sha256-rskkGmWlvYFb+CXedBiL8eWEuED0Es0XR4CkJ11RQKY=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "0274af4c92531ebfba4a5bd493251a143bc51f3c", "rev": "10986091e47fb1180620b78438512b294b7e8f67",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -27,11 +27,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717285511, "lastModified": 1715865404,
"narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", "narHash": "sha256-/GJvTdTpuDjNn84j82cU6bXztE0MSkdnTWClUCRub78=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", "rev": "8dc45382d5206bd292f9c2768b8058a8fd8311d9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -57,11 +57,11 @@
}, },
"nixos-2311": { "nixos-2311": {
"locked": { "locked": {
"lastModified": 1717017538, "lastModified": 1716767563,
"narHash": "sha256-S5kltvDDfNQM3xx9XcvzKEOyN2qk8Sa+aSOLqZ+1Ujc=", "narHash": "sha256-xaSLDTqKIU55HsCkDnzFKmPiJO2z1xAAvrhUlwlmT2M=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "64e468fd2652105710d86cd2ae3e65a5a6d58dec", "rev": "0c007b36981bdbd69ccf0c7df30a174e63660667",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -100,11 +100,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717040312, "lastModified": 1716786664,
"narHash": "sha256-yI/en4IxuCEClIUpIs3QTyYCCtmSPLOhwLJclfNwdeg=", "narHash": "sha256-iszhOLhxnv+TX/XM2gAX4LhTCoMzLuG51ObZq/eyDx8=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixos-images", "repo": "nixos-images",
"rev": "47bfb55316e105390dd761e0b6e8e0be09462b67", "rev": "2478833ef8cc6de3d9e331f53b6f3682e425f207",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -115,11 +115,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1717298511, "lastModified": 1716924492,
"narHash": "sha256-9sXuJn/nL+9ImeYtlspTvjt83z1wIgU+9AwfNbnq+tI=", "narHash": "sha256-9/Ro5/MfI+PNMF8jzh7+gXDPUHeOzL1e/iw3p4z6Ttc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6634a0509e9e81e980b129435fbbec518ab246d0", "rev": "4ae13643e7f2cd4bc6555fce074865d9d14e7c24",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -148,11 +148,11 @@
"nixpkgs-stable": [] "nixpkgs-stable": []
}, },
"locked": { "locked": {
"lastModified": 1717297459, "lastModified": 1716692524,
"narHash": "sha256-cZC2f68w5UrJ1f+2NWGV9Gx0dEYmxwomWN2B0lx0QRA=", "narHash": "sha256-sALodaA7Zkp/JD6ehgwc0UCBrSBfB4cX66uFGTsqeFU=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "ab2a43b0d21d1d37d4d5726a892f714eaeb4b075", "rev": "962797a8d7f15ed7033031731d0bb77244839960",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -168,11 +168,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717278143, "lastModified": 1715940852,
"narHash": "sha256-u10aDdYrpiGOLoxzY/mJ9llST9yO8Q7K/UlROoNxzDw=", "narHash": "sha256-wJqHMg/K6X3JGAE9YLM0LsuKrKb4XiBeVaoeMNlReZg=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "3eb96ca1ae9edf792a8e0963cc92fddfa5a87706", "rev": "2fba33a182602b9d49f0b2440513e5ee091d838b",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -24,14 +24,10 @@
}; };
outputs = outputs =
inputs@{ flake-parts, self, ... }: inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } ( flake-parts.lib.mkFlake { inherit inputs; } (
{ ... }: { ... }:
{ {
clan = {
# meta.name = "clan-core";
directory = self;
};
systems = [ systems = [
"x86_64-linux" "x86_64-linux"
"aarch64-linux" "aarch64-linux"

View File

@ -17,33 +17,6 @@ let
cfg = config.clan; cfg = config.clan;
in in
{ {
imports = [
# TODO: figure out how to print the deprecation warning
# "${inputs.nixpkgs}/nixos/modules/misc/assertions.nix"
(lib.mkRenamedOptionModule
[
"clan"
"clanName"
]
[
"clan"
"meta"
"name"
]
)
(lib.mkRenamedOptionModule
[
"clan"
"clanIcon"
]
[
"clan"
"meta"
"icon"
]
)
];
options.clan = { options.clan = {
directory = mkOption { directory = mkOption {
type = types.path; type = types.path;
@ -60,27 +33,15 @@ in
default = { }; default = { };
description = "Allows to include machine-specific modules i.e. machines.\${name} = { ... }"; description = "Allows to include machine-specific modules i.e. machines.\${name} = { ... }";
}; };
clanName = mkOption {
# Checks are performed in 'buildClan' type = types.str;
# Not everyone uses flake-parts description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.";
meta = { };
name = lib.mkOption { clanIcon = mkOption {
type = types.nullOr types.str; type = types.nullOr types.path;
default = null; default = null;
description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to."; description = "A path to an icon to be used for the clan, should be the same for all machines";
};
icon = mkOption {
type = types.nullOr types.path;
default = null;
description = "A path to an icon to be used for the clan in the GUI";
};
description = mkOption {
type = types.nullOr types.str;
default = null;
description = "A short description of the clan";
};
}; };
pkgsForSystem = mkOption { pkgsForSystem = mkOption {
type = types.functionTo types.raw; type = types.functionTo types.raw;
default = _system: null; default = _system: null;
@ -91,7 +52,6 @@ in
clanInternals = lib.mkOption { clanInternals = lib.mkOption {
type = lib.types.submodule { type = lib.types.submodule {
options = { options = {
meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; }; all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); }; machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
machinesFunc = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); }; machinesFunc = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
@ -105,8 +65,9 @@ in
directory directory
specialArgs specialArgs
machines machines
clanName
clanIcon
pkgsForSystem pkgsForSystem
meta
; ;
}; };
}; };

View File

@ -7,58 +7,16 @@
directory, # The directory containing the machines subdirectory directory, # The directory containing the machines subdirectory
specialArgs ? { }, # Extra arguments to pass to nixosSystem i.e. useful to make self available specialArgs ? { }, # Extra arguments to pass to nixosSystem i.e. useful to make self available
machines ? { }, # allows to include machine-specific modules i.e. machines.${name} = { ... } machines ? { }, # allows to include machine-specific modules i.e. machines.${name} = { ... }
# DEPRECATED: use meta.name instead clanName, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
clanName ? null, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
# DEPRECATED: use meta.icon instead
clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines
meta ? { }, # A set containing clan meta: name :: string, icon :: string, description :: string
pkgsForSystem ? (_system: null), # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system. pkgsForSystem ? (_system: null), # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
# This improves performance, but all nipxkgs.* options will be ignored. # This improves performance, but all nipxkgs.* options will be ignored.
}: }:
let let
deprecationWarnings = [
(lib.warnIf (
clanName != null
) "clanName is deprecated, please use meta.name instead. ${clanName}" null)
(lib.warnIf (clanIcon != null) "clanIcon is deprecated, please use meta.icon instead" null)
];
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") ( machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
builtins.readDir (directory + /machines) builtins.readDir (directory + /machines)
); );
mergedMeta =
let
metaFromFile =
if (builtins.pathExists "${directory}/clan/meta.json") then
let
settings = builtins.fromJSON (builtins.readFile "${directory}/clan/meta.json");
in
settings
else
{ };
legacyMeta = lib.filterAttrs (_: v: v != null) {
name = clanName;
icon = clanIcon;
};
optionsMeta = lib.filterAttrs (_: v: v != null) meta;
warnings =
builtins.map (
name:
if
metaFromFile.${name} or null != optionsMeta.${name} or null && optionsMeta.${name} or null != null
then
lib.warn "meta.${name} is set in different places. (exlicit option meta.${name} overrides ${directory}/clan/meta.json)" null
else
null
) (builtins.attrNames metaFromFile)
++ [ (if (res.name or null == null) then (throw "meta.name should be set") else null) ];
res = metaFromFile // legacyMeta // optionsMeta;
in
# Print out warnings before returning the merged result
builtins.deepSeq warnings res;
machineSettings = machineSettings =
machineName: machineName:
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily # CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
@ -100,15 +58,11 @@ let
(machines.${name} or { }) (machines.${name} or { })
( (
{ {
# Settings
clanCore.clanDir = directory;
# Inherited from clan wide settings
clanCore.clanName = meta.name or clanName;
clanCore.clanIcon = meta.icon or clanIcon;
# Machine specific settings
clanCore.machineName = name;
networking.hostName = lib.mkDefault name; networking.hostName = lib.mkDefault name;
clanCore.clanName = clanName;
clanCore.clanIcon = clanIcon;
clanCore.clanDir = directory;
clanCore.machineName = name;
nixpkgs.hostPlatform = lib.mkDefault system; nixpkgs.hostPlatform = lib.mkDefault system;
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs) # speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
@ -173,15 +127,10 @@ let
) supportedSystems ) supportedSystems
); );
in in
builtins.deepSeq deprecationWarnings { {
inherit nixosConfigurations; inherit nixosConfigurations;
clanInternals = { clanInternals = {
# Evaluated clan meta
# Merged /clan/meta.json with overrides from buildClan
meta = mergedMeta;
# machine specifics
machines = configsPerSystem; machines = configsPerSystem;
machinesFunc = configsFuncPerSystem; machinesFunc = configsFuncPerSystem;
all-machines-json = lib.mapAttrs ( all-machines-json = lib.mapAttrs (

View File

@ -152,7 +152,6 @@ For more detailed information, visit: https://docs.clan.lol/getting-started
), ),
formatter_class=argparse.RawTextHelpFormatter, formatter_class=argparse.RawTextHelpFormatter,
) )
flakes.register_parser(parser_flake) flakes.register_parser(parser_flake)
parser_config = subparsers.add_parser( parser_config = subparsers.add_parser(

View File

@ -2,7 +2,6 @@ import argparse
import json import json
import logging import logging
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError from ..errors import ClanError
from ..machines.machines import Machine from ..machines.machines import Machine
@ -41,10 +40,8 @@ def create_command(args: argparse.Namespace) -> None:
def register_create_parser(parser: argparse.ArgumentParser) -> None: def register_create_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", type=str, help="machine in the flake to create backups of" "machine", type=str, help="machine in the flake to create backups of"
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument("--provider", type=str, help="backup provider to use") parser.add_argument("--provider", type=str, help="backup provider to use")
parser.set_defaults(func=create_command) parser.set_defaults(func=create_command)

View File

@ -3,7 +3,6 @@ import json
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError from ..errors import ClanError
from ..machines.machines import Machine from ..machines.machines import Machine
@ -58,9 +57,8 @@ def list_command(args: argparse.Namespace) -> None:
def register_list_parser(parser: argparse.ArgumentParser) -> None: def register_list_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", type=str, help="machine in the flake to show backups of" "machine", type=str, help="machine in the flake to show backups of"
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument("--provider", type=str, help="backup provider to filter by") parser.add_argument("--provider", type=str, help="backup provider to filter by")
parser.set_defaults(func=list_command) parser.set_defaults(func=list_command)

View File

@ -56,7 +56,7 @@ def handle_output(process: subprocess.Popen, log: Log) -> tuple[str, str]:
sys.stderr.buffer.write(ret) sys.stderr.buffer.write(ret)
sys.stderr.flush() sys.stderr.flush()
stderr_buf += ret stderr_buf += ret
return stdout_buf.decode("utf-8", "replace"), stderr_buf.decode("utf-8", "replace") return stdout_buf.decode("utf-8"), stderr_buf.decode("utf-8")
class TimeTable: class TimeTable:
@ -101,19 +101,13 @@ TIME_TABLE = TimeTable()
def run( def run(
cmd: list[str], cmd: list[str],
*, *,
input: bytes | None = None, # noqa: A002
env: dict[str, str] | None = None, env: dict[str, str] | None = None,
cwd: Path = Path.cwd(), cwd: Path = Path.cwd(),
log: Log = Log.STDERR, log: Log = Log.STDERR,
check: bool = True, check: bool = True,
error_msg: str | None = None, error_msg: str | None = None,
) -> CmdOut: ) -> CmdOut:
if input: glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
glog.debug(
f"""$: echo "{input.decode('utf-8', 'replace')}" | {shlex.join(cmd)} \nCaller: {get_caller()}"""
)
else:
glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
tstart = datetime.now() tstart = datetime.now()
# Start the subprocess # Start the subprocess
@ -126,10 +120,7 @@ def run(
) )
stdout_buf, stderr_buf = handle_output(process, log) stdout_buf, stderr_buf = handle_output(process, log)
if input: rc = process.wait()
process.communicate(input)
else:
process.wait()
tend = datetime.now() tend = datetime.now()
global TIME_TABLE global TIME_TABLE
@ -145,7 +136,7 @@ def run(
msg=error_msg, msg=error_msg,
) )
if check and process.returncode != 0: if check and rc != 0:
raise ClanCmdError(cmd_out) raise ClanCmdError(cmd_out)
return cmd_out return cmd_out

View File

@ -1,157 +0,0 @@
import argparse
import json
import subprocess
import threading
from collections.abc import Callable, Iterable
from types import ModuleType
from typing import Any
from .cmd import run
from .nix import nix_eval
"""
This module provides dynamic completions.
The completions should feel fast.
We target a maximum of 1second on our average machine.
"""
argcomplete: ModuleType | None = None
try:
import argcomplete # type: ignore[no-redef]
except ImportError:
pass
# The default completion timeout for commands
COMPLETION_TIMEOUT: int = 3
def clan_dir(flake: str | None) -> str | None:
from .dirs import get_clan_flake_toplevel_or_env
path_result = get_clan_flake_toplevel_or_env()
return str(path_result) if path_result is not None else None
def complete_machines(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for machine names configured in the clan.
"""
machines: list[str] = []
def run_cmd() -> None:
try:
# In my tests this was consistently faster than:
# nix eval .#nixosConfigurations --apply builtins.attrNames
cmd = ["nix", "flake", "show", "--system", "no-eval", "--json"]
if (clan_dir_result := clan_dir(None)) is not None:
cmd.append(clan_dir_result)
result = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
)
data = json.loads(result.stdout)
try:
machines.extend(data.get("nixosConfigurations").keys())
except KeyError:
pass
except subprocess.CalledProcessError:
pass
thread = threading.Thread(target=run_cmd)
thread.start()
thread.join(timeout=COMPLETION_TIMEOUT)
if thread.is_alive():
return iter([])
machines_dict = {name: "machine" for name in machines}
return machines_dict
def complete_services_for_machine(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for machine facts generation services.
"""
services: list[str] = []
# TODO: consolidate, if multiple machines are used
machines: list[str] = parsed_args.machines
def run_cmd() -> None:
try:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
services_result = json.loads(
run(
nix_eval(
flags=[
f"{flake}#nixosConfigurations.{machines[0]}.config.clanCore.facts.services",
"--apply",
"builtins.attrNames",
],
),
).stdout.strip()
)
services.extend(services_result)
except subprocess.CalledProcessError:
pass
thread = threading.Thread(target=run_cmd)
thread.start()
thread.join(timeout=COMPLETION_TIMEOUT)
if thread.is_alive():
return iter([])
services_dict = {name: "service" for name in services}
return services_dict
def complete_secrets(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for clan secrets
"""
from pathlib import Path
from .secrets.secrets import ListSecretsOptions, list_secrets
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
options = ListSecretsOptions(
flake=Path(flake),
pattern=None,
)
secrets = list_secrets(options.flake, options.pattern)
secrets_dict = {name: "secret" for name in secrets}
return secrets_dict
def add_dynamic_completer(
action: argparse.Action,
completer: Callable[..., Iterable[str]],
) -> None:
"""
Add a completion function to an argparse action, this will only be added,
if the argcomplete module is loaded.
"""
if argcomplete:
# mypy doesn't check this correctly, so we ignore it
action.completer = completer # type: ignore[attr-defined]

View File

@ -2,7 +2,6 @@ import argparse
import importlib import importlib
import logging import logging
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -55,12 +54,10 @@ def check_command(args: argparse.Namespace) -> None:
def register_check_parser(parser: argparse.ArgumentParser) -> None: def register_check_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", "machine",
help="The machine to check secrets for", help="The machine to check secrets for",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument( parser.add_argument(
"--service", "--service",
help="the service to check", help="the service to check",

View File

@ -9,11 +9,6 @@ from tempfile import TemporaryDirectory
from clan_cli.cmd import run from clan_cli.cmd import run
from ..completions import (
add_dynamic_completer,
complete_machines,
complete_services_for_machine,
)
from ..errors import ClanError from ..errors import ClanError
from ..git import commit_files from ..git import commit_files
from ..machines.inventory import get_all_machines, get_selected_machines from ..machines.inventory import get_all_machines, get_selected_machines
@ -32,7 +27,6 @@ def read_multiline_input(prompt: str = "Finish with Ctrl-D") -> str:
""" """
print(prompt, flush=True) print(prompt, flush=True)
proc = subprocess.run(["cat"], stdout=subprocess.PIPE, text=True) proc = subprocess.run(["cat"], stdout=subprocess.PIPE, text=True)
log.info("Input received. Processing...")
return proc.stdout return proc.stdout
@ -222,23 +216,19 @@ def generate_command(args: argparse.Namespace) -> None:
def register_generate_parser(parser: argparse.ArgumentParser) -> None: def register_generate_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machines", "machines",
type=str, type=str,
help="machine to generate facts for. if empty, generate facts for all machines", help="machine to generate facts for. if empty, generate facts for all machines",
nargs="*", nargs="*",
default=[], default=[],
) )
add_dynamic_completer(machines_parser, complete_machines) parser.add_argument(
service_parser = parser.add_argument(
"--service", "--service",
type=str, type=str,
help="service to generate facts for, if empty, generate facts for every service", help="service to generate facts for, if empty, generate facts for every service",
default=None, default=None,
) )
add_dynamic_completer(service_parser, complete_services_for_machine)
parser.add_argument( parser.add_argument(
"--regenerate", "--regenerate",
type=bool, type=bool,

View File

@ -3,7 +3,6 @@ import importlib
import json import json
import logging import logging
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -38,10 +37,8 @@ def get_command(args: argparse.Namespace) -> None:
def register_list_parser(parser: argparse.ArgumentParser) -> None: def register_list_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", "machine",
help="The machine to print facts for", help="The machine to print facts for",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.set_defaults(func=get_command) parser.set_defaults(func=get_command)

View File

@ -2,7 +2,6 @@ import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from clan_cli.cmd import Log, run
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell from clan_cli.nix import nix_shell
@ -16,25 +15,25 @@ class SecretStore(SecretStoreBase):
def set( def set(
self, service: str, name: str, value: bytes, groups: list[str] self, service: str, name: str, value: bytes, groups: list[str]
) -> Path | None: ) -> Path | None:
run( subprocess.run(
nix_shell( nix_shell(
["nixpkgs#pass"], ["nixpkgs#pass"],
["pass", "insert", "-m", f"machines/{self.machine.name}/{name}"], ["pass", "insert", "-m", f"machines/{self.machine.name}/{name}"],
), ),
input=value, input=value,
log=Log.BOTH, check=True,
error_msg=f"Failed to insert secret {name}",
) )
return None # we manage the files outside of the git repo return None # we manage the files outside of the git repo
def get(self, service: str, name: str) -> bytes: def get(self, service: str, name: str) -> bytes:
return run( return subprocess.run(
nix_shell( nix_shell(
["nixpkgs#pass"], ["nixpkgs#pass"],
["pass", "show", f"machines/{self.machine.name}/{name}"], ["pass", "show", f"machines/{self.machine.name}/{name}"],
), ),
error_msg=f"Failed to get secret {name}", check=True,
).stdout.encode("utf-8") stdout=subprocess.PIPE,
).stdout
def exists(self, service: str, name: str) -> bool: def exists(self, service: str, name: str) -> bool:
password_store = os.environ.get( password_store = os.environ.get(
@ -49,7 +48,7 @@ class SecretStore(SecretStoreBase):
) )
hashes = [] hashes = []
hashes.append( hashes.append(
run( subprocess.run(
nix_shell( nix_shell(
["nixpkgs#git"], ["nixpkgs#git"],
[ [
@ -62,15 +61,13 @@ class SecretStore(SecretStoreBase):
f"machines/{self.machine.name}", f"machines/{self.machine.name}",
], ],
), ),
check=False, stdout=subprocess.PIPE,
) ).stdout.strip()
.stdout.encode("utf-8")
.strip()
) )
for symlink in Path(password_store).glob(f"machines/{self.machine.name}/**/*"): for symlink in Path(password_store).glob(f"machines/{self.machine.name}/**/*"):
if symlink.is_symlink(): if symlink.is_symlink():
hashes.append( hashes.append(
run( subprocess.run(
nix_shell( nix_shell(
["nixpkgs#git"], ["nixpkgs#git"],
[ [
@ -83,10 +80,8 @@ class SecretStore(SecretStoreBase):
str(symlink), str(symlink),
], ],
), ),
check=False, stdout=subprocess.PIPE,
) ).stdout.strip()
.stdout.encode("utf-8")
.strip()
) )
# we sort the hashes to make sure that the order is always the same # we sort the hashes to make sure that the order is always the same

View File

@ -5,7 +5,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ..cmd import Log, run from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
from ..nix import nix_shell from ..nix import nix_shell
@ -33,8 +32,6 @@ def upload_secrets(machine: Machine) -> None:
" ".join(["ssh"] + ssh_cmd[2:]), " ".join(["ssh"] + ssh_cmd[2:]),
"-az", "-az",
"--delete", "--delete",
"--chown=root:root",
"--chmod=D700,F600",
f"{tempdir!s}/", f"{tempdir!s}/",
f"{host.user}@{host.host}:{machine.secrets_upload_directory}/", f"{host.user}@{host.host}:{machine.secrets_upload_directory}/",
], ],
@ -49,10 +46,8 @@ def upload_command(args: argparse.Namespace) -> None:
def register_upload_parser(parser: argparse.ArgumentParser) -> None: def register_upload_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", "machine",
help="The machine to upload secrets to", help="The machine to upload secrets to",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.set_defaults(func=upload_command) parser.set_defaults(func=upload_command)

View File

@ -12,7 +12,6 @@ from tempfile import TemporaryDirectory
from typing import Any from typing import Any
from .cmd import Log, run from .cmd import Log, run
from .completions import add_dynamic_completer, complete_machines
from .errors import ClanError from .errors import ClanError
from .facts.secret_modules import SecretStoreBase from .facts.secret_modules import SecretStoreBase
from .machines.machines import Machine from .machines.machines import Machine
@ -174,13 +173,11 @@ def flash_command(args: argparse.Namespace) -> None:
def register_parser(parser: argparse.ArgumentParser) -> None: def register_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", "machine",
type=str, type=str,
help="machine to install", help="machine to install",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument( parser.add_argument(
"--disk", "--disk",
type=str, type=str,

View File

@ -5,7 +5,6 @@ from .create import register_create_parser
from .delete import register_delete_parser from .delete import register_delete_parser
from .install import register_install_parser from .install import register_install_parser
from .list import register_list_parser from .list import register_list_parser
from .show import register_show_parser
from .update import register_update_parser from .update import register_update_parser
@ -63,17 +62,6 @@ Examples:
) )
register_list_parser(list_parser) register_list_parser(list_parser)
show_parser = subparser.add_parser(
"show",
help="Show a machine",
epilog=(
"""
This subcommand shows the details of a machine managed by this clan like icon, description, etc
"""
),
)
register_show_parser(show_parser)
install_parser = subparser.add_parser( install_parser = subparser.add_parser(
"install", "install",
help="Install a machine", help="Install a machine",

View File

@ -1,7 +1,6 @@
import argparse import argparse
import shutil import shutil
from ..completions import add_dynamic_completer, complete_machines
from ..dirs import specific_machine_dir from ..dirs import specific_machine_dir
from ..errors import ClanError from ..errors import ClanError
@ -15,7 +14,5 @@ def delete_command(args: argparse.Namespace) -> None:
def register_delete_parser(parser: argparse.ArgumentParser) -> None: def register_delete_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument("host", type=str) parser.add_argument("host", type=str)
add_dynamic_completer(machines_parser, complete_machines)
parser.set_defaults(func=delete_command) parser.set_defaults(func=delete_command)

View File

@ -8,7 +8,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ..cmd import Log, run from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines
from ..facts.generate import generate_facts from ..facts.generate import generate_facts
from ..machines.machines import Machine from ..machines.machines import Machine
from ..nix import nix_shell from ..nix import nix_shell
@ -189,14 +188,11 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
help="do not ask for confirmation", help="do not ask for confirmation",
default=False, default=False,
) )
parser.add_argument(
machines_parser = parser.add_argument(
"machine", "machine",
type=str, type=str,
help="machine to install", help="machine to install",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument( parser.add_argument(
"target_host", "target_host",
type=str, type=str,

View File

@ -1,4 +1,5 @@
import argparse import argparse
import dataclasses
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
@ -11,15 +12,24 @@ from ..nix import nix_config, nix_eval
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@dataclasses.dataclass
class MachineInfo:
machine_name: str
machine_description: str | None
machine_icon: str | None
@API.register @API.register
def list_machines(flake_url: str | Path, debug: bool) -> list[str]: def list_machines(flake_url: str | Path, debug: bool) -> dict[str, MachineInfo]:
config = nix_config() config = nix_config()
system = config["system"] system = config["system"]
cmd = nix_eval( cmd = nix_eval(
[ [
f"{flake_url}#clanInternals.machines.{system}", f"{flake_url}#clanInternals.machines.{system}",
"--apply", "--apply",
"builtins.attrNames", """builtins.mapAttrs (name: attrs: {
inherit (attrs.config.clanCore) machineDescription machineIcon machineName;
})""",
"--json", "--json",
] ]
) )
@ -27,13 +37,27 @@ def list_machines(flake_url: str | Path, debug: bool) -> list[str]:
proc = run_no_stdout(cmd) proc = run_no_stdout(cmd)
res = proc.stdout.strip() res = proc.stdout.strip()
return json.loads(res) machines_dict = json.loads(res)
return {
k: MachineInfo(
machine_name=v.get("machineName"),
machine_description=v.get("machineDescription", None),
machine_icon=v.get("machineIcon", None),
)
for k, v in machines_dict.items()
}
def list_command(args: argparse.Namespace) -> None: def list_command(args: argparse.Namespace) -> None:
flake_path = Path(args.flake).resolve() flake_path = Path(args.flake).resolve()
for name in list_machines(flake_path, args.debug): print("Listing all machines:\n")
print(name) print("Source: ", flake_path)
print("-" * 40)
for name, machine in list_machines(flake_path, args.debug).items():
description = machine.machine_description or "[no description]"
print(f"{name}\n: {description}\n")
print("-" * 40)
def register_list_parser(parser: argparse.ArgumentParser) -> None: def register_list_parser(parser: argparse.ArgumentParser) -> None:

View File

@ -1,60 +0,0 @@
import argparse
import dataclasses
import json
import logging
from pathlib import Path
from clan_cli.api import API
from ..cmd import run_no_stdout
from ..completions import add_dynamic_completer, complete_machines
from ..nix import nix_config, nix_eval
from .types import machine_name_type
log = logging.getLogger(__name__)
@dataclasses.dataclass
class MachineInfo:
machine_name: str
machine_description: str | None
machine_icon: str | None
@API.register
def show_machine(flake_url: str | Path, machine_name: str, debug: bool) -> MachineInfo:
config = nix_config()
system = config["system"]
cmd = nix_eval(
[
f"{flake_url}#clanInternals.machines.{system}.{machine_name}",
"--apply",
"machine: { inherit (machine.config.clanCore) machineDescription machineIcon machineName; }",
"--json",
]
)
proc = run_no_stdout(cmd)
res = proc.stdout.strip()
machine = json.loads(res)
return MachineInfo(
machine_name=machine.get("machineName"),
machine_description=machine.get("machineDescription", None),
machine_icon=machine.get("machineIcon", None),
)
def show_command(args: argparse.Namespace) -> None:
flake_path = Path(args.flake).resolve()
machine = show_machine(flake_path, args.machine, args.debug)
print(f"Name: {machine.machine_name}")
print(f"Description: {machine.machine_description or ''}")
print(f"Icon: {machine.machine_icon or ''}")
def register_show_parser(parser: argparse.ArgumentParser) -> None:
parser.set_defaults(func=show_command)
machine_parser = parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type
)
add_dynamic_completer(machine_parser, complete_machines)

View File

@ -3,10 +3,9 @@ import json
import logging import logging
import os import os
import shlex import shlex
import subprocess
import sys import sys
from ..cmd import run
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError from ..errors import ClanError
from ..facts.generate import generate_facts from ..facts.generate import generate_facts
from ..facts.upload import upload_secrets from ..facts.upload import upload_secrets
@ -54,7 +53,11 @@ def upload_sources(
path, path,
] ]
) )
run(cmd, env=env, error_msg="failed to upload sources") proc = subprocess.run(cmd, stdout=subprocess.PIPE, env=env, check=False)
if proc.returncode != 0:
raise ClanError(
f"failed to upload sources: {shlex.join(cmd)} failed with {proc.returncode}"
)
return path return path
# Slow path: we need to upload all sources to the remote machine # Slow path: we need to upload all sources to the remote machine
@ -70,13 +73,16 @@ def upload_sources(
] ]
) )
log.info("run %s", shlex.join(cmd)) log.info("run %s", shlex.join(cmd))
proc = run(cmd, error_msg="failed to upload sources") proc = subprocess.run(cmd, stdout=subprocess.PIPE, check=False)
if proc.returncode != 0:
raise ClanError(
f"failed to upload sources: {shlex.join(cmd)} failed with {proc.returncode}"
)
try: try:
return json.loads(proc.stdout)["path"] return json.loads(proc.stdout)["path"]
except (json.JSONDecodeError, OSError) as e: except (json.JSONDecodeError, OSError) as e:
raise ClanError( raise ClanError(
f"failed to parse output of {shlex.join(cmd)}: {e}\nGot: {proc.stdout}" f"failed to parse output of {shlex.join(cmd)}: {e}\nGot: {proc.stdout.decode('utf-8', 'replace')}"
) )
@ -174,7 +180,7 @@ def update(args: argparse.Namespace) -> None:
def register_update_parser(parser: argparse.ArgumentParser) -> None: def register_update_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machines", "machines",
type=str, type=str,
nargs="*", nargs="*",
@ -182,9 +188,6 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None:
metavar="MACHINE", metavar="MACHINE",
help="machine to update. If no machine is specified, all machines will be updated.", help="machine to update. If no machine is specified, all machines will be updated.",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument( parser.add_argument(
"--target-host", "--target-host",
type=str, type=str,

View File

@ -1,7 +1,6 @@
import argparse import argparse
from pathlib import Path from pathlib import Path
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError from ..errors import ClanError
from ..git import commit_files from ..git import commit_files
from ..machines.types import machine_name_type, validate_hostname from ..machines.types import machine_name_type, validate_hostname
@ -148,28 +147,25 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
# Parser # Parser
get_parser = subparser.add_parser("get", help="get a machine public key") get_parser = subparser.add_parser("get", help="get a machine public key")
get_machine_parser = get_parser.add_argument( get_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type "machine", help="the name of the machine", type=machine_name_type
) )
add_dynamic_completer(get_machine_parser, complete_machines)
get_parser.set_defaults(func=get_command) get_parser.set_defaults(func=get_command)
# Parser # Parser
remove_parser = subparser.add_parser("remove", help="remove a machine") remove_parser = subparser.add_parser("remove", help="remove a machine")
remove_machine_parser = remove_parser.add_argument( remove_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type "machine", help="the name of the machine", type=machine_name_type
) )
add_dynamic_completer(remove_machine_parser, complete_machines)
remove_parser.set_defaults(func=remove_command) remove_parser.set_defaults(func=remove_command)
# Parser # Parser
add_secret_parser = subparser.add_parser( add_secret_parser = subparser.add_parser(
"add-secret", help="allow a machine to access a secret" "add-secret", help="allow a machine to access a secret"
) )
machine_add_secret_parser = add_secret_parser.add_argument( add_secret_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type "machine", help="the name of the machine", type=machine_name_type
) )
add_dynamic_completer(machine_add_secret_parser, complete_machines)
add_secret_parser.add_argument( add_secret_parser.add_argument(
"secret", help="the name of the secret", type=secret_name_type "secret", help="the name of the secret", type=secret_name_type
) )
@ -179,10 +175,9 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
remove_secret_parser = subparser.add_parser( remove_secret_parser = subparser.add_parser(
"remove-secret", help="remove a group's access to a secret" "remove-secret", help="remove a group's access to a secret"
) )
machine_remove_parser = remove_secret_parser.add_argument( remove_secret_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type "machine", help="the name of the group", type=machine_name_type
) )
add_dynamic_completer(machine_remove_parser, complete_machines)
remove_secret_parser.add_argument( remove_secret_parser.add_argument(
"secret", help="the name of the secret", type=secret_name_type "secret", help="the name of the secret", type=secret_name_type
) )

View File

@ -9,7 +9,6 @@ from pathlib import Path
from typing import IO from typing import IO
from .. import tty from .. import tty
from ..completions import add_dynamic_completer, complete_secrets
from ..errors import ClanError from ..errors import ClanError
from ..git import commit_files from ..git import commit_files
from .folders import ( from .folders import (
@ -154,12 +153,8 @@ def remove_command(args: argparse.Namespace) -> None:
remove_secret(Path(args.flake), args.secret) remove_secret(Path(args.flake), args.secret)
def add_secret_argument(parser: argparse.ArgumentParser, autocomplete: bool) -> None: def add_secret_argument(parser: argparse.ArgumentParser) -> None:
secrets_parser = parser.add_argument( parser.add_argument("secret", help="the name of the secret", type=secret_name_type)
"secret", help="the name of the secret", type=secret_name_type
)
if autocomplete:
add_dynamic_completer(secrets_parser, complete_secrets)
def machines_folder(flake_dir: Path, group: str) -> Path: def machines_folder(flake_dir: Path, group: str) -> Path:
@ -328,11 +323,11 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
parser_list.set_defaults(func=list_command) parser_list.set_defaults(func=list_command)
parser_get = subparser.add_parser("get", help="get a secret") parser_get = subparser.add_parser("get", help="get a secret")
add_secret_argument(parser_get, True) add_secret_argument(parser_get)
parser_get.set_defaults(func=get_command) parser_get.set_defaults(func=get_command)
parser_set = subparser.add_parser("set", help="set a secret") parser_set = subparser.add_parser("set", help="set a secret")
add_secret_argument(parser_set, False) add_secret_argument(parser_set)
parser_set.add_argument( parser_set.add_argument(
"--group", "--group",
type=str, type=str,
@ -364,10 +359,10 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
parser_set.set_defaults(func=set_command) parser_set.set_defaults(func=set_command)
parser_rename = subparser.add_parser("rename", help="rename a secret") parser_rename = subparser.add_parser("rename", help="rename a secret")
add_secret_argument(parser_rename, True) add_secret_argument(parser_rename)
parser_rename.add_argument("new_name", type=str, help="the new name of the secret") parser_rename.add_argument("new_name", type=str, help="the new name of the secret")
parser_rename.set_defaults(func=rename_command) parser_rename.set_defaults(func=rename_command)
parser_remove = subparser.add_parser("remove", help="remove a secret") parser_remove = subparser.add_parser("remove", help="remove a secret")
add_secret_argument(parser_remove, True) add_secret_argument(parser_remove)
parser_remove.set_defaults(func=remove_command) parser_remove.set_defaults(func=remove_command)

View File

@ -29,7 +29,6 @@
mypy, mypy,
nixpkgs, nixpkgs,
clan-core-path, clan-core-path,
gitMinimal,
}: }:
let let
# Dependencies that are directly used in the project # Dependencies that are directly used in the project
@ -114,13 +113,7 @@ python3.pkgs.buildPythonApplication {
format = "pyproject"; format = "pyproject";
# Arguments for the wrapper to unset LD_LIBRARY_PATH to avoid glibc version issues # Arguments for the wrapper to unset LD_LIBRARY_PATH to avoid glibc version issues
makeWrapperArgs = [ makeWrapperArgs = [ "--unset LD_LIBRARY_PATH" ];
"--unset LD_LIBRARY_PATH"
"--suffix"
"PATH"
":"
"${gitMinimal}/bin/git"
];
# Build-time dependencies. # Build-time dependencies.
nativeBuildInputs = [ nativeBuildInputs = [

View File

@ -17,12 +17,10 @@ def test_create_flake(
capsys: pytest.CaptureFixture, capsys: pytest.CaptureFixture,
temporary_home: Path, temporary_home: Path,
cli: Cli, cli: Cli,
clan_core: Path,
) -> None: ) -> None:
flake_dir = temporary_home / "test-flake" flake_dir = temporary_home / "test-flake"
url = f"{clan_core}#default" cli.run(["flakes", "create", str(flake_dir)])
cli.run(["flakes", "create", str(flake_dir), f"--url={url}"])
assert (flake_dir / ".clan-flake").exists() assert (flake_dir / ".clan-flake").exists()
monkeypatch.chdir(flake_dir) monkeypatch.chdir(flake_dir)
cli.run(["machines", "create", "machine1"]) cli.run(["machines", "create", "machine1"])
@ -49,34 +47,3 @@ def test_create_flake(
flake_outputs["nixosConfigurations"]["machine1"] flake_outputs["nixosConfigurations"]["machine1"]
except KeyError: except KeyError:
pytest.fail("nixosConfigurations.machine1 not found in flake outputs") pytest.fail("nixosConfigurations.machine1 not found in flake outputs")
@pytest.mark.impure
def test_ui_template(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture,
temporary_home: Path,
cli: Cli,
clan_core: Path,
) -> None:
flake_dir = temporary_home / "test-flake"
url = f"{clan_core}#empty"
cli.run(["flakes", "create", str(flake_dir), f"--url={url}"])
assert (flake_dir / ".clan-flake").exists()
monkeypatch.chdir(flake_dir)
cli.run(["machines", "create", "machine1"])
capsys.readouterr() # flush cache
cli.run(["machines", "list"])
assert "machine1" in capsys.readouterr().out
flake_show = subprocess.run(
["nix", "flake", "show", "--json"],
check=True,
capture_output=True,
text=True,
)
flake_outputs = json.loads(flake_show.stdout)
try:
flake_outputs["nixosConfigurations"]["machine1"]
except KeyError:
pytest.fail("nixosConfigurations.machine1 not found in flake outputs")

View File

@ -21,13 +21,6 @@ def test_machine_subcommands(
assert "vm1" in out.out assert "vm1" in out.out
assert "vm2" in out.out assert "vm2" in out.out
capsys.readouterr()
cli.run(["machines", "show", "--flake", str(test_flake_with_core.path), "machine1"])
out = capsys.readouterr()
assert "machine1" in out.out
assert "Description" in out.out
print(out)
cli.run( cli.run(
["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"] ["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"]
) )

View File

@ -46,12 +46,6 @@ class WebView:
self.method_registry: dict[str, Callable] = methods self.method_registry: dict[str, Callable] = methods
self.webview = WebKit.WebView() self.webview = WebKit.WebView()
settings = self.webview.get_settings()
# settings.
settings.set_property("enable-developer-extras", True)
self.webview.set_settings(settings)
self.manager = self.webview.get_user_content_manager() self.manager = self.webview.get_user_content_manager()
# Can be called with: window.webkit.messageHandlers.gtk.postMessage("...") # Can be called with: window.webkit.messageHandlers.gtk.postMessage("...")
# Important: it seems postMessage must be given some payload, otherwise it won't trigger the event # Important: it seems postMessage must be given some payload, otherwise it won't trigger the event

View File

@ -8,9 +8,8 @@ clean_temp_dir() {
rm -rf "$temp_dir" rm -rf "$temp_dir"
} }
is_installed() { is_nix_installed() {
name=$1 if [ -n "$(command -v nix)" ]; then
if [ -n "$(command -v "$name")" ]; then
return 0 return 0
else else
return 1 return 1
@ -18,18 +17,9 @@ is_installed() {
} }
install_nix() { install_nix() {
if is_installed curl; then curl --proto '=https' --tlsv1.2 -sSf -L \
curl --proto '=https' --tlsv1.2 -sSf -L \ https://install.determinate.systems/nix \
https://install.determinate.systems/nix \ > "$temp_dir"/install_nix.sh
> "$temp_dir"/install_nix.sh
elif is_installed wget; then
wget -qO- \
https://install.determinate.systems/nix \
> "$temp_dir"/install_nix.sh
else
echo "Either curl or wget is required to install Nix. Exiting."
exit 1
fi
NIX_INSTALLER_DIAGNOSTIC_ENDPOINT="" sh "$temp_dir"/install_nix.sh install NIX_INSTALLER_DIAGNOSTIC_ENDPOINT="" sh "$temp_dir"/install_nix.sh install
} }
@ -45,17 +35,15 @@ ask_then_install_nix() {
} }
ensure_nix_installed() { ensure_nix_installed() {
if ! is_installed nix; then if ! is_nix_installed; then
ask_then_install_nix ask_then_install_nix
fi fi
} }
start_clan_gui() { start_clan_gui() {
PATH="${PATH:+$PATH:}/nix/var/nix/profiles/default/bin" \ exec nix run \
exec nix run \ https://git.clan.lol/clan/clan-core/archive/main.tar.gz#clan-vm-manager \
https://git.clan.lol/clan/clan-core/archive/main.tar.gz#clan-vm-manager \ --extra-experimental-features flakes nix-command -- "$@"
--no-accept-flake-config \
--extra-experimental-features flakes nix-command -- "$@"
} }
main() { main() {

View File

@ -98,6 +98,9 @@ let
in in
{ {
clan = { clan = {
clanName = "clan-core";
directory = self;
# To build a generic installer image (without ssh pubkeys), # To build a generic installer image (without ssh pubkeys),
# use the following command: # use the following command:
# $ nix build .#iso-installer # $ nix build .#iso-installer

26
pkgs/merge-after-ci/merge-after-ci.py Executable file → Normal file
View File

@ -1,5 +1,4 @@
import argparse import argparse
import shlex
import subprocess import subprocess
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@ -12,17 +11,14 @@ args = parser.parse_args()
if not args.reviewers and not args.no_review: if not args.reviewers and not args.no_review:
parser.error("either --reviewers or --no-review must be given") parser.error("either --reviewers or --no-review must be given")
cmd = [ subprocess.run(
"tea-create-pr", [
"origin", "tea-create-pr",
"upstream", "origin",
"main", "main",
"--assignees", "--assignees",
",".join(["clan-bot", *args.reviewers]), ",".join(["clan-bot", *args.reviewers]),
*(["--labels", "needs-review"] if not args.no_review else []), *(["--labels", "needs-review"] if not args.no_review else []),
*args.args, *args.args,
] ]
)
print("Running:", shlex.join(cmd))
subprocess.run(cmd)

49
pkgs/tea-create-pr/script.sh Executable file → Normal file
View File

@ -1,46 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
remoteFork="${1:-origin}" remoteName="${1:-origin}"
remoteUpstream="${2:-upstream}" targetBranch="${2:-main}"
targetBranch="${3:-main}" shift && shift
shift && shift && shift
TMPDIR="$(mktemp -d)" TMPDIR="$(mktemp -d)"
currentBranch="$(git rev-parse --abbrev-ref HEAD)" currentBranch="$(git rev-parse --abbrev-ref HEAD)"
user_unparsed="$(tea whoami)" user="$(tea login list -o simple | cut -d" " -f4 | head -n1)"
user="$(echo "$user_unparsed" | tr -d '\n' | cut -f4 -d' ')"
tempRemoteBranch="$user-$currentBranch" tempRemoteBranch="$user-$currentBranch"
root_dir=$(git rev-parse --show-toplevel)
nix fmt -- --fail-on-change
# Function to check if a remote exists git log --reverse --pretty="format:%s%n%n%b%n%n" "$remoteName/$targetBranch..HEAD" > "$TMPDIR"/commit-msg
check_remote() {
if git remote get-url "$1" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
# Check if the remote 'upstream' is defined
if ! check_remote "$remoteUpstream"; then
echo "Error: $remoteUpstream remote is not defined."
echo "Please fork the repository and add the $remoteUpstream remote."
echo "$ git remote add $remoteUpstream <upstream-url>"
exit 1
fi
treefmt --fail-on-change -C "$root_dir"
upstream_url=$(git remote get-url "$remoteUpstream")
set -x
git fetch "$remoteUpstream" && git rebase "$remoteUpstream"/main --autostash
set +x
repo=$(echo "$upstream_url" | sed -E 's#.*:([^/]+/[^.]+)\.git#\1#')
git log --reverse --pretty="format:%s%n%n%b%n%n" "$remoteUpstream/$targetBranch..HEAD" > "$TMPDIR"/commit-msg
$EDITOR "$TMPDIR"/commit-msg $EDITOR "$TMPDIR"/commit-msg
@ -52,13 +23,11 @@ rest=$(echo "$COMMIT_MSG" | tail -n+2)
if [[ "$firstLine" == "$rest" ]]; then if [[ "$firstLine" == "$rest" ]]; then
rest="" rest=""
fi fi
git push --force -u "$remoteName" HEAD:refs/heads/"$tempRemoteBranch"
git push --force -u "$remoteFork" HEAD:refs/heads/"$tempRemoteBranch"
tea pr create \ tea pr create \
--repo "$repo" \
--head "$user:$tempRemoteBranch" \
--title "$firstLine" \ --title "$firstLine" \
--description "$rest" \ --description "$rest" \
--head "$tempRemoteBranch" \
--base "$targetBranch" \
"$@" "$@"

View File

@ -19,7 +19,6 @@ export default tseslint.config(
"error", "error",
{ {
callees: ["cx"], callees: ["cx"],
whitelist: ["material-icons"],
}, },
], ],
}, },

View File

@ -5,9 +5,6 @@ const distPath = path.resolve(__dirname, "dist");
const manifestPath = path.join(distPath, ".vite/manifest.json"); const manifestPath = path.join(distPath, ".vite/manifest.json");
const outputPath = path.join(distPath, "index.html"); const outputPath = path.join(distPath, "index.html");
const postcss = require("postcss");
const css_url = require("postcss-url");
fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => { fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
if (err) { if (err) {
return console.error("Failed to read manifest:", err); return console.error("Failed to read manifest:", err);
@ -28,48 +25,19 @@ fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
// Add linked stylesheets // Add linked stylesheets
assets.forEach((asset) => { assets.forEach((asset) => {
// console.log(asset); asset.css.forEach((cssEntry) => {
if (asset.src === "index.html") { htmlContent += `\n <link rel="stylesheet" href="${cssEntry}">`;
asset.css.forEach((cssEntry) => { });
// css to be processed
const css = fs.readFileSync(`dist/${cssEntry}`, "utf8");
// process css
postcss()
.use(
css_url({
url: (asset, dir) => {
const res = path.basename(asset.url);
console.log(`Rewriting CSS url(): ${asset.url} to ${res}`);
return res;
},
})
)
.process(css, {
from: `dist/${cssEntry}`,
to: `dist/${cssEntry}`,
})
.then((result) => {
fs.writeFileSync(`dist/${cssEntry}`, result.css, "utf8");
});
// Extend the HTML content with the linked stylesheet
console.log(`Relinking html css stylesheet: ${cssEntry}`);
htmlContent += `\n <link rel="stylesheet" href="${cssEntry}">`;
});
}
}); });
htmlContent += ` htmlContent += `
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
`; `;
// Add scripts // Add scripts
assets.forEach((asset) => { assets.forEach((asset) => {
if (asset.file.endsWith(".js")) { if (asset.file.endsWith(".js")) {
console.log(`Relinking js script: ${asset.file}`);
htmlContent += `\n <script src="${asset.file}"></script>`; htmlContent += `\n <script src="${asset.file}"></script>`;
} }
}); });

View File

@ -1,16 +0,0 @@
import { JSONSchemaFaker } from "json-schema-faker";
import { schema } from "./api/index";
import { OperationNames } from "./src/message";
const faker = JSONSchemaFaker;
faker.option({
alwaysFakeOptionals: true,
});
const getFakeResponse = (method: OperationNames, data: any) => {
const fakeData = faker.generate(schema.properties[method].properties.return);
return fakeData;
};
export { getFakeResponse };

View File

@ -9,12 +9,10 @@
"version": "0.0.1", "version": "0.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"material-icons": "^1.13.12",
"solid-js": "^1.8.11" "solid-js": "^1.8.11"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0", "@eslint/js": "^9.3.0",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^20.12.12", "@types/node": "^20.12.12",
"@typescript-eslint/parser": "^7.10.0", "@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
@ -22,10 +20,8 @@
"daisyui": "^4.11.1", "daisyui": "^4.11.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-tailwindcss": "^3.17.0", "eslint-plugin-tailwindcss": "^3.17.0",
"json-schema-faker": "^0.5.6",
"json-schema-to-ts": "^3.1.0", "json-schema-to-ts": "^3.1.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"solid-devtools": "^0.29.2", "solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
@ -1486,34 +1482,6 @@
"solid-js": "^1.6.12" "solid-js": "^1.6.12"
} }
}, },
"node_modules/@tailwindcss/typography": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
"integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==",
"dev": true,
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -2022,12 +1990,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"dev": true
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -2221,12 +2183,6 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"node_modules/cuint": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
"integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==",
"dev": true
},
"node_modules/culori": { "node_modules/culori": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
@ -2667,19 +2623,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esquery": { "node_modules/esquery": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
@ -2859,12 +2802,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/format-util": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz",
"integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==",
"dev": true
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@ -3260,53 +3197,6 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true "dev": true
}, },
"node_modules/json-schema-faker": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.6.tgz",
"integrity": "sha512-u/cFC26/GDxh2vPiAC8B8xVvpXAW+QYtG2mijEbKrimCk8IHtiwQBjCE8TwvowdhALWq9IcdIWZ+/8ocXvdL3Q==",
"dev": true,
"dependencies": {
"json-schema-ref-parser": "^6.1.0",
"jsonpath-plus": "^7.2.0"
},
"bin": {
"jsf": "bin/gen.cjs"
}
},
"node_modules/json-schema-ref-parser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz",
"integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==",
"deprecated": "Please switch to @apidevtools/json-schema-ref-parser",
"dev": true,
"dependencies": {
"call-me-maybe": "^1.0.1",
"js-yaml": "^3.12.1",
"ono": "^4.0.11"
}
},
"node_modules/json-schema-ref-parser/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/json-schema-ref-parser/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-schema-to-ts": { "node_modules/json-schema-to-ts": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz",
@ -3344,15 +3234,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonpath-plus": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz",
"integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==",
"dev": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -3433,18 +3314,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"dev": true
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -3497,11 +3366,6 @@
"semver": "bin/semver" "semver": "bin/semver"
} }
}, },
"node_modules/material-icons": {
"version": "1.13.12",
"resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.12.tgz",
"integrity": "sha512-/2YoaB79IjUK2B2JB+vIXXYGtBfHb/XG66LvoKVM5ykHW7yfrV5SP6d7KLX6iijY6/G9GqwgtPQ/sbhFnOURVA=="
},
"node_modules/merge-anything": { "node_modules/merge-anything": {
"version": "5.1.7", "version": "5.1.7",
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
@ -3687,15 +3551,6 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/ono": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz",
"integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==",
"dev": true,
"dependencies": {
"format-util": "^1.0.3"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -4012,73 +3867,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/postcss-url": {
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-10.1.3.tgz",
"integrity": "sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==",
"dev": true,
"dependencies": {
"make-dir": "~3.1.0",
"mime": "~2.5.2",
"minimatch": "~3.0.4",
"xxhashjs": "~0.2.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-url/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/postcss-url/node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/postcss-url/node_modules/mime": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz",
"integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/postcss-url/node_modules/minimatch": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
"integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/postcss-value-parser": { "node_modules/postcss-value-parser": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@ -4501,12 +4289,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -5226,15 +5008,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true "dev": true
}, },
"node_modules/xxhashjs": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
"integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
"dev": true,
"dependencies": {
"cuint": "^0.2.2"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -13,28 +13,24 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0", "@eslint/js": "^9.3.0",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^20.12.12", "@types/node": "^20.12.12",
"@typescript-eslint/parser": "^7.10.0", "@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"daisyui": "^4.11.1", "daisyui": "^4.11.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-tailwindcss": "^3.17.0",
"json-schema-faker": "^0.5.6",
"json-schema-to-ts": "^3.1.0", "json-schema-to-ts": "^3.1.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"solid-devtools": "^0.29.2", "solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^7.10.0", "typescript-eslint": "^7.10.0",
"vite": "^5.0.11", "vite": "^5.0.11",
"vite-plugin-solid": "^2.8.2" "vite-plugin-solid": "^2.8.2",
"eslint-plugin-tailwindcss": "^3.17.0"
}, },
"dependencies": { "dependencies": {
"material-icons": "^1.13.12",
"solid-js": "^1.8.11" "solid-js": "^1.8.11"
} }
} }

View File

@ -1,18 +1,59 @@
import { createSignal, type Component } from "solid-js"; import { Match, Switch, createSignal, type Component } from "solid-js";
import { CountProvider } from "./Config"; import { CountProvider } from "./Config";
// import { Nested } from "./nested";
import { Layout } from "./layout/layout"; import { Layout } from "./layout/layout";
import { Route, Router } from "./Routes"; import cx from "classnames";
import { Nested } from "./nested";
// Global state type Route = "home" | "machines";
const [route, setRoute] = createSignal<Route>("machines");
export { route, setRoute };
const App: Component = () => { const App: Component = () => {
const [route, setRoute] = createSignal<Route>("home");
return ( return (
<CountProvider> <CountProvider>
<Layout> <Layout>
<Router route={route} /> <div class="col-span-1">
<div class={cx("text-zinc-500")}>Navigation</div>
<ul>
<li>
<button
onClick={() => setRoute("home")}
classList={{ "bg-blue-500": route() === "home" }}
>
Home
</button>
</li>
<li>
{" "}
<button
onClick={() => setRoute("machines")}
classList={{ "bg-blue-500": route() === "machines" }}
>
Machines
</button>
</li>
</ul>
</div>
<div class="col-span-7">
<div>{route()}</div>
<Switch fallback={<p>{route()} not found</p>}>
<Match when={route() == "home"}>
<Nested />
</Match>
<Match when={route() == "machines"}>
<div class="grid grid-cols-3 gap-2">
<div class="h-10 w-20 bg-red-500">red</div>
<div class="h-10 w-20 bg-green-500">green</div>
<div class="h-10 w-20 bg-blue-500">blue</div>
<div class="h-10 w-20 bg-yellow-500">yellow</div>
<div class="h-10 w-20 bg-purple-500">purple</div>
<div class="h-10 w-20 bg-cyan-500">cyan</div>
<div class="h-10 w-20 bg-pink-500">pink</div>
</div>
</Match>
</Switch>
</div>
</Layout> </Layout>
</CountProvider> </CountProvider>
); );

View File

@ -10,7 +10,7 @@ import { OperationResponse, pyApi } from "./message";
export const makeCountContext = () => { export const makeCountContext = () => {
const [machines, setMachines] = createSignal< const [machines, setMachines] = createSignal<
OperationResponse<"list_machines"> OperationResponse<"list_machines">
>([]); >({});
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
pyApi.list_machines.receive((machines) => { pyApi.list_machines.receive((machines) => {
@ -41,7 +41,7 @@ export const CountContext = createContext<CountContextType>([
loading: () => false, loading: () => false,
// eslint-disable-next-line // eslint-disable-next-line
machines: () => ([]), machines: () => ({}),
}, },
{ {
// eslint-disable-next-line // eslint-disable-next-line

View File

@ -1,32 +0,0 @@
import { Accessor, For, Match, Switch } from "solid-js";
import { MachineListView } from "./routes/machines/view";
import { colors } from "./routes/colors/view";
export type Route = keyof typeof routes;
export const routes = {
machines: {
child: MachineListView,
label: "Machines",
icon: "devices_other",
},
colors: {
child: colors,
label: "Colors",
icon: "color_lens",
},
};
interface RouterProps {
route: Accessor<Route>;
}
export const Router = (props: RouterProps) => {
const { route } = props;
return (
<Switch fallback={<p>route {route()} not found</p>}>
<For each={Object.entries(routes)}>
{([key, { child }]) => <Match when={route() === key}>{child}</Match>}
</For>
</Switch>
);
};

View File

@ -1,33 +0,0 @@
import { Accessor, For, Setter } from "solid-js";
import { Route, routes } from "./Routes";
interface SidebarProps {
route: Accessor<Route>;
setRoute: Setter<Route>;
}
export const Sidebar = (props: SidebarProps) => {
const { route, setRoute } = props;
return (
<aside class="min-h-screen w-80 bg-base-100">
<div class="sticky top-0 z-20 hidden items-center gap-2 bg-base-100/90 px-4 py-2 shadow-sm backdrop-blur lg:flex">
Icon
</div>
<ul class="menu px-4 py-0">
<For each={Object.entries(routes)}>
{([key, { label, icon }]) => (
<li>
<button
onClick={() => setRoute(key as Route)}
class="group"
classList={{ "bg-blue-500": route() === key }}
>
<span class="material-icons">{icon}</span>
{label}
</button>
</li>
)}
</For>
</ul>
</aside>
);
};

View File

@ -1,5 +1,3 @@
@import 'material-icons/iconfont/filled.css';
/* List of icons: https://marella.me/material-icons/demo/ */
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@ -3,7 +3,7 @@ import { render } from "solid-js/web";
import "./index.css"; import "./index.css";
import App from "./App"; import App from "./App";
import { getFakeResponse } from "../mock";
const root = document.getElementById("app"); const root = document.getElementById("app");
window.clan = window.clan || {}; window.clan = window.clan || {};
@ -14,26 +14,5 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
); );
} }
console.log(import.meta.env);
if (import.meta.env.DEV) {
console.log("Development mode");
window.webkit = window.webkit || {
messageHandlers: {
gtk: {
postMessage: (postMessage) => {
const { method, data } = postMessage;
console.debug("Python API call", { method, data });
setTimeout(() => {
const mock = getFakeResponse(method, data);
console.log("mock", { mock });
window.clan[method](JSON.stringify(mock));
}, 1000);
},
},
},
};
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
render(() => <App />, root!); render(() => <App />, root!);

View File

@ -1,19 +0,0 @@
export const Header = () => {
return (
<div class="navbar bg-base-100">
<div class="flex-none">
<button class="btn btn-square btn-ghost">
<span class="material-icons">home</span>
</button>
</div>
<div class="flex-1">
<a class="btn btn-ghost text-xl">Clan</a>
</div>
<div class="flex-none">
<button class="btn btn-square btn-ghost">
<span class="material-icons">menu</span>
</button>
</div>
</div>
);
};

View File

@ -1,35 +1,9 @@
import { Component, JSXElement } from "solid-js"; import { Component, JSXElement } from "solid-js";
import { Header } from "./header";
import { Sidebar } from "../Sidebar";
import { route, setRoute } from "../App";
interface LayoutProps { interface LayoutProps {
children: JSXElement; children: JSXElement;
} }
export const Layout: Component<LayoutProps> = (props) => { export const Layout: Component<LayoutProps> = (props) => {
return ( return <div class="grid grid-cols-8">{props.children}</div>;
<>
<div class="drawer bg-base-100 lg:drawer-open">
<input
id="toplevel-drawer"
type="checkbox"
class="drawer-toggle hidden"
/>
<div class="drawer-content">
<Header />
{props.children}
</div>
<div class="drawer-side z-40">
<label
for="toplevel-drawer"
aria-label="close sidebar"
class="drawer-overlay"
></label>
<Sidebar route={route} setRoute={setRoute} />
</div>
</div>
</>
);
}; };

View File

@ -0,0 +1,39 @@
import { For, Match, Switch, createEffect, type Component } from "solid-js";
import { useCountContext } from "./Config";
export const Nested: Component = () => {
const [{ machines, loading }, { getMachines }] = useCountContext();
const list = () => Object.values(machines());
createEffect(() => {
console.log("1", list());
});
createEffect(() => {
console.log("2", machines());
});
return (
<div>
<button onClick={() => getMachines()} class="btn btn-primary">
Get machines
</button>
<div></div>
<Switch>
<Match when={loading()}>Loading...</Match>
<Match when={!loading() && Object.entries(machines()).length === 0}>
No machines found
</Match>
<Match when={!loading()}>
<For each={list()}>
{(entry, i) => (
<li>
{i() + 1}: {entry.machine_name}{" "}
{entry.machine_description || "No description"}
</li>
)}
</For>
</Match>
</Switch>
</div>
);
};

View File

@ -1,13 +0,0 @@
export const colors = () => {
return (
<div class="grid grid-cols-3 gap-2">
<div class="h-10 w-20 bg-red-500">red</div>
<div class="h-10 w-20 bg-green-500">green</div>
<div class="h-10 w-20 bg-blue-500">blue</div>
<div class="h-10 w-20 bg-yellow-500">yellow</div>
<div class="h-10 w-20 bg-purple-500">purple</div>
<div class="h-10 w-20 bg-cyan-500">cyan</div>
<div class="h-10 w-20 bg-pink-500">pink</div>
</div>
);
};

View File

@ -1,78 +0,0 @@
import { For, Match, Switch, createEffect, type Component } from "solid-js";
import { useCountContext } from "../../Config";
import { route } from "@/src/App";
export const MachineListView: Component = () => {
const [{ machines, loading }, { getMachines }] = useCountContext();
createEffect(() => {
if (route() === "machines") getMachines();
});
return (
<div class="max-w-screen-lg">
<div class="tooltip" data-tip="Refresh ">
<button class="btn btn-ghost" onClick={() => getMachines()}>
<span class="material-icons ">refresh</span>
</button>
</div>
<Switch>
<Match when={loading()}>
{/* Loading skeleton */}
<div>
<div class="card card-side m-2 bg-base-100 shadow-lg">
<figure class="pl-2">
<div class="skeleton size-12"></div>
</figure>
<div class="card-body">
<h2 class="card-title">
<div class="skeleton h-12 w-80"></div>
</h2>
<div class="skeleton h-8 w-72"></div>
</div>
</div>
</div>
</Match>
<Match when={!loading() && machines().length === 0}>
No machines found
</Match>
<Match when={!loading()}>
<ul>
<For each={machines()}>
{(entry) => (
<li>
<div class="card card-side m-2 bg-base-100 shadow-lg">
<figure class="pl-2">
<span class="material-icons content-center text-5xl">
devices_other
</span>
</figure>
<div class="card-body flex-row justify-between">
<div class="flex flex-col">
<h2 class="card-title">{entry}</h2>
{/*
<p
classList={{
"text-gray-400": !entry.machine_description,
"text-gray-600": !!entry.machine_description,
}}
>
{entry.machine_description || "No description"}
</p>
*/}
</div>
<div>
<button class="btn btn-ghost">
<span class="material-icons">more_vert</span>
</button>
</div>
</div>
</div>
</li>
)}
</For>
</ul>
</Match>
</Switch>
</div>
);
};

View File

@ -1,11 +1,9 @@
const typography = require("@tailwindcss/typography");
const daisyui = require("daisyui"); const daisyui = require("daisyui");
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"], content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [typography, daisyui], plugins: [daisyui],
}; };

View File

@ -11,7 +11,7 @@
npmDeps = pkgs.fetchNpmDeps { npmDeps = pkgs.fetchNpmDeps {
src = ./app; src = ./app;
hash = "sha256-EadzSkIsV/cJtdxpIUvvpQhu5h3VyF8bLMpwfksNmWQ="; hash = "sha256-E0++hupVKnDqmLk7ljoMcqcI4w+DIMlfRYRPbKUsT2c=";
}; };
# The prepack script runs the build script, which we'd rather do in the build phase. # The prepack script runs the build script, which we'd rather do in the build phase.
npmPackFlags = [ "--ignore-scripts" ]; npmPackFlags = [ "--ignore-scripts" ];

View File

@ -1,2 +0,0 @@
# DO NOT DELETE
# This file is used by the clan cli to discover a clan flake

View File

@ -1,5 +0,0 @@
{
"name": "My Empty Clan",
"description": "some nice description",
"icon": "A path to the png"
}

View File

@ -1,16 +0,0 @@
# Clan configuration file
# TODO: This file is used as a template for the simple GUI workflow
{
inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core";
outputs =
{ self, clan-core, ... }:
let
clan = clan-core.lib.buildClan {
# This clan builds all its configuration out of the current directory
directory = self;
};
in
{
inherit (clan) nixosConfigurations clanInternals;
};
}

View File

@ -5,10 +5,6 @@
description = "Initialize a new clan flake"; description = "Initialize a new clan flake";
path = ./new-clan; path = ./new-clan;
}; };
empty = {
description = "A empty clan template. Primarily for usage with the clan ui";
path = ./empty;
};
default = self.templates.new-clan; default = self.templates.new-clan;
}; };
} }

View File

@ -1,7 +1,7 @@
{ {
description = "<Put your description here>"; description = "<Put your description here>";
inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core"; inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
outputs = outputs =
{ self, clan-core, ... }: { self, clan-core, ... }: