Compare commits

...

91 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
121 changed files with 4328 additions and 2460 deletions

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

38
.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,36 +13,4 @@ dotenv_if_exists
use nix
# HACK: Workaround for direnv bug
unset TMP TMPDIR TEMP TEMPDIR
export NPINS_DIRECTORY="nix"
export APP_ENV=$USER
# 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

@@ -12,8 +12,6 @@ include:
file: template/Base.gitlab-ci.yml
- local: "/src/Atlantis/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Atlantis/**/*"
- "nix/packages/atlantis.nix"
@@ -21,44 +19,32 @@ include:
- "nix/containers.nix"
- local: "/src/Sorcerer/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Sorcerer/**/*"
- "nix/packages/sorcerer.nix"
- "nix/containers.nix"
- local: "/src/Archivist/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Archivist/**/*"
- "nix/packages/archivist.nix"
- local: "/src/Interfaces/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Interfaces/**/*"
- "nix/packages/api.nix"
- local: "/src/DataAgent/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/DataAgent/**/*"
- "nix/packages/dataagent.nix"
- local: "/src/ServerPack/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/ServerPack/**/*"
- "nix/packages/serverpack.nix"
- local: "/src/Codex/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Codex/**/*"
- "nix/packages/node-modules.nix"

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"/>

View File

@@ -1,5 +1,130 @@
# 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)

View File

@@ -1 +1 @@
1.37.1
1.40.5

823
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"
}
}

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-9XLCFORr+StPsCLdanXATD+vmIOptvy3Xhr7O34qzZc=";
outputHash = "sha256-bbCaGoZRE7vRuAS3eRyP8yHANYXBJVaHmuL99BAovjY=";
}

View File

@@ -28,8 +28,8 @@
"nixpkgs": {
"type": "Channel",
"name": "nixpkgs-unstable",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre920722.76eec3925eb9/nixexprs.tar.xz",
"hash": "sha256-IVq6jxkcTuudaj3c78xl2xG2fZSL9gS7JMPFUl3q7j4="
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre930822.ed142ab1b3a0/nixexprs.tar.xz",
"hash": "sha256-XH6awru9NnBc/m+2YhRNT8r1PAKEiPGF3gs//F3ods0="
},
"pre-commit": {
"type": "Git",
@@ -40,9 +40,9 @@
},
"branch": "master",
"submodules": false,
"revision": "f0927703b7b1c8d97511c4116eb9b4ec6645a0fa",
"url": "https://github.com/cachix/git-hooks.nix/archive/f0927703b7b1c8d97511c4116eb9b4ec6645a0fa.tar.gz",
"hash": "sha256-6MkqajPICgugsuZ92OMoQcgSHnD6sJHwk8AxvMcIgTE="
"revision": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
"url": "https://github.com/cachix/git-hooks.nix/archive/a1ef738813b15cf8ec759bdff5761b027e3e1d23.tar.gz",
"hash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U="
}
},
"version": 7

View File

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

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

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

View File

@@ -4,12 +4,12 @@
pre-commit ? import ./nix/pre-commit.nix,
}:
let
dotnet-sdk = (with pkgs.dotnetCorePackages; combinePackages [ sdk_9_0 sdk_10_0]);
dotnet-sdk = pkgs.dotnetCorePackages.sdk_10_0;
agenix = pkgs.callPackage "${sources.agenix}/pkgs/agenix.nix" { };
fable = pkgs.buildDotnetGlobalTool {
pname = "fable";
version = "4.26.0";
nugetHash = "sha256-nhIGVwu6kHTW+t0hiD1Pha3+ErE5xACBrVDgFP6qMnc=";
version = "4.24.0";
nugetHash = "sha256-ERewWqfEyyZKpHFFALpMGJT0fDWywBYY5buU/wTZZTg=";
};
in
pkgs.mkShellNoCC {
@@ -24,7 +24,7 @@ pkgs.mkShellNoCC {
# JavaScript
pkgs.bun
pkgs.nodejs
pkgs.nodejs_25
# Devlopment tools
pkgs.npins
@@ -32,6 +32,7 @@ pkgs.mkShellNoCC {
pkgs.dive
pkgs.nix-output-monitor
pkgs.just
pkgs.skopeo
# Secret management with agenix
agenix
@@ -47,6 +48,10 @@ pkgs.mkShellNoCC {
DOTNET_ROOT = "${dotnet-sdk}/share/dotnet";
LOG_LEVEL = "verbose";
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;
@@ -73,4 +78,4 @@ pkgs.mkShellNoCC {
'';
};
};
}
}

View File

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

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

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

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

@@ -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',
],

View File

