Compare commits

...

131 Commits

Author SHA1 Message Date
9490a06303 Merge branch 'automated/npins-update-20260123' into 'main'
chore: update npins dependencies

See merge request oceanbox/Poseidon!147
2026-01-23 13:34:02 +01:00
5fb1ae0678 Merge branch 'main' into 'automated/npins-update-20260123'
# Conflicts:
#   nix/sources.json
2026-01-23 13:13:42 +01:00
97c03e216b chore: update npins dependencies
Automated update of Nix dependencies via npins.

    Updated packages:
    +      "hash": "sha256-XH6awru9NnBc/m+2YhRNT8r1PAKEiPGF3gs//F3ods0="
+      "revision": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
+      "hash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U="
2026-01-23 12:11:33 +00:00
semantic-release-bot
bab4490847 chore(release): 1.40.5
## [1.40.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.4...v1.40.5) (2026-01-21)

### Bug Fixes

* **xtractor:** Reduce to 4 cores per task ([8e824d4](8e824d4afa))
2026-01-21 12:52:40 +00:00
8e824d4afa fix(xtractor): Reduce to 4 cores per task 2026-01-21 13:49:19 +01:00
semantic-release-bot
777cf1a31d chore(release): 1.40.4
## [1.40.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.3...v1.40.4) (2026-01-21)

### Bug Fixes

* **xtractor:** Reduce core requirement to 8 ([efacb2a](efacb2a332))
2026-01-21 09:41:49 +00:00
efacb2a332 fix(xtractor): Reduce core requirement to 8
This means we can run 8 in parallelle on the 64 core nodes.
Since it's very IO heavy.
2026-01-21 10:38:06 +01:00
semantic-release-bot
17c4e9dd22 chore(release): 1.40.3
## [1.40.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.2...v1.40.3) (2026-01-20)

### Bug Fixes

* **inbox|xtracto:** Delete/Read msg and allow non-ascii xtractor names ([d8d5e07](d8d5e076ba))
* **multiauth:** Add clientId to redirect on signout ([54c40d7](54c40d7acc))
2026-01-20 14:41:54 +00:00
503ccbb2ad Merge branch 'mrtz/oae' into 'main'
fix(inbox|xtractor): Delete/Read msg and allow non-ascii xtractor names

See merge request oceanbox/Poseidon!146
2026-01-20 15:39:09 +01:00
54c40d7acc fix(multiauth): Add clientId to redirect on signout
Previously we used `id_token_hint`, but it's saved in the cookie.
This will instead require a client_id (which identifies your application),
so Keycloak knows which application you’re requesting a redirect for.
2026-01-20 14:04:46 +01:00
d8d5e076ba fix(inbox|xtracto): Delete/Read msg and allow non-ascii xtractor names 2026-01-20 13:35:48 +01:00
semantic-release-bot
fd2b3fe691 chore(release): 1.40.2
## [1.40.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.1...v1.40.2) (2026-01-19)

### Bug Fixes

* **xtract:** Disabled if not allowed to simulate transport ([e429a85](e429a855e5))
2026-01-19 19:06:55 +00:00
6ae7a7dac8 Merge branch 'mrtz/disable-xtract' into 'main'
fix(xtract): Disabled if not allowed to simulate transport

See merge request oceanbox/Poseidon!145
2026-01-19 20:04:00 +01:00
e429a855e5 fix(xtract): Disabled if not allowed to simulate transport 2026-01-19 19:43:24 +01:00
semantic-release-bot
9ed60b7cc8 chore(release): 1.40.1
## [1.40.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.0...v1.40.1) (2026-01-19)

### Bug Fixes

* fix tilt build on net10 ([d5cde19](d5cde19250))
2026-01-19 16:15:18 +00:00
Stig Rune Jensen
175df0ce33 Merge branch 'stigrj/fix-net10' into 'main'
fix: fix tilt build on net10

See merge request oceanbox/Poseidon!144
2026-01-19 16:12:37 +00:00
Stig Rune Jensen
d5cde19250 fix: fix tilt build on net10 2026-01-19 16:25:45 +01:00
2e1165d99c Release codex 0.0.1 2026-01-19 16:04:29 +01:00
a100ffa77b atlantis: Revert renaming of atlantis tilt ing.
Sryy!
2026-01-19 15:46:59 +01:00
95c3608d85 Merge branch 'simkir/codex' into 'main'
Codex updates

See merge request oceanbox/Poseidon!142
2026-01-19 14:39:35 +01:00
15d91d87bf codex: Start showing archives for user
Currently only under group/user, since most archive access is only
gained through the group.
2026-01-19 11:10:11 +01:00
f6ec692ebf codex: Require user group objects to be a group type 2026-01-19 11:10:11 +01:00
f41617c08e codex: Switch fga checkbox spinner with FS.FluentUI 2026-01-19 11:10:11 +01:00
2bf0d82a5b codex: Allow for update of group archive permissions 2026-01-19 11:10:11 +01:00
3e61cfb939 codex: Switch delete archive permission button with Fui 2026-01-19 11:04:55 +01:00
21ec3a04ab codex: Add securityContext to pod 2026-01-19 11:04:55 +01:00
5d6fe5572b codex: Add ability to update user permissions
- Active
- Registered
- Disabled
2026-01-19 11:04:31 +01:00
0a543c7b21 sorcerer: Switch to wildcard allow origin in appsettings 2026-01-19 10:36:41 +01:00
b1ba2effe3 sorcerer: Point tilt sorcerer on staging openfga 2026-01-19 10:36:41 +01:00
626ce34dc0 serverpack: Add note about cookie token option 2026-01-19 10:36:41 +01:00
b879555e6a serverpack: Refactor multiauth sso cookie on validate principal 2026-01-19 10:36:41 +01:00
ed08980df3 codex: Do a dns lookup on openfga url on startup 2026-01-19 10:36:41 +01:00
6aee2bbc60 Format atlantis tilt values.yaml to 2 spaces 2026-01-19 10:36:41 +01:00
d4701c958c Add ports to atlantis allow origin tilt appsettings 2026-01-19 10:36:41 +01:00
446d4f4171 Refactor Atlantis server 2026-01-19 10:36:41 +01:00
1ed2a15c4c Add *.yaml to .editorconfig 2026-01-19 10:36:41 +01:00
42b746871a codex: Refactor OpenFga.useObjects 2026-01-19 10:36:41 +01:00
6c20b01cc2 codex: Remove redundant Router 2026-01-19 10:36:41 +01:00
4f879252a0 codex: Show group memberships no user page
And add group user, which shows a link to the group in the page.
2026-01-19 10:36:41 +01:00
semantic-release-bot
11724987b0 chore(release): 1.40.0
# [1.40.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.2...v1.40.0) (2026-01-16)

### Bug Fixes

* **Codex:** expose days instead of frames in arcive form ([6cf5262](6cf5262dd5))
* **Codex:** only allow inbounds time intervals on edit archive ([eaea4b2](eaea4b2e21))

### Features

* **Codex:** edit archives ([ec10932](ec109328fb))
2026-01-16 19:06:19 +00:00
d90703453f Merge branch 'ole/update-archive' into 'main'
Ole/update archive

See merge request oceanbox/Poseidon!143
2026-01-16 20:03:17 +01:00
eaea4b2e21 fix(Codex): only allow inbounds time intervals on edit archive 2026-01-16 10:08:40 +01:00
semantic-release-bot
68efc76e8e chore(release): 1.39.2
## [1.39.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.1...v1.39.2) (2026-01-15)

### Bug Fixes

* fix net10 issues ([f4943a1](f4943a148b))
2026-01-15 17:00:22 +00:00
6df17c88c7 Merge branch 'mrtz/net10' into 'main'
chore: Bump to net10

See merge request oceanbox/Poseidon!141
2026-01-15 17:57:24 +01:00
ec109328fb feat(Codex): edit archives 2026-01-15 17:52:47 +01:00
Stig Rune Jensen
f4943a148b fix: fix net10 issues 2026-01-15 17:47:02 +01:00
15e348c17b chore: Bump to net10
Also bumps gitlab ci to v4.5
2026-01-15 15:50:29 +01:00
6cf5262dd5 fix(Codex): expose days instead of frames in arcive form 2026-01-15 10:30:56 +01:00
semantic-release-bot
4b229cd7d7 chore(release): 1.39.1
## [1.39.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.0...v1.39.1) (2026-01-15)

### Bug Fixes

* **ci:** Remove schedule check ([ab37e88](ab37e88bb0))
2026-01-15 07:36:06 +00:00
e372be192a Release codex 0.0.1-beta.4 2026-01-15 08:32:49 +01:00
ab37e88bb0 fix(ci): Remove schedule check 2026-01-14 19:12:08 +01:00
semantic-release-bot
d38a784326 chore(release): 1.39.0
# [1.39.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.5...v1.39.0) (2026-01-14)

### Bug Fixes

* **Codex:** add * user in archmeister on public archives ([cd678a4](cd678a41f6))
* **Codex:** use feliz router guid matching ([7182f7c](7182f7c9f0))
* **Codex:** utc start_time ([492651e](492651e0f3))
* **DataAgent:** use files from parent attribs instead of archive_files ([eac23e7](eac23e7d1a))

### Features

* **Codex:** ability to add FVCOM archives ([d86db7a](d86db7a66c))
2026-01-14 15:55:00 +00:00
4d18b105c8 Merge branch 'ole/add-archive' into 'main'
Ole/add archive

See merge request oceanbox/Poseidon!140
2026-01-14 16:51:53 +01:00
492651e0f3 fix(Codex): utc start_time 2026-01-14 16:42:57 +01:00
cd678a41f6 fix(Codex): add * user in archmeister on public archives 2026-01-14 12:31:55 +01:00
03fbc14b72 Merge branch 'simkir/rider-sdk-script' into 'main'
Add script for updating which dotnet sdk rider uses

See merge request oceanbox/Poseidon!136
2026-01-14 10:34:13 +01:00
d86db7a66c feat(Codex): ability to add FVCOM archives 2026-01-14 10:23:17 +01:00
7182f7c9f0 fix(Codex): use feliz router guid matching 2026-01-14 10:23:16 +01:00
eac23e7d1a fix(DataAgent): use files from parent attribs instead of archive_files 2026-01-14 10:17:44 +01:00
b500fdb211 devel: correct justfile publish path 2026-01-14 10:17:44 +01:00
180aba4fa5 devel: point tilt codex to staging fga 2026-01-14 10:17:44 +01:00
semantic-release-bot
d9d7221e90 chore(release): 1.38.5
## [1.38.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.4...v1.38.5) (2026-01-13)

### Bug Fixes

* **Archmaester:** Rollback add archive if openfga fails ([46e86eb](46e86eb5f9))
2026-01-13 12:37:09 +00:00
b3fe6f8b70 Merge branch 'simkir/archive-add-rollback' into 'main'
Rollback add archive if openfga errors adding permissions

See merge request oceanbox/Poseidon!139
2026-01-13 13:34:31 +01:00
4792720d74 Small hipster refactoring 2026-01-13 11:20:54 +01:00
46e86eb5f9 fix(Archmaester): Rollback add archive if openfga fails 2026-01-13 11:20:54 +01:00
9a890f30fc Merge branch 'simkir/codex' into 'main'
Add ability to add group archive exec ticket in Codex

See merge request oceanbox/Poseidon!138
2026-01-12 17:40:44 +01:00
66c44976d8 Release codex 0.0.1-beta.3 2026-01-12 17:33:07 +01:00
608caeeda2 codex: Add ability to add group archive exec ticket
Also fix some state in the group archive permissions view
2026-01-12 17:33:07 +01:00
55bcaaf963 Release codex 0.0.1-beta.2 2026-01-12 14:45:20 +01:00
semantic-release-bot
ce10ea93db chore(release): 1.38.4
## [1.38.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.3...v1.38.4) (2026-01-12)

### Bug Fixes

* **nix:** Bump node deps ([e513d87](e513d87d24))
2026-01-12 13:44:08 +00:00
e513d87d24 fix(nix): Bump node deps 2026-01-12 14:41:00 +01:00
semantic-release-bot
08061bc6ce chore(release): 1.38.3
## [1.38.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.2...v1.38.3) (2026-01-12)

### Bug Fixes

* **xtractor:** Set maxEndDate to 1 year and format ([5315a05](5315a05fa2))
2026-01-12 13:31:57 +00:00
dd55a0c9df Merge branch 'mrtz/fix-xtractor-2' into 'main'
fix(xtractor): Set maxEndDate to 1 year and format

See merge request oceanbox/Poseidon!137
2026-01-12 14:29:48 +01:00
89f0f768e3 Merge branch 'simkir/codex' into 'main'
Ability to add new view permission to group archive

See merge request oceanbox/Poseidon!133
2026-01-12 14:25:39 +01:00
5315a05fa2 fix(xtractor): Set maxEndDate to 1 year and format 2026-01-12 14:22:34 +01:00
57b28daf4e Add script for updating which dotnet sdk rider uses 2026-01-12 13:51:16 +01:00
semantic-release-bot
0ba060d78c chore(release): 1.38.2
## [1.38.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.1...v1.38.2) (2026-01-12)

### Bug Fixes

* **atlas:** Disable Snowflakes ([4cd3673](4cd3673d15))
2026-01-12 11:54:28 +00:00
Stig Rune Jensen
b2077ae317 Merge branch 'mrtz/no-snow' into 'main'
fix(atlas): Disable Snowflakes

See merge request oceanbox/Poseidon!135
2026-01-12 11:51:48 +00:00
4cd3673d15 fix(atlas): Disable Snowflakes 2026-01-12 09:07:44 +01:00
semantic-release-bot
771712ad9a chore(release): 1.38.1
## [1.38.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.0...v1.38.1) (2026-01-11)

### Bug Fixes

* **xtractor:** Move to short partition and 16 CPU, also set max duration to 1 year ([56d3476](56d34767d7))
2026-01-11 14:26:39 +00:00
1d6941ecc6 Merge branch 'mrtz/xtract-fixes' into 'main'
fix(xtractor): Move to short partition and 16 CPU, also set max duration to 1 year

See merge request oceanbox/Poseidon!134
2026-01-11 15:23:14 +01:00
56d34767d7 fix(xtractor): Move to short partition and 16 CPU, also set max duration to 1 year 2026-01-11 14:04:01 +01:00
5fbd914e24 codex: Fix fetching archives user's can exec 2026-01-09 16:04:52 +01:00
5f193c559f codex: Add permission to group archive
Hook the Add permission button, and make it post the new tuples.

Still need to properly load the new permissions, so that you do not need
to refresh.
2026-01-09 16:03:03 +01:00
a998483d2c codex: Fix sidebar and content growing 2026-01-09 16:01:48 +01:00
semantic-release-bot
a4159f0fff chore(release): 1.38.0
# [1.38.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.37.1...v1.38.0) (2026-01-07)

### Bug Fixes

* client layout ([efb3292](efb3292d9f))

### Features

* add fluent ui to codex ([d8bf174](d8bf174d3a))
2026-01-07 13:28:36 +00:00
866f3a317b Merge branch 'ole/codex-fui' into 'main'
feat: add fluent ui to codex

See merge request oceanbox/Poseidon!132
2026-01-07 14:25:30 +01:00
4eac05cbb7 chore: bump codex do beta 2026-01-07 14:13:53 +01:00
efb3292d9f fix: client layout 2026-01-07 13:49:26 +01:00
6e822bd5d1 devel: template name into tilt manifest 2026-01-07 13:49:11 +01:00
d8bf174d3a feat: add fluent ui to codex 2026-01-07 13:09:20 +01:00
2f7be7b051 devel: downgrade to fable 4.26 2026-01-05 16:11:47 +01:00
18bb207e4a Merge branch 'ole/update-bootstrap' into 'main'
devel: update bootstrap guide and justfile

See merge request oceanbox/Poseidon!130
2026-01-05 15:00:27 +01:00
c914f4a477 devel(justfile): remove cwd argument 2026-01-05 14:56:35 +01:00
2da1be0c6b devel: update bootstrap guide 2026-01-05 14:54:23 +01:00
eb00b8c19d devel: install node deps in build script 2026-01-05 14:54:23 +01:00
09a9e47348 devel: add net9 to nix shell
until we bump projects to net10, include net9 in the dev shell so we can
run it
2026-01-05 14:52:13 +01:00
156ae2315a chore: remove unused redis templates 2026-01-05 14:52:13 +01:00
semantic-release-bot
cf6bedbd9b chore(release): 1.37.1
## [1.37.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.37.0...v1.37.1) (2026-01-05)

### Bug Fixes

* **xtractor:** Avoid using union types in Dapr Actors ([14c1a57](14c1a57331))
2026-01-05 13:21:22 +00:00
36aa90519e Merge branch 'mrtz/fix-xtractor' into 'main'
fix(xtractor): Avoid using union types in Dapr Actors

See merge request oceanbox/Poseidon!129
2026-01-05 14:16:25 +01:00
14c1a57331 fix(xtractor): Avoid using union types in Dapr Actors
Instead of using union types in the Xtractor we fetch
the individual types instead.
2026-01-05 14:10:26 +01:00
d7f0630693 Merge branch 'automated/npins-update-20260102' into 'main'
chore: update npins dependencies

See merge request oceanbox/Poseidon!128
2026-01-02 20:15:32 +01:00
3b7149f161 chore: update npins dependencies
Automated update of Nix dependencies via npins.

    Updated packages:
    +      "hash": "sha256-IVq6jxkcTuudaj3c78xl2xG2fZSL9gS7JMPFUl3q7j4="
+      "revision": "f0927703b7b1c8d97511c4116eb9b4ec6645a0fa",
+      "hash": "sha256-6MkqajPICgugsuZ92OMoQcgSHnD6sJHwk8AxvMcIgTE="
2026-01-02 18:25:04 +00:00
5545e90160 Merge branch 'automated/npins-update-20251229' into 'main'
chore: update npins dependencies

See merge request oceanbox/Poseidon!127
2025-12-29 18:03:40 +01:00
da5b38d1ea chore: update npins dependencies
Automated update of Nix dependencies via npins.

    Updated packages:
    +      "hash": "sha256-wyT7Pl6tMFbFrs8Lk/TlEs81N6L+VSybPfiIgzU8lbQ="
+      "hash": "sha256-y++BijM+FRkKDhVrL7YXZQiJ0DNVMiRN7yHf6QIXBUI="
+      "hash": "sha256-g5DRB9fAyEv6Xf41Bj9RpVl9th0Zz+v1jgvJVg51W3w="
+      "revision": "b68b780b69702a090c8bb1b973bab13756cc7a27",
+      "hash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0="
2025-12-29 17:02:08 +00:00
65928c4064 ci: Add schedueled updater 2025-12-29 17:57:27 +01:00
semantic-release-bot
033b61dd4f chore(release): 1.37.0
# [1.37.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.36.0...v1.37.0) (2025-12-22)

### Features

* Add XtractActor ([a8a187a](a8a187a412))
2025-12-22 12:15:31 +00:00
5725d43b11 Merge branch 'mrtz/plumxcavator' into 'main'
feat: Add XtractActor

Closes #92

See merge request oceanbox/Poseidon!126
2025-12-22 13:09:13 +01:00
a8a187a412 feat: Add XtractActor
Data extraction from archives. It utilizes Excavator
and some python scripts to extract data from the existing
archives and summarizes it in plots and csv files.

Currently it has a basic UI only supporting, name,
start/stop and coordinates as inputs. Written in an
extandable way for future data extraction methods.
2025-12-19 16:46:45 +01:00
semantic-release-bot
f30e16b15e chore(release): 1.36.0
# [1.36.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.35.2...v1.36.0) (2025-12-12)

### Features

* add Saturn.ReverseProxy middleware ([bd74504](bd745042df))
2025-12-12 13:49:34 +00:00
bd745042df feat: add Saturn.ReverseProxy middleware 2025-12-12 14:45:53 +01:00
70878e1423 chore: Bump deps 2025-12-09 12:58:14 +01:00
937b2c367b devel: Fix justfile for client 2025-12-09 10:10:11 +01:00
faa0a8533e Merge branch 'mrtz/just' into 'main'
devel: Migrate from FAKE to Just

See merge request oceanbox/Poseidon!125
2025-12-06 11:33:38 +01:00
1cb9d455db devel: Migrate from FAKE to Just
Also updates dotnetPackage CIs to v4.4
2025-12-06 09:39:59 +01:00
semantic-release-bot
453c9d234c chore(release): 1.35.2
## [1.35.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.35.1...v1.35.2) (2025-12-01)

### Bug Fixes

* **drifters:** reverse toggle on postdrift analysis ([563faa6](563faa6c0b))
2025-12-01 18:27:24 +00:00
369127e081 Merge branch 'stigrj/fix-rev' into 'main'
fix(drifters): reverse toggle on postdrift analysis

See merge request oceanbox/Poseidon!124
2025-12-01 19:23:58 +01:00
Stig Rune Jensen
563faa6c0b fix(drifters): reverse toggle on postdrift analysis 2025-12-01 15:01:16 +01:00
semantic-release-bot
17163ab002 chore(release): 1.35.1
## [1.35.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.35.0...v1.35.1) (2025-12-01)

### Bug Fixes

