Compare commits

...

166 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
semantic-release-bot
36307c822e chore(release): 1.34.0
# [1.34.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.11...v1.34.0) (2025-11-29)

### Bug Fixes

* extract data cage interaction matrix, closing [#84](https://gitlab.com/oceanbox/Poseidon/issues/84) ([afc888a](afc888ab60))
* reintroduce active layer selector, closes [#74](https://gitlab.com/oceanbox/Poseidon/issues/74) ([c1fa85f](c1fa85fd1b))
* remove parameter limitations on particle sims ([69b380e](69b380e665))

### Features

* fly-to coordinate button, closes [#85](https://gitlab.com/oceanbox/Poseidon/issues/85) ([a153238](a153238f79))
2025-11-29 11:41:15 +00:00
Stig Rune Jensen
dca32db800 Merge branch 'stigrj/mini-feats' into 'main'
Mini feats

Closes #77, #85, #74, and #84

See merge request oceanbox/Poseidon!119
2025-11-29 11:37:38 +00:00
Stig Rune Jensen
a153238f79 feat: fly-to coordinate button, closes #85 2025-11-28 23:21:32 +01:00
Stig Rune Jensen
c1fa85fd1b fix: reintroduce active layer selector, closes #74 2025-11-28 18:45:36 +01:00
Stig Rune Jensen
69b380e665 fix: remove parameter limitations on particle sims 2025-11-28 18:37:25 +01:00
Stig Rune Jensen
afc888ab60 fix: extract data cage interaction matrix, closing #84 2025-11-28 18:01:02 +01:00
semantic-release-bot
9a4ef08060 chore(release): 1.33.11
## [1.33.11](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.10...v1.33.11) (2025-11-28)

### Bug Fixes

* **ci:** Run archivist ci on coffee ([f68b7f6](f68b7f68c8))
2025-11-28 12:13:21 +00:00
f68b7f68c8 fix(ci): Run archivist ci on coffee 2025-11-28 13:10:00 +01:00
semantic-release-bot
d8143a6b8d chore(release): 1.33.10
## [1.33.10](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.9...v1.33.10) (2025-11-28)

### Bug Fixes

* **ci:** Build on Coffee ([e04d36c](e04d36ca12))
2025-11-28 11:45:23 +00:00
e04d36ca12 fix(ci): Build on Coffee 2025-11-28 12:41:40 +01:00
semantic-release-bot
275ec44a97 chore(release): 1.33.9
## [1.33.9](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.8...v1.33.9) (2025-11-28)

### Bug Fixes

* **nix:** Bump hash for node_modules ([fff7913](fff7913cd5))
2025-11-28 11:06:34 +00:00
fff7913cd5 fix(nix): Bump hash for node_modules 2025-11-28 12:03:15 +01:00
semantic-release-bot
23ba2efe96 chore(release): 1.33.8
## [1.33.8](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.7...v1.33.8) (2025-11-28)

### Bug Fixes

* **ci:** Format .gitlab-ci.yml and move codex to nix runner ([3140c07](3140c07ad0))
* **ci:** Remove unsed var ([759bbc6](759bbc6f60))
* **ci:** Try using v4.4 ([c1be7c4](c1be7c468d))
* **ci:** Use 4.4 for check ([b109dbd](b109dbdcbd))
2025-11-28 10:53:19 +00:00
759bbc6f60 fix(ci): Remove unsed var 2025-11-28 11:50:27 +01:00
3140c07ad0 fix(ci): Format .gitlab-ci.yml and move codex to nix runner 2025-11-28 11:49:22 +01:00
b109dbdcbd fix(ci): Use 4.4 for check 2025-11-28 11:44:03 +01:00
c1be7c468d fix(ci): Try using v4.4 2025-11-28 11:42:22 +01:00
semantic-release-bot
d3797115f7 chore(release): 1.33.7
## [1.33.7](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.6...v1.33.7) (2025-11-28)

### Bug Fixes

* **ci:** Change name to codex ([6d04af6](6d04af6230))
* **ci:** Correct tag for codex pipeline ([7cf5064](7cf50641f9))
* **ci:** Run Codex on Coffee ([dd398bd](dd398bd96b))
2025-11-28 10:32:08 +00:00
6d04af6230 fix(ci): Change name to codex 2025-11-28 11:28:21 +01:00
7cf50641f9 fix(ci): Correct tag for codex pipeline 2025-11-28 11:27:40 +01:00
dd398bd96b fix(ci): Run Codex on Coffee 2025-11-28 11:27:16 +01:00
semantic-release-bot
63d782ade4 chore(release): 1.33.6
## [1.33.6](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.5...v1.33.6) (2025-11-28)

### Bug Fixes

* **maps:** Make FluentUI DatePicker popup inline; closes [#83](https://gitlab.com/oceanbox/Poseidon/issues/83) ([e3a1f56](e3a1f56b87))
2025-11-28 09:09:07 +00:00
e3a1f56b87 fix(maps): Make FluentUI DatePicker popup inline; closes #83
So it's not a portal which puts the elem far out in the dom, which is
outside the FluentProvider?
2025-11-28 10:03:16 +01:00
semantic-release-bot
2f18948cce chore(release): 1.33.5
## [1.33.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.4...v1.33.5) (2025-11-27)

### Bug Fixes

* General fixes "stats" menu ([c3c9e8e](c3c9e8e4e2)), closes [#79](https://gitlab.com/oceanbox/Poseidon/issues/79) [#80](https://gitlab.com/oceanbox/Poseidon/issues/80) [#81](https://gitlab.com/oceanbox/Poseidon/issues/81) [#82](https://gitlab.com/oceanbox/Poseidon/issues/82)
* no more DateFlicker (thanks Simen) ([7584bf6](7584bf661f))
* remove goto today in date picker ([f23b3f1](f23b3f1821))
* **stats:** alert banner on missing stats ([5bb2ffd](5bb2ffd67c))
2025-11-27 17:43:29 +00:00
Stig Rune Jensen
4786850431 Merge branch 'mrtz/fixes' into 'main'
fix: General fixes in pipette menu

Closes #79, #80, #81, and #82

See merge request oceanbox/Poseidon!116
2025-11-27 17:39:28 +00:00
Stig Rune Jensen
7584bf661f fix: no more DateFlicker (thanks Simen) 2025-11-27 18:30:27 +01:00
Stig Rune Jensen
f23b3f1821 fix: remove goto today in date picker 2025-11-27 17:26:39 +01:00
Stig Rune Jensen
5bb2ffd67c fix(stats): alert banner on missing stats 2025-11-27 16:23:28 +01:00
c3c9e8e4e2 fix: General fixes "stats" menu
Closes #79
Closes #80
Closes #81
Closes #82
2025-11-27 16:23:28 +01:00
b03908f93e Update node-modules.nix hash 2025-11-27 14:54:53 +01:00
c2e7762df8 Codex: Copy bundle out of nix store for container
Since ASP.NET doesn't know how to follow a symbolic link...
2025-11-27 14:44:14 +01:00
semantic-release-bot
c83beadd45 chore(release): 1.33.4
## [1.33.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.3...v1.33.4) (2025-11-27)

### Bug Fixes

* **js:** Remove unused packages ([10a8c42](10a8c42319))
2025-11-27 13:03:14 +00:00
46d6b1277b Merge branch 'mrtz/fluent-cleanup' into 'main'
fix(js): Remove unused packages

See merge request oceanbox/Poseidon!117
2025-11-27 13:59:07 +01:00
Moritz Jörg
10a8c42319 fix(js): Remove unused packages 2025-11-27 13:48:24 +01:00
9ed4165ebc Add ca certs to Codex container 2025-11-27 13:21:50 +01:00
184 changed files with 6981 additions and 4277 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,49 +1,51 @@
# 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"
SKIP_SINGULARITY: "true"
SKIP_TESTS: "true"
default:
tags:
- nix
include:
- project: oceanbox/gitlab-ci
ref: v4.2
file: template/Base.gitlab-ci.yml
- local: '/src/Atlantis/.gitlab-ci.yml'
rules:
- changes:
- 'src/Atlantis/**/*'
- 'nix/packages/atlantis.nix'
- 'nix/packages/atlantis-client.nix'
- 'nix/containers.nix'
- local: '/src/Sorcerer/.gitlab-ci.yml'
rules:
- changes:
- 'src/Sorcerer/**/*'
- 'nix/packages/sorcerer.nix'
- 'nix/containers.nix'
- local: '/src/Archivist/.gitlab-ci.yml'
rules:
- changes:
- 'src/Archivist/**/*'
- 'nix/packages/archivist.nix'
- local: '/src/Interfaces/.gitlab-ci.yml'
rules:
- changes:
- 'src/Interfaces/**/*'
- 'nix/packages/api.nix'
- local: '/src/DataAgent/.gitlab-ci.yml'
rules:
- changes:
- 'src/DataAgent/**/*'
- 'nix/packages/dataagent.nix'
- local: '/src/ServerPack/.gitlab-ci.yml'
rules:
- changes:
- 'src/ServerPack/**/*'
- 'nix/packages/serverpack.nix'
- local: '/src/Codex/.gitlab-ci.yml'
rules:
- changes:
- 'src/Codex/**/*'
- 'nix/packages/node-modules.nix'
- 'nix/packages/sources.nix'
- project: oceanbox/gitlab-ci
ref: v4.5
file: template/Base.gitlab-ci.yml
- local: "/src/Atlantis/.gitlab-ci.yml"
rules:
- changes:
- "src/Atlantis/**/*"
- "nix/packages/atlantis.nix"
- "nix/packages/atlantis-client.nix"
- "nix/containers.nix"
- local: "/src/Sorcerer/.gitlab-ci.yml"
rules:
- changes:
- "src/Sorcerer/**/*"
- "nix/packages/sorcerer.nix"
- "nix/containers.nix"
- local: "/src/Archivist/.gitlab-ci.yml"
rules:
- changes:
- "src/Archivist/**/*"
- "nix/packages/archivist.nix"
- local: "/src/Interfaces/.gitlab-ci.yml"
rules:
- changes:
- "src/Interfaces/**/*"
- "nix/packages/api.nix"
- local: "/src/DataAgent/.gitlab-ci.yml"
rules:
- changes:
- "src/DataAgent/**/*"
- "nix/packages/dataagent.nix"
- local: "/src/ServerPack/.gitlab-ci.yml"
rules:
- changes:
- "src/ServerPack/**/*"
- "nix/packages/serverpack.nix"
- local: "/src/Codex/.gitlab-ci.yml"
rules:
- changes:
- "src/Codex/**/*"
- "nix/packages/node-modules.nix"
- "nix/packages/sources.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,265 @@
# 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)
### Bug Fixes
* extract data cage interaction matrix, closing [#84](https://gitlab.com/oceanbox/Poseidon/issues/84) ([afc888a](https://gitlab.com/oceanbox/Poseidon/commit/afc888ab604f4759e3787e3feab568a322109b34))
* reintroduce active layer selector, closes [#74](https://gitlab.com/oceanbox/Poseidon/issues/74) ([c1fa85f](https://gitlab.com/oceanbox/Poseidon/commit/c1fa85fd1b955e810bd76973e5873a7ab4cb8f18))
* remove parameter limitations on particle sims ([69b380e](https://gitlab.com/oceanbox/Poseidon/commit/69b380e6659521c9f9ab489fa75d1221cc9b7db2))
### Features
* fly-to coordinate button, closes [#85](https://gitlab.com/oceanbox/Poseidon/issues/85) ([a153238](https://gitlab.com/oceanbox/Poseidon/commit/a153238f79e2b2c87ac3553acce00bb17c1529f4))
## [1.33.11](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.10...v1.33.11) (2025-11-28)
### Bug Fixes
* **ci:** Run archivist ci on coffee ([f68b7f6](https://gitlab.com/oceanbox/Poseidon/commit/f68b7f68c8fd95d5d593702632cc2e9a7b36007a))
## [1.33.10](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.9...v1.33.10) (2025-11-28)
### Bug Fixes
* **ci:** Build on Coffee ([e04d36c](https://gitlab.com/oceanbox/Poseidon/commit/e04d36ca124693b471ac973cd4d0f39f42f0fec0))
## [1.33.9](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.8...v1.33.9) (2025-11-28)
### Bug Fixes
* **nix:** Bump hash for node_modules ([fff7913](https://gitlab.com/oceanbox/Poseidon/commit/fff7913cd5280f0732bb23bc1cb6ed5282631b90))
## [1.33.8](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.7...v1.33.8) (2025-11-28)
### Bug Fixes
* **ci:** Format .gitlab-ci.yml and move codex to nix runner ([3140c07](https://gitlab.com/oceanbox/Poseidon/commit/3140c07ad078d3b8b3082d2f86eaa73f4ba08969))
* **ci:** Remove unsed var ([759bbc6](https://gitlab.com/oceanbox/Poseidon/commit/759bbc6f60e5b364dd83653193d1501ce86c9eef))
* **ci:** Try using v4.4 ([c1be7c4](https://gitlab.com/oceanbox/Poseidon/commit/c1be7c468dd9a689eb0b1b61797841c398fadc02))
* **ci:** Use 4.4 for check ([b109dbd](https://gitlab.com/oceanbox/Poseidon/commit/b109dbdcbdfa3315e099b2edf72eb10518732c8f))
## [1.33.7](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.6...v1.33.7) (2025-11-28)
### Bug Fixes
* **ci:** Change name to codex ([6d04af6](https://gitlab.com/oceanbox/Poseidon/commit/6d04af6230b536c1cbadf9abd2b59cf13609b451))
* **ci:** Correct tag for codex pipeline ([7cf5064](https://gitlab.com/oceanbox/Poseidon/commit/7cf50641f986ab81b030e678ff7db0de83acae98))
* **ci:** Run Codex on Coffee ([dd398bd](https://gitlab.com/oceanbox/Poseidon/commit/dd398bd96b0d79c8585a3156edcd2158982744d9))
## [1.33.6](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.5...v1.33.6) (2025-11-28)
### Bug Fixes
* **maps:** Make FluentUI DatePicker popup inline; closes [#83](https://gitlab.com/oceanbox/Poseidon/issues/83) ([e3a1f56](https://gitlab.com/oceanbox/Poseidon/commit/e3a1f56b87c20ad9aada6108c92854b9d12671df))
## [1.33.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.4...v1.33.5) (2025-11-27)
### Bug Fixes
* General fixes "stats" menu ([c3c9e8e](https://gitlab.com/oceanbox/Poseidon/commit/c3c9e8e4e2b473564d1f6434a28dd934457189e9)), closes [#79](https://gitlab.com/oceanbox/Poseidon/issues/79) [#80](https://gitlab.com/oceanbox/Poseidon/issues/80) [#81](https://gitlab.com/oceanbox/Poseidon/issues/81) [#82](https://gitlab.com/oceanbox/Poseidon/issues/82)
* no more DateFlicker (thanks Simen) ([7584bf6](https://gitlab.com/oceanbox/Poseidon/commit/7584bf661f2fd1cbf95e473a3334efcd69c77392))
* remove goto today in date picker ([f23b3f1](https://gitlab.com/oceanbox/Poseidon/commit/f23b3f18213682e0b41c1db13b72e3c0bb0534b6))
* **stats:** alert banner on missing stats ([5bb2ffd](https://gitlab.com/oceanbox/Poseidon/commit/5bb2ffd67c16078854b9f86a6728958c3923626c))
## [1.33.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.3...v1.33.4) (2025-11-27)
### Bug Fixes
* **js:** Remove unused packages ([10a8c42](https://gitlab.com/oceanbox/Poseidon/commit/10a8c42319bbceebbf413d330fc72071f428e2dd))
## [1.33.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.2...v1.33.3) (2025-11-27)

View File

@@ -1 +1 @@
1.33.3
1.40.5

1256
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-V4uM+12m2nPTb9rdv1RjNheV1ipzW2gIH7/Fq+R5OJE=";
}
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,10 +18,13 @@
"vite-plugin-mkcert": "^1.17.8"
},
"dependencies": {
"@fluentui/react-components": "^9.72.9",
"@fluentui/react-datepicker-compat": "^0.6.20",
"@fluentui/react-calendar-compat": "^0.3.15",
"@fluentui/react-components": "^9.72.2",
"@fluentui/react-datepicker-compat": "^0.6.17",
"@fluentui/react-timepicker-compat": "^0.4.24",
"@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,11 +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.2
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

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,17 +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_SINGULARITY: "true"
SKIP_TESTS: "true"
include:
- project: oceanbox/gitlab-ci
ref: v4.2
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
# TODO(mrtz): Create a nix-runner
# dockerize-atlantis:
# tags:
# - saas-linux-large-amd64
dockerize-atlantis:
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

@@ -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

@@ -74,6 +74,7 @@ importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-copy.js
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-crosshairs.js"
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-target.js"
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-delete.js"
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-color-harmony.js"
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-deselect.js"
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-erase.js"
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-reorder.js"

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

@@ -496,8 +496,8 @@ type MapLayer =
| Networks -> "networks"
member x.Label() =
match x with
| Ocean -> "Ocean"
| Conc -> "Concentration"
| Ocean -> "Oceanography"
| Conc -> "Concentrations"
| SeaDistance -> "SeaDistance"
| GridCircle -> $"GridCircle"
| AzeContour -> "AzeContour"
@@ -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

@@ -22,6 +22,7 @@ let getPropCM prop =
match prop with
| Prop.Temp -> ColorMap.Ocean "thermal"
| Prop.Salt -> ColorMap.Ocean "haline"
| Prop.Dens -> ColorMap.Ocean "thermal"
| Prop.WC -> ColorMap.Ocean "curl"
| Prop.DW _ -> ColorMap.Ocean "curl"
| Prop.SedV2 -> ColorMap.Color16 "jet"
@@ -41,6 +42,7 @@ let defaultColors =
Prop.Bathy
Prop.Temp
Prop.Salt
Prop.Dens
Prop.Zeta
Prop.Speed
Prop.Conc2D

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
@@ -1863,7 +1863,7 @@ module private Deposition =
let coordsString, setCoordsString = Hook.useState ""
console.debug("[Drifters.SimControls] defaultSite :", defaultSite)
let maxSitesGroup = 20
let maxSitesGroup = 1000
let nSitesGroup =
xmodel'.selectedGroup
@@ -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)
@@ -2817,7 +2814,7 @@ let private setupMetaParams disable (maxDurationHr: float) (simulation: Simulati
Start date
</sp-field-label>
<sp-field-group horizontal style="padding-bottom: 10px;">
{FluentUI.Lit.DatePicker (startDateKey, disable, simulation.startTime, setStartT, minStartDate, maxStartDate)}
{FluentUI.Lit.DatePicker (disable, simulation.startTime, setStartT, minStartDate, maxStartDate)}
</sp-field-group>
</sp-field-group>
@@ -2826,7 +2823,7 @@ let private setupMetaParams disable (maxDurationHr: float) (simulation: Simulati
End date
</sp-field-label>
<sp-field-group horizontal style="padding-bottom: 10px;">
{FluentUI.Lit.DatePicker (endDateKey, disable, simulation.startTime.AddDays(simulation.simDays), setEndT, minEndDate, maxEndDate)}
{FluentUI.Lit.DatePicker (disable, simulation.startTime.AddDays(simulation.simDays), setEndT, minEndDate, maxEndDate)}
</sp-field-group>
</sp-field-group>
</div>
@@ -2875,14 +2872,14 @@ let private setupReleaseSites dispatch' (xmodel': DrifterModel) =
let coordsString, setCoordsString = Hook.useState ""
console.debug("[Drifters.SimControls] defaultSite :", defaultSite)
let maxSites =
match xmodel'.simulation.kind with
| DepositionSim -> 40
| WaterContactSim -> 10
| LiceSim -> 10
| VirusSim -> 10
| TransportSim -> 1
| DownwellingSim -> 1
let maxSites = 1000
// match xmodel'.simulation.kind with
// | DepositionSim -> 40
// | WaterContactSim -> 20
// | LiceSim -> 10
// | VirusSim -> 10
// | TransportSim -> 10
// | DownwellingSim -> 1
let nSites =
xmodel'.release.groups.Values
@@ -3468,7 +3465,7 @@ let private liceControls dispatch' xmodel' =
[<HookComponent>]
let private depositionControls dispatch' xmodel' =
let maxDays = (30.0, 730.0) // Max simdays for hourly/daily cohorts
let maxDays = (30.0, 10_000.0) // Max simdays for hourly/daily cohorts
html
$"""
<sp-accordion-item class="simulation-release-groups" ?open={true} label="Release groups">
@@ -3489,7 +3486,7 @@ let private depositionControls dispatch' xmodel' =
"""
let private waterContactControls dispatch' xmodel' =
let maxDays = (30.0, 180.0) // Max simdays for hourly/daily cohorts
let maxDays = (30.0, 10_000.0) // Max simdays for hourly/daily cohorts
html
$"""
<sp-accordion-item class="simulation-release-groups" ?open={true} label="Release sites">
@@ -3732,14 +3729,14 @@ let simulationControls (simType: SimType) (dispatch: Msg -> unit) (model: Model)
///////////////
// Absolute max duration
let maxDurationDays =
match xmodel'.simulation.kind with
| DepositionSim -> 731.
| WaterContactSim -> 366.
| DownwellingSim -> 366.
| LiceSim -> 366.
| VirusSim -> 366.
| TransportSim -> 30.
let maxDurationDays = 10_000.
// match xmodel'.simulation.kind with
// | DepositionSim -> 731.
// | WaterContactSim -> 366.
// | DownwellingSim -> 366.
// | LiceSim -> 366.
// | VirusSim -> 366.
// | TransportSim -> 30.
let maxDurationHr =
let buffer =
@@ -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

@@ -7,9 +7,6 @@ module private ReactLib =
open Feliz
open System
let webLightTheme: obj = import "webLightTheme" "@fluentui/react-components"
import "DayOfWeek" "@fluentui/react-calendar-compat"
import "DatePicker" "@fluentui/react-datepicker-compat"
import "FluentProvider" "@fluentui/react-components"
@@ -18,7 +15,6 @@ module private ReactLib =
[<JSX.Component>]
let DatePicker
(
componentKey: 'T,
disabled: bool,
value: DateTime,
onSelectDate: DateTime -> unit,
@@ -81,9 +77,12 @@ module private ReactLib =
JSX.html
$"""
<FluentProvider theme={webLightTheme}>
<FluentProvider theme={Lib.webLightTheme}>
<Field>
<DatePicker
firstDayOfWeek={DayOfWeek.Monday}
inlinePopup
firstDayOfWeek={1}
showGoToToday={false}
showWeekNumbers={true}
disabled={disabled}
value={internalValue}
@@ -91,15 +90,11 @@ module private ReactLib =
minDate={minDate}
maxDate={maxDate}
/>
</Field>
</FluentProvider>
"""
module Lit =
open Lit
open System
let DatePicker<'T> =
React.toLit (fun (key, disabled, value, onSelectDate, minDate: DateTime, maxDate: DateTime) ->
ReactLib.DatePicker (key, disabled, value, onSelectDate, minDate, maxDate)
|> Lib.React.fromJsx
)
let DatePicker = React.toLit (ReactLib.DatePicker >> Lib.React.fromJsx)

View File

@@ -72,4 +72,4 @@ module private ReactLib =
module Lit =
open Lit
let PeriodCalendar<'T> = React.toLit (ReactLib.PeriodCalendar >> Lib.React.fromJsx)
let PeriodCalendar<'T> = React.toLit (ReactLib.PeriodCalendar >> Lib.React.fromJsx)

View File

@@ -1,61 +0,0 @@
namespace FluentUI
module private ReactLib =
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open System
import "TimePicker" "@fluentui/react-timepicker-compat"
import "FluentProvider" "@fluentui/react-components"
import "Field" "@fluentui/react-components"
[<JSX.Component>]
let TimePicker
(
key: 'T,
fieldLabel: string,
value: DateTime,
onSelectTime: DateTime -> unit
) =
let handleSelectTime =
React.useCallback (
(fun (ev: obj) (data: obj) ->
console.debug ("[FluentUI] TimePicker event data: %o", data)
let timeStr = data?selectedTime
if not (isNull timeStr) then
console.debug ("[FluentUI] TimePicker selectedTime: %s", unbox timeStr)
try
let timeSpan = TimeSpan.Parse(unbox timeStr)
let newDateTime = DateTime(value.Year, value.Month, value.Day, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds)
onSelectTime newDateTime
with ex ->
console.error ("[FluentUI] TimePicker parse error:", ex)
),
[| box onSelectTime; box value |]
)
let timeValue = value.ToString("HH:mm")
React.useEffectOnce (fun () ->
console.debug ("[FluentUI] Mounting TimePicker with key: %O, label: %s, value: %s", key, fieldLabel, timeValue)
)
JSX.html
$"""
<FluentProvider theme={Lib.webLightTheme}>
<Field label={fieldLabel} >
<TimePicker
key={key}
selectedTime={timeValue}
onTimeChange={handleSelectTime}
/>
</Field>
</FluentProvider>
"""
module Lit =
open Lit
let TimePicker<'T> = React.toLit (ReactLib.TimePicker >> Lib.React.fromJsx)

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>
@@ -1372,10 +1377,13 @@ let activeLayerSelector (dispatch: Msg -> unit) (model: Model) =
|> Some
else
None)
html
$"""
<sp-radio-group vertical selected="{string model.activeLayer}">
{layers}
</sp-radio-group>
"""
if Seq.length layers > 1 then
html
$"""
<sp-field-label for="model-color-options">Layer</sp-field-label>
<sp-radio-group vertical selected="{string model.activeLayer}">
{layers}
</sp-radio-group>
"""
else
Lit.nothing

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
}
}
@@ -1535,21 +1539,34 @@ let update cmd model =
updated, Cmd.none
| TimeSeries ->
let updated =
let exists = ProbeView.props TimeSeries |> Array.contains probing.prop
if exists then
probing
else
{ probing with prop = Prop.Temp }
let stats = Stats.Utils.fromProp probing.prop
{
model with
isLoading = None
probePoint = probing
probePoint = updated
stats.propType = stats
stats.metrics = if resetMetrics then [||] else model.stats.metrics
},
Cmd.none
| RosePlots ->
let updated =
let exists = ProbeView.props RosePlots |> Array.contains probing.prop
if exists then
probing
else
{ probing with prop = Prop.Speed }
{
model with
isLoading = None
probePoint = probing
probePoint = updated
stats.propType = Current
stats.metrics = if resetMetrics then [||] else model.stats.metrics
},
@@ -1572,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
@@ -1795,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
@@ -1873,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>
@@ -15,7 +15,6 @@
<Compile Include="FluentUI/Lib.fs" />
<Compile Include="FluentUI/Select.fs" />
<Compile Include="FluentUI/DatePicker.fs" />
<Compile Include="FluentUI/TimePicker.fs" />
<Compile Include="FluentUI/ColormapSelect.fs" />
<Compile Include="FluentUI/PeriodCalendar.fs" />
<Compile Include="Colors.fs" />
@@ -38,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

@@ -99,14 +99,14 @@ type ProbeView =
static member props view : Prop array =
match view with
| DepthProfile -> [| Prop.Temp; Prop.Salt; Prop.Speed; Prop.Dens |]
| TimeSeries -> [| Prop.Temp; Prop.Salt; Prop.Speed; Prop.Zeta |]
| TimeSeries -> [| Prop.Temp; Prop.Salt; Prop.Speed; Prop.Zeta; Prop.Dens |]
| RosePlots -> [| Prop.Speed |]
type Probing = {
point: (float * float) option
idx: GridIdx array option
view: ProbeView
// TODO: I don't think this is needed anymore
/// TODO: I don't think this is needed anymore
series: bool
timeUnit: TimeUnit
selectedDepths: Map<int, bool>
@@ -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
@@ -17,6 +20,13 @@ open Layers
open Maps
open Model
let private noStatsBanner () =
html
$"""
<sp-alert-banner ?open={true}>This archive has no available statistics</sp-alert-banner>
"""
let private accountMenu dispatch model =
let handleActionMenu (ev: Types.Event) =
match ev.target.Value with
@@ -90,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
@@ -167,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>
"""
@@ -217,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 =
@@ -232,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>
"""
@@ -306,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
@@ -324,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
$"""
@@ -368,14 +396,14 @@ let private GraphRangeSlider (model: Model) dispatch =
"""
[<HookComponent>]
let private RosePlotControls (probePoint: Probing) (stats: Stats) dispatch =
let private RosePlotControls (statsAvailable: bool) (probePoint: Probing) (stats: Stats) dispatch =
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
@@ -398,9 +426,13 @@ let private RosePlotControls (probePoint: Probing) (stats: Stats) dispatch =
<sp-accordion>
<sp-accordion-item label="Statistics" open>
{Stats.Controls.PeriodSelection stats.period handlePeriodChange}
{if statsAvailable then
Stats.Controls.PeriodSelection stats.period handlePeriodChange
else
noStatsBanner ()}
</sp-accordion-item>
</sp-accordion>
</div>
"""
@@ -411,13 +443,14 @@ let private TimeSeriesControls (model: Model) dispatch =
let selectedProp: string = model.probePoint.prop |> string
let selectedTimeUnit = string model.probePoint.timeUnit
let yearMeanSelected =
match model.stats.metrics with
| [| Mean |] -> true
| _ -> false
let handlePropChange (ev: Types.Event) =
// NOTE: Stop button group change event from propagating up to tab event handler......
// NOTE: Stop button group change event from propagating up to tab event handler...
ev.stopPropagation ()
let target = ev.target
@@ -426,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 } |> dispatch)
|> Option.iter (fun prop ->
SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange }
|> dispatch
)
let handleUnitChange (ev: Types.Event) =
ev.stopPropagation ()
@@ -439,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 _ =
@@ -499,6 +534,38 @@ 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 = (Array.isEmpty >> not) model.statsAvailable
let statsAccordion () =
html
$"""
<sp-field-label for="metric-selector">
Metrics
</sp-field-label>
<sp-field-group id="metric-selector" horizontal>
<sp-checkbox
value="year-mean"
?disabled={disableMean}
?checked={yearMeanSelected}
@change={Ev handleYearMeanChange}
>
Year mean
</sp-checkbox>
<sp-help-text slot="help-text">
Currently, only year averages are available for time series probing.
</sp-help-text>
</sp-field-group>
"""
// TODO: Download button
html
$"""
@@ -515,6 +582,9 @@ let private TimeSeriesControls (model: Model) dispatch =
<sp-action-button value="{string Prop.Salt}">
Salinity
</sp-action-button>
<sp-action-button value="{string Prop.Dens}">
Density
</sp-action-button>
<sp-action-button value="{string Prop.Speed}">
Speed
</sp-action-button>
@@ -526,29 +596,18 @@ let private TimeSeriesControls (model: Model) dispatch =
<div class="flex-row" style="gap: 24px;">
{timeSeriesSelectors ()}
{if model.probePoint.selectedDepths.IsEmpty then
Lit.nothing
{if showDepth then
PropertyPlots.DepthSelectors false model.probePoint.selectedDepths handleDepthsChange
else
PropertyPlots.DepthSelectors false model.probePoint.selectedDepths handleDepthsChange}
Lit.nothing}
</div>
<sp-accordion>
<sp-accordion-item label="Statistics">
<sp-field-label for="metric-selector">
Metrics
</sp-field-label>
<sp-field-group id="metric-selector" horizontal>
<sp-checkbox
value="year-mean"
?checked={yearMeanSelected}
@change={Ev handleYearMeanChange}
>
Year mean
</sp-checkbox>
<sp-help-text slot="help-text">
Currently, only year averages are available for time series probing.
</sp-help-text>
</sp-field-group>
{if statsAvailable then
statsAccordion ()
else
noStatsBanner ()}
</sp-accordion-item>
</sp-accordion>
</div>
@@ -561,21 +620,18 @@ let private OceanPlotControls model dispatch =
let selectedProp: string = model.probePoint.prop |> string
let handlePropChange (ev: Types.Event) =
// NOTE: Stop button group change event from propagating up to tab event handler......
// NOTE: Stop button group change event from propagating up to tab event handler...
ev.stopPropagation ()
let target = ev.target
let selected: string array = target?selected
console.debug ("[PropetryPlots] Selected Prop changed %o", selected)
let propOpt = selected |> Array.tryHead |> Option.map Prop.fromString
if propOpt.IsSome then
let prop = propOpt.Value
SetProbing {
model.probePoint with
prop = prop
propRange = prop.viewRange
}
propOpt
|> Option.iter (fun prop ->
SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange }
|> dispatch
)
html
$"""
@@ -592,45 +648,41 @@ let private OceanPlotControls model dispatch =
<sp-action-button value="{string Prop.Salt}">
Salinity
</sp-action-button>
<sp-action-button value="{string Prop.Speed}">
Speed
</sp-action-button>
<sp-action-button value="{string Prop.Dens}">
Density
</sp-action-button>
<sp-action-button value="{string Prop.Speed}">
Speed
</sp-action-button>
</sp-action-group>
{GraphRangeSlider 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>
{PropertyPlots.dlButton model.archive.id model.frame model.probePoint}
</div>
"""
[<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
$"""
@@ -734,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
@@ -765,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
@@ -847,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
@@ -899,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 ->
@@ -914,7 +969,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
<sp-tab label="Rose plots" value="3"></sp-tab>
<sp-tab-panel value="1">{OceanPlotControls model dispatch}</sp-tab-panel>
<sp-tab-panel value="2">{TimeSeriesControls model dispatch}</sp-tab-panel>
<sp-tab-panel value="3">{RosePlotControls model.probePoint model.stats dispatch}</sp-tab-panel>
<sp-tab-panel value="3">{RosePlotControls statsAvailable model.probePoint model.stats dispatch}</sp-tab-panel>
</sp-tabs>
"""
@@ -924,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 ()
@@ -932,6 +988,8 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
let metric = model.stats.metrics |> Array.tryHead |> Option.defaultValue Mean
if model.stats.showInstant then
Lit.nothing
elif not statsAvailable then
noStatsBanner ()
else
html
$"""
@@ -1009,7 +1067,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
</sp-tabs>
</div>
{stats () (* TODO: if statsAvailable then stats () else divider () *)}
{stats ()}
{noAnalysisText ()}
@@ -1023,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 _
@@ -1058,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 _
@@ -1072,6 +1132,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
| ColorControls ->
html
$"""
{activeLayerSelector dispatch model}
{colorAccordion dispatch model}
<sp-accordion size="s" allow-multiple>
{colormapAccordion dispatch model}
@@ -1084,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 =
@@ -1184,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
@@ -1253,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

@@ -389,6 +389,7 @@ let toTimeSeries3D (t0: DateTime) (freq: int) (first: int, last: int, stride) (d
)
nt
data
|> Array.sortByDescending fst
|> Array.map (fun (d, ys) -> {
PlotData.empty with
x = ts |> Array.map float
@@ -616,7 +617,7 @@ module ReactLib =
"""
[<JSX.Component>]
let HeatmapPlot (input: HeatmapInput) =
let HeatmapPlot (showLabels, input) =
let network = input.network
let particleFilter = input.particleFilter
let points = network.matrix
@@ -655,9 +656,11 @@ module ReactLib =
filteredPoints |> Array.map (fun (_, type', _) -> type'.name)
let style = {| minHeight = "416px"; width = "100%" |}
let config = {| responsive = true; editable = false |}
let xlabel = if showLabels then "Receiver" else ""
let ylabel = if showLabels then "Sender" else ""
let layout = {|
xaxis = {| side = "top"; title = {| text = "Receiver"; standoff = 120 |} |}
yaxis = {| autorange = "reversed"; title = {| text = "Sender"; standoff = 120 |} |}
xaxis = {| side = "top"; title = {| text = xlabel; standoff = 120 |} |}
yaxis = {| autorange = "reversed"; title = {| text = ylabel; standoff = 120 |} |}
margin = {| t = 100; b = 50; l = 100; r = 50 |}
|}
let traces = newHeatMap siteNames siteNames weights
@@ -719,8 +722,8 @@ module ReactLib =
editable = false
|}
let layout = {|
xaxis = {| side = "top"; title = {| text = "Receiver"; standoff = 120 |} |}
yaxis = {| title = {| text = "Sender"; standoff = 120 |} |}
xaxis = {| side = "top"; title = {| standoff = 120 |} |}
yaxis = {| title = {| standoff = 120 |} |}
margin = {| t = 100; b = 50; l = 100; r = 50 |}
|}
let traces = newCageHeatMap groupNames' cageNames weights'

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

@@ -26,6 +26,7 @@ let private fetchSeries3D
| Prop.Temp -> fvcom.TimeSeries.GetTemp aid frames vidx
| Prop.Salt -> fvcom.TimeSeries.GetSalinity aid frames vidx
| Prop.Speed -> fvcom.TimeSeries.GetSpeed aid frames vidx
| Prop.Dens -> fvcom.TimeSeries.GetDensity aid frames vidx
| _ -> fun _ -> async.Return [||]
let! x = Async.StartAsPromise (f idx)
@@ -63,6 +64,7 @@ let private fetchStatSeries (archiveId: System.Guid) (propType: StatProp) (metri
| Temperature -> stats.FvStatsSeries.GetTemperature archiveId
| Salinity -> stats.FvStatsSeries.GetSalinity archiveId
| Speed -> stats.FvStatsSeries.GetSpeed archiveId
| Density
| U
| V
| WaterTransport
@@ -128,6 +130,7 @@ let fetchSeries frame archive probePoint gridIdx depthIndex : JS.Promise<single
match probePoint.prop with
| Prop.Temp
| Prop.Salt
| Prop.Dens
| Prop.Speed -> fetchSeries3D frame archive probePoint.prop probePoint.timeUnit gridIdx depthIndex
| Prop.Zeta -> fetchSeries2D frame archive probePoint.prop probePoint.timeUnit gridIdx
| _ -> Promise.lift [||]
@@ -211,6 +214,7 @@ let SeriesPlot
match probePoint.prop with
| Prop.Temp
| Prop.Salt
| Prop.Dens
| Prop.Speed ->
let p = data |> Plotly.toTimeSeries3D time archive.saveFreq frames
setPlotData p

View File

@@ -828,7 +828,7 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
Start date
</sp-field-label>
<sp-field-group horizontal style="padding-bottom: 10px;">
{FluentUI.Lit.DatePicker (startDateKey, false, xmodel'.simulation.start, setStartDateTime, minStartDate, maxStartDate)}
{FluentUI.Lit.DatePicker (false, xmodel'.simulation.start, setStartDateTime, minStartDate, maxStartDate)}
</sp-field-label>
</sp-field-group>
@@ -837,7 +837,7 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
End date
</sp-field-label>
<sp-field-group horizontal style="padding-bottom: 10px;">
{FluentUI.Lit.DatePicker (endDateKey, false, xmodel'.simulation.stop, setStopDateTime, minEndDate, maxEndDate)}
{FluentUI.Lit.DatePicker (false, xmodel'.simulation.stop, setStopDateTime, minEndDate, maxEndDate)}
</sp-field-group>
</sp-field-group>
</div>

View File

@@ -2294,8 +2294,25 @@ let private makeNetworkCircle (idx: int) radius (pos: float * float) =
circle
[<HookComponent>]
let CageNetworkDialogButton (downloadUrl: string) (archiveName: string) (input: Plotly.HeatmapInput) =
let CageNetworkDialogButton (archiveName: string) (hmi: Plotly.HeatmapInput) =
let isOpen, setOpen = Hook.useState false
let showExtractedData, setDataExtracted = Hook.useState false
let extractedData, setExtractedData = Hook.useState ""
let handleClearData () =
setDataExtracted false
setExtractedData ""
let handleExtractData () =
setDataExtracted true
hmi.network.matrix
|> Array.map (fun (_, row) ->
row
|> Array.map (fun x -> $"%.3f{x}")
|> String.concat ", "
)
|> String.concat "\n"
|> setExtractedData
Hook.useEffectOnce (fun () ->
let keyup (ev: Types.Event) =
@@ -2307,30 +2324,127 @@ let CageNetworkDialogButton (downloadUrl: string) (archiveName: string) (input:
Hook.createDisposable (fun () -> document.removeEventListener("keyup", keyup)))
let extractedTextfield () =
html
$"""
<textarea
class="textarea grow"
style="min-height: 500px; min-width: 1250px"
readonly
>{extractedData}</textarea>
"""
let cancelButton () =
html
$"""
<sp-action-button
static="primary"
@click={Ev(fun _ -> handleClearData ())}
>
Show heatmap
</sp-action-button>
"""
let extractButton () =
html
$"""
<sp-action-button
static="primary"
@click={Ev(fun _ -> handleExtractData ())}
>
Show data
</sp-action-button>
"""
html
$"""
<overlay-trigger type="modal">
<overlay-trigger type="modal" class="flex-row">
<sp-dialog-base slot="click-content" underlay responsive dismissable>
<sp-dialog size="l" style="min-width: 1024px;" dismissable>
<sp-dialog size="l" style="min-height: 800px; min-width: 1400px;" dismissable>
<h2 slot="heading">Connection matrix for {archiveName}</h2>
{Plotly.CageInteractionHeatmap input}
<a slot="footer" href="{downloadUrl}">Download (CSV)</a>
{if showExtractedData then extractedTextfield() else Plotly.CageInteractionHeatmap hmi}
<div slot="footer">
{if showExtractedData then cancelButton () else extractButton ()}
</div>
</sp-dialog>
</sp-dialog-base>
<sp-action-button
id="heatmap-fullscreen-button"
slot="trigger"
size="s"
>
<sp-icon-maximize slot="icon"></sp-icon-maximize>
<sp-tooltip self-managed placement="top">Fullscreen chart</sp-tooltip>
style="flex-grow: 1; min-width: 150px"
> Compute matrix
<sp-icon-color-harmony slot="icon"></sp-icon-color-harmony>
</sp-action-button>
</overlay-trigger>
"""
[<HookComponent>]
let NetworkDialogButton (downloadUrl: string) (archiveName: string) color network particleFilter =
let NetworkDialogButton (downloadUrl: string) (archiveName: string) (hmi: Plotly.HeatmapInput) =
let isOpen, setOpen = Hook.useState false
let showExtractedData, setDataExtracted = Hook.useState false
let extractedData, setExtractedData = Hook.useState ""
let handleClearData () =
setDataExtracted false
setExtractedData ""
let handleExtractData () =
setDataExtracted true
hmi.network.matrix
|> Array.map (fun (_, row) ->
row
|> Array.map (fun x -> $"%.3f{x}")
|> String.concat ", "
)
|> String.concat "\n"
|> setExtractedData
let extractedTextfield () =
html
$"""
<textarea
class="textarea grow"
style="min-height: 500px; min-width: 1250px"
readonly
>{extractedData}</textarea>
"""
let cancelButton () =
html
$"""
<sp-action-button
static="primary"
@click={Ev(fun _ -> handleClearData ())}
>
Show heatmap
</sp-action-button>
"""
let extractButton () =
html
$"""
<sp-action-button
static="primary"
@click={Ev(fun _ -> handleExtractData ())}
>
Show data
</sp-action-button>
"""
let downloadButton url =
html
$"""
<a href="{url}">
<sp-action-button
size="m"
style="width: 35px;"
>
<sp-icon-download slot="icon"></sp-icon-download>
<sp-tooltip placement="top" self-managed>Download (zip)</sp-tooltip>
</sp-action-button>
</a>
"""
Hook.useEffectOnce (fun () ->
let keyup (ev: Types.Event) =
@@ -2348,10 +2462,13 @@ let NetworkDialogButton (downloadUrl: string) (archiveName: string) color networ
$"""
<overlay-trigger type="modal">
<sp-dialog-base slot="click-content" underlay responsive dismissable>
<sp-dialog size="l" style="min-width: 1024px;" dismissable>
<sp-dialog size="l" style="min-height: 800px; min-width: 1400px;" dismissable>
<h2 slot="heading">Connection matrix for {archiveName}</h2>
{Plotly.WaterContactHeatmap { color = color; network = network; particleFilter = particleFilter }}
<a slot="footer" href="{downloadUrl}">Download (CSV)</a>
{if showExtractedData then extractedTextfield() else Plotly.WaterContactHeatmap (true, hmi)}
<div slot="footer">
{downloadButton downloadUrl}
{if showExtractedData then cancelButton () else extractButton ()}
</div>
</sp-dialog>
</sp-dialog-base>
<sp-action-button
@@ -2576,6 +2693,7 @@ let fieldsControlsWC (dispatch: Msg -> unit) (model: Model) postdrift =
Lit.nothing
else
let url = waterContactDownloadUrl model postdrift
let hmi : Plotly.HeatmapInput = { color = color; network = model.infectionNetwork; particleFilter = model.particleFilter }
html
$"""
<div>
@@ -2584,10 +2702,10 @@ let fieldsControlsWC (dispatch: Msg -> unit) (model: Model) postdrift =
<sp-icon-info></sp-icon-info>
<sp-help-text size="s">Sources in rows and receiving sites in columns</sp-help-text>
</div>
{NetworkDialogButton url postdrift.Archive.name color model.infectionNetwork filter}
{NetworkDialogButton url postdrift.Archive.name hmi}
</div>
{Plotly.WaterContactHeatmap { color = color; particleFilter = filter; network = model.infectionNetwork }}
{Plotly.WaterContactHeatmap (false, hmi)}
</div>
"""
@@ -2804,18 +2922,6 @@ let fieldsControlsSedV2 (dispatch: Msg -> unit) (model: Model) (layer: MapLayer)
// toggleAllKinds
// (dimap Some >> SelectNetworkSite >> dispatch)
let downloadMatrix model postdrift =
"foo"
// // let sorcererUrl = model.dataSvc
// // let aid = string postdrift.Archive.archiveId
// // let field = viewProp.FieldKind.ToString()
// // let state = AnyState.ToString()
// // let kind = filter.availableParticleTypes.Head.kind.ToInt()
// // let frame = Drifters.calcDriftersFrame model.archive postdrift model.frame
// // let zipName = $"{field}-matrix.zip"
// // let urlStart = $"%s{sorcererUrl}/download/connection"
// // urlStart + $"/%s{aid}/%i{frame}/%s{field}/%i{kind}/%s{state}/%s{zipName}"
let color =
let prop = Colors.getProp Conc model.glLayers
Map.tryFind prop.PropType model.propColors
@@ -2835,21 +2941,16 @@ let fieldsControlsSedV2 (dispatch: Msg -> unit) (model: Model) (layer: MapLayer)
</div>
"""
else
let url = downloadMatrix model postdrift
let hmi : Plotly.HeatmapInput = { color = color; particleFilter = filter; network = model.infectionNetwork }
html
$"""
<div>
<div class="flex-row">
<div class="grow">
{CageNetworkDialogButton postdrift.Archive.name hmi }
<div class="flex-row grow" style="gap: 8px; align-items: center;">
<sp-icon-info></sp-icon-info>
<sp-help-text size="s">Contributions to cage centers</sp-help-text>
<sp-help-text size="s">Contributions to the center point of each cage</sp-help-text>
</div>
{CageNetworkDialogButton url postdrift.Archive.name hmi }
</div>
{Plotly.CageInteractionHeatmap hmi }
</div>
"""
let downloadAze =

View File

@@ -34,6 +34,7 @@ let fetchDepthProfileData (archiveId: System.Guid) (period: Period) (metric: Sta
| Temperature -> stats.FvStatsByIndex.GetTemperature archiveId period
| Salinity -> stats.FvStatsByIndex.GetSalinity archiveId period
| Speed -> stats.FvStatsByIndex.GetSpeed archiveId period
| Density
| U
| V
| WaterTransport

View File

@@ -75,7 +75,7 @@ let getInteraction (map: OlMap) interactionId =
:?> Draw
[<HookComponent>]
let probeButton (model: Model) (disabled: bool) (selected: string option) (handleProbe: Event.MapBrowserEvent -> unit) =
let probeButton (model: Model) (show: bool) (selected: string option) (handleProbe: Event.MapBrowserEvent -> unit) =
let clickKey = Hook.useRef None
let cropMode = model.cropCoords.Length > 0
@@ -90,7 +90,7 @@ let probeButton (model: Model) (disabled: bool) (selected: string option) (handl
| _ -> false
let isSelected = selected = Some "probe"
let isDisabled = disabled || otherSelected || cropMode || plotMode
let isDisabled = otherSelected || cropMode || plotMode
// NOTE(simkir): If this button gets unmounted, the placing effect listener does not run its dispose function,
// therefore we have to here save the key, and dispose it on unmount. Terrible, I know :(
@@ -105,20 +105,23 @@ let probeButton (model: Model) (disabled: bool) (selected: string option) (handl
// Remove eventlistener based on the probing state
Hook.useEffectOnChange (selected, fun s -> Maps.crossHairSelect model.map clickKey handleProbe (s = Some "probe"))
html
$"""
<sp-action-button
size="m"
value="probe"
?selected={isSelected}
?disabled={isDisabled}
>
<sp-icon-sampler slot="icon"></sp-icon-sampler>
</sp-action-button>
"""
if show then
html
$"""
<sp-action-button
size="m"
value="probe"
?selected={isSelected}
?disabled={isDisabled}
>
<sp-icon-sampler slot="icon"></sp-icon-sampler>
</sp-action-button>
"""
else
Lit.nothing
[<HookComponent>]
let measureButton (model: Model) dispatch (selected: string option) setSelected handlePlot handleMeasure =
let measureButton (model: Model) dispatch (show: bool) (selected: string option) setSelected handlePlot handleMeasure =
let measure = Hook.useRef<Draw> (getInteraction model.map "measureInteraction")
let archiveId = Hook.useRef<System.Guid> model.archive.id
let lineCoords = Hook.useRef<(float * float) array> Array.empty
@@ -261,20 +264,23 @@ let measureButton (model: Model) dispatch (selected: string option) setSelected
reset ()
)
html
$"""
<sp-action-button
size="m"
value="measure"
?disabled={isDisabled}
?selected={isSelected}
>
<sp-icon-measure slot="icon"></sp-icon-measure>
</sp-action-button>
"""
if show then
html
$"""
<sp-action-button
size="m"
value="measure"
?disabled={isDisabled}
?selected={isSelected}
>
<sp-icon-measure slot="icon"></sp-icon-measure>
</sp-action-button>
"""
else
Lit.nothing
[<HookComponent>]
let circleButton (model: Model) dispatch (selected: string option) handleCircle =
let circleButton (model: Model) dispatch (show: bool) (selected: string option) handleCircle =
let clickKey = Hook.useRef None
let cropMode = model.cropCoords.Length > 0
let plotMode =
@@ -311,20 +317,76 @@ let circleButton (model: Model) dispatch (selected: string option) handleCircle
None |> SetGridCircle |> dispatch
)
html
$"""
<sp-action-button
size="m"
value="circle"
?disabled={isDisabled}
?selected={isSelected}
>
<sp-icon-circle slot="icon"></sp-icon-circle>
</sp-action-button>
"""
if show then
html
$"""
<sp-action-button
size="m"
value="circle"
?disabled={isDisabled}
?selected={isSelected}
>
<sp-icon-circle slot="icon"></sp-icon-circle>
</sp-action-button>
"""
else
Lit.nothing
[<HookComponent>]
let cropButton (model: Model) dispatch (selected: string option) handleCrop =
let locateButton (model: Model) (show: bool) (selected: string option) handleClear handleLocate =
let clickKey = Hook.useRef None
let cropMode = model.cropCoords.Length > 0
let plotMode =
model.probePoint.point.IsSome
|| model.probePoint.idx.IsSome
|| model.pickLine.Length > 0
let otherSelected =
match selected with
| Some "locate" -> false
| Some _ -> true
| _ -> false
let isSelected = selected = Some "locate"
let isDisabled = otherSelected || cropMode || plotMode
// NOTE(simkir): If this button gets unmounted, the placing effect listener does not run its dispose function,
// therefore we have to here save the key, and dispose it on unmount. Terrible, I know :(
Hook.useEffectOnce (fun () ->
Hook.createDisposable (fun () ->
let elem = model.map.getTargetElement ()
do elem?style?cursor <- ""
do clickKey.contents |> Option.iter Observable.unByKey
)
)
// Remove eventlistener based on the probing state
Hook.useEffectOnChange (
selected,
fun s ->
match s with
| Some "locate" -> Maps.crossHairSelect model.map clickKey handleLocate true |> ignore
| _ ->
handleClear ()
clickKey.contents |> Option.iter Observable.unByKey
)
if show then
html
$"""
<sp-action-button
size="m"
value="locate"
?disabled={isDisabled}
?selected={isSelected}
>
<sp-icon-location slot="icon"></sp-icon-location>
</sp-action-button>
"""
else
Lit.nothing
[<HookComponent>]
let cropButton (model: Model) dispatch (show: bool) (selected: string option) handleCrop =
let dragBoxEventKey = Hook.useRef<Event.EventsKey option> None
let cropMode = model.cropCoords.Length > 0
@@ -390,18 +452,21 @@ let cropButton (model: Model) dispatch (selected: string option) handleCrop =
)
)
html
$"""
<sp-action-button
id="crop-button"
value="crop"
size="m"
?disabled={isDisabled}
?selected={isSelected}
>
<sp-icon-crop slot="icon"></sp-icon-crop>
</sp-action-button>
"""
if show then
html
$"""
<sp-action-button
id="crop-button"
value="crop"
size="m"
?disabled={isDisabled}
?selected={isSelected}
>
<sp-icon-crop slot="icon"></sp-icon-crop>
</sp-action-button>
"""
else
Lit.nothing
// TODO: Why is this is Probing?:w
/// General purpose back button
@@ -461,10 +526,13 @@ let backButton model dispatch =
let measures (model: Model) dispatch =
let selected, setSelected = Hook.useState<string option> None
let radius = Hook.useRef<float> 10_000
let lonlat, setLonlat = Hook.useState<string> ""
let showProbe = true
let showMeasure = model.selectedPostdrift.IsNone || model.selectedDrifters.IsNone
let showCircle = model.selectedPostdrift.IsNone || model.selectedDrifters.IsNone
let collaps = match selected with | Some "circle" | Some "locate" -> true | _ -> false
let showProbe = not collaps && model.selectedDrifters.IsNone
let showMeasure = not collaps && model.selectedDrifters.IsNone
let showCircle = (selected <> Some "locate") && model.selectedDrifters.IsNone
let showLocate = (selected <> Some "circle") && model.selectedDrifters.IsNone
let showCrop = false
let handleProbeMapClick (ev: Event.MapBrowserEvent) =
@@ -494,6 +562,47 @@ let measures (model: Model) dispatch =
|> SetGridCircle
|> dispatch
let readCoord (line: string) : (float * float) option =
line.Split([| '\t'; ' '; ','; ';' |])
|> Array.filter ((<>) "")
|> function
| [| lat; lon |] -> tryParseFloat lat, tryParseFloat lon
| _ -> None, None
|> function
| Some lon, Some lat ->
(lon, lat)
|> toEpsg3857'
|> Some
| _ -> None
let handleLocateAccept lonlat =
lonlat
|> readCoord
|> Option.map (fun (x, y) ->
[| { SampleData.empty with name = ""; radius = 25.0; coord = [| x; y |] } |]
|> SetPointSamples
|> dispatch
setLayerVisible model.map MapLayer.PointSamples true
Maps.flyTo model.map 14 [| x; y |]
)
|> Option.defaultValue ()
let handleLocateMapClick (ev: Event.MapBrowserEvent) =
let x, y = ev.coordinate[0], ev.coordinate[1]
let ll = (x, y) |> toWgs84' |> fun (lon, lat) -> $"%.6f{lon}, %.6f{lat}"
setLonlat ll
handleLocateAccept ll
let handleLocateClear () =
Array.empty |> SetPointSamples |> dispatch
setLayerVisible model.map MapLayer.PointSamples false
setLonlat ""
let setLonLat' (ll: string) =
setLonlat ll
handleLocateAccept ll
let setRadius (r: float) =
radius.contents <- r
model.gridCircle
@@ -523,12 +632,33 @@ let measures (model: Model) dispatch =
else
setSelected (Some newSelected[0])
let locateField () =
html
$"""
<div class="flex-row gap-8">
<sp-textfield
size="m"
id="lonlat-field"
style="flex-grow: 1; padding-left: 10px;"
value="{lonlat}"
placeholder="Lon, Lat (decimal degrees)"
@change={EvVal(unbox >> setLonLat')}
></sp-textfield>
<sp-action-button
style="flex-grow: 1; padding-right: 10px; "
?disabled={lonlat.Length = 0}
@click="{Ev (fun _ -> handleLocateAccept lonlat)}"
>
<sp-icon-search slot="icon"></sp-icon-search>
</sp-action-button>
</div>
"""
let radiusField () =
html
$"""
<sp-field-label style="flex-grow: 1;">
Radius
<sp-field-label style="flex-grow: 1; padding-left: 50px">
Radius (m)
</sp-field-label>
<sp-number-field
size="m"
@@ -536,7 +666,7 @@ let measures (model: Model) dispatch =
format-options="{formatDigits 0 0}"
min={0.0} max={1_000_000.0}
step="100"
style="flex-grow: 1; padding-right: 15px"
style="flex-grow: 1; padding-right: 20px"
value="{radius.Value}"
@mousedown={Ev(fun e -> e.stopPropagation ())}
@change={EvVal(unbox >> setRadius)}
@@ -554,17 +684,19 @@ let measures (model: Model) dispatch =
.selected={currentSelected}
@change={Ev handleMeasureChange}
>
{if showProbe then probeButton model false selected handleProbeMapClick else Lit.nothing}
{if showMeasure then measureButton model dispatch selected setSelected handlePlotAccept handleMeasureAccept else Lit.nothing}
{if showCircle then circleButton model dispatch selected handleCircleMapClick else Lit.nothing}
{if showCrop then cropButton model dispatch selected handleCrop else Lit.nothing}
{probeButton model showProbe selected handleProbeMapClick}
{measureButton model dispatch showMeasure selected setSelected handlePlotAccept handleMeasureAccept}
{circleButton model dispatch showCircle selected handleCircleMapClick}
{locateButton model showLocate selected handleLocateClear handleLocateMapClick}
{cropButton model dispatch showCrop selected handleCrop}
</sp-action-group>
{match selected with | Some "circle" -> radiusField () | _ -> Lit.nothing }
{match selected with
| Some "circle" -> radiusField ()
| Some "locate" -> locateField ()
| _ -> Lit.nothing}
<sp-action-group class="measures-button-group">
{Drifters.driftersInputModal model (CloneDriftersInput >> dispatch)}
</sp-action-group>
{if selected <> Some "locate" then Drifters.driftersInputModal model (CloneDriftersInput >> dispatch) else Lit.nothing}
</div>
<sp-divider size="s"></sp-divider>

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,51 +142,41 @@ let View (dispatch: Model.Msg -> unit) (model: Model.Model) =
let handlePeriodChange (newPeriod: Period) =
Model.SetStatPeriod newPeriod |> dispatch
Hook.useEffectOnce (fun () ->
let statsApi = StatsApi (getDataUrl ())
if noStats then
html
$"""
<div class="stats-nav-container">
<sp-alert-banner ?open={noStats}>This archive has no available statistics</sp-alert-banner>
</div>
"""
else
html
$"""
<div class="stats-nav-container">
<div>
<div class="flex-row flex-align-center gap-16">
<div class="grow">
{PeriodSelection model.stats.period handlePeriodChange}
</div>
console.info("[Stats] Mounting statsControl for archive %s", model.archive.name)
<overlay-trigger triggered-by="hover">
<sp-action-button
slot="trigger"
size="m"
href={Lit.ifSome statsHrefOpt}
target="_blank"
?disabled={statsHrefOpt.IsNone}
>
<sp-icon-download slot="icon"></sp-icon-download>
</sp-action-button>
match model.statsAvailable with
| Some stats -> ()
| None ->
statsApi.FvStatsInfo.GetAvailableStats model.archive.id
|> Async.StartAsPromise
|> Promise.iter (fun avail ->
Model.SetStatsAvailable avail
|> dispatch
)
)
html
$"""
<div class="stats-nav-container">
<sp-alert-banner ?open={noStats}>This archive has no available statistics</sp-alert-banner>
<div>
<div class="flex-row flex-align-center gap-16">
<div class="grow">
{PeriodSelection model.stats.period handlePeriodChange}
<sp-tooltip slot="hover-content">
Download a bundle of pre-processed statistics at the probing point.
</sp-tooltip>
</overlay-trigger>
</div>
<overlay-trigger triggered-by="hover">
<sp-action-button
slot="trigger"
size="m"
href={Lit.ifSome statsHrefOpt}
target="_blank"
?disabled={statsHrefOpt.IsNone}
>
<sp-icon-download slot="icon"></sp-icon-download>
</sp-action-button>
<sp-tooltip slot="hover-content">
Download a bundle of pre-processed statistics at the probing point.
</sp-tooltip>
</overlay-trigger>
{MetricsAccordion model.stats.showInstant model.stats.metrics handleMetricsChange}
</div>
{MetricsAccordion model.stats.showInstant model.stats.metrics handleMetricsChange}
</div>
</div>
"""
"""

View File

@@ -23,6 +23,7 @@ let private fetchSeries (archiveId: System.Guid) (propType: StatProp) (metric: S
| Temperature -> stats.FvStatsSeries.GetTemperature archiveId
| Salinity -> stats.FvStatsSeries.GetSalinity archiveId
| Speed -> stats.FvStatsSeries.GetSpeed archiveId
| Density
| U
| V
| WaterTransport
@@ -52,6 +53,8 @@ let private fetchSeries (archiveId: System.Guid) (propType: StatProp) (metric: S
)
|> Array.rev
console.debug($"[SeriesPlot] fetched dephts {depths}")
let def: Plotly.PlotData = {
Plotly.PlotData.empty with
x = Array.init 12 float

View File

@@ -25,6 +25,7 @@ let toProp (p: StatProp) : Prop =
match p with // needed for colormap and scale
| Temperature -> Prop.Temp
| Salinity -> Prop.Salt
| Density -> Prop.Dens
| Speed -> Prop.Speed
| Current
| U
@@ -37,6 +38,7 @@ let fromProp (p: Prop) : StatProp =
| Prop.Temp -> Temperature
| Prop.Salt -> Salinity
| Prop.Speed -> Speed
| Prop.Dens -> Density
| _ -> Undefined
let fetchDepthMap (archiveId: System.Guid) (coordinate: (float * float)) : JS.Promise<Map<int, bool> option> =
@@ -81,6 +83,7 @@ let fetchStatProps (archiveId: System.Guid) (stats: Model.Stats) : JS.Promise<si
| Temperature -> api.FvStatsByLayer.GetTemperature
| Salinity -> api.FvStatsByLayer.GetSalinity
| Speed -> api.FvStatsByLayer.GetSpeed
| Density
| U
| V
| Current ->

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

@@ -1,12 +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_SINGULARITY: "true"
SKIP_TESTS: "true"
include:
- project: oceanbox/gitlab-ci
ref: v4.2
file: DotnetDeployment.gitlab-ci.yml
inputs:
project-name: codex
project-dir: src/Codex
- project: oceanbox/gitlab-ci
ref: v4.5
file: DotnetDeployment.gitlab-ci.yml
inputs:
project-name: codex
project-dir: src/Codex
dockerize-codex:
tags:
- nix

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,19 +5,23 @@
}:
dockerTools.buildLayeredImage {
name = "Codex";
tag = "0.0.0-rc1";
tag = "0.0.1";
created = "now";
contents = [
server
dockerTools.caCertificates
busybox
dockerTools.binSh
];
extraCommands = ''
mkdir -p app
cp -r ${server}/lib/Codex/* app
'';
config = {
cmd = [ "Codex.Server" ];
workingDir = "/lib/Codex";
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"
]
]

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