Compare commits
93 Commits
hgl-site
...
remove-mod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84dab9e329 | ||
|
|
ef02fca062 | ||
|
|
5f90d60bd6 | ||
|
|
02ed516a15 | ||
|
|
14a6af1259 | ||
|
|
fd26dcd0d0 | ||
|
|
3b316cbf3e | ||
|
|
d9657d8617 | ||
|
|
74fb95653a | ||
|
|
6073ab4a0b | ||
|
|
9e35d040da | ||
|
|
0e2904b34b | ||
|
|
71ba979120 | ||
|
|
fb4b8f2745 | ||
|
|
0028311805 | ||
|
|
abacd19c12 | ||
|
|
9446009738 | ||
|
|
dc7566951c | ||
|
|
67d2e18fb8 | ||
|
|
350b55ea16 | ||
|
|
ce50278621 | ||
|
|
d9262e47cb | ||
|
|
ce411b4784 | ||
|
|
55f0da45a8 | ||
|
|
dc7b10efe2 | ||
|
|
3687fd48ce | ||
|
|
a4d26497f9 | ||
|
|
1c14033d48 | ||
|
|
13ca0f5050 | ||
|
|
85f219ca1e | ||
|
|
503bb62864 | ||
|
|
c0225ea757 | ||
|
|
d6b27d8740 | ||
|
|
c09cb834fa | ||
|
|
91434044e0 | ||
|
|
56a76242ca | ||
|
|
7255673440 | ||
|
|
5b651752ba | ||
|
|
b1aa79b33f | ||
|
|
26fdfeec00 | ||
|
|
a0c3b6d33e | ||
|
|
8be8af9117 | ||
|
|
338a6ad340 | ||
|
|
38fafba6d8 | ||
|
|
2dcc4bba15 | ||
|
|
b57897c6c9 | ||
|
|
b640be3dd2 | ||
|
|
7266e4b273 | ||
|
|
546eeb22d0 | ||
|
|
936290b01d | ||
|
|
13d69bcd66 | ||
|
|
e342996306 | ||
|
|
bfec09e652 | ||
|
|
42ac9f3579 | ||
|
|
8178c41c7b | ||
|
|
143fbb929f | ||
|
|
6347bb7f3a | ||
|
|
9a7288df3d | ||
|
|
ce0ff60ad3 | ||
|
|
d76dc55325 | ||
|
|
70c1648caf | ||
|
|
2ddba36b17 | ||
|
|
d4cb206e3e | ||
|
|
1befc86308 | ||
|
|
999ade9dc4 | ||
|
|
01ab9e5dbf | ||
|
|
2ef248a10e | ||
|
|
24493e8768 | ||
|
|
b91158f454 | ||
|
|
66a6758db4 | ||
|
|
61df393c2d | ||
|
|
13458e4f58 | ||
|
|
dc3a41f403 | ||
|
|
a424f318e4 | ||
|
|
36adc38ec4 | ||
|
|
29581cd1f4 | ||
|
|
4523596eba | ||
|
|
e97b06c410 | ||
|
|
f79b3c2761 | ||
|
|
a509e16627 | ||
|
|
e05f4380d4 | ||
|
|
e8b5e2c2c5 | ||
|
|
9630b6dbe4 | ||
|
|
1c2b72c6f0 | ||
|
|
c49a7c8277 | ||
|
|
939f724878 | ||
|
|
8a56776032 | ||
|
|
60f7f8598b | ||
|
|
36282b92bc | ||
|
|
0cf35480a2 | ||
|
|
e875df5665 | ||
|
|
92d5ea82a7 | ||
|
|
8f5bf1ff2a |
26
.gitea/workflows/update-flake-inputs.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Update Flake Inputs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 5 hours
|
||||
- cron: "0 */5 * * *"
|
||||
workflow_dispatch:
|
||||
repository_dispatch:
|
||||
|
||||
jobs:
|
||||
update-flake-inputs:
|
||||
runs-on: nix
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Update flake inputs
|
||||
uses: Mic92/update-flake-inputs-gitea@main
|
||||
with:
|
||||
# Exclude private flakes and update-clan-core checks flake
|
||||
|
||||
exclude-patterns: "devFlake/private/flake.nix,checks/impure/flake.nix"
|
||||
auto-merge: true
|
||||
gitea-token: ${{ secrets.CI_BOT_TOKEN }}
|
||||
github-token: ${{ secrets.CI_BOT_GITHUB_TOKEN }}
|
||||
10
.gitignore
vendored
@@ -43,3 +43,13 @@ pkgs/clan-app/ui/api/Inventory.ts
|
||||
pkgs/clan-app/ui/api/modules_schemas.json
|
||||
pkgs/clan-app/ui/api/schema.json
|
||||
pkgs/clan-app/ui/.fonts
|
||||
|
||||
# To avoid accidentally committing large files
|
||||
# Can be added with `git add -f` after reviewing the filesize
|
||||
# Large files should be avoided or stored externally i.e. a gitea release
|
||||
*.jpg
|
||||
*.png
|
||||
*.jpeg
|
||||
*.gif
|
||||
*.mp4
|
||||
*.mkv
|
||||
|
||||
@@ -9,15 +9,37 @@
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.allowedKeys = lib.mkOption {
|
||||
default = { };
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
description = "The allowed public keys for ssh access to the admin user";
|
||||
example = {
|
||||
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
|
||||
|
||||
options = {
|
||||
allowedKeys = lib.mkOption {
|
||||
default = { };
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
description = "The allowed public keys for ssh access to the admin user";
|
||||
example = {
|
||||
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
|
||||
};
|
||||
};
|
||||
|
||||
rsaHostKey.enable = lib.mkEnableOption "Generate RSA host key";
|
||||
|
||||
# TODO: allow per-server domains that we than collect in the inventory
|
||||
#certicficateDomains = lib.mkOption {
|
||||
# type = lib.types.listOf lib.types.str;
|
||||
# default = [ ];
|
||||
# example = [ "git.mydomain.com" ];
|
||||
# description = "List of domains to include in the certificate. This option will not prepend the machine name in front of each domain.";
|
||||
#};
|
||||
|
||||
certificateSearchDomains = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "mydomain.com" ];
|
||||
description = ''
|
||||
List of domains to include in the certificate.
|
||||
This option will prepend the machine name in front of each domain before adding it to the certificate.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
perInstance =
|
||||
@@ -27,10 +49,15 @@
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
../../clanModules/sshd
|
||||
../../clanModules/root-password
|
||||
# We don't have a good way to specify dependencies between
|
||||
# clanServices for now. When it get's implemtende, we should just
|
||||
# use the ssh and users modules here.
|
||||
./ssh.nix
|
||||
./root-password.nix
|
||||
];
|
||||
|
||||
_module.args = { inherit settings; };
|
||||
|
||||
users.users.root.openssh.authorizedKeys.keys = builtins.attrValues settings.allowedKeys;
|
||||
};
|
||||
};
|
||||
|
||||
39
clanServices/admin/root-password.nix
Normal file
@@ -0,0 +1,39 @@
|
||||
# We don't have a way of specifying dependencies between clanServices for now.
|
||||
# When it get's added this file should be removed and the users module used instead.
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
users.mutableUsers = false;
|
||||
users.users.root.hashedPasswordFile =
|
||||
config.clan.core.vars.generators.root-password.files.password-hash.path;
|
||||
|
||||
clan.core.vars.generators.root-password = {
|
||||
files.password-hash.neededFor = "users";
|
||||
|
||||
files.password.deploy = false;
|
||||
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.mkpasswd
|
||||
pkgs.xkcdpass
|
||||
];
|
||||
|
||||
prompts.password.type = "hidden";
|
||||
prompts.password.persist = true;
|
||||
prompts.password.description = "You can autogenerate a password, if you leave this prompt blank.";
|
||||
|
||||
script = ''
|
||||
prompt_value="$(cat "$prompts"/password)"
|
||||
if [[ -n "''${prompt_value-}" ]]; then
|
||||
echo "$prompt_value" | tr -d "\n" > "$out"/password
|
||||
else
|
||||
xkcdpass --numwords 5 --delimiter - --count 1 | tr -d "\n" > "$out"/password
|
||||
fi
|
||||
mkpasswd -s -m sha-512 < "$out"/password | tr -d "\n" > "$out"/password-hash
|
||||
'';
|
||||
};
|
||||
}
|
||||
115
clanServices/admin/ssh.nix
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
let
|
||||
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
|
||||
|
||||
domains = stringSet settings.certificateSearchDomains;
|
||||
|
||||
in
|
||||
{
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings.PasswordAuthentication = false;
|
||||
|
||||
settings.HostCertificate = lib.mkIf (
|
||||
settings.certificateSearchDomains != [ ]
|
||||
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
|
||||
|
||||
hostKeys =
|
||||
[
|
||||
{
|
||||
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
|
||||
type = "ed25519";
|
||||
}
|
||||
]
|
||||
++ lib.optional settings.rsaHostKey.enable {
|
||||
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
|
||||
type = "rsa";
|
||||
};
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh = {
|
||||
files."ssh.id_ed25519" = { };
|
||||
files."ssh.id_ed25519.pub".secret = false;
|
||||
migrateFact = "openssh";
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/ssh.id_ed25519
|
||||
'';
|
||||
};
|
||||
|
||||
programs.ssh.knownHosts.clan-sshd-self-ed25519 = {
|
||||
hostNames = [
|
||||
"localhost"
|
||||
config.networking.hostName
|
||||
] ++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
|
||||
publicKey = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519.pub".value;
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh-rsa = lib.mkIf settings.rsaHostKey.enable {
|
||||
files."ssh.id_rsa" = { };
|
||||
files."ssh.id_rsa.pub".secret = false;
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t rsa -b 4096 -N "" -C "" -f "$out"/ssh.id_rsa
|
||||
'';
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh-cert = lib.mkIf (settings.certificateSearchDomains != [ ]) {
|
||||
files."ssh.id_ed25519-cert.pub".secret = false;
|
||||
dependencies = [
|
||||
"openssh"
|
||||
"openssh-ca"
|
||||
];
|
||||
validation = {
|
||||
name = config.clan.core.settings.machine.name;
|
||||
domains = lib.genAttrs settings.certificateSearchDomains lib.id;
|
||||
};
|
||||
runtimeInputs = [
|
||||
pkgs.openssh
|
||||
pkgs.jq
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen \
|
||||
-s $in/openssh-ca/id_ed25519 \
|
||||
-I ${config.clan.core.settings.machine.name} \
|
||||
-h \
|
||||
-n ${lib.concatMapStringsSep "," (d: "${config.clan.core.settings.machine.name}.${d}") domains} \
|
||||
$in/openssh/ssh.id_ed25519.pub
|
||||
mv $in/openssh/ssh.id_ed25519-cert.pub "$out"/ssh.id_ed25519-cert.pub
|
||||
'';
|
||||
};
|
||||
|
||||
clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
|
||||
share = true;
|
||||
files.id_ed25519.deploy = false;
|
||||
files."id_ed25519.pub" = {
|
||||
deploy = false;
|
||||
secret = false;
|
||||
};
|
||||
runtimeInputs = [
|
||||
pkgs.openssh
|
||||
];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
|
||||
'';
|
||||
};
|
||||
|
||||
programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
|
||||
certAuthority = true;
|
||||
extraHostNames = builtins.map (domain: "*.${domain}") settings.certificateSearchDomains;
|
||||
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
|
||||
};
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
roles.default = {
|
||||
interface =
|
||||
{ config, lib, ... }:
|
||||
{ lib, ... }:
|
||||
{
|
||||
options = {
|
||||
user = lib.mkOption {
|
||||
@@ -37,23 +37,6 @@
|
||||
- `clan vars get <machine-name> <name-of-password-variable>`
|
||||
'';
|
||||
};
|
||||
regularUser = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = config.user != "root";
|
||||
defaultText = lib.literalExpression "config.user != \"root\"";
|
||||
example = false;
|
||||
description = ''
|
||||
Whether the user should be a regular user or a system user.
|
||||
|
||||
Regular users are normal users that can log in and have a home directory.
|
||||
|
||||
System users are used for system services and do not have a home directory.
|
||||
|
||||
!!! Warning
|
||||
`root` cannot be a regular user.
|
||||
You must set this to `false` for `root`
|
||||
'';
|
||||
};
|
||||
groups = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
@@ -91,7 +74,7 @@
|
||||
}:
|
||||
{
|
||||
users.users.${settings.user} = {
|
||||
isNormalUser = settings.regularUser;
|
||||
isNormalUser = if settings.user == "root" then false else true;
|
||||
extraGroups = settings.groups;
|
||||
|
||||
hashedPasswordFile =
|
||||
|
||||
@@ -55,9 +55,37 @@ If you're using VSCode, it has a handy feature that makes paths to source code f
|
||||
|
||||
## Finding Print Messages
|
||||
|
||||
To identify where a specific print message comes from, you can enable a helpful feature. Simply set the environment variable `export TRACE_PRINT=1`. When you run commands with `--debug` mode, each print message will include information about its source location.
|
||||
To trace the origin of print messages in `clan-cli`, you can enable special debugging features using environment variables:
|
||||
|
||||
- Set `TRACE_PRINT=1` to include the source location with each print message:
|
||||
```bash
|
||||
export TRACE_PRINT=1
|
||||
```
|
||||
When running commands with `--debug`, every print will show where it was triggered in the code.
|
||||
|
||||
- To see a deeper stack trace for each print, set `TRACE_DEPTH` to the desired number of stack frames (e.g., 3):
|
||||
```bash
|
||||
export TRACE_DEPTH=3
|
||||
```
|
||||
|
||||
### Additional Debug Logging
|
||||
|
||||
You can enable more detailed logging for specific components by setting these environment variables:
|
||||
|
||||
- `CLAN_DEBUG_NIX_SELECTORS=1` — verbose logs for flake.select operations
|
||||
- `CLAN_DEBUG_NIX_PREFETCH=1` — verbose logs for flake.prefetch operations
|
||||
- `CLAN_DEBUG_COMMANDS=1` — print the diffed environment of executed commands
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export CLAN_DEBUG_NIX_SELECTORS=1
|
||||
export CLAN_DEBUG_NIX_PREFETCH=1
|
||||
export CLAN_DEBUG_COMMANDS=1
|
||||
```
|
||||
|
||||
These options help you pinpoint the source and context of print messages and debug logs during development.
|
||||
|
||||
|
||||
If you need more details, you can expand the stack trace information that appears with each print by setting the environment variable `export TRACE_DEPTH=3`.
|
||||
|
||||
## Analyzing Performance
|
||||
|
||||
|
||||
@@ -181,6 +181,13 @@ You can have a look and customize it if needed.
|
||||
!!! tip
|
||||
For advanced partitioning, see [Disko templates](https://github.com/nix-community/disko-templates) or [Disko examples](https://github.com/nix-community/disko/tree/master/example).
|
||||
|
||||
!!! Danger
|
||||
Don't change the `disko.nix` after the machine is installed for the first time.
|
||||
|
||||
Changing disko configuration requires wiping and reinstalling the machine.
|
||||
|
||||
Unless you really know what you are doing.
|
||||
|
||||
## Deploy the machine
|
||||
|
||||
**Finally deployment time!** Use one of the following commands to build and deploy the image via SSH onto your machine.
|
||||
@@ -267,4 +274,3 @@ clan {
|
||||
```
|
||||
|
||||
This is useful for machines that are not always online or are not part of the regular update cycle.
|
||||
|
||||
|
||||
29
flake.lock
generated
@@ -8,19 +8,16 @@
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": [
|
||||
"systems"
|
||||
],
|
||||
"treefmt-nix": [
|
||||
"treefmt-nix"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1751846468,
|
||||
"narHash": "sha256-h0mpWZIOIAKj4fmLNyI2HDG+c0YOkbYmyJXSj/bQ9s0=",
|
||||
"rev": "a2166c13b0cb3febdaf36391cd2019aa2ccf4366",
|
||||
"lastModified": 1752589312,
|
||||
"narHash": "sha256-BafZOenlzMYdumG12AzgVLhEVu+GcEa8nYNDSIYe1U0=",
|
||||
"rev": "496bbf05a2aa7b061ef464254db5804d1c6f45b4",
|
||||
"type": "tarball",
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/a2166c13b0cb3febdaf36391cd2019aa2ccf4366.tar.gz"
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/496bbf05a2aa7b061ef464254db5804d1c6f45b4.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -34,11 +31,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752113600,
|
||||
"narHash": "sha256-7LYDxKxZgBQ8LZUuolAQ8UkIB+jb4A2UmiR+kzY9CLI=",
|
||||
"lastModified": 1752718651,
|
||||
"narHash": "sha256-PkaR0qmyP9q/MDN3uYa+RLeBA0PjvEQiM0rTDDBXkL8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "79264292b7e3482e5702932949de9cbb69fedf6d",
|
||||
"rev": "d5ad4485e6f2edcc06751df65c5e16572877db88",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -118,10 +115,10 @@
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 315532800,
|
||||
"narHash": "sha256-mUlYenGbsUFP0A3EhfKJXmUl5+MQGJLhoEop2t3g5p4=",
|
||||
"rev": "ceb24d94c6feaa4e8737a8e2bd3cf71c3a7eaaa0",
|
||||
"narHash": "sha256-lUi+sPH7Kuh9uP3PyfgbENcJGReUM8Ffk9GxGBFbSN8=",
|
||||
"rev": "be9e214982e20b8310878ac2baa063a961c1bdf6",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre826033.ceb24d94c6fe/nixexprs.tar.xz"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre827262.be9e214982e2/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -149,11 +146,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1751606940,
|
||||
"narHash": "sha256-KrDPXobG7DFKTOteqdSVeL1bMVitDcy7otpVZWDE6MA=",
|
||||
"lastModified": 1752544651,
|
||||
"narHash": "sha256-GllP7cmQu7zLZTs9z0J2gIL42IZHa9CBEXwBY9szT0U=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "3633fc4acf03f43b260244d94c71e9e14a2f6e0d",
|
||||
"rev": "2c8def626f54708a9c38a5861866660395bb3461",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
inputs = {
|
||||
flake-parts.follows = "flake-parts";
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
systems.follows = "systems";
|
||||
treefmt-nix.follows = "treefmt-nix";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -101,19 +101,15 @@ let
|
||||
system:
|
||||
lib.nameValuePair system (
|
||||
lib.mapAttrs (
|
||||
name: _:
|
||||
moduleSystemConstructor.${machineClasses.${name}} {
|
||||
_: machine:
|
||||
machine.extendModules {
|
||||
modules = [
|
||||
(config.outputs.moduleForMachine.${name} or { })
|
||||
(lib.modules.importApply ../machineModules/overridePkgs.nix {
|
||||
pkgs = pkgsFor.${system};
|
||||
})
|
||||
];
|
||||
specialArgs = {
|
||||
inherit clan-core;
|
||||
} // specialArgs;
|
||||
}
|
||||
) allMachines
|
||||
) configurations
|
||||
)
|
||||
) supportedSystems
|
||||
);
|
||||
|
||||
@@ -7,8 +7,29 @@
|
||||
}:
|
||||
rec {
|
||||
buildClan =
|
||||
# TODO: Once all templates and docs are migrated add: lib.warn "'buildClan' is deprecated. Use 'clan-core.lib.clan' instead"
|
||||
module: (clan module).config;
|
||||
module:
|
||||
lib.warn ''
|
||||
==================== DEPRECATION NOTICE ====================
|
||||
Please migrate
|
||||
from: 'clan = inputs.<clan-core>.lib.buildClan'
|
||||
to : 'clan = inputs.<clan-core>.lib.clan'
|
||||
in your flake.nix.
|
||||
|
||||
Please also migrate
|
||||
from: 'inherit (clan) nixosConfigurations clanInternals; '
|
||||
to : "
|
||||
inherit (clan.config) nixosConfigurations clanInternals;
|
||||
clan = clan.config;
|
||||
"
|
||||
in your flake.nix.
|
||||
|
||||
Reason:
|
||||
- Improves consistency between flake-parts and non-flake-parts users.
|
||||
|
||||
- It also allows us to use the top level attribute 'clan' to expose
|
||||
attributes that can be used for cross-clan functionality.
|
||||
============================================================
|
||||
'' (clan module).config;
|
||||
|
||||
clan =
|
||||
{
|
||||
|
||||
@@ -48,6 +48,7 @@ in
|
||||
{
|
||||
options = {
|
||||
instances = lib.mkOption {
|
||||
default = { };
|
||||
# instances.<instanceName>...
|
||||
type = types.attrsOf (submoduleWith {
|
||||
modules = [
|
||||
@@ -57,6 +58,7 @@ in
|
||||
};
|
||||
# instances.<machineName>...
|
||||
machines = lib.mkOption {
|
||||
default = { };
|
||||
type = types.attrsOf (submoduleWith {
|
||||
modules = [
|
||||
config.exportsModule
|
||||
|
||||
@@ -26,6 +26,7 @@ in
|
||||
++ lib.optionals (_class == "nixos") [
|
||||
./secret/password-store.nix
|
||||
];
|
||||
|
||||
options.clan.core.vars = lib.mkOption {
|
||||
description = ''
|
||||
Generated Variables
|
||||
@@ -36,7 +37,14 @@ in
|
||||
- generate secrets like private keys automatically when they are needed
|
||||
- output multiple values like private and public keys simultaneously
|
||||
'';
|
||||
type = submodule { imports = [ ./interface.nix ]; };
|
||||
type = submodule {
|
||||
imports = [
|
||||
./interface.nix
|
||||
{
|
||||
settings.dependenciesType = lib.types.listOf lib.types.str;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
|
||||
@@ -10,6 +10,9 @@ let
|
||||
hashString
|
||||
toJSON
|
||||
;
|
||||
inherit (lib)
|
||||
mkOption
|
||||
;
|
||||
inherit (lib.types)
|
||||
attrsOf
|
||||
bool
|
||||
@@ -22,20 +25,29 @@ let
|
||||
package
|
||||
path
|
||||
str
|
||||
strMatching
|
||||
submoduleWith
|
||||
;
|
||||
# the original types.submodule has strange behavior
|
||||
submodule =
|
||||
module:
|
||||
submoduleWith {
|
||||
specialArgs.pkgs = pkgs;
|
||||
modules = [ module ];
|
||||
};
|
||||
|
||||
submoduleWithPkgs =
|
||||
module:
|
||||
submoduleWith {
|
||||
modules = [
|
||||
module
|
||||
{ config._module.args.pkgs = pkgs; }
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
options = {
|
||||
settings = import ./settings-opts.nix { inherit lib; };
|
||||
generators = lib.mkOption {
|
||||
generators = mkOption {
|
||||
description = ''
|
||||
A set of generators that can be used to generate files.
|
||||
Generators are scripts that produce files based on the values of other generators and user input.
|
||||
@@ -43,11 +55,11 @@ in
|
||||
'';
|
||||
default = { };
|
||||
type = attrsOf (
|
||||
submodule (generator: {
|
||||
submoduleWithPkgs (generator: {
|
||||
imports = [ ./generator.nix ];
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
name = mkOption {
|
||||
type = str;
|
||||
description = ''
|
||||
The name of the generator.
|
||||
This name will be used to refer to the generator in other generators.
|
||||
@@ -56,8 +68,7 @@ in
|
||||
default = generator.config._module.args.name;
|
||||
defaultText = "Name of the generator";
|
||||
};
|
||||
|
||||
dependencies = lib.mkOption {
|
||||
dependencies = mkOption {
|
||||
description = ''
|
||||
A list of other generators that this generator depends on.
|
||||
The output values of these generators will be available to the generator script as files.
|
||||
@@ -66,10 +77,10 @@ in
|
||||
|
||||
**A file `file1` of a generator named `dep1` will be available via `$in/dep1/file1`**
|
||||
'';
|
||||
type = listOf str;
|
||||
type = config.settings.dependenciesType;
|
||||
default = [ ];
|
||||
};
|
||||
migrateFact = lib.mkOption {
|
||||
migrateFact = mkOption {
|
||||
description = ''
|
||||
The fact service name to import the files from.
|
||||
|
||||
@@ -79,7 +90,7 @@ in
|
||||
example = "my_service";
|
||||
default = null;
|
||||
};
|
||||
validation = lib.mkOption {
|
||||
validation = mkOption {
|
||||
description = ''
|
||||
A set of values that invalidate the generated values.
|
||||
If any of these values change, the generated values will be re-generated.
|
||||
@@ -106,7 +117,7 @@ in
|
||||
]);
|
||||
};
|
||||
# the validationHash is the validation interface to the outside world
|
||||
validationHash = lib.mkOption {
|
||||
validationHash = mkOption {
|
||||
internal = true;
|
||||
description = ''
|
||||
A hash of the invalidation data.
|
||||
@@ -122,7 +133,7 @@ in
|
||||
hashString "sha256" (toJSON generator.config.validation);
|
||||
defaultText = "Hash of the invalidation data";
|
||||
};
|
||||
files = lib.mkOption {
|
||||
files = mkOption {
|
||||
description = ''
|
||||
A set of files to generate.
|
||||
The generator 'script' is expected to produce exactly these files under $out.
|
||||
@@ -152,8 +163,8 @@ in
|
||||
];
|
||||
options =
|
||||
{
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
name = mkOption {
|
||||
type = str;
|
||||
description = ''
|
||||
name of the public fact
|
||||
'';
|
||||
@@ -161,8 +172,8 @@ in
|
||||
default = file.config._module.args.name;
|
||||
defaultText = "Name of the file";
|
||||
};
|
||||
generatorName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
generatorName = mkOption {
|
||||
type = str;
|
||||
description = ''
|
||||
name of the generator
|
||||
'';
|
||||
@@ -170,8 +181,8 @@ in
|
||||
default = generator.config._module.args.name;
|
||||
defaultText = "Name of the generator that generates this file";
|
||||
};
|
||||
share = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
share = mkOption {
|
||||
type = bool;
|
||||
description = ''
|
||||
Whether the generated vars should be shared between machines.
|
||||
Shared vars are only generated once, when the first machine using it is deployed.
|
||||
@@ -182,7 +193,7 @@ in
|
||||
default = generator.config.share;
|
||||
defaultText = "Mirror of the share flag of the generator";
|
||||
};
|
||||
deploy = lib.mkOption {
|
||||
deploy = mkOption {
|
||||
description = ''
|
||||
Whether the file should be deployed to the target machine.
|
||||
|
||||
@@ -191,14 +202,14 @@ in
|
||||
type = bool;
|
||||
default = true;
|
||||
};
|
||||
secret = lib.mkOption {
|
||||
secret = mkOption {
|
||||
description = ''
|
||||
Whether the file should be treated as a secret.
|
||||
'';
|
||||
type = bool;
|
||||
default = true;
|
||||
};
|
||||
flakePath = lib.mkOption {
|
||||
flakePath = mkOption {
|
||||
description = ''
|
||||
The path to the file containing the content of the generated value.
|
||||
This will be set automatically
|
||||
@@ -206,7 +217,7 @@ in
|
||||
type = nullOr path;
|
||||
default = null;
|
||||
};
|
||||
path = lib.mkOption {
|
||||
path = mkOption {
|
||||
description = ''
|
||||
The path to the file containing the content of the generated value.
|
||||
This will be set automatically
|
||||
@@ -223,7 +234,7 @@ in
|
||||
path = file.config.flakePath;
|
||||
};
|
||||
};
|
||||
neededFor = lib.mkOption {
|
||||
neededFor = mkOption {
|
||||
description = ''
|
||||
This option determines when the secret will be decrypted and deployed to the target machine.
|
||||
|
||||
@@ -233,7 +244,7 @@ in
|
||||
By setting this to `user`, the secret will be deployed prior to users and groups are created, allowing
|
||||
users' passwords to be managed by vars. The secret will be stored in `/run/secrets-for-users` and `owner` and `group` must be `root`.
|
||||
'';
|
||||
type = lib.types.enum [
|
||||
type = enum [
|
||||
"partitioning"
|
||||
"activation"
|
||||
"users"
|
||||
@@ -241,22 +252,22 @@ in
|
||||
];
|
||||
default = "services";
|
||||
};
|
||||
owner = lib.mkOption {
|
||||
owner = mkOption {
|
||||
description = "The user name or id that will own the file.";
|
||||
default = "root";
|
||||
};
|
||||
group = lib.mkOption {
|
||||
group = mkOption {
|
||||
description = "The group name or id that will own the file.";
|
||||
default = if _class == "darwin" then "wheel" else "root";
|
||||
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.strMatching "^[0-7]{4}$";
|
||||
mode = mkOption {
|
||||
type = strMatching "^[0-7]{4}$";
|
||||
description = "The unix file mode of the file. Must be a 4-digit octal number.";
|
||||
default = "0400";
|
||||
};
|
||||
value =
|
||||
lib.mkOption {
|
||||
mkOption {
|
||||
description = ''
|
||||
The content of the generated value.
|
||||
Only available if the file is not secret.
|
||||
@@ -269,7 +280,7 @@ in
|
||||
};
|
||||
}
|
||||
// (lib.optionalAttrs (_class == "nixos") {
|
||||
restartUnits = lib.mkOption {
|
||||
restartUnits = mkOption {
|
||||
description = ''
|
||||
A list of systemd units that should be restarted after the file is deployed.
|
||||
This is useful for services that need to reload their configuration after the file is updated.
|
||||
@@ -283,7 +294,7 @@ in
|
||||
})
|
||||
);
|
||||
};
|
||||
prompts = lib.mkOption {
|
||||
prompts = mkOption {
|
||||
description = ''
|
||||
A set of prompts to ask the user for values.
|
||||
Prompts are available to the generator script as files.
|
||||
@@ -293,7 +304,7 @@ in
|
||||
type = attrsOf (
|
||||
submodule (prompt: {
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
name = mkOption {
|
||||
description = ''
|
||||
The name of the prompt.
|
||||
This name will be used to refer to the prompt in the generator script.
|
||||
@@ -302,7 +313,7 @@ in
|
||||
default = prompt.config._module.args.name;
|
||||
defaultText = "Name of the prompt";
|
||||
};
|
||||
persist = lib.mkOption {
|
||||
persist = mkOption {
|
||||
description = ''
|
||||
Whether the prompted value should be stored in a file with the same name as the prompt.
|
||||
|
||||
@@ -317,7 +328,7 @@ in
|
||||
type = bool;
|
||||
default = false;
|
||||
};
|
||||
description = lib.mkOption {
|
||||
description = mkOption {
|
||||
description = ''
|
||||
The description of the prompted value
|
||||
'';
|
||||
@@ -326,7 +337,7 @@ in
|
||||
default = prompt.config._module.args.name;
|
||||
defaultText = "Name of the prompt";
|
||||
};
|
||||
type = lib.mkOption {
|
||||
type = mkOption {
|
||||
description = ''
|
||||
The input type of the prompt.
|
||||
The following types are available:
|
||||
@@ -347,7 +358,7 @@ in
|
||||
})
|
||||
);
|
||||
};
|
||||
runtimeInputs = lib.mkOption {
|
||||
runtimeInputs = mkOption {
|
||||
description = ''
|
||||
A list of packages that the generator script requires.
|
||||
These packages will be available in the PATH when the script is run.
|
||||
@@ -355,7 +366,7 @@ in
|
||||
type = listOf package;
|
||||
default = [ ];
|
||||
};
|
||||
script = lib.mkOption {
|
||||
script = mkOption {
|
||||
description = ''
|
||||
The script to run to generate the files.
|
||||
The script will be run with the following environment variables:
|
||||
@@ -369,17 +380,17 @@ in
|
||||
type = either str path;
|
||||
default = "";
|
||||
};
|
||||
finalScript = lib.mkOption {
|
||||
finalScript = mkOption {
|
||||
description = ''
|
||||
The final generator script, wrapped, so:
|
||||
- all required programs are in PATH
|
||||
- sandbox is set up correctly
|
||||
'';
|
||||
type = lib.types.path;
|
||||
type = path;
|
||||
readOnly = true;
|
||||
internal = true;
|
||||
};
|
||||
share = lib.mkOption {
|
||||
share = mkOption {
|
||||
description = ''
|
||||
Whether the generated vars should be shared between machines.
|
||||
Shared vars are only generated once, when the first machine using it is deployed.
|
||||
|
||||
@@ -65,4 +65,14 @@
|
||||
Set it to pkgs.pass for GPG or pkgs.passage for age encryption.
|
||||
'';
|
||||
};
|
||||
|
||||
dependenciesType = lib.mkOption {
|
||||
type = lib.types.raw;
|
||||
description = ''
|
||||
The type of the `dependencies` option.
|
||||
'';
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
visible = false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ buildNpmPackage (_finalAttrs: {
|
||||
mkdir -p api
|
||||
cp -r ${clan-ts-api}/* api
|
||||
cp -r ${fonts} ".fonts"
|
||||
|
||||
# only needed for the next couple weeks to make sure this file doesn't make it back into the git history
|
||||
if [[ -f "${./ui}/src/routes/Onboarding/background.jpg" ]]; then
|
||||
echo "background.jpg found, exiting"
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
|
||||
# todo figure out why this fails only inside of Nix
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { StorybookConfig } from "@kachurun/storybook-solid-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
framework: "@kachurun/storybook-solid-vite",
|
||||
stories: ["../src/components/**/*.mdx", "../src/components/**/*.stories.tsx"],
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-docs",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="223" height="89" fill="currentColor">
|
||||
<g clip-path="url(#a)">
|
||||
<path d="M55.503 18.696h10.104a1.946 1.946 0 0 0 1.943-1.948v-7.79c0-1.075-.87-1.947-1.943-1.947h-3.186a1.863 1.863 0 0 1-1.866-1.87V1.947C60.555.872 59.685 0 58.612 0h-27.98a1.946 1.946 0 0 0-1.944 1.947v3.194c0 1.036-.832 1.87-1.865 1.87h-3.187a1.946 1.946 0 0 0-1.943 1.947v3.194c0 1.036-.832 1.87-1.866 1.87h-3.186a1.946 1.946 0 0 0-1.943 1.947s-.467 1.153-.467 23.253c0 19.763.467 21.913.467 21.913 0 1.075.87 1.948 1.943 1.948h3.186c1.034 0 1.866.833 1.866 1.87v3.271c0 1.036.831 1.87 1.865 1.87h3.265c1.033 0 1.865.833 1.865 1.87v3.193c0 1.075.87 1.948 1.943 1.948h27.981a1.946 1.946 0 0 0 1.943-1.948v-3.194c0-1.036.832-1.87 1.866-1.87h5.145a1.946 1.946 0 0 0 1.943-1.947v-9.285c0-1.075-.87-1.948-1.943-1.948H55.503a1.946 1.946 0 0 0-1.943 1.948v4.69c0 1.035-.832 1.869-1.866 1.869H37.55a1.863 1.863 0 0 1-1.866-1.87v-4.752c0-1.075-.87-1.947-1.943-1.947H29c-1.034 0-1.609.148-1.865-1.87-.078-.646-.125-1.44-.18-2.508-.147-2.68-.287-5.5-.287-13.539 0-11.24.288-16.81.466-18.369.18-1.558.832-1.87 1.866-1.87h4.741a1.946 1.946 0 0 0 1.943-1.947v-3.193c0-1.037.832-1.87 1.866-1.87h14.145c1.034 0 1.866.833 1.866 1.87v3.193c0 1.075.87 1.948 1.943 1.948M20.247 74.822h-2.293a.814.814 0 0 1-.808-.81v-2.298c0-.896-.723-1.62-1.617-1.62H9.327c-.894 0-1.617.724-1.617 1.62v2.298c0 .444-.365.81-.808.81H4.609c-.894 0-1.617.725-1.617 1.62v6.217c0 .896.723 1.62 1.617 1.62h2.293c.443 0 .808.366.808.81v2.299c0 .895.723 1.62 1.617 1.62h6.202c.894 0 1.617-.725 1.617-1.62v-2.299c0-.444.365-.81.808-.81h2.293c.894 0 1.617-.724 1.617-1.62v-6.216c0-.896-.723-1.62-1.617-1.62M221.135 35.04h-1.71a1.863 1.863 0 0 1-1.866-1.87v-3.272c0-1.036-.831-1.87-1.865-1.87h-3.265a1.863 1.863 0 0 1-1.865-1.87v-3.271c0-1.036-.832-1.87-1.865-1.87h-20.971a1.863 1.863 0 0 0-1.865 1.87v3.965c0 .514-.42.935-.933.935h-3.559c-.513 0-.84-.32-.933-.935l-.622-3.918c-.148-1.099-.676-1.777-1.788-1.777l-3.653-.14h-2.052a3.736 3.736 0 0 0-3.73 3.74V61.68a3.736 3.736 0 0 1-3.731 3.739h-8.394a1.863 1.863 0 0 1-1.866-1.87V36.714c0-11.825-7.461-18.813-22.556-18.813-13.718 0-20.325 5.04-21.203 14.443-.109 1.153.552 1.815 1.702 1.815l7.757.569c1.143.1 1.594-.554 1.811-1.652.77-3.74 4.174-5.827 9.933-5.827 7.081 0 10.042 3.358 10.042 9.076v3.014c0 1.036-.831 1.87-1.865 1.87l-.342-.024h-9.715c-15.421 0-22.984 5.983-22.984 17.956 0 3.802.778 7.058 2.254 9.738h-.59c-1.765-1.27-2.457-2.236-3.055-2.93-.256-.295-.653-.537-1.345-.537h-1.717l-5.993.008h-3.264a3.736 3.736 0 0 1-3.731-3.74V1.769C89.74.654 89.072 0 87.969 0H79.55c-1.034 0-1.865.732-1.865 1.768l-.024 54.304v13.554c0 4.13 3.343 7.479 7.462 7.479h50.84c8.448-.429 8.604-3.42 9.436-4.542.645 3.56 1.865 4.347 4.71 4.518 8.137.117 18.343.032 18.49.024h4.975c4.119 0 6.684-3.35 6.684-7.479l.777-27.264c0-1.036.832-1.87 1.866-1.87h2.021a1.56 1.56 0 0 0 1.554-1.558v-3.583c0-1.036.832-1.87 1.866-1.87h11.868a3.37 3.37 0 0 1 3.366 3.373v3.249c0 1.075.87 1.947 1.943 1.947h4.119c.513 0 .933.42.933.935v32.25c0 1.036.831 1.87 1.865 1.87h6.84a3.736 3.736 0 0 0 3.731-3.74V36.91c0-1.036-.832-1.87-1.866-1.87zM142.64 54.225c0 8.927-6.132 14.715-15.335 14.715-6.606 0-9.793-2.953-9.793-8.748 0-6.442 3.832-9.636 11.62-9.636h13.508v3.669"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path d="M0 0h223v89H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -22,4 +22,4 @@
|
||||
<rect x="24" y="24" width="6" height="6"/>
|
||||
<rect x="29" y="24" width="6" height="6"/>
|
||||
<rect x="6" y="30" width="6" height="6"/>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
5
pkgs/clan-app/ui/logos/clan.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 26" fill="none">
|
||||
<path d="M15.2735 5.89115H18.2129C18.5249 5.89115 18.7781 5.63627 18.7781 5.32222V3.04651C18.7781 2.73246 18.5249 2.47758 18.2129 2.47758H17.2858C16.9851 2.47758 16.7432 2.23408 16.7432 1.93141V0.998371C16.7432 0.684323 16.4899 0.429443 16.1779 0.429443H8.03798C7.72595 0.429443 7.47271 0.684323 7.47271 0.998371V1.93141C7.47271 2.23408 7.23077 2.47758 6.93005 2.47758H6.003C5.69097 2.47758 5.43773 2.73246 5.43773 3.04651V3.97955C5.43773 4.28222 5.1958 4.52572 4.89507 4.52572H3.96803C3.656 4.52572 3.40276 4.7806 3.40276 5.09465C3.40276 5.09465 3.26709 5.43146 3.26709 11.4582C3.26709 17.2317 3.40276 17.8598 3.40276 17.8598C3.40276 18.1739 3.656 18.4287 3.96803 18.4287H4.89507C5.1958 18.4287 5.43773 18.6722 5.43773 18.9749V19.9307C5.43773 20.2334 5.67967 20.4769 5.98039 20.4769H6.93005C7.23077 20.4769 7.47271 20.7204 7.47271 21.023V21.9561C7.47271 22.2701 7.72595 22.525 8.03798 22.525H16.1779C16.4899 22.525 16.7432 22.2701 16.7432 21.9561V21.023C16.7432 20.7204 16.9851 20.4769 17.2858 20.4769H18.7827C19.0947 20.4769 19.3479 20.222 19.3479 19.9079V17.1953C19.3479 16.8812 19.0947 16.6264 18.7827 16.6264H15.2735C14.9614 16.6264 14.7082 16.8812 14.7082 17.1953V18.5653C14.7082 18.8679 14.4662 19.1114 14.1655 19.1114H10.0503C9.74962 19.1114 9.50768 18.8679 9.50768 18.5653V17.1771C9.50768 16.863 9.25444 16.6082 8.94241 16.6082H7.56315C7.26243 16.6082 7.09511 16.6514 7.02049 16.062C6.99788 15.8731 6.98431 15.641 6.96849 15.3292C6.92552 14.5464 6.88483 13.7226 6.88483 11.374C6.88483 8.5196 6.96849 6.89246 7.02049 6.43732C7.0725 5.98218 7.26243 5.89115 7.56315 5.89115H8.94241C9.25444 5.89115 9.50768 5.63627 9.50768 5.32222V4.38918C9.50768 4.08651 9.74962 3.84301 10.0503 3.84301H14.1655C14.4662 3.84301 14.7082 4.08651 14.7082 4.38918V5.32222C14.7082 5.63627 14.9614 5.89115 15.2735 5.89115Z" fill="currentColor"/>
|
||||
<path d="M5.01717 21.8582H4.35015C4.22126 21.8582 4.11499 21.7513 4.11499 21.6216V20.9502C4.11499 20.6885 3.90471 20.4769 3.64469 20.4769H1.84034C1.58032 20.4769 1.37004 20.6885 1.37004 20.9502V21.6216C1.37004 21.7513 1.26377 21.8582 1.13488 21.8582H0.467864C0.207839 21.8582 -0.00244141 22.0699 -0.00244141 22.3316V24.1476C-0.00244141 24.4093 0.207839 24.6209 0.467864 24.6209H1.13488C1.26377 24.6209 1.37004 24.7279 1.37004 24.8576V25.5289C1.37004 25.7907 1.58032 26.0023 1.84034 26.0023H3.64469C3.90471 26.0023 4.11499 25.7907 4.11499 25.5289V24.8576C4.11499 24.7279 4.22126 24.6209 4.35015 24.6209H5.01717C5.27719 24.6209 5.48747 24.4093 5.48747 24.1476V22.3316C5.48747 22.0699 5.27719 21.8582 5.01717 21.8582Z" fill="currentColor"/>
|
||||
<path d="M63.4576 10.2361H62.9601C62.6594 10.2361 62.4175 9.99265 62.4175 9.68998V8.73418C62.4175 8.43151 62.1755 8.18801 61.8748 8.18801H60.9252C60.6244 8.18801 60.3825 7.94451 60.3825 7.64184V6.68604C60.3825 6.38337 60.1406 6.13987 59.8398 6.13987H53.7394C53.4387 6.13987 53.1968 6.38337 53.1968 6.68604V7.84438C53.1968 7.99457 53.0747 8.11746 52.9254 8.11746H51.8899C51.7406 8.11746 51.6457 8.02416 51.6185 7.84438L51.4376 6.69969C51.3947 6.37882 51.2409 6.18083 50.9176 6.18083L49.8549 6.13987H49.258C48.6588 6.13987 48.1726 6.62915 48.1726 7.23221V18.0191C48.1726 18.6221 47.6865 19.1114 47.0873 19.1114H44.6453C44.3446 19.1114 44.1027 18.8679 44.1027 18.5653V10.7254C44.1027 7.2709 41.932 5.22958 37.541 5.22958C33.5502 5.22958 31.6283 6.70197 31.3728 9.44875C31.3411 9.78556 31.5333 9.97899 31.868 9.97899L34.1245 10.1451C34.4569 10.1747 34.588 9.98354 34.6514 9.66267C34.8752 8.57033 35.8656 7.96044 37.541 7.96044C39.6009 7.96044 40.4623 8.94127 40.4623 10.6116V11.4923C40.4623 11.795 40.2204 12.0385 39.9197 12.0385L39.8202 12.0317H36.9938C32.5078 12.0317 30.3078 13.7794 30.3078 17.2772C30.3078 18.3877 30.5339 19.339 30.9635 20.1218H30.7917C30.2784 19.7509 30.0772 19.4687 29.9031 19.2662C29.8285 19.1797 29.7131 19.1091 29.5119 19.1091H29.0122L27.2689 19.1114H26.3193C25.7201 19.1114 25.2339 18.6221 25.2339 18.0191V0.516586C25.2339 0.19116 25.0395 0 24.7184 0H22.2697C21.9689 0 21.727 0.213917 21.727 0.516586L21.7202 16.3806V20.3403C21.7202 21.5464 22.6925 22.525 23.8909 22.525H38.6806C41.1384 22.3998 41.1836 21.5259 41.4256 21.1982C41.6132 22.2382 41.9682 22.4681 42.7958 22.5182C45.1631 22.5523 48.1319 22.5273 48.1749 22.525H49.622C50.8204 22.525 51.5665 21.5464 51.5665 20.3403L51.7926 12.3753C51.7926 12.0726 52.0346 11.8291 52.3353 11.8291H52.9232C53.1719 11.8291 53.3754 11.6243 53.3754 11.374V10.3272C53.3754 10.0245 53.6173 9.781 53.9181 9.781H57.3707C57.9111 9.781 58.3498 10.2225 58.3498 10.7664V11.7154C58.3498 12.0294 58.603 12.2843 58.915 12.2843H60.1134C60.2626 12.2843 60.3848 12.4072 60.3848 12.5574V21.9788C60.3848 22.2815 60.6267 22.525 60.9274 22.525H62.9172C63.5164 22.525 64.0025 22.0357 64.0025 21.4326V10.7823C64.0025 10.4796 63.7606 10.2361 63.4598 10.2361H63.4576ZM40.6229 15.8412C40.6229 18.4492 38.8389 20.14 36.1618 20.14C34.2398 20.14 33.3128 19.2775 33.3128 17.5844C33.3128 15.7024 34.4275 14.7694 36.6931 14.7694H40.6229V15.8389V15.8412Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
43
pkgs/clan-app/ui/logos/darknet.svg
Normal file
@@ -0,0 +1,43 @@
|
||||
<svg viewBox="0 0 200 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M191.879 19.297C191.736 19.297 191.602 19.2434 191.477 19.1362C191.37 19.029 191.316 18.9039 191.316 18.761V6.16432C191.316 6.02138 191.37 5.8963 191.477 5.7891C191.602 5.68189 191.736 5.62829 191.879 5.62829H193.246C193.568 5.62829 193.764 5.80697 193.836 6.16432C193.907 6.48594 193.979 6.72715 194.05 6.88796C194.139 7.0309 194.273 7.10237 194.452 7.10237C194.559 7.10237 194.676 7.06663 194.801 6.99516C194.926 6.90582 195.051 6.80755 195.176 6.70035C195.569 6.36086 195.971 6.10178 196.382 5.92311C196.793 5.72656 197.365 5.62829 198.097 5.62829H199.464C199.607 5.62829 199.732 5.68189 199.839 5.7891C199.946 5.8963 200 6.02138 200 6.16432V8.28163C200 8.42457 199.946 8.54964 199.839 8.65685C199.732 8.76405 199.607 8.81766 199.464 8.81766H197.936C196.9 8.81766 196.042 9.12141 195.363 9.7289C194.702 10.3364 194.372 11.2745 194.372 12.543V18.761C194.372 18.9039 194.318 19.029 194.211 19.1362C194.104 19.2434 193.979 19.297 193.836 19.297H191.879Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M182.709 19.7526C180.583 19.7526 178.93 19.0915 177.751 17.7693C176.589 16.4471 176.009 14.6782 176.009 12.4626C176.009 11.0154 176.268 9.74677 176.786 8.65685C177.304 7.54906 178.064 6.69141 179.064 6.08391C180.065 5.47642 181.253 5.17267 182.629 5.17267C184.648 5.17267 186.238 5.7623 187.399 6.94156C188.579 8.12082 189.168 9.74677 189.168 11.8194V12.7843C189.168 13.0165 189.106 13.1952 188.981 13.3203C188.856 13.4454 188.686 13.5079 188.471 13.5079H179.573C179.395 13.5079 179.305 13.6062 179.305 13.8027C179.341 14.839 179.681 15.6609 180.324 16.2684C180.967 16.8581 181.762 17.1529 182.709 17.1529C184.031 17.1529 184.978 16.6526 185.55 15.652C185.747 15.3304 185.97 15.1696 186.22 15.1696H188.284C188.445 15.1696 188.579 15.2321 188.686 15.3572C188.793 15.4644 188.82 15.6163 188.766 15.8128C188.427 16.9921 187.73 17.948 186.676 18.6806C185.622 19.3953 184.299 19.7526 182.709 19.7526ZM179.279 10.801C179.279 10.9796 179.377 11.069 179.573 11.069H185.791C185.952 11.069 186.032 10.9796 186.032 10.801C186.032 9.92545 185.72 9.19288 185.094 8.60325C184.487 7.99575 183.665 7.692 182.629 7.692C181.664 7.692 180.869 7.99575 180.243 8.60325C179.636 9.19288 179.314 9.92545 179.279 10.801Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M166.551 19.7526C165.621 19.7526 164.773 19.6008 164.004 19.297C163.236 18.9754 162.584 18.5108 162.048 17.9033C161.44 17.2601 160.976 16.4739 160.654 15.5448C160.333 14.6157 160.172 13.5883 160.172 12.4627C160.172 11.337 160.333 10.3096 160.654 9.38049C160.976 8.45137 161.44 7.6652 162.048 7.02197C162.584 6.43234 163.236 5.97671 164.004 5.6551C164.773 5.33348 165.621 5.17267 166.551 5.17267C167.247 5.17267 167.801 5.24414 168.212 5.38708C168.641 5.51215 169.079 5.69083 169.526 5.92311C169.561 5.94098 169.66 5.99458 169.82 6.08392C169.981 6.17326 170.133 6.21792 170.276 6.21792C170.598 6.21792 170.758 6.03032 170.758 5.6551V0.536027C170.758 0.393087 170.812 0.268014 170.919 0.160808C171.026 0.0536026 171.16 0 171.321 0H173.305C173.447 0 173.573 0.0536026 173.68 0.160808C173.787 0.268014 173.841 0.393087 173.841 0.536027V18.761C173.841 18.9039 173.787 19.029 173.68 19.1362C173.573 19.2434 173.447 19.297 173.305 19.297H171.911C171.661 19.297 171.446 19.1541 171.268 18.8682C171.16 18.7252 171.053 18.618 170.946 18.5466C170.857 18.4751 170.732 18.4394 170.571 18.4394C170.356 18.4394 170.062 18.5644 169.686 18.8146C169.24 19.1005 168.793 19.3238 168.346 19.4846C167.9 19.6633 167.301 19.7526 166.551 19.7526ZM163.308 12.4627C163.308 13.6955 163.62 14.6961 164.246 15.4644C164.567 15.8575 164.969 16.1702 165.452 16.4025C165.934 16.6169 166.488 16.7241 167.113 16.7241C168.328 16.7241 169.284 16.3042 169.981 15.4644C170.589 14.6961 170.892 13.6955 170.892 12.4627C170.892 11.2298 170.589 10.2381 169.981 9.4877C169.302 8.63005 168.346 8.20123 167.113 8.20123C166.488 8.20123 165.934 8.31737 165.452 8.54965C164.969 8.76406 164.567 9.07674 164.246 9.4877C163.62 10.2203 163.308 11.2119 163.308 12.4627Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M155.415 19.297C155.272 19.297 155.147 19.2434 155.04 19.1362C154.933 19.029 154.879 18.9039 154.879 18.761V0.536027C154.879 0.393087 154.933 0.268014 155.04 0.160808C155.147 0.0536026 155.272 0 155.415 0H157.399C157.542 0 157.667 0.0536026 157.774 0.160808C157.881 0.268014 157.935 0.393087 157.935 0.536027V18.761C157.935 18.9039 157.881 19.029 157.774 19.1362C157.667 19.2434 157.542 19.297 157.399 19.297H155.415Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M149.107 19.297C148.946 19.297 148.812 19.2434 148.705 19.1362C148.615 19.029 148.571 18.9039 148.571 18.761V6.16432C148.571 6.02138 148.615 5.89631 148.705 5.7891C148.812 5.68189 148.946 5.62829 149.107 5.62829H151.09C151.251 5.62829 151.376 5.68189 151.465 5.7891C151.573 5.87844 151.626 6.00351 151.626 6.16432V18.761C151.626 18.9218 151.573 19.0558 151.465 19.163C151.376 19.2523 151.251 19.297 151.09 19.297H149.107ZM148.33 0.696836C148.33 0.58963 148.365 0.491358 148.437 0.40202C148.526 0.312682 148.633 0.268013 148.758 0.268013H151.492C151.617 0.268013 151.715 0.312682 151.787 0.40202C151.876 0.491358 151.921 0.58963 151.921 0.696836V3.18937C151.921 3.31444 151.876 3.42164 151.787 3.51098C151.715 3.58245 151.617 3.61819 151.492 3.61819H148.758C148.633 3.61819 148.526 3.58245 148.437 3.51098C148.365 3.42164 148.33 3.31444 148.33 3.18937V0.696836Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M105.963 19.7526C105.213 19.7526 104.614 19.6633 104.168 19.4846C103.721 19.3238 103.274 19.1005 102.828 18.8146C102.452 18.5644 102.158 18.4394 101.943 18.4394C101.782 18.4394 101.648 18.4751 101.541 18.5466C101.452 18.618 101.353 18.7252 101.246 18.8682C101.068 19.1541 100.853 19.297 100.603 19.297H99.2094C99.0664 19.297 98.9414 19.2434 98.8342 19.1362C98.7269 19.029 98.6733 18.9039 98.6733 18.761V0.536027C98.6733 0.393087 98.7269 0.268014 98.8342 0.160808C98.9414 0.0536026 99.0664 0 99.2094 0H101.193C101.336 0 101.461 0.0536026 101.568 0.160808C101.675 0.268014 101.729 0.393087 101.729 0.536027V5.6551C101.729 6.03032 101.898 6.21792 102.238 6.21792C102.381 6.21792 102.533 6.17326 102.694 6.08392C102.854 5.99458 102.953 5.94098 102.988 5.92311C103.435 5.69083 103.864 5.51215 104.275 5.38708C104.704 5.24414 105.266 5.17267 105.963 5.17267C106.892 5.17267 107.741 5.33348 108.509 5.6551C109.278 5.97671 109.93 6.43234 110.466 7.02197C111.073 7.6652 111.538 8.45137 111.86 9.38049C112.181 10.3096 112.342 11.337 112.342 12.4627C112.342 13.5883 112.181 14.6157 111.86 15.5448C111.538 16.4739 111.073 17.2601 110.466 17.9033C109.93 18.5108 109.278 18.9754 108.509 19.297C107.741 19.6008 106.892 19.7526 105.963 19.7526ZM101.621 12.4627C101.621 13.6955 101.925 14.6961 102.533 15.4644C103.23 16.3042 104.185 16.7241 105.4 16.7241C106.026 16.7241 106.58 16.6169 107.062 16.4025C107.545 16.1702 107.947 15.8575 108.268 15.4644C108.894 14.6961 109.206 13.6955 109.206 12.4627C109.206 11.2119 108.894 10.2203 108.268 9.4877C107.947 9.07674 107.545 8.76406 107.062 8.54965C106.58 8.31737 106.026 8.20123 105.4 8.20123C104.168 8.20123 103.212 8.63005 102.533 9.4877C101.925 10.2381 101.621 11.2298 101.621 12.4627Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M93.5555 19.297C92.1619 19.297 91.0898 18.9039 90.3394 18.1177C89.6068 17.3316 89.2405 16.2863 89.2405 14.982V8.81765C89.2405 8.63897 89.1512 8.54964 88.9725 8.54964H86.2924C86.1494 8.54964 86.0244 8.49603 85.9172 8.38883C85.81 8.28162 85.7563 8.15655 85.7563 8.01361V6.16431C85.7563 6.02137 85.81 5.8963 85.9172 5.78909C86.0244 5.68189 86.1494 5.62828 86.2924 5.62828H88.9725C89.1512 5.62828 89.2405 5.53895 89.2405 5.36027V1.47407C89.2405 1.33113 89.2941 1.20605 89.4013 1.09885C89.5085 0.991643 89.6336 0.93804 89.7766 0.93804H91.7598C91.9028 0.93804 92.0279 0.991643 92.1351 1.09885C92.2423 1.20605 92.2959 1.33113 92.2959 1.47407V5.36027C92.2959 5.53895 92.3852 5.62828 92.5639 5.62828H96.2625C96.4054 5.62828 96.5305 5.68189 96.6377 5.78909C96.7449 5.8963 96.7985 6.02137 96.7985 6.16431V8.01361C96.7985 8.15655 96.7449 8.28162 96.6377 8.38883C96.5305 8.49603 96.4054 8.54964 96.2625 8.54964H92.5639C92.3852 8.54964 92.2959 8.63897 92.2959 8.81765V14.5531C92.2959 15.107 92.4477 15.5269 92.7515 15.8128C93.0552 16.0987 93.4751 16.2416 94.0112 16.2416H96.2625C96.4054 16.2416 96.5305 16.2952 96.6377 16.4024C96.7449 16.5096 96.7985 16.6347 96.7985 16.7777V18.761C96.7985 18.9039 96.7449 19.029 96.6377 19.1362C96.5305 19.2434 96.4054 19.297 96.2625 19.297H93.5555Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M78.7594 19.7526C76.6332 19.7526 74.9804 19.0915 73.8012 17.7693C72.6398 16.4471 72.0591 14.6782 72.0591 12.4626C72.0591 11.0154 72.3182 9.74677 72.8363 8.65685C73.3545 7.54906 74.1139 6.69141 75.1144 6.08391C76.115 5.47642 77.3032 5.17267 78.679 5.17267C80.698 5.17267 82.2883 5.7623 83.4496 6.94156C84.6289 8.12082 85.2185 9.74677 85.2185 11.8194V12.7843C85.2185 13.0165 85.156 13.1952 85.0309 13.3203C84.9059 13.4454 84.7361 13.5079 84.5217 13.5079H75.6237C75.445 13.5079 75.3556 13.6062 75.3556 13.8027C75.3914 14.839 75.7309 15.6609 76.3741 16.2684C77.0173 16.8581 77.8124 17.1529 78.7594 17.1529C80.0816 17.1529 81.0286 16.6526 81.6004 15.652C81.7969 15.3304 82.0202 15.1696 82.2704 15.1696H84.3341C84.4949 15.1696 84.6289 15.2321 84.7361 15.3572C84.8433 15.4644 84.8701 15.6163 84.8165 15.8128C84.477 16.9921 83.7802 17.948 82.726 18.6806C81.6718 19.3953 80.3496 19.7526 78.7594 19.7526ZM75.3288 10.801C75.3288 10.9796 75.4271 11.069 75.6237 11.069H81.8416C82.0024 11.069 82.0828 10.9796 82.0828 10.801C82.0828 9.92545 81.7701 9.19288 81.1447 8.60325C80.5372 7.99575 79.7153 7.692 78.679 7.692C77.7142 7.692 76.9191 7.99575 76.2937 8.60325C75.6862 9.19288 75.3646 9.92545 75.3288 10.801Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M58.246 19.297C58.103 19.297 57.978 19.2434 57.8708 19.1362C57.7636 19.029 57.71 18.9039 57.71 18.761V6.16432C57.71 6.02138 57.7636 5.8963 57.8708 5.7891C57.978 5.68189 58.103 5.62829 58.246 5.62829H59.6665C59.8273 5.62829 59.9523 5.66402 60.0417 5.7355C60.131 5.7891 60.2382 5.88737 60.3633 6.03031C60.4526 6.15538 60.533 6.25366 60.6045 6.32513C60.6938 6.37873 60.81 6.40553 60.9529 6.40553C61.078 6.40553 61.2031 6.3698 61.3281 6.29833C61.4532 6.22685 61.5783 6.14645 61.7034 6.05711C62.0964 5.7891 62.4985 5.57469 62.9094 5.41388C63.3382 5.25307 63.9279 5.17267 64.6783 5.17267C65.7146 5.17267 66.6348 5.39601 67.4388 5.8427C68.2429 6.28939 68.8682 6.94156 69.3149 7.7992C69.7795 8.65685 70.0118 9.68424 70.0118 10.8814V18.761C70.0118 18.9039 69.9582 19.029 69.851 19.1362C69.7438 19.2434 69.6187 19.297 69.4757 19.297H67.4924C67.3495 19.297 67.2244 19.2434 67.1172 19.1362C67.01 19.029 66.9564 18.9039 66.9564 18.761V11.3638C66.9564 10.3632 66.6705 9.58596 66.0988 9.03207C65.5449 8.47817 64.8034 8.20122 63.8743 8.20122C62.9452 8.20122 62.1947 8.47817 61.623 9.03207C61.0512 9.58596 60.7653 10.3632 60.7653 11.3638V18.761C60.7653 18.9039 60.7117 19.029 60.6045 19.1362C60.4973 19.2434 60.3722 19.297 60.2293 19.297H58.246Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M53.1744 19.297C52.8528 19.297 52.5669 19.1541 52.3168 18.8682L48.0822 13.8295C48.0107 13.7581 47.9303 13.7223 47.8409 13.7223C47.7695 13.7223 47.7069 13.7491 47.6533 13.8027C47.5997 13.8563 47.5729 13.9189 47.5729 13.9903V18.761C47.5729 18.9039 47.5193 19.029 47.4121 19.1362C47.3049 19.2434 47.1798 19.297 47.0369 19.297H45.0536C44.9107 19.297 44.7856 19.2434 44.6784 19.1362C44.5712 19.029 44.5176 18.9039 44.5176 18.761V0.536027C44.5176 0.393087 44.5712 0.268014 44.6784 0.160808C44.7856 0.0536026 44.9107 0 45.0536 0H47.0369C47.1798 0 47.3049 0.0536026 47.4121 0.160808C47.5193 0.268014 47.5729 0.393087 47.5729 0.536027V10.5061C47.5729 10.5955 47.5997 10.667 47.6533 10.7206C47.7069 10.7742 47.7695 10.801 47.8409 10.801C47.9124 10.801 47.9839 10.7652 48.0554 10.6938L52.424 6.00351C52.6741 5.75337 52.9421 5.62829 53.228 5.62829H55.3989C55.5597 5.62829 55.6848 5.6819 55.7741 5.7891C55.8813 5.89631 55.9349 6.02138 55.9349 6.16432C55.9349 6.32513 55.8813 6.45914 55.7741 6.56634L50.8695 11.8462C50.7623 11.9534 50.7087 12.0606 50.7087 12.1678C50.7087 12.275 50.7623 12.3822 50.8695 12.4895L55.8813 18.359C55.9885 18.484 56.0421 18.618 56.0421 18.761C56.0421 18.9218 55.9885 19.0558 55.8813 19.163C55.792 19.2523 55.6669 19.297 55.5061 19.297H53.1744Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M34.9505 19.297C34.8076 19.297 34.6736 19.2434 34.5485 19.1362C34.4413 19.029 34.3877 18.9039 34.3877 18.761V6.16432C34.3877 6.02138 34.4413 5.8963 34.5485 5.7891C34.6736 5.68189 34.8076 5.62829 34.9505 5.62829H36.3174C36.639 5.62829 36.8356 5.80697 36.907 6.16432C36.9785 6.48594 37.05 6.72715 37.1214 6.88796C37.2108 7.0309 37.3448 7.10237 37.5235 7.10237C37.6307 7.10237 37.7468 7.06663 37.8719 6.99516C37.9969 6.90582 38.122 6.80755 38.2471 6.70035C38.6402 6.36086 39.0422 6.10178 39.4531 5.92311C39.8641 5.72656 40.4359 5.62829 41.1684 5.62829H42.5353C42.6782 5.62829 42.8033 5.68189 42.9105 5.7891C43.0177 5.8963 43.0713 6.02138 43.0713 6.16432V8.28163C43.0713 8.42457 43.0177 8.54964 42.9105 8.65685C42.8033 8.76405 42.6782 8.81766 42.5353 8.81766H41.0076C39.9713 8.81766 39.1137 9.12141 38.4347 9.7289C37.7736 10.3364 37.443 11.2745 37.443 12.543V18.761C37.443 18.9039 37.3894 19.029 37.2822 19.1362C37.175 19.2434 37.05 19.297 36.907 19.297H34.9505Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M23.8885 19.7526C22.9594 19.7526 22.1107 19.6007 21.3424 19.297C20.5741 18.9754 19.9219 18.5108 19.3859 17.9033C18.7784 17.2601 18.3138 16.4739 17.9922 15.5448C17.6706 14.6157 17.5098 13.5883 17.5098 12.4626C17.5098 11.337 17.6706 10.3096 17.9922 9.38048C18.3138 8.45137 18.7784 7.6652 19.3859 7.02196C19.9219 6.43233 20.5741 5.97671 21.3424 5.65509C22.1107 5.33348 22.9594 5.17267 23.8885 5.17267C24.6389 5.17267 25.2375 5.262 25.6842 5.44068C26.1309 5.60149 26.5775 5.82483 27.0242 6.11072C27.3995 6.36086 27.6943 6.48594 27.9087 6.48594C28.0695 6.48594 28.1946 6.4502 28.2839 6.37873C28.3911 6.30726 28.4983 6.20005 28.6055 6.05711C28.7842 5.77123 28.9986 5.62829 29.2487 5.62829H30.6424C30.7854 5.62829 30.9104 5.68189 31.0176 5.7891C31.1248 5.8963 31.1784 6.02138 31.1784 6.16432V18.761C31.1784 18.9039 31.1248 19.029 31.0176 19.1362C30.9104 19.2434 30.7854 19.297 30.6424 19.297H29.2487C28.9986 19.297 28.7842 19.1541 28.6055 18.8682C28.4983 18.7252 28.3911 18.618 28.2839 18.5466C28.1946 18.4751 28.0695 18.4394 27.9087 18.4394C27.6943 18.4394 27.3995 18.5644 27.0242 18.8146C26.5775 19.1005 26.1309 19.3238 25.6842 19.4846C25.2375 19.6633 24.6389 19.7526 23.8885 19.7526ZM20.6455 12.4626C20.6455 13.6955 20.9582 14.6961 21.5836 15.4644C21.9052 15.8575 22.3072 16.1702 22.7896 16.4024C23.2721 16.6169 23.8259 16.7241 24.4513 16.7241C25.0767 16.7241 25.6306 16.6169 26.113 16.4024C26.5954 16.1702 26.9974 15.8575 27.3191 15.4644C27.9265 14.6961 28.2303 13.6955 28.2303 12.4626C28.2303 11.2298 27.9265 10.2381 27.3191 9.48769C26.9974 9.07674 26.5954 8.76405 26.113 8.54964C25.6306 8.31736 25.0767 8.20122 24.4513 8.20122C23.8259 8.20122 23.2721 8.31736 22.7896 8.54964C22.3072 8.76405 21.9052 9.07674 21.5836 9.48769C20.9582 10.2203 20.6455 11.2119 20.6455 12.4626Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M0.536027 19.297C0.393086 19.297 0.268013 19.2434 0.160808 19.1362C0.0536025 19.029 0 18.9039 0 18.761V1.07206C0 0.929116 0.0536025 0.804043 0.160808 0.696837C0.268013 0.589631 0.393086 0.536028 0.536027 0.536028H6.83434C9.47874 0.536028 11.6407 1.349 13.3203 2.97495C14.16 3.79686 14.8033 4.80638 15.25 6.00351C15.6966 7.18277 15.92 8.48711 15.92 9.91652C15.92 11.3459 15.6966 12.6592 15.25 13.8563C14.8033 15.0356 14.16 16.0362 13.3203 16.8581C11.6407 18.484 9.47874 19.297 6.83434 19.297H0.536027ZM3.24296 15.786C3.24296 15.9647 3.3323 16.054 3.51098 16.054H6.83434C8.53176 16.054 9.88076 15.5537 10.8813 14.5532C11.4352 14.0171 11.8551 13.356 12.141 12.5699C12.4269 11.7837 12.5698 10.8992 12.5698 9.91652C12.5698 8.9338 12.4269 8.04935 12.141 7.26318C11.8551 6.477 11.4352 5.8159 10.8813 5.27988C9.84502 4.27929 8.49602 3.779 6.83434 3.779H3.51098C3.3323 3.779 3.24296 3.86833 3.24296 4.04701V15.786Z"
|
||||
fill="currentColor"/>
|
||||
<g clip-path="url(#clip0_4498_21233)">
|
||||
<path d="M140.006 0.211082H142.457C143.062 0.211082 143.553 0.705733 143.553 1.31591V4.01815C143.553 4.62833 143.062 5.12298 142.457 5.12298H140.006C139.401 5.12298 138.911 4.62833 138.911 4.01815V1.31591C138.911 0.705733 139.401 0.211082 140.006 0.211082Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M116.73 0.211082H119.181C119.786 0.211082 120.277 0.705733 120.277 1.31591V4.01815C120.277 4.62833 119.786 5.12298 119.181 5.12298H116.73C116.125 5.12298 115.635 4.62833 115.635 4.01815V1.31591C115.635 0.705733 116.125 0.211082 116.73 0.211082Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M116.673 11.4481H119.184C119.499 11.4481 119.702 11.6806 119.754 12.0236L119.983 13.6601C120.017 13.8373 120.106 13.9363 120.256 13.9363H121.576C121.89 13.9363 122.089 14.1527 122.146 14.5118L122.374 15.8974C122.411 16.192 122.612 16.3716 122.922 16.4498C122.922 16.4498 124.223 16.8572 129.643 16.8572V20.3121C124.225 20.3121 121.119 19.9024 121.119 19.9024C120.861 19.8633 120.549 19.6446 120.549 19.327L120.32 18.1071C120.32 17.9551 120.197 17.8309 120.046 17.8309H118.49C118.175 17.8309 118.002 17.5754 117.919 17.2554L117.691 16.3278C117.657 16.1345 117.568 16.0516 117.417 16.0516H116.902C116.587 16.0516 116.331 15.7938 116.331 15.4762L116.103 12.0236C116.103 11.7059 116.358 11.4481 116.673 11.4481Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M136.912 15.8974L137.14 14.5118C137.197 14.1527 137.395 13.9363 137.71 13.9363H139.03C139.18 13.9363 139.269 13.8373 139.303 13.6601L139.532 12.0236C139.584 11.6806 139.787 11.4481 140.102 11.4481H142.613C142.927 11.4481 143.183 11.7059 143.183 12.0236L142.955 15.4762C142.955 15.7938 142.699 16.0516 142.384 16.0516H141.869C141.718 16.0516 141.629 16.1345 141.595 16.3278L141.366 17.2554C141.284 17.5754 141.111 17.8309 140.796 17.8309H139.239C139.089 17.8309 138.966 17.9551 138.966 18.1071L138.737 19.327C138.737 19.6446 138.425 19.8633 138.167 19.9024C138.167 19.9024 135.061 20.3121 129.643 20.3121V16.8572C135.063 16.8572 136.364 16.4498 136.364 16.4498C136.674 16.3716 136.875 16.192 136.912 15.8974Z"
|
||||
fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4498_21233">
|
||||
<rect width="27.9181" height="20.101" fill="currentColor" transform="matrix(-1 0 0 1 143.553 0.211082)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 18 KiB |
1405
pkgs/clan-app/ui/package-lock.json
generated
@@ -75,7 +75,8 @@
|
||||
"@tanstack/solid-query": "^5.76.0",
|
||||
"solid-js": "^1.9.7",
|
||||
"solid-toast": "^0.5.0",
|
||||
"three": "^0.176.0"
|
||||
"three": "^0.176.0",
|
||||
"valibot": "^1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-arm64": "^0.25.4",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
box-shadow: 0.125rem 0.125rem 0 0 theme(colors.bg.inv.acc.3) inset;
|
||||
|
||||
&.ghost {
|
||||
@apply bg-transparent border-none shadow-none;
|
||||
@apply bg-transparent border-transparent shadow-none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -71,7 +71,7 @@
|
||||
0.125rem 0.125rem 0 0 theme(colors.off.white) inset;
|
||||
|
||||
&.ghost {
|
||||
@apply bg-transparent border-none shadow-none;
|
||||
@apply bg-transparent border-transparent shadow-none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -138,6 +138,10 @@
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
& > span.typography {
|
||||
@apply max-w-full overflow-hidden whitespace-nowrap text-ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
/* button group */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
hr {
|
||||
@apply border-none outline-none bg-inv-2;
|
||||
@apply border-none outline-none bg-inv-2 self-stretch;
|
||||
|
||||
&.inverted {
|
||||
@apply bg-def-3;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Fieldset, FieldsetProps } from "@/src/components/Form/Fieldset";
|
||||
import {
|
||||
Fieldset,
|
||||
FieldsetFieldProps,
|
||||
FieldsetProps,
|
||||
} from "@/src/components/Form/Fieldset";
|
||||
import cx from "classnames";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/Form/TextArea";
|
||||
@@ -40,7 +44,7 @@ export type Story = StoryObj<typeof meta>;
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
legend: "Signup",
|
||||
children: (props: FieldProps) => (
|
||||
children: (props: FieldsetFieldProps) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...props}
|
||||
|
||||
@@ -12,24 +12,28 @@ export type FieldsetFieldProps = Pick<
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export interface FieldsetProps
|
||||
extends Pick<FieldProps, "orientation" | "inverted"> {
|
||||
export type FieldsetProps = Pick<FieldProps, "orientation" | "inverted"> & {
|
||||
legend?: string;
|
||||
disabled?: boolean;
|
||||
error?: string;
|
||||
children: (props: FieldsetFieldProps) => JSX.Element;
|
||||
}
|
||||
disabled?: boolean;
|
||||
name?: string;
|
||||
children: JSX.Element | ((props: FieldsetFieldProps) => JSX.Element);
|
||||
};
|
||||
|
||||
export const Fieldset = (props: FieldsetProps) => {
|
||||
const orientation = () => props.orientation || "vertical";
|
||||
|
||||
const [fieldProps] = splitProps(props, [
|
||||
"orientation",
|
||||
"inverted",
|
||||
"disabled",
|
||||
"error",
|
||||
"children",
|
||||
]);
|
||||
|
||||
const children = () =>
|
||||
typeof props.children === "function"
|
||||
? props.children(fieldProps)
|
||||
: props.children;
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
role="group"
|
||||
@@ -51,7 +55,7 @@ export const Fieldset = (props: FieldsetProps) => {
|
||||
</Typography>
|
||||
</legend>
|
||||
)}
|
||||
<div class="fields">{props.children(fieldProps)}</div>
|
||||
<div class="fields">{children()}</div>
|
||||
{props.error && (
|
||||
<div class="error" role="alert">
|
||||
<Typography
|
||||
|
||||
5
pkgs/clan-app/ui/src/components/Form/HostFileInput.css
Normal file
@@ -0,0 +1,5 @@
|
||||
div.form-field.host-file {
|
||||
button {
|
||||
@apply w-1/2;
|
||||
}
|
||||
}
|
||||
131
pkgs/clan-app/ui/src/components/Form/HostFileInput.stories.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||
import cx from "classnames";
|
||||
import {
|
||||
HostFileInput,
|
||||
HostFileInputProps,
|
||||
} from "@/src/components/Form/HostFileInput";
|
||||
|
||||
const Examples = (props: HostFileInputProps) => (
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-8 p-8">
|
||||
<HostFileInput {...props} />
|
||||
<HostFileInput {...props} size="s" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
|
||||
<HostFileInput {...props} inverted={true} />
|
||||
<HostFileInput {...props} inverted={true} size="s" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-8 p-8">
|
||||
<HostFileInput {...props} orientation="horizontal" />
|
||||
<HostFileInput {...props} orientation="horizontal" size="s" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
|
||||
<HostFileInput {...props} inverted={true} orientation="horizontal" />
|
||||
<HostFileInput
|
||||
{...props}
|
||||
inverted={true}
|
||||
orientation="horizontal"
|
||||
size="s"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const meta = {
|
||||
title: "Components/Form/HostFileInput",
|
||||
component: Examples,
|
||||
decorators: [
|
||||
(Story: StoryObj, context: StoryContext<HostFileInputProps>) => {
|
||||
return (
|
||||
<div
|
||||
class={cx({
|
||||
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
|
||||
"w-[1024px]": context.args.orientation == "horizontal",
|
||||
"bg-inv-acc-3": context.args.inverted,
|
||||
})}
|
||||
>
|
||||
<Story />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
} satisfies Meta<HostFileInputProps>;
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Bare: Story = {
|
||||
args: {
|
||||
onSelectFile: async () => {
|
||||
return "/home/github/clans/my-clan/foo/bar/baz/fizz/buzz";
|
||||
},
|
||||
input: {
|
||||
placeholder: "e.g. 11/06/89",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Label: Story = {
|
||||
args: {
|
||||
...Bare.args,
|
||||
label: "DOB",
|
||||
},
|
||||
};
|
||||
|
||||
export const Description: Story = {
|
||||
args: {
|
||||
...Label.args,
|
||||
description: "The date you were born",
|
||||
},
|
||||
};
|
||||
|
||||
export const Required: Story = {
|
||||
args: {
|
||||
...Description.args,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Tooltip: Story = {
|
||||
args: {
|
||||
...Required.args,
|
||||
tooltip: "The day you came out of your momma",
|
||||
},
|
||||
};
|
||||
|
||||
export const Icon: Story = {
|
||||
args: {
|
||||
...Tooltip.args,
|
||||
icon: "Checkmark",
|
||||
},
|
||||
};
|
||||
|
||||
export const Ghost: Story = {
|
||||
args: {
|
||||
...Icon.args,
|
||||
ghost: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Invalid: Story = {
|
||||
args: {
|
||||
...Tooltip.args,
|
||||
validationState: "invalid",
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
...Icon.args,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ReadOnly: Story = {
|
||||
args: {
|
||||
...Icon.args,
|
||||
readOnly: true,
|
||||
defaultValue: "14/05/02",
|
||||
},
|
||||
};
|
||||
108
pkgs/clan-app/ui/src/components/Form/HostFileInput.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
TextField,
|
||||
TextFieldInputProps,
|
||||
TextFieldRootProps,
|
||||
} from "@kobalte/core/text-field";
|
||||
|
||||
import cx from "classnames";
|
||||
import { Label } from "./Label";
|
||||
import { Button } from "../Button/Button";
|
||||
import "./HostFileInput.css";
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
import { createSignal } from "solid-js";
|
||||
import { Tooltip } from "@kobalte/core/tooltip";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
|
||||
export type HostFileInputProps = FieldProps &
|
||||
TextFieldRootProps & {
|
||||
onSelectFile: () => Promise<string>;
|
||||
input?: PolymorphicProps<"input", TextFieldInputProps<"input">>;
|
||||
};
|
||||
|
||||
export const HostFileInput = (props: HostFileInputProps) => {
|
||||
const [value, setValue] = createSignal<string>(props.value || "");
|
||||
|
||||
let actualInputElement: HTMLInputElement | undefined;
|
||||
|
||||
const selectFile = async () => {
|
||||
try {
|
||||
console.log("selecting file", props.onSelectFile);
|
||||
setValue(await props.onSelectFile());
|
||||
actualInputElement?.dispatchEvent(
|
||||
new Event("input", { bubbles: true, cancelable: true }),
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error selecting file", error);
|
||||
// todo work out how to display the error
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
class={cx("form-field", "host-file", props.size, props.orientation, {
|
||||
inverted: props.inverted,
|
||||
ghost: props.ghost,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={"start"}>
|
||||
<Label
|
||||
labelComponent={TextField.Label}
|
||||
descriptionComponent={TextField.Description}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<TextField.Input
|
||||
{...props.input}
|
||||
hidden={true}
|
||||
value={value()}
|
||||
ref={(el: HTMLInputElement) => {
|
||||
actualInputElement = el; // Capture for local use
|
||||
}}
|
||||
/>
|
||||
|
||||
{!value() && (
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
size={props.size}
|
||||
startIcon="Folder"
|
||||
onClick={selectFile}
|
||||
disabled={props.disabled || props.readOnly}
|
||||
>
|
||||
No Selection
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{value() && (
|
||||
<Tooltip placement="top">
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content class="tooltip-content">
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={!props.inverted}
|
||||
>
|
||||
{value()}
|
||||
</Typography>
|
||||
<Tooltip.Arrow />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
<Tooltip.Trigger
|
||||
as={Button}
|
||||
hierarchy="secondary"
|
||||
size={props.size}
|
||||
startIcon="Folder"
|
||||
onClick={selectFile}
|
||||
disabled={props.disabled || props.readOnly}
|
||||
>
|
||||
{value()}
|
||||
</Tooltip.Trigger>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Orienter>
|
||||
</TextField>
|
||||
);
|
||||
};
|
||||
@@ -22,40 +22,3 @@ div.form-label {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.tooltip-content {
|
||||
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
|
||||
|
||||
max-width: min(calc(100vw - 16px), 380px);
|
||||
transform-origin: var(--kb-tooltip-content-transform-origin);
|
||||
animation: tooltipHide 250ms ease-in forwards;
|
||||
|
||||
&[data-expanded] {
|
||||
animation: tooltipShow 250ms ease-out;
|
||||
}
|
||||
|
||||
&.inverted {
|
||||
@apply bg-def-2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tooltipShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes tooltipHide {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Show } from "solid-js";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Tooltip as KTooltip } from "@kobalte/core/tooltip";
|
||||
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
||||
import Icon from "@/src/components/Icon/Icon";
|
||||
import { TextField } from "@kobalte/core/text-field";
|
||||
import { Checkbox } from "@kobalte/core/checkbox";
|
||||
import { Combobox } from "@kobalte/core/combobox";
|
||||
import "./Label.css";
|
||||
import cx from "classnames";
|
||||
|
||||
export type Size = "default" | "s";
|
||||
|
||||
@@ -49,31 +48,27 @@ export const Label = (props: LabelProps) => {
|
||||
{props.label}
|
||||
</Typography>
|
||||
{props.tooltip && (
|
||||
<KTooltip placement="top">
|
||||
<KTooltip.Trigger>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
inverted={props.inverted}
|
||||
trigger={
|
||||
<Icon
|
||||
icon="Info"
|
||||
color="tertiary"
|
||||
inverted={props.inverted}
|
||||
size={props.size == "default" ? "0.85em" : "0.75rem"}
|
||||
/>
|
||||
<KTooltip.Portal>
|
||||
<KTooltip.Content
|
||||
class={cx("tooltip-content", { inverted: props.inverted })}
|
||||
>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={!props.inverted}
|
||||
>
|
||||
{props.tooltip}
|
||||
</Typography>
|
||||
<KTooltip.Arrow />
|
||||
</KTooltip.Content>
|
||||
</KTooltip.Portal>
|
||||
</KTooltip.Trigger>
|
||||
</KTooltip>
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={!props.inverted}
|
||||
>
|
||||
{props.tooltip}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)}
|
||||
</props.labelComponent>
|
||||
{props.description && (
|
||||
|
||||
@@ -102,6 +102,7 @@ export type IconVariant = keyof typeof icons;
|
||||
const viewBoxes: Partial<Record<IconVariant, string>> = {
|
||||
ClanIcon: "0 0 72 89",
|
||||
Offline: "0 0 38 27",
|
||||
Cursor: "0 0 35 42",
|
||||
};
|
||||
|
||||
export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {
|
||||
|
||||
88
pkgs/clan-app/ui/src/components/Logo/Logo.stories.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Component, For } from "solid-js";
|
||||
import { Logo, LogoProps, LogoVariant } from "./Logo";
|
||||
import cx from "classnames";
|
||||
|
||||
const variants: LogoVariant[] = ["Clan", "Darknet"];
|
||||
|
||||
const LogoExamples: Component<LogoProps> = (props) => (
|
||||
<div class="grid grid-cols-6 items-center gap-4">
|
||||
<For each={variants}>{(item) => <Logo {...props} variant={item} />}</For>
|
||||
</div>
|
||||
);
|
||||
|
||||
const meta: Meta<LogoProps> = {
|
||||
title: "Components/Logo",
|
||||
component: LogoExamples,
|
||||
decorators: [
|
||||
(Story: StoryObj, context: StoryContext<LogoProps>) => (
|
||||
<div class={cx(context.args.inverted || false ? "bg-inv-acc-3" : "")}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<LogoProps>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
color: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
color: "secondary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Tertiary: Story = {
|
||||
args: {
|
||||
color: "tertiary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Quaternary: Story = {
|
||||
args: {
|
||||
color: "quaternary",
|
||||
},
|
||||
};
|
||||
|
||||
export const PrimaryInverted: Story = {
|
||||
args: {
|
||||
...Primary.args,
|
||||
inverted: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const SecondaryInverted: Story = {
|
||||
args: {
|
||||
...Secondary.args,
|
||||
inverted: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const TertiaryInverted: Story = {
|
||||
args: {
|
||||
...Tertiary.args,
|
||||
inverted: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const QuaternaryInverted: Story = {
|
||||
args: {
|
||||
...Quaternary.args,
|
||||
inverted: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Inverted: Story = {
|
||||
args: {
|
||||
inverted: true,
|
||||
},
|
||||
};
|
||||
40
pkgs/clan-app/ui/src/components/Logo/Logo.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import Clan from "@/logos/clan.svg";
|
||||
import Darknet from "@/logos/darknet.svg";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import { Color, fgClass } from "@/src/components/colors";
|
||||
import { JSX, splitProps } from "solid-js";
|
||||
import cx from "classnames";
|
||||
|
||||
const logos = {
|
||||
Clan,
|
||||
Darknet,
|
||||
};
|
||||
|
||||
export type LogoVariant = keyof typeof logos;
|
||||
|
||||
export interface LogoProps extends JSX.SvgSVGAttributes<SVGElement> {
|
||||
class?: string;
|
||||
variant: LogoVariant;
|
||||
color?: Color;
|
||||
inverted?: boolean;
|
||||
}
|
||||
|
||||
export const Logo = (props: LogoProps) => {
|
||||
const [local, iconProps] = splitProps(props, [
|
||||
"variant",
|
||||
"color",
|
||||
"class",
|
||||
"inverted",
|
||||
]);
|
||||
|
||||
const Logo = logos[local.variant];
|
||||
return (
|
||||
<Dynamic
|
||||
component={Logo}
|
||||
class={cx("icon", local.class, fgClass(local.color, local.inverted), {
|
||||
inverted: local.inverted,
|
||||
})}
|
||||
data-logo-name={local.variant}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { TagProps } from "@/src/components/Tag/Tag";
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { fn } from "storybook/test";
|
||||
import { Modal, ModalContext, ModalProps } from "@/src/components/Modal/Modal";
|
||||
import { Fieldset } from "@/src/components/Form/Fieldset";
|
||||
import { Fieldset, FieldsetFieldProps } from "@/src/components/Form/Fieldset";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/Form/TextArea";
|
||||
import { Checkbox } from "@/src/components/Form/Checkbox";
|
||||
@@ -24,7 +24,7 @@ export const Default: Story = {
|
||||
children: ({ close }: ModalContext) => (
|
||||
<form class="flex flex-col gap-5">
|
||||
<Fieldset legend="General">
|
||||
{(props) => (
|
||||
{(props: FieldsetFieldProps) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...props}
|
||||
|
||||
30
pkgs/clan-app/ui/src/components/Toolbar/Toolbar.css
Normal file
@@ -0,0 +1,30 @@
|
||||
div.toolbar {
|
||||
@apply flex flex-row items-center justify-center gap-1.5 p-1 size-fit rounded-md self-stretch;
|
||||
|
||||
border: 0.0625rem solid theme(colors.off.toolbar_border);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
theme(colors.bg.inv.3) 0%,
|
||||
theme(colors.bg.inv.4) 100%
|
||||
);
|
||||
|
||||
& > hr {
|
||||
@apply h-full min-h-8;
|
||||
}
|
||||
|
||||
& > button.toolbar-button {
|
||||
@apply w-6 h-6 p-1 rounded-[0.25rem];
|
||||
|
||||
&:hover {
|
||||
@apply bg-inv-1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-inv-4;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@apply bg-bg-semantic-success-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
pkgs/clan-app/ui/src/components/Toolbar/Toolbar.stories.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Toolbar, ToolbarProps } from "@/src/components/Toolbar/Toolbar";
|
||||
import { Divider } from "@/src/components/Divider/Divider";
|
||||
import { ToolbarButton } from "./ToolbarButton";
|
||||
|
||||
const meta: Meta<ToolbarProps> = {
|
||||
title: "Components/Toolbar",
|
||||
component: Toolbar,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ToolbarProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<ToolbarButton name="select" icon="Cursor" />
|
||||
<ToolbarButton name="new-machine" icon="NewMachine" />
|
||||
<Divider orientation="vertical" />
|
||||
<ToolbarButton name="modules" icon="Modules" selected={true} />
|
||||
<ToolbarButton name="ai" icon="AI" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
14
pkgs/clan-app/ui/src/components/Toolbar/Toolbar.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import "./Toolbar.css";
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
export interface ToolbarProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const Toolbar = (props: ToolbarProps) => {
|
||||
return (
|
||||
<div class="toolbar" role="toolbar" aria-orientation="horizontal">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
pkgs/clan-app/ui/src/components/Toolbar/ToolbarButton.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import "./Toolbar.css";
|
||||
import cx from "classnames";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import Icon, { IconVariant } from "@/src/components/Icon/Icon";
|
||||
import type { JSX } from "solid-js";
|
||||
|
||||
export interface ToolbarButtonProps
|
||||
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon: IconVariant;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export const ToolbarButton = (props: ToolbarButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
class={cx("toolbar-button", { selected: props.selected })}
|
||||
{...props}
|
||||
>
|
||||
<Icon icon={props.icon} inverted={!props.selected} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
9
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.css
Normal file
@@ -0,0 +1,9 @@
|
||||
div.tooltip-content {
|
||||
@apply z-50 px-2 py-0.5 bg-inv-4 rounded-[0.125rem] leading-none;
|
||||
|
||||
max-width: min(calc(100vw - 16px), 380px);
|
||||
|
||||
&.inverted {
|
||||
@apply bg-def-2;
|
||||
}
|
||||
}
|
||||
40
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.stories.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Tooltip, TooltipProps } from "@/src/components/Tooltip/Tooltip";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
|
||||
const meta: Meta<TooltipProps> = {
|
||||
title: "Components/Tooltip",
|
||||
component: Tooltip,
|
||||
decorators: [
|
||||
(Story: StoryObj<TooltipProps>) => (
|
||||
<div class="p-16">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<TooltipProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placement: "top",
|
||||
inverted: false,
|
||||
trigger: <Button hierarchy="primary">Trigger</Button>,
|
||||
children: (
|
||||
<Typography hierarchy="body" size="xs" inverted={true} weight="medium">
|
||||
Your Clan is being created
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const AnimateBounce: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
animation: "bounce",
|
||||
},
|
||||
};
|
||||
34
pkgs/clan-app/ui/src/components/Tooltip/Tooltip.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import "./Tooltip.css";
|
||||
import {
|
||||
Tooltip as KTooltip,
|
||||
TooltipRootProps as KTooltipRootProps,
|
||||
} from "@kobalte/core/tooltip";
|
||||
import cx from "classnames";
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
export interface TooltipProps extends KTooltipRootProps {
|
||||
inverted?: boolean;
|
||||
trigger: JSX.Element;
|
||||
children: JSX.Element;
|
||||
animation?: "bounce";
|
||||
}
|
||||
|
||||
export const Tooltip = (props: TooltipProps) => {
|
||||
return (
|
||||
<KTooltip {...props}>
|
||||
<KTooltip.Trigger>{props.trigger}</KTooltip.Trigger>
|
||||
<KTooltip.Portal>
|
||||
<KTooltip.Content
|
||||
class={cx("tooltip-content", {
|
||||
inverted: props.inverted,
|
||||
"animate-bounce": props.animation == "bounce",
|
||||
})}
|
||||
>
|
||||
{props.placement == "bottom" && <KTooltip.Arrow />}
|
||||
{props.children}
|
||||
{props.placement == "top" && <KTooltip.Arrow />}
|
||||
</KTooltip.Content>
|
||||
</KTooltip.Portal>
|
||||
</KTooltip>
|
||||
);
|
||||
};
|
||||
@@ -148,6 +148,18 @@
|
||||
line-height: 116%;
|
||||
letter-spacing: 0.06rem;
|
||||
}
|
||||
|
||||
&.size-xl {
|
||||
font-size: 2.5rem;
|
||||
line-height: 104%;
|
||||
letter-spacing: -0.06rem;
|
||||
}
|
||||
|
||||
&.size-xxl {
|
||||
font-size: 3rem;
|
||||
line-height: 104%;
|
||||
letter-spacing: -0.06rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.teaser {
|
||||
@@ -161,4 +173,16 @@
|
||||
letter-spacing: -0.06rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ interface SizeForHierarchy {
|
||||
default: string;
|
||||
m: string;
|
||||
l: string;
|
||||
xl: string;
|
||||
xxl: string;
|
||||
};
|
||||
title: {
|
||||
default: string;
|
||||
@@ -52,6 +54,8 @@ const sizeHierarchyMap: SizeForHierarchy = {
|
||||
default: cx("size-default"),
|
||||
m: cx("size-m"),
|
||||
l: cx("size-l"),
|
||||
xl: cx("size-xl"),
|
||||
xxl: cx("size-xxl"),
|
||||
},
|
||||
title: {
|
||||
default: cx("size-default"),
|
||||
@@ -97,6 +101,7 @@ interface _TypographyProps<H extends Hierarchy> {
|
||||
tag?: Tag;
|
||||
class?: string;
|
||||
transform?: Transform;
|
||||
align?: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
||||
@@ -106,6 +111,7 @@ export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
||||
const size = () => sizeHierarchyMap[props.hierarchy][props.size] as string;
|
||||
const weight = () => weightMap[props.weight || "normal"];
|
||||
const color = () => fgClass(props.color, props.inverted);
|
||||
const align = () => `align-${props.align || "left"}`;
|
||||
|
||||
return (
|
||||
<Dynamic
|
||||
@@ -116,6 +122,7 @@ export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
||||
weight(),
|
||||
size(),
|
||||
color(),
|
||||
align(),
|
||||
props.transform,
|
||||
props.class,
|
||||
)}
|
||||
|
||||
92
pkgs/clan-app/ui/src/hooks/api.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { API } from "@/api/API";
|
||||
import { Schema as Inventory } from "@/api/Inventory";
|
||||
|
||||
type OperationNames = keyof API;
|
||||
type Services = NonNullable<Inventory["services"]>;
|
||||
type ServiceNames = keyof Services;
|
||||
|
||||
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
|
||||
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
||||
|
||||
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
|
||||
Services[T]
|
||||
>[string];
|
||||
|
||||
export type SuccessQuery<T extends OperationNames> = Extract<
|
||||
OperationResponse<T>,
|
||||
{ status: "success" }
|
||||
>;
|
||||
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
||||
|
||||
interface SendHeaderType {
|
||||
logging?: { group_path: string[] };
|
||||
}
|
||||
interface BackendSendType<K extends OperationNames> {
|
||||
body: OperationArgs<K>;
|
||||
header?: SendHeaderType;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface ReceiveHeaderType {}
|
||||
interface BackendReturnType<K extends OperationNames> {
|
||||
body: OperationResponse<K>;
|
||||
header: ReceiveHeaderType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing an API call with a unique identifier, result promise, and cancellation capability.
|
||||
*
|
||||
* @template K - A generic type parameter extending the set of operation names.
|
||||
*
|
||||
* @property {string} uuid - A unique identifier for the API call.
|
||||
* @property {Promise<BackendReturnType<K>>} result - A promise that resolves to the return type of the backend operation.
|
||||
* @property {() => Promise<void>} cancel - A function to cancel the API call, returning a promise that resolves when cancellation is completed.
|
||||
*/
|
||||
export interface ApiCall<K extends OperationNames> {
|
||||
uuid: string;
|
||||
result: Promise<OperationResponse<K>>;
|
||||
cancel: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const callApi = <K extends OperationNames>(
|
||||
method: K,
|
||||
args: OperationArgs<K>,
|
||||
backendOpts?: SendHeaderType,
|
||||
): ApiCall<K> => {
|
||||
// if window[method] does not exist, throw an error
|
||||
if (!(method in window)) {
|
||||
console.error(`Method ${method} not found on window object`);
|
||||
|
||||
return {
|
||||
uuid: "",
|
||||
result: Promise.reject(`Method ${method} not found on window object`),
|
||||
cancel: () => Promise.resolve(),
|
||||
};
|
||||
}
|
||||
|
||||
const req: BackendSendType<OperationNames> = {
|
||||
body: args,
|
||||
header: backendOpts,
|
||||
};
|
||||
|
||||
const result = (
|
||||
window as unknown as Record<
|
||||
OperationNames,
|
||||
(
|
||||
args: BackendSendType<OperationNames>,
|
||||
) => Promise<BackendReturnType<OperationNames>>
|
||||
>
|
||||
)[method](req) as Promise<BackendReturnType<K>>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const op_key = (result as any)._webviewMessageId as string;
|
||||
|
||||
return {
|
||||
uuid: op_key,
|
||||
result: result.then(({ body }) => body),
|
||||
cancel: async () => {
|
||||
console.log("Cancelling api call: ", op_key);
|
||||
await callApi("delete_task", { task_id: op_key }).result;
|
||||
},
|
||||
};
|
||||
};
|
||||
29
pkgs/clan-app/ui/src/hooks/clan.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { Params, Navigator } from "@solidjs/router";
|
||||
|
||||
export const selectClanFolder = async () => {
|
||||
const req = callApi("get_clan_folder", {});
|
||||
const res = await req.result;
|
||||
|
||||
if (res.status === "error") {
|
||||
throw new Error(res.errors[0].message);
|
||||
}
|
||||
|
||||
if (res.status === "success" && res.data) {
|
||||
const { identifier: uri } = res.data;
|
||||
addClanURI(uri);
|
||||
setActiveClanURI(uri);
|
||||
return uri;
|
||||
}
|
||||
|
||||
throw new Error("Illegal state exception");
|
||||
};
|
||||
|
||||
export const navigateToClan = (navigate: Navigator, uri: string) => {
|
||||
navigate("/clan/" + window.btoa(uri));
|
||||
};
|
||||
|
||||
export const clanURIParam = (params: Params) => {
|
||||
return window.atob(params.clanURI);
|
||||
};
|
||||
@@ -3,7 +3,9 @@ import { render } from "solid-js/web";
|
||||
|
||||
import "./index.css";
|
||||
import { QueryClient } from "@tanstack/solid-query";
|
||||
import { CubeScene } from "./scene/cubes";
|
||||
import { Routes } from "@/src/routes";
|
||||
import { Router } from "@solidjs/router";
|
||||
import { Layout } from "@/src/routes/Layout";
|
||||
|
||||
export const client = new QueryClient();
|
||||
|
||||
@@ -20,4 +22,4 @@ if (import.meta.env.DEV) {
|
||||
await import("solid-devtools");
|
||||
}
|
||||
|
||||
render(() => <CubeScene />, root!);
|
||||
render(() => <Router root={Layout}>{Routes}</Router>, root!);
|
||||
|
||||
10
pkgs/clan-app/ui/src/routes/Clan/Clan.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { RouteSectionProps, useParams } from "@solidjs/router";
|
||||
import { Component } from "solid-js";
|
||||
import { clanURIParam } from "@/src/hooks/clan";
|
||||
import { CubeScene } from "@/src/scene/cubes";
|
||||
|
||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||
const params = useParams();
|
||||
const clanURI = clanURIParam(params);
|
||||
return <CubeScene />;
|
||||
};
|
||||
18
pkgs/clan-app/ui/src/routes/Layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Component } from "solid-js";
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
import { activeClanURI } from "@/src/stores/clan";
|
||||
import { navigateToClan } from "@/src/hooks/clan";
|
||||
|
||||
export const Layout: Component<RouteSectionProps> = (props) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// check for an active clan uri and redirect to it on first load
|
||||
const activeURI = activeClanURI();
|
||||
if (!props.location.pathname.startsWith("/clan/") && activeURI) {
|
||||
navigateToClan(navigate, activeURI);
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
return <div class="size-full h-screen">{props.children}</div>;
|
||||
};
|
||||
90
pkgs/clan-app/ui/src/routes/Onboarding/Onboarding.css
Normal file
@@ -0,0 +1,90 @@
|
||||
main#welcome {
|
||||
@apply absolute top-0 left-0;
|
||||
@apply flex items-center justify-center;
|
||||
@apply min-h-screen w-full;
|
||||
|
||||
div.background {
|
||||
.layer-1 {
|
||||
@apply -z-30;
|
||||
background:
|
||||
url("./background.png") 0 -69.032px / 100% 119.049% no-repeat,
|
||||
url("./background.png") 50% / cover no-repeat;
|
||||
}
|
||||
|
||||
.layer-2 {
|
||||
@apply -z-20;
|
||||
background: #103131;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.layer-3 {
|
||||
@apply -z-10;
|
||||
background: #749095;
|
||||
mix-blend-mode: soft-light;
|
||||
}
|
||||
|
||||
.layer-1,
|
||||
.layer-2,
|
||||
.layer-3 {
|
||||
@apply absolute top-0 left-0 w-full h-full;
|
||||
}
|
||||
|
||||
svg[data-logo-name="Darknet"] {
|
||||
@apply w-52;
|
||||
@apply absolute top-28 left-1/2 transform -translate-x-1/2;
|
||||
}
|
||||
|
||||
svg[data-logo-name="Clan"] {
|
||||
@apply w-16;
|
||||
@apply absolute bottom-28 left-1/2 transform -translate-x-1/2;
|
||||
}
|
||||
|
||||
div.darknet-info {
|
||||
@apply absolute bottom-[6.5rem] left-12;
|
||||
@apply flex flex-col gap-y-2;
|
||||
|
||||
span.darknet-label {
|
||||
color: theme(colors.off.darknet_label);
|
||||
}
|
||||
|
||||
span.darknet-name {
|
||||
color: theme(colors.off.darknet_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > div.container {
|
||||
@apply flex flex-col items-center justify-evenly gap-y-20;
|
||||
@apply size-fit;
|
||||
|
||||
& > div.welcome {
|
||||
@apply flex flex-col min-w-80 gap-y-6;
|
||||
|
||||
& > div.separator {
|
||||
@apply grid grid-cols-3 grid-rows-1 gap-x-4 items-center;
|
||||
}
|
||||
}
|
||||
|
||||
& > div.setup {
|
||||
@apply flex flex-col min-w-[520px] gap-y-5;
|
||||
@apply pt-10 px-8 pb-8 bg-def-1 rounded-lg;
|
||||
|
||||
& > div.header {
|
||||
@apply flex items-center justify-start gap-x-2;
|
||||
}
|
||||
|
||||
form {
|
||||
@apply flex flex-col gap-y-5;
|
||||
|
||||
& > div.form-controls {
|
||||
@apply flex justify-end pt-6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.creating {
|
||||
@apply w-[17.0625rem] h-[20.4375rem];
|
||||
background: url(./cube.svg) center / cover no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
380
pkgs/clan-app/ui/src/routes/Onboarding/Onboarding.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
import {
|
||||
Accessor,
|
||||
Component,
|
||||
createSignal,
|
||||
Match,
|
||||
Setter,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
import "./Onboarding.css";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
||||
import { Alert } from "@/src/components/Alert/Alert";
|
||||
|
||||
import { Divider } from "@/src/components/Divider/Divider";
|
||||
import { Logo } from "@/src/components/Logo/Logo";
|
||||
import { navigateToClan, selectClanFolder } from "@/src/hooks/clan";
|
||||
import { activeClanURI, addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
||||
import {
|
||||
createForm,
|
||||
FormStore,
|
||||
getError,
|
||||
getErrors,
|
||||
getValue,
|
||||
SubmitHandler,
|
||||
valiForm,
|
||||
} from "@modular-forms/solid";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/Form/TextArea";
|
||||
import { Fieldset } from "@/src/components/Form/Fieldset";
|
||||
import * as v from "valibot";
|
||||
import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
|
||||
type State = "welcome" | "setup" | "creating";
|
||||
|
||||
const SetupSchema = v.object({
|
||||
name: v.pipe(
|
||||
v.string(),
|
||||
v.nonEmpty("Please enter a name."),
|
||||
v.regex(
|
||||
new RegExp("^[a-zA-Z0-9_\\-]+$"),
|
||||
"Name must be alphanumeric and can contain underscores and dashes, without spaces.",
|
||||
),
|
||||
),
|
||||
description: v.pipe(v.string(), v.nonEmpty("Please describe your clan.")),
|
||||
directory: v.pipe(
|
||||
// initial value is undefined, and I can't see how to handle this better in valibot, so for now when the type
|
||||
// is incorrect we treat it as empty
|
||||
v.string("Please select a directory."),
|
||||
v.nonEmpty("Please select a directory."),
|
||||
),
|
||||
});
|
||||
|
||||
type SetupForm = v.InferInput<typeof SetupSchema>;
|
||||
|
||||
const background = (props: { state: State; form: FormStore<SetupForm> }) => (
|
||||
<div class="background">
|
||||
<div class="layer-1" />
|
||||
<div class="layer-2" />
|
||||
<div class="layer-3" />
|
||||
<Logo variant="Darknet" inverted={true} />
|
||||
<Logo variant="Clan" inverted={true} />
|
||||
<Show when={props.state === "setup"}>
|
||||
<div class="darknet-info">
|
||||
<Typography
|
||||
class="darknet-label"
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="default"
|
||||
color="inherit"
|
||||
weight="medium"
|
||||
inverted={true}
|
||||
>
|
||||
Your Darknet:
|
||||
</Typography>
|
||||
<Typography
|
||||
class="darknet-name"
|
||||
hierarchy="teaser"
|
||||
size="default"
|
||||
color="inherit"
|
||||
weight="medium"
|
||||
inverted={true}
|
||||
>
|
||||
{getValue(props.form, "name")}
|
||||
</Typography>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
|
||||
const welcome = (props: {
|
||||
setState: Setter<State>;
|
||||
welcomeError: Accessor<string | undefined>;
|
||||
setWelcomeError: Setter<string | undefined>;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const selectFolder = async () => {
|
||||
const uri = await selectClanFolder();
|
||||
navigateToClan(navigate, uri);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="welcome">
|
||||
<Typography
|
||||
hierarchy="headline"
|
||||
size="xxl"
|
||||
weight="bold"
|
||||
align="center"
|
||||
inverted={true}
|
||||
>
|
||||
Build your <br />
|
||||
own darknet
|
||||
</Typography>
|
||||
{props.welcomeError() && (
|
||||
<Alert
|
||||
type="error"
|
||||
icon="Info"
|
||||
title="Your Clan creation failed"
|
||||
description={props.welcomeError() || ""}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
onClick={() => {
|
||||
// reset welcome error
|
||||
props.setWelcomeError(undefined);
|
||||
// move to next step
|
||||
props.setState("setup");
|
||||
}}
|
||||
>
|
||||
Start building
|
||||
</Button>
|
||||
<div class="separator">
|
||||
<Divider orientation="horizontal" inverted={true} />
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="s"
|
||||
weight="medium"
|
||||
inverted={true}
|
||||
align="center"
|
||||
>
|
||||
or
|
||||
</Typography>
|
||||
<Divider orientation="horizontal" inverted={true} />
|
||||
</div>
|
||||
<Button hierarchy="primary" ghost={true} onAction={selectFolder}>
|
||||
Select folder
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const creating = () => (
|
||||
<div class="animate-pulse">
|
||||
<Tooltip
|
||||
open={true}
|
||||
placement="top"
|
||||
animation="bounce"
|
||||
trigger={<div class="creating" />}
|
||||
>
|
||||
<Typography hierarchy="body" size="xs" weight="medium" inverted={true}>
|
||||
Your Clan is being created
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const activeURI = activeClanURI();
|
||||
if (activeURI) {
|
||||
// the user has already selected a clan, so we should navigate to it
|
||||
console.log("active clan detected, navigating to it", activeURI);
|
||||
navigateToClan(navigate, activeURI);
|
||||
}
|
||||
|
||||
const [state, setState] = createSignal<State>("welcome");
|
||||
|
||||
// used to display an error in the welcome screen in the event of a failed
|
||||
// clan creation
|
||||
const [welcomeError, setWelcomeError] = createSignal<string | undefined>();
|
||||
|
||||
//
|
||||
const [setupForm, { Form, Field }] = createForm<SetupForm>({
|
||||
validate: valiForm(SetupSchema),
|
||||
});
|
||||
|
||||
const formError = () => {
|
||||
const formErrors = getErrors(setupForm);
|
||||
return (
|
||||
formErrors.name ||
|
||||
formErrors.description ||
|
||||
formErrors.directory ||
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
||||
const onSelectFile = async () => {
|
||||
const req = callApi("get_system_file", {
|
||||
file_request: {
|
||||
mode: "select_folder",
|
||||
title: "Select a folder for you new Clan",
|
||||
},
|
||||
});
|
||||
|
||||
const resp = await req.result;
|
||||
|
||||
if (resp.status === "error") {
|
||||
// just throw the first error, I can't imagine why there would be multiple
|
||||
// errors for this call
|
||||
throw new Error(resp.errors[0].message);
|
||||
}
|
||||
|
||||
if (resp.status === "success" && resp.data) {
|
||||
return resp.data[0];
|
||||
}
|
||||
|
||||
throw new Error("No data returned from api call");
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<SetupForm> = async (
|
||||
{ name, description, directory },
|
||||
event,
|
||||
) => {
|
||||
const path = `${directory}/${name}`;
|
||||
|
||||
const req = callApi("create_clan", {
|
||||
opts: {
|
||||
dest: path,
|
||||
// todo allow users to select a template
|
||||
template: "minimal",
|
||||
initial: {
|
||||
meta: {
|
||||
name: name,
|
||||
description: description,
|
||||
// todo it tries to 'delete' icon if it's not provided
|
||||
// this logic is unexpected, and needs reviewed.
|
||||
icon: null,
|
||||
},
|
||||
machines: {},
|
||||
instances: {},
|
||||
services: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setState("creating");
|
||||
|
||||
const resp = await req.result;
|
||||
|
||||
if (resp.status === "error") {
|
||||
setWelcomeError(resp.errors[0].message);
|
||||
setState("welcome");
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp.status === "success") {
|
||||
addClanURI(path);
|
||||
setActiveClanURI(path);
|
||||
navigateToClan(navigate, path);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main id="welcome">
|
||||
{background({ form: setupForm, state: state() })}
|
||||
<div class="container">
|
||||
<Switch>
|
||||
<Match when={state() === "welcome"}>
|
||||
{welcome({
|
||||
setState,
|
||||
welcomeError,
|
||||
setWelcomeError,
|
||||
})}
|
||||
</Match>
|
||||
|
||||
<Match when={state() === "setup"}>
|
||||
<div class="setup">
|
||||
<div class="header">
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
ghost={true}
|
||||
icon="ArrowLeft"
|
||||
onClick={() => setState("welcome")}
|
||||
/>
|
||||
<Typography hierarchy="headline" size="default" weight="bold">
|
||||
Setup
|
||||
</Typography>
|
||||
</div>
|
||||
<Form onSubmit={onSubmit}>
|
||||
{formError() && (
|
||||
<Alert
|
||||
type="error"
|
||||
icon="Info"
|
||||
title="Form error"
|
||||
description={formError() || ""}
|
||||
/>
|
||||
)}
|
||||
<Fieldset name="meta">
|
||||
<Field name="name">
|
||||
{(field, input) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label="Name"
|
||||
value={field.value}
|
||||
required
|
||||
orientation="horizontal"
|
||||
validationState={
|
||||
getError(setupForm, "name") ? "invalid" : "valid"
|
||||
}
|
||||
input={{
|
||||
...input,
|
||||
placeholder: "Name your Clan",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Divider inverted={true} />
|
||||
<Field name="description">
|
||||
{(field, input) => (
|
||||
<TextArea
|
||||
{...field}
|
||||
value={field.value}
|
||||
label="Description"
|
||||
required
|
||||
orientation="horizontal"
|
||||
validationState={
|
||||
getError(setupForm, "description")
|
||||
? "invalid"
|
||||
: "valid"
|
||||
}
|
||||
input={input}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset name="location">
|
||||
<Field name="directory">
|
||||
{(field, input) => (
|
||||
<HostFileInput
|
||||
onSelectFile={onSelectFile}
|
||||
{...field}
|
||||
value={field.value}
|
||||
label="Select directory"
|
||||
orientation="horizontal"
|
||||
required={true}
|
||||
validationState={
|
||||
getError(setupForm, "directory") ? "invalid" : "valid"
|
||||
}
|
||||
input={input}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
|
||||
<div class="form-controls">
|
||||
<Button
|
||||
type="submit"
|
||||
hierarchy="primary"
|
||||
endIcon="ArrowRight"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={state() === "creating"}>{creating()}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
BIN
pkgs/clan-app/ui/src/routes/Onboarding/background.png
Normal file
|
After Width: | Height: | Size: 289 KiB |
316
pkgs/clan-app/ui/src/routes/Onboarding/cube.svg
Normal file
@@ -0,0 +1,316 @@
|
||||
<svg width="273" height="327" viewBox="0 0 273 327" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 137.399 89.7148)" fill="url(#paint0_linear_4542_12187)" stroke="url(#paint1_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.8281 125.194)" fill="url(#paint2_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.8281 125.194)" fill="url(#paint3_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.8281 125.194)" stroke="url(#paint4_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.326 160.673)" fill="url(#paint5_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.326 160.673)" fill="url(#paint6_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.326 160.673)" stroke="url(#paint7_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 62.5708 142.85)" fill="url(#paint8_linear_4542_12187)" stroke="url(#paint9_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 178.329)" fill="url(#paint10_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 178.329)" fill="url(#paint11_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 178.329)" stroke="url(#paint12_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 213.808)" fill="url(#paint13_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 213.808)" fill="url(#paint14_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 213.808)" stroke="url(#paint15_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499715 -0.86619 0.499715 210.502 137.882)" fill="url(#paint16_linear_4542_12187)" stroke="url(#paint17_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 173.361)" fill="url(#paint18_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 173.361)" fill="url(#paint19_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 173.361)" stroke="url(#paint20_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 208.84)" fill="url(#paint21_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 208.84)" fill="url(#paint22_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 208.84)" stroke="url(#paint23_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 137.158 185.084)" fill="url(#paint24_linear_4542_12187)" stroke="url(#paint25_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 220.563)" fill="url(#paint26_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 220.563)" fill="url(#paint27_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 220.563)" stroke="url(#paint28_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 256.042)" fill="url(#paint29_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 256.042)" fill="url(#paint30_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 256.042)" stroke="url(#paint31_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 135.915 0)" fill="url(#paint32_linear_4542_12187)" stroke="url(#paint33_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 74.3442 35.479)" fill="url(#paint34_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 74.3442 35.479)" fill="url(#paint35_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 74.3442 35.479)" stroke="url(#paint36_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 135.842 70.9579)" fill="url(#paint37_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 135.842 70.9579)" fill="url(#paint38_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 135.842 70.9579)" stroke="url(#paint39_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 62.5708 47.2027)" fill="url(#paint40_linear_4542_12187)" stroke="url(#paint41_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 82.6817)" fill="url(#paint42_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 82.6817)" fill="url(#paint43_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 1 82.6817)" stroke="url(#paint44_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 118.161)" fill="url(#paint45_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 118.161)" fill="url(#paint46_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 62.4976 118.161)" stroke="url(#paint47_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499715 -0.86619 0.499715 210.502 42.234)" fill="url(#paint48_linear_4542_12187)" stroke="url(#paint49_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 77.713)" fill="url(#paint50_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 77.713)" fill="url(#paint51_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 148.931 77.713)" stroke="url(#paint52_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 113.192)" fill="url(#paint53_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 113.192)" fill="url(#paint54_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499715 0 1 210.429 113.192)" stroke="url(#paint55_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9982" rx="1" transform="matrix(0.86619 0.499716 -0.86619 0.499716 137.158 89.4367)" fill="url(#paint56_linear_4542_12187)" stroke="url(#paint57_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 124.916)" fill="url(#paint58_linear_4542_12187)" fill-opacity="0.1"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 124.916)" fill="url(#paint59_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 0.499716 0 1 75.5869 124.916)" stroke="url(#paint60_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 160.395)" fill="url(#paint61_linear_4542_12187)"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 160.395)" fill="url(#paint62_linear_4542_12187)" fill-opacity="0.24"/>
|
||||
<rect width="70.9982" height="70.9578" rx="1" transform="matrix(0.86619 -0.499716 0 1 137.084 160.395)" stroke="url(#paint63_linear_4542_12187)" stroke-width="0.64"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.94"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.81"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.94"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint13_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.81"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint14_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint15_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint16_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.94"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint17_linear_4542_12187" x1="73.129" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint18_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint19_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42737" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint20_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint21_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.81"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint22_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint23_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint24_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.94"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint25_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint26_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint27_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint28_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint29_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.81"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint30_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1743" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint31_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint32_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.94"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint33_linear_4542_12187" x1="73.129" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint34_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint35_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint36_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint37_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.81"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint38_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint39_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint40_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.94"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint41_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint42_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint43_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint44_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint45_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.81"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint46_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint47_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint48_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.94"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint49_linear_4542_12187" x1="73.129" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint50_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint51_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint52_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint53_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.81"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint54_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint55_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint56_linear_4542_12187" x1="3.21525" y1="1.26987" x2="26.2123" y2="73.9372" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.94"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint57_linear_4542_12187" x1="73.1289" y1="21.7256" x2="19.1537" y2="-1.08702" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint58_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint59_linear_4542_12187" x1="71.0824" y1="68.7651" x2="43.5492" y2="9.42738" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint60_linear_4542_12187" x1="73.129" y1="21.7132" x2="19.163" y2="-1.10843" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint61_linear_4542_12187" x1="3.21525" y1="1.26914" x2="26.1885" y2="73.9026" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.81"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint62_linear_4542_12187" x1="22.5463" y1="67.3059" x2="51.7875" y2="28.1744" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#40A2A6"/>
|
||||
<stop offset="1" stop-color="#91ACAF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint63_linear_4542_12187" x1="73.1289" y1="21.7132" x2="19.163" y2="-1.10842" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10191A"/>
|
||||
<stop offset="1" stop-color="#2C4547"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 24 KiB |
14
pkgs/clan-app/ui/src/routes/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { RouteDefinition } from "@solidjs/router/dist/types";
|
||||
import { Onboarding } from "@/src/routes/Onboarding/Onboarding";
|
||||
import { Clan } from "@/src/routes/Clan/Clan";
|
||||
|
||||
export const Routes: RouteDefinition[] = [
|
||||
{
|
||||
path: "/",
|
||||
component: Onboarding,
|
||||
},
|
||||
{
|
||||
path: "/clan/:clanURI",
|
||||
component: Clan,
|
||||
},
|
||||
];
|
||||
15
pkgs/clan-app/ui/src/scene/cubes.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.cubes-scene-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
15
pkgs/clan-app/ui/src/scene/cubes.stories.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { CubeScene } from "./cubes";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "scene/cubes",
|
||||
component: CubeScene,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
88
pkgs/clan-app/ui/src/stores/clan.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { createStore, produce } from "solid-js/store";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
|
||||
interface ClanStoreType {
|
||||
clanURIs: string[];
|
||||
activeClanURI?: string;
|
||||
}
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<ClanStoreType>({
|
||||
clanURIs: [],
|
||||
}),
|
||||
{
|
||||
name: "clanStore",
|
||||
storage: localStorage,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the active clan URI from the store.
|
||||
*
|
||||
* @function
|
||||
* @returns {string} The URI of the active clan.
|
||||
*/
|
||||
const activeClanURI = (): string | undefined => store.activeClanURI;
|
||||
|
||||
/**
|
||||
* Updates the active Clan URI in the store.
|
||||
*
|
||||
* @param {string} uri - The URI to be set as the active Clan URI.
|
||||
*/
|
||||
const setActiveClanURI = (uri: string) => setStore("activeClanURI", uri);
|
||||
|
||||
/**
|
||||
* Retrieves the current list of clan URIs from the store.
|
||||
*
|
||||
* @function clanURIs
|
||||
* @returns {*} The clan URIs from the store.
|
||||
*/
|
||||
const clanURIs = (): string[] => store.clanURIs;
|
||||
|
||||
/**
|
||||
* Adds a new clan URI to the list of clan URIs in the store.
|
||||
*
|
||||
* @param {string} uri - The URI of the clan to be added.
|
||||
*
|
||||
*/
|
||||
const addClanURI = (uri: string) =>
|
||||
setStore("clanURIs", store.clanURIs.length, uri);
|
||||
|
||||
/**
|
||||
* Removes a specified URI from the clan URI list and updates the active clan URI.
|
||||
*
|
||||
* This function modifies the store in the following ways:
|
||||
* - Removes the specified URI from the `clanURIs` array.
|
||||
* - Clears the `activeClanURI` if the removed URI matches the currently active URI.
|
||||
* - Sets a new active clan URI to the last URI in the `clanURIs` array if the active clan URI is undefined
|
||||
* and there are remaining clan URIs in the list.
|
||||
*
|
||||
* @param {string} uri - The URI to be removed from the clan list.
|
||||
*/
|
||||
const removeClanURI = (uri: string) => {
|
||||
setStore(
|
||||
produce((state) => {
|
||||
// remove from the clan list
|
||||
state.clanURIs = state.clanURIs.filter((el) => el !== uri);
|
||||
|
||||
// clear active clan uri if it's the one being removed
|
||||
if (state.activeClanURI === uri) {
|
||||
state.activeClanURI = undefined;
|
||||
}
|
||||
|
||||
// select a new active URI if at least one remains
|
||||
if (!state.activeClanURI && state.clanURIs.length > 0) {
|
||||
state.activeClanURI = state.clanURIs[state.clanURIs.length - 1];
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
store,
|
||||
activeClanURI,
|
||||
setActiveClanURI,
|
||||
clanURIs,
|
||||
addClanURI,
|
||||
removeClanURI,
|
||||
};
|
||||
@@ -11,6 +11,9 @@ const primaries = {
|
||||
off: {
|
||||
white: toRGB("#ffffff"),
|
||||
black: toRGB("#000000"),
|
||||
darknet_name: toRGB("#00ff57"),
|
||||
darknet_label: toRGB("#2cff74"),
|
||||
toolbar_border: toRGB("#2e4a4b"),
|
||||
},
|
||||
primary: {
|
||||
50: toRGB("#f4f9f9"),
|
||||
|
||||
@@ -2,7 +2,7 @@ import argparse
|
||||
import logging
|
||||
|
||||
from clan_lib.backups.create import create_backup
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
|
||||
from clan_cli.completions import (
|
||||
@@ -15,10 +15,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
flake = require_flake(args.flake)
|
||||
machine = Machine(name=args.machine, flake=flake)
|
||||
create_backup(machine=machine, provider=args.provider)
|
||||
|
||||
|
||||
|
||||
15
pkgs/clan-cli/clan_cli/backups/create_test.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
def test_create_command_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["backups", "create", "machine1"])
|
||||
@@ -1,7 +1,7 @@
|
||||
import argparse
|
||||
|
||||
from clan_lib.backups.list import list_backups
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
|
||||
from clan_cli.completions import (
|
||||
@@ -12,11 +12,8 @@ from clan_cli.completions import (
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
flake = require_flake(args.flake)
|
||||
machine = Machine(name=args.machine, flake=flake)
|
||||
backups = list_backups(machine=machine, provider=args.provider)
|
||||
for backup in backups:
|
||||
print(backup.name)
|
||||
|
||||
13
pkgs/clan-cli/clan_cli/backups/list_test.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
def test_list_command_no_flake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["backups", "list", "machine1"])
|
||||
@@ -1,7 +1,7 @@
|
||||
import argparse
|
||||
|
||||
from clan_lib.backups.restore import restore_backup
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
|
||||
from clan_cli.completions import (
|
||||
@@ -12,10 +12,8 @@ from clan_cli.completions import (
|
||||
|
||||
|
||||
def restore_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
flake = require_flake(args.flake)
|
||||
machine = Machine(name=args.machine, flake=flake)
|
||||
restore_backup(
|
||||
machine=machine,
|
||||
provider=args.provider,
|
||||
|
||||
15
pkgs/clan-cli/clan_cli/backups/restore_test.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
def test_restore_command_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["backups", "restore", "machine1", "provider1", "backup1"])
|
||||
@@ -2,13 +2,13 @@ import argparse
|
||||
import logging
|
||||
|
||||
from clan_lib.clan.get import get_clan_details
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.flake import require_flake
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def show_command(args: argparse.Namespace) -> None:
|
||||
flake: Flake = args.flake
|
||||
flake = require_flake(args.flake)
|
||||
meta = get_clan_details(flake)
|
||||
|
||||
print(f"Name: {meta.get('name')}")
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.tests.fixtures_flakes import FlakeForTest
|
||||
from clan_cli.tests.helpers import cli
|
||||
@@ -14,3 +17,19 @@ def test_clan_show(
|
||||
assert "Name:" in output.out
|
||||
assert "Name: test_flake_with_core" in output.out
|
||||
assert "Description:" in output.out
|
||||
|
||||
|
||||
def test_clan_show_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capture_output: CaptureOutput
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError) as exc_info:
|
||||
cli.run(["show"])
|
||||
|
||||
assert "No clan flake found in the current directory or its parents" in str(
|
||||
exc_info.value
|
||||
)
|
||||
assert "Use the --flake flag to specify a clan flake path or URL" in str(
|
||||
exc_info.value
|
||||
)
|
||||
|
||||
@@ -250,12 +250,12 @@ This subcommand allows seamless ssh access to the nixos-image builders or a mach
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan ssh [ssh_args ...] berlin`
|
||||
$ clan ssh berlin
|
||||
|
||||
Will ssh in to the machine called `berlin`, using the
|
||||
`clan.core.networking.targetHost` specified in its configuration
|
||||
|
||||
$ clan ssh [ssh_args ...] --json [JSON]
|
||||
$ clan ssh --json [JSON] --host-key-check none
|
||||
Will ssh in to the machine based on the deployment information contained in
|
||||
the json string. [JSON] can either be a json formatted string itself, or point
|
||||
towards a file containing the deployment information
|
||||
@@ -297,6 +297,8 @@ For more detailed information, visit: {help_hyperlink("secrets", "https://docs.c
|
||||
description="Manage facts",
|
||||
epilog=(
|
||||
f"""
|
||||
Note: Facts are being deprecated, please use Vars instead.
|
||||
For a migration guide visit: {help_hyperlink("vars", "https://docs.clan.lol/guides/migrations/migration-facts-vars")}
|
||||
|
||||
This subcommand provides an interface to facts of clan machines.
|
||||
Facts are artifacts that a service can generate.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
@@ -37,9 +38,10 @@ def check_secrets(machine: Machine, service: None | str = None) -> bool:
|
||||
|
||||
|
||||
def check_command(args: argparse.Namespace) -> None:
|
||||
flake = require_flake(args.flake)
|
||||
machine = Machine(
|
||||
name=args.machine,
|
||||
flake=args.flake,
|
||||
flake=flake,
|
||||
)
|
||||
check_secrets(machine, service=args.service)
|
||||
|
||||
|
||||
15
pkgs/clan-cli/clan_cli/facts/check_test.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
def test_check_command_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["facts", "check", "machine1"])
|
||||
@@ -9,6 +9,7 @@ from tempfile import TemporaryDirectory
|
||||
|
||||
from clan_lib.cmd import RunOpts, run
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.git import commit_files
|
||||
from clan_lib.machines.list import list_full_machines
|
||||
from clan_lib.machines.machines import Machine
|
||||
@@ -223,11 +224,8 @@ def generate_facts(
|
||||
|
||||
|
||||
def generate_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
|
||||
machines: list[Machine] = list(list_full_machines(args.flake).values())
|
||||
flake = require_flake(args.flake)
|
||||
machines: list[Machine] = list(list_full_machines(flake).values())
|
||||
if len(args.machines) > 0:
|
||||
machines = list(
|
||||
filter(
|
||||
|
||||
15
pkgs/clan-cli/clan_cli/facts/generate_test.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
def test_generate_command_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["facts", "generate"])
|
||||
@@ -2,6 +2,7 @@ import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
@@ -10,7 +11,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_command(args: argparse.Namespace) -> None:
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
flake = require_flake(args.flake)
|
||||
machine = Machine(name=args.machine, flake=flake)
|
||||
|
||||
# the raw_facts are bytestrings making them not json serializable
|
||||
raw_facts = machine.public_facts_store.get_all()
|
||||
|
||||
13
pkgs/clan-cli/clan_cli/facts/list_test.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
def test_list_command_no_flake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["facts", "list", "machine1"])
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
from clan_lib.ssh.remote import Remote
|
||||
|
||||
@@ -25,7 +26,8 @@ def upload_secrets(machine: Machine, host: Remote) -> None:
|
||||
|
||||
|
||||
def upload_command(args: argparse.Namespace) -> None:
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
flake = require_flake(args.flake)
|
||||
machine = Machine(name=args.machine, flake=flake)
|
||||
with machine.target_host().ssh_control_master() as host:
|
||||
upload_secrets(machine, host)
|
||||
|
||||
|
||||
15
pkgs/clan-cli/clan_cli/facts/upload_test.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
def test_upload_command_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["facts", "upload", "machine1"])
|
||||
@@ -7,12 +7,11 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.flash.flash import Disk, SystemConfig, run_machine_flash
|
||||
from clan_lib.machines.machines import Machine
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
|
||||
from .flash import Disk, SystemConfig, run_machine_flash
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -1,79 +1,11 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TypedDict
|
||||
|
||||
from clan_lib.api import API
|
||||
from clan_lib.cmd import Log, RunOpts, run
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.nix import nix_build
|
||||
from clan_lib.flash.list import list_keymaps, list_languages
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FlashOptions(TypedDict):
|
||||
languages: list[str]
|
||||
keymaps: list[str]
|
||||
|
||||
|
||||
@API.register
|
||||
def get_machine_flash_options() -> FlashOptions:
|
||||
"""Retrieve available languages and keymaps for flash configuration.
|
||||
Returns:
|
||||
FlashOptions: A dictionary containing lists of available languages and keymaps.
|
||||
Raises:
|
||||
ClanError: If the locale file or keymaps directory does not exist.
|
||||
"""
|
||||
return {"languages": list_languages(), "keymaps": list_keymaps()}
|
||||
|
||||
|
||||
def list_languages() -> list[str]:
|
||||
cmd = nix_build(["nixpkgs#glibcLocales"])
|
||||
result = run(cmd, RunOpts(log=Log.STDERR, error_msg="Failed to find glibc locales"))
|
||||
locale_file = Path(result.stdout.strip()) / "share" / "i18n" / "SUPPORTED"
|
||||
|
||||
if not locale_file.exists():
|
||||
msg = f"Locale file '{locale_file}' does not exist."
|
||||
raise ClanError(msg)
|
||||
|
||||
with locale_file.open() as f:
|
||||
lines = f.readlines()
|
||||
|
||||
languages = []
|
||||
for line in lines:
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
if "SUPPORTED-LOCALES" in line:
|
||||
continue
|
||||
# Split by '/' and take the first part
|
||||
language = line.split("/")[0].strip()
|
||||
languages.append(language)
|
||||
|
||||
return languages
|
||||
|
||||
|
||||
def list_keymaps() -> list[str]:
|
||||
cmd = nix_build(["nixpkgs#kbd"])
|
||||
result = run(cmd, RunOpts(log=Log.STDERR, error_msg="Failed to find kbdinfo"))
|
||||
keymaps_dir = Path(result.stdout.strip()) / "share" / "keymaps"
|
||||
|
||||
if not keymaps_dir.exists():
|
||||
msg = f"Keymaps directory '{keymaps_dir}' does not exist."
|
||||
raise ClanError(msg)
|
||||
|
||||
keymap_files = []
|
||||
|
||||
for _root, _, files in os.walk(keymaps_dir):
|
||||
for file in files:
|
||||
if file.endswith(".map.gz"):
|
||||
# Remove '.map.gz' ending
|
||||
name_without_ext = file[:-7]
|
||||
keymap_files.append(name_without_ext)
|
||||
|
||||
return keymap_files
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
if args.cmd == "languages":
|
||||
languages = list_languages()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import get_args
|
||||
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.hardware import (
|
||||
HardwareConfig,
|
||||
HardwareGenerateOptions,
|
||||
@@ -9,6 +11,7 @@ from clan_lib.machines.hardware import (
|
||||
)
|
||||
from clan_lib.machines.machines import Machine
|
||||
from clan_lib.machines.suggestions import validate_machine_names
|
||||
from clan_lib.ssh.host_key import HostKeyCheck
|
||||
from clan_lib.ssh.remote import Remote
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
@@ -19,8 +22,9 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def update_hardware_config_command(args: argparse.Namespace) -> None:
|
||||
validate_machine_names([args.machine], args.flake)
|
||||
machine = Machine(flake=args.flake, name=args.machine)
|
||||
flake = require_flake(args.flake)
|
||||
validate_machine_names([args.machine], flake)
|
||||
machine = Machine(flake=flake, name=args.machine)
|
||||
opts = HardwareGenerateOptions(
|
||||
machine=machine,
|
||||
password=args.password,
|
||||
@@ -57,7 +61,7 @@ def register_update_hardware_config(parser: argparse.ArgumentParser) -> None:
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host-key-check",
|
||||
choices=["strict", "ask", "tofu", "none"],
|
||||
choices=list(get_args(HostKeyCheck)),
|
||||
default="ask",
|
||||
help="Host key (.ssh/known_hosts) check mode.",
|
||||
)
|
||||
|
||||
15
pkgs/clan-cli/clan_cli/machines/hardware_test.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
def test_create_command_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["machines", "update-hardware-config", "machine"])
|
||||
@@ -2,10 +2,13 @@ import argparse
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import get_args
|
||||
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.install import BuildOn, InstallOptions, run_machine_install
|
||||
from clan_lib.machines.machines import Machine
|
||||
from clan_lib.ssh.host_key import HostKeyCheck
|
||||
from clan_lib.ssh.remote import Remote
|
||||
|
||||
from clan_cli.completions import (
|
||||
@@ -21,15 +24,18 @@ log = logging.getLogger(__name__)
|
||||
|
||||
def install_command(args: argparse.Namespace) -> None:
|
||||
try:
|
||||
flake = require_flake(args.flake)
|
||||
# Only if the caller did not specify a target_host via args.target_host
|
||||
# Find a suitable target_host that is reachable
|
||||
target_host_str = args.target_host
|
||||
deploy_info: DeployInfo | None = ssh_command_parse(args)
|
||||
deploy_info: DeployInfo | None = (
|
||||
ssh_command_parse(args) if target_host_str is None else None
|
||||
)
|
||||
|
||||
use_tor = False
|
||||
if deploy_info and not args.target_host:
|
||||
if deploy_info:
|
||||
host = find_reachable_host(deploy_info)
|
||||
if host is None:
|
||||
if host is None or host.tor_socks:
|
||||
use_tor = True
|
||||
target_host_str = deploy_info.tor.target
|
||||
else:
|
||||
@@ -42,7 +48,7 @@ def install_command(args: argparse.Namespace) -> None:
|
||||
else:
|
||||
password = None
|
||||
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
machine = Machine(name=args.machine, flake=flake)
|
||||
host_key_check = args.host_key_check
|
||||
|
||||
if target_host_str is not None:
|
||||
@@ -56,15 +62,20 @@ def install_command(args: argparse.Namespace) -> None:
|
||||
msg = "Installing macOS machines is not yet supported"
|
||||
raise ClanError(msg)
|
||||
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
|
||||
if not args.yes:
|
||||
ask = input(f"Install {args.machine} to {target_host.target}? [y/N] ")
|
||||
if ask != "y":
|
||||
return None
|
||||
|
||||
if args.identity_file:
|
||||
target_host = target_host.override(private_key=args.identity_file)
|
||||
|
||||
if password:
|
||||
target_host = target_host.override(password=password)
|
||||
|
||||
if use_tor:
|
||||
target_host = target_host.override(tor_socks=True)
|
||||
|
||||
return run_machine_install(
|
||||
InstallOptions(
|
||||
machine=machine,
|
||||
@@ -72,11 +83,8 @@ def install_command(args: argparse.Namespace) -> None:
|
||||
phases=args.phases,
|
||||
debug=args.debug,
|
||||
no_reboot=args.no_reboot,
|
||||
build_on=BuildOn(args.build_on) if args.build_on is not None else None,
|
||||
build_on=args.build_on if args.build_on is not None else None,
|
||||
update_hardware_config=HardwareConfig(args.update_hardware_config),
|
||||
password=password,
|
||||
identity_file=args.identity_file,
|
||||
use_tor=use_tor,
|
||||
),
|
||||
target_host=target_host,
|
||||
)
|
||||
@@ -99,13 +107,14 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host-key-check",
|
||||
choices=["strict", "ask", "tofu", "none"],
|
||||
choices=list(get_args(HostKeyCheck)),
|
||||
default="ask",
|
||||
help="Host key (.ssh/known_hosts) check mode.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--build-on",
|
||||
choices=[x.value for x in BuildOn],
|
||||
choices=list(get_args(BuildOn)),
|
||||
default=None,
|
||||
help="where to build the NixOS configuration",
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from typing import get_args
|
||||
|
||||
from clan_lib.async_run import AsyncContext, AsyncOpts, AsyncRuntime
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.flake.flake import Flake
|
||||
from clan_lib.machines.actions import list_machines
|
||||
from clan_lib.machines.list import instantiate_inventory_to_machines
|
||||
@@ -11,6 +13,7 @@ from clan_lib.machines.machines import Machine
|
||||
from clan_lib.machines.suggestions import validate_machine_names
|
||||
from clan_lib.machines.update import run_machine_update
|
||||
from clan_lib.nix import nix_config
|
||||
from clan_lib.ssh.host_key import HostKeyCheck
|
||||
from clan_lib.ssh.remote import Remote
|
||||
|
||||
from clan_cli.completions import (
|
||||
@@ -95,13 +98,8 @@ def get_machines_for_update(
|
||||
|
||||
def update_command(args: argparse.Namespace) -> None:
|
||||
try:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
|
||||
machines_to_update = get_machines_for_update(
|
||||
args.flake, args.machines, args.tags
|
||||
)
|
||||
flake = require_flake(args.flake)
|
||||
machines_to_update = get_machines_for_update(flake, args.machines, args.tags)
|
||||
|
||||
if args.target_host is not None and len(machines_to_update) > 1:
|
||||
msg = "Target Host can only be set for one machines"
|
||||
@@ -111,7 +109,7 @@ def update_command(args: argparse.Namespace) -> None:
|
||||
config = nix_config()
|
||||
system = config["system"]
|
||||
machine_names = [machine.name for machine in machines_to_update]
|
||||
args.flake.precache(
|
||||
flake.precache(
|
||||
[
|
||||
f"clanInternals.machines.{system}.{{{','.join(machine_names)}}}.config.clan.core.vars.generators.*.validationHash",
|
||||
f"clanInternals.machines.{system}.{{{','.join(machine_names)}}}.config.clan.deployment.requireExplicitUpdate",
|
||||
@@ -178,7 +176,7 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
parser.add_argument(
|
||||
"--host-key-check",
|
||||
choices=["strict", "ask", "tofu", "none"],
|
||||
choices=list(get_args(HostKeyCheck)),
|
||||
default="ask",
|
||||
help="Host key (.ssh/known_hosts) check mode.",
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import Flake
|
||||
|
||||
from clan_cli.machines.update import get_machines_for_update
|
||||
|
||||
# Functions to test
|
||||
from clan_cli.tests.fixtures_flakes import FlakeForTest
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -159,4 +161,13 @@ def test_get_machines_for_update_implicit_all(
|
||||
assert names == expected_names
|
||||
|
||||
|
||||
def test_update_command_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["machines", "update", "machine1"])
|
||||
|
||||
|
||||
# TODO: Add more tests for requireExplicitUpdate
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.git import commit_files
|
||||
|
||||
from clan_cli.completions import (
|
||||
@@ -108,56 +108,44 @@ def remove_secret(
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
lst = list_sops_machines(args.flake.path)
|
||||
flake = require_flake(args.flake)
|
||||
lst = list_sops_machines(flake.path)
|
||||
if len(lst) > 0:
|
||||
print("\n".join(lst))
|
||||
|
||||
|
||||
def add_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
add_machine(args.flake.path, args.machine, args.key, args.force)
|
||||
flake = require_flake(args.flake)
|
||||
add_machine(flake.path, args.machine, args.key, args.force)
|
||||
|
||||
|
||||
def get_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
print(get_machine_pubkey(args.flake.path, args.machine))
|
||||
flake = require_flake(args.flake)
|
||||
print(get_machine_pubkey(flake.path, args.machine))
|
||||
|
||||
|
||||
def remove_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
remove_machine(args.flake.path, args.machine)
|
||||
flake = require_flake(args.flake)
|
||||
remove_machine(flake.path, args.machine)
|
||||
|
||||
|
||||
def add_secret_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
flake = require_flake(args.flake)
|
||||
add_secret(
|
||||
args.flake.path,
|
||||
flake.path,
|
||||
args.machine,
|
||||
sops_secrets_folder(args.flake.path) / args.secret,
|
||||
age_plugins=load_age_plugins(args.flake),
|
||||
sops_secrets_folder(flake.path) / args.secret,
|
||||
age_plugins=load_age_plugins(flake),
|
||||
)
|
||||
|
||||
|
||||
def remove_secret_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
flake = require_flake(args.flake)
|
||||
remove_secret(
|
||||
args.flake.path,
|
||||
flake.path,
|
||||
args.machine,
|
||||
args.secret,
|
||||
age_plugins=load_age_plugins(args.flake),
|
||||
age_plugins=load_age_plugins(flake),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.git import commit_files
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_secrets, complete_users
|
||||
@@ -122,10 +123,8 @@ def remove_secret(
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
lst = list_users(args.flake.path)
|
||||
flake = require_flake(args.flake)
|
||||
lst = list_users(flake.path)
|
||||
if len(lst) > 0:
|
||||
print("\n".join(lst))
|
||||
|
||||
@@ -193,66 +192,52 @@ def _key_args(args: argparse.Namespace) -> Iterable[sops.SopsKey]:
|
||||
|
||||
|
||||
def add_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
flake = require_flake(args.flake)
|
||||
|
||||
add_user(args.flake.path, args.user, _key_args(args), args.force)
|
||||
add_user(flake.path, args.user, _key_args(args), args.force)
|
||||
|
||||
|
||||
def get_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
keys = get_user(args.flake.path, args.user)
|
||||
flake = require_flake(args.flake)
|
||||
keys = get_user(flake.path, args.user)
|
||||
json.dump([key.as_dict() for key in keys], sys.stdout, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
def remove_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
remove_user(args.flake.path, args.user)
|
||||
flake = require_flake(args.flake)
|
||||
remove_user(flake.path, args.user)
|
||||
|
||||
|
||||
def add_secret_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
flake = require_flake(args.flake)
|
||||
add_secret(
|
||||
args.flake.path,
|
||||
flake.path,
|
||||
args.user,
|
||||
args.secret,
|
||||
age_plugins=load_age_plugins(args.flake),
|
||||
age_plugins=load_age_plugins(flake),
|
||||
)
|
||||
|
||||
|
||||
def remove_secret_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
flake = require_flake(args.flake)
|
||||
remove_secret(
|
||||
args.flake.path,
|
||||
flake.path,
|
||||
args.user,
|
||||
args.secret,
|
||||
age_plugins=load_age_plugins(args.flake),
|
||||
age_plugins=load_age_plugins(flake),
|
||||
)
|
||||
|
||||
|
||||
def add_key_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
flake = require_flake(args.flake)
|
||||
|
||||
add_user_key(args.flake.path, args.user, _key_args(args))
|
||||
add_user_key(flake.path, args.user, _key_args(args))
|
||||
|
||||
|
||||
def remove_key_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
flake = require_flake(args.flake)
|
||||
|
||||
remove_user_key(args.flake.path, args.user, _key_args(args))
|
||||
remove_user_key(flake.path, args.user, _key_args(args))
|
||||
|
||||
|
||||
def register_users_parser(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, get_args
|
||||
|
||||
from clan_lib.cmd import run
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.machines.machines import Machine
|
||||
from clan_lib.nix import nix_shell
|
||||
from clan_lib.ssh.remote import HostKeyCheck, Remote
|
||||
|
||||
@@ -37,20 +39,23 @@ class DeployInfo:
|
||||
raise ClanError(msg)
|
||||
return addrs[0]
|
||||
|
||||
@staticmethod
|
||||
def from_hostnames(
|
||||
hostname: list[str], host_key_check: HostKeyCheck
|
||||
def overwrite_remotes(
|
||||
self,
|
||||
host_key_check: HostKeyCheck | None = None,
|
||||
private_key: Path | None = None,
|
||||
ssh_options: dict[str, str] | None = None,
|
||||
) -> "DeployInfo":
|
||||
remotes = []
|
||||
for host in hostname:
|
||||
if not host:
|
||||
msg = "Hostname cannot be empty."
|
||||
raise ClanError(msg)
|
||||
remote = Remote.from_ssh_uri(
|
||||
machine_name="clan-installer", address=host
|
||||
).override(host_key_check=host_key_check)
|
||||
remotes.append(remote)
|
||||
return DeployInfo(addrs=remotes)
|
||||
"""Return a new DeployInfo with all Remotes overridden with the given host_key_check."""
|
||||
return DeployInfo(
|
||||
addrs=[
|
||||
addr.override(
|
||||
host_key_check=host_key_check,
|
||||
private_key=private_key,
|
||||
ssh_options=ssh_options,
|
||||
)
|
||||
for addr in self.addrs
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_json(data: dict[str, Any], host_key_check: HostKeyCheck) -> "DeployInfo":
|
||||
@@ -103,9 +108,22 @@ def find_reachable_host(deploy_info: DeployInfo) -> Remote | None:
|
||||
return None
|
||||
|
||||
|
||||
def ssh_shell_from_deploy(deploy_info: DeployInfo) -> None:
|
||||
def ssh_shell_from_deploy(
|
||||
deploy_info: DeployInfo, command: list[str] | None = None
|
||||
) -> None:
|
||||
if command and len(command) == 1 and command[0].count(" ") > 0:
|
||||
msg = (
|
||||
textwrap.dedent("""
|
||||
It looks like you quoted the remote command.
|
||||
The first argument should be the command to run, not a quoted string.
|
||||
""")
|
||||
.lstrip("\n")
|
||||
.rstrip("\n")
|
||||
)
|
||||
raise ClanError(msg)
|
||||
|
||||
if host := find_reachable_host(deploy_info):
|
||||
host.interactive_ssh()
|
||||
host.interactive_ssh(command)
|
||||
return
|
||||
|
||||
log.info("Could not reach host via clearnet 'addrs'")
|
||||
@@ -127,7 +145,7 @@ def ssh_shell_from_deploy(deploy_info: DeployInfo) -> None:
|
||||
log.info(
|
||||
"Host reachable via tor address, starting interactive ssh session."
|
||||
)
|
||||
tor_addr.interactive_ssh()
|
||||
tor_addr.interactive_ssh(command)
|
||||
return
|
||||
|
||||
log.error("Could not reach host via tor address.")
|
||||
@@ -135,19 +153,35 @@ def ssh_shell_from_deploy(deploy_info: DeployInfo) -> None:
|
||||
|
||||
def ssh_command_parse(args: argparse.Namespace) -> DeployInfo | None:
|
||||
host_key_check = args.host_key_check
|
||||
deploy = None
|
||||
|
||||
if args.json:
|
||||
json_file = Path(args.json)
|
||||
if json_file.is_file():
|
||||
data = json.loads(json_file.read_text())
|
||||
return DeployInfo.from_json(data, host_key_check)
|
||||
data = json.loads(args.json)
|
||||
return DeployInfo.from_json(data, host_key_check)
|
||||
if args.png:
|
||||
return DeployInfo.from_qr_code(Path(args.png), host_key_check)
|
||||
deploy = DeployInfo.from_json(data, host_key_check)
|
||||
elif args.png:
|
||||
deploy = DeployInfo.from_qr_code(Path(args.png), host_key_check)
|
||||
elif hasattr(args, "machine") and args.machine:
|
||||
machine = Machine(args.machine, args.flake)
|
||||
target = machine.target_host().override(
|
||||
command_prefix=machine.name, host_key_check=host_key_check
|
||||
)
|
||||
deploy = DeployInfo(addrs=[target])
|
||||
else:
|
||||
return None
|
||||
|
||||
if hasattr(args, "machines"):
|
||||
return DeployInfo.from_hostnames(args.machines, host_key_check)
|
||||
return None
|
||||
ssh_options = None
|
||||
if hasattr(args, "ssh_option") and args.ssh_option:
|
||||
for name, value in args.ssh_option:
|
||||
ssh_options = {}
|
||||
ssh_options[name] = value
|
||||
|
||||
deploy = deploy.overwrite_remotes(ssh_options=ssh_options)
|
||||
|
||||
return deploy
|
||||
|
||||
|
||||
def ssh_command(args: argparse.Namespace) -> None:
|
||||
@@ -155,36 +189,63 @@ def ssh_command(args: argparse.Namespace) -> None:
|
||||
if not deploy_info:
|
||||
msg = "No MACHINE, --json or --png data provided"
|
||||
raise ClanError(msg)
|
||||
|
||||
ssh_shell_from_deploy(deploy_info)
|
||||
ssh_shell_from_deploy(deploy_info, args.remote_command)
|
||||
|
||||
|
||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
machines_parser = group.add_argument(
|
||||
"machines",
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument(
|
||||
"machine",
|
||||
type=str,
|
||||
nargs="*",
|
||||
default=[],
|
||||
nargs="?",
|
||||
metavar="MACHINE",
|
||||
help="Machine to ssh into.",
|
||||
help="Machine to ssh into (uses clan.core.networking.targetHost from configuration).",
|
||||
)
|
||||
add_dynamic_completer(machines_parser, complete_machines)
|
||||
|
||||
group.add_argument(
|
||||
"-j",
|
||||
"--json",
|
||||
help="specify the json file for ssh data (generated by starting the clan installer)",
|
||||
type=str,
|
||||
help=(
|
||||
"Deployment information as a JSON string or path to a JSON file "
|
||||
"(generated by starting the clan installer)."
|
||||
),
|
||||
)
|
||||
group.add_argument(
|
||||
"-P",
|
||||
"--png",
|
||||
help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer)",
|
||||
type=str,
|
||||
help="Deployment information as a QR code image file (generated by starting the clan installer).",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--host-key-check",
|
||||
choices=["strict", "ask", "tofu", "none"],
|
||||
choices=list(get_args(HostKeyCheck)),
|
||||
default="tofu",
|
||||
help="Host key (.ssh/known_hosts) check mode.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--ssh-option",
|
||||
help="SSH option to set (can be specified multiple times)",
|
||||
nargs=2,
|
||||
metavar=("name", "value"),
|
||||
action="append",
|
||||
default=[],
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--remote-command",
|
||||
type=str,
|
||||
metavar="COMMAND",
|
||||
nargs=argparse.REMAINDER,
|
||||
help="Command to execute on the remote host, needs to be the LAST argument as it takes all remaining arguments.",
|
||||
)
|
||||
|
||||
add_dynamic_completer(
|
||||
parser._actions[1], # noqa: SLF001
|
||||
complete_machines,
|
||||
) # assumes 'machine' is the first positional
|
||||
|
||||
parser.set_defaults(func=ssh_command)
|
||||
|
||||
@@ -7,6 +7,8 @@ from clan_lib.nix import nix_shell
|
||||
from clan_lib.ssh.remote import Remote
|
||||
|
||||
from clan_cli.ssh.deploy_info import DeployInfo, find_reachable_host
|
||||
from clan_cli.tests.fixtures_flakes import ClanFlake
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
def test_qrcode_scan(temp_dir: Path) -> None:
|
||||
@@ -69,7 +71,10 @@ def test_from_json() -> None:
|
||||
@pytest.mark.with_core
|
||||
def test_find_reachable_host(hosts: list[Remote]) -> None:
|
||||
host = hosts[0]
|
||||
deploy_info = DeployInfo.from_hostnames(["172.19.1.2", host.ssh_url()], "none")
|
||||
|
||||
uris = ["172.19.1.2", host.ssh_url()]
|
||||
remotes = [Remote.from_ssh_uri(machine_name="some", address=uri) for uri in uris]
|
||||
deploy_info = DeployInfo(addrs=remotes)
|
||||
|
||||
assert deploy_info.addrs[0].address == "172.19.1.2"
|
||||
|
||||
@@ -77,3 +82,40 @@ def test_find_reachable_host(hosts: list[Remote]) -> None:
|
||||
|
||||
assert remote is not None
|
||||
assert remote.ssh_url() == host.ssh_url()
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_ssh_shell_from_deploy(
|
||||
hosts: list[Remote],
|
||||
flake: ClanFlake,
|
||||
) -> None:
|
||||
host = hosts[0]
|
||||
|
||||
machine1_config = flake.machines["m1_machine"]
|
||||
machine1_config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
machine1_config["clan"]["networking"]["targetHost"] = host.ssh_url()
|
||||
flake.refresh()
|
||||
|
||||
assert host.private_key
|
||||
|
||||
success_txt = flake.path / "success.txt"
|
||||
assert not success_txt.exists()
|
||||
cli.run(
|
||||
[
|
||||
"ssh",
|
||||
"--flake",
|
||||
str(flake.path),
|
||||
"m1_machine",
|
||||
"--host-key-check=none",
|
||||
"--ssh-option",
|
||||
"IdentityFile",
|
||||
str(host.private_key),
|
||||
"--remote-command",
|
||||
"touch",
|
||||
str(success_txt),
|
||||
"&&",
|
||||
"exit 0",
|
||||
]
|
||||
)
|
||||
|
||||
assert success_txt.exists()
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -29,9 +30,10 @@ def fix_vars(machine: Machine, generator_name: None | str = None) -> None:
|
||||
|
||||
|
||||
def fix_command(args: argparse.Namespace) -> None:
|
||||
flake = require_flake(args.flake)
|
||||
machine = Machine(
|
||||
name=args.machine,
|
||||
flake=args.flake,
|
||||
flake=flake,
|
||||
)
|
||||
fix_vars(machine, generator_name=args.generator)
|
||||
|
||||
|
||||
12
pkgs/clan-cli/clan_cli/vars/fix_test.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_cli.tests.helpers import cli
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
|
||||
def test_fix_command_no_flake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["vars", "fix", "machine1"])
|
||||
@@ -20,7 +20,7 @@ from clan_cli.vars.migration import check_can_migrate, migrate_files
|
||||
from clan_lib.api import API
|
||||
from clan_lib.cmd import RunOpts, run
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.flake import Flake, require_flake
|
||||
from clan_lib.git import commit_files
|
||||
from clan_lib.machines.list import list_full_machines
|
||||
from clan_lib.nix import nix_config, nix_shell, nix_test_store
|
||||
@@ -603,11 +603,8 @@ def generate_vars(
|
||||
|
||||
|
||||
def generate_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
msg = "Could not find clan flake toplevel directory"
|
||||
raise ClanError(msg)
|
||||
|
||||
machines: list[Machine] = list(list_full_machines(args.flake).values())
|
||||
flake = require_flake(args.flake)
|
||||
machines: list[Machine] = list(list_full_machines(flake).values())
|
||||
|
||||
if len(args.machines) > 0:
|
||||
machines = list(
|
||||
@@ -622,7 +619,7 @@ def generate_command(args: argparse.Namespace) -> None:
|
||||
system = config["system"]
|
||||
machine_names = [machine.name for machine in machines]
|
||||
# test
|
||||
args.flake.precache(
|
||||
flake.precache(
|
||||
[
|
||||
f"clanInternals.machines.{system}.{{{','.join(machine_names)}}}.config.clan.core.vars.generators.*.validationHash",
|
||||
]
|
||||
@@ -635,7 +632,7 @@ def generate_command(args: argparse.Namespace) -> None:
|
||||
fake_prompts=args.fake_prompts,
|
||||
)
|
||||
if has_changed:
|
||||
args.flake.invalidate_cache()
|
||||
flake.invalidate_cache()
|
||||
|
||||
|
||||
def register_generate_parser(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
14
pkgs/clan-cli/clan_cli/vars/generate_test.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_cli.tests.helpers import cli
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
|
||||
def test_generate_command_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["vars", "generate"])
|
||||
@@ -4,7 +4,7 @@ import sys
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.flake import Flake, require_flake
|
||||
|
||||
from .generate import Var
|
||||
from .list import get_machine_vars
|
||||
@@ -52,10 +52,11 @@ def get_command(machine_name: str, var_id: str, flake: Flake) -> None:
|
||||
def _get_command(
|
||||
args: argparse.Namespace,
|
||||
) -> None:
|
||||
flake = require_flake(args.flake)
|
||||
get_command(
|
||||
machine_name=args.machine,
|
||||
var_id=args.var_id,
|
||||
flake=args.flake,
|
||||
flake=flake,
|
||||
)
|
||||
|
||||
|
||||
|
||||
12
pkgs/clan-cli/clan_cli/vars/get_test.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_cli.tests.helpers import cli
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
|
||||
def test_get_command_no_flake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["vars", "get", "machine1", "var1"])
|
||||
@@ -2,7 +2,7 @@ import argparse
|
||||
import logging
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.flake import Flake, require_flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
|
||||
from .generate import Var
|
||||
@@ -37,7 +37,8 @@ def stringify_all_vars(machine: Machine) -> str:
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
flake = require_flake(args.flake)
|
||||
machine = Machine(name=args.machine, flake=flake)
|
||||
print(stringify_all_vars(machine))
|
||||
|
||||
|
||||
|
||||
12
pkgs/clan-cli/clan_cli/vars/list_test.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_cli.tests.helpers import cli
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
|
||||
def test_list_command_no_flake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["vars", "list", "machine1"])
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
from clan_lib.flake import require_flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
from clan_lib.ssh.remote import Remote
|
||||
|
||||
@@ -22,7 +23,8 @@ def populate_secret_vars(machine: Machine, directory: Path) -> None:
|
||||
|
||||
|
||||
def upload_command(args: argparse.Namespace) -> None:
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
flake = require_flake(args.flake)
|
||||
machine = Machine(name=args.machine, flake=flake)
|
||||
directory = None
|
||||
if args.directory:
|
||||
directory = Path(args.directory)
|
||||
|
||||
14
pkgs/clan-cli/clan_cli/vars/upload_test.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_cli.tests.helpers import cli
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
|
||||
def test_upload_command_no_flake(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
with pytest.raises(ClanError):
|
||||
cli.run(["vars", "upload", "machine1"])
|
||||
@@ -1 +1 @@
|
||||
from .flake import Flake # noqa
|
||||
from .flake import Flake, require_flake # noqa
|
||||
|
||||