@@ -14,7 +14,7 @@ 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 := "vite -c ../../vite.config.js"
vite := "bunx vite -c ../../vite.config.js -m development "
# Default recipe - show available commands
default:
@@ -67,7 +67,7 @@ run-server:
# Run client only in watch mode
[working-directory: 'src/Client']
run-client: install-client
fable watch -e .jsx -o build --run {{vite}}
fable watch -e .jsx -o build --test:MSBuildCracker --run {{vite}}
# Format code with Fantomas
format:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,12 +46,9 @@ let private update (msg: XtractMsg) (model: XtractModel) =
{ model with position = pos }, Elmish.Cmd.none
| SetData s ->
console.debug ("[DataExtraction] SetData msg:", s)
let data' = { model.data with name = s.name; fvcom = s.fvcom;}
{ model with data = data' }, 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
{ 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
//
@@ -105,7 +102,8 @@ let updateExtractionSite (posOpt: (float * float) option, map) =
match posOpt with
| Some pos ->
let p' = pos |> posToCoord
let point = Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
let point =
Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
let feature = Feature.feature [ feature.geometryOrProperties point ]
source.addFeature (feature)
| _ -> ()
@@ -124,28 +122,19 @@ let private extractionSiteControls (dispatch': XtractMsg -> unit) (xmodel': Xtra
if circle.intersectsCoordinate (pos |> posToCoord) then
Some pos
else
console.error("[DataExtraction] Trying to place extraction point outside of fencing radius")
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'
coordToPos coords |> tryFence |> SetExtractionSite |> dispatch'
let setPosition (pos: float * float) : unit =
Some pos
|> SetExtractionSite
|> dispatch'
Some pos |> SetExtractionSite |> dispatch'
let selectedPos =
xmodel'.position
|> Option.defaultValue (0.0, 0.0)
|> toWgs84'
let selectedPos = xmodel'.position |> Option.defaultValue (0.0, 0.0) |> toWgs84'
let deleteSite (_: Browser.Types.Event) =
None |> SetExtractionSite |> dispatch'
let deleteSite (_: Browser.Types.Event) = None |> SetExtractionSite |> dispatch'
let latitudeBox =
let latitude = snd selectedPos
@@ -264,21 +253,22 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
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 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
}
| None -> {
XtractData.empty with
fvcom = archive.id
start = archiveStartT
stop = archiveStartT.AddDays 2.0
}
{
fence = archive.polygon
start = false, None
@@ -309,11 +299,21 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
SetXtractModel (Some newModel) |> dispatch
)
let setStartDateTime (dt: DateTime) =
SetData { xmodel'.data with start = dt } |> dispatch'
// 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) =
SetData { xmodel'.data with stop = dt } |> dispatch'
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'
@@ -322,7 +322,10 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
let minStartDate = archiveStartUTC
let maxStartDate = archiveEndT
let minEndDate = archiveStartUTC
let maxEndDate = archiveEndT
// maxEndDate is one year after the selected start date
let maxEndDate =
let startDate = xmodel'.data.start
startDate.AddYears(1)
let metaControls =
html
@@ -579,4 +582,4 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
</div>
</div>
{submitButtons}
"""
"""

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

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

View File

@@ -100,7 +100,7 @@ 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.
// 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 =
@@ -189,7 +189,7 @@ let private simAccordion (dispatch: Msg -> unit) model =
<sp-action-button
static="primary"
style="flex-grow: 1"
?disabled={disabled}
?disabled={disabledXtract }
@click={Ev (chooseMode (DataExtraction DefaultXtract))}
>
Extract Data

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

@@ -25,37 +25,37 @@
min-width: 400px;
}
.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;
}
</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) {
@@ -65,26 +65,26 @@
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 = ['❄', '❅', '❆', '❇', '❈', '❉', '❊', '❋']
<!-- <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>
<!-- 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>

View File

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

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}"

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

@@ -83,6 +83,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 = None
let part = job.partition |> Option.defaultValue "long"
let cpt = 32
task {
try
@@ -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,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>2.6.6</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -286,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 }

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

@@ -66,6 +66,7 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.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"
@@ -82,7 +83,8 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
let g = defaultArg groups [||]
let grp = if g.Length > 0 then g[0] else ""
let dep = None
let part = partition |> Option.defaultValue "long"
let part = partition |> Option.defaultValue "short"
let cpt = 4
taskResult {
let! archiveName =
@@ -155,7 +157,7 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
Positions = 1
|}
let! r = slurm.Submit (job.name, this.myId, xtractScript, env, grp, part, dep, Some comment)
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 =

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

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.36.0, )",
"Oceanbox.ServerPack": "[1.40.0, )",
"Petimeter.Api": "[1.0.0, )"
}
},
@@ -1731,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",
@@ -1799,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,8 @@
"allowedOrigins": [
"http://*.oceanbox.io",
"https://*.oceanbox.io",
"https://*.oceanbox.io:8080",
"https://*.oceanbox.io:10380",
"https://*.vtn.obx",
"https://*.tox.obx",
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,23 +29,34 @@ type OpenFGA =
[<Hook>]
static member useObjects(user: string, relation: string, objectType: string, ?context: obj) : Objects =
let objects, setObjects = React.useState<Objects> Objects.Empty
let isLoading, setLoading = React.useState true
let error, setError = React.useState<string option> None
let objects, setObjects = React.useState<string array> Array.empty
React.useEffect (
(fun () ->
setObjects { objects with Loading = true; Error = None }
setLoading true
setError None
OpenFGA.fetchObjects(user, relation, objectType, context)
|> Promise.iter (fun res ->
match res with
| Ok newObjects ->
setObjects { objects with Loading = false; Objects = newObjects }
setObjects newObjects
| Error err ->
console.error("[OpenFGA] Error loading user objects %s", err)
setObjects { objects with Loading = false; Error = Some err }
console.error("[OpenFGA] Error loading user objects: %s", err)
setError (Some err)
setLoading false
)
),
[| box user |]
)
objects
let props = {
Loading = isLoading
Error = error
Objects = objects
}
props

View File

@@ -25,32 +25,37 @@ type OpenFGA =
[<Hook>]
static member useReadTuples(?user: string, ?relation: string, ?object: string) : Tuples =
let tuples, setTuples = React.useState<Tuples> Tuples.Empty
let isLoading, setLoading = React.useState true
let error, setError = React.useState None
let tuples, setTuples = React.useState<Remoting.Tuple array> [||]
let handleSetTuples (newTuples: Remoting.Tuple array) =
setTuples { tuples with Tuples = newTuples }
console.debug("[OpenFGA] Read tuples set by outside: %o", newTuples)
setTuples newTuples
React.useEffect (
(fun () ->
setTuples { tuples with Loading = true; Error = None }
setLoading true
OpenFGA.fetchTuples(?user = user, ?relation = relation, ?object = object)
|> Promise.iter (fun res ->
match res with
| Ok resp ->
let newTuples = resp.Tuples |> Array.map _.Key
setTuples {
tuples with
Loading = false
SetTuples = handleSetTuples
Tuples = newTuples
}
setTuples newTuples
| Error err ->
console.error("[OpenFGA] Error loading user objects %s", err)
setTuples { tuples with Loading = false; Error = Some err }
setError (Some err)
setLoading false
)
),
[| box user |]
)
tuples
{
Loading = isLoading
Error = error
SetTuples = handleSetTuples
Tuples = tuples
}

View File

@@ -1,136 +1,61 @@
namespace Oceanbox.Codex
open Browser
open Fable.Core
open Fable.Remoting.Client
open Feliz
open Feliz.Router
[<Erase>]
type User =
[<ReactComponent>]
static member List(user: string, relation: string, objectType: string, ?context: obj) =
let objects = OpenFGA.useObjects(user, relation, objectType, context)
if objects.Loading then
Html.p "Loading ..."
else
if Array.isEmpty objects.Objects then
Html.p (sprintf "No objects with user %s relation %s of type %s" user relation objectType)
else
Html.ul [
prop.children (
objects.Objects
|> Array.sort
|> Array.map (fun object ->
let split = object.Split ':'
match split with
| [| objectType; id |] ->
Html.li [
prop.key id
prop.children [
Html.a [
prop.href (Router.format("archives", id))
prop.text id
]
]
]
| _ ->
Html.li [
prop.text "Invalid object format"
]
)
)
]
module User =
[<ReactComponent>]
let private DeleteForm (user: string) =
let deleted, setDeleted = React.useState<Result<unit, string> option> None
let deleting, setDeleting = React.useState false
let handleDelete =
React.useCallback (
(fun () ->
setDeleting true
console.info("[User] Deleting user %s", user)
Remoting.adminApi.removeUsers [| user |]
|> Async.StartAsPromise
|> Promise.catch (fun ex ->
match ex with
| :? ProxyRequestException as e ->
let proxyError : Types.ProxyError = JS.JSON.parse e.ResponseText |> unbox
let msg = proxyError.error.errorMsg
Error msg
| ex ->
Error ex.Message
)
|> Promise.iter (fun res ->
match res with
| Ok () ->
console.info("[User] Successfully deleted user %s", user)
setDeleted (Some (Ok ()))
| Error err ->
console.error("[User] Error deleting user %s: %s", user, err)
setDeleted (Some (Error err))
)
),
[| box user |]
)
React.fragment [
match deleted with
| Some (Ok ()) ->
Html.p "User successfully deleted."
Html.a [
prop.onClick (fun ev ->
ev.preventDefault ()
Router.navigateBack ()
)
prop.href (Router.format "")
prop.text "Back"
]
| Some (Error err) ->
Html.p (sprintf "Error deleting user: %s" err)
| None ->
if deleting then
Html.div [
prop.classes [ "flex-row-center"; "gap-8" ]
prop.children [
Html.button [
prop.onClick (fun _ -> handleDelete ())
prop.text "Are you sure?"
]
Html.button [
prop.onClick (fun _ -> setDeleting false)
prop.text "Cancel"
]
]
]
Html.p "This will delete the user from the databases. Not disable the user."
else
Html.button [
prop.onClick (fun _ -> setDeleting true)
prop.text "Delete"
]
]
[<ReactComponent>]
let View (user: string) =
let fgaUser = sprintf "user:%s" user
let execCtx = {|
time = System.DateTime.Now
task = "*"
usage = "-1"
|}
let groups = OpenFGA.useObjects(fgaUser, "member", "group")
Html.main [
Html.h1 user
Html.section [
prop.children [
DeleteForm user
Users.DeleteForm user
]
]
Html.section [
prop.children [
Html.h2 "Groups"
Html.div [
prop.children [
Html.ul [
prop.children (
groups.Objects
|> Array.map (fun group ->
let split = group.Split ':'
match split with
| [| "group"; groupName |] ->
Html.li [
prop.children [
Html.a [
prop.href (Router.format("groups", groupName))
prop.text groupName
]
]
]
| _ ->
Html.none
)
)
]
]
]
]
]
Html.section [
prop.children [
Html.h2 "Archmaester"
@@ -170,7 +95,7 @@ module User =
style.maxHeight (length.px 512)
]
prop.children [
User.List(fgaUser, "owner", "archive")
Users.OpenFgaList(fgaUser, "owner", "archive")
]
]
]
@@ -192,7 +117,7 @@ module User =
style.maxHeight (length.px 512)
]
prop.children [
User.List(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
Users.OpenFgaList(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
]
]
]
@@ -214,7 +139,7 @@ module User =
style.maxHeight (length.px 512)
]
prop.children [
User.List(fgaUser, "exec", "archive", {| time = System.DateTime.Now |})
Users.OpenFgaList(fgaUser, "exec", "archive", execCtx)
]
]
]
@@ -223,4 +148,4 @@ module User =
]
]
]
]
]

