Compare commits
49 Commits
remove-bui
...
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 |
@@ -1,20 +0,0 @@
|
||||
name: Build Clan App (Darwin)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 4 hours
|
||||
- cron: "0 */4 * * *"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-clan-app-darwin:
|
||||
runs-on: nix
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build clan-app for x86_64-darwin
|
||||
run: |
|
||||
nix build .#packages.x86_64-darwin.clan-app --system x86_64-darwin --log-format bar-with-logs
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
#!/usr/bin/env bash
|
||||
# Shared script for creating pull requests in Gitea workflows
|
||||
set -eu
|
||||
set -euo pipefail
|
||||
|
||||
# Required environment variables:
|
||||
# - CI_BOT_TOKEN: Gitea bot token for authentication
|
||||
@@ -9,22 +8,22 @@ set -eu
|
||||
# - PR_TITLE: Title of the pull request
|
||||
# - PR_BODY: Body/description of the pull request
|
||||
|
||||
if [ -z "${CI_BOT_TOKEN:-}" ]; then
|
||||
if [[ -z "${CI_BOT_TOKEN:-}" ]]; then
|
||||
echo "Error: CI_BOT_TOKEN is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${PR_BRANCH:-}" ]; then
|
||||
if [[ -z "${PR_BRANCH:-}" ]]; then
|
||||
echo "Error: PR_BRANCH is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${PR_TITLE:-}" ]; then
|
||||
if [[ -z "${PR_TITLE:-}" ]]; then
|
||||
echo "Error: PR_TITLE is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${PR_BODY:-}" ]; then
|
||||
if [[ -z "${PR_BODY:-}" ]]; then
|
||||
echo "Error: PR_BODY is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -44,12 +43,9 @@ resp=$(nix run --inputs-from . nixpkgs#curl -- -X POST \
|
||||
}" \
|
||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls")
|
||||
|
||||
if ! pr_number=$(echo "$resp" | jq -r '.number'); then
|
||||
echo "Error parsing response from pull request creation" >&2
|
||||
exit 1
|
||||
fi
|
||||
pr_number=$(echo "$resp" | jq -r '.number')
|
||||
|
||||
if [ "$pr_number" = "null" ]; then
|
||||
if [[ "$pr_number" == "null" ]]; then
|
||||
echo "Error creating pull request:" >&2
|
||||
echo "$resp" | jq . >&2
|
||||
exit 1
|
||||
@@ -68,15 +64,12 @@ while true; do
|
||||
"delete_branch_after_merge": true
|
||||
}' \
|
||||
"https://git.clan.lol/api/v1/repos/clan/clan-core/pulls/$pr_number/merge")
|
||||
if ! msg=$(echo "$resp" | jq -r '.message'); then
|
||||
echo "Error parsing merge response" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$msg" != "Please try again later" ]; then
|
||||
msg=$(echo "$resp" | jq -r '.message')
|
||||
if [[ "$msg" != "Please try again later" ]]; then
|
||||
break
|
||||
fi
|
||||
echo "Retrying in 2 seconds..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Pull request #$pr_number merge initiated"
|
||||
echo "Pull request #$pr_number merge initiated"
|
||||
@@ -38,6 +38,7 @@
|
||||
recommendedOptimisation = lib.mkDefault true;
|
||||
recommendedProxySettings = lib.mkDefault true;
|
||||
recommendedTlsSettings = lib.mkDefault true;
|
||||
recommendedZstdSettings = lib.mkDefault true;
|
||||
|
||||
# Nginx sends all the access logs to /var/log/nginx/access.log by default.
|
||||
# instead of going to the journal!
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/internet";
|
||||
manifest.description = "direct access (or via ssh jumphost) to machines";
|
||||
manifest.categories = [
|
||||
"System"
|
||||
"Network"
|
||||
];
|
||||
roles.default = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options = {
|
||||
host = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
ip address or hostname (domain) of the machine
|
||||
'';
|
||||
};
|
||||
jumphosts = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
optional list of jumphosts to use to connect to the machine
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
perInstance =
|
||||
{
|
||||
roles,
|
||||
lib,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
{
|
||||
exports.networking = {
|
||||
# TODO add user space network support to clan-cli
|
||||
peers = lib.mapAttrs (_name: machine: {
|
||||
host.plain = machine.settings.host;
|
||||
SSHOptions = map (_x: "-J x") machine.settings.jumphosts;
|
||||
}) roles.default.machines;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
internet = module;
|
||||
};
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/tor";
|
||||
manifest.description = "Onion routing, use Hidden services to connect your machines";
|
||||
manifest.categories = [
|
||||
"System"
|
||||
"Network"
|
||||
];
|
||||
|
||||
roles.client = {
|
||||
perInstance =
|
||||
{
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
...
|
||||
}:
|
||||
{
|
||||
config = {
|
||||
services.tor = {
|
||||
enable = true;
|
||||
torsocks.enable = true;
|
||||
client.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
roles.server = {
|
||||
# interface =
|
||||
# { lib, ... }:
|
||||
# {
|
||||
# options = {
|
||||
# OciSettings = lib.mkOption {
|
||||
# type = lib.types.raw;
|
||||
# default = null;
|
||||
# description = "NixOS settings for virtualisation.oci-container.<name>.settings";
|
||||
# };
|
||||
# buildContainer = lib.mkOption {
|
||||
# type = lib.types.nullOr lib.types.str;
|
||||
# default = null;
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
perInstance =
|
||||
{
|
||||
instanceName,
|
||||
roles,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
exports.networking = {
|
||||
priority = lib.mkDefault 10;
|
||||
# TODO add user space network support to clan-cli
|
||||
module = "clan_lib.network.tor";
|
||||
peers = lib.mapAttrs (name: machine: {
|
||||
host.var = {
|
||||
machine = name;
|
||||
generator = "tor_${instanceName}";
|
||||
file = "hostname";
|
||||
};
|
||||
}) roles.server.machines;
|
||||
};
|
||||
nixosModule =
|
||||
{
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
config = {
|
||||
services.tor = {
|
||||
enable = true;
|
||||
relay.onionServices."clan_${instanceName}" = {
|
||||
version = 3;
|
||||
# TODO get ports from instance machine config
|
||||
map = [
|
||||
{
|
||||
port = 22;
|
||||
target.port = 22;
|
||||
}
|
||||
];
|
||||
secretKey = config.clan.core.vars.generators."tor_${instanceName}".files.hs_ed25519_secret_key.path;
|
||||
};
|
||||
};
|
||||
clan.core.vars.generators."tor_${instanceName}" = {
|
||||
files.hs_ed25519_secret_key = { };
|
||||
files.hostname = { };
|
||||
runtimeInputs = with pkgs; [
|
||||
coreutils
|
||||
tor
|
||||
];
|
||||
script = ''
|
||||
mkdir -p data
|
||||
echo -e "DataDirectory ./data\nSocksPort 0\nHiddenServiceDir ./hs\nHiddenServicePort 80 127.0.0.1:80" > torrc
|
||||
timeout 2 tor -f torrc || :
|
||||
mv hs/hs_ed25519_secret_key $out/hs_ed25519_secret_key
|
||||
mv hs/hostname $out/hostname
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
tor = module;
|
||||
};
|
||||
}
|
||||
@@ -39,7 +39,7 @@ in
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ instanceName, settings, ... }:
|
||||
{ settings, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{ pkgs, config, ... }:
|
||||
@@ -86,7 +86,7 @@ in
|
||||
|
||||
# service to generate the environment file containing all secrets, as
|
||||
# expected by the nixos NetworkManager-ensure-profile service
|
||||
systemd.services."NetworkManager-setup-secrets-${instanceName}" = {
|
||||
systemd.services.NetworkManager-setup-secrets = {
|
||||
description = "Generate wifi secrets for NetworkManager";
|
||||
requiredBy = [ "NetworkManager-ensure-profiles.service" ];
|
||||
partOf = [ "NetworkManager-ensure-profiles.service" ];
|
||||
|
||||
@@ -7,16 +7,8 @@
|
||||
inventory = {
|
||||
|
||||
machines.test = { };
|
||||
machines.second = { };
|
||||
|
||||
instances = {
|
||||
wg-test-all = {
|
||||
module.name = "@clan/wifi";
|
||||
module.input = "self";
|
||||
roles.default.tags.all = { };
|
||||
roles.default.settings.networks.all = { };
|
||||
};
|
||||
|
||||
wg-test-one = {
|
||||
module.name = "@clan/wifi";
|
||||
module.input = "self";
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
clanOptions = self'.legacyPackages.clan-internals-docs;
|
||||
buildClanOptions = self'.legacyPackages.clan-internals-docs;
|
||||
# Simply evaluated options (JSON)
|
||||
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
|
||||
jsonDocs = pkgs.callPackage ./get-module-docs.nix {
|
||||
@@ -99,7 +99,7 @@
|
||||
# Frontmatter format for clanModules
|
||||
export CLAN_MODULES_FRONTMATTER_DOCS=${clanModulesFrontmatter}/share/doc/nixos/options.json
|
||||
|
||||
export BUILD_CLAN_PATH=${clanOptions}/share/doc/nixos/options.json
|
||||
export BUILD_CLAN_PATH=${buildClanOptions}/share/doc/nixos/options.json
|
||||
|
||||
mkdir $out
|
||||
|
||||
|
||||
@@ -465,10 +465,6 @@ Learn how to use `clanServices` in practice in the [Using clanServices guide](..
|
||||
service_links: dict[str, dict[str, dict[str, Any]]] = json.load(f3)
|
||||
|
||||
for module_name, module_info in service_links.items():
|
||||
# Skip specific modules that are not ready for documentation
|
||||
if module_name in ["internet", "tor"]:
|
||||
continue
|
||||
|
||||
output = f"# {module_name}\n\n"
|
||||
# output += f"`clan.modules.{module_name}`\n"
|
||||
output += f"*{module_info['manifest']['description']}*\n"
|
||||
|
||||
@@ -35,37 +35,6 @@ services = {
|
||||
};
|
||||
```
|
||||
|
||||
### Complex Example: Multi-service Setup
|
||||
|
||||
```nix
|
||||
# Old format
|
||||
services = {
|
||||
borgbackup.production = {
|
||||
roles.server.machines = [ "backup-server" ];
|
||||
roles.server.config = {
|
||||
directory = "/var/backup/borg";
|
||||
};
|
||||
roles.client.tags = [ "backup" ];
|
||||
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
|
||||
};
|
||||
|
||||
zerotier.company-network = {
|
||||
roles.controller.machines = [ "network-controller" ];
|
||||
roles.moon.machines = [ "moon-1" "moon-2" ];
|
||||
roles.peer.tags = [ "nixos" ];
|
||||
};
|
||||
|
||||
sshd.internal = {
|
||||
roles.server.tags = [ "nixos" ];
|
||||
roles.client.tags = [ "nixos" ];
|
||||
config.certificate.searchDomains = [
|
||||
"internal.example.com"
|
||||
"vpn.example.com"
|
||||
];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ After: New `instances` Definition with `clanServices`
|
||||
@@ -101,56 +70,6 @@ instances = {
|
||||
};
|
||||
```
|
||||
|
||||
### Complex Example Migrated
|
||||
|
||||
```nix
|
||||
# New format
|
||||
instances = {
|
||||
borgbackup-production = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan-core";
|
||||
};
|
||||
roles.server.machines."backup-server" = { };
|
||||
roles.server.settings = {
|
||||
directory = "/var/backup/borg";
|
||||
};
|
||||
roles.client.tags.backup = { };
|
||||
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
|
||||
};
|
||||
|
||||
zerotier-company-network = {
|
||||
module = {
|
||||
name = "zerotier";
|
||||
input = "clan-core";
|
||||
};
|
||||
roles.controller.machines."network-controller" = { };
|
||||
roles.moon.machines."moon-1".settings = {
|
||||
stableEndpoints = [ "10.0.0.1" "2001:db8::1" ];
|
||||
};
|
||||
roles.moon.machines."moon-2".settings = {
|
||||
stableEndpoints = [ "10.0.0.2" "2001:db8::2" ];
|
||||
};
|
||||
roles.peer.tags.nixos = { };
|
||||
};
|
||||
|
||||
sshd-internal = {
|
||||
module = {
|
||||
name = "sshd";
|
||||
input = "clan-core";
|
||||
};
|
||||
roles.server.tags.nixos = { };
|
||||
roles.client.tags.nixos = { };
|
||||
roles.client.settings = {
|
||||
certificate.searchDomains = [
|
||||
"internal.example.com"
|
||||
"vpn.example.com"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Steps to Migrate
|
||||
@@ -212,33 +131,6 @@ roles.default.machines."test-inventory-machine".settings = {
|
||||
};
|
||||
```
|
||||
|
||||
### Important Type Changes
|
||||
|
||||
The new `instances` format uses **attribute sets** instead of **lists** for tags and machines:
|
||||
|
||||
```nix
|
||||
# ❌ Old format (lists)
|
||||
roles.client.tags = [ "backup" ];
|
||||
roles.server.machines = [ "blob64" ];
|
||||
|
||||
# ✅ New format (attribute sets)
|
||||
roles.client.tags.backup = { };
|
||||
roles.server.machines.blob64 = { };
|
||||
```
|
||||
|
||||
### Handling Multiple Machines/Tags
|
||||
|
||||
When you need to assign multiple machines or tags to a role:
|
||||
|
||||
```nix
|
||||
# ❌ Old format
|
||||
roles.moon.machines = [ "eva" "eve" ];
|
||||
|
||||
# ✅ New format - each machine gets its own attribute
|
||||
roles.moon.machines.eva = { };
|
||||
roles.moon.machines.eve = { };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
!!! Warning
|
||||
@@ -246,89 +138,8 @@ roles.moon.machines.eve = { };
|
||||
* `inventory.services` is no longer recommended; use `inventory.instances` instead.
|
||||
* Module authors should begin exporting service modules under the `clan.modules` attribute of their flake.
|
||||
|
||||
## Troubleshooting Common Migration Errors
|
||||
|
||||
### Error: "not of type `attribute set of (submodule)`"
|
||||
|
||||
This error occurs when using lists instead of attribute sets for tags or machines:
|
||||
|
||||
```
|
||||
error: A definition for option `flake.clan.inventory.instances.borgbackup-blob64.roles.client.tags' is not of type `attribute set of (submodule)'.
|
||||
```
|
||||
|
||||
**Solution**: Convert lists to attribute sets as shown in the "Important Type Changes" section above.
|
||||
|
||||
### Error: "unsupported attribute `module`"
|
||||
|
||||
This error indicates the module structure is incorrect:
|
||||
|
||||
```
|
||||
error: Module ':anon-4:anon-1' has an unsupported attribute `module'.
|
||||
```
|
||||
|
||||
**Solution**: Ensure the `module` attribute has exactly two fields: `name` and `input`.
|
||||
|
||||
### Error: "attribute 'pkgs' missing"
|
||||
|
||||
This suggests the instance configuration is trying to use imports incorrectly:
|
||||
|
||||
```
|
||||
error: attribute 'pkgs' missing
|
||||
```
|
||||
|
||||
**Solution**: Use the `module = { name = "..."; input = "..."; }` format instead of `imports`.
|
||||
|
||||
### Removed Features
|
||||
|
||||
The following features from the old `services` format are no longer supported in `instances`:
|
||||
|
||||
- Top-level `config` attribute (use `roles.<role>.settings` instead)
|
||||
- Direct module imports (use the `module` declaration instead)
|
||||
|
||||
### extraModules Support
|
||||
|
||||
The `extraModules` attribute is still supported in the new instances format! The key change is how modules are specified:
|
||||
|
||||
**Old format (string paths relative to clan root):**
|
||||
```nix
|
||||
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
|
||||
```
|
||||
|
||||
**New format (NixOS modules):**
|
||||
```nix
|
||||
# Direct module reference
|
||||
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
|
||||
|
||||
# Or using self
|
||||
roles.client.extraModules = [ self.nixosModules.borgbackup ];
|
||||
|
||||
# Or inline module definition
|
||||
roles.client.extraModules = [
|
||||
{ config, ... }: {
|
||||
# Your module configuration here
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
The `extraModules` now expects actual **NixOS modules** rather than string paths. This provides better type checking and more flexibility in how modules are specified.
|
||||
|
||||
**Alternative: Using @clan/importer**
|
||||
|
||||
For scenarios where you need to import modules with specific tag-based targeting, you can also use the dedicated `@clan/importer` service:
|
||||
|
||||
```nix
|
||||
instances = {
|
||||
my-importer = {
|
||||
module.name = "@clan/importer";
|
||||
module.input = "clan-core";
|
||||
roles.default.tags.my-tag = { };
|
||||
roles.default.extraModules = [ self.nixosModules.myModule ];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Further reference
|
||||
|
||||
* [Authoring a 'clan.service' module](../authoring/clanServices/index.md)
|
||||
* [ClanServices](../clanServices.md)
|
||||
* [Inventory Reference](../../reference/nix-api/inventory.md)
|
||||
* [Inventory Reference](../../reference/nix-api/inventory.md)
|
||||
14
flake.lock
generated
14
flake.lock
generated
@@ -13,11 +13,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753067306,
|
||||
"narHash": "sha256-jyoEbaXa8/MwVQ+PajUdT63y3gYhgD9o7snO/SLaikw=",
|
||||
"rev": "18dfd42bdb2cfff510b8c74206005f733e38d8b9",
|
||||
"lastModified": 1752589312,
|
||||
"narHash": "sha256-BafZOenlzMYdumG12AzgVLhEVu+GcEa8nYNDSIYe1U0=",
|
||||
"rev": "496bbf05a2aa7b061ef464254db5804d1c6f45b4",
|
||||
"type": "tarball",
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/18dfd42bdb2cfff510b8c74206005f733e38d8b9.tar.gz"
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/496bbf05a2aa7b061ef464254db5804d1c6f45b4.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -181,11 +181,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753006367,
|
||||
"narHash": "sha256-tzbhc4XttkyEhswByk5R38l+ztN9UDbnj0cTcP6Hp9A=",
|
||||
"lastModified": 1752055615,
|
||||
"narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "421b56313c65a0815a52b424777f55acf0b56ddf",
|
||||
"rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -21,6 +21,7 @@ lib.fix (
|
||||
{
|
||||
|
||||
inherit (buildClanLib)
|
||||
buildClan
|
||||
clan
|
||||
;
|
||||
/**
|
||||
|
||||
@@ -78,87 +78,7 @@ in
|
||||
internal = true;
|
||||
visible = false;
|
||||
type = types.deferredModule;
|
||||
default = {
|
||||
options.networking = lib.mkOption {
|
||||
default = null;
|
||||
type = lib.types.nullOr (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
priority = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 1000;
|
||||
description = ''
|
||||
priority with which this network should be tried.
|
||||
higher priority means it gets used earlier in the chain
|
||||
'';
|
||||
};
|
||||
module = lib.mkOption {
|
||||
# type = lib.types.enum [
|
||||
# "clan_lib.network.direct"
|
||||
# "clan_lib.network.tor"
|
||||
# ];
|
||||
type = lib.types.str;
|
||||
default = "clan_lib.network.direct";
|
||||
description = ''
|
||||
the technology this network uses to connect to the target
|
||||
This is used for userspace networking with socks proxies.
|
||||
'';
|
||||
};
|
||||
# should we call this machines? hosts?
|
||||
peers = lib.mkOption {
|
||||
# <name>
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
};
|
||||
SSHOptions = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
};
|
||||
host = lib.mkOption {
|
||||
description = '''';
|
||||
type = lib.types.attrTag {
|
||||
plain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
a plain value, which can be read directly from the config
|
||||
'';
|
||||
};
|
||||
var = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
machine = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "jon";
|
||||
};
|
||||
generator = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "tor-ssh";
|
||||
};
|
||||
file = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "hostname";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
default = { };
|
||||
description = ''
|
||||
A module that is used to define the module of flake level exports -
|
||||
|
||||
|
||||
@@ -5,7 +5,32 @@
|
||||
clan-core,
|
||||
...
|
||||
}:
|
||||
{
|
||||
rec {
|
||||
buildClan =
|
||||
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 =
|
||||
{
|
||||
self ? lib.warn "Argument: 'self' must be set" null, # Reference to the current flake
|
||||
|
||||
@@ -29,7 +29,10 @@ def _get_lib_names() -> list[str]:
|
||||
msg = f"Unsupported architecture: {machine}"
|
||||
raise RuntimeError(msg)
|
||||
if system == "darwin":
|
||||
return ["libwebview.dylib"]
|
||||
if machine == "arm64":
|
||||
return ["libwebview.dylib"]
|
||||
msg = "Not supported"
|
||||
raise RuntimeError(msg)
|
||||
# linux
|
||||
return ["libwebview.so"]
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
@@ -9,7 +9,6 @@ import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/Form/TextArea";
|
||||
import { Checkbox } from "@/src/components/Form/Checkbox";
|
||||
import { FieldProps } from "./Field";
|
||||
import { HostFileInput } from "@/src/components/Form/HostFileInput";
|
||||
|
||||
const FieldsetExamples = (props: FieldsetProps) => (
|
||||
<div class="flex flex-col gap-8">
|
||||
@@ -27,7 +26,7 @@ const meta = {
|
||||
<div
|
||||
class={cx({
|
||||
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
|
||||
"w-[512px]": context.args.orientation == "horizontal",
|
||||
"w-[1024px]": context.args.orientation == "horizontal",
|
||||
"bg-inv-acc-3": context.args.inverted,
|
||||
})}
|
||||
>
|
||||
@@ -64,11 +63,6 @@ export const Default: Story = {
|
||||
label="Bio"
|
||||
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
|
||||
/>
|
||||
<HostFileInput
|
||||
{...props}
|
||||
label="Profile pic"
|
||||
onSelectFile={async () => "/home/foo/bar/baz/fizz/buzz/bla/bizz"}
|
||||
/>
|
||||
<Checkbox {...props} label="Accept Terms" required={true} />
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
div.form-field.host-file {
|
||||
button {
|
||||
@apply w-fit;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
button {
|
||||
@apply grow max-w-[18rem];
|
||||
}
|
||||
@apply w-1/2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,7 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Orienter
|
||||
orientation={props.orientation}
|
||||
align={props.orientation == "horizontal" ? "center" : "start"}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={"start"}>
|
||||
<Label
|
||||
labelComponent={TextField.Label}
|
||||
descriptionComponent={TextField.Description}
|
||||
|
||||
@@ -5,7 +5,7 @@ div.orienter {
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
@apply flex-row justify-between gap-0;
|
||||
@apply flex-row justify-start;
|
||||
|
||||
& > div.form-label {
|
||||
@apply w-1/2 shrink;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
div.modal-content {
|
||||
@apply min-w-[320px] max-w-[512px];
|
||||
@apply max-w-[512px];
|
||||
@apply rounded-md;
|
||||
|
||||
/* todo replace with a theme() color */
|
||||
@@ -12,7 +12,7 @@ div.modal-content {
|
||||
@apply border border-def-2 rounded-tl-md rounded-tr-md;
|
||||
@apply border-b-def-3;
|
||||
|
||||
& > .modal-title {
|
||||
& > .title {
|
||||
@apply mx-auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Dialog as KDialog } from "@kobalte/core/dialog";
|
||||
import "./Modal.css";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
import Icon from "../Icon/Icon";
|
||||
import cx from "classnames";
|
||||
|
||||
export interface ModalContext {
|
||||
close(): void;
|
||||
@@ -14,8 +13,6 @@ export interface ModalProps {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: (ctx: ModalContext) => JSX.Element;
|
||||
mount?: Node;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const Modal = (props: ModalProps) => {
|
||||
@@ -23,33 +20,18 @@ export const Modal = (props: ModalProps) => {
|
||||
|
||||
return (
|
||||
<KDialog id={props.id} open={open()} modal={true}>
|
||||
<KDialog.Portal mount={props.mount}>
|
||||
<KDialog.Content class={cx("modal-content", props.class)}>
|
||||
<KDialog.Portal>
|
||||
<KDialog.Content class="modal-content">
|
||||
<div class="header">
|
||||
<Typography
|
||||
class="modal-title"
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size="xs"
|
||||
>
|
||||
<Typography class="title" hierarchy="label" family="mono" size="xs">
|
||||
{props.title}
|
||||
</Typography>
|
||||
<KDialog.CloseButton
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
<KDialog.CloseButton onClick={() => setOpen(false)}>
|
||||
<Icon icon="Close" size="0.75rem" />
|
||||
</KDialog.CloseButton>
|
||||
</div>
|
||||
<div class="body">
|
||||
{props.children({
|
||||
close: () => {
|
||||
setOpen(false);
|
||||
props.onClose();
|
||||
},
|
||||
})}
|
||||
{props.children({ close: () => setOpen(false) })}
|
||||
</div>
|
||||
</KDialog.Content>
|
||||
</KDialog.Portal>
|
||||
|
||||
@@ -14,35 +14,35 @@ import { StoryContext } from "@kachurun/storybook-solid-vite";
|
||||
|
||||
const sidebarNavProps: SidebarNavProps = {
|
||||
clanLinks: [
|
||||
{ label: "Brian's Clan", path: "/clans/1" },
|
||||
{ label: "Dave's Clan", path: "/clans/2" },
|
||||
{ label: "Mic92's Clan", path: "/clans/3" },
|
||||
{ label: "Brian's Clan", path: "/clan/1" },
|
||||
{ label: "Dave's Clan", path: "/clan/2" },
|
||||
{ label: "Mic92's Clan", path: "/clan/3" },
|
||||
],
|
||||
clanDetail: {
|
||||
label: "Brian's Clan",
|
||||
settingsPath: "/clans/1/settings",
|
||||
settingsPath: "/clan/1/settings",
|
||||
machines: [
|
||||
{
|
||||
label: "Backup & Home",
|
||||
path: "/clans/1/machine/backup",
|
||||
path: "/clan/1/machine/backup",
|
||||
serviceCount: 3,
|
||||
status: "Online",
|
||||
},
|
||||
{
|
||||
label: "Raspberry Pi",
|
||||
path: "/clans/1/machine/pi",
|
||||
path: "/clan/1/machine/pi",
|
||||
serviceCount: 1,
|
||||
status: "Offline",
|
||||
},
|
||||
{
|
||||
label: "Mom's Laptop",
|
||||
path: "/clans/1/machine/moms-laptop",
|
||||
path: "/clan/1/machine/moms-laptop",
|
||||
serviceCount: 2,
|
||||
status: "Installed",
|
||||
},
|
||||
{
|
||||
label: "Dad's Laptop",
|
||||
path: "/clans/1/machine/dads-laptop",
|
||||
path: "/clan/1/machine/dads-laptop",
|
||||
serviceCount: 4,
|
||||
status: "Not Installed",
|
||||
},
|
||||
@@ -52,10 +52,10 @@ const sidebarNavProps: SidebarNavProps = {
|
||||
{
|
||||
label: "Tools",
|
||||
links: [
|
||||
{ label: "Borgbackup", path: "/clans/1/service/borgbackup" },
|
||||
{ label: "Syncthing", path: "/clans/1/service/syncthing" },
|
||||
{ label: "Mumble", path: "/clans/1/service/mumble" },
|
||||
{ label: "Minecraft", path: "/clans/1/service/minecraft" },
|
||||
{ label: "Borgbackup", path: "/clan/1/service/borgbackup" },
|
||||
{ label: "Syncthing", path: "/clan/1/service/syncthing" },
|
||||
{ label: "Mumble", path: "/clan/1/service/mumble" },
|
||||
{ label: "Minecraft", path: "/clan/1/service/minecraft" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -81,7 +81,7 @@ const meta: Meta<RouteSectionProps> = {
|
||||
component: SidebarNav,
|
||||
render: (_: never, context: StoryContext<SidebarNavProps>) => {
|
||||
const history = createMemoryHistory();
|
||||
history.set({ value: "/clans/1/machine/backup" });
|
||||
history.set({ value: "/clan/1/machine/backup" });
|
||||
|
||||
return (
|
||||
<div style="height: 670px;">
|
||||
@@ -93,7 +93,7 @@ const meta: Meta<RouteSectionProps> = {
|
||||
</Suspense>
|
||||
)}
|
||||
>
|
||||
<Route path="/clans/1/machine/backup" component={(props) => <></>} />
|
||||
<Route path="/clan/1/machine/backup" component={(props) => <></>} />
|
||||
</MemoryRouter>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { addClanURI, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { Params, Navigator, useParams } from "@solidjs/router";
|
||||
import { Params, Navigator } from "@solidjs/router";
|
||||
|
||||
export const selectClanFolder = async () => {
|
||||
const req = callApi("get_clan_folder", {});
|
||||
@@ -21,37 +21,9 @@ export const selectClanFolder = async () => {
|
||||
};
|
||||
|
||||
export const navigateToClan = (navigate: Navigator, uri: string) => {
|
||||
navigate("/clans/" + window.btoa(uri));
|
||||
navigate("/clan/" + window.btoa(uri));
|
||||
};
|
||||
|
||||
export const clanURIParam = (params: Params) => {
|
||||
return window.atob(params.clanURI);
|
||||
};
|
||||
|
||||
export function useClanURI(opts: { force: true }): string;
|
||||
export function useClanURI(opts: { force: boolean }): string | null;
|
||||
export function useClanURI(
|
||||
opts: { force: boolean } = { force: false },
|
||||
): string | null {
|
||||
const maybe = () => {
|
||||
const params = useParams();
|
||||
if (!params.clanURI) {
|
||||
return null;
|
||||
}
|
||||
const clanURI = clanURIParam(params);
|
||||
if (!clanURI) {
|
||||
throw new Error(
|
||||
"Could not decode clan URI from params: " + params.clanURI,
|
||||
);
|
||||
}
|
||||
return clanURI;
|
||||
};
|
||||
|
||||
const uri = maybe();
|
||||
if (!uri && opts.force) {
|
||||
throw new Error(
|
||||
"ClanURI is not set. Use this function only within contexts, where clanURI is guaranteed to have been set.",
|
||||
);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
import "./index.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||
import { QueryClient } from "@tanstack/solid-query";
|
||||
import { Routes } from "@/src/routes";
|
||||
import { Router } from "@solidjs/router";
|
||||
import { Layout } from "@/src/routes/Layout";
|
||||
@@ -22,11 +22,4 @@ if (import.meta.env.DEV) {
|
||||
await import("solid-devtools");
|
||||
}
|
||||
|
||||
render(
|
||||
() => (
|
||||
<QueryClientProvider client={client}>
|
||||
<Router root={Layout}>{Routes}</Router>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
root!,
|
||||
);
|
||||
render(() => <Router root={Layout}>{Routes}</Router>, root!);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useQuery, UseQueryResult } from "@tanstack/solid-query";
|
||||
import { callApi, SuccessData } from "../hooks/api";
|
||||
|
||||
export type ListMachines = SuccessData<"list_machines">;
|
||||
|
||||
export type MachinesQueryResult = UseQueryResult<ListMachines>;
|
||||
|
||||
interface MachinesQueryParams {
|
||||
clanURI: string | null;
|
||||
}
|
||||
export const useMachinesQuery = (props: MachinesQueryParams) =>
|
||||
useQuery<ListMachines>(() => ({
|
||||
queryKey: ["clans", props.clanURI, "machines"],
|
||||
enabled: !!props.clanURI,
|
||||
queryFn: async () => {
|
||||
if (!props.clanURI) {
|
||||
return {};
|
||||
}
|
||||
const api = callApi("list_machines", {
|
||||
flake: {
|
||||
identifier: props.clanURI,
|
||||
},
|
||||
});
|
||||
const result = await api.result;
|
||||
if (result.status === "error") {
|
||||
console.error("Error fetching machines:", result.errors);
|
||||
return {};
|
||||
}
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
@@ -1,13 +0,0 @@
|
||||
.fade-out {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.create-backdrop {
|
||||
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.create-modal {
|
||||
@apply min-w-96;
|
||||
}
|
||||
@@ -1,231 +1,10 @@
|
||||
import { RouteSectionProps } from "@solidjs/router";
|
||||
import { Component, JSX, Show, createSignal } from "solid-js";
|
||||
import { useClanURI } from "@/src/hooks/clan";
|
||||
import { RouteSectionProps, useParams } from "@solidjs/router";
|
||||
import { Component } from "solid-js";
|
||||
import { clanURIParam } from "@/src/hooks/clan";
|
||||
import { CubeScene } from "@/src/scene/cubes";
|
||||
import { MachinesQueryResult, useMachinesQuery } from "@/src/queries/queries";
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { store, setStore } from "@/src/stores/clan";
|
||||
import { produce } from "solid-js/store";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { Splash } from "@/src/scene/splash";
|
||||
import cx from "classnames";
|
||||
import "./Clan.css";
|
||||
import { Modal } from "@/src/components/Modal/Modal";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { createForm, FieldValues, reset } from "@modular-forms/solid";
|
||||
|
||||
export const Clan: Component<RouteSectionProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
<ClanSceneController />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface CreateFormValues extends FieldValues {
|
||||
name: string;
|
||||
}
|
||||
interface MockProps {
|
||||
onClose: () => void;
|
||||
onSubmit: (formValues: CreateFormValues) => void;
|
||||
}
|
||||
const MockCreateMachine = (props: MockProps) => {
|
||||
let container: Node;
|
||||
|
||||
const [form, { Form, Field, FieldArray }] = createForm<CreateFormValues>();
|
||||
|
||||
return (
|
||||
<div ref={(el) => (container = el)} class="create-backdrop">
|
||||
<Modal
|
||||
mount={container!}
|
||||
onClose={() => {
|
||||
reset(form);
|
||||
props.onClose();
|
||||
}}
|
||||
class="create-modal"
|
||||
title="Create Machine"
|
||||
>
|
||||
{() => (
|
||||
<Form class="flex flex-col" onSubmit={props.onSubmit}>
|
||||
<Field name="name">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...field}
|
||||
label="Name"
|
||||
size="s"
|
||||
required={true}
|
||||
input={{ ...props, placeholder: "name" }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div class="flex w-full items-center justify-end gap-4">
|
||||
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="s"
|
||||
type="submit"
|
||||
hierarchy="primary"
|
||||
onClick={close}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ClanSceneController = () => {
|
||||
const clanURI = useClanURI({ force: true });
|
||||
|
||||
const [dialogHandlers, setDialogHandlers] = createSignal<{
|
||||
resolve: ({ id }: { id: string }) => void;
|
||||
reject: (err: unknown) => void;
|
||||
} | null>(null);
|
||||
|
||||
const onCreate = async (): Promise<{ id: string }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setShowModal(true);
|
||||
setDialogHandlers({ resolve, reject });
|
||||
});
|
||||
};
|
||||
|
||||
const sendCreate = async (values: CreateFormValues) => {
|
||||
const api = callApi("create_machine", {
|
||||
opts: {
|
||||
clan_dir: {
|
||||
identifier: clanURI,
|
||||
},
|
||||
machine: {
|
||||
name: values.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
const res = await api.result;
|
||||
if (res.status === "error") {
|
||||
// TODO: Handle displaying errors
|
||||
console.error("Error creating machine:");
|
||||
|
||||
// Important: rejects the promise
|
||||
throw new Error(res.errors[0].message);
|
||||
}
|
||||
return { id: values.name };
|
||||
};
|
||||
|
||||
const [showModal, setShowModal] = createSignal(false);
|
||||
|
||||
return (
|
||||
<SceneDataProvider clanURI={clanURI}>
|
||||
{({ query }) => {
|
||||
return (
|
||||
<>
|
||||
<Show when={showModal()}>
|
||||
<MockCreateMachine
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
dialogHandlers()?.reject(new Error("User cancelled"));
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
const result = await sendCreate(values);
|
||||
dialogHandlers()?.resolve(result);
|
||||
setShowModal(false);
|
||||
} catch (err) {
|
||||
dialogHandlers()?.reject(err);
|
||||
setShowModal(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<div
|
||||
class="flex flex-row"
|
||||
style={{ position: "absolute", top: "10px", left: "10px" }}
|
||||
>
|
||||
<Button
|
||||
ghost
|
||||
onClick={() => {
|
||||
setStore(
|
||||
produce((s) => {
|
||||
for (const machineId in s.sceneData[clanURI]) {
|
||||
// Reset the position of each machine to [0, 0]
|
||||
s.sceneData[clanURI] = {}; // Clear the entire object
|
||||
// delete s.sceneData[clanURI][machineId];
|
||||
}
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Reset Store
|
||||
</Button>
|
||||
<Button
|
||||
ghost
|
||||
onClick={() => {
|
||||
console.log("Refetching API");
|
||||
query.refetch();
|
||||
}}
|
||||
>
|
||||
Refetch API
|
||||
</Button>
|
||||
</div>
|
||||
{/* TODO: Add minimal display time */}
|
||||
<div class={cx({ "fade-out": !query.isLoading })}>
|
||||
<Splash />
|
||||
</div>
|
||||
|
||||
<CubeScene
|
||||
isLoading={query.isLoading}
|
||||
cubesQuery={query}
|
||||
onCreate={onCreate}
|
||||
sceneStore={() => {
|
||||
const clanURI = useClanURI({ force: true });
|
||||
return store.sceneData?.[clanURI];
|
||||
}}
|
||||
setMachinePos={(machineId: string, pos: [number, number]) => {
|
||||
console.log("calling setStore", machineId, pos);
|
||||
setStore(
|
||||
produce((s) => {
|
||||
if (!s.sceneData) {
|
||||
s.sceneData = {};
|
||||
}
|
||||
if (!s.sceneData[clanURI]) {
|
||||
s.sceneData[clanURI] = {};
|
||||
}
|
||||
if (!s.sceneData[clanURI][machineId]) {
|
||||
s.sceneData[clanURI][machineId] = { position: pos };
|
||||
} else {
|
||||
s.sceneData[clanURI][machineId].position = pos;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</SceneDataProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const SceneDataProvider = (props: {
|
||||
clanURI: string | null;
|
||||
children: (sceneData: { query: MachinesQueryResult }) => JSX.Element;
|
||||
}) => {
|
||||
const machinesQuery = useMachinesQuery({ clanURI: props.clanURI });
|
||||
|
||||
// This component can be used to provide scene data or context if needed
|
||||
return props.children({ query: machinesQuery });
|
||||
const params = useParams();
|
||||
const clanURI = clanURIParam(params);
|
||||
return <CubeScene />;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export const Layout: Component<RouteSectionProps> = (props) => {
|
||||
|
||||
// check for an active clan uri and redirect to it on first load
|
||||
const activeURI = activeClanURI();
|
||||
if (!props.location.pathname.startsWith("/clans/") && activeURI) {
|
||||
if (!props.location.pathname.startsWith("/clan/") && activeURI) {
|
||||
navigateToClan(navigate, activeURI);
|
||||
} else {
|
||||
navigate("/");
|
||||
|
||||
@@ -1,663 +0,0 @@
|
||||
div.creating {
|
||||
@apply flex flex-col items-center justify-center;
|
||||
|
||||
div.scene {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
perspective: 1000px;
|
||||
/*background: red;*/
|
||||
|
||||
& > .frame {
|
||||
position: relative;
|
||||
top: 100px;
|
||||
left: 65px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
/*background: green;*/
|
||||
|
||||
/*transform: rotate3d(-2, -2, 1, 45deg);*/
|
||||
transform: rotate3d(-1.5, -2, 0.5, 45deg);
|
||||
transform-style: preserve-3d;
|
||||
|
||||
& > .cube {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
transform-style: preserve-3d;
|
||||
|
||||
.cube-face {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.56) 0%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
border: 1px #10191a solid;
|
||||
|
||||
opacity: 1;
|
||||
|
||||
&.front {
|
||||
transform: rotateY(0deg) translateZ(50px);
|
||||
}
|
||||
|
||||
&.right {
|
||||
transform: rotateY(90deg) translateZ(50px);
|
||||
}
|
||||
|
||||
&.back {
|
||||
transform: rotateY(180deg) translateZ(50px);
|
||||
}
|
||||
|
||||
&.left {
|
||||
transform: rotateY(-90deg) translateZ(50px);
|
||||
}
|
||||
|
||||
&.top {
|
||||
transform: rotateX(90deg) translateZ(50px);
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
transform: rotateX(-90deg) translateZ(50px);
|
||||
}
|
||||
}
|
||||
|
||||
&.state-1 {
|
||||
animation: anim-cube-1-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
||||
infinite;
|
||||
}
|
||||
|
||||
&.state-1-1 {
|
||||
transform: translateZ(-120px);
|
||||
animation: anim-cube-1-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
||||
infinite;
|
||||
}
|
||||
|
||||
&.state-2 {
|
||||
left: 120px;
|
||||
animation: anim-cube-2-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
||||
infinite;
|
||||
}
|
||||
|
||||
&.state-2-2 {
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
animation: anim-cube-2-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
||||
infinite;
|
||||
}
|
||||
|
||||
&.state-3 {
|
||||
top: 120px;
|
||||
animation: anim-cube-3-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
||||
infinite;
|
||||
}
|
||||
|
||||
&.state-3-3 {
|
||||
top: 120px;
|
||||
transform: translateZ(-120px);
|
||||
animation: anim-cube-3-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
||||
infinite;
|
||||
}
|
||||
|
||||
&.state-4 {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
animation: anim-cube-4-1 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
||||
infinite;
|
||||
}
|
||||
|
||||
&.state-4-4 {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
animation: anim-cube-4-2 8s 0.32s cubic-bezier(0.34, 1.56, 0.64, 1)
|
||||
infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes anim-cube-1-1 {
|
||||
/* STEP 1 */
|
||||
0% {
|
||||
left: 0px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
2.083% {
|
||||
left: -40px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
16.666% {
|
||||
left: -40px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
/* STEP 2 */
|
||||
18.749% {
|
||||
left: 0;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
33.332% {
|
||||
left: 0;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
/* STEP 3 */
|
||||
35.415% {
|
||||
left: 0;
|
||||
transform: translateZ(40px);
|
||||
}
|
||||
|
||||
49.998% {
|
||||
left: 0;
|
||||
transform: translateZ(40px);
|
||||
}
|
||||
|
||||
/* STEP 4 */
|
||||
52.081% {
|
||||
left: 0;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
66.664% {
|
||||
left: 0;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Step 5 */
|
||||
68.747% {
|
||||
left: -60px;
|
||||
transform: translateZ(60px);
|
||||
}
|
||||
83.33% {
|
||||
left: -60px;
|
||||
transform: translateZ(60px);
|
||||
}
|
||||
|
||||
/* Step 6 */
|
||||
85.413% {
|
||||
left: 0px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
100% {
|
||||
left: 0px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes anim-cube-2-1 {
|
||||
/* STEP 1 */
|
||||
0% {
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
2.083% {
|
||||
left: 180px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
16.666% {
|
||||
left: 180px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
/* STEP 2 */
|
||||
18.749% {
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
33.332% {
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
/* STEP 3 */
|
||||
35.415% {
|
||||
left: 240px;
|
||||
transform: translateZ(120px);
|
||||
}
|
||||
|
||||
49.998% {
|
||||
left: 240px;
|
||||
transform: translateZ(120px);
|
||||
}
|
||||
|
||||
/* STEP 4 */
|
||||
52.081% {
|
||||
left: 120px;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
66.664% {
|
||||
left: 120px;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Step 5 */
|
||||
68.747% {
|
||||
left: 60px;
|
||||
transform: translateZ(60px);
|
||||
}
|
||||
83.33% {
|
||||
left: 60px;
|
||||
transform: translateZ(60px);
|
||||
}
|
||||
|
||||
/* Step 6 */
|
||||
85.413% {
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
100% {
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes anim-cube-3-1 {
|
||||
/* STEP 1 */
|
||||
0% {
|
||||
top: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
2.083% {
|
||||
top: 220px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
16.666% {
|
||||
top: 220px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
/* STEP 2 */
|
||||
18.749% {
|
||||
top: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
33.332% {
|
||||
top: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
|
||||
/* STEP 3 */
|
||||
35.415% {
|
||||
top: 120px;
|
||||
transform: translateZ(40px);
|
||||
}
|
||||
|
||||
49.998% {
|
||||
top: 120px;
|
||||
transform: translateZ(40px);
|
||||
}
|
||||
|
||||
/* STEP 4 */
|
||||
52.081% {
|
||||
top: 120px;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
66.664% {
|
||||
top: 120px;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Step 5 */
|
||||
68.747% {
|
||||
top: 180px;
|
||||
transform: translateZ(80px);
|
||||
}
|
||||
83.33% {
|
||||
top: 180px;
|
||||
transform: translateZ(80px);
|
||||
}
|
||||
|
||||
/* Step 6 */
|
||||
85.413% {
|
||||
top: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
100% {
|
||||
top: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes anim-cube-4-1 {
|
||||
/* STEP 1 */
|
||||
0% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
2.083% {
|
||||
top: 220px;
|
||||
left: 180px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
16.666% {
|
||||
top: 220px;
|
||||
left: 180px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
/* STEP 2 */
|
||||
18.749% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
33.332% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
/* STEP 3 */
|
||||
35.415% {
|
||||
top: 120px;
|
||||
left: 240px;
|
||||
transform: translateZ(120px);
|
||||
}
|
||||
49.998% {
|
||||
top: 120px;
|
||||
left: 240px;
|
||||
transform: translateZ(120px);
|
||||
}
|
||||
/* STEP 4 */
|
||||
52.081% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
66.664% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
/* Step 5 */
|
||||
68.747% {
|
||||
top: 180px;
|
||||
left: 260px;
|
||||
transform: translateZ(80px);
|
||||
}
|
||||
83.33% {
|
||||
top: 180px;
|
||||
left: 260px;
|
||||
transform: translateZ(80px);
|
||||
}
|
||||
/* Step 6 */
|
||||
85.413% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
100% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes anim-cube-1-2 {
|
||||
/* STEP 1 */
|
||||
0% {
|
||||
left: 0px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
2.083% {
|
||||
left: -40px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
16.666% {
|
||||
left: -40px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* STEP 2 */
|
||||
18.749% {
|
||||
left: 0px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
33.332% {
|
||||
left: 0px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* STEP 3 */
|
||||
35.415% {
|
||||
left: 0px;
|
||||
transform: translateZ(-200px);
|
||||
}
|
||||
49.998% {
|
||||
left: 0px;
|
||||
transform: translateZ(-200px);
|
||||
}
|
||||
/* STEP 4 */
|
||||
52.081% {
|
||||
left: 0px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
66.664% {
|
||||
left: 0px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* Step 5 */
|
||||
68.747% {
|
||||
left: -60px;
|
||||
transform: translateZ(-180px);
|
||||
}
|
||||
83.33% {
|
||||
left: -60px;
|
||||
transform: translateZ(-180px);
|
||||
}
|
||||
/* Step 6 */
|
||||
85.413% {
|
||||
left: 0px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
100% {
|
||||
left: 0px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes anim-cube-2-2 {
|
||||
/* STEP 1 */
|
||||
0% {
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
2.083% {
|
||||
left: 180px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
16.666% {
|
||||
left: 180px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* STEP 2 */
|
||||
18.749% {
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
33.332% {
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* STEP 3 */
|
||||
35.415% {
|
||||
left: 240px;
|
||||
transform: translateZ(-200px);
|
||||
}
|
||||
49.998% {
|
||||
left: 240px;
|
||||
transform: translateZ(-200px);
|
||||
}
|
||||
/* STEP 4 */
|
||||
52.081% {
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
66.664% {
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* Step 5 */
|
||||
68.747% {
|
||||
left: 60px;
|
||||
transform: translateZ(-180px);
|
||||
}
|
||||
83.33% {
|
||||
left: 60px;
|
||||
transform: translateZ(-180px);
|
||||
}
|
||||
/* Step 6 */
|
||||
85.413% {
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
100% {
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes anim-cube-3-2 {
|
||||
/* STEP 1 */
|
||||
0% {
|
||||
top: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
2.083% {
|
||||
top: 220px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
16.666% {
|
||||
top: 220px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* STEP 2 */
|
||||
18.749% {
|
||||
top: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
33.332% {
|
||||
top: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* STEP 3 */
|
||||
35.415% {
|
||||
top: 120px;
|
||||
transform: translateZ(-200px);
|
||||
}
|
||||
49.998% {
|
||||
top: 120px;
|
||||
transform: translateZ(-200px);
|
||||
}
|
||||
/* STEP 4 */
|
||||
52.081% {
|
||||
top: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
66.664% {
|
||||
top: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* Step 5 */
|
||||
68.747% {
|
||||
top: 180px;
|
||||
transform: translateZ(-180px);
|
||||
}
|
||||
83.33% {
|
||||
top: 180px;
|
||||
transform: translateZ(-180px);
|
||||
}
|
||||
/* Step 6 */
|
||||
85.413% {
|
||||
top: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
100% {
|
||||
top: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes anim-cube-4-2 {
|
||||
/* STEP 1 */
|
||||
0% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
2.083% {
|
||||
top: 220px;
|
||||
left: 180px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
16.666% {
|
||||
top: 220px;
|
||||
left: 180px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* STEP 2 */
|
||||
18.749% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
33.332% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* STEP 3 */
|
||||
35.415% {
|
||||
top: 120px;
|
||||
left: 240px;
|
||||
transform: translateZ(-200px);
|
||||
}
|
||||
49.998% {
|
||||
top: 120px;
|
||||
left: 240px;
|
||||
transform: translateZ(-200px);
|
||||
}
|
||||
/* STEP 4 */
|
||||
52.081% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
66.664% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
/* Step 5 */
|
||||
68.747% {
|
||||
top: 180px;
|
||||
left: 260px;
|
||||
transform: translateZ(-180px);
|
||||
}
|
||||
83.33% {
|
||||
top: 180px;
|
||||
left: 260px;
|
||||
transform: translateZ(-180px);
|
||||
}
|
||||
/* Step 6 */
|
||||
85.413% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
100% {
|
||||
top: 120px;
|
||||
left: 120px;
|
||||
transform: translateZ(-120px);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Tooltip } from "@/src/components/Tooltip/Tooltip";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
|
||||
import "./Creating.css";
|
||||
|
||||
export const Creating = () => (
|
||||
<div class="creating">
|
||||
<Tooltip open={true} placement="top" trigger={<div />}>
|
||||
<Typography hierarchy="body" size="xs" weight="medium" inverted={true}>
|
||||
Your Clan is being created
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
|
||||
<div class="scene">
|
||||
<div class="frame">
|
||||
<div id="cube-1" class="cube state-1">
|
||||
<div class="cube-face front"></div>
|
||||
<div class="cube-face left"></div>
|
||||
<div class="cube-face right"></div>
|
||||
<div class="cube-face top"></div>
|
||||
<div class="cube-face bottom"></div>
|
||||
<div class="cube-face back"></div>
|
||||
</div>
|
||||
|
||||
<div id="cube-2" class="cube state-2">
|
||||
<div class="cube-face front"></div>
|
||||
<div class="cube-face left"></div>
|
||||
<div class="cube-face right"></div>
|
||||
<div class="cube-face top"></div>
|
||||
<div class="cube-face bottom"></div>
|
||||
<div class="cube-face back"></div>
|
||||
</div>
|
||||
|
||||
<div id="cube-3" class="cube state-3">
|
||||
<div class="cube-face front"></div>
|
||||
<div class="cube-face left"></div>
|
||||
<div class="cube-face right"></div>
|
||||
<div class="cube-face top"></div>
|
||||
<div class="cube-face bottom"></div>
|
||||
<div class="cube-face back"></div>
|
||||
</div>
|
||||
|
||||
<div id="cube-4" class="cube state-4">
|
||||
<div class="cube-face front"></div>
|
||||
<div class="cube-face left"></div>
|
||||
<div class="cube-face right"></div>
|
||||
<div class="cube-face top"></div>
|
||||
<div class="cube-face bottom"></div>
|
||||
<div class="cube-face back"></div>
|
||||
</div>
|
||||
|
||||
<div id="cube-1-1" class="cube state-1-1">
|
||||
<div class="cube-face front"></div>
|
||||
<div class="cube-face left"></div>
|
||||
<div class="cube-face right"></div>
|
||||
<div class="cube-face top"></div>
|
||||
<div class="cube-face bottom"></div>
|
||||
<div class="cube-face back"></div>
|
||||
</div>
|
||||
|
||||
<div id="cube-2-2" class="cube state-2-2">
|
||||
<div class="cube-face front"></div>
|
||||
<div class="cube-face left"></div>
|
||||
<div class="cube-face right"></div>
|
||||
<div class="cube-face top"></div>
|
||||
<div class="cube-face bottom"></div>
|
||||
<div class="cube-face back"></div>
|
||||
</div>
|
||||
|
||||
<div id="cube-3-3" class="cube state-3-3">
|
||||
<div class="cube-face front"></div>
|
||||
<div class="cube-face left"></div>
|
||||
<div class="cube-face right"></div>
|
||||
<div class="cube-face top"></div>
|
||||
<div class="cube-face bottom"></div>
|
||||
<div class="cube-face back"></div>
|
||||
</div>
|
||||
|
||||
<div id="cube-4-4" class="cube state-4-4">
|
||||
<div class="cube-face front"></div>
|
||||
<div class="cube-face left"></div>
|
||||
<div class="cube-face right"></div>
|
||||
<div class="cube-face top"></div>
|
||||
<div class="cube-face bottom"></div>
|
||||
<div class="cube-face back"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -54,7 +54,8 @@ main#welcome {
|
||||
}
|
||||
|
||||
& > div.container {
|
||||
@apply flex flex-col items-center justify-evenly gap-y-20 size-fit;
|
||||
@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;
|
||||
@@ -65,7 +66,7 @@ main#welcome {
|
||||
}
|
||||
|
||||
& > div.setup {
|
||||
@apply flex flex-col w-[33rem] gap-y-5;
|
||||
@apply flex flex-col min-w-[520px] gap-y-5;
|
||||
@apply pt-10 px-8 pb-8 bg-def-1 rounded-lg;
|
||||
|
||||
& > div.header {
|
||||
@@ -80,5 +81,10 @@ main#welcome {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.creating {
|
||||
@apply w-[17.0625rem] h-[20.4375rem];
|
||||
background: url(./cube.svg) center / cover no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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";
|
||||
@@ -32,7 +33,6 @@ 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";
|
||||
import { Creating } from "./Creating";
|
||||
|
||||
type State = "welcome" | "setup" | "creating";
|
||||
|
||||
@@ -154,6 +154,21 @@ const welcome = (props: {
|
||||
);
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
@@ -357,9 +372,7 @@ export const Onboarding: Component<RouteSectionProps> = (props) => {
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={state() === "creating"}>
|
||||
<Creating />
|
||||
</Match>
|
||||
<Match when={state() === "creating"}>{creating()}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
316
pkgs/clan-app/ui/src/routes/Onboarding/cube.svg
Normal file
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 |
@@ -8,41 +8,7 @@ export const Routes: RouteDefinition[] = [
|
||||
component: Onboarding,
|
||||
},
|
||||
{
|
||||
path: "/clans",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => (
|
||||
<h1>
|
||||
Clans (index) - (Doesnt really exist, just to keep the scene
|
||||
mounted)
|
||||
</h1>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/:clanURI",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: Clan,
|
||||
},
|
||||
{
|
||||
path: "/machines",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => <h1>Machines (Index)</h1>,
|
||||
},
|
||||
{
|
||||
path: "/:machineID",
|
||||
component: (props) => (
|
||||
<h1>Machine ID: {props.params.machineID}</h1>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: "/clan/:clanURI",
|
||||
component: Clan,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -9,15 +9,10 @@ import {
|
||||
import "./cubes.css";
|
||||
|
||||
import * as THREE from "three";
|
||||
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
|
||||
|
||||
import { Toolbar } from "../components/Toolbar/Toolbar";
|
||||
import { ToolbarButton } from "../components/Toolbar/ToolbarButton";
|
||||
import { Divider } from "../components/Divider/Divider";
|
||||
import { MachinesQueryResult } from "../queries/queries";
|
||||
import { SceneData } from "../stores/clan";
|
||||
import { unwrap } from "solid-js/store";
|
||||
import { Accessor } from "solid-js";
|
||||
|
||||
function garbageCollectGroup(group: THREE.Group) {
|
||||
for (const child of group.children) {
|
||||
@@ -58,38 +53,21 @@ function getFloorPosition(
|
||||
return intersection.toArray() as [number, number, number];
|
||||
}
|
||||
|
||||
function keyFromPos(pos: [number, number]): string {
|
||||
return `${pos[0]},${pos[1]}`;
|
||||
}
|
||||
|
||||
// type SceneDataUpdater = (sceneData: SceneData) => void;
|
||||
|
||||
export function CubeScene(props: {
|
||||
cubesQuery: MachinesQueryResult;
|
||||
onCreate: () => Promise<{ id: string }>;
|
||||
sceneStore: Accessor<SceneData>;
|
||||
setMachinePos: (machineId: string, pos: [number, number]) => void;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
export function CubeScene() {
|
||||
let container: HTMLDivElement;
|
||||
let scene: THREE.Scene;
|
||||
let camera: THREE.PerspectiveCamera;
|
||||
let renderer: THREE.WebGLRenderer;
|
||||
let floor: THREE.Mesh;
|
||||
let controls: MapControls;
|
||||
// Raycaster for clicking
|
||||
const raycaster = new THREE.Raycaster();
|
||||
let initBase: THREE.Mesh | undefined;
|
||||
|
||||
let needsRender = false; // Flag to control rendering
|
||||
|
||||
// Create background scene
|
||||
const bgScene = new THREE.Scene();
|
||||
const bgCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
|
||||
const groupMap = new Map<string, THREE.Group>();
|
||||
let raycaster: THREE.Raycaster;
|
||||
|
||||
const occupiedPositions = new Set<string>();
|
||||
const groupMap = new Map<string, THREE.Group>();
|
||||
const positionMap = new Map<string, THREE.Vector3>();
|
||||
|
||||
let sharedCubeGeometry: THREE.BoxGeometry;
|
||||
let sharedBaseGeometry: THREE.BoxGeometry;
|
||||
@@ -101,15 +79,23 @@ export function CubeScene(props: {
|
||||
let isAnimating = false; // Flag to prevent multiple loops
|
||||
let frameCount = 0;
|
||||
|
||||
const [ids, setIds] = createSignal<string[]>([]);
|
||||
const [positionMode, setPositionMode] = createSignal<"grid" | "circle">(
|
||||
"grid",
|
||||
);
|
||||
|
||||
const [nextBasePosition, setNextPosition] =
|
||||
createSignal<THREE.Vector3 | null>(null);
|
||||
const [worldMode, setWorldMode] = createSignal<"view" | "create">("view");
|
||||
|
||||
const [cursorPosition, setCursorPosition] = createSignal<[number, number]>();
|
||||
const [selectedIds, setSelectedIds] = createSignal<Set<string>>(new Set());
|
||||
// Backed camera position for restoring after switching mode
|
||||
const [backedCameraPosition, setBackedCameraPosition] = createSignal<{
|
||||
pos: THREE.Vector3;
|
||||
dir: THREE.Vector3;
|
||||
}>();
|
||||
|
||||
const [selectedIds, setSelectedIds] = createSignal<Set<string>>(new Set());
|
||||
const [deletingIds, setDeletingIds] = createSignal<Set<string>>(new Set());
|
||||
const [creatingIds, setCreatingIds] = createSignal<Set<string>>(new Set());
|
||||
const [cameraInfo, setCameraInfo] = createSignal({
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
spherical: { radius: 0, theta: 0, phi: 0 },
|
||||
@@ -145,95 +131,25 @@ export function CubeScene(props: {
|
||||
const CREATE_BASE_COLOR = 0x636363;
|
||||
const CREATE_BASE_EMISSIVE = 0xc5fad7;
|
||||
|
||||
createEffect(() => {
|
||||
console.log("Direct query data hook");
|
||||
// Update when API updates.
|
||||
if (props.cubesQuery.data) {
|
||||
const actualMachines = Object.keys(props.cubesQuery.data);
|
||||
const rawStored = unwrap(props.sceneStore());
|
||||
const placed: Set<string> = rawStored
|
||||
? new Set(Object.keys(rawStored))
|
||||
: new Set();
|
||||
const nonPlaced = actualMachines.filter((m) => !placed.has(m));
|
||||
|
||||
// Initialize occupied positions from previously placed cubes
|
||||
for (const id of placed) {
|
||||
occupiedPositions.add(keyFromPos(rawStored[id].position));
|
||||
}
|
||||
|
||||
// Push not explizitly placed machines to the scene
|
||||
// TODO: Make the user place them manually
|
||||
// We just calculate some next free position
|
||||
for (const id of nonPlaced) {
|
||||
console.log("adding", id);
|
||||
const position = nextGridPos();
|
||||
console.log("Got pos", position);
|
||||
|
||||
// Add the machine to the store
|
||||
// Adding it triggers a reactive update
|
||||
props.setMachinePos(id, position);
|
||||
occupiedPositions.add(keyFromPos(position));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function requestRenderIfNotRequested() {
|
||||
if (!needsRender) {
|
||||
needsRender = true;
|
||||
requestAnimationFrame(renderScene);
|
||||
}
|
||||
}
|
||||
|
||||
function renderScene() {
|
||||
if (!isAnimating) {
|
||||
console.warn("Not animating!");
|
||||
return;
|
||||
}
|
||||
console.log("Rendering scene...", initBase?.position);
|
||||
|
||||
needsRender = false;
|
||||
|
||||
frameCount++;
|
||||
|
||||
renderer.autoClear = false;
|
||||
renderer.render(bgScene, bgCamera);
|
||||
controls.update(); // optional; see note below
|
||||
renderer.render(scene, camera);
|
||||
|
||||
if (frameCount % 30 === 0) logMemoryUsage();
|
||||
}
|
||||
|
||||
function getGridPosition(id: string): [number, number, number] {
|
||||
function getGridPosition(
|
||||
id: string,
|
||||
index: number,
|
||||
total: number,
|
||||
): [number, number, number] {
|
||||
// TODO: Detect collision with other cubes
|
||||
const machine = props.sceneStore()[id];
|
||||
console.log("getGridPosition", id, machine);
|
||||
if (machine) {
|
||||
return [machine.position[0], 0, machine.position[1]];
|
||||
const pos = positionMap.get(id);
|
||||
if (pos) {
|
||||
return pos.toArray() as [number, number, number];
|
||||
}
|
||||
// Some fallback to get the next free position
|
||||
// If the position wasn't avilable in the store
|
||||
console.warn(`Position for ${id} not set`);
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
function nextGridPos(): [number, number] {
|
||||
// Scales up to 10*10 grid = 100 positions
|
||||
// TODO: Make this more scalable and nicer
|
||||
const maxRows = 10; // or dynamic limit if needed
|
||||
const maxCols = 10;
|
||||
|
||||
for (let y = 0; y < maxRows; y++) {
|
||||
for (let x = 0; x < maxCols; x++) {
|
||||
const pos = [x * CUBE_SPACING, y * CUBE_SPACING] as [number, number];
|
||||
const key = keyFromPos(pos);
|
||||
|
||||
if (!occupiedPositions.has(key)) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
const nextPos = nextBasePosition();
|
||||
if (!nextPos) {
|
||||
// Use next position if available
|
||||
throw new Error("Next position is not set");
|
||||
}
|
||||
|
||||
throw new Error("No free grid positions available.");
|
||||
const next = nextPos.toArray() as [number, number, number];
|
||||
positionMap.set(id, new THREE.Vector3(...next));
|
||||
return next;
|
||||
}
|
||||
|
||||
// Circle IDEA:
|
||||
@@ -248,14 +164,14 @@ export function CubeScene(props: {
|
||||
return [x, CUBE_Y, z];
|
||||
};
|
||||
|
||||
// Reactive cubes memo - this recalculates whenever data changes
|
||||
// Reactive cubes memo - this recalculates whenever ids() changes
|
||||
const cubes = createMemo(() => {
|
||||
console.log("Calculating cubes...");
|
||||
const sceneData = props.sceneStore(); // keep it reactive
|
||||
if (!sceneData) return [];
|
||||
const currentIds = ids();
|
||||
const deleting = deletingIds();
|
||||
const creating = creatingIds();
|
||||
|
||||
const currentIds = Object.keys(sceneData);
|
||||
console.log("Current IDs:", currentIds);
|
||||
// Include both active and deleting cubes for smooth transitions
|
||||
const allIds = [...new Set([...currentIds, ...Array.from(deleting)])];
|
||||
|
||||
let cameraTarget = [0, 0, 0] as [number, number, number];
|
||||
if (camera && floor) {
|
||||
@@ -266,10 +182,16 @@ export function CubeScene(props: {
|
||||
? getGridPosition
|
||||
: getCirclePosition(cameraTarget);
|
||||
|
||||
return currentIds.map((id, index) => {
|
||||
return allIds.map((id, index) => {
|
||||
const isDeleting = deleting.has(id);
|
||||
const isCreating = creating.has(id);
|
||||
const activeIndex = currentIds.indexOf(id);
|
||||
|
||||
const position = getCubePosition(id, index, currentIds.length);
|
||||
const position = getCubePosition(
|
||||
id,
|
||||
isDeleting ? -1 : activeIndex >= 0 ? activeIndex : index,
|
||||
currentIds.length,
|
||||
);
|
||||
|
||||
const targetPosition =
|
||||
activeIndex >= 0
|
||||
@@ -279,6 +201,8 @@ export function CubeScene(props: {
|
||||
return {
|
||||
id,
|
||||
position,
|
||||
isDeleting,
|
||||
isCreating,
|
||||
targetPosition,
|
||||
};
|
||||
});
|
||||
@@ -305,7 +229,99 @@ export function CubeScene(props: {
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
requestRenderIfNotRequested();
|
||||
}
|
||||
}
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
// Create animation helper
|
||||
function animateCreate(
|
||||
mesh: THREE.Mesh,
|
||||
baseMesh: THREE.Mesh,
|
||||
onComplete: () => void,
|
||||
) {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Start with zero scale and full opacity
|
||||
mesh.scale.setScalar(0);
|
||||
baseMesh.scale.setScalar(0);
|
||||
|
||||
// Ensure materials are fully opaque
|
||||
// if (Array.isArray(mesh.material)) {
|
||||
// mesh.material.forEach((material) => {
|
||||
// (material as THREE.MeshBasicMaterial).opacity = 1;
|
||||
// material.transparent = false;
|
||||
// });
|
||||
// } else {
|
||||
// (mesh.material as THREE.MeshBasicMaterial).opacity = 1;
|
||||
// mesh.material.transparent = false;
|
||||
// }
|
||||
|
||||
// if (Array.isArray(baseMesh.material)) {
|
||||
// baseMesh.material.forEach((material) => {
|
||||
// (material as THREE.MeshBasicMaterial).opacity = 1;
|
||||
// material.transparent = false;
|
||||
// });
|
||||
// } else {
|
||||
// (baseMesh.material as THREE.MeshBasicMaterial).opacity = 1;
|
||||
// baseMesh.material.transparent = false;
|
||||
// }
|
||||
|
||||
function animate() {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / CREATE_ANIMATION_DURATION, 1);
|
||||
|
||||
// Smooth easing function with slight overshoot effect
|
||||
let easeProgress;
|
||||
if (progress < 0.8) {
|
||||
// First 80% - smooth scale up
|
||||
easeProgress = 1 - Math.pow(1 - progress / 0.8, 3);
|
||||
} else {
|
||||
// Last 20% - slight overshoot and settle
|
||||
const overshootProgress = (progress - 0.8) / 0.2;
|
||||
const overshoot = Math.sin(overshootProgress * Math.PI) * 0.1;
|
||||
easeProgress = 1 + overshoot;
|
||||
}
|
||||
|
||||
const scale = easeProgress;
|
||||
mesh.scale.setScalar(scale);
|
||||
baseMesh.scale.setScalar(scale);
|
||||
|
||||
if (progress >= 1) {
|
||||
// Ensure final scale is exactly 1
|
||||
mesh.scale.setScalar(1);
|
||||
baseMesh.scale.setScalar(1);
|
||||
onComplete();
|
||||
} else {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
// Delete animation helper
|
||||
function animateDelete(group: THREE.Group, onComplete: () => void) {
|
||||
const startTime = Date.now();
|
||||
// const startScale = group.scale.clone();
|
||||
// const startOpacity = 1;
|
||||
|
||||
function animate() {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / DELETE_ANIMATION_DURATION, 1);
|
||||
|
||||
// Smooth easing function
|
||||
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
||||
const scale = 1 - easeProgress;
|
||||
// const opacity = startOpacity * (1 - easeProgress);
|
||||
|
||||
group.scale.setScalar(scale);
|
||||
|
||||
if (progress >= 1) {
|
||||
onComplete();
|
||||
} else {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,9 +347,31 @@ export function CubeScene(props: {
|
||||
return base;
|
||||
}
|
||||
|
||||
// === Add/Delete Cube API ===
|
||||
function addCube() {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
// Add to creating set first
|
||||
setCreatingIds((prev) => new Set([...prev, id]));
|
||||
|
||||
// Add to ids
|
||||
setIds((prev) => [...prev, id]);
|
||||
|
||||
// Remove from creating set after animation completes
|
||||
setTimeout(() => {
|
||||
setCreatingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
}, CREATE_ANIMATION_DURATION);
|
||||
}
|
||||
|
||||
function deleteSelectedCubes(selectedSet: Set<string>) {
|
||||
if (selectedSet.size === 0) return;
|
||||
|
||||
// Add to deleting set to start animation
|
||||
setDeletingIds(selectedSet);
|
||||
console.log("Deleting cubes:", selectedSet);
|
||||
|
||||
// Start delete animations
|
||||
@@ -351,13 +389,19 @@ export function CubeScene(props: {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
animateDelete(group, () => {
|
||||
// Remove from deleting set when animation completes
|
||||
setDeletingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
setSelectedIds(new Set<string>()); // Clear selection after deletion
|
||||
|
||||
garbageCollectGroup(group); // Clean up geometries and materials
|
||||
scene.remove(group); // Remove from scene
|
||||
groupMap.delete(id); // Remove from group map
|
||||
}
|
||||
setIds((prev) => prev.filter((existingId) => existingId !== id));
|
||||
|
||||
console.log("Done deleting", id, ids());
|
||||
});
|
||||
} else {
|
||||
console.warn(`DELETE: Group not found for id: ${id}`);
|
||||
}
|
||||
@@ -433,18 +477,15 @@ export function CubeScene(props: {
|
||||
|
||||
cubeMaterial.color.set(CUBE_COLOR);
|
||||
}
|
||||
|
||||
requestRenderIfNotRequested();
|
||||
}
|
||||
|
||||
function logMemoryUsage() {
|
||||
if (renderer && renderer.info) {
|
||||
console.debug("Three.js Memory:", {
|
||||
frame: renderer.info.render.frame,
|
||||
calls: renderer.info.render.calls,
|
||||
console.log("Three.js Memory:", {
|
||||
geometries: renderer.info.memory.geometries,
|
||||
textures: renderer.info.memory.textures,
|
||||
programs: renderer.info.programs?.length || 0,
|
||||
calls: renderer.info.render.calls,
|
||||
triangles: renderer.info.render.triangles,
|
||||
});
|
||||
}
|
||||
@@ -460,6 +501,8 @@ export function CubeScene(props: {
|
||||
),
|
||||
);
|
||||
|
||||
let initBase: THREE.Mesh;
|
||||
|
||||
const grid = new THREE.GridHelper(1000, 1000 / 1, 0xe1edef, 0xe1edef);
|
||||
|
||||
onMount(() => {
|
||||
@@ -522,13 +565,6 @@ export function CubeScene(props: {
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
controls = new MapControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
|
||||
// Enable the context menu,
|
||||
// TODO: disable in production
|
||||
controls.mouseButtons.RIGHT = null;
|
||||
controls.addEventListener("change", requestRenderIfNotRequested);
|
||||
|
||||
// Lighting
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5);
|
||||
scene.add(ambientLight);
|
||||
@@ -596,17 +632,9 @@ export function CubeScene(props: {
|
||||
BASE_SIZE,
|
||||
);
|
||||
|
||||
// Important create CubeBase depends on sharedBaseGeometry
|
||||
initBase = createCubeBase(
|
||||
[1, BASE_HEIGHT / 2, 1],
|
||||
1,
|
||||
CREATE_BASE_COLOR,
|
||||
CREATE_BASE_EMISSIVE,
|
||||
);
|
||||
initBase.visible = false;
|
||||
|
||||
scene.add(initBase);
|
||||
|
||||
// Basic OrbitControls implementation (simplified)
|
||||
let isDragging = false;
|
||||
let previousMousePosition = { x: 0, y: 0 };
|
||||
// const spherical = new THREE.Spherical();
|
||||
// spherical.setFromVector3(camera.position);
|
||||
|
||||
@@ -631,30 +659,136 @@ export function CubeScene(props: {
|
||||
// Initial camera info update
|
||||
updateCameraInfo();
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
isDragging = true;
|
||||
previousMousePosition = { x: event.clientX, y: event.clientY };
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (worldMode() === "create") {
|
||||
if (isDragging) return;
|
||||
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
const mouse = new THREE.Vector2(
|
||||
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
||||
-((event.clientY - rect.top) / rect.height) * 2 + 1,
|
||||
);
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
const intersects = raycaster.intersectObject(floor);
|
||||
if (intersects.length > 0) {
|
||||
const point = intersects[0].point;
|
||||
|
||||
// Snap to grid
|
||||
const snapped = new THREE.Vector3(
|
||||
Math.round(point.x / GRID_SIZE) * GRID_SIZE,
|
||||
BASE_HEIGHT / 2,
|
||||
Math.round(point.z / GRID_SIZE) * GRID_SIZE,
|
||||
);
|
||||
if (!initBase) {
|
||||
// Create initial base mesh if it doesn't exist
|
||||
initBase = createCubeBase(
|
||||
[snapped.x, BASE_HEIGHT / 2, snapped.z],
|
||||
1,
|
||||
CREATE_BASE_COLOR,
|
||||
CREATE_BASE_EMISSIVE, // Emissive color
|
||||
);
|
||||
} else {
|
||||
initBase.position.set(snapped.x, BASE_HEIGHT / 2, snapped.z);
|
||||
}
|
||||
scene.remove(initBase); // Remove any existing base mesh
|
||||
scene.add(initBase);
|
||||
setNextPosition(snapped); // Update next position for cube creation
|
||||
}
|
||||
// If in create mode, don't allow camera movement
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDragging) return;
|
||||
|
||||
const deltaX = event.clientX - previousMousePosition.x;
|
||||
const deltaY = event.clientY - previousMousePosition.y;
|
||||
// const deltaY = event.clientY - previousMousePosition.y;
|
||||
if (positionMode() === "circle") {
|
||||
const spherical = new THREE.Spherical();
|
||||
spherical.setFromVector3(camera.position);
|
||||
spherical.theta -= deltaX * 0.01;
|
||||
// spherical.phi += deltaY * 0.01;
|
||||
// spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
|
||||
|
||||
// const lightPos = new THREE.Spherical();
|
||||
// lightPos.setFromVector3(directionalLight.position);
|
||||
// lightPos.theta = spherical.theta - Math.PI / 2; // 90 degrees offset
|
||||
// directionalLight.position.setFromSpherical(lightPos);
|
||||
|
||||
// directionalLight.lookAt(0, 0, 0);
|
||||
|
||||
camera.position.setFromSpherical(spherical);
|
||||
camera.lookAt(0, 0, 0);
|
||||
} else {
|
||||
const movementSpeed = 0.015;
|
||||
|
||||
// Get camera direction vectors
|
||||
const cameraDirection = new THREE.Vector3();
|
||||
camera.getWorldDirection(cameraDirection);
|
||||
cameraDirection.y = 0; // Ignore vertical direction
|
||||
|
||||
const cameraRight = new THREE.Vector3();
|
||||
cameraRight.crossVectors(camera.up, cameraDirection).normalize(); // Get right vector
|
||||
|
||||
// Move camera based on mouse deltas
|
||||
camera.position.addScaledVector(cameraRight, deltaX * movementSpeed); // horizontal drag
|
||||
camera.position.addScaledVector(
|
||||
cameraDirection,
|
||||
deltaY * movementSpeed,
|
||||
); // vertical drag (forward/back)
|
||||
|
||||
setBackedCameraPosition({
|
||||
pos: camera.position.clone(),
|
||||
dir: camera.getWorldDirection(new THREE.Vector3()).clone(),
|
||||
});
|
||||
}
|
||||
updateCameraInfo();
|
||||
|
||||
previousMousePosition = { x: event.clientX, y: event.clientY };
|
||||
};
|
||||
|
||||
const onWheel = (event: WheelEvent) => {
|
||||
const spherical = new THREE.Spherical();
|
||||
spherical.setFromVector3(camera.position);
|
||||
event.preventDefault();
|
||||
spherical.radius += event.deltaY * 0.01;
|
||||
spherical.radius = Math.max(3, Math.min(10, spherical.radius)); // Clamp radius between 5 and 50
|
||||
camera.position.setFromSpherical(spherical);
|
||||
// camera.lookAt(0, 0, 0);
|
||||
updateCameraInfo();
|
||||
};
|
||||
|
||||
// Event listeners
|
||||
renderer.domElement.addEventListener("mousedown", onMouseDown);
|
||||
renderer.domElement.addEventListener("mouseup", onMouseUp);
|
||||
renderer.domElement.addEventListener("mousemove", onMouseMove);
|
||||
renderer.domElement.addEventListener("wheel", onWheel);
|
||||
|
||||
// Raycaster for clicking
|
||||
raycaster = new THREE.Raycaster();
|
||||
|
||||
// Click handler:
|
||||
// - Select/deselects a cube in "view" mode
|
||||
// - Creates a new cube in "create" mode
|
||||
const onClick = (event: MouseEvent) => {
|
||||
if (worldMode() === "create") {
|
||||
props
|
||||
.onCreate()
|
||||
.then(({ id }) => {
|
||||
//Successfully created machine
|
||||
const pos = cursorPosition();
|
||||
if (!pos) {
|
||||
console.warn("No position set for new cube");
|
||||
return;
|
||||
}
|
||||
props.setMachinePos(id, pos);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error creating cube:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
if (initBase) initBase.visible = false;
|
||||
|
||||
setWorldMode("view");
|
||||
});
|
||||
if (initBase) {
|
||||
scene.remove(initBase); // Remove the base mesh after adding cube
|
||||
setWorldMode("view");
|
||||
addCube();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
@@ -678,45 +812,40 @@ export function CubeScene(props: {
|
||||
|
||||
renderer.domElement.addEventListener("click", onClick);
|
||||
|
||||
isAnimating = true;
|
||||
const animate = () => {
|
||||
if (!isAnimating) return; // Exit if component is unmounted
|
||||
|
||||
requestRenderIfNotRequested();
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
frameCount++;
|
||||
renderer.autoClear = false;
|
||||
renderer.render(bgScene, bgCamera); // Render background scene
|
||||
|
||||
renderer.render(scene, camera);
|
||||
|
||||
// Uncomment for memory debugging:
|
||||
if (frameCount % 300 === 0) logMemoryUsage(); // Log every 60 frames
|
||||
};
|
||||
isAnimating = true;
|
||||
animate();
|
||||
|
||||
// Handle window resize
|
||||
const handleResize = () => {
|
||||
camera.aspect = container.clientWidth / container.clientHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||
|
||||
// Update background shader resolution
|
||||
uniforms.resolution.value.set(
|
||||
container.clientWidth,
|
||||
container.clientHeight,
|
||||
);
|
||||
|
||||
renderer.render(bgScene, bgCamera);
|
||||
requestRenderIfNotRequested();
|
||||
};
|
||||
|
||||
renderer.domElement.addEventListener("mousemove", onMouseMove);
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
// For debugging,
|
||||
// TODO: Remove in production
|
||||
window.addEventListener(
|
||||
"contextmenu",
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
},
|
||||
{ capture: true },
|
||||
);
|
||||
|
||||
// Cleanup function
|
||||
onCleanup(() => {
|
||||
// Stop animation loop
|
||||
isAnimating = false;
|
||||
renderer.domElement.removeEventListener("click", onClick);
|
||||
renderer.domElement.removeEventListener("mousedown", onMouseDown);
|
||||
renderer.domElement.removeEventListener("mouseup", onMouseUp);
|
||||
renderer.domElement.removeEventListener("mousemove", onMouseMove);
|
||||
renderer.domElement.removeEventListener("wheel", onWheel);
|
||||
renderer.domElement.removeEventListener("click", onClick);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
|
||||
if (initBase) {
|
||||
@@ -731,26 +860,30 @@ export function CubeScene(props: {
|
||||
if (container) {
|
||||
container.innerHTML = "";
|
||||
}
|
||||
|
||||
groupMap.forEach((group) => {
|
||||
garbageCollectGroup(group);
|
||||
scene.remove(group);
|
||||
});
|
||||
groupMap.clear();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Move into css
|
||||
// createEffect(
|
||||
// on(positionMode, (mode) => {
|
||||
// console.log("Position mode changed:", mode);
|
||||
// if (mode === "circle") {
|
||||
// grid.visible = false; // Hide grid when in circle mode
|
||||
// } else if (mode === "grid") {
|
||||
// grid.visible = true; // Show grid when in grid mode
|
||||
// }
|
||||
// }),
|
||||
// );
|
||||
createEffect(() => {
|
||||
if (!container) return;
|
||||
if (worldMode() === "create") {
|
||||
// Show the plus button when in create mode
|
||||
container.style.cursor = "crosshair";
|
||||
} else {
|
||||
container.style.cursor = "pointer";
|
||||
}
|
||||
});
|
||||
createEffect(
|
||||
// Fly back and forth between circle and grid positions
|
||||
// ? Do we want to do this.
|
||||
// We could shift the center of the circle to the camera look at position
|
||||
on(positionMode, (mode) => {
|
||||
if (mode === "circle") {
|
||||
grid.visible = false; // Hide grid when in circle mode
|
||||
} else if (mode === "grid") {
|
||||
grid.visible = true; // Show grid when in grid mode
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
function createCube(
|
||||
gridPosition: [number, number],
|
||||
@@ -787,28 +920,30 @@ export function CubeScene(props: {
|
||||
// Effect to manage cube meshes - this runs whenever cubes() changes
|
||||
createEffect(() => {
|
||||
const currentCubes = cubes();
|
||||
console.log("Current cubes:", currentCubes);
|
||||
|
||||
const existing = new Set(groupMap.keys());
|
||||
const deleting = deletingIds();
|
||||
const creating = creatingIds();
|
||||
|
||||
// Update existing cubes and create new ones
|
||||
currentCubes.forEach((cube) => {
|
||||
const existingGroup = groupMap.get(cube.id);
|
||||
|
||||
console.log(
|
||||
"Processing cube:",
|
||||
cube.id,
|
||||
"Existing group:",
|
||||
existingGroup,
|
||||
);
|
||||
if (!existingGroup) {
|
||||
const group = createCube([cube.position[0], cube.position[2]], {
|
||||
id: cube.id,
|
||||
});
|
||||
scene.add(group);
|
||||
groupMap.set(cube.id, group);
|
||||
} else {
|
||||
console.log("Updating existing cube:", cube.id);
|
||||
|
||||
// Start create animation if this cube is being created
|
||||
if (creating.has(cube.id)) {
|
||||
const mesh = group.children[0] as THREE.Mesh;
|
||||
const base = group.children[1] as THREE.Mesh;
|
||||
animateCreate(mesh, base, () => {
|
||||
// Animation complete callback - could add additional logic here
|
||||
});
|
||||
}
|
||||
} else if (!deleting.has(cube.id)) {
|
||||
// Only animate position if not being deleted
|
||||
const targetPosition = cube.targetPosition || cube.position;
|
||||
const currentPosition = existingGroup.position.toArray() as [
|
||||
@@ -832,20 +967,13 @@ export function CubeScene(props: {
|
||||
|
||||
// Remove cubes that are no longer in the state and not being deleted
|
||||
existing.forEach((id) => {
|
||||
if (!currentCubes.find((d) => d.id == id)) {
|
||||
if (!deleting.has(id)) {
|
||||
const group = groupMap.get(id);
|
||||
if (group) {
|
||||
console.log("Cleaning...", id);
|
||||
garbageCollectGroup(group);
|
||||
scene.remove(group);
|
||||
groupMap.delete(id);
|
||||
const pos = group.position.toArray() as [number, number, number];
|
||||
occupiedPositions.delete(keyFromPos([pos[0], pos[2]]));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
requestRenderIfNotRequested();
|
||||
});
|
||||
|
||||
createEffect(
|
||||
@@ -856,6 +984,39 @@ export function CubeScene(props: {
|
||||
}),
|
||||
);
|
||||
|
||||
// Effect to clean up deleted cubes after animation
|
||||
createEffect(() => {
|
||||
const deleting = deletingIds();
|
||||
const currentIds = ids();
|
||||
|
||||
// Clean up cubes that finished their delete animation
|
||||
deleting.forEach((id) => {
|
||||
if (!currentIds.includes(id)) {
|
||||
const group = groupMap.get(id);
|
||||
if (group) {
|
||||
scene.remove(group);
|
||||
group.children.forEach((child) => {
|
||||
// Child is finished with its destroy animation
|
||||
if (child instanceof THREE.Mesh && child.scale.x <= 0.01) {
|
||||
child.geometry.dispose();
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((material) => material.dispose());
|
||||
} else {
|
||||
child.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
groupMap.delete(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
selectedIds(); // Track the signal
|
||||
// updateMeshColors();
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
for (const group of groupMap.values()) {
|
||||
garbageCollectGroup(group);
|
||||
@@ -871,65 +1032,38 @@ export function CubeScene(props: {
|
||||
});
|
||||
|
||||
const onHover = (inside: boolean) => (event: MouseEvent) => {
|
||||
const pos = nextGridPos();
|
||||
if (!initBase) return;
|
||||
|
||||
if (initBase.visible === false && inside) {
|
||||
initBase.position.set(pos[0], BASE_HEIGHT / 2, pos[1]);
|
||||
initBase.visible = true;
|
||||
// Hover over the plus button, shows a preview of the base mesh
|
||||
const currentCubes = cubes();
|
||||
if (currentCubes.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initBase) {
|
||||
// Create initial base mesh if it doesn't exist
|
||||
initBase = createCubeBase(
|
||||
[0, BASE_HEIGHT / 2, 0],
|
||||
1,
|
||||
CREATE_BASE_COLOR,
|
||||
CREATE_BASE_EMISSIVE,
|
||||
); // Emissive color
|
||||
}
|
||||
if (inside) {
|
||||
scene.add(initBase);
|
||||
} else {
|
||||
scene.remove(initBase);
|
||||
}
|
||||
requestRenderIfNotRequested();
|
||||
};
|
||||
|
||||
const onAddClick = (event: MouseEvent) => {
|
||||
setPositionMode("grid");
|
||||
setWorldMode("create");
|
||||
};
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (worldMode() !== "create") return;
|
||||
if (!initBase) return;
|
||||
|
||||
initBase.visible = true;
|
||||
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
const mouse = new THREE.Vector2(
|
||||
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
||||
-((event.clientY - rect.top) / rect.height) * 2 + 1,
|
||||
);
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObject(floor);
|
||||
if (intersects.length > 0) {
|
||||
const point = intersects[0].point;
|
||||
// Snap to grid
|
||||
const snapped = new THREE.Vector3(
|
||||
Math.round(point.x / GRID_SIZE) * GRID_SIZE,
|
||||
0,
|
||||
Math.round(point.z / GRID_SIZE) * GRID_SIZE,
|
||||
);
|
||||
|
||||
if (
|
||||
Math.abs(initBase.position.x - snapped.x) > 0.01 ||
|
||||
Math.abs(initBase.position.z - snapped.z) > 0.01
|
||||
) {
|
||||
// Only request render if the position actually changed
|
||||
initBase.position.set(snapped.x, 0, snapped.z);
|
||||
setCursorPosition([snapped.x, snapped.z]); // Update next position for cube creation
|
||||
requestRenderIfNotRequested();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="cubes-scene-container" ref={(el) => (container = el)} />
|
||||
<div class="toolbar-container">
|
||||
<Toolbar>
|
||||
<ToolbarButton
|
||||
name="Select"
|
||||
icon="Cursor"
|
||||
onClick={() => setWorldMode("view")}
|
||||
selected={worldMode() === "view"}
|
||||
/>
|
||||
<ToolbarButton
|
||||
name="new-machine"
|
||||
icon="NewMachine"
|
||||
@@ -945,10 +1079,8 @@ export function CubeScene(props: {
|
||||
onClick={() => {
|
||||
if (positionMode() === "grid") {
|
||||
setPositionMode("circle");
|
||||
grid.visible = false;
|
||||
} else {
|
||||
setPositionMode("grid");
|
||||
grid.visible = true;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
#splash {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: linear-gradient(to top, #e3e7e7, #edf1f1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#splash .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply h-8 mb-8;
|
||||
}
|
||||
|
||||
.loader {
|
||||
@apply h-3 w-60 mb-3;
|
||||
width: 18rem;
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#bfd0d2 0px,
|
||||
#bfd0d2 10px,
|
||||
#f7f9fa 10px,
|
||||
#f7f9fa 20px
|
||||
);
|
||||
animation: stripe-move 1s linear infinite;
|
||||
background-size: 28px 28px; /* Sqrt(20^2 + 20^2) ~= 28 */
|
||||
|
||||
@apply border-2 border-solid rounded-[3px] border-bg-def-1;
|
||||
}
|
||||
|
||||
@keyframes stripe-move {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 28px 0;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Splash } from "./splash";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "scene/splash",
|
||||
component: Splash,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import Logo from "@/logos/darknet-builder-logo.svg";
|
||||
import "./splash.css";
|
||||
import { Typography } from "../components/Typography/Typography";
|
||||
|
||||
export const Splash = () => (
|
||||
<div id="splash">
|
||||
<div class="content">
|
||||
<span class="title">
|
||||
<Logo />
|
||||
</span>
|
||||
<div class="loader"></div>
|
||||
|
||||
<Typography hierarchy="label" size="xs" weight="medium">
|
||||
Loading new Clan
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,18 +1,14 @@
|
||||
import { createStore, produce } from "solid-js/store";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
|
||||
export type SceneData = Record<string, { position: [number, number] }>;
|
||||
|
||||
export interface ClanStoreType {
|
||||
interface ClanStoreType {
|
||||
clanURIs: string[];
|
||||
activeClanURI?: string;
|
||||
sceneData: Record<string, SceneData>;
|
||||
}
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<ClanStoreType>({
|
||||
clanURIs: [],
|
||||
sceneData: {},
|
||||
}),
|
||||
{
|
||||
name: "clanStore",
|
||||
@@ -26,7 +22,7 @@ const [store, setStore] = makePersisted(
|
||||
* @function
|
||||
* @returns {string} The URI of the active clan.
|
||||
*/
|
||||
const activeClanURI = () => store.activeClanURI;
|
||||
const activeClanURI = (): string | undefined => store.activeClanURI;
|
||||
|
||||
/**
|
||||
* Updates the active Clan URI in the store.
|
||||
@@ -49,10 +45,8 @@ const clanURIs = (): string[] => store.clanURIs;
|
||||
* @param {string} uri - The URI of the clan to be added.
|
||||
*
|
||||
*/
|
||||
const addClanURI = (uri: string) => {
|
||||
const addClanURI = (uri: string) =>
|
||||
setStore("clanURIs", store.clanURIs.length, uri);
|
||||
setStore("sceneData", uri, {}); // Initialize empty scene data for every new clan URI
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a specified URI from the clan URI list and updates the active clan URI.
|
||||
@@ -86,7 +80,6 @@ const removeClanURI = (uri: string) => {
|
||||
|
||||
export {
|
||||
store,
|
||||
setStore,
|
||||
activeClanURI,
|
||||
setActiveClanURI,
|
||||
clanURIs,
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
{
|
||||
gtk4,
|
||||
webkitgtk_6_0,
|
||||
lib,
|
||||
clangStdenv,
|
||||
fetchFromGitea,
|
||||
gnumake,
|
||||
cmake,
|
||||
clang-tools,
|
||||
pkg-config,
|
||||
stdenv,
|
||||
...
|
||||
}:
|
||||
{ pkgs, ... }:
|
||||
|
||||
clangStdenv.mkDerivation {
|
||||
pkgs.clangStdenv.mkDerivation {
|
||||
pname = "webview";
|
||||
version = "nightly";
|
||||
|
||||
@@ -20,7 +8,7 @@ clangStdenv.mkDerivation {
|
||||
# We disallow remote connections from the UI on Linux
|
||||
# TODO: Disallow remote connections on MacOS
|
||||
|
||||
src = fetchFromGitea {
|
||||
src = pkgs.fetchFromGitea {
|
||||
domain = "git.clan.lol";
|
||||
owner = "clan";
|
||||
repo = "webview";
|
||||
@@ -49,19 +37,23 @@ clangStdenv.mkDerivation {
|
||||
];
|
||||
|
||||
# Dependencies used during the build process, if any
|
||||
nativeBuildInputs = [
|
||||
nativeBuildInputs = with pkgs; [
|
||||
gnumake
|
||||
cmake
|
||||
clang-tools
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = lib.optionals stdenv.isLinux [
|
||||
webkitgtk_6_0
|
||||
gtk4
|
||||
];
|
||||
buildInputs =
|
||||
with pkgs;
|
||||
[
|
||||
]
|
||||
++ pkgs.lib.optionals stdenv.isLinux [
|
||||
webkitgtk_6_0
|
||||
gtk4
|
||||
];
|
||||
|
||||
meta = with lib; {
|
||||
meta = with pkgs.lib; {
|
||||
description = "Tiny cross-platform webview library for C/C++. Uses WebKit (GTK/Cocoa) and Edge WebView2 (Windows)";
|
||||
homepage = "https://github.com/webview/webview";
|
||||
license = licenses.mit;
|
||||
|
||||
@@ -25,7 +25,6 @@ from .facts import cli as facts
|
||||
from .flash import cli as flash_cli
|
||||
from .hyperlink import help_hyperlink
|
||||
from .machines import cli as machines
|
||||
from .network import cli as network_cli
|
||||
from .profiler import profile
|
||||
from .ssh import deploy_info as ssh_cli
|
||||
from .vars import cli as vars_cli
|
||||
@@ -429,26 +428,6 @@ Examples:
|
||||
)
|
||||
select.register_parser(parser_select)
|
||||
|
||||
parser_network = subparsers.add_parser(
|
||||
"network",
|
||||
aliases=["net"],
|
||||
# TODO: Add help="Manage networks" when network code is ready
|
||||
# help="Manage networks",
|
||||
description="Manage networks",
|
||||
epilog=(
|
||||
"""
|
||||
show information about configured networks
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan network list
|
||||
Will list networks
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
network_cli.register_parser(parser_network)
|
||||
|
||||
parser_state = subparsers.add_parser(
|
||||
"state",
|
||||
aliases=["st"],
|
||||
@@ -483,7 +462,7 @@ For more detailed information, visit: {help_hyperlink("getting-started", "https:
|
||||
state.register_parser(parser_state)
|
||||
|
||||
if argcomplete:
|
||||
argcomplete.autocomplete(parser, exclude=["morph", "network", "net"])
|
||||
argcomplete.autocomplete(parser, exclude=["morph"])
|
||||
|
||||
register_common_flags(parser)
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
# !/usr/bin/env python3
|
||||
import argparse
|
||||
|
||||
from .list import register_list_parser
|
||||
from .overview import register_overview_parser
|
||||
from .ping import register_ping_parser
|
||||
|
||||
|
||||
# takes a (sub)parser and configures it
|
||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
subparser = parser.add_subparsers(
|
||||
title="command",
|
||||
description="the command to run",
|
||||
help="the command to run",
|
||||
required=True,
|
||||
)
|
||||
|
||||
list_parser = subparser.add_parser(
|
||||
"list",
|
||||
help="list all networks",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand allows listing all networks
|
||||
```
|
||||
[NETWORK1] [PRIORITY] [MODULE] [PEER1, PEER2]
|
||||
[NETOWKR2] [PRIORITY] [MODULE] [PEER1, PEER2]
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan network list
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
register_list_parser(list_parser)
|
||||
|
||||
ping_parser = subparser.add_parser(
|
||||
"ping",
|
||||
help="ping a machine to check if it's online",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand allows pinging a machine to check if it's online
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan network ping machine1
|
||||
Check machine1 on all networks (in priority order)
|
||||
|
||||
$ clan network ping machine1 --network tor
|
||||
Check machine1 only on the tor network
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
register_ping_parser(ping_parser)
|
||||
|
||||
overview_parser = subparser.add_parser(
|
||||
"overview",
|
||||
help="show the overview of all network and hosts",
|
||||
epilog=(
|
||||
"""
|
||||
This command shows the complete state of all networks
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan network overview
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
register_overview_parser(overview_parser)
|
||||
@@ -1,64 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.network.network import networks_from_flake
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
flake: Flake = args.flake
|
||||
networks = networks_from_flake(flake)
|
||||
|
||||
if not networks:
|
||||
print("No networks found")
|
||||
return
|
||||
|
||||
# Calculate column widths
|
||||
col_network = max(12, max(len(name) for name in networks))
|
||||
col_priority = 8
|
||||
col_module = max(
|
||||
10, max(len(net.module_name.split(".")[-1]) for net in networks.values())
|
||||
)
|
||||
col_running = 8
|
||||
|
||||
# Print header
|
||||
header = f"{'Network':<{col_network}} {'Priority':<{col_priority}} {'Module':<{col_module}} {'Running':<{col_running}} {'Peers'}"
|
||||
print(header)
|
||||
print("-" * len(header))
|
||||
|
||||
# Print network entries
|
||||
for network_name, network in sorted(
|
||||
networks.items(), key=lambda network: -network[1].priority
|
||||
):
|
||||
# Extract simple module name from full module path
|
||||
module_name = network.module_name.split(".")[-1]
|
||||
|
||||
# Create peer list with truncation
|
||||
peer_names = list(network.peers.keys())
|
||||
max_peers_shown = 3
|
||||
|
||||
if not peer_names:
|
||||
peers_str = "No peers"
|
||||
elif len(peer_names) <= max_peers_shown:
|
||||
peers_str = ", ".join(peer_names)
|
||||
else:
|
||||
shown_peers = peer_names[:max_peers_shown]
|
||||
remaining = len(peer_names) - max_peers_shown
|
||||
peers_str = f"{', '.join(shown_peers)} ...({remaining} more)"
|
||||
|
||||
# Check if network is running
|
||||
try:
|
||||
is_running = network.is_running()
|
||||
running_status = "Yes" if is_running else "No"
|
||||
except Exception:
|
||||
running_status = "Error"
|
||||
|
||||
print(
|
||||
f"{network_name:<{col_network}} {network.priority:<{col_priority}} {module_name:<{col_module}} {running_status:<{col_running}} {peers_str}"
|
||||
)
|
||||
|
||||
|
||||
def register_list_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.set_defaults(func=list_command)
|
||||
@@ -1,21 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.network.network import get_network_overview, networks_from_flake
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def overview_command(args: argparse.Namespace) -> None:
|
||||
flake: Flake = args.flake
|
||||
networks = networks_from_flake(flake)
|
||||
overview = get_network_overview(networks)
|
||||
for network_name, network in overview.items():
|
||||
print(f"{network_name} {'[ONLINE]' if network['status'] else '[OFFLINE]'}")
|
||||
for peer_name, peer in network["peers"].items():
|
||||
print(f"\t{peer_name}: {'[OFFLINE]' if not peer else f'[{peer}]'}")
|
||||
|
||||
|
||||
def register_overview_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.set_defaults(func=overview_command)
|
||||
@@ -1,67 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.network.network import networks_from_flake
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def ping_command(args: argparse.Namespace) -> None:
|
||||
flake: Flake = args.flake
|
||||
machine = args.machine
|
||||
network_name = args.network
|
||||
|
||||
networks = networks_from_flake(flake)
|
||||
|
||||
if not networks:
|
||||
print("No networks found in the flake")
|
||||
|
||||
# If network is specified, only check that network
|
||||
if network_name:
|
||||
networks_to_check = [(network_name, networks[network_name])]
|
||||
|
||||
else:
|
||||
# Sort networks by priority (highest first)
|
||||
networks_to_check = sorted(networks.items(), key=lambda x: -x[1].priority)
|
||||
|
||||
found = False
|
||||
results = []
|
||||
for net_name, network in networks_to_check:
|
||||
if machine in network.peers:
|
||||
found = True
|
||||
|
||||
# Check if network technology is running
|
||||
if not network.is_running():
|
||||
results.append(f"{machine} ({net_name}): network not running")
|
||||
continue
|
||||
|
||||
# Check if peer is online
|
||||
ping = network.ping(machine)
|
||||
results.append(f"{machine} ({net_name}): {ping}")
|
||||
|
||||
if not found:
|
||||
msg = f"Machine '{machine}' not found in any network"
|
||||
raise ClanError(msg)
|
||||
|
||||
# Print all results
|
||||
for result in results:
|
||||
print(result)
|
||||
|
||||
|
||||
def register_ping_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"machine",
|
||||
type=str,
|
||||
help="Machine name to ping",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--network",
|
||||
"-n",
|
||||
type=str,
|
||||
help="Specific network to use for ping (if not specified, checks all networks)",
|
||||
)
|
||||
|
||||
parser.set_defaults(func=ping_command)
|
||||
@@ -1,98 +0,0 @@
|
||||
import contextlib
|
||||
import importlib
|
||||
import inspect
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar, cast
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClassSource:
|
||||
module_name: str
|
||||
file_path: Path
|
||||
object_name: str
|
||||
line_number: int | None = None
|
||||
|
||||
def vscode_clickable_path(self) -> str:
|
||||
"""Return a VSCode-clickable path for the class source."""
|
||||
return (
|
||||
f"{self.module_name}.{self.object_name}: {self.file_path}:{self.line_number}"
|
||||
if self.line_number is not None
|
||||
else f"{self.module_name}.{self.object_name}: {self.file_path}"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.vscode_clickable_path()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.vscode_clickable_path()
|
||||
|
||||
|
||||
def import_with_source[T](
|
||||
module_name: str,
|
||||
class_name: str,
|
||||
base_class: type[T],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> T:
|
||||
"""
|
||||
Import a class from a module and instantiate it with source information.
|
||||
|
||||
This function dynamically imports a class and adds source location metadata
|
||||
that can be used for debugging. The instantiated object will have VSCode-clickable
|
||||
paths in its string representation.
|
||||
|
||||
Args:
|
||||
module_name: The fully qualified module name to import
|
||||
class_name: The name of the class to import from the module
|
||||
base_class: The base class type for type checking
|
||||
*args: Additional positional arguments to pass to the class constructor
|
||||
**kwargs: Additional keyword arguments to pass to the class constructor
|
||||
|
||||
Returns:
|
||||
An instance of the imported class with source information
|
||||
|
||||
Example:
|
||||
>>> from .network import NetworkTechnologyBase, ClassSource
|
||||
>>> tech = import_with_source(
|
||||
... "clan_lib.network.tor",
|
||||
... "NetworkTechnology",
|
||||
... NetworkTechnologyBase
|
||||
... )
|
||||
>>> print(tech) # Outputs: ~/Projects/clan-core/.../tor.py:7
|
||||
"""
|
||||
# Import the module
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
# Get the class from the module
|
||||
cls = getattr(module, class_name)
|
||||
|
||||
# Get the line number of the class definition
|
||||
line_number = None
|
||||
with contextlib.suppress(Exception):
|
||||
line_number = inspect.getsourcelines(cls)[1]
|
||||
|
||||
# Get the file path
|
||||
file_path_str = module.__file__
|
||||
assert file_path_str is not None, f"Module {module_name} file path cannot be None"
|
||||
|
||||
# Make the path relative to home for better readability
|
||||
try:
|
||||
file_path = Path(file_path_str).relative_to(Path.home())
|
||||
file_path = Path("~", file_path)
|
||||
except ValueError:
|
||||
# If not under home directory, use absolute path
|
||||
file_path = Path(file_path_str)
|
||||
|
||||
# Create source information
|
||||
source = ClassSource(
|
||||
module_name=module_name,
|
||||
file_path=file_path,
|
||||
object_name=class_name,
|
||||
line_number=line_number,
|
||||
)
|
||||
|
||||
# Instantiate the class with source information
|
||||
return cast(T, cls(source, *args, **kwargs))
|
||||
@@ -1,145 +0,0 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from clan_lib.import_utils import import_with_source
|
||||
from clan_lib.network.network import NetworkTechnologyBase
|
||||
|
||||
|
||||
def test_import_with_source(tmp_path: Path) -> None:
|
||||
"""Test importing a class with source information."""
|
||||
# Create a temporary module file
|
||||
module_dir = tmp_path / "test_module"
|
||||
module_dir.mkdir()
|
||||
|
||||
# Create __init__.py to make it a package
|
||||
(module_dir / "__init__.py").write_text("")
|
||||
|
||||
# Create a test module with a NetworkTechnology class
|
||||
test_module_path = module_dir / "test_tech.py"
|
||||
test_module_path.write_text(
|
||||
dedent("""
|
||||
from clan_lib.network.network import NetworkTechnologyBase
|
||||
|
||||
class NetworkTechnology(NetworkTechnologyBase):
|
||||
def __init__(self, source):
|
||||
super().__init__(source)
|
||||
self.test_value = "test"
|
||||
|
||||
def is_running(self) -> bool:
|
||||
return True
|
||||
""")
|
||||
)
|
||||
|
||||
# Add the temp directory to sys.path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(tmp_path))
|
||||
|
||||
try:
|
||||
# Import the class using import_with_source
|
||||
instance = import_with_source(
|
||||
"test_module.test_tech",
|
||||
"NetworkTechnology",
|
||||
cast(Any, NetworkTechnologyBase),
|
||||
)
|
||||
|
||||
# Verify the instance is created correctly
|
||||
assert isinstance(instance, NetworkTechnologyBase)
|
||||
assert instance.is_running() is True
|
||||
assert hasattr(instance, "test_value")
|
||||
assert instance.test_value == "test"
|
||||
|
||||
# Verify source information
|
||||
assert instance.source.module_name == "test_module.test_tech"
|
||||
assert instance.source.file_path.name == "test_tech.py"
|
||||
assert instance.source.object_name == "NetworkTechnology"
|
||||
assert instance.source.line_number == 4 # Line where class is defined
|
||||
|
||||
# Test string representations
|
||||
str_repr = str(instance)
|
||||
assert "test_tech.py:" in str_repr
|
||||
assert "NetworkTechnology" in str_repr
|
||||
assert str(instance.source.line_number) in str_repr
|
||||
|
||||
repr_repr = repr(instance)
|
||||
assert "NetworkTechnology" in repr_repr
|
||||
assert "test_tech.py:" in repr_repr
|
||||
assert "test_module.test_tech.NetworkTechnology" in repr_repr
|
||||
|
||||
finally:
|
||||
# Clean up sys.path
|
||||
sys.path.remove(str(tmp_path))
|
||||
|
||||
|
||||
def test_import_with_source_with_args() -> None:
|
||||
"""Test importing a class with additional constructor arguments."""
|
||||
# Create a temporary test file
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
||||
f.write(
|
||||
dedent("""
|
||||
from clan_lib.network.network import NetworkTechnologyBase
|
||||
|
||||
class NetworkTechnology(NetworkTechnologyBase):
|
||||
def __init__(self, source, extra_arg, keyword_arg=None):
|
||||
super().__init__(source)
|
||||
self.extra_arg = extra_arg
|
||||
self.keyword_arg = keyword_arg
|
||||
|
||||
def is_running(self) -> bool:
|
||||
return False
|
||||
""")
|
||||
)
|
||||
temp_file = Path(f.name)
|
||||
|
||||
# Import module dynamically
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
spec = importlib.util.spec_from_file_location("temp_module", temp_file)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["temp_module"] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
try:
|
||||
# Import with additional arguments
|
||||
instance = import_with_source(
|
||||
"temp_module",
|
||||
"NetworkTechnology",
|
||||
cast(Any, NetworkTechnologyBase),
|
||||
"extra_value",
|
||||
keyword_arg="keyword_value",
|
||||
)
|
||||
|
||||
# Verify arguments were passed correctly
|
||||
assert instance.extra_arg == "extra_value" # type: ignore[attr-defined]
|
||||
assert instance.keyword_arg == "keyword_value" # type: ignore[attr-defined]
|
||||
assert instance.source.object_name == "NetworkTechnology"
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
del sys.modules["temp_module"]
|
||||
temp_file.unlink()
|
||||
|
||||
|
||||
def test_import_with_source_module_not_found() -> None:
|
||||
"""Test error handling when module is not found."""
|
||||
with pytest.raises(ModuleNotFoundError):
|
||||
import_with_source(
|
||||
"non_existent_module", "SomeClass", cast(Any, NetworkTechnologyBase)
|
||||
)
|
||||
|
||||
|
||||
def test_import_with_source_class_not_found() -> None:
|
||||
"""Test error handling when class is not found in module."""
|
||||
with pytest.raises(AttributeError):
|
||||
import_with_source(
|
||||
"clan_lib.network.network",
|
||||
"NonExistentClass",
|
||||
cast(Any, NetworkTechnologyBase),
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
from clan_lib.network.network import NetworkTechnologyBase
|
||||
|
||||
|
||||
class NetworkTechnology(NetworkTechnologyBase):
|
||||
"""Direct network connection technology - checks SSH connectivity"""
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Direct connections are always 'running' as they don't require a daemon"""
|
||||
return True
|
||||
@@ -1,156 +0,0 @@
|
||||
import logging
|
||||
import textwrap
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.vars.get import get_machine_var
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.import_utils import ClassSource, import_with_source
|
||||
from clan_lib.ssh.parse import parse_ssh_uri
|
||||
from clan_lib.ssh.remote import Remote, check_machine_ssh_reachable
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Peer:
|
||||
_host: dict[str, str | dict[str, str]]
|
||||
flake: Flake
|
||||
|
||||
@cached_property
|
||||
def host(self) -> str:
|
||||
if "plain" in self._host and isinstance(self._host["plain"], str):
|
||||
return self._host["plain"]
|
||||
if "var" in self._host and isinstance(self._host["var"], dict):
|
||||
_var: dict[str, str] = self._host["var"]
|
||||
machine_name = _var["machine"]
|
||||
generator = _var["generator"]
|
||||
var = get_machine_var(
|
||||
str(self.flake),
|
||||
machine_name,
|
||||
f"{generator}/{_var['file']}",
|
||||
)
|
||||
if not var.exists:
|
||||
msg = (
|
||||
textwrap.dedent(f"""
|
||||
It looks like you added a networking module to your machine, but forgot
|
||||
to deploy your changes. Please run "clan machines update {machine_name}"
|
||||
so that the appropriate vars are generated and deployed properly.
|
||||
""")
|
||||
.rstrip("\n")
|
||||
.lstrip("\n")
|
||||
)
|
||||
raise ClanError(msg)
|
||||
return var.value.decode()
|
||||
msg = f"Unknown Var Type {self._host}"
|
||||
raise ClanError(msg)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetworkTechnologyBase(ABC):
|
||||
source: ClassSource
|
||||
|
||||
@abstractmethod
|
||||
def is_running(self) -> bool:
|
||||
pass
|
||||
|
||||
# TODO this will depend on the network implementation if we do user networking at some point, so it should be abstractmethod
|
||||
def ping(self, peer: Peer) -> None | float:
|
||||
if self.is_running():
|
||||
try:
|
||||
# Parse the peer's host address to create a Remote object, use peer here since we don't have the machine_name here
|
||||
remote = parse_ssh_uri(machine_name="peer", address=peer.host)
|
||||
|
||||
# Use the existing SSH reachability check
|
||||
now = time.time()
|
||||
result = check_machine_ssh_reachable(remote)
|
||||
|
||||
if result.ok:
|
||||
return (time.time() - now) * 1000
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
log.debug(f"Error checking peer {peer.host}: {e}")
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Network:
|
||||
peers: dict[str, Peer]
|
||||
module_name: str
|
||||
priority: int = 1000
|
||||
|
||||
@cached_property
|
||||
def module(self) -> NetworkTechnologyBase:
|
||||
res = import_with_source(
|
||||
self.module_name,
|
||||
"NetworkTechnology",
|
||||
NetworkTechnologyBase, # type: ignore[type-abstract]
|
||||
)
|
||||
return res
|
||||
|
||||
def is_running(self) -> bool:
|
||||
return self.module.is_running()
|
||||
|
||||
def ping(self, peer: str) -> float | None:
|
||||
return self.module.ping(self.peers[peer])
|
||||
|
||||
|
||||
def networks_from_flake(flake: Flake) -> dict[str, Network]:
|
||||
networks: dict[str, Network] = {}
|
||||
networks_ = flake.select("clan.exports.instances.*.networking")
|
||||
for network_name, network in networks_.items():
|
||||
if network:
|
||||
peers: dict[str, Peer] = {}
|
||||
for _peer in network["peers"].values():
|
||||
peers[_peer["name"]] = Peer(_host=_peer["host"], flake=flake)
|
||||
networks[network_name] = Network(
|
||||
peers=peers,
|
||||
module_name=network["module"],
|
||||
priority=network["priority"],
|
||||
)
|
||||
return networks
|
||||
|
||||
|
||||
def get_best_remote(machine_name: str, networks: dict[str, Network]) -> Remote | None:
|
||||
for network_name, network in sorted(
|
||||
networks.items(), key=lambda network: -network[1].priority
|
||||
):
|
||||
if machine_name in network.peers:
|
||||
if network.is_running() and network.ping(machine_name):
|
||||
print(f"connecting via {network_name}")
|
||||
return Remote.from_ssh_uri(
|
||||
machine_name=machine_name,
|
||||
address=network.peers[machine_name].host,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def get_network_overview(networks: dict[str, Network]) -> dict:
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for network_name, network in networks.items():
|
||||
result[network_name] = {}
|
||||
result[network_name]["status"] = None
|
||||
result[network_name]["peers"] = {}
|
||||
network_online = False
|
||||
module = network.module
|
||||
log.debug(f"Using network module: {module}")
|
||||
if module.is_running():
|
||||
result[network_name]["status"] = True
|
||||
network_online = True
|
||||
for peer_name in network.peers:
|
||||
if network_online:
|
||||
try:
|
||||
result[network_name]["peers"][peer_name] = network.ping(peer_name)
|
||||
except ClanError:
|
||||
log.warning(
|
||||
f"getting host for machine: {peer_name} in network: {network_name} failed"
|
||||
)
|
||||
else:
|
||||
result[network_name]["peers"][peer_name] = None
|
||||
return result
|
||||
@@ -1,106 +0,0 @@
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.network.network import Network, Peer, networks_from_flake
|
||||
|
||||
|
||||
@patch("clan_lib.network.network.get_machine_var")
|
||||
def test_networks_from_flake(mock_get_machine_var: MagicMock) -> None:
|
||||
# Create a mock flake
|
||||
flake = MagicMock(spec=Flake)
|
||||
|
||||
# Mock the var decryption
|
||||
def mock_var_side_effect(flake_path: str, machine: str, var_path: str) -> Any:
|
||||
if machine == "machine1" and var_path == "wireguard/address":
|
||||
mock_var = MagicMock()
|
||||
mock_var.value.decode.return_value = "192.168.1.10"
|
||||
return mock_var
|
||||
if machine == "machine2" and var_path == "wireguard/address":
|
||||
mock_var = MagicMock()
|
||||
mock_var.value.decode.return_value = "192.168.1.11"
|
||||
return mock_var
|
||||
return None
|
||||
|
||||
mock_get_machine_var.side_effect = mock_var_side_effect
|
||||
|
||||
# Define the expected return value from flake.select
|
||||
mock_networking_data = {
|
||||
"vpn-network": {
|
||||
"peers": {
|
||||
"machine1": {
|
||||
"name": "machine1",
|
||||
"host": {
|
||||
"var": {
|
||||
"machine": "machine1",
|
||||
"generator": "wireguard",
|
||||
"file": "address",
|
||||
}
|
||||
},
|
||||
},
|
||||
"machine2": {
|
||||
"name": "machine2",
|
||||
"host": {
|
||||
"var": {
|
||||
"machine": "machine2",
|
||||
"generator": "wireguard",
|
||||
"file": "address",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"module": "clan_lib.network.tor",
|
||||
"priority": 1000,
|
||||
},
|
||||
"local-network": {
|
||||
"peers": {
|
||||
"machine1": {
|
||||
"name": "machine1",
|
||||
"host": {"plain": "10.0.0.10"},
|
||||
},
|
||||
"machine3": {
|
||||
"name": "machine3",
|
||||
"host": {"plain": "10.0.0.12"},
|
||||
},
|
||||
},
|
||||
"module": "clan_lib.network.direct",
|
||||
"priority": 500,
|
||||
},
|
||||
}
|
||||
|
||||
# Mock the select method
|
||||
flake.select.return_value = mock_networking_data
|
||||
|
||||
# Call the function
|
||||
networks = networks_from_flake(flake)
|
||||
|
||||
# Verify the flake.select was called with the correct pattern
|
||||
flake.select.assert_called_once_with("clan.exports.instances.*.networking")
|
||||
|
||||
# Verify the returned networks
|
||||
assert len(networks) == 2
|
||||
assert "vpn-network" in networks
|
||||
assert "local-network" in networks
|
||||
|
||||
# Check vpn-network
|
||||
vpn_network = networks["vpn-network"]
|
||||
assert isinstance(vpn_network, Network)
|
||||
assert vpn_network.module_name == "clan_lib.network.tor"
|
||||
assert vpn_network.priority == 1000
|
||||
assert len(vpn_network.peers) == 2
|
||||
assert "machine1" in vpn_network.peers
|
||||
assert "machine2" in vpn_network.peers
|
||||
|
||||
# Check peer details - this will call get_machine_var to decrypt the var
|
||||
machine1_peer = vpn_network.peers["machine1"]
|
||||
assert isinstance(machine1_peer, Peer)
|
||||
assert machine1_peer.host == "192.168.1.10"
|
||||
assert machine1_peer.flake == flake
|
||||
|
||||
# Check local-network
|
||||
local_network = networks["local-network"]
|
||||
assert local_network.module_name == "clan_lib.network.direct"
|
||||
assert local_network.priority == 500
|
||||
assert len(local_network.peers) == 2
|
||||
assert "machine1" in local_network.peers
|
||||
assert "machine3" in local_network.peers
|
||||
@@ -1,20 +0,0 @@
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import urlopen
|
||||
|
||||
from .network import NetworkTechnologyBase
|
||||
|
||||
|
||||
class NetworkTechnology(NetworkTechnologyBase):
|
||||
socks_port: int
|
||||
command_port: int
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Check if Tor is running by sending HTTP request to SOCKS port."""
|
||||
try:
|
||||
response = urlopen("http://127.0.0.1:9050", timeout=5)
|
||||
content = response.read().decode("utf-8", errors="ignore")
|
||||
return "tor" in content.lower()
|
||||
except HTTPError as e:
|
||||
return "tor" in str(e).lower()
|
||||
except Exception:
|
||||
return False
|
||||
@@ -139,7 +139,7 @@ class InventoryStore:
|
||||
def _load_merged_inventory(self) -> InventorySnapshot:
|
||||
"""
|
||||
Loads the evaluated inventory.
|
||||
After all merge operations with eventual nix code in lib.clan.
|
||||
After all merge operations with eventual nix code in buildClan.
|
||||
|
||||
Evaluates clanInternals.inventoryClass.inventory with nix. Which is performant.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
|
||||
from clan_cli.cli import create_parser
|
||||
|
||||
hidden_subcommands = ["machine", "b", "f", "m", "se", "st", "va", "net", "network"]
|
||||
hidden_subcommands = ["machine", "b", "f", "m", "se", "st", "va"]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -3,18 +3,12 @@
|
||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
clan-core,
|
||||
nixpkgs,
|
||||
...
|
||||
}@inputs:
|
||||
{ self, clan-core, ... }:
|
||||
let
|
||||
# Usage see: https://docs.clan.lol
|
||||
clan = clan-core.lib.clan {
|
||||
inherit self;
|
||||
imports = [ ./clan.nix ];
|
||||
specialArgs = { inherit inputs; };
|
||||
};
|
||||
in
|
||||
{
|
||||
@@ -22,7 +16,7 @@
|
||||
# Add the Clan cli tool to the dev shell.
|
||||
# Use "nix develop" to enter the dev shell.
|
||||
devShells =
|
||||
nixpkgs.lib.genAttrs
|
||||
clan-core.inputs.nixpkgs.lib.genAttrs
|
||||
[
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
|
||||
Reference in New Issue
Block a user