* **stats:** no stats banner showing when stats are available, closing [#87](https://gitlab.com/oceanbox/Poseidon/issues/87) ([e75ffc4](e75ffc41e5))
* **stats:** set priority order of depth plots, closing [#88](https://gitlab.com/oceanbox/Poseidon/issues/88) ([2887e6a](2887e6a909))
2025-12-01 13:33:48 +00:00
Stig Rune Jensen
6293e9e67a Merge branch 'stigrj/fix-stats' into 'main'
Fix: minor release bugs

Closes #88 and #87

See merge request oceanbox/Poseidon!123
2025-12-01 13:30:10 +00:00
semantic-release-bot
a68ef32614 chore(release): 1.35.0
# [1.35.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.34.2...v1.35.0) (2025-12-01)

### Features

* ❄️ ([a620c26](a620c26812))
2025-12-01 13:11:49 +00:00
4de10614be Merge branch 'mrtz/flakes' into 'main'
feat: ❄️

See merge request oceanbox/Poseidon!122
2025-12-01 14:08:05 +01:00
Stig Rune Jensen
2887e6a909 fix(stats): set priority order of depth plots, closing #88 2025-12-01 13:37:05 +01:00
Stig Rune Jensen
e75ffc41e5 fix(stats): no stats banner showing when stats are available, closing #87 2025-12-01 10:37:03 +01:00
Moritz Jörg
a620c26812 feat: ❄️ 2025-11-30 13:13:45 +01:00
semantic-release-bot
f2bb57b50d chore(release): 1.34.2
## [1.34.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.34.1...v1.34.2) (2025-11-30)

### Bug Fixes

* Temporarily use vtn as only source for wind barbs ([048e803](048e80356b))
2025-11-30 11:36:13 +00:00
8e0cb2105a Merge branch 'mrtz/vtn-arome' into 'main'
fix: Temporarily use vtn as only source for wind barbs

See merge request oceanbox/Poseidon!121
2025-11-30 12:32:35 +01:00
Moritz Jörg
048e80356b fix: Temporarily use vtn as only source for wind barbs 2025-11-30 12:11:14 +01:00
semantic-release-bot
a47fb89143 chore(release): 1.34.1
## [1.34.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.34.0...v1.34.1) (2025-11-29)

### Bug Fixes

* **stats:** find available stats archives ([48d46ed](48d46eda62))
2025-11-29 15:32:26 +00:00
Stig Rune Jensen
1b8167c66e Merge branch 'stigrj/fix-stats' into 'main'
fix(stats): find available stats archives

See merge request oceanbox/Poseidon!120
2025-11-29 15:14:50 +00:00
Stig Rune Jensen
48d46eda62 fix(stats): find available stats archives 2025-11-29 15:35:09 +01:00
166 changed files with 6276 additions and 3512 deletions

View File

@@ -1,37 +0,0 @@
open Fake.Core
open Fake.IO
open Farmer
open Farmer.Builders
open Helpers
initializeContext()
let packPath = Path.getFullName "packages"
Target.create "Clean" (fun _ -> Shell.cleanDir packPath)
Target.create "InstallClient" (fun _ ->
run dotnet "tool restore" "."
)
Target.create "Run" ignore
Target.create "Format" (fun _ ->
run dotnet "fantomas . -r" "src"
)
open Fake.Core.TargetOperators
let dependencies = [
"Clean"
==> "InstallClient"
"Run"
==> "InstallClient"
"Format"
]
[<EntryPoint>]
let main args = runOrDefault args

View File

@@ -1,129 +0,0 @@
module Helpers
open Fake.Core
let initializeContext () =
let execContext = Context.FakeExecutionContext.Create false "build.fsx" [ ]
Context.setExecutionContext (Context.RuntimeContext.Fake execContext)
module Proc =
module Parallel =
open System
let locker = obj()
let colors =
[| ConsoleColor.Blue
ConsoleColor.Yellow
ConsoleColor.Magenta
ConsoleColor.Cyan
ConsoleColor.DarkBlue
ConsoleColor.DarkYellow
ConsoleColor.DarkMagenta
ConsoleColor.DarkCyan |]
let print color (colored: string) (line: string) =
lock locker
(fun () ->
let currentColor = Console.ForegroundColor
Console.ForegroundColor <- color
Console.Write colored
Console.ForegroundColor <- currentColor
Console.WriteLine line)
let onStdout index name (line: string) =
let color = colors.[index % colors.Length]
if isNull line then
print color $"{name}: --- END ---" ""
else if String.isNotNullOrEmpty line then
print color $"{name}: " line
let onStderr name (line: string) =
let color = ConsoleColor.Red
if isNull line |> not then
print color $"{name}: " line
let redirect (index, (name, createProcess)) =
createProcess
|> CreateProcess.redirectOutputIfNotRedirected
|> CreateProcess.withOutputEvents (onStdout index name) (onStderr name)
let printStarting indexed =
for (index, (name, c: CreateProcess<_>)) in indexed do
let color = colors.[index % colors.Length]
let wd =
c.WorkingDirectory
|> Option.defaultValue ""
let exe = c.Command.Executable
let args = c.Command.Arguments.ToStartInfo
print color $"{name}: {wd}> {exe} {args}" ""
let run cs =
cs
|> Seq.toArray
|> Array.indexed
|> fun x -> printStarting x; x
|> Array.map redirect
|> Array.Parallel.map Proc.run
let createProcess exe arg dir =
CreateProcess.fromRawCommandLine exe arg
|> CreateProcess.withWorkingDirectory dir
|> CreateProcess.ensureExitCode
let dotnet = createProcess "dotnet"
// NOTE: Uses dotnet-tools from nixpkgs
let fable = createProcess "fable"
let fantomas = createProcess "fantomas"
let bun =
let bunPath =
match ProcessUtils.tryFindFileOnPath "bun" with
| Some path -> path
| None ->
"bun was not found in path. Please install it and make sure it's available from your path. " +
"See https://safe-stack.github.io/docs/quickstart/#install-pre-requisites for more info"
|> failwith
createProcess bunPath
let bunx = createProcess "bunx"
type BundleMode =
| Prod
| Devel
| Watch
with
override this.ToString() =
match this with
| Prod -> "production"
| Devel -> "development"
| Watch -> "watch"
let viteCmd (m: BundleMode) outDir =
match m with
| Prod -> $"vite build -c ../../vite.config.js -m {m} --emptyOutDir --outDir {outDir}/public"
| Devel -> $"vite build -c ../../vite.config.js -m {m} --minify false --sourcemap true --emptyOutDir --outDir {outDir}/public"
| Watch -> "vite -c ../../vite.config.js"
let run proc arg dir =
proc arg dir
|> Proc.run
|> ignore
let runParallel processes =
processes
|> Proc.Parallel.run
|> ignore
let runOrDefault args =
try
match args with
| [| target |] -> Target.runOrDefault target
| _ ->
Target.runOrDefault "Run"
0
with e ->
printfn "%A" e
1

View File

@@ -23,6 +23,9 @@ max_line_length= 80
indent_size = 2
max_line_length = 80
[*.yaml]
indent_size = 2
[*.fs]
max_line_length= 120

35
.envrc
View File

@@ -1,4 +1,8 @@
#!/usr/bin/env bash
export NPINS_DIRECTORY="nix"
export APP_ENV=$USER
# the shebang is ignored, but nice for editors
watch_file nix/sources.json
@@ -9,33 +13,4 @@ dotenv_if_exists
use nix
# HACK: Workaround for direnv bug
unset TMP TMPDIR TEMP TEMPDIR
# HACK: Configure Rider to use the correct .NET paths from an ambient .NET
use_rider_dotnet() {
# Get paths
DOTNET_PATH=$(readlink "$(which dotnet)")
SETTINGS_FILE=$(find . -maxdepth 1 -type f -name '*.sln.DotSettings.user')
MSBUILD=$(realpath "$(find "$(dirname "$DOTNET_PATH")/../share/dotnet/sdk" -maxdepth 2 -type f -name MSBuild.dll)")
# Update Rider settings if they exist
if [ -f "$SETTINGS_FILE" ] ; then
xmlstarlet ed --inplace \
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
-N s="clr-namespace:System;assembly=mscorlib" \
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" \
--value "$(realpath "$(dirname "$DOTNET_PATH")/../share/dotnet/dotnet")" \
"$SETTINGS_FILE"
xmlstarlet ed --inplace \
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
-N s="clr-namespace:System;assembly=mscorlib" \
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" \
--value "$MSBUILD" \
"$SETTINGS_FILE"
fi
}
unset TMP TMPDIR TEMP TEMPDIR

View File

@@ -1,11 +1,14 @@
# yaml-language-server: $schema=https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json
variables:
SDK_VERSION: 9.0
SKIP_TESTS: "true"
default:
tags:
- nix
include:
- project: oceanbox/gitlab-ci
ref: v4.4
ref: v4.5
file: template/Base.gitlab-ci.yml
- local: "/src/Atlantis/.gitlab-ci.yml"
rules:
@@ -46,7 +49,3 @@ include:
- "src/Codex/**/*"
- "nix/packages/node-modules.nix"
- "nix/packages/sources.nix"
check:
tags:
- nix

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include=".build/Helpers.fs" />
<Compile Include=".build/Build.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fake.Core.Target" />
<PackageReference Include="Fake.DotNet.Cli" />
<PackageReference Include="Fake.IO.FileSystem" />
<PackageReference Include="Farmer" />
<PackageReference Include="FSharp.Core" />
</ItemGroup>
</Project>

View File

@@ -39,6 +39,7 @@
<PackageVersion Include="Matplotlib.ColorMaps" Version="3.0.1" />
<PackageVersion Include="Thoth.Fetch" Version="3.0.1" />
<PackageVersion Include="Thoth.Json" Version="10.4.1"/>
<PackageVersion Include="FS.FluentUI" Version="3.0.0"/>
<!-- Serverpack -->
<PackageVersion Include="OpenFga.Sdk" Version="0.7.0"/>
<PackageVersion Include="FSharp.SystemTextJson" Version="1.3.13"/>
@@ -81,11 +82,6 @@
<PackageVersion Include="MessagePack" Version="3.1.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="ProjNet.FSharp" Version="5.2.0" />
<!-- Build -->
<PackageVersion Include="Fake.Core.Target" Version="6.1.3" />
<PackageVersion Include="Fake.DotNet.Cli" Version="6.1.3" />
<PackageVersion Include="Fake.IO.FileSystem" Version="6.1.3" />
<PackageVersion Include="Farmer" Version="1.9.6" />
<!-- Dapperizer -->
<PackageVersion Include="Oceanbox.SDSLite" Version="2.8.0" />
<PackageVersion Include="Dapper.FSharp" Version="4.9.0"/>

View File

@@ -69,25 +69,7 @@ kubectl --context oceanbox -n default get pods
Required helm manifests are hosted in a separate repository: <https://gitlab.com/oceanbox/manifests>.
Clone it into a directory _in the same parent directory as this repository._
The Bitnami respository must also be added to helm:
```shell
helm repo add bitnami https://charts.bitnami.com/bitnami
```
### DNS
Some DNS masking is required. Add the following to your NixOS configuration:
```nix
services.dnsmasq = {
enable = true;
settings.address = [
"/.local/127.0.0.1"
"/.local.oceanbox.io/127.0.0.1"
];
};
```
You'll have to run `helm dependency update` in the atlantis directory within the manifest repo to download the charts.
### NuGet
@@ -102,14 +84,30 @@ To retrieve packages from the private Oceanbox nuget registry, configure it with
</packageSources>
<packageSourceCredentials>
<oceanbox>
<add key="Username" value="oceanbox-nuget" />
<add key="ClearTextPassword" value="<...>" />
<add key="Username" value="<Your-GitLab-Username>" />
<add key="ClearTextPassword" value="<Your-GitLab-PAT>" />
</oceanbox>
</packageSourceCredentials>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="oceanbox">
<package pattern="Oceanbox.*" />
<package pattern="ProjNet.FSharp" />
<package pattern="Drifters.Api" />
<package pattern="Fable.Lit" />
<package pattern="Fable.Lit.*" />
<package pattern="Fable.SignalR" />
<package pattern="Fable.SignalR.*" />
<package pattern="Fable.OpenLayers" />
<package pattern="Matplotlib.*" />
</packageSource>
</packageSourceMapping>
</configuration>
```
Substitute `<...>` for the corresponding secret.
Substitute with your own gitlab username and PAT in the credentials.
Now, we should be able to `restore`:
@@ -168,7 +166,7 @@ You should now be able to access the Atlantis client (with HMR) on <atlantis.loc
### Trust Root Certificate
> [!note]
> You'll need to run `dotnet run bundle` in `src/Atlantis` to generate the `/certs` directory
> You'll need to run `just run-client` in `src/Atlantis` to generate the certificates in `~/.vite-plugin-mkcert/certs`
In order for your browser to allow you to access the web application, you must add the root certificate generated by `mkcert` to the list of trusted authorities in your browser:
@@ -179,9 +177,9 @@ In order for your browser to allow you to access the web application, you must a
### Add `user` to OpenFGA
Ask [sales](moritz.jorg@oceanbox.io) to add your `azure-ad-user` to OpenFGA.
Ask [sales](support@oceanbox.io) to add your `azure-ad-user` to OpenFGA.
### CORS for Sorcerer
Add the `url` of your instance to the CORS list of Sorcerer
[here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52).
[here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52).

View File

@@ -1,5 +1,187 @@
# Changelog
## [1.40.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.4...v1.40.5) (2026-01-21)
### Bug Fixes
* **xtractor:** Reduce to 4 cores per task ([8e824d4](https://gitlab.com/oceanbox/Poseidon/commit/8e824d4afa0b03f59e006d3a0d50fb216e71483e))
## [1.40.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.3...v1.40.4) (2026-01-21)
### Bug Fixes
* **xtractor:** Reduce core requirement to 8 ([efacb2a](https://gitlab.com/oceanbox/Poseidon/commit/efacb2a3322de0ced45db4eec240846f4e371a75))
## [1.40.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.2...v1.40.3) (2026-01-20)
### Bug Fixes
* **inbox|xtracto:** Delete/Read msg and allow non-ascii xtractor names ([d8d5e07](https://gitlab.com/oceanbox/Poseidon/commit/d8d5e076baf8b559200f2da91237f9874678b216))
* **multiauth:** Add clientId to redirect on signout ([54c40d7](https://gitlab.com/oceanbox/Poseidon/commit/54c40d7accc4bbc43f66dda0df647ccac482a2b0))
## [1.40.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.1...v1.40.2) (2026-01-19)
### Bug Fixes
* **xtract:** Disabled if not allowed to simulate transport ([e429a85](https://gitlab.com/oceanbox/Poseidon/commit/e429a855e5bd00493e2f99647092aebce9c99a2a))
## [1.40.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.0...v1.40.1) (2026-01-19)
### Bug Fixes
* fix tilt build on net10 ([d5cde19](https://gitlab.com/oceanbox/Poseidon/commit/d5cde19250847f7b091cfa5f65eb703405c202b6))
# [1.40.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.2...v1.40.0) (2026-01-16)
### Bug Fixes
* **Codex:** expose days instead of frames in arcive form ([6cf5262](https://gitlab.com/oceanbox/Poseidon/commit/6cf5262dd5c98517a3c767f410c858fe32c07bd5))
* **Codex:** only allow inbounds time intervals on edit archive ([eaea4b2](https://gitlab.com/oceanbox/Poseidon/commit/eaea4b2e215669cec19f2a8cec122ba670a7a202))
### Features
* **Codex:** edit archives ([ec10932](https://gitlab.com/oceanbox/Poseidon/commit/ec109328fbf5f237a52ef77cbd44dff571deee5f))
## [1.39.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.1...v1.39.2) (2026-01-15)
### Bug Fixes
* fix net10 issues ([f4943a1](https://gitlab.com/oceanbox/Poseidon/commit/f4943a148b72fb7e10a745cc3e806b9c4bdd76d8))
## [1.39.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.0...v1.39.1) (2026-01-15)
### Bug Fixes
* **ci:** Remove schedule check ([ab37e88](https://gitlab.com/oceanbox/Poseidon/commit/ab37e88bb0f669a7aa94bf831f95f8c60dc28804))
# [1.39.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.5...v1.39.0) (2026-01-14)
### Bug Fixes
* **Codex:** add * user in archmeister on public archives ([cd678a4](https://gitlab.com/oceanbox/Poseidon/commit/cd678a41f64a64c6f3616f32bafeaae6715c08a4))
* **Codex:** use feliz router guid matching ([7182f7c](https://gitlab.com/oceanbox/Poseidon/commit/7182f7c9f094d65c884e8e02d4aaa89561ca5e82))
* **Codex:** utc start_time ([492651e](https://gitlab.com/oceanbox/Poseidon/commit/492651e0f34035d8c61174aa50336222bcfd979c))
* **DataAgent:** use files from parent attribs instead of archive_files ([eac23e7](https://gitlab.com/oceanbox/Poseidon/commit/eac23e7d1a541f2e90374a2add689846d9e7b642))
### Features
* **Codex:** ability to add FVCOM archives ([d86db7a](https://gitlab.com/oceanbox/Poseidon/commit/d86db7a66ca5ecb6a9ad45ce3d47be3a98d56bb8))
## [1.38.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.4...v1.38.5) (2026-01-13)
### Bug Fixes
* **Archmaester:** Rollback add archive if openfga fails ([46e86eb](https://gitlab.com/oceanbox/Poseidon/commit/46e86eb5f961a45fba2da1525c1472bdca79ab47))
## [1.38.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.3...v1.38.4) (2026-01-12)
### Bug Fixes
* **nix:** Bump node deps ([e513d87](https://gitlab.com/oceanbox/Poseidon/commit/e513d87d249843423f0e0a62275afe45e6c73a46))
## [1.38.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.2...v1.38.3) (2026-01-12)
### Bug Fixes
* **xtractor:** Set maxEndDate to 1 year and format ([5315a05](https://gitlab.com/oceanbox/Poseidon/commit/5315a05fa255c6164d3ef73c0f5e20cdb4c632d0))
## [1.38.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.1...v1.38.2) (2026-01-12)
### Bug Fixes
* **atlas:** Disable Snowflakes ([4cd3673](https://gitlab.com/oceanbox/Poseidon/commit/4cd3673d15783afa72ca3358e6bd8c3a8cfbfd16))
## [1.38.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.0...v1.38.1) (2026-01-11)
### Bug Fixes
* **xtractor:** Move to short partition and 16 CPU, also set max duration to 1 year ([56d3476](https://gitlab.com/oceanbox/Poseidon/commit/56d34767d7a2e0bc6aadaa5987344c6e7a58698a))
# [1.38.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.37.1...v1.38.0) (2026-01-07)
### Bug Fixes
* client layout ([efb3292](https://gitlab.com/oceanbox/Poseidon/commit/efb3292d9f4a8fccf2cebbdd75b3d3ffc186fa11))
### Features
* add fluent ui to codex ([d8bf174](https://gitlab.com/oceanbox/Poseidon/commit/d8bf174d3aa181169365c178b4335052e13eabc5))
## [1.37.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.37.0...v1.37.1) (2026-01-05)
### Bug Fixes
* **xtractor:** Avoid using union types in Dapr Actors ([14c1a57](https://gitlab.com/oceanbox/Poseidon/commit/14c1a57331f981b1e1e0793426448ea261002e6d))
# [1.37.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.36.0...v1.37.0) (2025-12-22)
### Features
* Add XtractActor ([a8a187a](https://gitlab.com/oceanbox/Poseidon/commit/a8a187a412c13d3e9d21cbbcfc2e1813c0e38dfe))
# [1.36.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.35.2...v1.36.0) (2025-12-12)
### Features
* add Saturn.ReverseProxy middleware ([bd74504](https://gitlab.com/oceanbox/Poseidon/commit/bd745042dfa51fbbef7bf7b55d31b8b57e6ad0a4))
## [1.35.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.35.1...v1.35.2) (2025-12-01)
### Bug Fixes
* **drifters:** reverse toggle on postdrift analysis ([563faa6](https://gitlab.com/oceanbox/Poseidon/commit/563faa6c0bd20a7d3f184bba66ae5df340e7ef4e))
## [1.35.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.35.0...v1.35.1) (2025-12-01)
### Bug Fixes
* **stats:** no stats banner showing when stats are available, closing [#87](https://gitlab.com/oceanbox/Poseidon/issues/87) ([e75ffc4](https://gitlab.com/oceanbox/Poseidon/commit/e75ffc41e5d298e2ecf92c5c4d11e0f930f633ab))
* **stats:** set priority order of depth plots, closing [#88](https://gitlab.com/oceanbox/Poseidon/issues/88) ([2887e6a](https://gitlab.com/oceanbox/Poseidon/commit/2887e6a90951f4aaa088ad14b3d2bbcb6fd25b93))
# [1.35.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.34.2...v1.35.0) (2025-12-01)
### Features
* ❄️ ([a620c26](https://gitlab.com/oceanbox/Poseidon/commit/a620c26812d3ec7517c34e7931e03b411f725907))
## [1.34.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.34.1...v1.34.2) (2025-11-30)
### Bug Fixes
* Temporarily use vtn as only source for wind barbs ([048e803](https://gitlab.com/oceanbox/Poseidon/commit/048e80356b59cfcd408bf6784bbc2e22aebe25c6))
## [1.34.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.34.0...v1.34.1) (2025-11-29)
### Bug Fixes
* **stats:** find available stats archives ([48d46ed](https://gitlab.com/oceanbox/Poseidon/commit/48d46eda62160d3efe1423238a25f26a439c6b88))
# [1.34.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.11...v1.34.0) (2025-11-29)

View File

@@ -1 +1 @@
1.34.0
1.40.5

822
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -17,8 +17,8 @@ let
in
clean version;
dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0;
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_9_0;
dotnet-sdk = pkgs.dotnetCorePackages.sdk_10_0;
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_10_0;
deps = nix-utils.output.lib.nuget.deps;
# Usage: export NETRC="$(agenix -d netrc.age)" in `./nix/secrets`

View File

@@ -1,6 +1,6 @@
{
"sdk": {
"version": "9.0.0",
"version": "10.0.100",
"rollForward": "latestMinor"
}
}

42
justfile Normal file
View File

@@ -0,0 +1,42 @@
# Poseidon build commands
# Install just: https://github.com/casey/just
#
# Sub-projects with justfiles:
# - Atlantis (src/Atlantis) - Server + Client application with Fable/Vite
# - ServerPack (src/ServerPack) - Server package library
# - DataAgent (src/DataAgent) - Data agent library
# - Interfaces (src/Interfaces) - API interfaces library
# - Archivist (src/Archivist) - CLI tool with client
# - Sorcerer (src/Sorcerer) - Server application with client
#
# Run 'just <project>' to see available commands for each project (e.g., 'just atlantis')
set dotenv-load
# Default recipe - show available commands
default:
@just --list
# Show available commands for Atlantis (src/Atlantis)
atlantis:
@just src/Atlantis/
# Show available commands for ServerPack (src/ServerPack)
serverpack:
@just src/ServerPack/
# Show available commands for DataAgent (src/DataAgent)
dataagent:
@just src/DataAgent/
# Show available commands for Interfaces (src/Interfaces)
interfaces:
@just src/Interfaces/
# Show available commands for Archivist (src/Archivist)
archivist:
@just src/Archivist/
# Show available commands for Sorcerer (src/Sorcerer)
sorcerer:
@just src/Sorcerer/

View File

@@ -9,8 +9,15 @@
*/
# Generated by npins. Do not modify; will be overwritten regularly
let
data = builtins.fromJSON (builtins.readFile ./sources.json);
version = data.version;
# Backwards-compatibly make something that previously didn't take any arguments take some
# The function must return an attrset, and will unfortunately be eagerly evaluated
# Same thing, but it catches eval errors on the default argument so that one may still call it with other arguments
mkFunctor =
fn:
let
e = builtins.tryEval (fn { });
in
(if e.success then e.value else { error = fn { }; }) // { __functor = _self: fn; };
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
range =
@@ -21,7 +28,6 @@ let
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
concatMapStrings = f: list: concatStrings (map f list);
concatStrings = builtins.concatStringsSep "";
# If the environment variable NPINS_OVERRIDE_${name} is set, then use
@@ -48,41 +54,87 @@ let
mkSource =
name: spec:
{
pkgs ? null,
}:
assert spec ? type;
let
# Unify across builtin and pkgs fetchers.
# `fetchGit` requires a wrapper because of slight API differences.
fetchers =
if pkgs == null then
{
inherit (builtins) fetchTarball fetchurl;
# For some fucking reason, fetchGit has a different signature than the other builtin fetchers …
fetchGit = args: (builtins.fetchGit args).outPath;
}
else
{
fetchTarball =
{
url,
sha256,
}:
pkgs.fetchzip {
inherit url sha256;
extension = "tar";
};
inherit (pkgs) fetchurl;
fetchGit =
{
url,
submodules,
rev,
name,
narHash,
}:
pkgs.fetchgit {
inherit url rev name;
fetchSubmodules = submodules;
hash = narHash;
};
};
# Dispatch to the correct code path based on the type
path =
if spec.type == "Git" then
mkGitSource spec
mkGitSource fetchers spec
else if spec.type == "GitRelease" then
mkGitSource spec
mkGitSource fetchers spec
else if spec.type == "PyPi" then
mkPyPiSource spec
mkPyPiSource fetchers spec
else if spec.type == "Channel" then
mkChannelSource spec
mkChannelSource fetchers spec
else if spec.type == "Tarball" then
mkTarballSource spec
mkTarballSource fetchers spec
else if spec.type == "Container" then
mkContainerSource pkgs spec
else
builtins.throw "Unknown source type ${spec.type}";
in
spec // { outPath = mayOverride name path; };
mkGitSource =
{
fetchTarball,
fetchGit,
...
}:
{
repository,
revision,
url ? null,
submodules,
hash,
branch ? null,
...
}:
assert repository ? type;
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
# In the latter case, there we will always be an url to the tarball
if url != null && !submodules then
builtins.fetchTarball {
fetchTarball {
inherit url;
sha256 = hash; # FIXME: check nix version & use SRI hashes
sha256 = hash;
}
else
let
@@ -93,6 +145,8 @@ let
"https://github.com/${repository.owner}/${repository.repo}.git"
else if repository.type == "GitLab" then
"${repository.server}/${repository.repo_path}.git"
else if repository.type == "Forgejo" then
"${repository.server}/${repository.owner}/${repository.repo}.git"
else
throw "Unrecognized repository type ${repository.type}";
urlToName =
@@ -107,40 +161,89 @@ let
"${if matched == null then "source" else builtins.head matched}${appendShort}";
name = urlToName url revision;
in
builtins.fetchGit {
fetchGit {
rev = revision;
inherit name;
# hash = hash;
inherit url submodules;
narHash = hash;
inherit name submodules url;
};
mkPyPiSource =
{ url, hash, ... }:
builtins.fetchurl {
{ fetchurl, ... }:
{
url,
hash,
...
}:
fetchurl {
inherit url;
sha256 = hash;
};
mkChannelSource =
{ url, hash, ... }:
builtins.fetchTarball {
{ fetchTarball, ... }:
{
url,
hash,
...
}:
fetchTarball {
inherit url;
sha256 = hash;
};
mkTarballSource =
{ fetchTarball, ... }:
{
url,
locked_url ? url,
hash,
...
}:
builtins.fetchTarball {
fetchTarball {
url = locked_url;
sha256 = hash;
};
mkContainerSource =
pkgs:
{
image_name,
image_tag,
image_digest,
...
}:
if pkgs == null then
builtins.throw "container sources require passing in a Nixpkgs value: https://github.com/andir/npins/blob/master/README.md#using-the-nixpkgs-fetchers"
else
pkgs.dockerTools.pullImage {
imageName = image_name;
imageDigest = image_digest;
finalImageTag = image_tag;
};
in
if version == 5 then
builtins.mapAttrs mkSource data.pins
else
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"
mkFunctor (
{
input ? ./sources.json,
}:
let
data =
if builtins.isPath input then
# while `readFile` will throw an error anyways if the path doesn't exist,
# we still need to check beforehand because *our* error can be caught but not the one from the builtin
# *piegames sighs*
if builtins.pathExists input then
builtins.fromJSON (builtins.readFile input)
else
throw "Input path ${toString input} does not exist"
else if builtins.isAttrs input then
input
else
throw "Unsupported input type ${builtins.typeOf input}, must be a path or an attrset";
version = data.version;
in
if version == 7 then
builtins.mapAttrs (name: spec: mkFunctor (mkSource name spec)) data.pins
else
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"
)

View File

@@ -25,7 +25,7 @@ buildDotnetModule rec {
;
name = "Archivist";
pname = name;
dotnet-runtime = pkgs.dotnetCorePackages.runtime_9_0;
dotnet-runtime = pkgs.dotnetCorePackages.runtime_10_0;
dotnetRestoreFlags = "--force-evaluate";
nugetDeps = deps {
inherit

View File

@@ -48,5 +48,5 @@ stdenvNoCC.mkDerivation {
outputHashMode = "recursive";
outputHashAlgo = "sha256";
# NOTE: Empty this when a new dependency is added
outputHash = "sha256-yEUFqJ4il3uEYuB954zxTZj7HfSz7n5uFhaQ6hoovcE=";
outputHash = "sha256-bbCaGoZRE7vRuAS3eRyP8yHANYXBJVaHmuL99BAovjY=";
}

View File

@@ -9,9 +9,9 @@
},
"branch": "main",
"submodules": false,
"revision": "2f0f812f69f3eb4140157fe15e12739adf82e32a",
"url": "https://github.com/ryantm/agenix/archive/2f0f812f69f3eb4140157fe15e12739adf82e32a.tar.gz",
"hash": "1d4m7hsq727q7ndjqmgyl8vkbkqjwps962ygmv2mcc5dbqzgn963"
"revision": "fcdea223397448d35d9b31f798479227e80183f6",
"url": "https://github.com/ryantm/agenix/archive/fcdea223397448d35d9b31f798479227e80183f6.tar.gz",
"hash": "sha256-wyT7Pl6tMFbFrs8Lk/TlEs81N6L+VSybPfiIgzU8lbQ="
},
"nix-utils": {
"type": "Git",
@@ -23,13 +23,13 @@
"submodules": false,
"revision": "098f594425d2b9dde0657becad0f6498d074f8b3",
"url": null,
"hash": "0hh52w1fkpr1xx6j8cjm6g88j2352yv2ysqm1q51j59y6f583vyb"
"hash": "sha256-y++BijM+FRkKDhVrL7YXZQiJ0DNVMiRN7yHf6QIXBUI="
},
"nixpkgs": {
"type": "Channel",
"name": "nixpkgs-unstable",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre880343.87848bf0cc4f/nixexprs.tar.xz",
"hash": "134c1sx06gxh7a4jnf618bi4c2wa949fm14w34cjhsryqjs3a8ha"
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre930822.ed142ab1b3a0/nixexprs.tar.xz",
"hash": "sha256-XH6awru9NnBc/m+2YhRNT8r1PAKEiPGF3gs//F3ods0="
},
"pre-commit": {
"type": "Git",
@@ -40,10 +40,10 @@
},
"branch": "master",
"submodules": false,
"revision": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
"url": "https://github.com/cachix/git-hooks.nix/archive/ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37.tar.gz",
"hash": "11r45r45qcfv77rx024mqpra2yixnc5g248kp7rmccq09vll1y85"
"revision": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
"url": "https://github.com/cachix/git-hooks.nix/archive/a1ef738813b15cf8ec759bdff5761b027e3e1d23.tar.gz",
"hash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U="
}
},
"version": 5
"version": 7
}

View File

@@ -18,8 +18,13 @@
"vite-plugin-mkcert": "^1.17.8"
},
"dependencies": {
"@fluentui/react-components": "^9.72.2",
"@fluentui/react-components": "^9.72.9",
"@fluentui/react-datepicker-compat": "^0.6.20",
"@fluentui/react-calendar-compat": "^0.3.15",
"@fluentui/react-timepicker-compat": "^0.4.26",
"@fluentui-contrib/react-data-grid-react-window": "^1.4.2",
"@fluentui/react-icons": "^2.0.316",
"react-window": "^2.2.3",
"@fortawesome/fontawesome-free": "^6.7.2",
"@lit/context": "^1.1.6",
"@microsoft/signalr": "^8.0.17",

101
scripts/update-rider.sh Executable file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env nix-shell
#! nix-shell -i bash --pure
#! nix-shell -p bash which xmlstarlet
if [[ ! $# -eq 1 ]]; then
echo "Usage: $0 <dotnet-path>"
exit 1
fi
dotnet_path=$1
function stderr() {
echo "$@" 1>&2;
}
function create_settings_file() {
cat << EOF
<?xml version="1.0"?>
<wpf:ResourceDictionary
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:ss="urn:schemas-jetbrains-com:settings-storage-xaml"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xml:space="preserve"
>
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">${1}</s:String>
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">${2}</s:String>
</wpf:ResourceDictionary>
EOF
}
# HACK: Configure Rider to use the correct .NET paths from an ambient .NET
function use_rider_dotnet() {
local solution_file=$(find . -maxdepth 1 -type f -name '*.slnx' | cut -d'.' -f2 | cut -d'/' -f2)
local settings_file=$(find . -maxdepth 1 -type f -name '*.sln.DotSettings.user')
# Get paths
local cli_path=$(realpath "$dotnet_path")
local dir=$(dirname $cli_path)
local msbuild_path=$(find "$dir" -maxdepth 3 -type f -name MSBuild.dll)
# stderr "dotnet path is $dir"
# stderr "Found msbuild: $msbuild_path"
if [ -f "$settings_file" ] ; then
# stderr "Updating rider settings file: $settings_file"
# stderr "Setting DotNetCliExePath to $cli_path"
# NOTE: check if dotnet binary in share folder settings exists
xml sel -t -v "wpf:ResourceDictionary/s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" "$settings_file"
if [[ $? -eq 0 ]]; then
xml ed --inplace \
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
-N s="clr-namespace:System;assembly=mscorlib" \
-N ss="urn:schemas-jetbrains-com:settings-storage-xaml" \
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" \
--value "$cli_path" \
"$settings_file"
else
xml ed --inplace \
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
-N s="clr-namespace:System;assembly=mscorlib" \
-N ss="urn:schemas-jetbrains-com:settings-storage-xaml" \
-s /wpf:ResourceDictionary -t elem -n s:String -v "$cli_path" \
--var new_node '$prev' \
-i '$new_node' -t attr -n "x:Key" -v "/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue" \
"$settings_file"
fi
xml sel -t -v "wpf:ResourceDictionary/s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" "$settings_file"
if [[ $? -eq 0 ]]; then
xmlstarlet ed --inplace \
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
-N s="clr-namespace:System;assembly=mscorlib" \
-N ss="urn:schemas-jetbrains-com:settings-storage-xaml" \
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" \
--value "$msbuild_path" \
"$settings_file"
else
xml ed --inplace \
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
-N s="clr-namespace:System;assembly=mscorlib" \
-N ss="urn:schemas-jetbrains-com:settings-storage-xaml" \
-s /wpf:ResourceDictionary -t elem -n s:String -v "$cli_path" \
--var new_node '$prev' \
-i '$new_node' -t attr -n "x:Key" -v "/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue" \
"$settings_file"
fi
else
create_settings_file $cli_path $msbuild_path > "$solution_file.sln.DotSettings.user"
fi
}
function main() {
use_rider_dotnet
}
main

View File

@@ -4,48 +4,65 @@
pre-commit ? import ./nix/pre-commit.nix,
}:
let
dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0;
dotnet-sdk = pkgs.dotnetCorePackages.sdk_10_0;
agenix = pkgs.callPackage "${sources.agenix}/pkgs/agenix.nix" { };
fable = pkgs.buildDotnetGlobalTool {
pname = "fable";
version = "4.24.0";
nugetHash = "sha256-ERewWqfEyyZKpHFFALpMGJT0fDWywBYY5buU/wTZZTg=";
};
in
pkgs.mkShellNoCC {
buildInputs = [ dotnet-sdk ];
packages = with pkgs; [
packages = [
# F#
fable
dotnet-outdated
fantomas
fsautocomplete
pkgs.dotnet-outdated
pkgs.fantomas
pkgs.fsautocomplete
# JavaScript
bun
nodejs
pkgs.bun
pkgs.nodejs_25
# Devlopment tools
npins
mkcert
dive
nix-output-monitor
pkgs.npins
pkgs.mkcert
pkgs.dive
pkgs.nix-output-monitor
pkgs.just
pkgs.skopeo
# Secret management with agenix
agenix
# Kubernetes tools
tilt
dapr-cli
kustomize
kubernetes-helm
pkgs.tilt
pkgs.dapr-cli
pkgs.kustomize
pkgs.kubernetes-helm
];
# Environment variables
DOTNET_ROOT = "${dotnet-sdk}/share/dotnet";
DOTNET_CLI_TELEMETRY_OPTOUT = "true";
LOG_LEVEL = "verbose";
NPINS_DIRECTORY = "nix";
shellHook = ''
scripts/update-rider.sh ${dotnet-sdk}/bin/dotnet
'';
# Alternative shells
passthru = pkgs.lib.mapAttrs (name: value: pkgs.mkShellNoCC (value // { inherit name; })) {
pre-commit.shellHook = pre-commit.shellHook;
ci-shell = {
packages = [
pkgs.npins
];
shellHook = ''
export NPINS_DIRECTORY="nix"
'';
};
agenix-gen = {
packages = [ agenix ];
shellHook = ''
@@ -61,4 +78,4 @@ pkgs.mkShellNoCC {
'';
};
};
}
}

View File

@@ -1,67 +0,0 @@
open Fake.Core
open Fake.IO
open Farmer
open Farmer.Builders
open Helpers
initializeContext()
let clientPath = Path.getFullName "src/Client"
let cliPath = Path.getFullName "src/Cli"
let testPath = Path.getFullName "tests"
let distPath = Path.getFullName "dist"
let vite = $"bunx --bun vite -c ../../vite.config.js"
let viteBundle = $"{vite} build --outDir {distPath}/public"
Target.create "Clean" (fun _ -> Shell.cleanDir distPath)
// Target.create "Bundle" (fun _ ->
// let vite = $"{viteBundle} -m production"
// run dotnet $"publish -c Release -o \"{distPath}\"" serverPath
// run fable $"-o build/client --run {vite}" clientPath
// )
// Target.create "BundleDebug" (fun _ ->
// let vite = $"{viteBundle} -m development --minify false"
// run dotnet $"publish -c Debug -o \"{distPath}\"" serverPath
// run fable $"-o build/client --run {vite}" clientPath
// )
Target.create "Bundle" (fun _ ->
run dotnet $"publish -c Release -o \"{distPath}\"" cliPath
)
Target.create "BundleDebug" (fun _ ->
run dotnet $"publish -c Debug -o \"{distPath}\"" cliPath
)
Target.create "Format" (fun _ ->
run fantomas ". -r" "src"
)
Target.create "Test" (fun _ ->
if System.IO.Directory.Exists testPath then
run dotnet "run" testPath
else ()
)
Target.create "Run" (fun _ -> Target.runOrDefault "Bundle")
open Fake.Core.TargetOperators
let dependencies = [
"Clean"
==> "Bundle"
"Clean"
==> "BundleDebug"
"Clean"
==> "Test"
]
[<EntryPoint>]
let main args = runOrDefault args

View File

@@ -1,116 +0,0 @@
module Helpers
open Fake.Core
let initializeContext () =
let execContext = Context.FakeExecutionContext.Create false "build.fsx" [ ]
Context.setExecutionContext (Context.RuntimeContext.Fake execContext)
module Proc =
module Parallel =
open System
let locker = obj()
let colors =
[| ConsoleColor.Blue
ConsoleColor.Yellow
ConsoleColor.Magenta
ConsoleColor.Cyan
ConsoleColor.DarkBlue
ConsoleColor.DarkYellow
ConsoleColor.DarkMagenta
ConsoleColor.DarkCyan |]
let print color (colored: string) (line: string) =
lock locker
(fun () ->
let currentColor = Console.ForegroundColor
Console.ForegroundColor <- color
Console.Write colored
Console.ForegroundColor <- currentColor
Console.WriteLine line)
let onStdout index name (line: string) =
let color = colors.[index % colors.Length]
if isNull line then
print color $"{name}: --- END ---" ""
else if String.isNotNullOrEmpty line then
print color $"{name}: " line
let onStderr name (line: string) =
let color = ConsoleColor.Red
if isNull line |> not then
print color $"{name}: " line
let redirect (index, (name, createProcess)) =
createProcess
|> CreateProcess.redirectOutputIfNotRedirected
|> CreateProcess.withOutputEvents (onStdout index name) (onStderr name)
let printStarting indexed =
for (index, (name, c: CreateProcess<_>)) in indexed do
let color = colors.[index % colors.Length]
let wd =
c.WorkingDirectory
|> Option.defaultValue ""
let exe = c.Command.Executable
let args = c.Command.Arguments.ToStartInfo
print color $"{name}: {wd}> {exe} {args}" ""
let run cs =
cs
|> Seq.toArray
|> Array.indexed
|> fun x -> printStarting x; x
|> Array.map redirect
|> Array.Parallel.map Proc.run
let createProcess exe arg dir =
CreateProcess.fromRawCommandLine exe arg
|> CreateProcess.withWorkingDirectory dir
|> CreateProcess.ensureExitCode
let dotnet = createProcess "dotnet"
let fable = createProcess "fable"
let fantomas = createProcess "fantomas"
type BundleMode =
| Prod
| Devel
| Watch
with
override this.ToString() =
match this with
| Prod -> "production"
| Devel -> "development"
| Watch -> "watch"
let viteCmd (m: BundleMode) outDir =
match m with
| Prod -> $"vite build -c ../../vite.config.js -m {m} --emptyOutDir --outDir {outDir}/public"
| Devel -> $"vite build -c ../../vite.config.js -m {m} --minify false --sourcemap true --emptyOutDir --outDir {outDir}/public"
| Watch -> "vite -c ../../vite.config.js"
let run proc arg dir =
proc arg dir
|> Proc.run
|> ignore
let runParallel processes =
processes
|> Proc.Parallel.run
|> ignore
let runOrDefault args =
try
match args with
| [| target |] -> Target.runOrDefault target
| _ ->
Target.runOrDefault "Run"
0
with e ->
printfn "%A" e
1

View File

@@ -1,15 +1,15 @@
# yaml-language-server: $schema=https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json
variables:
SKIP_TESTS: "true"
SKIP_TESTS: "true"
include:
- project: oceanbox/gitlab-ci
ref: v4.4
file: DotnetDeployment.gitlab-ci.yml
inputs:
project-name: archivist
project-dir: src/Cli
- project: oceanbox/gitlab-ci
ref: v4.5
file: DotnetDeployment.gitlab-ci.yml
inputs:
project-name: archivist
project-dir: src/Cli
dockerize-archivist:
tags:
- nix
- nix

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include=".build/Helpers.fs" />
<Compile Include=".build/Build.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fake.Core.Target" />
<PackageReference Include="Fake.DotNet.Cli" />
<PackageReference Include="Fake.IO.FileSystem" />
<PackageReference Include="Farmer" />
<PackageReference Include="FSharp.Core" />
</ItemGroup>
</Project>

40
src/Archivist/justfile Normal file
View File

@@ -0,0 +1,40 @@
# Archivist build commands
# Install just: https://github.com/casey/just
set dotenv-load
src_path := "src"
client_path := "src/Client"
cli_path := "src/Cli"
test_path := "tests"
dist_path := "dist"
# Default recipe - show available commands
default:
@just --list
# Clean build artifacts
clean:
rm -rf {{dist_path}}
# Build production bundle
bundle: clean
dotnet publish -c Release -o {{dist_path}} {{cli_path}}
# Build debug bundle
bundle-debug: clean
dotnet publish -c Debug -o {{dist_path}} {{cli_path}}
# Format code with Fantomas
format:
fantomas {{src_path}} -r
# Run tests
test: clean
#!/usr/bin/env bash
if [ -d "{{test_path}}" ]; then
dotnet run {{test_path}}
fi
# Run (builds bundle)
run: bundle

View File

@@ -29,7 +29,7 @@ pkgs.mkShellNoCC {
SERVER_PORT = port + 85;
TILT_PORT = port + 50;
DOTNET_ROOT = "${pkgs.dotnetCorePackages.sdk_9_0}/share/dotnet";
DOTNET_ROOT = "${pkgs.dotnetCorePackages.sdk_10_0}/share/dotnet";
shellHook = ''
export PATH="$PWD/src/Cli/bin/Release/net9.0/linux-x64/:$PATH"

View File

@@ -631,7 +631,6 @@ let instantiateArchiveDto (idx, modelArea, basePath, files, reverse, json, publi
}
let retireArchive (archive: string) =
// TODO: retire all dependent archies
let aid =
try
Guid.Parse archive

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<AssemblyName>archivist</AssemblyName>

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Fargo.CmdLine": {
"type": "Direct",
"requested": "[1.7.5, )",
@@ -71,8 +71,7 @@
"Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
"Microsoft.Extensions.DependencyModel": "9.0.1",
"Microsoft.Extensions.Logging": "9.0.1",
"Mono.TextTemplating": "3.0.0",
"System.Text.Json": "9.0.1"
"Mono.TextTemplating": "3.0.0"
}
},
"Microsoft.EntityFrameworkCore.Tools": {
@@ -233,8 +232,7 @@
"resolved": "5.3.2",
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
"dependencies": {
"FSharp.Core": "4.3.2",
"System.Reflection.Emit.Lightweight": "4.3.0"
"FSharp.Core": "4.3.2"
}
},
"Google.Api.CommonProtos": {
@@ -331,10 +329,7 @@
"resolved": "4.8.0",
"contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
"dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
"System.Collections.Immutable": "7.0.0",
"System.Reflection.Metadata": "7.0.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
"Microsoft.CodeAnalysis.Analyzers": "3.3.4"
}
},
"Microsoft.CodeAnalysis.CSharp": {
@@ -364,9 +359,7 @@
"Humanizer.Core": "2.14.1",
"Microsoft.Bcl.AsyncInterfaces": "7.0.0",
"Microsoft.CodeAnalysis.Common": "[4.8.0]",
"System.Composition": "7.0.0",
"System.IO.Pipelines": "7.0.0",
"System.Threading.Channels": "7.0.0"
"System.Composition": "7.0.0"
}
},
"Microsoft.CodeAnalysis.Workspaces.MSBuild": {
@@ -376,8 +369,7 @@
"dependencies": {
"Microsoft.Build.Framework": "16.10.0",
"Microsoft.CodeAnalysis.Common": "[4.8.0]",
"Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]",
"System.Text.Json": "7.0.3"
"Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]"
}
},
"Microsoft.EntityFrameworkCore.Abstractions": {
@@ -534,16 +526,6 @@
"resolved": "17.11.4",
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
"Microsoft.NETCore.Targets": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
},
"Mono.TextTemplating": {
"type": "Transitive",
"resolved": "3.0.0",
@@ -563,11 +545,7 @@
"ProjNET": {
"type": "Transitive",
"resolved": "2.0.0",
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
"dependencies": {
"System.Memory": "4.5.3",
"System.Numerics.Vectors": "4.5.0"
}
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
},
"Serilog.Sinks.File": {
"type": "Transitive",
@@ -591,11 +569,6 @@
"resolved": "6.0.0",
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
},
"System.Collections.Immutable": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ=="
},
"System.Composition": {
"type": "Transitive",
"resolved": "7.0.0",
@@ -644,128 +617,6 @@
"System.Composition.Runtime": "7.0.0"
}
},
"System.IO": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"System.Text.Encoding": "4.3.0",
"System.Threading.Tasks": "4.3.0"
}
},
"System.IO.Pipelines": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw=="
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
},
"System.Reflection": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.IO": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Emit.ILGeneration": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==",
"dependencies": {
"System.Reflection": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Emit.Lightweight": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==",
"dependencies": {
"System.Reflection": "4.3.0",
"System.Reflection.Emit.ILGeneration": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
"dependencies": {
"System.Collections.Immutable": "7.0.0"
}
},
"System.Reflection.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Text.Encoding": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Text.Json": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "eqWHDZqYPv1PvuvoIIx5pF74plL3iEOZOl/0kQP+Y0TEbtgNnM2W6k8h8EPYs+LTJZsXuWa92n5W5sHTWvE3VA=="
},
"System.Threading.Channels": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA=="
},
"System.Threading.Tasks": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"entity": {
"type": "Project",
"dependencies": {
@@ -911,10 +762,7 @@
"type": "CentralTransitive",
"requested": "[2.5.0, )",
"resolved": "2.5.0",
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
"dependencies": {
"System.Memory": "4.5.4"
}
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
},
"Newtonsoft.Json": {
"type": "CentralTransitive",
@@ -1006,136 +854,6 @@
}
}
},
"net9.0/linux-x64": {
"runtime.any.System.IO": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "SDZ5AD1DtyRoxYtEcqQ3HDlcrorMYXZeCt7ZhG9US9I5Vva+gpIWDGMkcwa5XiKL0ceQKRZIX2x0XEjLX7PDzQ=="
},
"runtime.any.System.Reflection": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "hLC3A3rI8jipR5d9k7+f0MgRCW6texsAp0MWkN/ci18FMtQ9KH7E2vDn/DH2LkxsszlpJpOn9qy6Z6/69rH6eQ=="
},
"runtime.any.System.Reflection.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "Nrm1p3armp6TTf2xuvaa+jGTTmncALWFq22CpmwRvhDf6dE9ZmH40EbOswD4GnFLrMRS0Ki6Kx5aUPmKK/hZBg=="
},
"runtime.any.System.Runtime": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "fRS7zJgaG9NkifaAxGGclDDoRn9HC7hXACl52Or06a/fxdzDajWb5wov3c6a+gVSlekRoexfjwQSK9sh5um5LQ==",
"dependencies": {
"System.Private.Uri": "4.3.0"
}
},
"runtime.any.System.Text.Encoding": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "+ihI5VaXFCMVPJNstG4O4eo1CfbrByLxRrQQTqOTp1ttK0kUKDqOdBSTaCB2IBk/QtjDrs6+x4xuezyMXdm0HQ=="
},
"runtime.any.System.Threading.Tasks": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "OhBAVBQG5kFj1S+hCEQ3TUHBAEtZ3fbEMgZMRNdN8A0Pj4x+5nTELEqL59DU0TjKVE6II3dqKw4Dklb3szT65w=="
},
"runtime.native.System": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"runtime.unix.System.Private.Uri": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "ooWzobr5RAq34r9uan1r/WPXJYG1XWy9KanrxNvEnBzbFdQbMG7Y3bVi4QxR7xZMNLOxLLTAyXvnSkfj5boZSg==",
"dependencies": {
"runtime.native.System": "4.3.0"
}
},
"System.IO": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"System.Text.Encoding": "4.3.0",
"System.Threading.Tasks": "4.3.0",
"runtime.any.System.IO": "4.3.0"
}
},
"System.Private.Uri": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "I4SwANiUGho1esj4V4oSlPllXjzCZDE+5XXso2P03LW2vOda2Enzh8DWOxwN6hnrJyp314c7KuVu31QYhRzOGg==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"runtime.unix.System.Private.Uri": "4.3.0"
}
},
"System.Reflection": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.IO": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0",
"runtime.any.System.Reflection": "4.3.0"
}
},
"System.Reflection.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"runtime.any.System.Reflection.Primitives": "4.3.0"
}
},
"System.Runtime": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"runtime.any.System.Runtime": "4.3.0"
}
},
"System.Text.Encoding": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"runtime.any.System.Text.Encoding": "4.3.0"
}
},
"System.Threading.Tasks": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"runtime.any.System.Threading.Tasks": "4.3.0"
}
}
}
"net10.0/linux-x64": {}
}
}

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>7.1.0</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>7.1.0</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,90 +0,0 @@
open Fake.Core
open Fake.IO
open Farmer
open Farmer.Builders
open Helpers
initializeContext()
let serverPath = Path.getFullName "src/Server"
let clientPath = Path.getFullName "src/Client"
let testPath = Path.getFullName "test"
let libPath = Path.getFullName "src/Interfaces" |> Some
let distPath = Path.getFullName "dist"
let packPath = Path.getFullName "packages"
let versionFile = Path.getFullName ".version"
let vite = """bunx --bun vite -c ../../vite.config.js"""
let fableOpt opts =
$"-e .jsx -o build --test:MSBuildCracker --run {vite} build --emptyOutDir --outDir {distPath}/public {opts}"
let fableWatch = $"watch -e .jsx -o build --run {vite}"
Target.create "Clean" (fun _ -> Shell.cleanDir distPath)
Target.create "Bundle" (fun _ ->
[ "server", dotnet $"build -tl -c Release -o {distPath} -p:DefineConstants=" serverPath
"client", fable (fableOpt "-m production") clientPath ]
|> runParallel
)
Target.create "BundleDebug" (fun _ ->
[ "server", dotnet $"build -tl -c Debug -o {distPath} -p:DefineConstants=" serverPath
"client", fable (fableOpt "-m development --minify false --sourcemap true") clientPath ]
|> runParallel
)
Target.create "Pack" (fun _ ->
match libPath with
| Some p -> run dotnet $"pack -c Release -o \"{packPath}\"" p
| None -> ()
)
Target.create "Run" (fun _ ->
[ "server", dotnet "watch run" serverPath
"client", fable fableWatch clientPath ]
|> runParallel
)
Target.create "Client" (fun _ ->
run fable fableWatch clientPath
)
Target.create "Format" (fun _ ->
run fantomas ". -r" "src"
)
Target.create "Test" (fun _ ->
if System.IO.Directory.Exists testPath then
[ "server", dotnet "run" (testPath + "/Server")
"client", fable $"-e .jsx -o build --run {vite}" (testPath + "/Client") ]
|> runParallel
else ()
)
open Fake.Core.TargetOperators
let dependencies = [
"Clean"
==> "Bundle"
"Clean"
==> "BundleDebug"
"Clean"
==> "Test"
"Clean"
==> "Run"
"Clean"
==> "Pack"
"Client"
]
[<EntryPoint>]
let main args = runOrDefault args

View File

@@ -1,15 +1,15 @@
# yaml-language-server: $schema=https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json
variables:
SKIP_TESTS: "true"
SKIP_TESTS: "true"
include:
- project: oceanbox/gitlab-ci
ref: v4.4
file: DotnetDeployment.gitlab-ci.yml
inputs:
project-name: atlantis
project-dir: src/Atlantis
- project: oceanbox/gitlab-ci
ref: v4.5
file: DotnetDeployment.gitlab-ci.yml
inputs:
project-name: atlantis
project-dir: src/Atlantis
dockerize-atlantis:
tags:
- nix
- nix

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="../../.build/Helpers.fs" />
<Compile Include=".build/Build.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fake.Core.Target" />
<PackageReference Include="Fake.DotNet.Cli" />
<PackageReference Include="Fake.IO.FileSystem" />
<PackageReference Include="Farmer" />
<PackageReference Include="FSharp.Core" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0.6
FROM mcr.microsoft.com/dotnet/aspnet:10.0
RUN apt-get update \
&& apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev
@@ -12,4 +12,4 @@ ENV SERVER_CONTENT_ROOT=/app/public
COPY dist/ /app
WORKDIR /app
CMD [ "dotnet", "/app/Server.dll" ]
CMD [ "dotnet", "/app/Server.dll" ]

View File

@@ -60,13 +60,13 @@ k8s_yaml(namespace_inject(blob(kustomizations), namespace))
local_resource(
'create-bundle',
cmd='dotnet run bundledebug',
cmd='just bundle-debug',
trigger_mode=TRIGGER_MODE_MANUAL
)
local_resource(
'build-server',
cmd='dotnet publish -o ./dist src/Server',
cmd='just bundle-debug-server',
deps=[
'./src/Server',
'./src/Shared'
@@ -74,6 +74,10 @@ local_resource(
ignore=[
'src/Server/bin',
'src/Server/obj',
'src/Server/Archmaester/obj',
'src/Server/Hipster/obj',
'src/Server/Petimeter/obj',
'src/Server/Common/obj',
'src/Shared/bin',
'src/Shared/obj',
],
@@ -84,7 +88,7 @@ local_resource(
local_resource(
'run-client',
serve_cmd='fable watch -e .jsx -o build --run vite -c ../../vite.config.js',
serve_cmd='just run-client',
serve_dir='./src/Client',
links=['https://{name}.local.oceanbox.io:{port}'.format(name=name, port=clientPort)],
resource_deps=['build-server'],

86
src/Atlantis/justfile Normal file
View File

@@ -0,0 +1,86 @@
# Atlantis build commands
# Install just: https://github.com/casey/just
set dotenv-load
src_path := "src"
server_path := "src/Server"
client_path := "src/Client"
test_path := "test"
lib_path := "src/Interfaces"
dist_path := "../../dist"
pack_path := "../../packages"
vite_prod := "bunx --bun vite build -c ../../vite.config.js -m production --emptyOutDir --outDir " + "../../dist/public"
vite_dev := "bunx --bun vite build -c ../../vite.config.js -m development --minify false --sourcemap true --emptyOutDir --outDir " + "../../dist/public"
vite := "bunx vite -c ../../vite.config.js -m development "
# Default recipe - show available commands
default:
@just --list
# Clean build artifacts
clean:
rm -rf {{dist_path}}
# Build production bundle (server + client)
[parallel]
bundle: clean bundle-server bundle-client
[working-directory: 'src/Server']
bundle-server:
dotnet build -tl -c Release -o {{dist_path}}
[working-directory: 'src/Client']
install-client:
bun install --frozen-lockfile
[working-directory: 'src/Client']
bundle-client: install-client
# Build debug bundle (server + client)
[parallel]
bundle-debug: clean bundle-debug-server bundle-debug-client
[working-directory: 'src/Server']
bundle-debug-server:
dotnet build -tl -c Debug -o {{dist_path}}
[working-directory: 'src/Client']
bundle-debug-client:
fable -e .jsx -o build --test:MSBuildCracker --run {{vite_dev}}
# Create NuGet package
[working-directory: 'src/Server']
pack: clean
dotnet pack -c Release -o "{{pack_path}}" {{lib_path}}
# Run development server (watch mode)
[parallel]
run: clean run-server run-client
[working-directory: 'src/Server']
run-server:
dotnet watch run
# Run client only in watch mode
[working-directory: 'src/Client']
run-client: install-client
fable watch -e .jsx -o build --test:MSBuildCracker --run {{vite}}
# Format code with Fantomas
format:
fantomas {{src_path}} -r
# Run tests
[parallel]
test: clean test-server test-client
[working-directory: 'src']
test-server:
dotnet run {{test_path}}/Server
[working-directory: 'src/Client']
test-client: install-client
fable -e .jsx -o build --run {{vite}}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<DefineConstants>FABLE_COMPILER</DefineConstants>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<Version>2.87.0</Version>

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Fable.Browser.IndexedDB": {
"type": "Direct",
"requested": "[2.2.0, )",

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>6.20.0</Version>
<RootNamespace>Archivist</RootNamespace>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<DefineConstants>FABLE_COMPILER</DefineConstants>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<Version>2.102.0</Version>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<DefineConstants>FABLE_COMPILER</DefineConstants>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<Version>2.87.0</Version>

View File

@@ -68,6 +68,7 @@ type barbTile =
static member inline drawColor(value: string) = unbox ("drawColor", value)
static member inline time(value: int) = unbox ("time", value)
static member inline url(value: string) = unbox ("url", value)
static member inline template(value: string) = unbox ("template", value)
module BarbTile =
type BarbTile =

View File

@@ -193,4 +193,10 @@ let plumeApi () =
Remoting.createApi ()
|> Remoting.withCredentials true
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|> Remoting.buildProxy<Api.Plume>
|> Remoting.buildProxy<Api.Plume>
let xtractApi () =
Remoting.createApi ()
|> Remoting.withCredentials true
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|> Remoting.buildProxy<Api.Xtract>

View File

@@ -768,21 +768,48 @@ type PlumeType =
match this with
| DefaultPlume -> "Plume"
type XtractType =
| DefaultXtract
override this.ToString (): string =
match this with
| DefaultXtract -> "xtract"
member this.ToLabel() =
match this with
| DefaultXtract -> "Xtract"
type XtractData = {
name: string
fvcom: System.Guid
start: DateTime
stop: DateTime
} with
static member empty = {
name = ""
fvcom = System.Guid.Empty
start = DateTime.Now
stop = DateTime.Now.AddDays (2)
}
type SimControlKind =
| Drifters of SimType
| Plume of PlumeType
| DataExtraction of XtractType
override this.ToString (): string =
match this with
| Drifters simType -> string simType
| Plume plumeType -> string plumeType
| DataExtraction xtractType -> string xtractType
member this.ToLabel() =
match this with
| Drifters simType -> simType.ToLabel ()
| Plume plumeType -> plumeType.ToLabel ()
| DataExtraction xtractType -> xtractType.ToLabel ()
member this.simTypeOpt =
match this with
| Drifters simType -> Some simType
| Plume plumeType -> None
| DataExtraction xtractType -> None
// TODO: Not sure if anything but Mapster needs to know about this
type SideNavMode =

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Fable.Browser.IndexedDB": {
"type": "Direct",
"requested": "[2.2.0, )",

View File

@@ -0,0 +1,585 @@
module DataExtraction
open System
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Fable.OpenLayers
open Lit
open Lit.Elmish
open Maps
open Remoting
open Atlantis.Types
open Atlantis.Shared.Notification
open Utils
open Hipster.Job
open Model
open Layers
//
// === Elmish ===
//
type SiteIdx = int
type private XtractMsg =
| SetExtractionSite of (float * float) option
| SetData of XtractData
| SetStarted of bool * int option
| ResetModel of XtractModel
| Noop of unit
let statusMessage (job: JobInfo) =
match job.status with
| JobStatus.New -> Note.info "New extraction"
| JobStatus.Waiting -> Note.info "Waiting..."
| JobStatus.Running -> Note.info "Running..."
| JobStatus.Completed -> Note.success "Extraction finished"
| JobStatus.Unknown -> Note.warn "Job status is unknown"
| _ (*Failed*) -> Note.error "Extraction failed"
let private update (msg: XtractMsg) (model: XtractModel) =
match msg with
| SetExtractionSite pos ->
console.debug ("[DataExtraction] SetExtractionSite msg:", pos)
{ model with position = pos }, Elmish.Cmd.none
| SetData s ->
console.debug ("[DataExtraction] SetData msg:", s)
{ model with data = s }, Elmish.Cmd.none
| XtractMsg.SetStarted (started, jobIdOpt) -> { model with start = started, jobIdOpt }, Elmish.Cmd.none
| XtractMsg.ResetModel m -> { m with data.name = model.data.name }, Elmish.Cmd.none
| XtractMsg.Noop () -> model, Elmish.Cmd.none
//
// === Views and components ===
//
[<HookComponent>]
let placingToggleButton (disabled: bool) (map: OlMap) (onPlace: Coordinate -> unit) =
let placing, setPlacing = Hook.useState false
let mapClickKey = Hook.useRef<Event.EventsKey> ()
let releaseClickHandler (e: Event.MapBrowserEvent) =
onPlace e.coordinate
setPlacing false
Hook.useEffectOnce (fun () ->
Hook.createDisposable (fun () ->
let elem = map.getTargetElement ()
elem?style?cursor <- ""
mapClickKey.contents |> Option.iter Observable.unByKey
)
)
Hook.useEffectOnChange (placing, crossHairSelect map mapClickKey releaseClickHandler)
html
$"""
<sp-action-button
style="width: 300px"
?disabled="{disabled}"
?selected={placing}
@click={Ev (fun _ -> setPlacing (not placing))}
>
<sp-icon-target slot="icon"></sp-icon-target>
Add extraction point
</sp-action-button>
"""
/// <summary>
/// Update the extraction site marker on the map
/// </summary>
let updateExtractionSite (posOpt: (float * float) option, map) =
map
|> updateBaseLayer
MapLayer.SelectedReleaseGroup
(fun baseLayer ->
let layer = baseLayer :?> VectorLayer
let source = layer.getSource () :?> VectorSource
source.clear ()
match posOpt with
| Some pos ->
let p' = pos |> posToCoord
let point =
Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
let feature = Feature.feature [ feature.geometryOrProperties point ]
source.addFeature (feature)
| _ -> ()
)
[<HookComponent>]
let private extractionSiteControls (dispatch': XtractMsg -> unit) (xmodel': XtractModel) =
let tryFence (pos: float * float) : (float * float) option =
match xmodel'.fence with
| None -> Some pos
| Some pts ->
let radius = sessionStorage["fence_radius"] |> float
let coords' = toEpsg3857 pts[0]
let radius' = radius * mercatorScaleFactor (snd pts[0])
let circle = Geometry.circle coords' radius' GeometryLayout.XY
if circle.intersectsCoordinate (pos |> posToCoord) then
Some pos
else
console.error ("[DataExtraction] Trying to place extraction point outside of fencing radius")
None
let handleMapPlaceExtraction (coords: Coordinate) =
console.debug ($"[DataExtraction] Click add site: %s{coords.ToString ()}")
coordToPos coords |> tryFence |> SetExtractionSite |> dispatch'
let setPosition (pos: float * float) : unit =
Some pos |> SetExtractionSite |> dispatch'
let selectedPos = xmodel'.position |> Option.defaultValue (0.0, 0.0) |> toWgs84'
let deleteSite (_: Browser.Types.Event) = None |> SetExtractionSite |> dispatch'
let latitudeBox =
let latitude = snd selectedPos
let disabled = xmodel'.position.IsNone
html
$"""
<sp-field-group vertical>
<sp-field-label for="latitude">
Latitude
</sp-field-label>
<sp-number-field
id="latitude"
style="width: 140px"
size="m"
step="0.000001"
format-options="{formatDigits 6 6}"
value={latitude}
?disabled="{disabled}"
@change={EvVal (
unbox<float>
>> fun v ->
let newPos = toEpsg3857' (fst selectedPos, v)
setPosition newPos
)}
>
</sp-number-field>
</sp-field-group>
"""
let longitudeBox =
let longitude = fst selectedPos
let disabled = xmodel'.position.IsNone
html
$"""
<sp-field-group vertical>
<sp-field-label for="longitude">
Longitude
</sp-field-label>
<sp-number-field
id="longitude"
style="width: 140px"
size="m"
step="0.000001"
format-options="{formatDigits 6 6}"
value={longitude}
?disabled="{disabled}"
@change={EvVal (
unbox<float>
>> fun v ->
let newPos = toEpsg3857' (v, snd selectedPos)
setPosition newPos
)}
></sp-number-field>
</sp-field-group>
"""
let siteDisplay =
match xmodel'.position with
| Some _ ->
let lon, lat = selectedPos
html
$"""
<div style="padding-top: 10px; padding-bottom: 10px">
<sp-divider style="width: 300px"></sp-divider>
</div>
<div style="padding-top: 5px">
<sp-field-label>Extraction Point</sp-field-label>
<div style="display: flex; gap: 8px; padding-top: 5px">
<sp-action-button style="width: 110px">
{lat |> sprintf "%.6f"}
</sp-action-button>
<sp-action-button style="width: 110px">
{lon |> sprintf "%.6f"}
</sp-action-button>
<sp-action-button
style="width: 35px"
@click={Ev deleteSite}
>
<sp-icon-delete slot="icon"></sp-icon-delete>
<sp-tooltip placement="right" self-managed>Remove point</sp-tooltip>
</sp-action-button>
</div>
</div>
"""
| None -> Lit.nothing
html
$"""
<sp-field-group vertical>
<sp-field-group horizontal style="padding-top: 5px; padding-bottom: 20px">
{latitudeBox}
{longitudeBox}
</sp-field-group>
</sp-field-group>
<div style="padding-top: 10px; padding-bottom: 5px">
{placingToggleButton xmodel'.position.IsSome xmodel'.openLayersMap handleMapPlaceExtraction}
</div>
{siteDisplay}
"""
[<HookComponent>]
let private extractionControls (dispatch': XtractMsg -> unit) (xmodel': XtractModel) =
html
$"""
<sp-accordion-item class="extraction-site" ?open={true} label="Extraction point">
{extractionSiteControls dispatch' xmodel'}
</sp-accordion-item>
"""
[<HookComponent>]
let controls xtractType (dispatch: Msg -> unit) (model: Model) =
let archive = model.archive
let xtractModelOpt = model.xtractModelOpt
let map = model.map
let currentFrame = model.frame
let archiveStartUTC = archive.startTime.ToUniversalTime ()
let archiveEndT =
archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
let archiveStartT =
archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
let submitted, setSubmitted = Hook.useState false
let createNewModel () : XtractModel =
let data =
match xtractModelOpt with
| Some existing -> { existing.data with fvcom = archive.id }
| None -> {
XtractData.empty with
fvcom = archive.id
start = archiveStartT
stop = archiveStartT.AddDays 2.0
}
{
fence = archive.polygon
start = false, None
kind = xtractType
data = data
position = None
openLayersMap = map
}
let xmodel', dispatch' =
Hook.useElmish (
(fun () ->
let xmodel' =
match xtractModelOpt with
| Some existingModel -> existingModel
| None -> createNewModel ()
xmodel', Elmish.Cmd.none
),
update
)
let modelRef = Hook.useRef (Some xmodel')
Hook.useEffectOnChange (
xmodel',
fun newModel ->
modelRef.contents <- Some newModel
SetXtractModel (Some newModel) |> dispatch
)
let setStartDateTime (dt: DateTime) =
// Set time to midday (12:00)
let startDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
let stopDate = xmodel'.data.stop
let newStop = if startDate >= stopDate then startDate.AddDays(1.0) else stopDate
console.debug ("[DataExtraction] setStartDateTime:", startDate, "stop:", newStop)
SetData { xmodel'.data with start = startDate; stop = newStop } |> dispatch'
let setStopDateTime (dt: DateTime) =
let endDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
let startDate = xmodel'.data.start
let newStart = if endDate <= startDate then endDate.AddDays(-1.0) else startDate
console.debug ("[DataExtraction] setStopDateTime:", endDate, "start:", newStart)
SetData { xmodel'.data with stop = endDate; start = newStart } |> dispatch'
let setName (s: string) =
let currentModel = modelRef.contents |> Option.defaultValue xmodel'
SetData { currentModel.data with name = s } |> dispatch'
let minStartDate = archiveStartUTC
let maxStartDate = archiveEndT
let minEndDate = archiveStartUTC
// maxEndDate is one year after the selected start date
let maxEndDate =
let startDate = xmodel'.data.start
startDate.AddYears(1)
let metaControls =
html
$"""
<div style="margin: 10px; padding-bottom: 5px;">
<sp-field-label>
Name (required)
</sp-field-label>
<sp-field-group horizontal id="vw" style="padding-bottom: 5px">
<sp-textfield
id="xtract-name"
label="Name"
placeholder="extraction-name"
required="true"
value="{xmodel'.data.name}"
@change={EvVal (setName)}
style="width: 300px"
></sp-textfield>
</sp-field-group>
</div>
<div class="grow m-8">
<div style="padding-bottom: 5px; padding-top: 20px">
<sp-field-group style="flex-grow: 1;" vertical>
<sp-field-label size="s" for="vertical">
Start date
</sp-field-label>
<sp-field-group horizontal style="padding-bottom: 10px;">
{FluentUI.Lit.DatePicker (false, xmodel'.data.start, setStartDateTime, minStartDate, maxStartDate)}
</sp-field-group>
</sp-field-group>
<sp-field-group style="flex-grow: 1;" vertical>
<sp-field-label size="s" for="vertical">
End date
</sp-field-label>
<sp-field-group horizontal style="padding-bottom: 10px;">
{FluentUI.Lit.DatePicker (false, xmodel'.data.stop, setStopDateTime, minEndDate, maxEndDate)}
</sp-field-group>
</sp-field-group>
</div>
</div>
"""
let submit _ =
if archive.id <> Guid.Empty && xmodel'.position.IsSome then
let data' = xmodel'.data
let pos' = xmodel'.position.Value |> toWgs84'
let id = Guid.NewGuid ()
let payload: XtractPayload = {
id = id
name = data'.name
archiveId = archive.id
positions = { Lat = snd pos'; Long = fst pos' }
start = data'.start
stop = data'.stop
basePath = "" // Set by server
caseName = "" // Set by server
projection = "" // Set by server
files = [||] // Set by server
}
console.log $"\n-------------- Data Extraction input ----------------"
console.log $"Name: {data'.name}"
console.log $"%A{payload}"
let api = xtractApi ()
async {
let! job = api.startXtract payload None
setSubmitted true
do
Lib.Umami.track (
"mapster-submit-extraction",
{|
archiveId = string archive.id
archiveName = string archive.name
status = if job.IsSome then "success" else "failed"
|}
)
match job with
| None ->
Note.failure "[DataExtraction] Job submission failed"
|> SetNotification
|> dispatch
XtractMsg.SetStarted (true, Some 0) |> dispatch'
| Some j ->
j |> statusMessage |> SetNotification |> dispatch
if
j.status = JobStatus.Waiting
|| j.status = JobStatus.Running
|| j.status = JobStatus.Completed
|| j.status = JobStatus.New
then
XtractMsg.SetStarted (true, Some j.jobId) |> dispatch'
let msg: Petimeter.Inbox.InboxItem = {
id = j.archiveId
content =
Thoth.Json.Encode.Auto.toString<JobMessage> (
{
aid = j.archiveId
job = j.jobId
name = j.name
status = j.status
}
: JobMessage
)
unread = true
type' = Petimeter.Inbox.MessageType.Xtract
created = DateTime.Now
}
msg
|> Atlantis.Shared.Hub.InboxMsg.Post
|> Atlantis.Shared.Hub.Action.Inbox
|> HubMsg
|> dispatch
}
|> Async.StartImmediate
let reset (_: Browser.Types.Event) =
console.debug ("[DataExtraction] Reset extraction")
clearFeatures map MapLayer.SelectedReleaseGroup
createNewModel () |> XtractMsg.ResetModel |> dispatch'
let cancel (_: Browser.Types.Event) =
console.debug ("[DataExtraction] Cancel extraction")
clearFeatures map MapLayer.SelectedReleaseGroup
modelRef.contents <- None
SetXtractModel None |> dispatch
SetMode Mode.Moot |> dispatch
let submitButtons =
let noName = String.IsNullOrWhiteSpace (xmodel'.data.name)
let noSite = xmodel'.position.IsNone
html
$"""
<div
id="extraction-submit-controls"
style="
display: flex;
flex-direction: row;
justify-content: center;
margin: 2px;
padding: 5px;
"
>
<sp-action-group
horizontal
compact
size="m"
>
<sp-action-button
static="primary"
style="width: 100px;"
?disabled={noName || noSite || submitted}
@click={Ev (submit)}
>
Submit
</sp-action-button>
<sp-action-button
static="primary"
style="width: 100px;"
@click={Ev (reset)}
>
Reset
</sp-action-button>
<sp-action-button
static="primary"
style="width: 100px;"
@click={Ev (cancel)}
>
Cancel
</sp-action-button>
</sp-action-group>
</div>
"""
Hook.useEffectOnce (fun () ->
console.debug ("[DataExtraction] === mounting ===")
Hook.createDisposable (fun () ->
console.log "[DataExtraction] Leaving extraction controls"
modelRef.contents |> SetXtractModel |> dispatch
)
)
Hook.useEffectOnChange (
xmodel',
fun newModel ->
console.debug ("[DataExtraction] Model changed", newModel)
modelRef.contents <- Some newModel
)
Hook.useEffectOnChange (
xmodel'.position,
fun posOpt ->
console.debug ("[DataExtraction] Position changed", posOpt)
updateExtractionSite (posOpt, xmodel'.openLayersMap) |> ignore
)
Hook.useEffectOnChange (
xmodel'.start,
fun (updatedStarted, _) ->
if updatedStarted then
console.log ("[DataExtraction] Extraction started: resetting")
do clearFeatures map MapLayer.SelectedReleaseGroup
Msg.SetSideNavMode OceanControls |> dispatch
modelRef.contents <- None
)
let measuresHeight =
tryGetElemRect "measures-controls"
|> Option.map _.height
|> Option.defaultValue 80
html
$"""
<div
style="
display: flex;
flex-direction: column;
align-items: center;
height: 95%%;
"
>
<h3>Data Extraction</h3>
<div
style="
width: 100%%;
max-height: calc(100%% - ({measuresHeight}px));
flex-grow: 1;
overflow-y: scroll;
border-bottom: 1px solid #eaeaea;
border-top: 1px solid #eaeaea;
"
>
<sp-accordion
allow-multiple
size="s"
density="spacious"
>
{metaControls}
{extractionControls dispatch' xmodel'}
</sp-accordion>
</div>
</div>
{submitButtons}
"""

View File

@@ -183,7 +183,7 @@ let fetchDrifters (api: ArchivesApi) (aid: Guid) : SimArchive [] JS.Promise =
let active = [||]
let active' =
active
|> Array.map (fun x ->
|> Array.map (fun (x: JobInfo) ->
{
Archive =
{ ArchiveProps.empty with
@@ -2272,7 +2272,7 @@ module private Deposition =
</div>
"""
let sitesRow idx site =
let sitesRow idx (site: ReleaseSite) =
let lon, lat = site.position |> toWgs84'
html
$"""
@@ -2781,9 +2781,6 @@ let private setupMetaParams disable (maxDurationHr: float) (simulation: Simulati
let r' = not simulation.reverse
{ simulation with reverse = r' } |> setSimModel
let startDateKey = "start-date"
let endDateKey = "end-date"
// Calculate min/max dates with buffer
let minStartDate = archiveStart
let maxStartDate = archiveEnd - TimeSpan.FromDays(simulation.simDays)
@@ -3872,6 +3869,7 @@ let simulationControls (simType: SimType) (dispatch: Msg -> unit) (model: Model)
name = j.name
}
Status = status
Reverse = input.simulation.reverse |> Option.defaultValue false
}
)
|> SetTmpDrifter

View File

@@ -43,16 +43,16 @@ let inboxDialog
let table = document.getElementById "inbox-table"
async {
let! mbox = Remoting.inboxApi().getMessages ()
// if mbox.Length = 0 then
// table?items <- [| {
// id = Guid.Empty
// content = ""
// unread = false
// type' = MessageType.Note
// created = DateTime.Now
// } |]
// else
table?items <- mbox
if mbox.Length = 0 then
table?items <- [| {
id = Guid.Empty
content = ""
unread = false
type' = MessageType.Note
created = DateTime.Now
} |]
else
table?items <- mbox
} |> Async.StartImmediate
Hook.useEffectOnChange(arg.unread, loadMessages)
@@ -65,30 +65,18 @@ let inboxDialog
|> Set.ofSeq
|> setSelected
let doDelete _ =
let doDelete selected _ =
let table = document.getElementById "inbox-table"
let selectedSet : Guid JS.Set = table?selectedSet
let items: InboxItem array = table?items
async {
let toDelete =
items
|> Array.filter (fun item -> selectedSet.has(item.id))
|> Array.map (fun item -> item.id)
console.debug("Deleting", toDelete.Length, "messages")
items
|> Array.filter (fun item -> Set.contains item.id selected)
|> Array.iter (fun item ->
console.log("Delete: %A", item.content)
do arg.deleteMessage item.id
)
for id in toDelete do
arg.deleteMessage id
// Clear selection immediately
selectedSet.clear()
setSelected Set.empty
// Wait a bit for server to process, then reload
do! Async.Sleep 200
let! mbox = Remoting.inboxApi().getMessages ()
table?items <- mbox
} |> Async.StartImmediate
loadMessages ()
let doRead selected _ =
let table = document.getElementById "inbox-table"
@@ -118,7 +106,7 @@ let inboxDialog
html $"""
<sp-field-group horizontal>
<sp-action-button
@click={Ev(doDelete)}
@click={Ev(doDelete selected)}
?disabled={selected.Count = 0}>
<sp-icon-delete slot="icon"></sp-icon-delete> Delete selected
</sp-action-button>
@@ -148,6 +136,7 @@ let inboxDialog
match item.type' with
| MessageType.Progress -> ()
| MessageType.Plume
| MessageType.Xtract
| MessageType.Drifters ->
let job = decodeJobMessage item.content
doRead (Set.singleton item.id) ()
@@ -180,6 +169,7 @@ let inboxDialog
let renderItem item =
let message, inactive =
match item.type' with
| MessageType.Xtract
| MessageType.Plume ->
let job = decodeJobMessage item.content
renderJobMessage job |> formatMsg item.unread false, false
@@ -227,11 +217,16 @@ let inboxDialog
| _ -> formatMsg item.unread false item.content, false
let downloadButton =
let disabled = not (item.type' = MessageType.Plume)
let disabled = not (item.type' = MessageType.Plume || item.type' = MessageType.Xtract)
let sorcerer = sessionStorage["sorcerer_url"]
let name = (decodeJobMessage item.content).name
let job = decodeJobMessage item.content
let downloadUrl =
match item.type' with
| MessageType.Plume -> $"{sorcerer}/download/plume/{arg.aid}/{item.id}/plume"
| MessageType.Xtract -> $"{sorcerer}/download/xtract/{arg.aid}/{job.job}/{job.name}"
| _ -> ""
html $"""
<a href="{sorcerer}/download/plume/{arg.aid}/{item.id}/plume">
<a href="{downloadUrl}">
<sp-action-button ?disabled={disabled} @click={Ev(_.stopPropagation())}>
<sp-icon-download slot="icon"></sp-icon-download>
</sp-action-button>
@@ -301,11 +296,8 @@ let inboxDialog
let sortFn = if sortDir = "asc" then Array.sortBy else Array.sortByDescending
table?items
|> sortFn (fun item -> JS.expr_js $"{item}[{sortKey}]")
|> fun items -> table?items <- items))
let table =
html $"""
"""
|> fun items -> table?items <- items)
)
html $"""
<div class="inbox-dialog">

View File

@@ -897,7 +897,12 @@ let reRenderStreams model : unit =
[<HookComponent>]
let oceanControls dispatch model =
let disabled = model.archive.id = Guid.Empty
let stats = not model.stats.showInstant
let noTemp = stats && (model.statsAvailable |> Array.contains StatProp.Temperature |> not)
let noSalt = stats && (model.statsAvailable |> Array.contains StatProp.Salinity |> not)
let noSpeed = stats && (model.statsAvailable |> Array.contains StatProp.Speed |> not)
let prop = model.glLayers[Ocean]
let setProp p (ev: Types.Event) =
ev.stopPropagation()
@@ -956,9 +961,9 @@ let oceanControls dispatch model =
</sp-field-label>
<sp-radio-group id="model-views" selected="{string prop.PropType}" vertical>
<sp-radio value="{Prop.Map}" @change={Ev(setProp Prop.Map)}>Map</sp-radio>
<sp-radio ?disabled={disabled} value="{Prop.Temp}" @change={Ev(setProp Prop.Temp)}>Temperature</sp-radio>
<sp-radio ?disabled={disabled} value="{Prop.Salt}" @change={Ev(setProp Prop.Salt)}>Salinity</sp-radio>
<sp-radio ?disabled={disabled} value="{Prop.Speed}" @change={Ev(setProp Prop.Speed)}>Speed</sp-radio>
<sp-radio ?disabled={disabled || noTemp} value="{Prop.Temp}" @change={Ev(setProp Prop.Temp)}>Temperature</sp-radio>
<sp-radio ?disabled={disabled || noSalt} value="{Prop.Salt}" @change={Ev(setProp Prop.Salt)}>Salinity</sp-radio>
<sp-radio ?disabled={disabled || noSpeed} value="{Prop.Speed}" @change={Ev(setProp Prop.Speed)}>Speed</sp-radio>
<sp-radio ?disabled={disabled || stats} value="{Prop.Zeta}" @change={Ev(setProp Prop.Zeta)}>Elevation</sp-radio>
<sp-radio ?disabled={disabled || stats} value="{Prop.Bathy}" @change={Ev(setProp Prop.Bathy)}>Depth</sp-radio>
</sp-radio-group>

View File

@@ -220,14 +220,14 @@ let initializeArchive model =
// Wind barbs
let windBarbSource =
let url = "/api/v2/atmo/Wind/WindTile"
let url = "https://sorcerer.vtn.oceanbox.io/api/v2/atmo/Wind/WindTile"
console.debug ("initializeArchive windbarb source url:", url)
BarbTile.barbTile [
barbTile.archive aid
barbTile.arrows model.arrows
barbTile.arrowsPerTile model.arrowsPerTile
barbTile.time model.frame
barbTile.url url
barbTile.template url
]
let windBarbLayer =
Layer.tileLayer [
@@ -250,6 +250,9 @@ let initializeArchive model =
// TODO(simkir): add existing release sites to release layer
// #6 is the wireframe, but it is created after being loaded from indexeddb
let statsApi = StatsApi (getDataUrl ())
let! stats = statsApi.FvStatsInfo.GetAvailableStats aid
let prop = {
PropType = Prop.Map
FieldKind = UndefinedField
@@ -265,6 +268,7 @@ let initializeArchive model =
grid = g
uvs = uvFlat
mode = Mode.Ocean
statsAvailable = stats
glLayers = Map.add Ocean prop model.glLayers
}
}
@@ -1585,7 +1589,7 @@ let update cmd model =
| SetStats stats ->
let updated = { stats with propType = Stats.Utils.fromProp model.probePoint.prop }
{ model with stats = updated }, Cmd.none
| SetStatsAvailable stats -> { model with statsAvailable = Some stats }, Cmd.none
| SetStatsAvailable stats -> { model with statsAvailable = stats }, Cmd.none
| SetStatShowInstant showInstant ->
let updated = { model with isLoading = Some MapLoading.Progress; stats.showInstant = showInstant }
updated, Cmd.OfPromise.perform updateOceanProp updated SetProp
@@ -1808,6 +1812,9 @@ let update cmd model =
| SetPlumeModel modelOpt ->
console.debug ("[Mapster] SetPlumeModel:", modelOpt)
{ model with plumeModelOpt = modelOpt }, Cmd.none
| SetXtractModel modelOpt ->
console.debug ("[Mapster] SetXtractModel:", modelOpt)
{ model with xtractModelOpt = modelOpt }, Cmd.none
| HubMsg msg -> { model with hubAction = Some msg }, Cmd.none
| Noop _ -> model, Cmd.none
@@ -1886,7 +1893,7 @@ let fetchArchive (archiveId: System.Guid) : ArchiveInfo Async =
polygon = a.polygon |> Option.map (Array.map (fun (x, y) -> float x, float y))
frames = archiveFrames
}
| Result.Error err ->
| Error err ->
console.error $"Could not retrieve the selected archive!: {err}"
return ArchiveInfo.empty
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<DefineConstants>FABLE_COMPILER</DefineConstants>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<Version>2.87.0</Version>
@@ -37,6 +37,7 @@
<Compile Include="Fiskeridir.fs" />
<Compile Include="BarentsWatch.fs" />
<Compile Include="Plume.fs" />
<Compile Include="DataExtraction.fs" />
<Compile Include="Drifters.fs" />
<Compile Include="ContourModel.fs" />
<Compile Include="Postdrift.fs" />

View File

@@ -242,6 +242,15 @@ type PlumeModel = {
openLayersMap: OlMap
}
type XtractModel = {
fence: (float * float) array option
start: bool * int option
data: XtractData
kind: XtractType
position: (float * float) option
openLayersMap: OlMap
}
type ContourData = (float * float)[][]
type ContourStyle = {
lineWidth: float
@@ -350,10 +359,11 @@ type Model = {
// choose what stats to consider. Could of course just wrap these two in another structure. Or, perhaps of saving
// the available stats here in the model, the sidebar could be responsible and store the avaiable stats in local
// store instead.
statsAvailable: StatProp array option
statsAvailable: StatProp array
stats: Stats
plumeModelOpt: PlumeModel option
xtractModelOpt: XtractModel option
infectionNetwork: NetworkState
customGrid: CircleGrid option
@@ -455,8 +465,9 @@ type Model = {
inboxUnread = 0
hubAction = None
plumeModelOpt = None
xtractModelOpt = None
infectionNetwork = NetworkState.empty
statsAvailable = None
statsAvailable = Array.empty
stats = Stats.empty
}
@@ -484,6 +495,7 @@ type Msg =
| DeleteArchive of System.Guid
| CancelJob of int
| SetPlumeModel of PlumeModel option
| SetXtractModel of XtractModel option
| ShowReleases of bool
// Map / Layers

View File

@@ -3,12 +3,15 @@ module Navigation
open System
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Fable.OpenLayers
open Lit
open Remoting
open Archmaester.Dto
open Atlantis.Types
open Atlantis.Shared
open Sorcerer.Types
open Colors
open Drifters.ApiTypes
@@ -97,28 +100,30 @@ let private simAccordion (dispatch: Msg -> unit) model =
console.debug $"policies: %A{model.simPolicies}"
let disabled = model.archive.id = Guid.Empty
// TODO(mrtz): Create custom policy for plumes, for now just inherit from drifters.
let disabledPlume = model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
let disabledTransport = model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
let disabledDeposition = model.simPolicies |> Array.contains (DriftersPolicy.SubmitSedimentation false)
// TODO(mrtz): Create custom policy for plume and xtract, for now just inherit from drifters.
let disabledPlume =
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
let disabledXtract =
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
let disabledTransport =
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
let disabledDeposition =
model.simPolicies |> Array.contains (DriftersPolicy.SubmitSedimentation false)
let chooseMode (kind: SimControlKind) _ =
// NOTE: Once you have commited to creating a new simulation, clear whatever sims you have chosen
UnsetSelectedDrifter () |> dispatch
SimControls kind
|> SetSideNavMode
|> dispatch
SimControls kind |> SetSideNavMode |> dispatch
do Lib.Umami.track $"mapster-enter-{string kind}-simulation"
Mode.Simulation Placing
|> SetMode
|> dispatch
Mode.Simulation Placing |> SetMode |> dispatch
html
$"""
<div class="full-box flex-column" style="align-items: center;">
<h3>Particle Simulations</h3>
<h3>Simulations</h3>
<sp-action-group
vertical
@@ -174,6 +179,22 @@ let private simAccordion (dispatch: Msg -> unit) model =
Plume
</sp-action-button>
</sp-action-group>
<h3>Data Extraction</h3>
<sp-action-group
vertical
size="m"
style="width: 90%%;"
>
<sp-action-button
static="primary"
style="flex-grow: 1"
?disabled={disabledXtract }
@click={Ev (chooseMode (DataExtraction DefaultXtract))}
>
Extract Data
</sp-action-button>
</sp-action-group>
</div>
"""
@@ -224,7 +245,7 @@ let private aquacultureLookup (onClick: int option -> unit) =
"""
[<HookComponent>]
let private mapsSelect (model: Model) (dispatch: Msg -> unit) =
let private mapsSelect (model: Model) (dispatch: Msg -> unit) =
Hook.useHmr hmr
let items =
@@ -239,20 +260,20 @@ let private mapsSelect (model: Model) (dispatch: Msg -> unit) =
NorgesKart BackgroundNorway
OSM
|]
|> Array.map (fun map -> !!{| value = string map; label = map.ToLabel(); |})
|> Array.map (fun map -> !!{| value = string map; label = map.ToLabel () |})
let handleChange (ev: Types.Event) (data: obj) =
console.debug("[Nav] Map layer changed: %o", data)
let value : string = data?value
console.debug ("[Nav] Map layer changed: %o", data)
let value: string = data?value
match MapKind.OfString value with
| Some mapKind -> SetMapKind mapKind |> dispatch
| None -> console.error("[Nav] Got invalid map layer: %s", value)
| None -> console.error ("[Nav] Got invalid map layer: %s", value)
html
$"""
<div style="padding-top: 10px">
<sp-field-label for="map-layer-picker">Map type</sp-field-label>
{FluentUI.Lit.Select(string model.mapKind, items, handleChange)}
{FluentUI.Lit.Select (string model.mapKind, items, handleChange)}
</div>
"""
@@ -313,17 +334,17 @@ let private GraphRangeSlider (model: Model) dispatch =
let rangeLow, rangeHigh = model.probePoint.propRange
let handleInputChange (ev: Types.Event) =
ev.stopPropagation()
console.debug("[Nav] Graph slider input: %o", ev)
ev.stopPropagation ()
console.debug ("[Nav] Graph slider input: %o", ev)
let target = ev.target
let value = target.Value
if target?name = "high" then
console.debug("[Nav] High value is: %s", value)
console.debug ("[Nav] High value is: %s", value)
let newHigh = fst model.probePoint.propRange, unbox value
SetProbing { model.probePoint with propRange = newHigh } |> dispatch
elif target?name = "low" then
let value = target.Value
console.debug("[Nav] Low value is: %s", value)
console.debug ("[Nav] Low value is: %s", value)
let newLow = unbox value, snd model.probePoint.propRange
SetProbing { model.probePoint with propRange = newLow } |> dispatch
else
@@ -331,21 +352,21 @@ let private GraphRangeSlider (model: Model) dispatch =
// console.error("[Nav] Error")
()
let handleChangeEvent (ev: Types.Event) =
ev.stopPropagation()
console.debug("[Nav] Graph slider change: %o", ev)
ev.stopPropagation ()
console.debug ("[Nav] Graph slider change: %o", ev)
let target = ev.target
let value = target.Value
if target?name = "high" then
console.debug("[Nav] High value changed: %s", value)
console.debug ("[Nav] High value changed: %s", value)
let newHigh = fst model.probePoint.propRange, unbox value
SetProbing { model.probePoint with propRange = newHigh } |> dispatch
elif target?name = "low" then
let value = target.Value
console.debug("[Nav] Low value changed: %s", value)
console.debug ("[Nav] Low value changed: %s", value)
let newLow = unbox value, snd model.probePoint.propRange
SetProbing { model.probePoint with propRange = newLow } |> dispatch
else
console.error("[Nav] Error")
console.error ("[Nav] Error")
html
$"""
@@ -379,10 +400,10 @@ let private RosePlotControls (statsAvailable: bool) (probePoint: Probing) (stats
Hook.useHmr hmr
let handleDepthsChange (depths: Map<int, bool>) =
console.debug("[RosePlotControls] Depths changed: %s", Map.toArray depths)
console.debug ("[RosePlotControls] Depths changed: %s", Map.toArray depths)
SetProbing { probePoint with selectedDepths = depths } |> dispatch
let handlePeriodChange (period: Sorcerer.Types.Period) =
console.debug("[RosePlotControls] period changed: %s", string period)
console.debug ("[RosePlotControls] period changed: %s", string period)
SetStatPeriod period |> dispatch
// TODO: Download button
@@ -408,7 +429,7 @@ let private RosePlotControls (statsAvailable: bool) (probePoint: Probing) (stats
{if statsAvailable then
Stats.Controls.PeriodSelection stats.period handlePeriodChange
else
noStatsBanner()}
noStatsBanner ()}
</sp-accordion-item>
</sp-accordion>
@@ -438,7 +459,10 @@ let private TimeSeriesControls (model: Model) dispatch =
let propOpt = selected |> Array.tryHead |> Option.map Prop.fromString
propOpt
|> Option.iter (fun prop -> SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange} |> dispatch)
|> Option.iter (fun prop ->
SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange }
|> dispatch
)
let handleUnitChange (ev: Types.Event) =
ev.stopPropagation ()
@@ -451,11 +475,10 @@ let private TimeSeriesControls (model: Model) dispatch =
if yearMeanExists then
SetStatMetrics Mean |> dispatch
let handleDepthsChange newDepths =
SetProbeDepths newDepths |> dispatch
let handleDepthsChange newDepths = SetProbeDepths newDepths |> dispatch
let handleYearMeanChange (ev: Types.Event) =
ev.stopPropagation()
ev.stopPropagation ()
SetStatMetrics Mean |> dispatch
let timeSeriesSelectors _ =
@@ -511,9 +534,16 @@ let private TimeSeriesControls (model: Model) dispatch =
</div>
"""
let showDepth = match model.probePoint.prop with | Prop.Zeta -> false | _ -> true
let disableMean = match model.probePoint.prop with | Prop.Zeta | Prop.Dens -> true | _ -> false
let statsAvailable = model.statsAvailable |> Option.map (Array.isEmpty >> not) |> Option.defaultValue false
let showDepth =
match model.probePoint.prop with
| Prop.Zeta -> false
| _ -> true
let disableMean =
match model.probePoint.prop with
| Prop.Zeta
| Prop.Dens -> true
| _ -> false
let statsAvailable = (Array.isEmpty >> not) model.statsAvailable
let statsAccordion () =
html
@@ -564,7 +594,7 @@ let private TimeSeriesControls (model: Model) dispatch =
</sp-action-group>
<div class="flex-row" style="gap: 24px;">
{timeSeriesSelectors()}
{timeSeriesSelectors ()}
{if showDepth then
PropertyPlots.DepthSelectors false model.probePoint.selectedDepths handleDepthsChange
@@ -574,7 +604,10 @@ let private TimeSeriesControls (model: Model) dispatch =
<sp-accordion>
<sp-accordion-item label="Statistics">
{if statsAvailable then statsAccordion() else noStatsBanner()}
{if statsAvailable then
statsAccordion ()
else
noStatsBanner ()}
</sp-accordion-item>
</sp-accordion>
</div>
@@ -595,7 +628,10 @@ let private OceanPlotControls model dispatch =
let propOpt = selected |> Array.tryHead |> Option.map Prop.fromString
propOpt
|> Option.iter (fun prop -> SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange} |> dispatch)
|> Option.iter (fun prop ->
SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange }
|> dispatch
)
html
$"""
@@ -624,7 +660,10 @@ let private OceanPlotControls model dispatch =
<sp-accordion>
<sp-accordion-item label="Statistics">
{if model.archive.id <> Guid.Empty then Stats.Controls.View dispatch model else Lit.nothing}
{if model.archive.id <> Guid.Empty then
Stats.Controls.View dispatch model
else
Lit.nothing}
</sp-accordion-item>
</sp-accordion>
</div>
@@ -633,22 +672,17 @@ let private OceanPlotControls model dispatch =
[<HookComponent>]
let LinePlotControls dispatch (model: Model) =
let handlePropChange (ev: Types.Event) =
console.debug("[Nav] Line plot prop changed: %o", ev)
let selected : string array = ev.target?selected
console.debug ("[Nav] Line plot prop changed: %o", ev)
let selected: string array = ev.target?selected
let opt = selected |> Array.tryHead
match opt with
| Some propStr ->
let prop = Prop.fromString propStr
console.debug("[Nav] Line plot selected new prop: %s", prop)
SetProbing {
model.probePoint with
prop = prop
propRange = prop.viewRange
}
console.debug ("[Nav] Line plot selected new prop: %s", prop)
SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange }
|> dispatch
| None ->
console.error("[Nav] Unexpected selected from action-group")
| None -> console.error ("[Nav] Unexpected selected from action-group")
html
$"""
@@ -752,22 +786,21 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
let selectedStats = if model.stats.showInstant then 1 else 2
let handleStatsTabChange (ev: Types.Event) =
ev.stopPropagation()
console.debug("[Stats] Stats tab changed: %o", ev)
let selectedStr : string = ev.target?selected
console.debug("[Stats] Selected is: %o", selectedStr)
ev.stopPropagation ()
console.debug ("[Stats] Stats tab changed: %o", ev)
let selectedStr: string = ev.target?selected
console.debug ("[Stats] Selected is: %o", selectedStr)
let selected = int selectedStr
// TODO: Enum?
console.debug("[Stats] Stats tab changed to: %d", selected)
console.debug ("[Stats] Stats tab changed to: %d", selected)
match selected with
| 1 ->
SetStatShowInstant true |> dispatch
| 1 -> SetStatShowInstant true |> dispatch
| 2 ->
ShowStreams false |> dispatch
ShowWindBarbs false |> dispatch
SetStatShowInstant false |> dispatch
| _ -> console.error("[Stats] Invalid stats tab (%d) selected. Somehow...", selected)
| _ -> console.error ("[Stats] Invalid stats tab (%d) selected. Somehow...", selected)
let selectedTab =
match model.probePoint.view with
@@ -783,25 +816,31 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
| _ -> console.error "simControls: Coordinate in wrong format!"
let handleTabChange (ev: Types.Event) =
console.debug("[Nav] Probe tab changed: %o", ev)
let selectedStr : string = ev.target?selected
console.debug("[Nav] Selected is: %o", selectedStr)
console.debug ("[Nav] Probe tab changed: %o", ev)
let selectedStr: string = ev.target?selected
console.debug ("[Nav] Selected is: %o", selectedStr)
let selected = int selectedStr
// TODO: Enum?
console.debug("[Nav] Probe tab changed to: %d", selected)
console.debug ("[Nav] Probe tab changed to: %d", selected)
match selected with
| 1 -> SetProbing { model.probePoint with view = DepthProfile; series = false } |> dispatch
| 2 -> SetProbing { model.probePoint with view = TimeSeries; series = true } |> dispatch
| 3 -> SetProbing { model.probePoint with view = RosePlots; series = false } |> dispatch
| _ -> console.error("[Nav] Invalid probe tab (%d) selected. Somehow...", selected)
| 1 ->
SetProbing { model.probePoint with view = DepthProfile; series = false }
|> dispatch
| 2 ->
SetProbing { model.probePoint with view = TimeSeries; series = true }
|> dispatch
| 3 ->
SetProbing { model.probePoint with view = RosePlots; series = false }
|> dispatch
| _ -> console.error ("[Nav] Invalid probe tab (%d) selected. Somehow...", selected)
let handleStatPeriodChange (newPeriod: Period) =
console.debug("[Nav] Stat period changed %s", string newPeriod)
console.debug ("[Nav] Stat period changed %s", string newPeriod)
SetStatPeriod newPeriod |> dispatch
let handleStatMetricChange (newMetric: StatMetric) =
console.debug("[Nav] Stat metric changed %s", newMetric)
console.debug ("[Nav] Stat metric changed %s", newMetric)
// TODO: Uhm, yeah. Add a RemoveStatMetric msg, I guess. Or, StatMetricRemove and StatMetricAdd
let existing = model.stats.metrics |> Array.tryHead
if existing.IsSome then
@@ -865,8 +904,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
match model.mode with
| Mode.Simulation SimMode.Placing -> setGridding true
| _ -> setGridding false
| None ->
setGridding false
| None -> setGridding false
)
// If you are hovering over a vector feature on the release layer, aka the release points, remove the event
@@ -917,8 +955,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
let activeControls () =
let pointPlotMode = model.probePoint.point.IsSome && model.probePoint.idx.IsSome
let linePlotMode = (Array.isEmpty >> not) model.pickLine
// TODO: This needs to be fetching in the model on loading archive
let statsAvailable = model.statsAvailable |> Option.map (Array.isEmpty >> not) |> Option.defaultValue false
let statsAvailable = (Array.isEmpty >> not) model.statsAvailable
match model.sideNavMode with
| OceanControls ->
@@ -942,7 +979,8 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
elif linePlotMode then
LinePlotControls dispatch model
else
let divider () = html $"""<sp-divider size="s"></sp-divider>"""
let divider () =
html $"""<sp-divider size="s"></sp-divider>"""
let stats () =
match model.selectedDrifters with
| Some _ -> divider ()
@@ -951,7 +989,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
if model.stats.showInstant then
Lit.nothing
elif not statsAvailable then
noStatsBanner()
noStatsBanner ()
else
html
$"""
@@ -1029,7 +1067,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
</sp-tabs>
</div>
{stats () (* TODO: if statsAvailable then stats () else divider () *)}
{stats ()}
{noAnalysisText ()}
@@ -1043,13 +1081,16 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
let simSelector (dispatch: Msg -> unit) (model: Model) =
match model.mode with
| Mode.Simulation Placing ->
match kind.simTypeOpt with
| Some simType ->
match kind with
| Drifters simType ->
console.log $"drifter simControls : {kind}"
Drifters.simulationControls simType dispatch model
| None ->
| Plume _ ->
console.log $"plume simControls : {kind}"
Plume.simulationControls DefaultPlume dispatch model
| DataExtraction xtractType ->
console.log $"extraction simControls : {kind}"
DataExtraction.controls xtractType dispatch model
| Mode.Moot
| Mode.Ocean
| Mode.Stats _
@@ -1078,8 +1119,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
| AnalysisControls archive ->
let simSelector (dispatch: Msg -> unit) (model: Model) =
match model.mode with
| Mode.Simulation Placing ->
Postdrift.analysisControls archive dispatch model
| Mode.Simulation Placing -> Postdrift.analysisControls archive dispatch model
| Mode.Moot
| Mode.Ocean
| Mode.Stats _
@@ -1105,8 +1145,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
{aquacultureLookup (SetAquaculture >> dispatch)}
{mapsSelect model dispatch}
"""
| CropControls ->
ProbingControls.cropControls dispatch model
| CropControls -> ProbingControls.cropControls dispatch model
let timeControls () =
let n =
@@ -1205,18 +1244,23 @@ let toolboxNav setArchivesOpen setInboxOpen model dispatch =
| Mode.Ocean -> ()
| _ -> SetMode Mode.Ocean |> dispatch
let selectCompute (driftersModelOpt: DrifterModel option) (plumeModelOpt: PlumeModel option) ev =
let selectCompute (driftersModelOpt: DrifterModel option) (plumeModelOpt: PlumeModel option) (xtractModelOpt: XtractModel option) ev =
if canSubmit then
match driftersModelOpt, plumeModelOpt with
| Some d, _ ->
console.debug $"We already have an ongoing driftser sim : {d.simulation.kind}"
match driftersModelOpt, plumeModelOpt, xtractModelOpt with
| Some d, _, _ ->
console.debug $"We already have an ongoing drifters sim : {d.simulation.kind}"
Drifters d.simulation.kind |> SimControls |> SetSideNavMode |> dispatch
SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch
| None, Some p ->
| None, Some p, _ ->
console.debug $"We already have an ongoing plume sim"
Plume p.kind |> SimControls |> SetSideNavMode |> dispatch
SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch
| None, None, Some x ->
console.debug $"We already have an ongoing extraction"
DataExtraction x.kind |> SimControls |> SetSideNavMode |> dispatch
SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch
| _ ->
Drifters SimType.TransportSim |> SimControls |> SetSideNavMode |> dispatch
@@ -1274,8 +1318,8 @@ let toolboxNav setArchivesOpen setInboxOpen model dispatch =
<overlay-trigger id="trigger" placement="right" offset="6" triggered-by="hover">
<sp-tooltip slot="hover-content">Compute</sp-tooltip>
<div slot="trigger" class="toolbox-control{if canSubmit then " " + isSimControls else ".disabled"}">
<div class="toolbox-icon" @click={Ev (selectCompute model.drifterModelOpt model.plumeModelOpt)}>
{if model.drifterModelOpt.IsSome then
<div class="toolbox-icon" @click={Ev (selectCompute model.drifterModelOpt model.plumeModelOpt model.xtractModelOpt)}>
{if model.drifterModelOpt.IsSome || model.plumeModelOpt.IsSome || model.xtractModelOpt.IsSome then
statusIcon
else
Lit.nothing}

View File

@@ -6,9 +6,35 @@ open FsToolkit.ErrorHandling
open Atlantis.Types
open Sorcerer.Types
open Stats.Controls
let private hmr = Lit.HMR.createToken ()
let private priorityMap =
[|
"instant"
string StatMetric.Mean
string StatMetric.Std
string StatMetric.Q05
string StatMetric.Q25
string StatMetric.Q50
string StatMetric.Q75
string StatMetric.Q95
string StatMetric.Q99
|] |> Array.mapi (fun i s -> s, i) |> Map.ofArray
let private sortStats (inp: string array) : string array =
let toPriority (s: string) : int =
s.Split(' ')
|> Array.last
|> priorityMap.TryFind
|> Option.defaultValue -1
inp
|> Array.map (fun s -> toPriority s, s)
|> Array.sortBy fst
|> Array.map snd
let private fetchPropData (archiveId: System.Guid) (frame: FrameIdx) (gridIdx: GridIdx) (prop: Prop) : JS.Promise<single array> =
promise {
let dataUrl = Remoting.getDataUrl ()
@@ -286,7 +312,9 @@ let View
let model, dispatch = Hook.useElmish ((fun () -> init archive.id gridIdx stats.showInstant probeProp), update)
let traceArray: Plotly.ITraces array = model.TraceMap |> Map.values |> Array.ofSeq
let sortedKeys = model.TraceMap |> Map.keys |> Array.ofSeq |> sortStats
let traceArray: Plotly.ITraces array = sortedKeys |> Array.choose (fun k -> Map.tryFind k model.TraceMap)
let plotInfo: Plotly.PlotInfoWithTraces = {
title = $"{probeProp.ToLabel ()}"
ylegend = "Depth [m]"

View File

@@ -6,7 +6,6 @@ open Fable.Core.JsInterop
open Lit
open Atlantis.Types
open Remoting
open Sorcerer.Types
@@ -88,8 +87,6 @@ let private MetricsAccordion (showInstant: bool) (selectedMetrics: StatMetric ar
</div>
"""
let private testStats propType = Option.map (Array.contains propType >> not) >> Option.defaultValue false
[<HookComponent>]
let PeriodSelection (period: Period) (onSelect: Period -> unit) =
Hook.useHmr hmr
@@ -120,9 +117,9 @@ let PeriodSelection (period: Period) (onSelect: Period -> unit) =
let View (dispatch: Model.Msg -> unit) (model: Model.Model) =
Hook.useHmr hmr
let noTemp = model.statsAvailable |> testStats StatProp.Temperature
let noSalt = model.statsAvailable |> testStats StatProp.Salinity
let noSpeed = model.statsAvailable |> testStats StatProp.Speed
let noTemp = model.statsAvailable |> Array.contains StatProp.Temperature |> not
let noSalt = model.statsAvailable |> Array.contains StatProp.Salinity |> not
let noSpeed = model.statsAvailable |> Array.contains StatProp.Speed |> not
let noStats = noTemp && noSalt && noSpeed
let statsHrefOpt =
@@ -145,22 +142,6 @@ let View (dispatch: Model.Msg -> unit) (model: Model.Model) =
let handlePeriodChange (newPeriod: Period) =
Model.SetStatPeriod newPeriod |> dispatch
Hook.useEffectOnce (fun () ->
let statsApi = StatsApi (getDataUrl ())
console.info("[Stats] Mounting statsControl for archive %s", model.archive.name)
match model.statsAvailable with
| Some stats -> ()
| None ->
statsApi.FvStatsInfo.GetAvailableStats model.archive.id
|> Async.StartAsPromise
|> Promise.iter (fun avail ->
Model.SetStatsAvailable avail
|> dispatch
)
)
if noStats then
html
$"""

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Fable.Browser.IndexedDB": {
"type": "Direct",
"requested": "[2.2.0, )",

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<DefineConstants>FABLE_COMPILER</DefineConstants>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<Version>1.9.8</Version>

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Fable.Core": {
"type": "Direct",
"requested": "[4.4.0, )",

View File

@@ -24,7 +24,38 @@
min-height: 100px;
min-width: 400px;
}
</style>
<!-- .winterContainer { -->
<!-- position:fixed; -->
<!-- top:0; -->
<!-- left:0; -->
<!-- width:100%; -->
<!-- height:100%; -->
<!-- pointer-events:none; -->
<!-- overflow:hidden; -->
<!-- z-index:10; -->
<!-- } -->
<!-- @keyframes snowfall { -->
<!-- 0% { -->
<!-- transform: translateY(0) translateX(0) rotate(0deg); -->
<!-- opacity: 0.9; -->
<!-- } -->
<!-- 100% { -->
<!-- transform: translateY(100vh) translateX(var(--swirl)) rotate(var(--rot)); -->
<!-- opacity: 0.9; -->
<!-- } -->
<!-- } -->
<!-- .winterSnowflake { -->
<!-- position: absolute; -->
<!-- top: 0; -->
<!-- left: 0; -->
<!-- font-size: 1rem; -->
<!-- color: white; -->
<!-- opacity: 0; -->
<!-- animation: snowfall linear infinite; -->
<!-- will-change: transform; -->
<!-- } -->
<script>
// NOTE: This should only be sent when we mount the script after confirming the id exists in sessionStorage
function beforeSendHandler(type, payload) {
@@ -34,6 +65,27 @@
return payload;
}
</script>
<!-- <div id="winterContainer" class="winterContainer" aria-hidden="true"></div> -->
<!-- <script> -->
<!-- // NOTE(mrtz): Add some snowflakes -->
<!-- const container = document.getElementById('winterContainer'); -->
<!-- const snowflakeCount = 150; -->
<!-- const snowflakeSymbols = ['❄', '❅', '❆', '❇', '❈', '❉', '❊', '❋'] -->
<!-- for (let i = 0; i < snowflakeCount; i++) { -->
<!-- const snowflake = document.createElement('div'); -->
<!-- const randomSymbol = snowflakeSymbols[Math.floor(Math.random() * snowflakeSymbols.length)]; -->
<!-- snowflake.className = 'winterSnowflake'; -->
<!-- snowflake.style.left = `${Math.random() * 100}%`; -->
<!-- snowflake.style.animationDuration = `${10 + Math.random() * 10}s`; -->
<!-- snowflake.style.animationDelay = `${Math.random() * 10}s`; -->
<!-- snowflake.style.setProperty('--swirl', `${Math.random() * 20 - 10}vw`); -->
<!-- snowflake.style.setProperty('--rot', `${Math.random() * 720 + 360}deg`); -->
<!-- snowflake.textContent = randomSymbol; -->
<!-- container.appendChild(snowflake); -->
<!-- } -->
<!-- </script> -->
</head>
<body>

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"FSharp.Core": {
"type": "Direct",
"requested": "[9.0.303, )",

View File

@@ -569,6 +569,62 @@ module Handlers =
getSimInfo = getPlumeSimInfo ctx
}
let private runXtract
(ctx: HttpContext)
(xtractJob: XtractPayload)
(partition: string option)
=
task {
let user = getUserName ctx
let group = getGroup ctx
let groups = if String.IsNullOrEmpty group then None else Some [| group |]
let id = ActorId user
try
let proxy = ActorProxy.Create<IXtractActor> (id, "XtractActor")
Log.Information ("runXtract: user {username}, archive={@archive}, name={@name}", user, xtractJob.archiveId, xtractJob.name)
match! proxy.SubmitXtract(xtractJob, groups, partition) with
| Ok info ->
Log.Debug $"job {info}"
return Some info
| Error e ->
Log.Warning $"job submit failed: {e}"
match SignalRHub.getHub ctx with
| Some h ->
do! h.Clients.User(user).Send (Hub.Response.Note (Note.failure $"Job submission failed: {e}"))
return None
| None ->
Log.Error "Could not get signalr hub context"
return None
with exn ->
Log.Error $"runXtract: {exn.Message}"
Log.Verbose $"runXtract: %A{exn}"
return None
}
|> Async.AwaitTask
let private cancelXtractJob (ctx: HttpContext) (jobId: int) =
task {
let user = getUserName ctx
let id = ActorId user
try
let proxy = ActorProxy.Create<IJobActor> (id, "XtractActor")
let! result = proxy.Cancel jobId
return result
with exn ->
Log.Error $"[Xtract] cancelJob: {exn.Message}"
return Error exn.Message
}
|> Async.AwaitTask
let xtractApi (ctx: HttpContext) : Api.Xtract = {
startXtract = runXtract ctx
cancelJob = cancelXtractJob ctx
}
module Endpoints =
let authEndpoints: HttpHandler =
Remoting.createApi ()
@@ -604,4 +660,10 @@ module Endpoints =
Remoting.createApi ()
|> Remoting.fromContext Handlers.plumeApi
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|> Remoting.buildHttpHandler
let xtractEndpoints: HttpHandler =
Remoting.createApi ()
|> Remoting.fromContext Handlers.xtractApi
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|> Remoting.buildHttpHandler

View File

@@ -4,6 +4,8 @@ open System
open System.Data
open System.Linq
open System.Security.Claims
open System.Threading.Tasks
open Archmaester.Actors
open Archmaester.Dto
open Dapper.FSharp.PostgreSQL
@@ -145,46 +147,47 @@ module Handlers =
}
|> Async.AwaitTask
let private setNewArchivePermissions (p: Permission) =
let id = ActorId p.uid
let t = DateTime.UtcNow
let term: Term = { start_time = t; end_time = t }
let ticket: Ticket = {
task = JobType.Any
quota = 100.0
start_time = t
end_time = t
}
let private setNewArchivePermissions (p: Permission) : Async<Result<bool, exn>> =
task {
let proxy = ActorProxy.Create<IArchiveAccessActor> (id, nameof ArchiveAccessActor)
let! (o: bool seq) = p.owners |> Array.map (fun x -> proxy.AddOwner (p.aid, x)) |> sequence
let! uv = p.users |> Array.map (fun x -> proxy.AllowUserView (p.aid, x, term)) |> sequence
let! ux =
p.users
|> Array.map (fun x -> proxy.AllowUserExec (p.aid, x, ticket))
|> sequence
let! gv =
p.groups
|> Array.map (fun x -> proxy.AllowGroupView (p.aid, x, term))
|> sequence
try
let id = ActorId p.uid
let t = DateTime.UtcNow
let term: Term = { start_time = t; end_time = t }
let ticket: Ticket = {
task = JobType.Any
quota = 100.0
start_time = t
end_time = t
}
let proxy = ActorProxy.Create<IArchiveAccessActor> (id, nameof ArchiveAccessActor)
let! (o: bool seq) = p.owners |> Array.map (fun x -> proxy.AddOwner (p.aid, x)) |> sequence
let! uv = p.users |> Array.map (fun x -> proxy.AllowUserView (p.aid, x, term)) |> sequence
let! ux =
p.users
|> Array.map (fun x -> proxy.AllowUserExec (p.aid, x, ticket))
|> sequence
let! gv =
p.groups
|> Array.map (fun x -> proxy.AllowGroupView (p.aid, x, term))
|> sequence
if p.ref.IsSome then
let proxy =
ActorProxy.Create<IArchiveAccessActor> (ActorId p.uid, nameof ArchiveAccessActor)
if p.ref.IsSome then
let proxy =
ActorProxy.Create<IArchiveAccessActor> (ActorId p.uid, nameof ArchiveAccessActor)
let! _ = proxy.AddParent (p.aid, p.ref.Value) |> Async.AwaitTask
()
let! _ = proxy.AddParent (p.aid, p.ref.Value) |> Async.AwaitTask
()
let all = Seq.concat [ o; uv; ux; gv ]
let res = all |> Seq.reduce (&&)
let all = Seq.concat [ o; uv; ux; gv ]
let res = all |> Seq.reduce (&&)
if not res then
Log.Warning $"Archmaester.setArchivePermissions returned false: %A{all}"
if not res then
Log.Warning $"Archmaester.setArchivePermissions returned false: %A{all}"
return res
return Ok res
with ex ->
return Error ex
}
|> Async.AwaitTask
@@ -219,10 +222,12 @@ module Handlers =
Log.Information $"Adding archive: {item.props.name}"
async {
let db = Archives.Archivist (Db.getDataSource ())
let archivist = Archives.Archivist (Db.getDataSource ())
use db = archivist.startConnection ()
let tr = archivist.startTransaction db
try
let saveRes = db.tryAddArchive item
let saveRes = archivist.tryAddArchive(db, item)
Log.Debug $"saveRes %A{saveRes}"
match saveRes with
@@ -241,7 +246,7 @@ module Handlers =
let aid = archive.ArchiveId
let! _ =
let! res =
setNewArchivePermissions {
uid = uid
gid = gid
@@ -252,9 +257,22 @@ module Handlers =
ref = item.props.reference
}
ctx.SetStatusCode 201
return Ok ()
match res with
| Ok ok ->
if ok then
tr.Commit ()
ctx.SetStatusCode 201
return Ok ()
else
tr.Rollback ()
Log.Error("addArchive: error: one of the permissions failed")
return Error "Error adding archive"
| Error ex ->
tr.Rollback ()
Log.Error(ex, "addArchive: error")
return Error "Error adding archive"
with exn ->
tr.Rollback ()
Log.Error $"addArchive: error: {exn}"
ctx.SetStatusCode 500
return Error $"Could not add Archive: {exn.Message}"
@@ -413,6 +431,21 @@ module Handlers =
return Error $"Could not retrieve archive {aid}: {err}"
}
let getBasePath (aid: ArchiveId) =
Log.Information $"Getting archive basePath: {aid}"
async {
let db = Archives.Archivist (Db.getDataSource ())
match db.getBasePath aid with
| Ok path ->
Log.Debug $"getBasePath: {path}"
return Ok path
| Error err ->
Log.Error $"getBasePath: archive with id {aid} not found"
return Error $"Could not retrieve archive {aid}: {err}"
}
let getFiles (aid: ArchiveId) =
Log.Information $"Getting archive files: {aid}"
@@ -436,10 +469,10 @@ module Handlers =
match db.getAllArchiveFiles aid with
| Ok files ->
Log.Debug $"getFiles: {files.basePath} {files.series.Length}"
Log.Debug $"getAllFiles: {files.basePath} {files.series.Length}"
return Ok files
| Error err ->
Log.Error $"getFiles: archive with id {aid} not found"
Log.Error $"getAllFiles: archive with id {aid} not found"
return Error $"Could not retrieve archive {aid}: {err}"
}

View File

@@ -6,6 +6,7 @@ open Dapr.Actors.Runtime
open Oceanbox.ServerPack
open Archmaester.Actors
open System.Threading.Tasks
type ArchivistActor(host: ActorHost, observatory: Observer.ObserverFactory) =
inherit Actor(host)
@@ -53,7 +54,14 @@ type ArchivistActor(host: ActorHost, observatory: Observer.ObserverFactory) =
Api.Handlers.getArchivePropsById aid |> Result.map _.json |> Task.FromResult
member this.GetBasePath(aid) =
let db = Oceanbox.DataAgent.Archives.Archivist (Db.getDataSource ())
db.getBasePath aid |> Task.FromResult
Api.Handlers.getBasePath aid |> Async.StartAsTask
member this.GetProjection(aid) =
Api.Handlers.getArchivePropsById aid
|> Result.map _.projection
|> Task.FromResult
member this.GetArchiveFiles(aid) =
Api.Handlers.getFiles aid |> Async.StartAsTask
override this.OnActivateAsync() = task { return () }

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>6.20.0</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Utils.fs" />

View File

@@ -66,6 +66,7 @@ let handleSlurmEvents (next: HttpFunc) (ctx: HttpContext) =
let proxy =
match SlurmJobType.FromString job.jobType with
| DriftersJob -> ActorProxy.Create<IJobActor>(id, "DriftersActor")
| XtractJob -> ActorProxy.Create<IJobActor>(id, "XtractActor")
| PlumeJob -> ActorProxy.Create<IJobActor>(id, "PlumeActor")
| UnknownJob -> failwith "Unknown job"
do! proxy.HandleJobEvent(job)

View File

@@ -83,12 +83,13 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
let grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
let dep = None
let part = job.partition |> Option.defaultValue "long"
let cpt = 32
task {
try
let env: DriftersEnv = {
JOB_INPUT = Encode.Auto.toString (4, job.input) |> base64e
JOB_TYPE = job.model.ToString ()
JOB_TYPE = job.model.ToString()
JOB_ID = job.aid
REF_ID = job.input.simulation.archiveId
}
@@ -101,7 +102,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
Sites = job.input.release.groups |> Seq.sumBy _.sites.Length
|}
let! r = slurm.Submit (job.name, this.myId, driftersScript, env, grp, part, dep, Some comment)
let! r = slurm.Submit (job.name, this.myId, driftersScript, env, grp, part, cpt, dep, Some comment)
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
match Decode.Auto.fromString<SlurmSubmissionResponse> s with
@@ -163,6 +164,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
let grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
let dep = job.dependency |> Option.map (fun jobId -> $"afterok:{jobId}")
let part = job.partition |> Option.defaultValue "long"
let cpt = 32
task {
try
@@ -179,7 +181,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
SimType = job.model.ToString()
|}
let! r = slurm.Submit (job.name, this.myId, postdriftScript, env, grp, part, dep, Some comment)
let! r = slurm.Submit (job.name, this.myId, postdriftScript, env, grp, part, cpt, dep, Some comment)
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
match Decode.Auto.fromString<SlurmSubmissionResponse> s with

View File

@@ -2,14 +2,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>2.6.6</Version>
</PropertyGroup>
<ItemGroup>
<Compile Include="Slurm.fs" />
<Compile Include="JobActor.fs" />
<Compile Include="DriftersActor.fs" />
<Compile Include="PlumeActor.fs" />
<Compile Include="XtractActor.fs" />
<Compile Include="DriftersActor.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapr.Actors" />
@@ -20,6 +21,7 @@
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="ProjNet.FSharp" />
<PackageReference Include="Serilog" />
<PackageReference Include="FsToolkit.ErrorHandling" />
<PackageReference Include="Thoth.Json.Net" />
<PackageReference Include="FSharp.Core" />
</ItemGroup>

View File

@@ -2,6 +2,8 @@ module Hipster.JobActor
open System
open System.Collections.Generic
open System.IO
open System.IO.Compression
open System.Text.Json
open System.Threading.Tasks
open Dapr.Client
@@ -284,31 +286,35 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
}
member this.getActiveJobs aid =
Log.Debug $"get active jobs for: {this.myId}"
task {
Log.Debug $"get active jobs for: {this.myId}"
let active = [|
for v in this.jobs do
Log.Debug $"job: {aid} -> %A{v.Value}"
let active = [|
for v in this.jobs do
Log.Debug $"job: {aid} -> %A{v.Value}"
if v.Value.refId = aid then
match v.Value.status with
| JobStatus.New
| JobStatus.Waiting
| JobStatus.Failed
| JobStatus.Running -> v.Value
| _ -> ()
else
()
|]
if v.Value.refId = aid then
match v.Value.status with
| JobStatus.New
| JobStatus.Waiting
| JobStatus.Failed
| JobStatus.Running -> v.Value
| _ -> ()
else
()
|]
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
task { return active }
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
return active
}
member this.getFenceRadius() =
Log.Information "fence?"
let r = settings.file.fenceRadius
Log.Information $"fence: {r}"
Task.FromResult r
task {
Log.Information "fence?"
let r = settings.file.fenceRadius
Log.Information $"fence: {r}"
return r
}
member this.checkFence aid (pts: (float * float) list) =
task { return this.validatePoints aid pts }
@@ -321,7 +327,7 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
let! msgIds = this.StateManager.GetOrAddStateAsync (this.msgKey, this.msgIds)
Log.Debug $"JobActor activating: {this.myId}, jobs={jobs.Count} msgs={msgIds.Count}"
let doRemove x =
let doRemove (x: JobInfo) =
x.archiveId = Guid.Empty
|| x.refId = Guid.Empty && x.simDays = 0.0
|| (DateTime.UtcNow - x.submitTime).Seconds > stateTtl

View File

@@ -38,7 +38,6 @@ type Arguments =
let private handleEvents (next: HttpFunc) (ctx: HttpContext) =
Log.Debug "handleEvent()"
task {
let! job = ctx.BindJsonAsync<SlurmJobStatusMsg> ()
Log.Information $"received event: {job}"
@@ -49,6 +48,7 @@ let private handleEvents (next: HttpFunc) (ctx: HttpContext) =
match SlurmJobType.FromString job.jobType with
| DriftersJob -> ActorProxy.Create<IJobActor> (id, "DriftersActor")
| PlumeJob -> ActorProxy.Create<IJobActor> (id, "PlumeActor")
| XtractJob -> ActorProxy.Create<IJobActor> (id, "XtractActor")
| UnknownJob -> failwith "Unknown job"
do! proxy.HandleJobEvent (job)
@@ -94,6 +94,7 @@ let configureActors (o: Runtime.ActorRuntimeOptions) =
o.JsonSerializerOptions <- jopt
o.Actors.RegisterActor<DriftersActor> ()
o.Actors.RegisterActor<PlumeActor.PlumeActor> ()
o.Actors.RegisterActor<XtractActor> ()
let jsonOptions =
let conv = JsonFSharpConverter (JsonFSharpOptions.ThothLike ())

View File

@@ -149,7 +149,7 @@ type SlurmClient(client: HttpClient, settings: ISlurmClientSettings) =
Log.Information $"base: {client.BaseAddress}"
client.GetStringAsync "diag"
member this.Submit(jobName, agentId, exec: string, env: IJobEnv, group, partition, dependency, comment) =
member this.Submit(jobName, agentId, exec: string, env: IJobEnv, group, partition, cpu_per_task, dependency, comment) =
let jobProps = {
SlurmJobProps.empty with
name = jobName
@@ -162,6 +162,7 @@ type SlurmClient(client: HttpClient, settings: ISlurmClientSettings) =
GROUP_ID = group
env = env
}
cpus_per_task = cpu_per_task
partition = partition
account = Some group[1..] // stripping leading '/' in group name
comment = comment

View File

@@ -0,0 +1,239 @@
module Hipster.XtractActor
open System
open System.IO
open System.Threading.Tasks
open Dapr.Client
open Dapr.Actors
open Dapr.Actors.Client
open Dapr.Actors.Runtime
open FsToolkit.ErrorHandling
open Serilog
open Thoth.Json.Net
open Archmaester.Actors
open Hipster.Actors
open Hipster.Job
open Hipster.Slurm
open Oceanbox.ServerPack
type Amqp = { auth: string; host: string }
type Slurm = {
baseUrl: string
slurmApi: string
dbdApi: string
user: string
password: string
}
type AppEnv =
| Production
| PreProd
| Staging
| Review
static member FromString(s: string) =
match s.ToLower () with
| "prod" -> Production
| "preprod" -> PreProd
| "staging" -> Staging
| _ -> Review
type XtractEnv = {
JOB_INPUT: string
JOB_TYPE: string
JOB_ID: Guid
REF_ID: Guid
} with
interface IJobEnv with
member this.toJson() = [
"JOB_INPUT", Encode.string this.JOB_INPUT
"JOB_TYPE", Encode.string this.JOB_TYPE
"JOB_ID", Encode.guid this.JOB_ID
"REF_ID", Encode.guid this.REF_ID
]
static member empty = {
JOB_INPUT = ""
JOB_TYPE = ""
JOB_ID = Guid.Empty
REF_ID = Guid.Empty
}
type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings, _observatory: Observer.ObserverFactory)
=
inherit JobActor.JobActor(host, slurm, settings)
let xtractScript =
let env = settings.appEnv |> AppEnv.FromString
// TODO(mrtz): Add additional environments once excavator is migrated to atlas
match env with
| Production -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
| PreProd -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
| Staging -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
| Review -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
interface IXtractActor with
member this.SubmitXtract
(job: XtractPayload, groups: string array option, partition: string option)
: Task<Result<JobInfo, string>> =
Log.Debug $"[XtractActor.SubmitXtract]: {this.myId}, job: {job.name}"
let submitJob () =
let g = defaultArg groups [||]
let grp = if g.Length > 0 then g[0] else ""
let dep = None
let part = partition |> Option.defaultValue "short"
let cpt = 4
taskResult {
let! archiveName =
this.archivist.GetArchiveName job.archiveId
|> TaskResult.mapError (fun e ->
Log.Error $"[XtractActor.SubmitXtract] Failed to get archive name: %s{e}"
$"Failed to get archive name: {e}"
)
let! projection =
this.archivist.GetProjection job.archiveId
|> TaskResult.mapError (fun e ->
Log.Error $"[XtractActor.SubmitXtract] Failed to get projection: %s{e}"
$"Failed to get archive projection: {e}"
)
let! basePath =
this.archivist.GetBasePath job.archiveId
|> TaskResult.mapError (fun e ->
Log.Error $"[XtractActor.SubmitXtract] Failed to get basePath: %s{e}"
$"Failed to get archive basePath: {e}"
)
let! archiveFiles =
this.archivist.GetArchiveFiles job.archiveId
|> TaskResult.mapError (fun e ->
Log.Error $"[XtractActor.SubmitXtract] Failed to get files: %s{e}"
$"Failed to get archive files: {e}"
)
Log.Debug
$"[XtractActor.SubmitXtract] Got archive name: '{archiveName}', basePath: '{basePath}', projection: '{projection}'"
let uniqueOutputDirs =
archiveFiles.series
|> Array.map (fun f ->
let dir = System.IO.Path.GetDirectoryName (f.name)
System.IO.Path.Combine (basePath, dir)
)
|> Array.distinct
let sampleDirs =
uniqueOutputDirs
|> Array.take (min 3 uniqueOutputDirs.Length)
|> String.concat ", "
let enrichedJob = {
job with
basePath = basePath
caseName = archiveName
projection = projection
files = uniqueOutputDirs
}
let jobJson =
Encode.Auto.toString (4, enrichedJob, caseStrategy = CaseStrategy.CamelCase)
let env: XtractEnv = {
JOB_INPUT = jobJson |> base64e
JOB_TYPE = "xtract"
JOB_ID = job.id
REF_ID = job.archiveId
}
let comment =
Encode.Auto.toString {|
User = this.myId
JobType = "Xtract"
Duration = (job.stop - job.start).TotalHours
Positions = 1
|}
let! r = slurm.Submit (job.name, this.myId, xtractScript, env, grp, part, cpt, dep, Some comment)
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
let! slurmResponse =
Decode.Auto.fromString<SlurmSubmissionResponse> s
|> Result.mapError (fun e ->
Log.Error $"[XtractActor.SubmitXtract] Failed to decode slurm response: %s{e}"
e
)
let jobInfo = {
owner = this.myId
groups = g
jobId = slurmResponse.job_id
archiveId = job.id
refId = job.archiveId
status = JobStatus.New
name = job.name
startTime = job.start
simDays = (job.stop - job.start).TotalDays
submitTime = DateTime.Now
submitQueue = part
}
Log.Debug
$"[XtractActor.SubmitXtract]: jobKey: {this.jobsKey}, jobInfo: {jobInfo}, jobs: %A{this.jobs}"
this.jobs.Add (slurmResponse.job_id, jobInfo)
do! this.StateManager.SetStateAsync (this.jobsKey, this.jobs)
return jobInfo
}
|> TaskResult.mapError (fun e ->
Log.Error $"[XtractActor.SubmitXtract] Submit failed: %s{e}"
e
)
Log.Debug $"jobs: {this.jobs.Count}"
Log.Debug $"submit: {this.myId}"
// let pts = job.positions |> Array.map (fun p -> (p.Long, p.Lat)) |> Array.toList
let pts = [ job.positions |> (fun p -> (p.Long, p.Lat)) ]
task {
let proxy = ActorProxy.Create<IArchiveAccessActor> (this.Id, "ArchiveAccessActor")
let! allowed = proxy.CanRun (job.archiveId, JobType.Any)
if allowed then
Log.Debug $"[XtractActor.SubmitXtract] user validated: {this.myId}"
if this.validatePoints job.archiveId pts then
Log.Debug $"XtractActor.SubmitXtract extraction points validated: {this.myId}"
return! submitJob ()
else
let msg = "One or more extraction points outside boundary."
Log.Error msg
return Error msg
else
let msg = "You don't have credentials to submit the job."
Log.Error msg
return Error msg
}
interface IJobActor with
member this.Cancel(jobId) = this.cancel jobId
member this.HandleJobEvent(job) = this.handleJobEvent job
member this.Remove(jobid) = this.remove jobid
member this.RemoveById(aid: Guid) = this.removeById aid
member this.Clear() = this.clear ()
member this.GetJobState(jobid) = this.getJobState jobid
member this.GetActiveJobs aid = this.getActiveJobs aid
member this.GetFenceRadius() = this.getFenceRadius ()
member this.CheckFence(aid, pts) = this.checkFence aid pts
interface IRemindable with
member this.ReceiveReminderAsync(name: string, state: byte[], due: TimeSpan, term: TimeSpan) =
Log.Debug $"XtractActor received reminder: {name} {state} {due} {term}"
Task.CompletedTask
override this.OnActivateAsync() = base.OnActivateAsync ()
override this.OnDeactivateAsync() = base.SaveStateAsync ()

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>1.9.8</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,33 +1,35 @@
module Main
open System
open System.Text.Json
open System.Text.Json.Serialization
open System.Net.Http
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Cors.Infrastructure
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.SignalR
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.FileProviders
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open Argu
open Dapr.Actors
open Dapr.Actors.Client
open Dapr.Client
open FSharp.Data
open FsToolkit.ErrorHandling
open Fable.SignalR
open Giraffe
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Cors.Infrastructure
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.SignalR
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Logging
open Microsoft.Extensions.FileProviders
open Microsoft.Extensions.Hosting
open System.Net.Http
open Oceanbox.DataAgent
open Polly
open Prometheus
open Saturn
open Saturn.Dapr
open Saturn.OpenTelemetry
open Saturn.Observer
open Saturn.OpenTelemetry
open Sentry
open Sentry.AspNetCore
open Sentry.Extensibility
@@ -37,6 +39,7 @@ open Serilog.Sinks.OpenTelemetry
open Atlantis
open Atlantis.Shared
open Oceanbox.DataAgent
open Oceanbox.ServerPack.MultiAuth
open Saturn.OpenFga
open Settings
@@ -82,7 +85,8 @@ let configureSerilog () =
let configureLogging (builder: ILoggingBuilder) =
builder
.ClearProviders()
.AddSerilog() |> ignore
.AddSerilog()
|> ignore
let corsPolicy (policy: CorsPolicyBuilder) =
policy
@@ -91,7 +95,7 @@ let corsPolicy (policy: CorsPolicyBuilder) =
.AllowAnyMethod()
.WithOrigins(appsettings.file.allowedOrigins)
.SetIsOriginAllowedToAllowWildcardSubdomains()
|> ignore
|> ignore
type UserIdProvider() =
interface IUserIdProvider with
@@ -194,6 +198,7 @@ let configureActors (options: Runtime.ActorRuntimeOptions) =
options.Actors.RegisterActor<Archmaester.ArchivistActor.ArchivistActor>()
options.Actors.RegisterActor<Hipster.DriftersActor.DriftersActor>()
options.Actors.RegisterActor<Hipster.PlumeActor.PlumeActor>()
options.Actors.RegisterActor<Hipster.XtractActor.XtractActor>()
printfn "Configured actors."
// Options for sentry
@@ -253,12 +258,12 @@ let stopImpersonating (next: HttpFunc) (ctx: HttpContext) =
let getBarentsWatchToken (next: HttpFunc) (ctx: HttpContext) =
task {
let! tokenRes =
tryGetEnv "BARENTSWATCH_CLIENT_ID"
|> Option.bind (fun id ->
tryGetEnv "BARENTSWATCH_SECRET"
|> Option.map (BarentsWatch.getToken appsettings.redis id))
|> Option.defaultValue (Error "Secret or client id missing" |> async.Return)
|> Async.StartAsTask
taskResult {
let! id = tryGetEnv "BARENTSWATCH_CLIENT_ID" |> Result.requireSome "Missing barentswatch client id"
let! secret = tryGetEnv "BARENTSWATCH_SECRET" |> Result.requireSome "Missing barentswatch secret"
let! token = BarentsWatch.getToken appsettings.redis id secret
return token
}
match tokenRes with
| Error err ->
@@ -317,6 +322,7 @@ let webApp =
routeStartsWith "/api/v1/-/" >=> requireUser >=> choose [
Api.Endpoints.driftersEndpoints
Api.Endpoints.plumeEndpoints
Api.Endpoints.xtractEndpoints
Api.Endpoints.inboxEndpoints
Archmaester.Api.Endpoints.inventoryEndpoints
]

View File

@@ -2,7 +2,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ParallelCompilation>true</ParallelCompilation>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<Version>2.102.0</Version>
<RootNamespace>Server</RootNamespace>

View File

@@ -81,7 +81,7 @@ let configureEnv () : Async<unit> =
| Some x when x = "1" || x = "true" ->
Log.Information "[Settings] > Waiting for Dapr..."
Threading.Thread.Sleep 2000
do! Async.Sleep (TimeSpan.FromMilliseconds 2000)
do! Async.Sleep (TimeSpan.FromMilliseconds 2000.0)
do! setupAzureEnv ()
Log.Information $"[Settings] > Azure Keyvault credentials in {appsettings.keyVault}"
do! setupDbEnv ()

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Argu": {
"type": "Direct",
"requested": "[6.2.5, )",
@@ -56,9 +56,7 @@
"dependencies": {
"Azure.Core": "1.44.1",
"Microsoft.Identity.Client": "4.67.2",
"Microsoft.Identity.Client.Extensions.Msal": "4.67.2",
"System.Memory": "4.5.5",
"System.Threading.Tasks.Extensions": "4.5.4"
"Microsoft.Identity.Client.Extensions.Msal": "4.67.2"
}
},
"Azure.Security.KeyVault.Secrets": {
@@ -67,10 +65,7 @@
"resolved": "4.7.0",
"contentHash": "uOPCojkm41V4dKTORyGzl3/f/lriKpxSQ43fWDn4StRJBVmbF1F/DNWJhwm207kCnqgE/W9+tskJSimIKHCZkw==",
"dependencies": {
"Azure.Core": "1.44.1",
"System.Memory": "4.5.5",
"System.Text.Json": "6.0.10",
"System.Threading.Tasks.Extensions": "4.5.4"
"Azure.Core": "1.44.1"
}
},
"Dapr.Actors": {
@@ -251,8 +246,7 @@
"resolved": "1.3.13",
"contentHash": "znp8odpdkVGKVX0AvbhiXdmeMi0KJ+A4AyAQWSkfAEAe4Z4clRE+rVhrLnAGrFD1VEIUX2lsQ4o84ywpWZUSGw==",
"dependencies": {
"FSharp.Core": "4.7.0",
"System.Text.Json": "6.0.0"
"FSharp.Core": "4.7.0"
}
},
"FSharpPlus": {
@@ -273,8 +267,7 @@
"FSharp.Core": "6.0.0",
"FSharp.SystemTextJson": "1.3.13",
"Giraffe.ViewEngine": "1.4.0",
"Microsoft.IO.RecyclableMemoryStream": "3.0.1",
"System.Text.Json": "8.0.5"
"Microsoft.IO.RecyclableMemoryStream": "3.0.1"
}
},
"IdentityModel.AspNetCore": {
@@ -489,12 +482,7 @@
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
"System.ClientModel": "1.1.0",
"System.Diagnostics.DiagnosticSource": "6.0.1",
"System.Memory.Data": "6.0.0",
"System.Numerics.Vectors": "4.5.0",
"System.Text.Encodings.Web": "6.0.0",
"System.Text.Json": "6.0.10",
"System.Threading.Tasks.Extensions": "4.5.4"
"System.Memory.Data": "6.0.0"
}
},
"Azure.Security.KeyVault.Keys": {
@@ -502,10 +490,7 @@
"resolved": "4.6.0",
"contentHash": "1KbCIkXmLaj+kDDNm1Va5rNlzgcJ/fVtnsoVmzZPKa38jz6DXhPyojdvGaOX8AdupGJceg0X1vrsGvZKN79Qzw==",
"dependencies": {
"Azure.Core": "1.37.0",
"System.Memory": "4.5.4",
"System.Text.Json": "4.7.2",
"System.Threading.Tasks.Extensions": "4.5.4"
"Azure.Core": "1.37.0"
}
},
"Azure.Storage.Blobs": {
@@ -513,8 +498,7 @@
"resolved": "12.23.0",
"contentHash": "wokJ5KX/iViQQ32xyCu69+Ter0aR4B9QQ+oR9NCpc/WPIanxnDErrmFfdmE7K8ZdccjHkvE/wEnqJxaF1+5wFg==",
"dependencies": {
"Azure.Storage.Common": "12.22.0",
"System.Text.Json": "6.0.10"
"Azure.Storage.Common": "12.22.0"
}
},
"Azure.Storage.Common": {
@@ -586,8 +570,7 @@
"Fable.Remoting.MsgPack": "1.24.0",
"Fable.SignalR.Shared": "2.1.0",
"Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson": "9.0.0",
"Microsoft.AspNetCore.SignalR.StackExchangeRedis": "9.0.0",
"System.Text.Encodings.Web": "9.0.0"
"Microsoft.AspNetCore.SignalR.StackExchangeRedis": "9.0.0"
}
},
"Fable.SignalR.Shared": {
@@ -683,8 +666,7 @@
"resolved": "5.3.2",
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
"dependencies": {
"FSharp.Core": "4.3.2",
"System.Reflection.Emit.Lightweight": "4.3.0"
"FSharp.Core": "4.3.2"
}
},
"Giraffe.ViewEngine": {
@@ -836,8 +818,7 @@
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
"Microsoft.AspNetCore.Http.Features": "2.2.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
@@ -881,8 +862,7 @@
"resolved": "2.2.0",
"contentHash": "9ErxAAKaDzxXASB/b5uLEkLgUWv1QbeVxyJYEHQwMaxXOeFFVkQxiq8RyfVcifLU7NR0QY0p3acqx4ZpYfhHDg==",
"dependencies": {
"Microsoft.Net.Http.Headers": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
"Microsoft.Net.Http.Headers": "2.2.0"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
@@ -1089,8 +1069,7 @@
"resolved": "4.67.2",
"contentHash": "37t0TfekfG6XM8kue/xNaA66Qjtti5Qe1xA41CK+bEd8VD76/oXJc+meFJHGzygIC485dCpKoamG/pDfb9Qd7Q==",
"dependencies": {
"Microsoft.IdentityModel.Abstractions": "6.35.0",
"System.Diagnostics.DiagnosticSource": "6.0.1"
"Microsoft.IdentityModel.Abstractions": "6.35.0"
}
},
"Microsoft.Identity.Client.Extensions.Msal": {
@@ -1158,8 +1137,7 @@
"resolved": "2.2.0",
"contentHash": "iZNkjYqlo8sIOI0bQfpsSoMTmB/kyvmV2h225ihyZT33aTp48ZpF6qYnXxzSXmHt8DpBAwBTX+1s1UFLbYfZKg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0",
"System.Buffers": "4.5.0"
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.NET.StringTools": {
@@ -1188,10 +1166,7 @@
"OpenTelemetry.Api": {
"type": "Transitive",
"resolved": "1.10.0",
"contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w==",
"dependencies": {
"System.Diagnostics.DiagnosticSource": "9.0.0"
}
"contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w=="
},
"OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive",
@@ -1272,17 +1247,13 @@
"dependencies": {
"Microsoft.Extensions.Options": "9.0.0",
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.10.0, 2.0.0)",
"StackExchange.Redis": "[2.6.122, 3.0.0)",
"System.Reflection.Emit.Lightweight": "4.7.0"
"StackExchange.Redis": "[2.6.122, 3.0.0)"
}
},
"Pipelines.Sockets.Unofficial": {
"type": "Transitive",
"resolved": "2.2.8",
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==",
"dependencies": {
"System.IO.Pipelines": "5.0.1"
}
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
},
"Polly": {
"type": "Transitive",
@@ -1300,11 +1271,7 @@
"ProjNET": {
"type": "Transitive",
"resolved": "2.0.0",
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
"dependencies": {
"System.Memory": "4.5.3",
"System.Numerics.Vectors": "4.5.0"
}
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
},
"prometheus-net": {
"type": "Transitive",
@@ -1404,18 +1371,12 @@
"Pipelines.Sockets.Unofficial": "2.2.8"
}
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "pL2ChpaRRWI/p4LXyy4RgeWlYF2sgfj/pnVMvBqwNFr5cXg7CXNnWZWxrOONLg8VGdFB8oB+EG2Qw4MLgTOe+A=="
},
"System.ClientModel": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==",
"dependencies": {
"System.Memory.Data": "1.0.2",
"System.Text.Json": "6.0.9"
"System.Memory.Data": "1.0.2"
}
},
"System.ComponentModel.Annotations": {
@@ -1431,11 +1392,6 @@
"System.Security.Cryptography.ProtectedData": "4.4.0"
}
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "8hy61dsFYYSDjT9iTAfygGMU3A0EAnG69x5FUXeKsCjMhBmtTBt4UMUEW3ipprFoorOW6Jw/7hDMjXtlrsOvVQ=="
},
"System.IdentityModel.Tokens.Jwt": {
"type": "Transitive",
"resolved": "8.0.1",
@@ -1450,33 +1406,10 @@
"resolved": "6.0.0",
"contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g=="
},
"System.IO.Pipelines": {
"type": "Transitive",
"resolved": "5.0.1",
"contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw=="
},
"System.Memory.Data": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ==",
"dependencies": {
"System.Text.Json": "6.0.0"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
},
"System.Reflection.Emit.Lightweight": {
"type": "Transitive",
"resolved": "4.7.0",
"contentHash": "a4OLB4IITxAXJeV74MDx49Oq2+PsF6Sml54XAFv+2RyWwtDBcabzoxiiJRhdhx+gaohLh4hEGCLQyBozXoQPqA=="
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
@@ -1496,16 +1429,6 @@
"System.Security.Cryptography.Pkcs": "9.0.2"
}
},
"System.Text.Json": {
"type": "Transitive",
"resolved": "8.0.5",
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg=="
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
},
"archmaester": {
"type": "Project",
"dependencies": {
@@ -1545,7 +1468,7 @@
"Hipster.Api": "[1.0.1, )",
"Oceanbox.DataAgent": "[7.3.0, )",
"Oceanbox.DataAgent.Api": "[7.2.1, )",
"Oceanbox.ServerPack": "[1.33.0, )",
"Oceanbox.ServerPack": "[1.40.0, )",
"Petimeter.Api": "[1.0.0, )"
}
},
@@ -1568,6 +1491,7 @@
"FSharp.Data": "[6.4.1, )",
"FSharp.SystemTextJson": "[1.3.13, )",
"FSharpPlus": "[1.7.0, )",
"FsToolkit.ErrorHandling": "[5.0.1, )",
"Newtonsoft.Json": "[13.0.3, )",
"ProjNet.FSharp": "[5.2.0, )",
"Serilog": "[4.2.0, )",
@@ -1682,6 +1606,15 @@
"FSharp.Core": "4.7.1"
}
},
"FsToolkit.ErrorHandling": {
"type": "CentralTransitive",
"requested": "[5.0.1, )",
"resolved": "5.0.1",
"contentHash": "93oG3WSogK05H4gkikAmx5pBf30TQJfO1Jky+o/N/nv+RTP3nfOfjlmCHzuyUjQCRFOQog/xQabcky+WBWceeQ==",
"dependencies": {
"FSharp.Core": "9.0.300"
}
},
"MessagePack": {
"type": "CentralTransitive",
"requested": "[3.1.3, )",
@@ -1721,10 +1654,7 @@
"type": "CentralTransitive",
"requested": "[2.5.0, )",
"resolved": "2.5.0",
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
"dependencies": {
"System.Memory": "4.5.4"
}
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
},
"Newtonsoft.Json": {
"type": "CentralTransitive",
@@ -1789,8 +1719,7 @@
"contentHash": "vlOKvmigJ3Sumoulp1HwCTFXgX4KuERVGIIw4ZqmhgUJnSiApDmY183ddzzHo2FIdIJ8vGwrMGx98v9cLAezFA==",
"dependencies": {
"Microsoft.Extensions.Http": "9.0.9",
"System.ComponentModel.Annotations": "5.0.0",
"System.Diagnostics.DiagnosticSource": "9.0.9"
"System.ComponentModel.Annotations": "5.0.0"
}
},
"ProjNet.FSharp": {

View File

@@ -41,7 +41,7 @@
}
},
"fga": {
"apiUrl": "http://prod-openfga.openfga.svc.cluster.local:8080",
"apiUrl": "http://staging-openfga.staging-openfga.svc.cluster.local:8080",
"apiKey": "",
"storeId": "01JKTZXMP7ANN4GG2P5W8Y56M6",
"modelId": "01JKTZYMCZZBVSBG66W27XMW0A"
@@ -63,6 +63,10 @@
"allowedOrigins": [
"http://*.oceanbox.io",
"https://*.oceanbox.io",
"https://*.oceanbox.io:8080",
"https://*.oceanbox.io:10380",
"https://*.vtn.obx",
"https://*.tox.obx",
],
"appName": "atlantis",
"appEnv": "<x>",

View File

@@ -1,29 +0,0 @@
architecture: standalone
# NOTE(mrtz): Hack for working with legacy registry
global:
security:
allowInsecureImages: true
image:
repository: bitnamilegacy/redis
replica:
replicaCount: 1
auth:
enabled: true
sentinel: true
password: ""
usePasswordFiles: false
existingSecretPasswordKey: ""
existingSecret: <x>-atlantis-redis
master:
resources:
limits:
ephemeral-storage: 1024Mi
memory: 192Mi
requests:
cpu: 150m
ephemeral-storage: 50Mi
memory: 128Mi

View File

@@ -1,103 +1,103 @@
replicaCount: 1
image:
tag: latest
tag: latest
podAnnotations:
dapr.io/enabled: "true"
dapr.io/app-id: "<x>-atlantis"
dapr.io/app-port: "8085"
dapr.io/api-token-secret: "dapr-api-token"
dapr.io/config: "tracing"
dapr.io/app-protocol: "http"
dapr.io/log-as-json: "true"
dapr.io/sidecar-cpu-request: "10m"
dapr.io/sidecar-memory-request: "50Mi"
# dapr.io/sidecar-cpu-limit: "300m"
# dapr.io/sidecar-memory-limit: "1000Mi"
dapr.io/enabled: "true"
dapr.io/app-id: "<x>-atlantis"
dapr.io/app-port: "8085"
dapr.io/api-token-secret: "dapr-api-token"
dapr.io/config: "tracing"
dapr.io/app-protocol: "http"
dapr.io/log-as-json: "true"
dapr.io/sidecar-cpu-request: "10m"
dapr.io/sidecar-memory-request: "50Mi"
# dapr.io/sidecar-cpu-limit: "300m"
# dapr.io/sidecar-memory-limit: "1000Mi"
env:
- name: APP_NAMESPACE
value: <x>-atlantis
- name: APP_VERSION
value: "<x>-tilt"
- name: LOG_LEVEL
value: "verbose"
- name: REDIS_USER
value: default
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: <x>-atlantis-redis
key: redis-password
- name: DB_HOST
value: <x>-atlantis-db-rw
- name: DB_PORT
value: "5432"
- name: DB_USER
valueFrom:
secretKeyRef:
name: <x>-atlantis-db-app
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: <x>-atlantis-db-app
key: password
- name: DAPR_API_TOKEN
valueFrom:
secretKeyRef:
name: dapr-api-token
key: token
- name: ANALYTICS_WEB_ID
value: 6f26c702-2c6d-46ea-8122-ffcedda5f762
- name: APP_NAMESPACE
value: <x>-atlantis
- name: APP_VERSION
value: "<x>-tilt"
- name: LOG_LEVEL
value: "verbose"
- name: REDIS_USER
value: default
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: <x>-atlantis-redis
key: redis-password
- name: DB_HOST
value: <x>-atlantis-db-rw
- name: DB_PORT
value: "5432"
- name: DB_USER
valueFrom:
secretKeyRef:
name: <x>-atlantis-db-app
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: <x>-atlantis-db-app
key: password
- name: DAPR_API_TOKEN
valueFrom:
secretKeyRef:
name: dapr-api-token
key: token
- name: ANALYTICS_WEB_ID
value: 6f26c702-2c6d-46ea-8122-ffcedda5f762
ingress:
enabled: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
hosts:
- host: <x>-atlantis.dev.oceanbox.io
paths:
- path: /
pathType: ImplementationSpecific
internal:
- path: /internal
pathType: ImplementationSpecific
- path: /dapr
pathType: ImplementationSpecific
- path: /actors
pathType: ImplementationSpecific
- path: /job
pathType: ImplementationSpecific
- path: /events
pathType: ImplementationSpecific
- path: /metrics
pathType: ImplementationSpecific
tls:
- hosts:
- <x>-atlantis.dev.oceanbox.io
secretName: <x>-atlantis-tls
enabled: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
hosts:
- host: <x>-atlantis.dev.oceanbox.io
paths:
- path: /
pathType: ImplementationSpecific
internal:
- path: /internal
pathType: ImplementationSpecific
- path: /dapr
pathType: ImplementationSpecific
- path: /actors
pathType: ImplementationSpecific
- path: /job
pathType: ImplementationSpecific
- path: /events
pathType: ImplementationSpecific
- path: /metrics
pathType: ImplementationSpecific
tls:
- hosts:
- <x>-atlantis.dev.oceanbox.io
secretName: <x>-atlantis-tls
storage:
enabled: true
size: 1G
accessMode: ReadWriteOnce
storageClass: ceph-rdb
enabled: true
size: 1G
accessMode: ReadWriteOnce
storageClass: ceph-rdb
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: false
runAsUser: 0
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: false
runAsUser: 0
cluster:
backup:
enabled: false
backup:
enabled: false
redis:
enabled: true

View File

@@ -4,7 +4,7 @@ variables:
include:
- project: oceanbox/gitlab-ci
ref: v4.4
ref: v4.5
file: DotnetDeployment.gitlab-ci.yml
inputs:
project-name: codex

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY dist /app

View File

@@ -2,6 +2,7 @@ name='codex'
cluster='oceanbox'
env=os.getenv('APP_ENV')
username=os.getenv('USER')
namespace=os.getenv('APP_NAMESPACE')
app = '{}-{}'.format(env, name)
@@ -39,7 +40,9 @@ docker_build_with_restart(
ignore = [ 'src/Client' ]
)
k8s_yaml('tilt/k8s.yaml')
local('cat tilt/k8s.yaml | sed "s/<x>/{}/" > tilt/_k8s.yaml'.format(username))
k8s_yaml('tilt/_k8s.yaml')
k8s_resource(app, port_forwards='8085:8085')
# vim:ft=python

View File

@@ -5,7 +5,7 @@
}:
dockerTools.buildLayeredImage {
name = "Codex";
tag = "0.0.0-alpha.1";
tag = "0.0.1";
created = "now";
contents = [
@@ -24,4 +24,4 @@ dockerTools.buildLayeredImage {
cmd = [ "Codex.Server" ];
workingDir = "/app";
};
}
}

View File

@@ -5,6 +5,7 @@ open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Feliz.Router
open FS.FluentUI
module Archive =
let private fetchArchiveAcl (id: System.Guid) : JS.Promise<Result<Archmaester.Dto.ArchiveAcl, string>> =
@@ -29,16 +30,6 @@ module Archive =
return subs
}
let private deleteArchive (id: System.Guid) =
promise {
try
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
return res
with e ->
console.error("Error deleting archive: %o", e)
return Error "Error deleting archive"
}
let private addArchiveGroup (archiveId: System.Guid) (group: string) =
promise {
try
@@ -227,9 +218,6 @@ module Archive =
let View (archiveId: System.Guid) =
let loading, setLoading = React.useState true
let error, setError = React.useState<string option> None
let editing, setEditing = React.useState false
let deleting, setDeleting = React.useState false
let deleted, setDeleted = React.useState false
let selectedGroup, setSelectedGroup = React.useState<string option> None
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
let aclOpt, setAcl = React.useState<Archmaester.Dto.ArchiveAcl option> None
@@ -257,21 +245,6 @@ module Archive =
| None ->
console.warn("ACL has not been downloaded")
let handleDeleteArchive (id: System.Guid) =
deleteArchive id
|> Promise.iter (fun res ->
match res with
| Ok deleted ->
if deleted then
console.info("Archive deleted successfully")
setDeleted true
else
setError (Some "Failed to delete archive")
| Error err ->
console.error("Error deleting archive: %s", err)
setError (Some err)
)
let handleSelectedGroupChange (groupOpt: string option) =
console.debug("Selected group: %s", groupOpt)
setSelectedGroup groupOpt
@@ -319,94 +292,27 @@ module Archive =
| None ->
match archiveOpt with
| Some archive ->
Html.h1 (sprintf "Archive %s" archive.name)
Fui.text.title1 (sprintf "Archive %s" archive.name)
if deleting then
Html.h2 "Deleting archive ..."
else
Html.none
if deleted then
Html.div [
prop.children [
Html.h2 "Archive successfully deleted"
Html.a [
prop.href (Router.format "archives")
prop.text "Return to archives listing"
]
]
Html.div [
prop.classes [ "flex-row"; "gap-8" ]
prop.children [
Archives.EditArchiveDialog archive (fun edited ->
Some
{archive with
name = edited.Name
startTime = edited.StartTime
endTime = edited.StartTime.AddHours(edited.Frames)
frames = edited.Frames
isPublished = edited.Published
isPublic = edited.Public
}
|> setArchive
console.debug ("response: ", edited)
)
Archives.DeleteArchiveDialog archive
]
else
Html.div [
prop.classes [ "flex-row"; "gap-8" ]
prop.children [
if deleting then
Html.button [
prop.onClick (fun ev ->
setDeleting false
handleDeleteArchive archive.archiveId
)
prop.text "Save"
]
Html.button [
prop.onClick (fun ev ->
setDeleting false
)
prop.text "Cancel"
]
elif editing then
Html.button [
prop.onClick (fun ev ->
setEditing false
)
prop.text "Save"
]
Html.button [
prop.onClick (fun ev ->
setEditing false
)
prop.text "Cancel"
]
else
Html.button [
prop.onClick (fun ev ->
setEditing true
)
prop.text "Edit"
]
Html.button [
prop.onClick (fun ev ->
setDeleting true
)
prop.text "Delete"
]
]
]
if editing then
Html.form [
prop.children [
Html.div [
prop.children [
Html.label [
prop.htmlFor "published-checkbox"
prop.text "Published: "
]
Html.input [
prop.id "published-checkbox"
prop.type' "checkbox"
prop.custom ("checked", archive.isPublished)
]
]
]
]
]
else
Html.none
]
Archives.InfoSection archive
@@ -744,4 +650,4 @@ module Archive =
| None ->
Html.h1 "Archive not found"
]
]

View File

@@ -1,6 +1,8 @@
namespace Oceanbox.Codex
open Fable.Core
open Feliz
open FS.FluentUI
type Archives =
[<ReactComponent>]
@@ -13,49 +15,131 @@ type Archives =
Html.section [
prop.classes [ "flex-row"; "flex-wrap"; "gap-16" ]
prop.children [
Html.ul [
prop.children [
Html.li [
prop.text (sprintf "Description: %s" archive.description)
]
Html.li [
prop.text (sprintf "Archive type: %s" (string archive.archiveType))
]
Html.li [
prop.text (sprintf "Projection: %s" archive.projection)
]
Html.li [
prop.text (sprintf "Frequency: %d" archive.freq)
]
Html.li [
prop.text (sprintf "Frames: %d" archive.frames)
]
Html.li [
prop.text (sprintf "Created: %s" (archive.created.ToLongDateString()))
]
Html.li [
prop.text (sprintf "Start time: %s" (archive.startTime.ToLongDateString()))
]
Html.li [
prop.text (sprintf "End time: %s" (archive.endTime.ToLongDateString()))
]
Html.li [
prop.text (sprintf "Length: %d days %d hours" archiveLength.Days archiveLength.Hours)
]
Html.li [
prop.text (sprintf "Owner: %s" archive.owner)
]
Html.li [
prop.text (sprintf "Expires: %s" (archive.expires |> Option.map string |> Option.defaultValue ""))
]
Html.li [
prop.text (sprintf "Publised: %b" archive.isPublished)
]
Html.li [
prop.text (sprintf "Public: %b" archive.isPublic)
]
Html.li [
prop.text (sprintf "Location: %s" "tos")
Fui.table [
table.classes [ "flex-basis-7"; "flex-grow" ]
table.children [
Fui.tableBody [
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.textDescriptionRegular [])
tableCellLayout.children [ Fui.text "Description" ]
]
]
Fui.tableCell [ Fui.text archive.description ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.contactCardGenericRegular [])
tableCellLayout.children [ Fui.text "Archive type" ]
]
]
Fui.tableCell [ Fui.text (string archive.archiveType) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.globeSurfaceRegular [])
tableCellLayout.children [ Fui.text "Projection" ]
]
]
Fui.tableCell [ Fui.text archive.projection ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.timerRegular [])
tableCellLayout.children [ Fui.text "Frequency" ]
]
]
Fui.tableCell [ Fui.text (string archive.freq) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.filmstripRegular [])
tableCellLayout.children [ Fui.text "Frames" ]
]
]
Fui.tableCell [ Fui.text (string archive.frames) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.calendarAddRegular [])
tableCellLayout.children [ Fui.text "Time created" ]
]
]
Fui.tableCell [ Fui.text (archive.created.ToLongDateString()) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.playRegular [])
tableCellLayout.children [ Fui.text "Start time" ]
]
]
Fui.tableCell [ Fui.text (archive.startTime.ToLongDateString()) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.stopRegular [])
tableCellLayout.children [ Fui.text "End time" ]
]
]
Fui.tableCell [ Fui.text (archive.endTime.ToLongDateString()) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.autoFitWidthRegular [])
tableCellLayout.children [ Fui.text "Length" ]
]
]
Fui.tableCell [ Fui.text (sprintf "%d days %d hours" archiveLength.Days archiveLength.Hours) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.eyeRegular [])
tableCellLayout.children [ Fui.text "Published" ]
]
]
Fui.tableCell [ Fui.text (string archive.isPublished) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.peopleEyeRegular [])
tableCellLayout.children [ Fui.text "Public" ]
]
]
Fui.tableCell [ Fui.text (string archive.isPublic) ]
]
]
]
]
]
@@ -70,3 +154,342 @@ type Archives =
]
]
]
[<ReactComponent>]
static member EditArchiveDialog(archive: Archmaester.Dto.ArchiveProps) onEdit =
let initForm : Remoting.EditArchiveRequest =
{
Name = archive.name
StartTime = archive.startTime
Frames = archive.frames
Published = archive.isPublished
Public = archive.isPublic
}
let form, setForm = React.useState initForm
let isOpen, setIsOpen = React.useState false
let errMsg, setErrMsg = React.useState None
let dataSet, setDataSet = React.useState None
React.useEffectOnce (fun _ ->
Remoting.adminApi.getArchiveDataSet archive.archiveId
|> Async.StartAsPromise
|> Promise.iter (fun res ->
match res with
| Ok ds -> setDataSet (Some ds)
| Error msg ->
Browser.Dom.console.error("Error fetching dataset: %s", msg)
)
)
let handleEditArchive () =
let utcTime = System.DateTime(form.StartTime.Year, form.StartTime.Month, form.StartTime.Day, 0, 0, 0, System.DateTimeKind.Utc)
Remoting.adminApi.updateArchive archive.archiveId {form with StartTime = utcTime}
|> Async.StartAsPromise
|> Promise.iter (fun res ->
match res with
| Ok newArchive ->
Browser.Dom.console.info("Added archive %s with id %s", newArchive.Name, string newArchive.Id)
onEdit newArchive
setIsOpen false
| Error msg ->
Browser.Dom.console.error("Error adding archive %s: %s", form.Name, msg)
setErrMsg (Some msg)
)
let framesExceedEnd =
dataSet
|> Option.map (fun ds ->
form.StartTime.AddHours(form.Frames) > ds.EndTime
)
|> Option.defaultValue false
Fui.dialog [
dialog.open' isOpen
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
setIsOpen d.``open``
// NOTE: Reset form on open, so it's not noticeable in the UI
if d.``open`` then
setForm initForm
)
dialog.children [
Fui.dialogTrigger [
dialogTrigger.children (
Fui.button [
button.icon (Fui.icon.editRegular [])
button.text "Edit"
]
)
]
Fui.dialogSurface [
dialogSurface.classes []
dialogSurface.children [
Fui.dialogBody [
dialogBody.classes []
dialogBody.children [
Fui.dialogTitle [
dialogTitle.text (sprintf "Edit archive %s" archive.name)
dialogTitle.action (
Fui.dialogTrigger [
dialogTrigger.action.close
dialogTrigger.children (
Fui.button [
button.appearance.transparent
button.icon (Fui.icon.dismissRegular [])
]
)
]
)
]
Fui.dialogContent [
dialogContent.classes ["flex-column"; "gap-8"]
dialogContent.children [
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Name"
]
)
field.children (
Fui.input [
input.value form.Name
input.onChange (fun (v: ValueProp<string>) ->
if v.value.Length <= 80 then
setForm {form with Name = v.value}
)
]
)
field.hint $"{form.Name.Length}/80"
]
Html.div [
prop.classes ["flex-row"; "gap-8"]
prop.children [
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Start Date"
]
)
field.children (
Fui.datePicker [
datePicker.placeholder "Select a date..."
datePicker.showWeekNumbers true
datePicker.formatDate (fun d -> d.ToShortDateString())
match dataSet with
| None -> ()
| Some ds ->
datePicker.minDate ds.StartTime
datePicker.maxDate ds.EndTime
datePicker.value (Some form.StartTime)
datePicker.onSelectDate (fun d ->
d |> Option.iter (fun d' ->
setForm {form with StartTime = d'}
)
)
]
)
]
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Days"
]
)
field.children (
Fui.spinButton [
spinButton.value (form.Frames / 24)
spinButton.min 1
spinButton.onChange (fun (d: SpinButtonOnChangeData) ->
match d.value with
| Some v ->
setForm {form with Frames = v * 24}
| None ->
if d.displayValue.ToCharArray() |> Array.forall System.Char.IsDigit then
setForm {form with Frames = int d.displayValue * 24}
)
]
)
]
]
]
Fui.text.caption1 [
if framesExceedEnd then
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
let endDate = form.StartTime.AddHours(form.Frames).ToShortDateString()
text.text
($"End Date: {endDate}, {form.Frames} frames"
+
if framesExceedEnd then
" (Exceeds DataSet Bounds!)"
else
"")
]
Fui.checkbox [
checkbox.label "Published"
checkbox.checked' form.Published
checkbox.onCheckedChange (fun c ->
if not c then
setForm {form with Published = c; Public = c}
else
setForm {form with Published = c}
)
]
Fui.checkbox [
checkbox.label "Public"
checkbox.checked' form.Public
checkbox.onCheckedChange (fun c -> setForm {form with Public = c})
]
match errMsg with
| None -> Html.none
| Some msg ->
Fui.text [
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
text.text msg
]
]
]
Fui.dialogActions [
dialogActions.position.end'
dialogActions.children [
Fui.dialogTrigger [
dialogTrigger.action.close
dialogTrigger.children (
Fui.button [
button.icon (Fui.icon.dismissRegular [])
button.text "Cancel"
]
)
]
Fui.button [
button.appearance.primary
button.icon (Fui.icon.saveRegular [])
button.text "Save changes"
button.disabled <| (form = initForm)
button.onClick (fun _ -> handleEditArchive ())
]
]
]
]
]
]
]
]
]
[<ReactComponent>]
static member DeleteArchiveDialog(archive: Archmaester.Dto.ArchiveProps) =
let isOpen, setIsOpen = React.useState false
let userConfirmed, setUserConfirmed = React.useState false
let errMsg, setErrMsg = React.useState None
let deleteArchive (id: System.Guid) =
promise {
try
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
return res
with e ->
Browser.Dom.console.error("Error deleting archive: %o", e)
return Error "Error deleting archive"
}
let handleDeleteArchive () =
deleteArchive archive.archiveId
|> Promise.iter (fun res ->
match res with
| Ok deleted ->
if deleted then
Browser.Dom.console.info("Archive deleted successfully")
setIsOpen false
Router.Router.navigateBack ()
else
setErrMsg (Some "Failed to delete archive")
| Error err ->
Browser.Dom.console.error("Error deleting archive: %s", err)
setErrMsg (Some err)
)
Fui.dialog [
dialog.open' isOpen
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
setIsOpen d.``open``
if d.``open`` then
setUserConfirmed false
setErrMsg None
)
dialog.children [
Fui.dialogTrigger [
dialogTrigger.children (
Fui.button [
button.icon (Fui.icon.deleteRegular [])
button.text "Delete"
]
)
]
Fui.dialogSurface [
dialogSurface.classes []
dialogSurface.children [
Fui.dialogBody [
dialogBody.classes []
dialogBody.children [
Fui.dialogTitle [
dialogTitle.text (sprintf "Delete archive %s" archive.name)
dialogTitle.action (
Fui.dialogTrigger [
dialogTrigger.action.close
dialogTrigger.children (
Fui.button [
button.appearance.transparent
button.icon (Fui.icon.dismissRegular [])
]
)
]
)
]
Fui.dialogContent [
dialogContent.classes ["flex-column"; "gap-8"]
dialogContent.children [
Fui.text "Are you sure you want to delete the archive? This action cannot be reverted!"
Fui.checkbox [
checkbox.label "Yes, I am sure"
checkbox.checked' userConfirmed
checkbox.onCheckedChange setUserConfirmed
]
match errMsg with
| None -> Html.none
| Some msg ->
Fui.text [
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
text.text msg
]
]
]
Fui.dialogActions [
dialogActions.position.end'
dialogActions.children [
Fui.dialogTrigger [
dialogTrigger.action.close
dialogTrigger.children (
Fui.button [
button.icon (Fui.icon.dismissRegular [])
button.text "Cancel"
]
)
]
Fui.button [
button.appearance.primary
button.icon (Fui.icon.deleteRegular [])
button.text "Delete"
button.disabled (not userConfirmed)
button.onClick (fun _ -> handleDeleteArchive ())
]
]
]
]
]
]
]
]
]

View File

@@ -97,7 +97,8 @@ type Archives =
| Ok archives ->
setArchives archives
setError None
| Error err -> setError (Some "Error fetching archives")
| Error err ->
setError (Some "Error fetching archives")
setLoading false
)
@@ -196,4 +197,4 @@ type Archives =
]
]
]
]
]

View File

@@ -5,6 +5,7 @@ module Utils =
open Fable.Core
open Fable.Core.JsInterop
open Fable.Remoting.Client
open FsToolkit.ErrorHandling
open Oceanbox.Codex
open Oceanbox.Codex.Types
@@ -71,3 +72,52 @@ module Utils =
return Error "Error fetching archive count"
}
let private fetchArchmaesterArchives (group: string) : JS.Promise<Result<Archmaester.Dto.ArchiveProps array, string>> =
promise {
let filter : Archmaester.Dto.ArchiveFilter = {
id = None
searchTerm = None
archiveType = None
owner = None
user = None
groups = Some [| group |]
}
let! result = Remoting.adminApi.getArchives 0 -1 filter |> Async.StartAsPromise
return result
}
let fetchGroupArchiveProps (group: string) : JS.Promise<Types.Archive array> =
promise {
let fgaUser = sprintf "group:%s#member" (Groups.Utils.canonicalizeName group)
let! archivesRes = fetchArchmaesterArchives group
let! viewsRes =
OpenFGA.fetchObjects(fgaUser, "view", "archive", context = {| time = System.DateTime.Now |})
let! execsRes =
OpenFGA.fetchObjects(fgaUser, "exec", "archive", context = {| task = "*"; usage = -1; time = System.DateTime.Now |})
let res =
result {
// TODO: Create specific exception
let! props = archivesRes |> Result.mapError System.Exception
let! views = viewsRes |> Result.mapError System.Exception
let! execs = execsRes |> Result.mapError System.Exception
let viewArchiveIds = views |> Array.choose extractFgaArchiveId
let execArchiveIds = execs |> Array.choose extractFgaArchiveId
let archives =
props
|> Array.map (fun prop -> {
Props = prop
CanView = viewArchiveIds |> Array.contains prop.archiveId
CanExec = execArchiveIds |> Array.contains prop.archiveId
})
return archives
}
match res with
| Ok archives ->
return archives
| Error ex ->
raise ex
return [||]
}

View File

@@ -1,14 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Oceanbox</RootNamespace>
<Version>0.0.0-alpha.1</Version>
<Version>0.0.1</Version>
<DefineConstants>FABLE_COMPILER</DefineConstants>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="../Shared/Remoting.fs" />
<Compile Include="Types.fs" />
<Compile Include="Utils.fs" />
<Compile Include="Extensions.fs" />
<Compile Include="Remoting.fs" />
<Compile Include="Map.fs" />
@@ -18,6 +19,8 @@
<Compile Include="OpenFGA/useReadTuples.fs" />
<Compile Include="OpenFGA/Checkbox.fs" />
<Compile Include="OpenFGA/ArchiveOwnerList.fs" />
<Compile Include="Users/DeleteForm.fs" />
<Compile Include="Users/OpenFgaList.fs" />
<Compile Include="Groups/Utils.fs" />
<Compile Include="Groups/useGroups.fs" />
<Compile Include="Groups/List.fs" />
@@ -34,6 +37,7 @@
<Compile Include="Groups.fs" />
<Compile Include="GroupArchiveAddForm.fs" />
<Compile Include="GroupArchive.fs" />
<Compile Include="GroupUser.fs" />
<Compile Include="Group.fs" />
<Compile Include="ArchivesList.fs" />
<Compile Include="Archives.fs" />
@@ -54,6 +58,7 @@
<PackageReference Include="Fable.Remoting.Client" />
<PackageReference Include="Feliz" />
<PackageReference Include="Feliz.Router" />
<PackageReference Include="FS.FluentUI" />
<PackageReference Include="Feliz.UseElmish" />
<PackageReference Include="FsToolkit.ErrorHandling" />
</ItemGroup>

View File

@@ -4,8 +4,108 @@ open Browser
open Fable.Core
open Feliz
open Feliz.Router
open FS.FluentUI
type private NavTab =
| Index
| Archives
| ModelAreas
| Groups
| Organizations
override this.ToString () =
match this with
| Index -> ""
| Archives -> "archives"
| ModelAreas -> "model-areas"
| Groups -> "groups"
| Organizations -> "organizations"
static member FromString str =
match str with
| "archives" -> Archives
| "model-areas" -> ModelAreas
| "groups" -> Groups
| "organizations" -> Organizations
| _ -> Index
member this.ToPath () =
[this.ToString ()] |> Router.format
static member FromPath path =
match path with
| [] -> NavTab.Index
| list -> list |> List.head |> NavTab.FromString
type Components =
/// <summary>
/// Navigation sidebar
/// </summary>
[<ReactComponent>]
static member Sidebar currentUrl : ReactElement =
let currentTab = NavTab.FromPath currentUrl
Html.div [
prop.classes ["vh-100"; "w-256"; "br-solid"; "bcol-neutral-stroke-1"]
prop.children [
Fui.navDrawer [
navDrawer.style [style.height (length.perc 100); style.width (length.perc 100)]
navDrawer.selectedValue (currentTab.ToString())
navDrawer.type'.inline'
navDrawer.open' true
navDrawer.children [
Fui.navDrawerHeader [
Fui.link [
link.style [style.textDecoration.none]
link.href (NavTab.Index.ToPath ())
link.children [
Fui.text.title1 [
text.text "Codex"
text.style [style.color Theme.tokens.colorBrandForeground1]
]
]
]
]
Fui.navDivider []
Fui.navDrawerBody [
navDrawerBody.children [
Fui.navItem [
navItem.icon (Fui.bundleIcon bundleIcons.home [icon.size.``20``])
navItem.href (NavTab.Index.ToPath ())
navItem.value (NavTab.Index.ToString ())
navItem.children [Html.text "Index"]
]
Fui.navItem [
navItem.icon (Fui.bundleIcon bundleIcons.archive <| [icon.size.``20``])
navItem.href (NavTab.Archives.ToPath ())
navItem.value (NavTab.Archives.ToString ())
navItem.children [Html.text "Archives"]
]
Fui.navItem [
navItem.icon (Fui.bundleIcon bundleIcons.map <| [icon.size.``20``])
navItem.href (NavTab.ModelAreas.ToPath ())
navItem.value (NavTab.ModelAreas.ToString ())
navItem.children [Html.text "Model Areas"]
]
Fui.navItem [
navItem.icon (Fui.bundleIcon bundleIcons.people <| [icon.size.``20``])
navItem.href (NavTab.Groups.ToPath ())
navItem.value (NavTab.Groups.ToString ())
navItem.children [Html.text "Groups"]
]
Fui.navItem [
navItem.icon (Fui.bundleIcon bundleIcons.organization <| [icon.size.``20``])
navItem.href (NavTab.Organizations.ToPath ())
navItem.value (NavTab.Organizations.ToString ())
navItem.children [Html.text "Organizations"]
]
]
]
]
]
]
]
/// <summary>
/// A React component that uses Feliz.Router to determine what to show based on the current URL
/// </summary>
@@ -39,19 +139,30 @@ type Components =
elif not authed then
Html.h1 "Redirecting to sign-in..."
else
match currentUrl with
| [ ] -> Index.View()
| [ "archives" ] -> Archives.View ()
| [ "archives"; archive ] -> Archive.View (System.Guid archive)
| [ "model-areas" ] -> ModelAreas.List ()
| [ "model-areas"; id ] -> ModelArea.View (System.Guid id)
| [ "groups" ] -> Groups.View ()
| [ "groups"; group ] -> Group.View group
| [ "groups"; group; "archives"; id ] -> GroupArchive.View group (System.Guid id)
| [ "groups"; group; "users"; user ] -> User.View user
| [ "users"; user ] -> User.View user
| [ "organizations" ] -> Organizations.List ()
| [ "organizations"; org ] -> Organization.View org
| otherwise -> Html.h1 "Not found"
Html.div [
prop.classes ["flex-row"; "gap-16"]
prop.children [
Components.Sidebar currentUrl
Html.div [
prop.classes ["grow"]
prop.children [
match currentUrl with
| [ ] -> Index.View()
| [ "archives" ] -> Archives.View ()
| [ "archives"; Route.Guid archive ] -> Archive.View archive
| [ "model-areas" ] -> ModelAreas.List ()
| [ "model-areas"; Route.Guid id ] -> ModelArea.View id
| [ "groups" ] -> Groups.View ()
| [ "groups"; group ] -> Group.View group
| [ "groups"; group; "archives"; Route.Guid id ] -> GroupArchive.View group id
| [ "groups"; group; "users"; user ] -> GroupUser.View group user
| [ "users"; user ] -> User.View user
| [ "organizations" ] -> Organizations.List ()
| [ "organizations"; org ] -> Organization.View org
| otherwise -> Html.h1 "Not found"
]
]
]
]
]
]
]

View File

@@ -3,7 +3,6 @@ namespace Oceanbox.Codex
module Group =
open Browser
open Fable.Core
open FsToolkit.ErrorHandling
open Oceanbox.Codex.Types
@@ -17,59 +16,9 @@ module Group =
return Error (sprintf "Error fetching users for group %s" group)
}
let private fetchArchmaesterArchives (group: string) : JS.Promise<Result<Archmaester.Dto.ArchiveProps array, string>> =
promise {
let filter : Archmaester.Dto.ArchiveFilter = {
id = None
searchTerm = None
archiveType = None
owner = None
user = None
groups = Some [| group |]
}
let! result = Remoting.adminApi.getArchives 0 -1 filter |> Async.StartAsPromise
return result
}
module private Elmish =
open Elmish
let fetchArchiveProps (group: string) : JS.Promise<Types.Archive array> =
promise {
let fgaUser = sprintf "group:%s#member" (Groups.Utils.canonicalizeName group)
let! archivesRes = fetchArchmaesterArchives group
let! viewsRes =
OpenFGA.fetchObjects(fgaUser, "view", "archive", context = {| time = System.DateTime.Now |})
let! execsRes =
OpenFGA.fetchObjects(fgaUser, "exec", "archive", context = {| task = "*"; usage = -1; time = System.DateTime.Now |})
let res =
result {
// TODO: Create specific exception
let! props = archivesRes |> Result.mapError System.Exception
let! views = viewsRes |> Result.mapError System.Exception
let! execs = execsRes |> Result.mapError System.Exception
let viewArchiveIds = views |> Array.choose Archives.Utils.extractFgaArchiveId
let execArchiveIds = execs |> Array.choose Archives.Utils.extractFgaArchiveId
let archives =
props
|> Array.map (fun prop -> {
Props = prop
CanView = viewArchiveIds |> Array.contains prop.archiveId
CanExec = execArchiveIds |> Array.contains prop.archiveId
})
return archives
}
match res with
| Ok archives ->
return archives
| Error ex ->
raise ex
return [||]
}
type Msg =
| FetchArchives of string
| SetArchiveAdding of bool
@@ -97,7 +46,7 @@ module Group =
let update msg model =
match msg with
| FetchArchives group ->
model, Cmd.OfPromise.either fetchArchiveProps group SetArchives HandleExn
model, Cmd.OfPromise.either Archives.Utils.fetchGroupArchiveProps group SetArchives HandleExn
| HandleExn ex ->
let msg =
match ex with
@@ -470,4 +419,4 @@ module Group =
]
]
]
]
]

View File

@@ -1,10 +1,40 @@
namespace Oceanbox.Codex
type private Permission = {
Tuple: Remoting.Tuple
Relation: Remoting.ArchiveRelation
}
module GroupArchive =
open Browser
open Fable.Core
open Feliz
open Feliz.Router
open FS.FluentUI
let private postPermissions (group: string) (archiveId: System.Guid) (permissions: Remoting.ArchiveRelation array) =
promise {
console.debug("Posting new relations: %o", permissions)
let req : Remoting.AddGroupPermissionsRequest = {
Group = Groups.Utils.canonicalizeName group
ArchiveId = archiveId
Permissions = permissions
}
let! res = Remoting.adminApi.addGroupPermissions req |> Async.StartAsPromise
return res
}
let private putPermissions (group: string) (archiveId: System.Guid) (permissions: Remoting.ArchiveRelation array) =
promise {
console.debug("Updating existing relations: %o", permissions)
let req : Remoting.AddGroupPermissionsRequest = {
Group = Groups.Utils.canonicalizeName group
ArchiveId = archiveId
Permissions = permissions
}
let! res = Remoting.adminApi.updateGroupPermissions req |> Async.StartAsPromise
return res
}
[<ReactComponent>]
let private DeleteRelationButton onDelete (tuple: Remoting.Tuple) =
@@ -20,74 +50,137 @@ module GroupArchive =
else
// TODO: Should probably just return unit and error if not deleted
console.warn ("[Group] Tuple was not deleted: %o", tuple)
| Error err -> console.error ("[Group] Error deleting tuple: %s\n%o", err, tuple)
| Error err ->
console.error ("[Group] Error deleting tuple: %s\n%o", err, tuple)
)
Html.button [ prop.onClick handleDelete; prop.text "Delete" ]
Fui.button [
button.onClick handleDelete
button.icon (Fui.icon.deleteRegular [])
]
[<ReactComponent>]
let private ViewTerm group archiveId (onDelete: Remoting.Tuple -> unit) (viewTerm: Remoting.ViewTerm) =
let tuple =
Remoting.Tuple.delete (
user = Groups.Utils.fgaMember group,
relation = "view",
object = sprintf "archive:%O" archiveId
)
let private PermissionCard (title: string) onDelete (tuple: Remoting.Tuple) (children: ReactElement array) =
Html.div [
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
prop.style [ style.flexBasis (length.px 320) ]
prop.style [ style.flexBasis (length.px 384) ]
prop.children [
Html.div [
prop.classes [ "flex-row-center" ]
prop.children [
Html.div [ prop.classes [ "grow" ]; prop.children [ Html.b "View Term" ] ]
Html.div [
prop.classes [ "grow" ]
prop.children [
Html.b [
prop.style [
style.fontSize(length.px 16)
]
prop.text title
]
]
]
DeleteRelationButton onDelete tuple
]
]
Html.div [
prop.classes [ "ml-16" ]
prop.children [
Html.div (sprintf "Start time: %s" (Intl.shortDateTime viewTerm.StartTime))
Html.div (sprintf "End time: %s" (Intl.shortDateTime viewTerm.EndTime))
]
prop.children children
]
]
]
[<ReactComponent>]
let private ExecTicket (group: string) (archiveId: System.Guid) onDelete (ticket: Remoting.ExecTicket) =
let tuple =
Remoting.Tuple.delete (
user = Groups.Utils.fgaMember group,
relation = "exec",
object = sprintf "archive:%O" archiveId
let private ViewTerm
key
(onUpdate: Permission -> unit)
(permission: Permission)
(viewTerm: Remoting.ViewTerm)
=
let updateCond (newCond: Remoting.ViewTerm) =
let updatedCond =
permission.Tuple.Condition
|> Option.map (fun cond ->
{ cond with Context = JS.JSON.stringify newCond.JsonObj }
)
onUpdate {
permission with
Tuple.Condition = updatedCond
Relation = Remoting.ArchiveRelation.ViewTerm newCond
}
let handleStartChange =
React.useCallback (
(fun (newStartOpt: System.DateTime option) ->
match newStartOpt with
| Some newStart ->
let updated = { viewTerm with StartTime = newStart }
updateCond updated
| None ->
console.error("Got no date from date picker")
),
[| permission |]
)
let handleEndChange =
React.useCallback (
(fun (newEndOpt: System.DateTime option) ->
match newEndOpt with
| Some newEnd ->
let updated = { viewTerm with EndTime = newEnd }
updateCond updated
| None ->
console.error("Got no date from date picker")
),
[| permission |]
)
Html.div [
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
prop.style [ style.flexBasis (length.px 320) ]
prop.children [
Html.div [
prop.classes [ "flex-row-center" ]
prop.children [
Html.div [ prop.classes [ "grow" ]; prop.children [ Html.b "Exec Ticket" ] ]
Fui.table [
table.size.medium
table.children [
Fui.tableBody [
tableBody.children [
Fui.tableRow [
tableRow.key "view-term-start-time"
tableRow.children [
Fui.tableCell [
tableCell.text "Start time"
]
DeleteRelationButton onDelete tuple
]
]
Html.div [
prop.classes [ "ml-16" ]
prop.children [
Html.div (sprintf "Start time: %s" (Intl.shortDateTime ticket.StartTime))
Html.div (sprintf "End time: %s" (Intl.shortDateTime ticket.EndTime))
Html.div (sprintf "Quota: %.1f" ticket.Quota)
Html.div [
prop.children [
Html.span "Tasks:"
Html.ul [
prop.children (ticket.Tasks |> Array.map (fun task -> Html.li task))
Fui.tableCell [
tableCell.children (
Fui.tableCellLayout [
tableCellLayout.children (
Fui.datePicker [
datePicker.size.small
datePicker.onSelectDate handleStartChange
datePicker.value (Some viewTerm.StartTime)
]
)
]
)
]
]
]
Fui.tableRow [
tableRow.key "view-term-end-time"
tableRow.children [
Fui.tableCell [
tableCell.text "End time"
]
Fui.tableCell [
tableCell.children (
Fui.tableCellLayout [
tableCellLayout.children (
Fui.datePicker [
datePicker.size.small
datePicker.onSelectDate handleEndChange
datePicker.value (Some viewTerm.EndTime)
]
)
]
)
]
]
]
@@ -97,14 +190,278 @@ module GroupArchive =
]
[<ReactComponent>]
let private PermissionForm (permissions: OpenFGA.Types.ArchiveRelation array) (group: string) =
let private ExecTicket
(key: string)
(onUpdate: Permission -> unit)
(permission: Permission)
(ticket: Remoting.ExecTicket)
=
let updateCond (newCond: Remoting.ExecTicket) =
let updatedCond =
permission.Tuple.Condition
|> Option.map (fun cond ->
{ cond with Context = JS.JSON.stringify newCond.JsonObj }
)
onUpdate {
permission with
Tuple.Condition = updatedCond
Relation = Remoting.ArchiveRelation.ExecTicket newCond
}
let handleStartChange =
React.useCallback (
(fun (newStartOpt: System.DateTime option) ->
match newStartOpt with
| Some newStart ->
let updated = { ticket with StartTime = newStart }
updateCond updated
| None ->
console.error("Got no date from date picker")
),
[| ticket |]
)
let handleEndChange =
React.useCallback (
(fun (newEndOpt: System.DateTime option) ->
match newEndOpt with
| Some newEnd ->
let updated = { ticket with EndTime = newEnd }
updateCond updated
| None ->
console.error("Got no date from date picker")
),
[| ticket |]
)
Fui.table [
table.size.medium
table.children [
Fui.tableBody [
tableBody.children [
Fui.tableRow [
tableRow.key "exec-ticket-start-time"
tableRow.children [
Fui.tableCell [
tableCell.text "Start time"
]
Fui.tableCell [
tableCell.children (
Fui.tableCellLayout [
tableCellLayout.children (
Fui.datePicker [
datePicker.size.small
datePicker.onSelectDate handleStartChange
datePicker.value (Some ticket.StartTime)
]
)
]
)
]
]
]
Fui.tableRow [
tableRow.key "exec-ticket-end-time"
tableRow.children [
Fui.tableCell [
tableCell.text "End time"
]
Fui.tableCell [
tableCell.children (
Fui.tableCellLayout [
tableCellLayout.children (
Fui.datePicker [
datePicker.size.small
datePicker.onSelectDate handleEndChange
datePicker.value (Some ticket.EndTime)
]
)
]
)
]
]
]
Fui.tableRow [
tableRow.key "exec-ticket-quota"
tableRow.children [
Fui.tableCell [
tableCell.text "Quota"
]
Fui.tableCell [
tableCell.children (
Fui.tableCellLayout [
tableCellLayout.children (
Fui.input [
input.size.small
input.type'.number
input.value ticket.Quota
]
)
]
)
]
]
]
Fui.tableRow [
tableRow.key "exec-ticket-tasks"
tableRow.children [
Fui.tableCell [
tableCell.text "Tasks"
]
Fui.tableCell [
tableCell.children (
Fui.tableCellLayout [
tableCellLayout.children (
Fui.text (
ticket.Tasks
|> String.concat ", "
)
)
]
)
]
]
]
]
]
]
]
[<ReactComponent>]
let private PermissionCreateCard group archiveId (onCreate: Permission -> unit) (defaultPermission: Permission) =
let isLoading, setLoading = React.useState false
let permission, setPermission = React.useState defaultPermission
let handleCreate (ev: Types.Event) =
setLoading true
postPermissions group archiveId [| permission.Relation |]
|> Promise.iter (fun res ->
match res with
| Ok () ->
console.info("Success.")
onCreate permission
| Error msg ->
console.error("Error adding permissions %s.", msg)
setLoading false
)
let handleUpdateRelation (permission: Permission) =
setPermission permission
console.debug("Permission: %o", permission)
Html.div [
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
prop.style [ style.flexBasis (length.px 384) ]
prop.children [
Html.div [
prop.classes [ "flex-row-center" ]
prop.children [
Html.div [
prop.classes [ "grow" ]
prop.children [
Html.b [
prop.style [
style.fontSize(length.px 16)
]
prop.text (
match permission.Relation with
| Remoting.ArchiveRelation.ViewTerm _ -> "View Term"
| Remoting.ArchiveRelation.ExecTicket _ -> "Exec Ticket"
)
]
]
]
Fui.button [
button.onClick handleCreate
button.icon (
if isLoading then
Fui.spinner [ spinner.size.tiny ]
else
Fui.icon.addRegular []
)
]
]
]
Html.div [
prop.children [|
match permission.Relation with
| Remoting.ArchiveRelation.ViewTerm term ->
ViewTerm
"create-permission-view-term"
handleUpdateRelation
permission
term
| Remoting.ArchiveRelation.ExecTicket ticket ->
ExecTicket
"create-permission-exec-ticket"
handleUpdateRelation
permission
ticket
|]
]
]
]
let private allRelations = [|
Remoting.ArchiveRelation.ViewTerm Remoting.ViewTerm.empty
Remoting.ArchiveRelation.ExecTicket Remoting.ExecTicket.empty
|]
[<ReactComponent>]
let private PermissionForm
(group: string)
(archiveId: System.Guid)
(onAdd: Permission -> unit)
(permissions: Permission array)
=
let adding, setAdding = React.useState false
let loading, setLoading = React.useState false
let success, setSuccess = React.useState false
let handleAddClick (ev: Types.Event) = setAdding true
let handleCancelClick (ev: Types.Event) = setAdding false
// Create a list of permissions missing from the archive
let availablePermissions : Permission array =
allRelations
|> Array.choose (fun relation ->
let exists =
permissions
|> Array.exists (fun permission ->
let name = Remoting.ArchiveRelation.ConditionName relation
permission.Tuple.Condition
|> Option.map (fun cond -> cond.Name = name)
|> Option.defaultValue false
)
let hasViewTerm = permissions |> Array.exists _.IsViewTerm
let hasExecTicket = permissions |> Array.exists _.IsExecTicket
if exists then
None
else
let user = Groups.Utils.fgaMember group
let object = sprintf "archive:%O" archiveId
Some {
Tuple = OpenFGA.Types.ArchiveRelation.toTuple user object relation
Relation = relation
}
)
let handleAddClick (ev: Types.Event) =
setAdding true
let handleCancelClick (ev: Types.Event) =
setAdding false
let handlePermissionAdd (newPermission: Permission) =
console.debug("Added new permission: %o", newPermission)
onAdd newPermission
React.fragment [
Html.div [
@@ -113,52 +470,39 @@ module GroupArchive =
"gap-8"
]
prop.children [
if adding then
Html.button [ prop.onClick handleAddClick; prop.text "Save" ]
Html.button [
prop.onClick handleCancelClick
prop.text "Cancel"
]
if loading then
Html.p "Loading ..."
else
Html.button [ prop.onClick handleAddClick; prop.text "Add" ]
if adding then
Html.button [
prop.onClick handleCancelClick
prop.text "Cancel"
]
else
Html.button [
prop.disabled (Array.isEmpty availablePermissions)
prop.onClick handleAddClick
prop.text "Add"
]
]
]
if not loading && success then
Html.p "Success."
if adding then
Html.div [
prop.id "group-archive-exec-form"
prop.classes [
"flex-row"
"flex-row-start"
"gap-32"
]
prop.children [
if not hasViewTerm then
Html.div [
prop.children [
Html.b "View"
Groups.ViewForm (Remoting.ViewTerm.empty, ignore)
]
]
if not hasExecTicket then
Html.div [
prop.classes [
"flex-column"
"gap-8"
"shadow"
"brad-8"
"m-8"
"p-16"
]
prop.style [
style.flexBasis (length.px 512)
]
prop.children [
Html.b "Exec"
Groups.ExecForm (Remoting.ExecTicket.empty, ignore)
]
]
availablePermissions
|> Array.map (fun permission ->
PermissionCreateCard group archiveId handlePermissionAdd permission
)
|> unbox
]
]
]
@@ -167,22 +511,77 @@ module GroupArchive =
let View (group: string) (archiveId: System.Guid) =
let loading, setLoading = React.useState true
let error, setError = React.useState<string option> None
let archiveOpt, setArchive =
React.useState<Archmaester.Dto.ArchiveProps option> None
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
let fgaUser = Groups.Utils.fgaMember group
let tuples =
OpenFGA.useReadTuples (fgaUser, object = sprintf "archive:%O" archiveId)
let tuples = OpenFGA.useReadTuples (fgaUser, object = sprintf "archive:%O" archiveId)
let relations: OpenFGA.Types.ArchiveRelation array =
let permissions: Permission array =
tuples.Tuples
|> Array.choose (fun tuple -> tuple.Condition |> Option.bind OpenFGA.Types.ArchiveRelation.tryOfCondition)
|> Array.choose (fun tuple ->
tuple.Condition
|> Option.bind (fun cond ->
cond
|> OpenFGA.Types.ArchiveRelation.tryOfCondition
|> Option.map (fun rel -> {
Tuple = tuple
Relation = rel
})
)
)
let handlePermissionDelete (tuple: Remoting.Tuple) =
console.debug("Deleting %o from %o", tuple, tuples)
tuples.Tuples
|> Array.filter (fun existing -> existing.Relation <> tuple.Relation)
|> tuples.SetTuples
let handlePermissionUpdate (updated: Permission) =
console.debug("Updated permission tuple %o", updated)
putPermissions group archiveId [| updated.Relation |]
|> Promise.iter (fun res ->
match res with
| Ok () ->
tuples.Tuples
|> Array.map (fun existing ->
let equal =
existing.Object = updated.Tuple.Object
&& existing.Relation = updated.Tuple.Relation
&& existing.User = updated.Tuple.User
if equal then
updated.Tuple
else
existing
)
|> tuples.SetTuples
| Error msg ->
setError (Some msg)
)
let handleUpdateRelations =
React.useCallback (
(fun (updated: Remoting.Tuple array) ->
console.debug("Relations updated: %o with current: %o", updated, tuples.Tuples)
tuples.SetTuples updated
),
[| box tuples |]
)
let handleAddPermission =
React.useCallback (
(fun (newPermission: Permission) ->
console.debug("New relation added: %o with current: %o", newPermission, tuples.Tuples)
newPermission.Tuple
|> Array.singleton
|> Array.append tuples.Tuples
|> tuples.SetTuples
),
[| box tuples |]
)
React.useEffect (
(fun () ->
setLoading true
@@ -210,7 +609,10 @@ module GroupArchive =
Html.h1 [
prop.children [
Html.text "Group "
Html.a [ prop.href (Router.format ("groups", group)); prop.text group ]
Html.a [
prop.href (Router.format ("groups", group))
prop.text group
]
Html.text " / "
Html.text "Archive "
Html.a [
@@ -220,7 +622,14 @@ module GroupArchive =
]
]
Html.div [ prop.children [ Html.button [ prop.text "Remove" ] ] ]
Html.div [
prop.children [
Html.button [
prop.disabled true
prop.text "Remove (TODO)"
]
]
]
Archives.InfoSection archive
@@ -228,9 +637,13 @@ module GroupArchive =
prop.children [
Html.h2 "Permissions"
if not tuples.Loading then
if tuples.Loading then
Html.p "Loading ..."
else
Html.div [
prop.children [ PermissionForm relations group ]
prop.children [
PermissionForm group archive.archiveId handleAddPermission permissions
]
]
Html.div [
@@ -240,23 +653,28 @@ module GroupArchive =
]
]
if Array.isEmpty relations then
if Array.isEmpty permissions then
Html.p "No permissions"
else
Html.div [
prop.classes [ "flex-row"; "gap-32" ]
prop.classes [ "flex-row-start"; "gap-32" ]
prop.children (
relations
|> Array.map (
function
| OpenFGA.Types.ArchiveRelation.ViewTerm term ->
ViewTerm group archive.archiveId handlePermissionDelete term
| OpenFGA.Types.ArchiveRelation.ExecTicket ticket ->
ExecTicket group archive.archiveId handlePermissionDelete ticket
permissions
|> Array.map (fun permission ->
match permission.Relation with
| Remoting.ArchiveRelation.ViewTerm term ->
PermissionCard "View Term" handlePermissionDelete permission.Tuple [|
ViewTerm "view-term-table" handlePermissionUpdate permission term
|]
| Remoting.ArchiveRelation.ExecTicket ticket ->
PermissionCard "Exec Ticket" handlePermissionDelete permission.Tuple [|
ExecTicket "exec-ticket-table" handlePermissionUpdate permission ticket
|]
)
)
]
]
]
| None -> Html.h1 (sprintf "Group %s / Archive %O not found" group archiveId)
]
| None ->
Html.h1 (sprintf "Group %s / Archive %O not found" group archiveId)
]

View File

@@ -0,0 +1,204 @@
namespace Oceanbox.Codex
open Feliz
open Feliz.Router
module GroupUser =
[<ReactComponent>]
let private ArchiveList key (group: string) (title: string) (archives: Types.Archive array) =
Html.div [
prop.style [
style.flexBasis (length.px 256)
]
prop.children [
Html.h3 title
Html.ul [
prop.children (
archives
|> Array.sortBy _.Props.name
|> Array.map (fun archive ->
let text = Archives.Utils.archiveName archive
Html.li [
prop.children [
Html.a [
prop.href (Router.format("groups", group, "archives", string archive.Props.archiveId))
prop.text text
]
]
]
)
)
]
]
]
[<ReactComponent>]
let private ArchiveLists (group: string) =
let archives, setArchives = React.useState<Types.Archive array> [||]
React.useEffect (
(fun () ->
Archives.Utils.fetchGroupArchiveProps group
|> Promise.iter (fun res ->
setArchives res
)
),
[| box group |]
)
let fvcom =
archives
|> Array.filter (fun archive ->
match archive.Props.archiveType with
| Archmaester.Dto.Fvcom _ -> true
| _ -> false
)
let drifters =
archives
|> Array.filter (fun archive ->
match archive.Props.archiveType with
| Archmaester.Dto.Drifters _ -> true
| _ -> false
)
Html.div [
prop.classes ["flex-row-start"; "gap-8"]
prop.children [
ArchiveList "fvcom-ul-list" group "FVCOM" fvcom
ArchiveList "drifters-ul-list" group "Drifters" drifters
]
]
[<ReactComponent>]
let View (group: string) (user: string) =
let fgaUser = sprintf "user:%s" user
let execCtx = {|
time = System.DateTime.Now
task = "*"
usage = "-1"
|}
Html.main [
Html.h1 [
prop.children [
Html.text "Group "
Html.a [
prop.href (Router.format ("groups", group))
prop.text group
]
Html.text " / "
Html.text "User "
Html.a [
prop.href (Router.format ("users", user))
prop.text user
]
]
]
Html.section [
prop.children [
Users.DeleteForm user
]
]
Html.section [
prop.children [
Html.h2 "Archmaester"
Html.p "TODO"
]
]
Html.section [
prop.children [
Html.h2 "OpenFGA"
Html.div [
prop.classes [ "flex-row"; "flex-wrap"; "gap-8" ]
prop.children [
OpenFGA.Checkbox("active-checkbox", "Active", fgaUser, "active", fgaUser)
OpenFGA.Checkbox("registered-checkbox", "Registered", fgaUser, "registered", fgaUser)
OpenFGA.Checkbox("disabled-checkbox", "Disabled", fgaUser, "disabled", fgaUser)
]
]
Html.div [
prop.classes [ "flex-row"; "flex-wrap"; "gap-32" ]
prop.children [
Html.div [
prop.classes [ "grow" ]
prop.style [
style.flexBasis (length.px 320)
style.maxWidth (length.px 512)
style.minWidth (length.px 320)
]
prop.children [
Html.h3 "Archives with Owner"
Html.div [
prop.style [
style.overflowY.auto
style.maxHeight (length.px 512)
]
prop.children [
Users.OpenFgaList(fgaUser, "owner", "archive")
]
]
]
]
Html.div [
prop.classes [ "grow" ]
prop.style [
style.flexBasis (length.px 320)
style.maxWidth (length.px 512)
style.minWidth (length.px 320)
]
prop.children [
Html.h3 "Archives with View"
Html.div [
prop.style [
style.overflowY.auto
style.maxHeight (length.px 512)
]
prop.children [
Users.OpenFgaList(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
]
]
]
]
Html.div [
prop.classes [ "grow" ]
prop.style [
style.flexBasis (length.px 320)
style.maxWidth (length.px 512)
style.minWidth (length.px 320)
]
prop.children [
Html.h3 "Archives with exec"
Html.div [
prop.style [
style.overflowY.auto
style.maxHeight (length.px 512)
]
prop.children [
Users.OpenFgaList(fgaUser, "exec", "archive", execCtx)
]
]
]
]
]
]
]
]
Html.section [
prop.children [
Html.h2 "Archives via group"
ArchiveLists group
]
]
]

View File

@@ -20,11 +20,16 @@ type Groups =
onChange { view with EndTime = endTime }
Html.div [
prop.classes [ "flex-column"; "gap-8"]
prop.children [
Html.div [
prop.classes [ "flex-row-start"]
prop.children [
Html.label [
prop.text "Start date"
prop.style [
style.flexBasis (length.px 128)
]
prop.text "Start date:"
]
Html.input [
@@ -36,9 +41,13 @@ type Groups =
]
Html.div [
prop.classes [ "flex-row-start"]
prop.children [
Html.label [
prop.text "End date"
prop.style [
style.flexBasis (length.px 128)
]
prop.text "End date:"
]
Html.input [
@@ -52,4 +61,3 @@ type Groups =
Html.p "NB: If the start date is the same or after the end date, there is no time restriction."
]
]

View File

@@ -1,45 +1,569 @@
namespace Oceanbox.Codex
open Fable.Core
open Feliz
open Feliz.Router
open FS.FluentUI
open Browser
module Index =
[<Literal>]
let private noGroup = "Select Group..."
type private User = { Name: string; Group: string }
// TODO: Elmish?
[<ReactComponent>]
let View () =
Html.main [
Html.h1 "Codex"
Html.h2 "Index"
Html.div [
prop.children [
Html.ul [
Html.li [
Html.a [
prop.href (Router.format "archives")
prop.text "archives"
let private UserAddForm onAdd =
let email, setEmail = React.useState ""
let selectedGroup, setSelectedGroup = React.useState noGroup
let recentlyAddedUsers, setRecentlyAddedUsers = React.useState<User array> [||]
let groups = Groups.useGroups ()
// TODO: Fui Toast?
let errMsg, setErrMsg = React.useState None
let handleAddUser () =
let newUser = {
Name = email
Group = Groups.Utils.canonicalizeName selectedGroup
}
Remoting.adminApi.addUsers { Group = newUser.Group; Users = [| newUser.Name |] }
|> Async.StartAsPromise
|> Promise.iter (fun res ->
match res with
| Ok () ->
console.info("Added user %s to selectedGroup %s", newUser.Name, newUser.Group)
onAdd (Some (email, selectedGroup))
recentlyAddedUsers |> Array.append [|newUser|] |> setRecentlyAddedUsers
| Error msg ->
setErrMsg (Some msg)
console.error("Error adding user %s to group %s: %s", newUser.Name, newUser.Group, msg)
)
Fui.card [
card.classes [ "flex-column"; "gap-16"; "w-512"; "minh-512"]
card.children [
React.fragment [
Fui.cardHeader [
cardHeader.image (Fui.icon.personAddRegular [icon.size.``32``])
cardHeader.header (Fui.text.title3 [ text.weight.semibold; text.text "Add new users"])
]
if groups.Loading then
Html.div [
prop.classes ["flex-row"]
prop.children [
Fui.spinner []
Fui.text.body1 "Loading groups..."
]
]
else
match groups.Error with
| Some _error ->
Html.div [
prop.classes ["flex-row"]
prop.children [
Fui.icon.warningRegular []
Fui.text.body1 "Failed to load groups"
]
]
| None ->
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Group"
]
)
// field.style [style.maxWidth 256]
field.children (
Fui.dropdown [
dropdown.value selectedGroup
dropdown.selectedOptions [|selectedGroup|]
dropdown.onOptionSelect (fun (d: OptionOnSelectData) ->
d.optionText
|> Option.iter setSelectedGroup
)
dropdown.children [
yield!
groups.Groups
|> Array.sort
|> Array.map (fun group ->
Fui.option [
option.key group
option.text group
option.children (Fui.text group)
]
)
]
]
)
]
Html.li [
Html.a [
prop.href (Router.format "model-areas")
prop.text "model areas"
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Email"
]
)
// field.style [style.maxWidth 256]
field.children (
Fui.input [
input.contentBefore (Fui.icon.mailRegular [])
input.type'.email
input.onChange (fun (v: ValueProp<string>) -> setEmail v.value)
]
)
]
]
match errMsg with
| None -> Html.none
| Some msg ->
Html.div [
prop.classes [ "flex-row"; "gap-8"]
prop.children [
Fui.text [
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
text.text msg
]
Fui.button [
button.appearance.subtle
button.icon (Fui.icon.dismissRegular [])
button.onClick (fun _ -> setErrMsg None)
]
]
]
Html.li [
Html.a [
prop.href (Router.format "groups")
prop.text "groups"
]
Html.div [
prop.classes [ "flex-row"; "gap-8"; ]
prop.children [
Fui.button [
button.onClick (fun _ -> handleAddUser ())
button.text "Save"
button.icon (Fui.icon.saveRegular [])
button.appearance.primary
button.disabled <| (email = "" || selectedGroup = noGroup)
]
Html.li [
Html.a [
prop.href (Router.format "organizations")
prop.text "organizations"
]
Fui.button [
button.onClick (fun _ ->
setEmail ""
setSelectedGroup noGroup
)
button.text "Clear"
button.icon (Fui.icon.arrowResetRegular [])
]
]
]
match recentlyAddedUsers with
| [||] -> Html.none
| users ->
Html.div [
prop.classes ["flex-column"; "gap-16"]
prop.children [
Fui.text.title3 "Recently added"
Fui.table [
Fui.tableHeader [
Fui.tableRow [
Fui.tableHeaderCell [
tableHeaderCell.children [ Fui.text "Name"]
]
Fui.tableHeaderCell [
tableHeaderCell.children [ Fui.text "Group"]
]
]
]
Fui.tableBody [
yield!
users |> Array.map (fun user ->
Fui.tableRow [
tableRow.key user.Name
tableRow.children [
Fui.tableCell [
Fui.link [
link.href (Router.format ["users"; user.Name])
link.text user.Name
]
]
Fui.tableCell [
Fui.link [
link.href (Router.format ["groups"; user.Group])
link.text user.Group
]
]
]
]
)
]
]
]
]
]
]
[<Literal>]
let private noDataSet = "Select DataSet..."
[<ReactComponent>]
let private ArchiveAddForm () =
let dataSets, setDataSets = React.useState [||]
let isLoading, setIsLoading = React.useState true
let recentlyAddedArchives, setRecentlyAddedArchives = React.useState [||]
let errMsg, setErrMsg = React.useState None
let form, setForm = React.useState (Remoting.AddArchiveRequest.empty ())
let fetchDataSets () : JS.Promise<Result<Remoting.DataSet array, string>> =
promise {
try
console.debug("Fetching all dataSets")
let! res = Remoting.adminApi.getDataSets () |> Async.StartAsPromise
return res
with ex ->
console.error("Error fetching dataSets: %s", ex.Message)
return Error "Error fetching dataSets"
}
React.useEffectOnce (fun _ ->
fetchDataSets ()
|> Promise.iter (fun res ->
setIsLoading false
match res with
| Ok r ->
setDataSets r
| Error e -> setErrMsg (Some e)
)
)
let handleAddArchive () =
let utcTime = System.DateTime(form.StartTime.Year, form.StartTime.Month, form.StartTime.Day, 0, 0, 0, System.DateTimeKind.Utc)
Remoting.adminApi.addArchive {form with StartTime = utcTime}
|> Async.StartAsPromise
|> Promise.iter (fun res ->
match res with
| Ok newArchive ->
console.info("Added archive %s with id %s", newArchive.Name, string newArchive.Id)
recentlyAddedArchives |> Array.append [|newArchive|] |> setRecentlyAddedArchives
setForm (Remoting.AddArchiveRequest.empty())
| Error msg ->
console.error("Error adding archive %s: %s", form.Name, msg)
setErrMsg (Some msg)
)
let selectedDataSet =
dataSets
|> Array.tryFind (fun ds -> ds.Id = form.DataSetId)
let framesExceedEnd =
selectedDataSet
|> Option.map (fun ds ->
form.StartTime.AddHours(form.Frames) > ds.EndTime
)
|> Option.defaultValue false
Fui.card [
card.classes [ "flex-column"; "gap-16"; "w-512"; "minh-512"]
card.children [
React.fragment [
Fui.cardHeader [
cardHeader.image (Fui.icon.archiveRegular [icon.size.``32``])
cardHeader.header (Fui.text.title3 [ text.weight.semibold; text.text "Add new archives"])
]
match dataSets with
| [||] ->
if isLoading then
Html.div [
prop.classes ["flex-row"]
prop.children [
Fui.spinner []
Fui.text.body1 "Loading groups..."
]
]
else
Html.div [
prop.classes ["flex-row"]
prop.children [
Fui.icon.warningRegular []
Fui.text.body1 "Failed to load groups"
]
]
| dataSets' ->
Fui.field [
field.label (
Fui.label [
label.required true
label.text "DataSet"
]
)
// field.style [style.maxWidth 256]
field.children (
Fui.dropdown [
let selectedPath = selectedDataSet |> Option.map _.BasePath |> Option.defaultValue noDataSet
dropdown.value selectedPath
dropdown.selectedOptions [|selectedPath|]
dropdown.onOptionSelect (fun (d: OptionOnSelectData) ->
d.optionText
|> Option.bind (fun path -> dataSets |> Array.tryFind (fun ds -> ds.BasePath = path))
|> Option.iter (fun ds ->
setForm {form with DataSetId = ds.Id; StartTime = ds.StartTime}
)
)
dropdown.children [
yield!
dataSets'
|> Array.groupBy _.ModelAreaName
|> Array.sortBy fst
|> Array.map (fun (modelArea, dataSetsInArea) ->
Fui.optionGroup [
optionGroup.key modelArea
optionGroup.label modelArea
optionGroup.children [
yield!
dataSetsInArea
|> Array.sort
|> Array.map (fun dataSet ->
Fui.option [
option.key dataSet.Id
option.text dataSet.BasePath
option.children (Fui.text dataSet.BasePath)
]
)
]
]
)
]
]
)
]
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Name"
]
)
// field.style [style.maxWidth 256]
field.children (
Fui.input [
input.value form.Name
input.onChange (fun (v: ValueProp<string>) ->
if v.value.Length <= 80 then
setForm {form with Name = v.value}
)
]
)
field.hint $"{form.Name.Length}/80"
]
Html.div [
prop.classes ["flex-row"; "gap-8"]
prop.children [
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Start Date"
]
)
field.children (
Fui.datePicker [
datePicker.placeholder "Select a date..."
datePicker.showWeekNumbers true
match selectedDataSet with
| None -> ()
| Some ds ->
datePicker.maxDate ds.EndTime
datePicker.minDate ds.StartTime
datePicker.formatDate (fun d -> d.ToShortDateString())
datePicker.value (Some form.StartTime)
datePicker.onSelectDate (fun d ->
d |> Option.iter (fun d' ->
setForm {form with StartTime = d'}
console.debug("d': %s", d'.ToString())
console.debug("d'.Kind: %s", d'.Kind)
console.debug("utc: %s", d'.ToUniversalTime().ToString())
console.debug("utc.Kind: %s", d'.ToUniversalTime().Kind)
)
)
]
)
]
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Days"
]
)
field.children (
Fui.spinButton [
spinButton.value (form.Frames / 24)
spinButton.min 1
spinButton.onChange (fun (d: SpinButtonOnChangeData) ->
match d.value with
| Some v ->
setForm {form with Frames = v * 24}
| None ->
if d.displayValue.ToCharArray() |> Array.forall System.Char.IsDigit then
setForm {form with Frames = int d.displayValue * 24}
)
]
)
]
]
]
Fui.text.caption1 [
if framesExceedEnd then
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
let endDate = form.StartTime.AddHours(form.Frames).ToShortDateString()
text.text
($"End Date: {endDate}, {form.Frames} frames"
+
if framesExceedEnd then
" (Exceeds DataSet Bounds!)"
else
"")
]
Fui.checkbox [
checkbox.label "Published"
checkbox.checked' form.Published
checkbox.onCheckedChange (fun c -> setForm {form with Published = c})
]
Fui.checkbox [
checkbox.label "Public"
checkbox.checked' form.Public
checkbox.onCheckedChange (fun c -> setForm {form with Public = c})
]
]
Html.div [
prop.classes [ "flex-row"; "gap-8"; ]
prop.children [
Fui.button [
button.onClick (fun _ -> handleAddArchive ())
button.text "Save"
button.icon (Fui.icon.saveRegular [])
button.appearance.primary
button.disabled (
selectedDataSet.IsNone
|| form.Frames < 24
|| form.Name |> System.String.IsNullOrWhiteSpace
|| framesExceedEnd
)
]
Fui.button [
button.onClick (fun _ ->
setForm (Remoting.AddArchiveRequest.empty ())
)
button.text "Clear"
button.icon (Fui.icon.arrowResetRegular [])
]
]
]
match errMsg with
| None -> Html.none
| Some msg ->
Html.div [
prop.classes [ "flex-row"; "gap-8"]
prop.children [
Fui.text [
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
text.text msg
]
Fui.button [
button.appearance.subtle
button.icon (Fui.icon.dismissRegular [])
button.onClick (fun _ -> setErrMsg None)
]
]
]
match recentlyAddedArchives with
| [||] -> Html.none
| archives ->
Html.div [
prop.classes ["flex-column"; "gap-16"]
prop.children [
Fui.text.title3 "Recently added"
Fui.table [
Fui.tableHeader [
Fui.tableRow [
Fui.tableHeaderCell [
tableHeaderCell.children [ Fui.text "Name"]
]
Fui.tableHeaderCell [
tableHeaderCell.children [ Fui.text "Start Time"]
]
Fui.tableHeaderCell [
tableHeaderCell.children [ Fui.text "Frames"]
]
Fui.tableHeaderCell [
tableHeaderCell.children [ Fui.text "Published"]
]
Fui.tableHeaderCell [
tableHeaderCell.children [ Fui.text "Public"]
]
]
]
Fui.tableBody [
yield!
archives |> Array.map (fun archive ->
Fui.tableRow [
tableRow.key archive.Name
tableRow.children [
Fui.tableCell [
Fui.link [
link.href (Router.format ["archives"; string archive.Id])
link.text archive.Name
]
]
Fui.tableCell [
Fui.text (archive.StartTime.ToShortDateString())
]
Fui.tableCell [
Fui.text (string archive.Frames)
]
Fui.tableCell [
if archive.Published then
Fui.icon.checkmarkRegular []
else
Fui.icon.dismissRegular []
]
Fui.tableCell [
if archive.Public then
Fui.icon.checkmarkRegular []
else
Fui.icon.dismissRegular []
]
]
]
)
]
]
]
]
]
]
[<ReactComponent>]
let View () =
let message, setMessage = React.useState None
Html.main [
prop.classes ["flex-column"; "gap-16"]
prop.children [
Fui.text.title2 "Actions"
Html.div [
prop.classes ["flex-row"; "flex-wrap";"gap-16"]
prop.children [
UserAddForm (Option.map (fun email group -> $"User {email} was added to group {group}") >> setMessage)
ArchiveAddForm ()
]
]
]
]

View File

@@ -3,6 +3,33 @@ namespace Oceanbox.Codex
module Main =
open Browser
open Feliz
open FS.FluentUI
let maritimeBlueBrands = {
``10`` = "#020304"
``20`` = "#11181F"
``30`` = "#172736"
``40`` = "#1B3449"
``50`` = "#20405B"
``60`` = "#314D66"
``70`` = "#415972"
``80`` = "#50667D"
``90`` = "#607489"
``100`` = "#708195"
``110`` = "#808FA1"
``120`` = "#909DAD"
``130`` = "#A0ACB9"
``140`` = "#B1BAC5"
``150`` = "#C2C9D2"
``160`` = "#D3D8DE"
}
[<ReactComponent>]
let FluentProvider () =
Fui.fluentProvider [
fluentProvider.theme.createLightTheme maritimeBlueBrands
fluentProvider.children [Components.Router()]
]
let root = ReactDOM.createRoot(document.getElementById "feliz-app")
root.render(Components.Router())
root.render(FluentProvider())

View File

@@ -3,20 +3,42 @@ namespace Oceanbox.Codex
open Browser
open Fable.Core
open Feliz
open FS.FluentUI
[<Erase>]
type OpenFGA =
[<ReactComponent>]
static member Checkbox(key, label: string, user: string, relation: string, object: string) =
let isLoading, setLoading = React.useState false
let isChecked, setChecked = React.useState false
let handleChange (ev: Types.Event) =
console.debug("[OpenFGA.Checkbox] Checkbox %s changed to %o", key, not isChecked)
console.debug("[OpenFGA.Checkbox] Checkbox %s for user %s rel %s changed to %o", key, user, relation, not isChecked)
// TODO: Write to OpenFGA
setChecked(not isChecked)
let newChecked = not isChecked
// setChecked
setLoading true
Remoting.adminApi.setUserPermissions {
User = user
Permissions = [|
{ Name = relation; Enabled = newChecked }
|]
}
|> Async.StartAsPromise
|> Promise.iter (fun res ->
match res with
| Ok () ->
console.debug("Success.")
setChecked newChecked
| Error err ->
console.error("Error: %s", err)
setLoading false
)
React.useEffect (
(fun () ->
setLoading true
Remoting.openFgaApi.Check {
User = user
Relation = relation
@@ -30,6 +52,8 @@ type OpenFGA =
setChecked hasRelation
| Error err ->
console.error("[OpenFGA.Checkbox] Error checking user %s has relation %s to %s", user, relation, object)
setLoading false
)
),
[| |]
@@ -38,17 +62,21 @@ type OpenFGA =
Html.div [
prop.classes [ "flex-row-center" ]
prop.children [
Html.input [
prop.id (sprintf "openfga-checkbox-%s" key)
prop.type'.checkbox
prop.onChange handleChange
prop.custom("checked", isChecked)
]
if isLoading then
Fui.spinner [
spinner.size.tiny
]
else
Html.input [
prop.id (sprintf "openfga-checkbox-%s" key)
prop.type'.checkbox
prop.onChange handleChange
prop.custom("checked", isChecked)
]
Html.label [
prop.htmlFor (sprintf "openfga-checkbox-%s" key)
prop.text label
]
]
]
]

View File

@@ -31,6 +31,8 @@ module Types =
let decode = Decode.fromString decoder
let encode : Remoting.ViewTerm -> string = Encode.Auto.toString
module ExecTicket =
open Thoth.Json
@@ -55,12 +57,59 @@ module Types =
let decode = Decode.fromString decoder
[<RequireQualifiedAccess>]
type ArchiveRelation =
| ViewTerm of Remoting.ViewTerm
| ExecTicket of Remoting.ExecTicket
static member tryOfCondition(cond: Remoting.Condition) =
module ArchiveRelation =
open Browser
open Fable.Core
let tryOfCondition(cond: Remoting.Condition) =
match cond.Name with
| "term" -> cond.Context |> ViewTerm.decode |> Result.toOption |> Option.map ViewTerm
| "ticket" -> cond.Context |> ExecTicket.decode |> Result.toOption |> Option.map ExecTicket
| _ -> None
| "term" ->
match ViewTerm.decode cond.Context with
| Ok ctx -> Remoting.ArchiveRelation.ViewTerm ctx |> Some
| Error err ->
console.error("Error decoding term: %s", err)
None
| "ticket" ->
match ExecTicket.decode cond.Context with
| Ok ctx -> Remoting.ArchiveRelation.ExecTicket ctx |> Some
| Error err ->
console.error("Error decoding ticket: %s", err)
None
| _ ->
console.error("Got unknown condition: %s", cond.Name)
None
let toCond (relation: Remoting.ArchiveRelation) : Remoting.Condition =
match relation with
| Remoting.ArchiveRelation.ViewTerm term -> {
Name = "term"
Context = JS.JSON.stringify term.JsonObj
}
| Remoting.ArchiveRelation.ExecTicket ticket -> {
Name = "ticket"
Context = JS.JSON.stringify ticket.JsonObj
}
// TODO
let toTuple user object (relation: Remoting.ArchiveRelation) : Remoting.Tuple =
match relation with
| Remoting.ArchiveRelation.ViewTerm term ->
{
User = user
Relation = "view"
Object = object
Condition = Some {
Name = "term"
Context = JS.JSON.stringify term.JsonObj
}
}
| Remoting.ArchiveRelation.ExecTicket ticket ->
{
User = user
Relation = "exec"
Object = object
Condition = Some {
Name = "ticket"
Context = JS.JSON.stringify ticket.JsonObj
}
}

Some files were not shown because too many files have changed in this diff Show More