View File

@@ -0,0 +1,81 @@
namespace Oceanbox.Codex
open Browser
open Fable.Core
open Fable.Remoting.Client
open Feliz
open Feliz.Router
module Users =
[<ReactComponent>]
let DeleteForm (user: string) =
let deleted, setDeleted = React.useState<Result<unit, string> option> None
let deleting, setDeleting = React.useState false
let handleDelete =
React.useCallback (
(fun () ->
setDeleting true
console.info("[User] Deleting user %s", user)
Remoting.adminApi.removeUsers [| user |]
|> Async.StartAsPromise
|> Promise.catch (fun ex ->
match ex with
| :? ProxyRequestException as e ->
let proxyError : Types.ProxyError = JS.JSON.parse e.ResponseText |> unbox
let msg = proxyError.error.errorMsg
Error msg
| ex ->
Error ex.Message
)
|> Promise.iter (fun res ->
match res with
| Ok () ->
console.info("[User] Successfully deleted user %s", user)
setDeleted (Some (Ok ()))
| Error err ->
console.error("[User] Error deleting user %s: %s", user, err)
setDeleted (Some (Error err))
)
),
[| box user |]
)
React.fragment [
match deleted with
| Some (Ok ()) ->
Html.p "User successfully deleted."
Html.a [
prop.onClick (fun ev ->
ev.preventDefault ()
Router.navigateBack ()
)
prop.href (Router.format "")
prop.text "Back"
]
| Some (Error err) ->
Html.p (sprintf "Error deleting user: %s" err)
| None ->
if deleting then
Html.div [
prop.classes [ "flex-row-center"; "gap-8" ]
prop.children [
Html.button [
prop.onClick (fun _ -> handleDelete ())
prop.text "Are you sure?"
]
Html.button [
prop.onClick (fun _ -> setDeleting false)
prop.text "Cancel"
]
]
]
Html.p "This will delete the user from the databases. Not disable the user."
else
Html.button [
prop.onClick (fun _ -> setDeleting true)
prop.text "Delete"
]
]

View File

@@ -0,0 +1,42 @@
namespace Oceanbox.Codex
open Fable.Core
open Feliz
open Feliz.Router
[<Erase>]
type Users =
[<ReactComponent>]
static member OpenFgaList(user: string, relation: string, objectType: string, ?context: obj) =
let objects = OpenFGA.useObjects(user, relation, objectType, context)
if objects.Loading then
Html.p "Loading ..."
else
if Array.isEmpty objects.Objects then
Html.p (sprintf "No objects with user %s relation %s of type %s" user relation objectType)
else
Html.ul [
prop.children (
objects.Objects
|> Array.sort
|> Array.map (fun object ->
let split = object.Split ':'
match split with
| [| objectType; id |] ->
Html.li [
prop.key id
prop.children [
Html.a [
prop.href (Router.format("archives", id))
prop.text id
]
]
]
| _ ->
Html.li [
prop.text "Invalid object format"
]
)
)
]

View File

@@ -0,0 +1,7 @@
namespace Oceanbox.Codex
module Utils =
open Fable.Core
open Feliz
let toReact (el: JSX.Element) : ReactElement = unbox el

View File

@@ -2,5 +2,6 @@
echo Building Codex frontend
fable -e .jsx -o build --verbose --run \
bunx --bun vite build -d -c ../../vite.config.js --mode development --minify false --outDir /home/simkir/oceanbox/poseidon/src/Codex/dist/WebRoot
fable -e .jsx -o build --verbose --test:MSBuildCracker --run \
bunx --bun \
vite build -d -c ../../vite.config.js --mode development --minify false --outDir ../../dist/WebRoot

View File

