Compare commits

..

2 Commits

Author SHA1 Message Date
Valentin Gagarin
8ac3c62d5d use a custom installer for pre-commit hooks 2024-05-22 13:46:46 +02:00
Valentin Gagarin
444c61d736 add pre-commit check
make sure things are sane before they hit CI.
this re-purposes the existing treefmt configuration.
2024-05-22 13:46:46 +02:00
56 changed files with 2338 additions and 2588 deletions

View File

@@ -7,8 +7,6 @@
#!${pkgs.bash}/bin/bash
set -euo pipefail
unset CLAN_DIR
export PATH="${
lib.makeBinPath [
pkgs.gitMinimal

View File

@@ -17,7 +17,6 @@
static-hosts = ./static-hosts;
syncthing = ./syncthing;
thelounge = ./thelounge;
trusted-nix-caches = ./trusted-nix-caches;
user-password = ./user-password;
xfce = ./xfce;
zerotier-static-peers = ./zerotier-static-peers;

View File

@@ -1,2 +0,0 @@
This module sets the `clan.lol` and `nix-community` cache up as a trusted cache.
----

View File

@@ -1,10 +0,0 @@
{
nix.settings.trusted-substituters = [
"https://cache.clan.lol"
"https://nix-community.cachix.org"
];
nix.settings.trusted-public-keys = [
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
"cache.clan.lol-1:3KztgSAB5R1M+Dz7vzkBGzXdodizbgLXGXKXlcQLA28="
];
}

View File

@@ -28,7 +28,6 @@ markdown_extensions:
- pymdownx.highlight:
use_pygments: true
anchor_linenums: true
- pymdownx.keys
- toc:
title: On this page
@@ -65,7 +64,6 @@ nav:
- reference/clanModules/syncthing.md
- reference/clanModules/static-hosts.md
- reference/clanModules/thelounge.md
- reference/clanModules/trusted-nix-caches.md
- reference/clanModules/user-password.md
- reference/clanModules/xfce.md
- reference/clanModules/zerotier-static-peers.md
@@ -94,8 +92,8 @@ docs_dir: site
site_dir: out
theme:
logo: https://clan.lol/static/logo/clan-white.png
favicon: https://clan.lol/static/logo/clan-dark.png
logo: static/clan-white.png
favicon: static/clan-dark.png
name: material
features:
- navigation.instant
@@ -105,8 +103,6 @@ theme:
- content.tabs.link
icon:
repo: fontawesome/brands/git
font:
code: Roboto Mono
palette:
# Palette toggle for light mode

View File

@@ -2,8 +2,6 @@
pkgs,
module-docs,
clan-cli-docs,
asciinema-player-js,
asciinema-player-css,
...
}:
let
@@ -29,10 +27,6 @@ pkgs.stdenv.mkDerivation {
mkdir -p ./site/reference/cli
cp -af ${module-docs}/* ./site/reference/
cp -af ${clan-cli-docs}/* ./site/reference/cli/
mkdir -p ./site/static/asciinema-player
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
ln -snf ${asciinema-player-css} ./site/static/asciinema-player/asciinema-player.css
'';
buildPhase = ''

View File

@@ -40,15 +40,6 @@
mypy --strict $out
'';
asciinema-player-js = pkgs.fetchurl {
url = "https://github.com/asciinema/asciinema-player/releases/download/v3.7.0/asciinema-player.min.js";
sha256 = "sha256-Ymco/+FinDr5YOrV72ehclpp4amrczjo5EU3jfr/zxs=";
};
asciinema-player-css = pkgs.fetchurl {
url = "https://github.com/asciinema/asciinema-player/releases/download/v3.7.0/asciinema-player.css";
sha256 = "sha256-GZMeZFFGvP5GMqqh516mjJKfQaiJ6bL38bSYOXkaohc=";
};
module-docs = pkgs.runCommand "rendered" { nativeBuildInputs = [ pkgs.python3 ]; } ''
export CLAN_CORE=${jsonDocs.clanCore}/share/doc/nixos/options.json
# A file that contains the links to all clanModule docs
@@ -65,16 +56,12 @@
devShells.docs = pkgs.callPackage ./shell.nix {
inherit (self'.packages) docs clan-cli-docs;
inherit module-docs;
inherit asciinema-player-js;
inherit asciinema-player-css;
};
packages = {
docs = pkgs.python3.pkgs.callPackage ./default.nix {
inherit (self'.packages) clan-cli-docs;
inherit (inputs) nixpkgs;
inherit module-docs;
inherit asciinema-player-js;
inherit asciinema-player-css;
};
deploy-docs = pkgs.callPackage ./deploy-docs.nix { inherit (config.packages) docs; };
inherit module-docs;

View File

@@ -3,8 +3,6 @@
pkgs,
module-docs,
clan-cli-docs,
asciinema-player-js,
asciinema-player-css,
...
}:
pkgs.mkShell {
@@ -16,9 +14,5 @@ pkgs.mkShell {
chmod +w ./site/reference/*
echo "Generated API documentation in './site/reference/' "
mkdir -p ./site/static/asciinema-player
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
ln -snf ${asciinema-player-css} ./site/static/asciinema-player/asciinema-player.css
'';
}

View File

@@ -1,26 +1,21 @@
authors:
DavHau:
name: "DavHau"
description: "Core Developer"
avatar: "https://clan.lol/static/profiles/davhau.jpg"
url: "https://DavHau.com"
Lassulus:
name: "Lassulus"
description: "Core Developer"
avatar: "https://clan.lol/static/profiles/lassulus.jpg"
description: "Contributor to Clan"
avatar: "https://avatars.githubusercontent.com/u/621759?v=4"
url: "https://http://lassul.us/"
Mic92:
name: "Mic92"
description: "Core Developer"
avatar: "https://clan.lol/static/profiles/mic92.jpg"
description: "Contributor to Clan"
avatar: "https://avatars.githubusercontent.com/u/96200?v=4"
url: "https://thalheim.io"
W:
name: "W"
description: "Founder of Clan"
avatar: "https://clan.lol/static/profiles/w_profile.webp"
avatar: "/static/w_profile.webp"
url: ""
Qubasa:
name: "Qubasa"
description: "Core Developer"
avatar: "https://clan.lol/static/profiles/qubasa.png"
url: "https://github.com/Qubasa"
description: "Contributor to Clan"
avatar: "https://avatars.githubusercontent.com/u/22085373?v=4"
url: "https://github.com/Qubasa"

View File

@@ -1,194 +0,0 @@
---
title: "Dev Report: Introducing the NixOS to JSON Schema Converter"
description: "Discover our new library designed to extract JSON schema interfaces from NixOS modules, streamlining frontend development"
authors:
- DavHau
date: 2024-05-25
slug: jsonschema-converter
---
## Overview
Weve developed a new library designed to extract interfaces from NixOS modules and convert them into JSON schemas, paving the way for effortless GUI generation. This blog post outlines the motivations behind this development, demonstrates the capabilities of the library, and guides you through leveraging it to create GUIs seamlessly.
## Motivation
In recent months, our team has been exploring various graphical user interfaces (GUIs) to streamline NixOS machine configuration. While our opinionated Clan modules simplify NixOS configurations, there's a need to configure these modules from diverse frontends, such as:
- Command-line interfaces (CLIs)
- Web-based UIs
- Desktop applications
- Mobile applications
- Large Language Models (LLMs)
Given this need, a universal format like JSON is a natural choice. It is already possible as of now, to import json based NixOS configurations, as illustrated below:
`configuration.json`:
```json
{ "networking": { "hostName": "my-machine" } }
```
This configuration can be then imported inside a classic NixOS config:
```nix
{config, lib, pkgs, ...}: {
imports = [
(lib.importJSON ./configuration.json)
];
}
```
This straightforward approach allows us to build a frontend that generates JSON, enabling the configuration of NixOS machines. But, two critical questions arise:
1. How does the frontend learn about existing configuration options?
2. How can it verify user input without running Nix?
Introducing [JSON schema](https://json-schema.org/), a widely supported standard that defines interfaces in JSON and validates input against them.
Example schema for `networking.hostName`:
```json
{
"type": "object",
"properties": {
"networking": {
"type": "object",
"properties": {
"hostName": {
"type": "string",
"pattern": "^$|^[a-z0-9]([a-z0-9_-]{0,61}[a-z0-9])?$"
}
}
}
}
}
```
## Client-Side Input Validation
Validating input against JSON schemas is both efficient and well-supported across numerous programming languages. Using JSON schema validators, you can accurately check configurations like our `configuration.json`.
Validation example:
```shell
$ nix-shell -p check-jsonschema
$ jsonschema -o pretty ./schema.json -i ./configuration.json
===[SUCCESS]===(./configuration.json)===
```
In case of invalid input, schema validators provide explicit error messages:
```shell
$ echo '{ "networking": { "hostName": "my/machine" } }' > configuration.json
$ jsonschema -o pretty ./schema.json -i ./configuration.json
===[ValidationError]===(./configuration.json)===
'my/machine' does not match '^$|^[a-z0-9]([a-z0-9_-]{0,61}[a-z0-9])?$'
Failed validating 'pattern' in schema['properties']['networking']['properties']['hostName']:
{'pattern': '^$|^[a-z0-9]([a-z0-9_-]{0,61}[a-z0-9])?$',
'type': 'string'}
On instance['networking']['hostName']:
'my/machine'
```
## Automatic GUI Generation
Certain libraries facilitate straightforward GUI generation from JSON schemas. For instance, the [react-jsonschema-form playground](https://rjsf-team.github.io/react-jsonschema-form/) auto-generates a form for any given schema.
## NixOS Module to JSON Schema Converter
To enable the development of responsive frontends, our library allows the extraction of interfaces from NixOS modules to JSON schemas. Open-sourced for community collaboration, this library supports building sophisticated user interfaces for NixOS.
Heres a preview of our library's functions exposed through the [clan-core](https://git.clan.lol/clan/clan-core) flake:
- `lib.jsonschema.parseModule` - Generates a schema for a NixOS module.
- `lib.jsonschema.parseOption` - Generates a schema for a single NixOS option.
- `lib.jsonschema.parseOptions` - Generates a schema from an attrset of NixOS options.
Example:
`module.nix`:
```nix
{lib, config, pkgs, ...}: {
# a simple service with two options
options.services.example-web-service = {
enable = lib.mkEnableOption "Example web service";
port = lib.mkOption {
type = lib.types.int;
description = "Port used to serve the content";
};
};
}
```
Converted, using the `parseModule` function:
```shell
$ cd clan-core
$ nix eval --json --impure --expr \
'(import ./lib/jsonschema {}).parseModule ./module.nix' | jq | head
{
"properties": {
"services": {
"properties": {
"example-web-service": {
"properties": {
"enable": {
"default": false,
"description": "Whether to enable Example web service.",
"examples": [
...
```
This utility can also generate interfaces for existing NixOS modules or options.
## GUI for NGINX in Under a Minute
Creating a prototype GUI for the NGINX module using our library and [react-jsonschema-form playground](https://rjsf-team.github.io/react-jsonschema-form/) can be done quickly:
1. Export all NGINX options into a JSON schema using a Nix expression:
```nix
# export.nix
let
pkgs = import <nixpkgs> {};
clan-core = builtins.getFlake "git+https://git.clan.lol/clan/clan-core";
options = (pkgs.nixos {}).options.services.nginx;
in
clan-core.lib.jsonschema.parseOption options
```
2. Write the schema into a file:
```shell
$ nix eval --json -f ./export.nix | jq > nginx.json
```
3. Open the [react-jsonschema-form playground](https://rjsf-team.github.io/react-jsonschema-form/), select `Blank` and paste the `nginx.json` contents.
This provides a quick look at a potential GUI (screenshot is cropped).
![Image title](https://clan.lol/static/blog-post-jsonschema/nginx-gui.jpg)
## Limitations
### Laziness
JSON schema mandates the declaration of all required fields upfront, which might be configured implicitly or remain unused. For instance, `services.nginx.virtualHosts.<name>.sslCertificate` must be specified even if SSL isnt enabled.
### Limited Types
Certain NixOS module types, like `types.functionTo` and `types.package`, do not map straightforwardly to JSON. For full compatibility, adjustments to NixOS modules might be necessary, such as substituting `listOf package` with `listOf str`.
### Parsing NixOS Modules
Currently, our converter relies on the `options` attribute of evaluated NixOS modules, extracting information from the `type.name` attribute, which is suboptimal. Enhanced introspection capabilities within the NixOS module system would be beneficial.
## Future Prospects
We hope these experiments inspire the community, encourage contributions and further development in this space. Share your ideas and contributions through our issue tracker or matrix channel!
## Links
- [Comments on NixOS Discourse](https://discourse.nixos.org/t/introducing-the-nixos-to-json-schema-converter/45948)
- [Source Code of the JSON Schema Library](https://git.clan.lol/clan/clan-core/src/branch/main/lib/jsonschema)
- [Our Issue Tracker](https://git.clan.lol/clan/clan-core/issues)
- [Our Matrix Channel](https://matrix.to/#/#clan:lassul.us)
- [react-jsonschema-form Playground](https://rjsf-team.github.io/react-jsonschema-form/)

View File

@@ -1,63 +0,0 @@
---
title: "Git Based Machine Deployment with Clan-Core"
description: ""
authors:
- Qubasa
date: 2024-05-25
---
## Revolutionizing Server Management
In the world of server management, countless tools claim to offer seamless deployment of multiple machines. Yet, many fall short, leaving server admins and self-hosting enthusiasts grappling with complexity. Enter the Clan-Core Framework—a groundbreaking all in one solution designed to transform decentralized self-hosting into an effortless and scalable endeavor.
### The Power of Clan-Core
Imagine having the power to manage your servers with unparalleled ease, scaling your IT infrastructure like never before. Clan-Core empowers you to do just that. At its core, Clan-Core leverages a single Git repository to define everything about your machines. This central repository utilizes Nix or JSON files to specify configurations, including disk formatting, ensuring a streamlined and unified approach.
### Simplified Deployment Process
With Clan-Core, the cumbersome task of bootstrapping a specific ISO is a thing of the past. All you need is SSH access to your Linux server. Clan-Core allows you to overwrite any existing Linux distribution live over SSH, eliminating time-consuming setup processes. This capability means you can deploy updates or new configurations swiftly and efficiently, maximizing uptime and minimizing hassle.
### Secure and Efficient Secret Management
Security is paramount in server management, and Clan-Core takes it seriously. Passwords and other sensitive information are encrypted within the Git repository, automatically decrypted during deployment. This not only ensures the safety of your secrets but also simplifies their management. Clan-Core supports sharing secrets with other admins, fostering collaboration and maintaining reproducibillity and security without sacrificing convenience.
### Services as Apps
Setting up a service can be quite difficult. Many server adjustments need to be made, from setting up a database to adjusting webserver configurations and generating the correct private keys. However, Clan-Core aims to make setting up a service as easy as installing an application. Through Clan-Core's Module system, everything down to secrets can be automatically set up. This transforms the often daunting task of service setup into a smooth, automated process, making it accessible to all.
### Decentralized Mesh VPN
Building on these features is a self-configuring decentralized mesh VPN that interconnects all your machines into a private darknet. This ensures that sensitive services, which might have too much attack surface to be hosted on the public internet, can still be made available privately without the need to worry about potential system compromise. By creating a secure, private network, Clan-Core offers an additional layer of protection for your most critical services.
### Decentralized Domain Name System
Current DNS implementations are distributed but not truly decentralized. For Clan-Core, we implemented our own truly decentralized DNS module. This module uses simple flooding and caching algorithms to discover available domains inside the darknet. This approach ensures that your internal domain name system is robust, reliable, and independent of external control, enhancing the resilience and security of your infrastructure.
### A New Era of Decentralized Self-Hosting
Clan-Core is more than just a tool; it's a paradigm shift in server management. By consolidating machine definitions, secrets and network configuration, into a single, secure repository, it transforms how you manage and scale your infrastructure. Whether you're a seasoned server admin or a self-hosting enthusiast, Clan-Core offers a powerful, user-friendly solution to take your capabilities to the next level.
### Key Features of Clan-Core:
- **Unified Git Repository**: All machine configurations and secrets stored in a single repository.
- **Live Overwrites**: Deploy configurations over existing Linux distributions via SSH.
- **Automated Service Setup**: Easily set up services with Clan-Core's Module system.
- **Decentralized Mesh VPN**: Securely interconnect all machines into a private darknet.
- **Decentralized DNS**: Robust, independent DNS using flooding and caching algorithms.
- **Automated Secret Management**: Encrypted secrets that are automatically decrypted during deployment.
- **Collaboration Support**: Share secrets securely with other admins.
## Clan-Cores Future
Our vision for Clan-Core extends far beyond being just another deployment tool. Clan-Core is a framework we've developed to achieve something much greater. We want to put the "personal" back into "personal computing." Our goal is for everyday users to fully customize their phones or laptops and create truly private spaces for friends and family.
Our first major step is to develop a Graphical User Interface (GUI) that makes configuring all this possible. Initial tests have shown that AI can be leveraged as an alternative to traditional GUIs. This paves the way for a future where people can simply talk to their computers, and they will configure themselves according to the users' wishes.
By adopting Clan, you're not just embracing a tool—you're joining a movement towards a more efficient, secure, and scalable approach to server management. Join us and revolutionize your IT infrastructure today.

View File

@@ -59,7 +59,7 @@ Adding or configuring a new machine requires two simple steps:
Which should show something like:
```{.shellSession hl_lines="6" .no-copy}
```bash hl_lines="6"
NAME ID-LINK FSTYPE SIZE MOUNTPOINT
sda usb-ST_16GB_AA6271026J1000000509-0:0 14.9G
├─sda1 usb-ST_16GB_AA6271026J1000000509-0:0-part1 1M

View File

@@ -52,7 +52,7 @@ This process involves preparing a suitable hardware and disk partitioning config
This is an example of the booted installer.
```{ .bash .annotate .no-copy .nohighlight}
```{ .bash .annotate .no-copy }
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────┐ │
│ │███████████████████████████│ # This is the QR Code (1) │
@@ -151,7 +151,7 @@ Clan CLI enables you to remotely update your machines over SSH. This requires se
### Setting the Target Host
Replace `root@jon` with the actual hostname or IP address of your target machine:
```{.nix hl_lines="9" .no-copy}
```nix hl_lines="9"
buildClan {
# ...
machines = {
@@ -192,7 +192,7 @@ it is also possible to specify a build host instead.
During an update, the cli will ssh into the build host and run `nixos-rebuild` from there.
```{.nix hl_lines="5" .no-copy}
```nix hl_lines="5"
buildClan {
# ...
machines = {
@@ -208,7 +208,7 @@ buildClan {
To exclude machines from being updated when running `clan machines update` without any machines specified,
one can set the `clan.deployment.requireExplicitUpdate` option to true:
```{.nix hl_lines="5" .no-copy}
```nix hl_lines="5"
buildClan {
# ...
machines = {

View File

@@ -94,4 +94,9 @@ Below is a guide on how to structure this in your flake.nix:
For detailed information about configuring `flake-parts` and the available options within Clan,
refer to the Clan module documentation located [here](https://git.clan.lol/clan/clan-core/src/branch/main/flakeModules/clan.nix).
## Whats next?
- [Configure Machines](configure.md): Customize machine configuration
- [Deploying](deploy.md): Deploying a Machine configuration
---

View File

@@ -22,7 +22,7 @@ Follow our step-by-step guide to create and transfer this image onto a bootable
lsblk
```
```{.shellSession hl_lines="2" .no-copy}
```shellSession hl_lines="2"
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sdb 8:0 1 117,2G 0 disk
└─sdb1 8:1 1 117,2G 0 part /run/media/qubasa/INTENSO
@@ -123,7 +123,7 @@ This will enter `iwd`
Now run the following command to connect to your Wifi:
```{.shellSession .no-copy}
```shellSession
# Identify your network device.
device list
@@ -148,10 +148,10 @@ Connected network FRITZ!Box (Your router device)
IPv4 address 192.168.188.50 (Your new local ip)
```
Press ++ctrl+d++ to exit `IWD`.
Press `ctrl-d` to exit `IWD`.
!!! Important
Press ++ctrl+d++ **again** to update the displayed QR code and connection information.
Press `ctrl-d` **again** to update the displayed QR code and connection information.
You're all set up
@@ -161,4 +161,4 @@ You're all set up
- [Configure Machines](configure.md): Customize machine configuration
---
---

View File

@@ -9,7 +9,7 @@ include a new machine into the VPN.
By default all machines within one clan are connected via a chosen network technology.
```{.no-copy}
```
Clan
Node A
<-> (zerotier / mycelium / ...)
@@ -36,7 +36,7 @@ peers. Once addresses are allocated, the controller's continuous operation is no
```
3. **Update the Controller Machine**: Execute the following:
```bash
clan machines update <CONTROLLER>
$ clan machines update <CONTROLLER>
```
Your machine is now operational as the VPN controller.

View File

@@ -1 +0,0 @@
/nix/store/8y5h98wk5p94mv1wyb2c4gkrr7bswd19-asciinema-player.css

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
/nix/store/w0i3f9qzn9n6jmfnfgiw5wnab2f9ssdw-asciinema-player.min.js

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,45 @@
.asciinema-player-theme-alabaster-auto {
--term-color-foreground: #000000; /* Black for foreground text */
--term-color-background: #f7f7f7; /* Very light gray for background */
--term-color-0: #000000; /* Black */
--term-color-1: #aa3731; /* Red */
--term-color-2: #448c37; /* Green */
--term-color-3: #cb9000; /* Yellow */
--term-color-4: #325cc0; /* Blue */
--term-color-5: #7a3e9d; /* Magenta */
--term-color-6: #0083b2; /* Cyan */
--term-color-7: #bbbbbb; /* White */
--term-color-8: #777777; /* Bright black (gray) */
--term-color-9: #f05050; /* Bright red */
--term-color-10: #60cb00; /* Bright green */
--term-color-11: #ffbc5d; /* Bright yellow */
--term-color-12: #007acc; /* Bright blue */
--term-color-13: #e64ce6; /* Bright magenta */
--term-color-14: #00aacb; /* Bright cyan */
--term-color-15: #ffffff; /* Bright white */
}
@media (prefers-color-scheme: dark) {
.asciinema-player-theme-solarized-auto {
--term-color-foreground: #839496;
--term-color-background: #002b36;
--term-color-0: #073642;
--term-color-1: #dc322f;
--term-color-2: #859900;
--term-color-3: #b58900;
--term-color-4: #268bd2;
--term-color-5: #d33682;
--term-color-6: #2aa198;
--term-color-7: #eee8d5;
--term-color-8: #002b36;
--term-color-9: #cb4b16;
--term-color-10: #586e75;
--term-color-11: #657b83;
--term-color-12: #839496;
--term-color-13: #6c71c4;
--term-color-14: #93a1a1;
--term-color-15: #fdf6e3;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

16
flake.lock generated
View File

@@ -7,15 +7,15 @@
]
},
"locked": {
"lastModified": 1716394172,
"narHash": "sha256-B+pNhV8GFeCj9/MoH+qtGqKbgv6fU4hGaw2+NoYYtB0=",
"owner": "nix-community",
"lastModified": 1714400597,
"narHash": "sha256-AA1TCyEl4O6+6F5man/V5VH9Zl9HPBpK91tSkZ16i2E=",
"owner": "Qubasa",
"repo": "disko",
"rev": "23c63fb09334c3e8958b57e2ddc3870b75b9111d",
"rev": "58785136b8c37aeb2f67081387b48f663b166331",
"type": "github"
},
"original": {
"owner": "nix-community",
"owner": "Qubasa",
"repo": "disko",
"type": "github"
}
@@ -43,11 +43,11 @@
"git-hooks": {
"flake": false,
"locked": {
"lastModified": 1716413087,
"narHash": "sha256-nSTIB7JeJGBGsvtqlyfhUByh/isyK1nfOq2YMxUOFJQ=",
"lastModified": 1715912400,
"narHash": "sha256-7GYXKJP7bglYgofN3/PW5EBGaGOIKbBKSgFb6m4ttC4=",
"owner": "fricklerhandwerk",
"repo": "git-hooks",
"rev": "99a78fcf7dc03ba7b1d5c00af109c1e28ced3490",
"rev": "5a23214ca74a7656048fe1402c4e98f5cd2b3729",
"type": "github"
},
"original": {

View File

@@ -8,7 +8,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small";
disko.url = "github:nix-community/disko";
disko.url = "github:Qubasa/disko";
disko.inputs.nixpkgs.follows = "nixpkgs";
sops-nix.url = "github:Mic92/sops-nix";
sops-nix.inputs.nixpkgs.follows = "nixpkgs";

View File

@@ -83,20 +83,20 @@ rec {
in
# either type
# TODO: if all nested options are excluded, the parent should be excluded too
# TODO: if all nested optiosn are excluded, the parent sould be excluded too
if
option.type.name or null == "either" || option.type.name or null == "coercedTo"
option.type.name or null == "either"
# return jsonschema property definition for either
then
let
optionsList' = [
{
type = option.type.nestedTypes.left or option.type.nestedTypes.coercedType;
type = option.type.nestedTypes.left;
_type = "option";
loc = option.loc;
}
{
type = option.type.nestedTypes.right or option.type.nestedTypes.finalType;
type = option.type.nestedTypes.right;
_type = "option";
loc = option.loc;
}
@@ -157,21 +157,12 @@ rec {
# TODO: Add support for intMatching in jsonschema
# parse port type aka. "unsignedInt16"
else if
option.type.name == "unsignedInt16"
|| option.type.name == "unsignedInt"
|| option.type.name == "pkcs11"
|| option.type.name == "intBetween"
then
else if option.type.name == "unsignedInt16" then
default // example // description // { type = "integer"; }
# parse string
# TODO: parse more precise string types
else if
option.type.name == "str"
|| option.type.name == "singleLineStr"
|| option.type.name == "passwdEntry str"
|| option.type.name == "passwdEntry path"
# return jsonschema property definition for string
then
default // example // description // { type = "string"; }

View File

@@ -1,12 +1,10 @@
import json
from clan_cli.api import API
def main() -> None:
schema = API.to_json_schema()
print(
f"""export const schema = {json.dumps(schema, indent=2)} as const;
f"""export const schema = {schema} as const;
"""
)

View File

@@ -52,17 +52,7 @@ class AppendOptionAction(argparse.Action):
def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog=prog,
description="cLAN tool",
epilog=(
"""
Online reference for the clan cli tool: https://docs.clan.lol/reference/cli/
For more detailed information, visit: https://docs.clan.lol
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser = argparse.ArgumentParser(prog=prog, description="cLAN tool")
parser.add_argument(
"--debug",
@@ -100,142 +90,28 @@ For more detailed information, visit: https://docs.clan.lol
"backups",
help="manage backups of clan machines",
description="manage backups of clan machines",
epilog=(
"""
This subcommand provides an interface to backups that clan machines expose.
Examples:
$ clan backups list [MACHINE]
List backups for the machine [MACHINE]
$ clan backups create [MACHINE]
Create a backup for the machine [MACHINE].
$ clan backups restore [MACHINE] [PROVIDER] [NAME]
The backup to restore for the machine [MACHINE] with the configured [PROVIDER]
with the name [NAME].
For more detailed information, visit: https://docs.clan.lol/getting-started/backups/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
backups.register_parser(parser_backups)
parser_flake = subparsers.add_parser(
"flakes",
help="create a clan flake inside the current directory",
epilog=(
"""
Examples:
$ clan flakes create [DIR]
Will create a new clan flake in the specified directory and create it if it
doesn't exist yet. The flake will be created from a default template.
For more detailed information, visit: https://docs.clan.lol/getting-started
"""
),
formatter_class=argparse.RawTextHelpFormatter,
"flakes", help="create a clan flake inside the current directory"
)
flakes.register_parser(parser_flake)
parser_config = subparsers.add_parser(
"config",
help="set nixos configuration",
epilog=(
"""
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser_config = subparsers.add_parser("config", help="set nixos configuration")
config.register_parser(parser_config)
parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine")
ssh_cli.register_parser(parser_ssh)
parser_secrets = subparsers.add_parser(
"secrets",
help="manage secrets",
epilog=(
"""
This subcommand provides an interface to secret facts.
Examples:
$ clan secrets list [regex]
Will list secrets for all managed machines.
It accepts an optional regex, allowing easy filtering of returned secrets.
$ clan secrets get [SECRET]
Will display the content of the specified secret.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser_secrets = subparsers.add_parser("secrets", help="manage secrets")
secrets.register_parser(parser_secrets)
parser_facts = subparsers.add_parser(
"facts",
help="manage facts",
epilog=(
"""
This subcommand provides an interface to facts of clan machines.
Facts are artifacts that a service can generate.
There are public and secret facts.
Public facts can be referenced by other machines directly.
Public facts can include: ip addresses, public keys.
Secret facts can include: passwords, private keys.
A service is an included clan-module that implements facts generation functionality.
For example the zerotier module will generate private and public facts.
In this case the public fact will be the resulting zerotier-ip of the machine.
The secret fact will be the zerotier-identity-secret, which is used by zerotier
to prove the machine has control of the zerotier-ip.
Examples:
$ clan facts generate
Will generate facts for all machines.
$ clan facts generate --service [SERVICE] --regenerate
Will regenerate facts, if they are already generated for a specific service.
This is especially useful for resetting certain passwords while leaving the rest
of the facts for a machine in place.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser_facts = subparsers.add_parser("facts", help="manage facts")
facts.register_parser(parser_facts)
parser_machine = subparsers.add_parser(
"machines",
help="manage machines and their configuration",
epilog=(
"""
This subcommand provides an interface to machines managed by clan.
Examples:
$ clan machines list
List all the machines managed by clan.
$ clan machines update [MACHINES]
Will update the specified machine [MACHINE], if [MACHINE] is omitted, the command
will attempt to update every configured machine.
$ clan machines install [MACHINES] [TARGET_HOST]
Will install the specified machine [MACHINE], to the specified [TARGET_HOST].
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
"""
),
formatter_class=argparse.RawTextHelpFormatter,
"machines", help="manage machines and their configuration"
)
machines.register_parser(parser_machine)

View File

@@ -1,37 +1,18 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic, Literal, TypeVar
T = TypeVar("T")
ResponseDataType = TypeVar("ResponseDataType")
@dataclass
class ApiError:
message: str
description: str | None
location: list[str] | None
@dataclass
class ApiResponse(Generic[ResponseDataType]):
status: Literal["success", "error"]
errors: list[ApiError] | None
data: ResponseDataType | None
from typing import Any
class _MethodRegistry:
def __init__(self) -> None:
self._registry: dict[str, Callable[[Any], Any]] = {}
self._registry: dict[str, Callable] = {}
def register(self, fn: Callable[..., T]) -> Callable[..., T]:
def register(self, fn: Callable) -> Callable:
self._registry[fn.__name__] = fn
return fn
def to_json_schema(self) -> dict[str, Any]:
def to_json_schema(self) -> str:
# Import only when needed
import json
from typing import get_type_hints
from clan_cli.api.util import type_to_dict
@@ -40,51 +21,25 @@ class _MethodRegistry:
"$comment": "An object containing API methods. ",
"type": "object",
"additionalProperties": False,
"required": [func_name for func_name in self._registry.keys()],
"required": ["list_machines"],
"properties": {},
}
for name, func in self._registry.items():
hints = get_type_hints(func)
serialized_hints = {
key: type_to_dict(
"argument" if key != "return" else "return": type_to_dict(
value, scope=name + " argument" if key != "return" else "return"
)
for key, value in hints.items()
}
return_type = serialized_hints.pop("return")
api_schema["properties"][name] = {
"type": "object",
"required": ["arguments", "return"],
"required": [k for k in serialized_hints.keys()],
"additionalProperties": False,
"properties": {
"return": return_type,
"arguments": {
"type": "object",
"required": [k for k in serialized_hints.keys()],
"additionalProperties": False,
"properties": serialized_hints,
},
},
"properties": {**serialized_hints},
}
return api_schema
def get_method_argtype(self, method_name: str, arg_name: str) -> Any:
from inspect import signature
func = self._registry.get(method_name, None)
if func:
sig = signature(func)
param = sig.parameters.get(arg_name)
if param:
param_class = param.annotation
return param_class
return None
return json.dumps(api_schema, indent=2)
API = _MethodRegistry()

View File

@@ -42,14 +42,9 @@ def type_to_dict(t: Any, scope: str = "") -> dict:
return {"type": "array", "items": type_to_dict(t.__args__[0], scope)}
elif issubclass(origin, dict):
value_type = t.__args__[1]
if value_type is Any:
return {"type": "object", "additionalProperties": True}
else:
return {
"type": "object",
"additionalProperties": type_to_dict(value_type, scope),
}
return {
"type": "object",
}
raise BaseException(f"Error api type not yet supported {t!s}")

View File

@@ -23,42 +23,7 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
register_list_parser(list_parser)
parser_generate = subparser.add_parser(
"generate",
help="generate public and secret facts for machines",
epilog=(
"""
This subcommand allows control of the generation of facts.
Often this function will be invoked automatically on deploying machines,
but there are situations the user may want to have more granular control,
especially for the regeneration of certain services.
A service is an included clan-module that implements facts generation functionality.
For example the zerotier module will generate private and public facts.
In this case the public fact will be the resulting zerotier-ip of the machine.
The secret fact will be the zerotier-identity-secret, which is used by zerotier
to prove the machine has control of the zerotier-ip.
Examples:
$ clan facts generate
Will generate facts for all machines.
$ clan facts generate [MACHINE]
Will generate facts for the specified machine.
$ clan facts generate [MACHINE] --service [SERVICE]
Will generate facts for the specified machine for the specified service.
$ clan facts generate --service [SERVICE] --regenerate
Will regenerate facts, if they are already generated for a specific service.
This is especially useful for resetting certain passwords while leaving the rest
of the facts for a machine in place.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
"generate", help="generate secrets for machines if they don't exist yet"
)
register_generate_parser(parser_generate)

View File

@@ -33,7 +33,6 @@ def read_multiline_input(prompt: str = "Finish with Ctrl-D") -> str:
def generate_service_facts(
machine: Machine,
service: str,
regenerate: bool,
secret_facts_store: SecretStoreBase,
public_facts_store: FactStoreBase,
tmpdir: Path,
@@ -43,7 +42,7 @@ def generate_service_facts(
# check if all secrets exist and generate them if at least one is missing
needs_regeneration = not check_secrets(machine, service=service)
log.debug(f"{service} needs_regeneration: {needs_regeneration}")
if not (needs_regeneration or regenerate):
if not needs_regeneration:
return False
if not isinstance(machine.flake, Path):
msg = f"flake is not a Path: {machine.flake}"
@@ -135,11 +134,7 @@ def prompt_func(text: str) -> str:
def _generate_facts_for_machine(
machine: Machine,
service: str | None,
regenerate: bool,
tmpdir: Path,
prompt: Callable[[str], str] = prompt_func,
machine: Machine, tmpdir: Path, prompt: Callable[[str], str] = prompt_func
) -> bool:
local_temp = tmpdir / machine.name
local_temp.mkdir()
@@ -150,23 +145,10 @@ def _generate_facts_for_machine(
public_facts_store = public_facts_module.FactStore(machine=machine)
machine_updated = False
if service and service not in machine.facts_data:
services = list(machine.facts_data.keys())
raise ClanError(
f"Could not find service with name: {service}. The following services are available: {services}"
)
if service:
machine_service_facts = {service: machine.facts_data[service]}
else:
machine_service_facts = machine.facts_data
for service in machine_service_facts:
for service in machine.facts_data:
machine_updated |= generate_service_facts(
machine=machine,
service=service,
regenerate=regenerate,
secret_facts_store=secret_facts_store,
public_facts_store=public_facts_store,
tmpdir=local_temp,
@@ -179,10 +161,7 @@ def _generate_facts_for_machine(
def generate_facts(
machines: list[Machine],
service: str | None,
regenerate: bool,
prompt: Callable[[str], str] = prompt_func,
machines: list[Machine], prompt: Callable[[str], str] = prompt_func
) -> bool:
was_regenerated = False
with TemporaryDirectory() as tmp:
@@ -191,9 +170,7 @@ def generate_facts(
for machine in machines:
errors = 0
try:
was_regenerated |= _generate_facts_for_machine(
machine, service, regenerate, tmpdir, prompt
)
was_regenerated |= _generate_facts_for_machine(machine, tmpdir, prompt)
except Exception as exc:
log.error(f"Failed to generate facts for {machine.name}: {exc}")
errors += 1
@@ -212,7 +189,7 @@ def generate_command(args: argparse.Namespace) -> None:
machines = get_all_machines(args.flake)
else:
machines = get_selected_machines(args.flake, args.machines)
generate_facts(machines, args.service, args.regenerate)
generate_facts(machines)
def register_generate_parser(parser: argparse.ArgumentParser) -> None:
@@ -223,17 +200,4 @@ def register_generate_parser(parser: argparse.ArgumentParser) -> None:
nargs="*",
default=[],
)
parser.add_argument(
"--service",
type=str,
help="service to generate facts for, if empty, generate facts for every service",
default=None,
)
parser.add_argument(
"--regenerate",
type=bool,
action=argparse.BooleanOptionalAction,
help="whether to regenerate facts for the specified machine",
default=None,
)
parser.set_defaults(func=generate_command)

View File

@@ -26,14 +26,7 @@ def get_all_facts(machine: Machine) -> dict:
def get_command(args: argparse.Namespace) -> None:
machine = Machine(name=args.machine, flake=args.flake)
# the raw_facts are bytestrings making them not json serializable
raw_facts = get_all_facts(machine)
facts = dict()
for key in raw_facts["TODO"]:
facts[key] = raw_facts["TODO"][key].decode("utf8")
print(json.dumps(facts, indent=4))
print(json.dumps(get_all_facts(machine), indent=4))
def register_list_parser(parser: argparse.ArgumentParser) -> None:

View File

@@ -39,7 +39,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
system = config["system"]
# Check if the machine exists
machines = list_machines(flake_url, False)
machines = list_machines(flake_url)
if machine_name not in machines:
raise ClanError(
f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}"

View File

@@ -1,27 +1,13 @@
import argparse
import logging
from dataclasses import dataclass
from pathlib import Path
from clan_cli.api import API
from clan_cli.config.machine import set_config_for_machine
log = logging.getLogger(__name__)
@dataclass
class MachineCreateRequest:
name: str
config: dict[str, int]
@API.register
def create_machine(flake_dir: str | Path, machine: MachineCreateRequest) -> None:
set_config_for_machine(Path(flake_dir), machine.name, machine.config)
def create_command(args: argparse.Namespace) -> None:
create_machine(args.flake, MachineCreateRequest(args.machine, dict()))
set_config_for_machine(args.flake, args.machine, dict())
def register_create_parser(parser: argparse.ArgumentParser) -> None:

View File

@@ -35,7 +35,7 @@ def install_nixos(
target_host = f"{h.user or 'root'}@{h.host}"
log.info(f"target host: {target_host}")
generate_facts([machine], None, False)
generate_facts([machine])
with TemporaryDirectory() as tmpdir_:
tmpdir = Path(tmpdir_)

View File

@@ -1,66 +1,37 @@
import argparse
import dataclasses
import json
import logging
from pathlib import Path
from clan_cli.api import API
from ..cmd import Log, run
from ..cmd import run
from ..nix import nix_config, nix_eval
log = logging.getLogger(__name__)
@dataclasses.dataclass
class MachineInfo:
machine_name: str
machine_description: str | None
machine_icon: str | None
@API.register
def list_machines(flake_url: str | Path, debug: bool) -> dict[str, MachineInfo]:
def list_machines(flake_url: Path | str) -> list[str]:
config = nix_config()
system = config["system"]
cmd = nix_eval(
[
f"{flake_url}#clanInternals.machines.{system}",
"--apply",
"""builtins.mapAttrs (name: attrs: {
inherit (attrs.config.clanCore) machineDescription machineIcon machineName;
})""",
"builtins.attrNames",
"--json",
]
)
if not debug:
proc = run(cmd, log=Log.NONE)
else:
proc = run(cmd)
proc = run(cmd)
res = proc.stdout.strip()
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()
}
return json.loads(res)
def list_command(args: argparse.Namespace) -> None:
flake_path = Path(args.flake).resolve()
print("Listing all machines:\n")
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)
for machine in list_machines(Path(args.flake)):
print(machine)
def register_list_parser(parser: argparse.ArgumentParser) -> None:

View File

@@ -98,7 +98,7 @@ def deploy_nixos(machines: MachineGroup) -> None:
env = os.environ.copy()
env["NIX_SSHOPTS"] = ssh_arg
generate_facts([machine], None, False)
generate_facts([machine])
upload_secrets(machine)
path = upload_sources(".", target)

View File

@@ -69,7 +69,7 @@ def get_secrets(
secret_facts_module = importlib.import_module(machine.secret_facts_module)
secret_facts_store = secret_facts_module.SecretStore(machine=machine)
generate_facts([machine], None, False)
generate_facts([machine])
secret_facts_store.upload(secrets_dir)
return secrets_dir

View File

@@ -26,14 +26,6 @@ def test_create_flake(
cli.run(["machines", "create", "machine1"])
capsys.readouterr() # flush cache
# create a hardware-configuration.nix that doesn't throw an eval error
for patch_machine in ["jon", "sara"]:
with open(
flake_dir / "machines" / f"{patch_machine}/hardware-configuration.nix", "w"
) as hw_config_nix:
hw_config_nix.write("{}")
cli.run(["machines", "list"])
assert "machine1" in capsys.readouterr().out
flake_show = subprocess.run(

View File

@@ -16,10 +16,7 @@ def test_machine_subcommands(
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
out = capsys.readouterr()
assert "machine1" in out.out
assert "vm1" in out.out
assert "vm2" in out.out
assert "machine1\nvm1\nvm2\n" == out.out
cli.run(
["--flake", str(test_flake_with_core.path), "machines", "delete", "machine1"]
@@ -28,7 +25,4 @@ def test_machine_subcommands(
capsys.readouterr()
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
out = capsys.readouterr()
assert "machine1" not in out.out
assert "vm1" in out.out
assert "vm2" in out.out
assert "vm1\nvm2\n" == out.out

View File

@@ -106,7 +106,7 @@ class MainApplication(Adw.Application):
def on_activate(self, source: "MainApplication") -> None:
if not self.window:
self.init_style()
self.window = MainWindow(config=ClanConfig(initial_view="list"))
self.window = MainWindow(config=ClanConfig(initial_view="webview"))
self.window.set_application(self)
self.window.show()

View File

@@ -1,4 +1,3 @@
import dataclasses
import json
import logging
import sys
@@ -9,7 +8,6 @@ from threading import Lock
from typing import Any
import gi
from clan_cli.api import API
gi.require_version("WebKit", "6.0")
@@ -24,23 +22,6 @@ site_index: Path = (
log = logging.getLogger(__name__)
def dataclass_to_dict(obj: Any) -> Any:
"""
Utility function to convert dataclasses to dictionaries
It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries
It does NOT convert member functions.
"""
if dataclasses.is_dataclass(obj):
return {k: dataclass_to_dict(v) for k, v in dataclasses.asdict(obj).items()}
elif isinstance(obj, list | tuple):
return [dataclass_to_dict(item) for item in obj]
elif isinstance(obj, dict):
return {k: dataclass_to_dict(v) for k, v in obj.items()}
else:
return obj
class WebView:
def __init__(self, methods: dict[str, Callable]) -> None:
self.method_registry: dict[str, Callable] = methods
@@ -96,35 +77,12 @@ class WebView:
self.queue_size += 1
def threaded_handler(
self,
handler_fn: Callable[
...,
Any,
],
data: dict[str, Any] | None,
method_name: str,
self, handler_fn: Callable[[Any], Any], data: Any, method_name: str
) -> None:
with self.mutex_lock:
log.debug("Executing... ", method_name)
log.debug(f"{data}")
if data is None:
result = handler_fn()
else:
reconciled_arguments = {}
for k, v in data.items():
# Some functions expect to be called with dataclass instances
# But the js api returns dictionaries.
# Introspect the function and create the expected dataclass from dict dynamically
# Depending on the introspected argument_type
arg_type = API.get_method_argtype(method_name, k)
if dataclasses.is_dataclass(arg_type):
reconciled_arguments[k] = arg_type(**v)
else:
reconciled_arguments[k] = v
result = handler_fn(**reconciled_arguments)
serialized = json.dumps(dataclass_to_dict(result))
result = handler_fn(data)
serialized = json.dumps(result)
# Use idle_add to queue the response call to js on the main GTK thread
GLib.idle_add(self.return_data_to_js, method_name, serialized)

View File

@@ -62,7 +62,8 @@ class MainWindow(Adw.ApplicationWindow):
stack_view.add_named(Logs(), "logs")
webview = WebView(methods=API._registry)
stack_view.add_named(webview.get_webview(), "webview")
stack_view.add_named(webview.get_webview(), "list")
stack_view.set_visible_child_name(config.initial_view)

View File

@@ -1,5 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"tailwindCSS.experimental.classRegex": [["cx\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"]],
"editor.wordWrap": "on"
}

View File

@@ -1,26 +0,0 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import tailwind from "eslint-plugin-tailwindcss";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strict,
...tseslint.configs.stylistic,
...tailwind.configs["flat/recommended"],
{
rules: {
"tailwindcss/no-contradicting-classname": [
"error",
{
callees: ["cx"],
},
],
"tailwindcss/no-custom-classname": [
"error",
{
callees: ["cx"],
},
],
},
}
);

File diff suppressed because it is too large Load Diff

View File

@@ -8,29 +8,22 @@
"build": "npm run check && vite build && npm run convert-html",
"convert-html": "node gtk.webview.js",
"serve": "vite preview",
"check": "tsc --noEmit --skipLibCheck && eslint ./src"
"check": "tsc --noEmit --skipLibCheck"
},
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.3.0",
"@types/node": "^20.12.12",
"@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.19",
"classnames": "^2.5.1",
"daisyui": "^4.11.1",
"eslint": "^8.57.0",
"json-schema-to-ts": "^3.1.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"typescript-eslint": "^7.10.0",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"vite-plugin-solid": "^2.8.2",
"eslint-plugin-tailwindcss": "^3.17.0"
"vite-plugin-solid": "^2.8.2"
},
"dependencies": {
"@types/node": "^20.12.12",
"json-schema-to-ts": "^3.1.0",
"solid-js": "^1.8.11"
}
}

View File

@@ -1,60 +1,34 @@
import { Match, Switch, createSignal, type Component } from "solid-js";
import { CountProvider } from "./Config";
// import { Nested } from "./nested";
import { Layout } from "./layout/layout";
import cx from "classnames";
import { Nested } from "./nested";
type Route = "home" | "machines";
type Route = "home" | "graph";
const App: Component = () => {
const [route, setRoute] = createSignal<Route>("home");
return (
<CountProvider>
<Layout>
<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="w-full flex items-center flex-col gap-2 my-2">
<div>Clan</div>
<p>Current route: {route()}</p>
<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 class="flex items-center">
<button
onClick={() => setRoute((o) => (o === "graph" ? "home" : "graph"))}
class="btn btn-link"
>
Navigate to {route() === "home" ? "graph" : "home"}
</button>
</div>
</Layout>
<Switch fallback={<p>{route()} not found</p>}>
<Match when={route() == "home"}>
<Nested />
</Match>
<Match when={route() == "graph"}>
<p></p>
</Match>
</Switch>
</div>
</CountProvider>
);
};

View File

@@ -1,16 +1,8 @@
import {
createSignal,
createContext,
useContext,
JSXElement,
createEffect,
} from "solid-js";
import { OperationResponse, pyApi } from "./message";
import { createSignal, createContext, useContext, JSXElement } from "solid-js";
import { pyApi } from "./message";
export const makeCountContext = () => {
const [machines, setMachines] = createSignal<
OperationResponse<"list_machines">
>({});
const [machines, setMachines] = createSignal<string[]>([]);
const [loading, setLoading] = createSignal(false);
pyApi.list_machines.receive((machines) => {
@@ -18,17 +10,13 @@ export const makeCountContext = () => {
setMachines(machines);
});
createEffect(() => {
console.log("The count is now", machines());
});
return [
{ loading, machines },
{
getMachines: () => {
// When the gtk function sends its data the loading state will be set to false
setLoading(true);
pyApi.list_machines.dispatch({ debug: true, flake_url: "." });
pyApi.list_machines.dispatch(".");
},
},
] as const;
@@ -37,14 +25,8 @@ export const makeCountContext = () => {
type CountContextType = ReturnType<typeof makeCountContext>;
export const CountContext = createContext<CountContextType>([
{ loading: () => false, machines: () => [] },
{
loading: () => false,
// eslint-disable-next-line
machines: () => ({}),
},
{
// eslint-disable-next-line
getMachines: () => {},
},
]);

View File

@@ -6,6 +6,7 @@ import App from "./App";
const root = document.getElementById("app");
// @ts-ignore: add the clan scope to the window object so we can register callbacks for gtk
window.clan = window.clan || {};
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -14,5 +15,4 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
render(() => <App />, root!);

View File

@@ -1,9 +0,0 @@
import { Component, JSXElement } from "solid-js";
interface LayoutProps {
children: JSXElement;
}
export const Layout: Component<LayoutProps> = (props) => {
return <div class="grid grid-cols-8">{props.children}</div>;
};

View File

@@ -1,11 +1,11 @@
import { FromSchema } from "json-schema-to-ts";
import { schema } from "@/api";
export type API = FromSchema<typeof schema>;
type API = FromSchema<typeof schema>;
export type OperationNames = keyof API;
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
export type OperationResponse<T extends OperationNames> = API[T]["return"];
type OperationNames = keyof API;
type OperationArgs<T extends OperationNames> = API[T]["argument"];
type OperationResponse<T extends OperationNames> = API[T]["return"];
declare global {
interface Window {
@@ -15,10 +15,7 @@ declare global {
webkit: {
messageHandlers: {
gtk: {
postMessage: (message: {
method: OperationNames;
data: OperationArgs<OperationNames>;
}) => void;
postMessage: (message: { method: OperationNames; data: any }) => void;
};
};
};
@@ -34,7 +31,7 @@ function createFunctions<K extends OperationNames>(
return {
dispatch: (args: OperationArgs<K>) => {
console.log(
`Operation: ${String(operationName)}, Arguments: ${JSON.stringify(args)}`
`Operation: ${operationName}, Arguments: ${JSON.stringify(args)}`
);
// Send the data to the gtk app
window.webkit.messageHandlers.gtk.postMessage({
@@ -43,7 +40,7 @@ function createFunctions<K extends OperationNames>(
});
},
receive: (fn: (response: OperationResponse<K>) => void) => {
window.clan[operationName] = deserialize(fn);
window.clan.list_machines = deserialize(fn);
},
};
}
@@ -62,7 +59,6 @@ const deserialize =
<T>(fn: (response: T) => void) =>
(str: string) => {
try {
console.debug("Received data: ", str);
fn(JSON.parse(str) as T);
} catch (e) {
alert(`Error parsing JSON: ${e}`);
@@ -72,10 +68,7 @@ const deserialize =
// Create the API object
const pyApi: PyApi = {} as PyApi;
operationNames.forEach((opName) => {
const name = opName as OperationNames;
// @ts-expect-error - TODO: Fix this. Typescript is not recognizing the receive function correctly
operationNames.forEach((name) => {
pyApi[name] = createFunctions(name);
});
export { pyApi };

View File

@@ -1,34 +1,24 @@
import { For, Match, Switch, createEffect, type Component } from "solid-js";
import { For, Match, Switch, 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>
<hr />
<Switch>
<Match when={loading()}>Loading...</Match>
<Match when={!loading() && Object.entries(machines()).length === 0}>
<Match when={!loading() && machines().length === 0}>
No machines found
</Match>
<Match when={!loading()}>
<For each={list()}>
{(entry, i) => (
<Match when={!loading() && machines().length}>
<For each={machines()}>
{(machine, i) => (
<li>
{i() + 1}: {entry.machine_name}{" "}
{entry.machine_description || "No description"}
{i() + 1}: {machine}
</li>
)}
</For>

View File

@@ -0,0 +1,32 @@
{
dream2nix,
config,
src,
...
}:
{
imports = [ dream2nix.modules.dream2nix.WIP-nodejs-builder-v3 ];
mkDerivation = {
inherit src;
};
deps =
{ nixpkgs, ... }:
{
inherit (nixpkgs) stdenv;
};
WIP-nodejs-builder-v3 = {
packageLockFile = "${config.mkDerivation.src}/package-lock.json";
# config.groups.all.packages.${config.name}.${config.version}.
};
public.out = {
checkPhase = ''
echo "Running tests"
echo "Tests passed"
'';
};
name = "@clan/webview-ui";
version = "0.0.1";
}

View File

@@ -9,9 +9,10 @@
src = ./app;
# npmDepsHash = "sha256-bRD2vzijhdOOvcEi6XaG/neSqhkVQMqIX/8bxvRQkTc=";
npmDeps = pkgs.fetchNpmDeps {
src = ./app;
hash = "sha256-E0++hupVKnDqmLk7ljoMcqcI4w+DIMlfRYRPbKUsT2c=";
hash = "sha256-bRD2vzijhdOOvcEi6XaG/neSqhkVQMqIX/8bxvRQkTc=";
};
# The prepack script runs the build script, which we'd rather do in the build phase.
npmPackFlags = [ "--ignore-scripts" ];