@@ -1,15 +1,52 @@
<!doctype html>
<html>
<head>
<title>Feliz App</title>
<title>Codex</title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/png" href="/img/favicon-32x32.png" sizes="32x32" />
<link rel="shortcut icon" type="image/png" href="/img/favicon-16x16.png" sizes="16x16" />
<link rel="stylesheet" href="/main.css" />
<style>
@font-face {
font-family: 'Segoe UI Web (West European)';
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-light.woff2);
font-weight: 100;
font-style: 'normal';
font-display: swap;
}
@font-face {
font-family: 'Segoe UI Web (West European)';
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-semilight.woff2);
font-weight: 300;
font-style: 'normal';
font-display: swap;
}
@font-face {
font-family: 'Segoe UI Web (West European)';
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-regular.woff2);
font-weight: 400;
font-style: 'normal';
font-display: swap;
}
@font-face {
font-family: 'Segoe UI Web (West European)';
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-semibold.woff2);
font-weight: 600;
font-style: 'normal';
font-display: swap;
}
@font-face {
font-family: 'Segoe UI Web (West European)';
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-bold.woff2);
font-weight: 700;
font-style: 'normal';
font-display: swap;
}
</style>
</head>
<body>
<body style="margin: 0;">
<div id="feliz-app"></div>
<script type="module" src="/build/Main.jsx"></script>
</body>
</html>
</html>

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Fable.Core": {
"type": "Direct",
"requested": "[4.4.0, )",
@@ -75,6 +75,16 @@
"Fable.Elmish": "4.0.0"
}
},
"FS.FluentUI": {
"type": "Direct",
"requested": "[3.0.0, )",
"resolved": "3.0.0",
"contentHash": "QXFfClv7q7UGYRJJ5K5Gz9B2QvxNFNn5sLSbojLb0rITbQE8BfkyJEYaY3MmRyQgAsbWu3Xr62AR+W2fJbcfsA==",
"dependencies": {
"FSharp.Core": "6.0.1",
"Feliz": "2.9.0"
}
},
"FsToolkit.ErrorHandling": {
"type": "Direct",
"requested": "[5.0.1, )",

View File

@@ -116,6 +116,15 @@ h1 {
border-radius: 8px;
}
.bcol-neutral-stroke-1 {
border-color: var(--colorNeutralStroke1);
}
.br-solid {
border-right-width: 2px;
border-right-style: solid;
}
.text-overflow {
overflow: hidden;
white-space: nowrap;
@@ -126,6 +135,26 @@ h1 {
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
}
.vh-100 {
height: 100vh;
}
.w-256 {
width: 256px;
}
.w-512 {
width: 512px;
}
.minh-512 {
min-height: 512px;
}
.mw-128 {
max-width: 128px;
}
/* special stuff */
.archives-list {
@@ -147,4 +176,4 @@ h1 {
flex: 2 1 512px;
min-width: 512px;
max-width: 576px;
}
}

View File

@@ -13,6 +13,19 @@ module Admin =
open Oceanbox
// TODO: Better name and/or place for this?
module private EFType =
let toArchive (efArchive: Entity.Archive) : Remoting.Archive = {
Id = efArchive.ArchiveId
Name = efArchive.Name
DataSetId = efArchive.AttribsId
StartTime = efArchive.StartTime
Frames = efArchive.Frames
Published = efArchive.Published
Public = efArchive.Public
}
module private Handler =
let addUsers (ctx: HttpContext) (req: Remoting.AddUsersRequest) : Async<Result<unit, string>> =
let archmaesterAdd (db: Entity.ArchiveContext) =
@@ -63,7 +76,7 @@ module Admin =
with e ->
do logger.LogError ("OpenFGA write errored with: {Msg}. Rolling back archmaester.", e.Message)
do! tr.RollbackAsync ()
return! Error (sprintf "Error deleting users from OpenFGA: %s" e.Message)
return! Error (sprintf "Error adding users to OpenFGA: %s" e.Message)
with e ->
do logger.LogError (e, "Failed connecting to database")
return! Error (sprintf "Error deleting users from OpenFGA: %s" e.Message)
@@ -76,21 +89,50 @@ module Admin =
do logger.LogInformation ("Add archive groups from {User}: {Request}", user, req)
try
let db = ctx.GetService<Entity.ArchiveContext> ()
let fga = ctx.GetService<OpenFgaClient> ()
let! created = Archmaester.EFCore.addArchiveGroups db req.Id req.Groups
do logger.LogInformation ("Added {CreatedCount} archive group entries", created)
let fga = ctx.GetService<OpenFgaClient> ()
let req = OpenFGA.Group.addArchive req
let! fgaResp = fga.Write req |> Async.AwaitTask
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaResp.ToJson ())
return Ok ()
with e ->
do logger.LogError(e, "Error adding group to archive")
do logger.LogError (e, "Error adding group to archive")
return Error (sprintf "Error adding archive groups: %s" e.Message)
}
let private permissionToTuple
(archiveId: System.Guid)
(groupName: string)
(relation: Remoting.ArchiveRelation)
: Model.ClientTupleKey =
match relation with
| Remoting.ArchiveRelation.ViewTerm term -> OpenFGA.Group.viewArchive archiveId term groupName
| Remoting.ArchiveRelation.ExecTicket ticket -> OpenFGA.Group.execArchive archiveId ticket groupName
let addGroupPermissions (ctx: HttpContext) (req: Remoting.AddGroupPermissionsRequest) =
async {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
do logger.LogInformation ("Add group archive permissions from {User}: {Request}", user, req)
try
let fga = ctx.GetService<OpenFgaClient> ()
let fgaReq: Model.ClientWriteRequest =
req.Permissions
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|> OpenFGA.Queries.write'
let! fgaResp = fga.Write fgaReq |> Async.AwaitTask
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaResp.ToJson ())
return Ok ()
with e ->
do logger.LogError (e, "Error adding archive permission to group")
return Error (sprintf "Error adding group permissions: %s" e.Message)
}
let deleteArchive (ctx: HttpContext) (archiveId: System.Guid) : Async<Result<bool, string>> =
async {
let user = ctx.User.Identity.Name
@@ -179,7 +221,7 @@ module Admin =
else
return Error "Filter must include archive id"
with e ->
do logger.LogError (e, "Error in getArchives from {User}", user)
do logger.LogError (e, "Error in getArchiveRefs from {User}", user)
return Error "Error fetching archive count"
}
@@ -235,6 +277,186 @@ module Admin =
return Error "Error fetching archive count"
}
let getAllDataSets (ctx: HttpContext) =
async {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
try
do logger.LogInformation("getAllDataSets from {User}", user)
let db = ctx.GetService<NpgsqlDataSource> ()
let! dataSets =
Archmaester.Dapper.queryDataSets db
return Ok dataSets
with e ->
do logger.LogError (e, "getAllDataSets from {User}", user)
return Error "Error fetching dataSets"
}
let getArchiveDataSet (ctx: HttpContext) (archiveId: System.Guid) =
async {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
try
do logger.LogInformation("getArchiveDataSet {Archive} from {User}", archiveId, user)
let db = ctx.GetService<NpgsqlDataSource> ()
let! dataSet = Archmaester.Dapper.queryArchiveDataSet db archiveId
return Ok dataSet
with e ->
do logger.LogError (e, "getArchiveDataSet {Archive} from {User}", archiveId, user)
return Error "Error fetching dataset"
}
let addArchive (ctx: HttpContext) (archive: Remoting.AddArchiveRequest) =
let archmaesterAdd (db: Entity.ArchiveContext) =
async {
try
let! nameTaken = Archmaester.EFCore.checkArchiveNameTaken db archive.Name
if nameTaken then
return Error "Error adding archive: an archive with that name already exists"
else
let! newArchive = Archmaester.EFCore.addArchive db archive
return Ok newArchive
with e ->
return Error (sprintf "Error adding archive: %s" e.Message)
}
let archmaesterAddPublic (db: Entity.ArchiveContext) (archiveId: System.Guid) =
async {
try
let! success = Archmaester.EFCore.setArchivePublic db archiveId true
if success > 0 then
return Ok ()
else
return Error "Failed to add * user to public archive"
with e ->
return Error (sprintf "Error adding * user to archive: %s" e.Message)
}
asyncResult {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
try
do logger.LogInformation("addArchive {Archive} from {User}", archive.Name, user)
let fga = ctx.GetService<OpenFgaClient> ()
let db = ctx.GetService<Entity.ArchiveContext> ()
let tr = db.Database.BeginTransaction()
let! newArchive = archmaesterAdd db
try
if newArchive.Public then
do! archmaesterAddPublic db newArchive.ArchiveId
let tuples =
if newArchive.Public then
[|
OpenFGA.Archive.defaultPrincipal newArchive.ArchiveId
OpenFGA.Archive.publicArchive newArchive.ArchiveId
|]
else
[|OpenFGA.Archive.defaultPrincipal newArchive.ArchiveId|]
let! fgaResp =
let req = OpenFGA.Queries.write' tuples
fga.Write req |> Async.AwaitTask
logger.LogInformation (
"addArchive: {Archive} openFGA write responded with {JSON}",
archive.Name,
fgaResp.ToJson ()
)
do! tr.CommitAsync ()
return! newArchive |> EFType.toArchive |> Ok
with e ->
// TODO: Ideally, we should rollback fga too
do logger.LogError (e, "addArchive OpenFGA failed, rolling back Archmaester")
do! tr.RollbackAsync ()
return! Error (sprintf "Error adding archive: %s" e.Message)
with e ->
do logger.LogError (e, "addArchive {Archive} from {User}", archive.Name, user)
return! Error "Error adding archive"
}
let updateArchive (ctx: HttpContext) (archiveId: System.Guid) (archive: Remoting.EditArchiveRequest) =
let archmaesterEdit (db: Entity.ArchiveContext) =
async {
try
let! existingName = Archmaester.EFCore.getArchiveName db archiveId
let! nameTaken = Archmaester.EFCore.checkArchiveNameTaken db archive.Name
if nameTaken && not (existingName = archive.Name) then
return Error "Error updating archive: an archive with that name already exists"
else
let! newArchive = Archmaester.EFCore.editArchive db archiveId archive
return Ok newArchive
with e ->
return Error (sprintf "Error updating archive: %s" e.Message)
}
let archmaesterSetPublic (db: Entity.ArchiveContext) (setPublic: bool) =
async {
try
let! success = Archmaester.EFCore.setArchivePublic db archiveId setPublic
if success > 0 then
return Ok ()
else
return Error "Failed to add * user to public archive"
with e ->
return Error (sprintf "Error adding/removing * user to/from archive: %s" e.Message)
}
let fgaSetPublic (fga: OpenFgaClient) (setPublic: bool) =
async {
try
let publicTuple = OpenFGA.Archive.publicArchive archiveId
let checkReq =
OpenFGA.Queries.check { User = publicTuple.User; Relation = publicTuple.Relation; Object = publicTuple.Object}
let! publicResp = fga.Check checkReq |> Async.AwaitTask
let publicTupleExists = publicResp.Allowed |> Option.ofNullable |> Option.defaultValue false
if setPublic && not publicTupleExists then
let writeReq = OpenFGA.Queries.write' [|publicTuple|]
let! writeResp = fga.Write writeReq |> Async.AwaitTask
match writeResp.Writes |> Array.ofSeq with
| [||] -> return Error (sprintf "Error writing public tuple to fga: %s" (writeResp.ToJson()))
| _ -> return Ok ()
elif not setPublic && publicTupleExists then
let deleteReq = OpenFGA.Queries.delete' [|publicTuple|]
let! deleteResp = fga.Write deleteReq |> Async.AwaitTask
match deleteResp.Deletes |> Array.ofSeq with
| [||] -> return Error (sprintf "Error deleting public tuple from fga: %s" (deleteResp.ToJson()))
| _ -> return Ok ()
else
return Ok ()
with e ->
return Error (sprintf "Error adding/removing fga user:* view relation to/from archive: %s" e.Message)
}
asyncResult {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
try
do logger.LogInformation("updateArchive {Archive} from {User}", archive.Name, user)
let fga = ctx.GetService<OpenFgaClient> ()
let db = ctx.GetService<Entity.ArchiveContext> ()
let tr = db.Database.BeginTransaction()
let! newArchive = archmaesterEdit db
try
if newArchive.Public && not archive.Public then
do! archmaesterSetPublic db true
do! fgaSetPublic fga true
elif not newArchive.Public && archive.Public then
do! archmaesterSetPublic db false
do! fgaSetPublic fga false
do! tr.CommitAsync ()
return! newArchive |> EFType.toArchive |> Ok
with e ->
do logger.LogError (e, "updateArchive setting public failed, rolling back Archmaester")
do! tr.RollbackAsync ()
return! Error (sprintf "Error updating archive: %s" e.Message)
with e ->
do logger.LogError (e, "updateArchive {Archive} from {User}", archive.Name, user)
return! Error (sprintf "Error updating archive: %s" e.Message)
}
let getArchiveTypes (ctx: HttpContext) =
async {
let user = ctx.User.Identity.Name
@@ -306,18 +528,101 @@ module Admin =
return ()
}
let setUserPermissions (ctx: HttpContext) (req: Remoting.UserPermissionRequest) =
async {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
do logger.LogInformation ("setUserPermissions from {User}: {@Req}", user, req)
// TODO(simkir): Sanitize/check the request, aka turn the dto to an internal type
try
let fga = ctx.GetService<OpenFgaClient> ()
let writes: Model.ClientWriteRequest =
req.Permissions
|> Array.choose (fun permission ->
if permission.Enabled then
Some {
Remoting.Tuple.empty with
User = req.User
Relation = permission.Name
Object = req.User
}
else
None
)
|> OpenFGA.Queries.write
if writes.Writes.Count > 0 then
let! fgaWriteResp = fga.Write writes |> Async.AwaitTask
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaWriteResp.ToJson ())
let deletes =
req.Permissions
|> Array.choose (fun permission ->
if permission.Enabled then
None
else
Remoting.Tuple.delete(req.User, permission.Name, req.User)
|> Some
)
|> OpenFGA.Queries.deleteTuples
if deletes.Count > 0 then
let! fgaDeleteResp = fga.DeleteTuples deletes |> Async.AwaitTask
do logger.LogInformation ("OpenFGA delete responded with: {JSON}", fgaDeleteResp.ToJson ())
return Ok ()
with e ->
do logger.LogError (e, "Error setting user permissions")
// TODO: Maybe do not send exn message
return Error (sprintf "Error setting user permissions: %s" e.Message)
}
let updateGroupPermissions (ctx: HttpContext) (req: Remoting.AddGroupPermissionsRequest) =
async {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
do logger.LogInformation ("updateGroupPermissions from {User}: {@Req}", user, req)
try
let fga = ctx.GetService<OpenFgaClient> ()
let deletes =
req.Permissions
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|> OpenFGA.Queries.deleteTuples'
let! deleteResp = fga.DeleteTuples deletes |> Async.AwaitTask
do logger.LogInformation ("OpenFGA delete responded with: {JSON}", deleteResp.ToJson ())
let writes =
req.Permissions
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|> ResizeArray
let! writeResp = fga.WriteTuples writes |> Async.AwaitTask
do logger.LogInformation ("OpenFGA write responded with: {JSON}", writeResp.ToJson ())
return Ok ()
with e ->
do logger.LogError (e, "Error updating group permissions")
// TODO: Maybe do not send exn message
return Error (sprintf "Error updating group permissions: %s" e.Message)
}
let private impl (ctx: HttpContext) : Remoting.Api.Admin = {
addUsers = Handler.addUsers ctx
addArchive = Handler.addArchive ctx
addArchiveGroups = Handler.addArchiveGroups ctx
addGroupPermissions = Handler.addGroupPermissions ctx
addUsers = Handler.addUsers ctx
deleteArchive = Handler.deleteArchive ctx
getAllGroups = Handler.getAllGroups ctx
getArchive = Handler.getArchive ctx
getArchiveCount = Handler.getArchiveCount ctx
getArchiveDataSet = Handler.getArchiveDataSet ctx
getArchiveRefs = Handler.getArchiveRefs ctx
getArchiveTypes = fun () -> Handler.getArchiveTypes ctx
getArchives = Handler.getArchives ctx
getDataSets = fun () -> Handler.getAllDataSets ctx
getGroupUsers = Handler.getGroupUsers ctx
removeUsers = Handler.removeUsers ctx
setUserPermissions = Handler.setUserPermissions ctx
updateArchive = Handler.updateArchive ctx
updateGroupPermissions = Handler.updateGroupPermissions ctx
}
let endpoints: HttpHandler =
@@ -325,4 +630,4 @@ module Admin =
|> Remoting.withErrorHandler Utils.rpcErrorHandler
|> Remoting.fromContext impl
|> Remoting.withRouteBuilder Remoting.routeBuilder
|> Remoting.buildHttpHandler
|> Remoting.buildHttpHandler

View File

@@ -5,6 +5,7 @@ module Archmaester =
open Archmaester
let getDataSource (connStr: string) : NpgsqlDataSource =
let dataSourceBuilder = NpgsqlDataSourceBuilder connStr
let _mapper = dataSourceBuilder.UseNetTopologySuite ()
@@ -18,6 +19,16 @@ module Archmaester =
open Oceanbox.DataAgent.Dapper
// TODO: Is there another, better way to do this?
type DataSetTable = {
a_id: int
base_path: string
m_id: System.Guid
m_name: string
start_time: System.DateTime
end_time: System.DateTime
}
let private canonicalizeGroupNames (groups: string array option) : string array =
groups
|> Option.defaultValue [||]
@@ -138,6 +149,116 @@ module Archmaester =
return count
}
// NOTE: Currently called attribs, but soon to be renamed
let queryDataSets (db: NpgsqlDataSource) =
async {
use conn = db.OpenConnection ()
let query =
"""
SELECT
a.id AS a_id,
a.base_path,
m.id AS m_id,
m.name AS m_name,
MIN(f.start_time) AS start_time,
MAX(f.start_time) AS end_time
FROM
attribs AS a
JOIN
model_areas AS m
ON m.id = a.model_area_id
LEFT JOIN
files AS f
ON f.attribs_id = a.id
WHERE
-- Type 1 is FVCOM
a.type_id = 1
AND NOT a.retired
GROUP BY
a.id,
a.base_path,
m.id,
m.name
;
"""
let! res =
conn.QueryAsync<DataSetTable>(query)
|> Async.AwaitTask
return
res
|> Array.ofSeq
|> Array.map (fun x ->
{
Id = x.a_id
BasePath = x.base_path
ModelAreaId = x.m_id
ModelAreaName = x.m_name
StartTime = x.start_time
EndTime = x.end_time.AddHours(24)
} : Remoting.DataSet
)
}
let queryArchiveDataSet (db: NpgsqlDataSource) (archiveId: System.Guid) =
async {
use conn = db.OpenConnection ()
let query =
"""
SELECT
a.id AS a_id,
a.base_path,
m.id AS m_id,
m.name AS m_name,
MIN(f.start_time) AS start_time,
MAX(f.start_time) AS end_time
FROM
attribs AS a
JOIN
model_areas AS m
ON m.id = a.model_area_id
LEFT JOIN
files AS f
ON f.attribs_id = a.id
LEFT JOIN
archives as ar
ON ar.attribs_id = a.id
WHERE
ar.id = @archive_id
GROUP BY
a.id,
a.base_path,
m.id,
m.name
;
"""
let param =
dict [
"archive_id", box archiveId
]
let! res =
conn.QueryAsync<DataSetTable>(query, param)
|> Async.AwaitTask
return
res
|> Array.ofSeq
|> Array.map (fun x ->
{
Id = x.a_id
BasePath = x.base_path
ModelAreaId = x.m_id
ModelAreaName = x.m_name
StartTime = x.start_time
EndTime = x.end_time.AddHours(24)
} : Remoting.DataSet
) // TODO: is there a better way to select single rows?
|> Array.head
}
module EFCore =
open System.Linq
@@ -180,6 +301,114 @@ module Archmaester =
return created
}
let checkArchiveNameTaken (db: Entity.ArchiveContext) (name: string) : Async<bool> =
db.Archives
.AsNoTracking()
.Where(fun archive -> archive.Name = name)
.AnyAsync()
|> Async.AwaitTask
let getArchiveName (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<string> =
db.Archives
.AsNoTracking()
.Where(fun archive -> archive.ArchiveId = archiveId)
.Select(_.Name)
.SingleOrDefaultAsync()
|> Async.AwaitTask
/// Align archive start time with the actual files in the dataset
let private getAlignedStartTime (db: Entity.ArchiveContext) (dataSetId: int) (archiveStartTime: System.DateTime) =
let utcStartTime = archiveStartTime.ToUniversalTime()
db.Files
.AsNoTracking()
.Where(fun file ->
file.AttribsId = dataSetId
&& file.StartTime >= utcStartTime
)
.OrderBy(_.StartTime)
.Select(_.StartTime)
.FirstAsync()
|> Async.AwaitTask
let addArchive (db: Entity.ArchiveContext) (archive: Remoting.AddArchiveRequest) : Async<Entity.Archive> =
async {
let! alignedStartTime = getAlignedStartTime db archive.DataSetId archive.StartTime
let newArchive =
Entity.Archive(
Name = archive.Name,
Frames = archive.Frames,
StartTime = alignedStartTime,
Published = archive.Published,
Public = archive.Public,
AttribsId = archive.DataSetId
)
db.Add newArchive |> ignore
let! obxGroupId =
db.Groups
.AsNoTracking()
.Where(fun group -> group.Name = "/oceanbox")
.Select(_.GroupId)
.SingleAsync()
|> Async.AwaitTask
let defaultGroup = Entity.ArchiveGroup(ArchiveId = newArchive.ArchiveId, GroupId = obxGroupId)
db.Add defaultGroup |> ignore
do! db.SaveChangesAsync () |> Async.AwaitTask |> Async.Ignore
return newArchive
}
let editArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) (archive: Remoting.EditArchiveRequest) : Async<Entity.Archive> =
async {
let! existingArchive =
db.Archives
.Where(fun archive -> archive.ArchiveId = archiveId)
.SingleAsync()
|> Async.AwaitTask
let! alignedStartTime = getAlignedStartTime db existingArchive.AttribsId archive.StartTime
existingArchive.Name <- archive.Name
existingArchive.StartTime <- alignedStartTime
existingArchive.Frames <- archive.Frames
existingArchive.Published <- archive.Published
existingArchive.Public <- archive.Public
do db.Update existingArchive |> ignore
do! db.SaveChangesAsync () |> Async.AwaitTask |> Async.Ignore
return existingArchive
}
let setArchivePublic (db: Entity.ArchiveContext) (archiveId: System.Guid) (setPublic: bool) : Async<int> =
async {
let! starUserId =
db.Users
.AsNoTracking()
.Where(fun user -> user.Name = "*")
.Select(_.UserId)
.SingleAsync()
|> Async.AwaitTask
if setPublic then
let starArchiveUser = Entity.ArchiveUser(UserId = starUserId, ArchiveId = archiveId)
db.Add starArchiveUser |> ignore
else
let! starArchiveUser =
db.ArchiveUsers
.Where(fun au -> au.UserId = starUserId && au.ArchiveId = archiveId)
.SingleOrDefaultAsync()
|> Async.AwaitTask
starArchiveUser |> Option.ofObj |> Option.iter (db.Remove >> ignore)
return! db.SaveChangesAsync () |> Async.AwaitTask
}
let deleteArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<int> =
async {
let! entity =
@@ -420,4 +649,4 @@ module Archmaester =
|> Async.AwaitTask
return entities
}
}

View File

@@ -2,10 +2,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<PackageId>Codex.Server</PackageId>
<RootNamespace>Oceanbox</RootNamespace>
<Version>0.0.0-alpha.1</Version>
<Version>0.0.1</Version>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<EnableDefaultContentItems>false</EnableDefaultContentItems>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>

View File

@@ -71,7 +71,8 @@ module OpenFGA =
|> Seq.toArray
|> Array.map (fun t ->
let condition : Remoting.Condition option =
Option.ofObj t.Key.Condition
t.Key.Condition
|> Option.ofObj
|> Option.map (fun cond -> {
Name = cond.Name
Context = JsonSerializer.Serialize cond.Context
@@ -148,6 +149,15 @@ module OpenFGA =
result
let read' (tuple: ClientTupleKey) : ClientReadRequest =
let result = ClientReadRequest ()
do result.User <- tuple.User
do result.Relation <- tuple.Relation
do result.Object <- tuple.Object
result
let delete (tuples: Remoting.Tuple array) =
let result = ClientWriteRequest ()
@@ -167,6 +177,57 @@ module OpenFGA =
result
/// To be used with OpenFga.Sdk.Client.OpenFgaClient.DeleteTuples
let deleteTuples (tuples: Remoting.Tuple array) : ResizeArray<ClientTupleKeyWithoutCondition> =
let deletes: ClientTupleKeyWithoutCondition array =
tuples
|> Array.map (fun tuple ->
let result = ClientTupleKeyWithoutCondition ()
do result.Object <- tuple.Object
do result.Relation <- tuple.Relation
do result.User <- tuple.User
result
)
ResizeArray deletes
let delete' (tuples: ClientTupleKey array) : ClientWriteRequest =
let result = ClientWriteRequest ()
let deletes: ClientTupleKeyWithoutCondition array =
tuples
|> Array.map (fun tuple ->
let result = ClientTupleKeyWithoutCondition ()
do result.Object <- tuple.Object
do result.Relation <- tuple.Relation
do result.User <- tuple.User
result
)
do result.Deletes <- ResizeArray deletes
result
/// To be used with OpenFga.Sdk.Client.OpenFgaClient.DeleteTuples
let deleteTuples' (tuples: ClientTupleKey array) : ResizeArray<ClientTupleKeyWithoutCondition> =
let deletes: ClientTupleKeyWithoutCondition array =
tuples
|> Array.map (fun tuple ->
let result = ClientTupleKeyWithoutCondition ()
do result.Object <- tuple.Object
do result.Relation <- tuple.Relation
do result.User <- tuple.User
result
)
ResizeArray deletes
let write (tuples: Remoting.Tuple array) =
let result = ClientWriteRequest ()
@@ -236,6 +297,28 @@ module OpenFGA =
|])
|> Queries.write'
module Archive =
/// Creates a default principal tuple for the oceanbox group
let defaultPrincipal (id: Guid) : ClientTupleKey =
let tuple = ClientTupleKey ()
do tuple.Object <- sprintf "archive:%s" (string id)
do tuple.Relation <- "principal"
do tuple.User <- "group:/oceanbox"
tuple
/// Gives view permission to all users. This tuple should be added to public archives
let publicArchive (id: Guid) : ClientTupleKey =
let tuple = ClientTupleKey ()
do tuple.Object <- sprintf "archive:%s" (string id)
do tuple.Relation <- "view"
do tuple.User <- "user:*"
tuple
module private Handlers =
let check (ctx: HttpContext) (req: Remoting.CheckRequest) =
async {
@@ -260,9 +343,10 @@ module OpenFGA =
let logger = ctx.GetLogger<Remoting.Api.OpenFGA> ()
let fga = ctx.GetService<OpenFgaClient> ()
try
let deleteRequest = Queries.delete [| tuple |]
do logger.LogInformation ("Delete req: {Request}", deleteRequest.ToJson ())
let! resp = fga.Write deleteRequest |> Async.AwaitTask
let deleteRequest = Queries.deleteTuples [| tuple |]
let json = JsonSerializer.Serialize deleteRequest
do logger.LogInformation ("Delete req: {Request}", json)
let! resp = fga.DeleteTuples deleteRequest |> Async.AwaitTask
do logger.LogInformation ("Delete resp: {Response}", resp.ToJson ())
return Ok (resp.Deletes.Count >= 1)
with e ->
@@ -329,4 +413,4 @@ module OpenFGA =
Remoting.createApi ()
|> Remoting.withRouteBuilder Remoting.routeBuilder
|> Remoting.fromContext impl
|> Remoting.buildHttpHandler
|> Remoting.buildHttpHandler

View File

@@ -155,7 +155,11 @@ let webApp: HttpHandler =
>=> choose [
Auth.endpoints
authorize >=> choose [ Acl.endpoints; Admin.endpoints; OpenFGA.endpoints ]
authorize >=> choose [
Acl.endpoints
Admin.endpoints
OpenFGA.endpoints
]
]
]
@@ -203,8 +207,16 @@ let configureApp (app: IApplicationBuilder) =
.UseStaticFiles()
.UseGiraffe webApp
let private getIp (uri: Uri) =
uri.DnsSafeHost
|> Net.Dns.GetHostAddresses
|> Array.head
let configureServices (settings: Settings) (services: IServiceCollection) =
let authSettings = settings.Auth
let uri = Uri settings.Fga.apiUrl
let ip = getIp uri
eprintfn "OpenFGA uri is %s (%s)" uri.DnsSafeHost (string ip)
let fga: OpenFgaClient = Fga.newFgaClient settings.Fga
let archmaesterDatasource = Archmaester.getDataSource settings.DbConnectionString
do Oceanbox.DataAgent.Dapper.register ()
@@ -293,4 +305,4 @@ let main args =
return 0
}
)
)

View File

@@ -14,7 +14,7 @@ let
in
buildDotnetModule {
pname = name;
version = "0.0.0-alpha.1";
version = "0.0.1";
inherit dotnet-sdk dotnet-runtime;

View File

@@ -1,17 +1,14 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Azure.Security.KeyVault.Secrets": {
"type": "Direct",
"requested": "[4.7.0, )",
"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"
}
},
"Fable.Remoting.Giraffe": {
@@ -53,8 +50,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"
}
},
"Azure.Core": {
@@ -64,12 +60,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": {
@@ -77,10 +68,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": {
@@ -88,8 +76,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": {
@@ -233,8 +220,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": {
@@ -527,8 +513,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": {
@@ -617,10 +602,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",
@@ -701,26 +683,18 @@
"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=="
},
"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=="
},
"Sentry": {
"type": "Transitive",
@@ -769,8 +743,7 @@
"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": {
@@ -778,11 +751,6 @@
"resolved": "5.0.0",
"contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "8hy61dsFYYSDjT9iTAfygGMU3A0EAnG69x5FUXeKsCjMhBmtTBt4UMUEW3ipprFoorOW6Jw/7hDMjXtlrsOvVQ=="
},
"System.IdentityModel.Tokens.Jwt": {
"type": "Transitive",
"resolved": "8.0.1",
@@ -797,38 +765,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=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
@@ -848,16 +788,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=="
},
"entity": {
"type": "Project",
"dependencies": {
@@ -953,9 +883,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"
}
},
"Dapper.FSharp": {
@@ -1121,8 +1049,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": {
@@ -1211,10 +1138,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",
@@ -1300,8 +1224,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": {
@@ -1372,15 +1295,6 @@
"Serilog": "4.0.0"
}
},
"System.Text.Encodings.Web": {
"type": "CentralTransitive",
"requested": "[9.0.2, )",
"resolved": "6.0.0",
"contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"Thoth.Json.Net": {
"type": "CentralTransitive",
"requested": "[12.0.0, )",

View File

@@ -122,6 +122,16 @@ module Remoting =
EndTime = System.DateTime.Now
}
[<RequireQualifiedAccess>]
type ArchiveRelation =
| ViewTerm of ViewTerm
| ExecTicket of ExecTicket
with
static member ConditionName (permission: ArchiveRelation) =
match permission with
| ViewTerm _ -> "term"
| ExecTicket _ -> "ticket"
[<Struct>]
type AddArchiveGroupsRequest = {
Id: Archmaester.Dto.ArchiveId
@@ -130,28 +140,102 @@ module Remoting =
Exec: ExecTicket option
}
[<Struct>]
type DataSet = {
Id: int
BasePath: string
ModelAreaId: System.Guid
ModelAreaName: string
StartTime: System.DateTime
EndTime: System.DateTime
}
[<Struct>]
type Archive = {
Id: System.Guid
Name: string
DataSetId: int
StartTime: System.DateTime
Frames: int
Published: bool
Public: bool
}
[<Struct>]
type AddArchiveRequest = {
Name: string
DataSetId: int
StartTime: System.DateTime
Frames: int
Published: bool
Public: bool
} with
static member empty () = {
Name = ""
DataSetId = -1
StartTime = System.DateTime.Now.AddDays(-14.)
Frames = 48
Published = true
Public = false
}
[<Struct>]
type EditArchiveRequest = {
Name: string
StartTime: System.DateTime
Frames: int
Published: bool
Public: bool
}
[<Struct>]
type AddUsersRequest = {
Group: string
Users: string array
}
[<Struct>]
type AddGroupPermissionsRequest = {
Group: string
ArchiveId: System.Guid
Permissions: ArchiveRelation array
}
[<Struct>]
type Permission = {
Name: string
Enabled: bool
}
[<Struct>]
type UserPermissionRequest = {
User: string
Permissions: Permission array
}
[<RequireQualifiedAccess>]
module Api =
type Auth = { IsAuthenticated: Async<bool> }
type Admin = {
addUsers: AddUsersRequest -> Async<Result<unit, string>>
addArchive: AddArchiveRequest -> Async<Result<Archive, string>>
addArchiveGroups: AddArchiveGroupsRequest -> Async<Result<unit, string>>
addGroupPermissions: AddGroupPermissionsRequest -> Async<Result<unit, string>>
addUsers: AddUsersRequest -> Async<Result<unit, string>>
deleteArchive: Archmaester.Dto.ArchiveId -> Async<Result<bool, string>>
getAllGroups: Async<string array>
getArchive: Archmaester.Dto.ArchiveId -> Async<Result<Archmaester.Dto.ArchiveProps, string>>
getArchiveCount: Archmaester.Dto.ArchiveFilter -> Async<Result<int, string>>
getArchiveDataSet: System.Guid -> Async<Result<DataSet, string>>
getArchiveRefs: Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
getArchiveTypes: unit -> Async<Archmaester.Dto.ArchiveType array>
getArchives: int -> int -> Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
getDataSets: unit -> Async<Result<DataSet array, string>>
getGroupUsers: string -> Async<string array>
removeUsers: string array -> Async<Result<unit, string>>
setUserPermissions: UserPermissionRequest -> Async<Result<unit, string>>
updateArchive: System.Guid -> EditArchiveRequest -> Async<Result<Archive, string>>
updateGroupPermissions: AddGroupPermissionsRequest -> Async<Result<unit, string>>
}
type OpenFGA = {
@@ -160,4 +244,4 @@ module Remoting =
ListObjects: ListObjectsRequest -> Async<Result<string array, string>>
ListUsers: ListUsersRequest -> Async<Result<string array, string>>
Read: ReadRequest -> Async<Result<ReadResponse, string>>
}
}

View File

@@ -2,21 +2,21 @@ apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: simkir-codex
name: simkir-codex
namespace: simkir-atlantis
app: <x>-codex
name: <x>-codex
namespace: <x>-atlantis
spec:
replicas: 1
selector:
matchLabels:
app: simkir-codex
app: <x>-codex
template:
metadata:
labels:
app: simkir-codex
app: <x>-codex
spec:
containers:
- image: yolo-registry.dev.oceanbox.io/simkir/codex
- image: yolo-registry.dev.oceanbox.io/<x>/codex
imagePullPolicy: Always
name: codex
ports:
@@ -33,27 +33,27 @@ spec:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: simkir-atlantis-db-app
name: <x>-atlantis-db-app
key: host
- name: DB_PORT
valueFrom:
secretKeyRef:
name: simkir-atlantis-db-app
name: <x>-atlantis-db-app
key: port
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: simkir-atlantis-db-app
name: <x>-atlantis-db-app
key: dbname
- name: DB_USER
valueFrom:
secretKeyRef:
name: simkir-atlantis-db-app
name: <x>-atlantis-db-app
key: user
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: simkir-atlantis-db-app
name: <x>-atlantis-db-app
key: password
- name: FGA_DB_HOST
valueFrom:
@@ -83,6 +83,15 @@ spec:
envFrom:
- secretRef:
name: azure-keyvault
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: "RuntimeDefault"
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
@@ -97,33 +106,33 @@ metadata:
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,213.239.94.191/32
name: simkir-codex
namespace: simkir-atlantis
name: <x>-codex
namespace: <x>-atlantis
spec:
ingressClassName: nginx
rules:
- host: simkir-codex.dev.oceanbox.io
- host: <x>-codex.dev.oceanbox.io
http:
paths:
- backend:
service:
name: simkir-codex
name: <x>-codex
port:
name: http
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- simkir-codex.dev.oceanbox.io
secretName: simkir-codex-tls
- <x>-codex.dev.oceanbox.io
secretName: <x>-codex-tls
---
apiVersion: v1
kind: Service
metadata:
labels:
app: simkir-codex
name: simkir-codex
namespace: simkir-atlantis
app: <x>-codex
name: <x>-codex
namespace: <x>-atlantis
spec:
internalTrafficPolicy: Cluster
ipFamilies:
@@ -135,4 +144,4 @@ spec:
protocol: TCP
targetPort: http
selector:
app: simkir-codex
app: <x>-codex

View File

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

View File

@@ -101,10 +101,12 @@ let private archiveToDto (ctx: Entity.ArchiveContext) (a: Entity.Archive) =
.ToArray ()
let files =
ctx.Files
.Where(fun f -> a.Files.Select(_.FileId).Contains f.FileId)
.OrderBy(_.Ordering)
.ToArray ()
a.Attribs.Files
|> Array.ofSeq
|> Array.filter (fun file ->
file.StartTime >= a.StartTime && file.StartTime.AddSeconds(float (file.Frames * a.Attribs.Freq)) <= a.StartTime.AddSeconds(float (a.Frames * a.Attribs.Freq))
)
|> Array.sortBy _.Ordering
let modelArea =
ctx.ModelAreas.SingleOrDefault (fun y -> y.Attribs.Select(_.AttribsId).Contains a.AttribsId)
@@ -230,7 +232,7 @@ let retireDanglingAttribs (ctx: Entity.ArchiveContext) =
type Archivist(dataSource: NpgsqlDataSource) =
let withDb qry =
try
let ctx = new Entity.ArchiveContext(dataSource, true)
use ctx = new Entity.ArchiveContext(dataSource, true)
qry ctx |> Ok
with e ->
Log.Error $"DataAgent.Archives.Archivist.withDb: {e}"
@@ -844,6 +846,12 @@ type Archivist(dataSource: NpgsqlDataSource) =
ctx.SaveChanges () |> ignore
}
member _.startConnection () =
new Entity.ArchiveContext (dataSource)
member _.startTransaction (ctx: Entity.ArchiveContext) =
ctx.Database.BeginTransaction ()
member x.tryAddArchive(item: ArchiveDto) =
monad {
do!
@@ -886,6 +894,45 @@ type Archivist(dataSource: NpgsqlDataSource) =
return! Error e
}
member x.tryAddArchive(ctx: Entity.ArchiveContext, item: ArchiveDto) =
monad {
do!
if not (x.modelAreaExists item.props.modelArea) then
let msg = $"ModelArea {item.props.modelArea} does not exist"
Log.Error msg
Error msg
else
Ok ()
do!
if x.archiveExists item.props.archiveId then
let msg = $"Archive {item.props.name} already exists with id {item.props.archiveId}"
Log.Error msg
Error msg
else
Ok ()
let p = item.props
let files = item.files.series
if verifyStartAndEndTimes p.startTime p.endTime files |> not then
do! Error "Start and end times don't match file series"
let! _ = verifyContiguousSeries files
Log.Information $"Adding new archive with Guid: {p.archiveId}"
do! x.addAcl' (ctx, item.acl)
Log.Information $"Adding new archive item: {(p.name, item.files.basePath)}"
Log.Debug $"Adding new archive item\n%A{item}"
match x.addArchive' (ctx, item) with
| Ok a -> return! Ok a
| Error e ->
Log.Error e
return! Error e
}
member x.tryAddSubArchive(item: SubArchiveDef) =
let ctx = new Entity.ArchiveContext (dataSource)
let transaction = ctx.Database.BeginTransaction ()
@@ -1330,11 +1377,12 @@ type Archivist(dataSource: NpgsqlDataSource) =
.Where(fun y -> y.ArchiveId = aid)
.Include(_.Attribs)
.ThenInclude(_.Type)
.Include(_.Attribs)
.ThenInclude(_.Files)
.Include(_.Ref)
.Include(_.Owners)
.Include(_.Users)
.Include(_.Groups)
.Include(fun y -> y.Files.OrderBy _.File.Ordering)
.ToArray ()
|> fun y ->
if y.Length = 0 then

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<IsPackable>true</IsPackable>
<PackageId>Oceanbox.DataAgent</PackageId>

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Dapper.FSharp": {
"type": "Direct",
"requested": "[4.9.0, )",
@@ -103,10 +103,7 @@
"type": "Direct",
"requested": "[2.5.0, )",
"resolved": "2.5.0",
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
"dependencies": {
"System.Memory": "4.5.4"
}
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
},
"Npgsql.EntityFrameworkCore.PostgreSQL": {
"type": "Direct",
@@ -313,8 +310,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": {
@@ -530,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=="
},
"NetTopologySuite.IO.PostGis": {
"type": "Transitive",
"resolved": "2.1.0",
@@ -551,11 +537,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",
@@ -574,100 +556,6 @@
"Serilog.Sinks.File": "6.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.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.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.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.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": {
@@ -767,136 +655,6 @@
"contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA=="
}
},
"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,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<!-- <RuntimeIdentifier>linux-x64</RuntimeIdentifier> -->
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Microsoft.EntityFrameworkCore": {
"type": "Direct",
"requested": "[9.0.1, )",
@@ -31,8 +31,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.Relational": {
@@ -107,10 +106,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": {
@@ -140,9 +136,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": {
@@ -152,8 +146,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": {
@@ -265,11 +258,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",
@@ -318,47 +306,11 @@
"System.Composition.Runtime": "7.0.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.Reflection.Metadata": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
"dependencies": {
"System.Collections.Immutable": "7.0.0"
}
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"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=="
},
"NetTopologySuite": {
"type": "CentralTransitive",
"requested": "[2.5.0, )",
"resolved": "2.5.0",
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
"dependencies": {
"System.Memory": "4.5.4"
}
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
},
"Npgsql": {
"type": "CentralTransitive",

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