Merge branch 'main' into 'automated/npins-update-20260123'

# Conflicts:
#   nix/sources.json
This commit is contained in:
2026-01-23 13:13:42 +01:00
125 changed files with 4411 additions and 2557 deletions

View File

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

37
.envrc
View File

@@ -1,4 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export NPINS_DIRECTORY="nix"
export APP_ENV=$USER
# the shebang is ignored, but nice for editors # the shebang is ignored, but nice for editors
watch_file nix/sources.json watch_file nix/sources.json
@@ -9,35 +13,4 @@ dotenv_if_exists
use nix use nix
# HACK: Workaround for direnv bug # HACK: Workaround for direnv bug
unset TMP TMPDIR TEMP TEMPDIR unset TMP TMPDIR TEMP TEMPDIR
export NPINS_DIRECTORY="nix"
# 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
}

View File

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

View File

@@ -39,6 +39,7 @@
<PackageVersion Include="Matplotlib.ColorMaps" Version="3.0.1" /> <PackageVersion Include="Matplotlib.ColorMaps" Version="3.0.1" />
<PackageVersion Include="Thoth.Fetch" Version="3.0.1" /> <PackageVersion Include="Thoth.Fetch" Version="3.0.1" />
<PackageVersion Include="Thoth.Json" Version="10.4.1"/> <PackageVersion Include="Thoth.Json" Version="10.4.1"/>
<PackageVersion Include="FS.FluentUI" Version="3.0.0"/>
<!-- Serverpack --> <!-- Serverpack -->
<PackageVersion Include="OpenFga.Sdk" Version="0.7.0"/> <PackageVersion Include="OpenFga.Sdk" Version="0.7.0"/>
<PackageVersion Include="FSharp.SystemTextJson" Version="1.3.13"/> <PackageVersion Include="FSharp.SystemTextJson" Version="1.3.13"/>

View File

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

View File

@@ -1,5 +1,137 @@
# Changelog # Changelog
## [1.40.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.4...v1.40.5) (2026-01-21)
### Bug Fixes
* **xtractor:** Reduce to 4 cores per task ([8e824d4](https://gitlab.com/oceanbox/Poseidon/commit/8e824d4afa0b03f59e006d3a0d50fb216e71483e))
## [1.40.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.3...v1.40.4) (2026-01-21)
### Bug Fixes
* **xtractor:** Reduce core requirement to 8 ([efacb2a](https://gitlab.com/oceanbox/Poseidon/commit/efacb2a3322de0ced45db4eec240846f4e371a75))
## [1.40.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.2...v1.40.3) (2026-01-20)
### Bug Fixes
* **inbox|xtracto:** Delete/Read msg and allow non-ascii xtractor names ([d8d5e07](https://gitlab.com/oceanbox/Poseidon/commit/d8d5e076baf8b559200f2da91237f9874678b216))
* **multiauth:** Add clientId to redirect on signout ([54c40d7](https://gitlab.com/oceanbox/Poseidon/commit/54c40d7accc4bbc43f66dda0df647ccac482a2b0))
## [1.40.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.1...v1.40.2) (2026-01-19)
### Bug Fixes
* **xtract:** Disabled if not allowed to simulate transport ([e429a85](https://gitlab.com/oceanbox/Poseidon/commit/e429a855e5bd00493e2f99647092aebce9c99a2a))
## [1.40.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.0...v1.40.1) (2026-01-19)
### Bug Fixes
* fix tilt build on net10 ([d5cde19](https://gitlab.com/oceanbox/Poseidon/commit/d5cde19250847f7b091cfa5f65eb703405c202b6))
# [1.40.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.2...v1.40.0) (2026-01-16)
### Bug Fixes
* **Codex:** expose days instead of frames in arcive form ([6cf5262](https://gitlab.com/oceanbox/Poseidon/commit/6cf5262dd5c98517a3c767f410c858fe32c07bd5))
* **Codex:** only allow inbounds time intervals on edit archive ([eaea4b2](https://gitlab.com/oceanbox/Poseidon/commit/eaea4b2e215669cec19f2a8cec122ba670a7a202))
### Features
* **Codex:** edit archives ([ec10932](https://gitlab.com/oceanbox/Poseidon/commit/ec109328fbf5f237a52ef77cbd44dff571deee5f))
## [1.39.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.1...v1.39.2) (2026-01-15)
### Bug Fixes
* fix net10 issues ([f4943a1](https://gitlab.com/oceanbox/Poseidon/commit/f4943a148b72fb7e10a745cc3e806b9c4bdd76d8))
## [1.39.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.0...v1.39.1) (2026-01-15)
### Bug Fixes
* **ci:** Remove schedule check ([ab37e88](https://gitlab.com/oceanbox/Poseidon/commit/ab37e88bb0f669a7aa94bf831f95f8c60dc28804))
# [1.39.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.5...v1.39.0) (2026-01-14)
### Bug Fixes
* **Codex:** add * user in archmeister on public archives ([cd678a4](https://gitlab.com/oceanbox/Poseidon/commit/cd678a41f64a64c6f3616f32bafeaae6715c08a4))
* **Codex:** use feliz router guid matching ([7182f7c](https://gitlab.com/oceanbox/Poseidon/commit/7182f7c9f094d65c884e8e02d4aaa89561ca5e82))
* **Codex:** utc start_time ([492651e](https://gitlab.com/oceanbox/Poseidon/commit/492651e0f34035d8c61174aa50336222bcfd979c))
* **DataAgent:** use files from parent attribs instead of archive_files ([eac23e7](https://gitlab.com/oceanbox/Poseidon/commit/eac23e7d1a541f2e90374a2add689846d9e7b642))
### Features
* **Codex:** ability to add FVCOM archives ([d86db7a](https://gitlab.com/oceanbox/Poseidon/commit/d86db7a66ca5ecb6a9ad45ce3d47be3a98d56bb8))
## [1.38.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.4...v1.38.5) (2026-01-13)
### Bug Fixes
* **Archmaester:** Rollback add archive if openfga fails ([46e86eb](https://gitlab.com/oceanbox/Poseidon/commit/46e86eb5f961a45fba2da1525c1472bdca79ab47))
## [1.38.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.3...v1.38.4) (2026-01-12)
### Bug Fixes
* **nix:** Bump node deps ([e513d87](https://gitlab.com/oceanbox/Poseidon/commit/e513d87d249843423f0e0a62275afe45e6c73a46))
## [1.38.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.2...v1.38.3) (2026-01-12)
### Bug Fixes
* **xtractor:** Set maxEndDate to 1 year and format ([5315a05](https://gitlab.com/oceanbox/Poseidon/commit/5315a05fa255c6164d3ef73c0f5e20cdb4c632d0))
## [1.38.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.1...v1.38.2) (2026-01-12)
### Bug Fixes
* **atlas:** Disable Snowflakes ([4cd3673](https://gitlab.com/oceanbox/Poseidon/commit/4cd3673d15783afa72ca3358e6bd8c3a8cfbfd16))
## [1.38.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.0...v1.38.1) (2026-01-11)
### Bug Fixes
* **xtractor:** Move to short partition and 16 CPU, also set max duration to 1 year ([56d3476](https://gitlab.com/oceanbox/Poseidon/commit/56d34767d7a2e0bc6aadaa5987344c6e7a58698a))
# [1.38.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.37.1...v1.38.0) (2026-01-07)
### Bug Fixes
* client layout ([efb3292](https://gitlab.com/oceanbox/Poseidon/commit/efb3292d9f4a8fccf2cebbdd75b3d3ffc186fa11))
### Features
* add fluent ui to codex ([d8bf174](https://gitlab.com/oceanbox/Poseidon/commit/d8bf174d3aa181169365c178b4335052e13eabc5))
## [1.37.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.37.0...v1.37.1) (2026-01-05)
### Bug Fixes
* **xtractor:** Avoid using union types in Dapr Actors ([14c1a57](https://gitlab.com/oceanbox/Poseidon/commit/14c1a57331f981b1e1e0793426448ea261002e6d))
# [1.37.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.36.0...v1.37.0) (2025-12-22) # [1.37.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.36.0...v1.37.0) (2025-12-22)

View File

@@ -1 +1 @@
1.37.0 1.40.5

823
bun.lock

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,8 +18,13 @@
"vite-plugin-mkcert": "^1.17.8" "vite-plugin-mkcert": "^1.17.8"
}, },
"dependencies": { "dependencies": {
"@fluentui/react-components": "^9.72.2", "@fluentui/react-components": "^9.72.9",
"@fluentui/react-datepicker-compat": "^0.6.20", "@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", "@fortawesome/fontawesome-free": "^6.7.2",
"@lit/context": "^1.1.6", "@lit/context": "^1.1.6",
"@microsoft/signalr": "^8.0.17", "@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

@@ -8,8 +8,8 @@ let
agenix = pkgs.callPackage "${sources.agenix}/pkgs/agenix.nix" { }; agenix = pkgs.callPackage "${sources.agenix}/pkgs/agenix.nix" { };
fable = pkgs.buildDotnetGlobalTool { fable = pkgs.buildDotnetGlobalTool {
pname = "fable"; pname = "fable";
version = "4.28.0"; version = "4.24.0";
nugetHash = "sha256-t5Kex6sVe1B/xErMfDav+WGEjeZjndRNQA2r0FvL92g="; nugetHash = "sha256-ERewWqfEyyZKpHFFALpMGJT0fDWywBYY5buU/wTZZTg=";
}; };
in in
pkgs.mkShellNoCC { pkgs.mkShellNoCC {
@@ -24,7 +24,7 @@ pkgs.mkShellNoCC {
# JavaScript # JavaScript
pkgs.bun pkgs.bun
pkgs.nodejs pkgs.nodejs_25
# Devlopment tools # Devlopment tools
pkgs.npins pkgs.npins
@@ -32,6 +32,7 @@ pkgs.mkShellNoCC {
pkgs.dive pkgs.dive
pkgs.nix-output-monitor pkgs.nix-output-monitor
pkgs.just pkgs.just
pkgs.skopeo
# Secret management with agenix # Secret management with agenix
agenix agenix
@@ -47,6 +48,10 @@ pkgs.mkShellNoCC {
DOTNET_ROOT = "${dotnet-sdk}/share/dotnet"; DOTNET_ROOT = "${dotnet-sdk}/share/dotnet";
LOG_LEVEL = "verbose"; LOG_LEVEL = "verbose";
shellHook = ''
scripts/update-rider.sh ${dotnet-sdk}/bin/dotnet
'';
# Alternative shells # Alternative shells
passthru = pkgs.lib.mapAttrs (name: value: pkgs.mkShellNoCC (value // { inherit name; })) { passthru = pkgs.lib.mapAttrs (name: value: pkgs.mkShellNoCC (value // { inherit name; })) {
pre-commit.shellHook = pre-commit.shellHook; pre-commit.shellHook = pre-commit.shellHook;

View File

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

View File

@@ -29,7 +29,7 @@ pkgs.mkShellNoCC {
SERVER_PORT = port + 85; SERVER_PORT = port + 85;
TILT_PORT = port + 50; TILT_PORT = port + 50;
DOTNET_ROOT = "${pkgs.dotnetCorePackages.sdk_9_0}/share/dotnet"; DOTNET_ROOT = "${pkgs.dotnetCorePackages.sdk_10_0}/share/dotnet";
shellHook = '' shellHook = ''
export PATH="$PWD/src/Cli/bin/Release/net9.0/linux-x64/:$PATH" 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) = let retireArchive (archive: string) =
// TODO: retire all dependent archies
let aid = let aid =
try try
Guid.Parse archive Guid.Parse archive

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"version": 2, "version": 2,
"dependencies": { "dependencies": {
"net9.0": { "net10.0": {
"Fargo.CmdLine": { "Fargo.CmdLine": {
"type": "Direct", "type": "Direct",
"requested": "[1.7.5, )", "requested": "[1.7.5, )",
@@ -71,8 +71,7 @@
"Microsoft.Extensions.Configuration.Abstractions": "9.0.1", "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
"Microsoft.Extensions.DependencyModel": "9.0.1", "Microsoft.Extensions.DependencyModel": "9.0.1",
"Microsoft.Extensions.Logging": "9.0.1", "Microsoft.Extensions.Logging": "9.0.1",
"Mono.TextTemplating": "3.0.0", "Mono.TextTemplating": "3.0.0"
"System.Text.Json": "9.0.1"
} }
}, },
"Microsoft.EntityFrameworkCore.Tools": { "Microsoft.EntityFrameworkCore.Tools": {
@@ -233,8 +232,7 @@
"resolved": "5.3.2", "resolved": "5.3.2",
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==", "contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
"dependencies": { "dependencies": {
"FSharp.Core": "4.3.2", "FSharp.Core": "4.3.2"
"System.Reflection.Emit.Lightweight": "4.3.0"
} }
}, },
"Google.Api.CommonProtos": { "Google.Api.CommonProtos": {
@@ -331,10 +329,7 @@
"resolved": "4.8.0", "resolved": "4.8.0",
"contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
"dependencies": { "dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "3.3.4", "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.CSharp": { "Microsoft.CodeAnalysis.CSharp": {
@@ -364,9 +359,7 @@
"Humanizer.Core": "2.14.1", "Humanizer.Core": "2.14.1",
"Microsoft.Bcl.AsyncInterfaces": "7.0.0", "Microsoft.Bcl.AsyncInterfaces": "7.0.0",
"Microsoft.CodeAnalysis.Common": "[4.8.0]", "Microsoft.CodeAnalysis.Common": "[4.8.0]",
"System.Composition": "7.0.0", "System.Composition": "7.0.0"
"System.IO.Pipelines": "7.0.0",
"System.Threading.Channels": "7.0.0"
} }
}, },
"Microsoft.CodeAnalysis.Workspaces.MSBuild": { "Microsoft.CodeAnalysis.Workspaces.MSBuild": {
@@ -376,8 +369,7 @@
"dependencies": { "dependencies": {
"Microsoft.Build.Framework": "16.10.0", "Microsoft.Build.Framework": "16.10.0",
"Microsoft.CodeAnalysis.Common": "[4.8.0]", "Microsoft.CodeAnalysis.Common": "[4.8.0]",
"Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]", "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]"
"System.Text.Json": "7.0.3"
} }
}, },
"Microsoft.EntityFrameworkCore.Abstractions": { "Microsoft.EntityFrameworkCore.Abstractions": {
@@ -534,16 +526,6 @@
"resolved": "17.11.4", "resolved": "17.11.4",
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA==" "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": { "Mono.TextTemplating": {
"type": "Transitive", "type": "Transitive",
"resolved": "3.0.0", "resolved": "3.0.0",
@@ -563,11 +545,7 @@
"ProjNET": { "ProjNET": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.0.0", "resolved": "2.0.0",
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==", "contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
"dependencies": {
"System.Memory": "4.5.3",
"System.Numerics.Vectors": "4.5.0"
}
}, },
"Serilog.Sinks.File": { "Serilog.Sinks.File": {
"type": "Transitive", "type": "Transitive",
@@ -591,11 +569,6 @@
"resolved": "6.0.0", "resolved": "6.0.0",
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
}, },
"System.Collections.Immutable": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ=="
},
"System.Composition": { "System.Composition": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.0.0", "resolved": "7.0.0",
@@ -644,128 +617,6 @@
"System.Composition.Runtime": "7.0.0" "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": { "entity": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
@@ -911,10 +762,7 @@
"type": "CentralTransitive", "type": "CentralTransitive",
"requested": "[2.5.0, )", "requested": "[2.5.0, )",
"resolved": "2.5.0", "resolved": "2.5.0",
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==", "contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
"dependencies": {
"System.Memory": "4.5.4"
}
}, },
"Newtonsoft.Json": { "Newtonsoft.Json": {
"type": "CentralTransitive", "type": "CentralTransitive",
@@ -1006,136 +854,6 @@
} }
} }
}, },
"net9.0/linux-x64": { "net10.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"
}
}
}
} }
} }

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ variables:
include: include:
- project: oceanbox/gitlab-ci - project: oceanbox/gitlab-ci
ref: v4.4 ref: v4.5
file: DotnetDeployment.gitlab-ci.yml file: DotnetDeployment.gitlab-ci.yml
inputs: inputs:
project-name: atlantis 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 \ RUN apt-get update \
&& apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev && apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev
@@ -12,4 +12,4 @@ ENV SERVER_CONTENT_ROOT=/app/public
COPY dist/ /app COPY dist/ /app
WORKDIR /app WORKDIR /app
CMD [ "dotnet", "/app/Server.dll" ] CMD [ "dotnet", "/app/Server.dll" ]

View File

@@ -74,6 +74,10 @@ local_resource(
ignore=[ ignore=[
'src/Server/bin', 'src/Server/bin',
'src/Server/obj', 'src/Server/obj',
'src/Server/Archmaester/obj',
'src/Server/Hipster/obj',
'src/Server/Petimeter/obj',
'src/Server/Common/obj',
'src/Shared/bin', 'src/Shared/bin',
'src/Shared/obj', 'src/Shared/obj',
], ],

View File

@@ -9,12 +9,12 @@ client_path := "src/Client"
test_path := "test" test_path := "test"
lib_path := "src/Interfaces" lib_path := "src/Interfaces"
dist_path := "dist" dist_path := "../../dist"
pack_path := "packages" pack_path := "../../packages"
vite_prod := "bunx --bun vite build -c ../../vite.config.js -m production --emptyOutDir --outDir " + "../../dist/public" 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_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 recipe - show available commands
default: default:
@@ -28,23 +28,31 @@ clean:
[parallel] [parallel]
bundle: clean bundle-server bundle-client bundle: clean bundle-server bundle-client
[working-directory: 'src/Server']
bundle-server: bundle-server:
dotnet build -tl -c Release -o {{dist_path}} {{server_path}} dotnet build -tl -c Release -o {{dist_path}}
bundle-client: [working-directory: 'src/Client']
fable --cwd {{client_path}} -e .jsx -o build --test:MSBuildCracker --run {{vite_prod}} install-client:
bun install --frozen-lockfile
[working-directory: 'src/Client']
bundle-client: install-client
# Build debug bundle (server + client) # Build debug bundle (server + client)
[parallel] [parallel]
bundle-debug: clean bundle-debug-server bundle-debug-client bundle-debug: clean bundle-debug-server bundle-debug-client
[working-directory: 'src/Server']
bundle-debug-server: bundle-debug-server:
dotnet build -tl -c Debug -o {{dist_path}} {{server_path}} dotnet build -tl -c Debug -o {{dist_path}}
[working-directory: 'src/Client']
bundle-debug-client: bundle-debug-client:
fable --cwd {{client_path}} -e .jsx -o build --test:MSBuildCracker --run {{vite_dev}} fable -e .jsx -o build --test:MSBuildCracker --run {{vite_dev}}
# Create NuGet package # Create NuGet package
[working-directory: 'src/Server']
pack: clean pack: clean
dotnet pack -c Release -o "{{pack_path}}" {{lib_path}} dotnet pack -c Release -o "{{pack_path}}" {{lib_path}}
@@ -52,12 +60,14 @@ pack: clean
[parallel] [parallel]
run: clean run-server run-client run: clean run-server run-client
[working-directory: 'src/Server']
run-server: run-server:
dotnet watch run {{server_path}} dotnet watch run
# Run client only in watch mode # Run client only in watch mode
run-client: [working-directory: 'src/Client']
fable watch --cwd {{client_path}} -e .jsx -o build --run {{vite}} run-client: install-client
fable watch -e .jsx -o build --test:MSBuildCracker --run {{vite}}
# Format code with Fantomas # Format code with Fantomas
format: format:
@@ -67,8 +77,10 @@ format:
[parallel] [parallel]
test: clean test-server test-client test: clean test-server test-client
[working-directory: 'src']
test-server: test-server:
dotnet run {{test_path}}/Server dotnet run {{test_path}}/Server
test-client: [working-directory: 'src/Client']
fable --cwd {{test_path}}/Client -e .jsx -o build --run {{vite}} test-client: install-client
fable -e .jsx -o build --run {{vite}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,12 +46,9 @@ let private update (msg: XtractMsg) (model: XtractModel) =
{ model with position = pos }, Elmish.Cmd.none { model with position = pos }, Elmish.Cmd.none
| SetData s -> | SetData s ->
console.debug ("[DataExtraction] SetData msg:", s) console.debug ("[DataExtraction] SetData msg:", s)
let data' = { model.data with name = s.name; fvcom = s.fvcom;} { model with data = s }, Elmish.Cmd.none
{ model with data = data' }, Elmish.Cmd.none | XtractMsg.SetStarted (started, jobIdOpt) -> { model with start = started, jobIdOpt }, Elmish.Cmd.none
| XtractMsg.SetStarted (started, jobIdOpt) -> | XtractMsg.ResetModel m -> { m with data.name = model.data.name }, Elmish.Cmd.none
{ 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 | XtractMsg.Noop () -> model, Elmish.Cmd.none
// //
@@ -105,7 +102,8 @@ let updateExtractionSite (posOpt: (float * float) option, map) =
match posOpt with match posOpt with
| Some pos -> | Some pos ->
let p' = pos |> posToCoord 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 ] let feature = Feature.feature [ feature.geometryOrProperties point ]
source.addFeature (feature) source.addFeature (feature)
| _ -> () | _ -> ()
@@ -124,28 +122,19 @@ let private extractionSiteControls (dispatch': XtractMsg -> unit) (xmodel': Xtra
if circle.intersectsCoordinate (pos |> posToCoord) then if circle.intersectsCoordinate (pos |> posToCoord) then
Some pos Some pos
else 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 None
let handleMapPlaceExtraction (coords: Coordinate) = let handleMapPlaceExtraction (coords: Coordinate) =
console.debug ($"[DataExtraction] Click add site: %s{coords.ToString ()}") console.debug ($"[DataExtraction] Click add site: %s{coords.ToString ()}")
coordToPos coords coordToPos coords |> tryFence |> SetExtractionSite |> dispatch'
|> tryFence
|> SetExtractionSite
|> dispatch'
let setPosition (pos: float * float) : unit = let setPosition (pos: float * float) : unit =
Some pos Some pos |> SetExtractionSite |> dispatch'
|> SetExtractionSite
|> dispatch'
let selectedPos = let selectedPos = xmodel'.position |> Option.defaultValue (0.0, 0.0) |> toWgs84'
xmodel'.position
|> Option.defaultValue (0.0, 0.0)
|> toWgs84'
let deleteSite (_: Browser.Types.Event) = let deleteSite (_: Browser.Types.Event) = None |> SetExtractionSite |> dispatch'
None |> SetExtractionSite |> dispatch'
let latitudeBox = let latitudeBox =
let latitude = snd selectedPos let latitude = snd selectedPos
@@ -264,21 +253,22 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
let map = model.map let map = model.map
let currentFrame = model.frame let currentFrame = model.frame
let archiveStartUTC = archive.startTime.ToUniversalTime () let archiveStartUTC = archive.startTime.ToUniversalTime ()
let archiveEndT = archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float) let archiveEndT =
let archiveStartT = archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float) archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
let archiveStartT =
archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
let submitted, setSubmitted = Hook.useState false let submitted, setSubmitted = Hook.useState false
let createNewModel () : XtractModel = let createNewModel () : XtractModel =
let data = let data =
match xtractModelOpt with match xtractModelOpt with
| Some existing -> { existing.data with fvcom = archive.id } | Some existing -> { existing.data with fvcom = archive.id }
| None -> | None -> {
{ XtractData.empty with
XtractData.empty with fvcom = archive.id
fvcom = archive.id start = archiveStartT
start = archiveStartT stop = archiveStartT.AddDays 2.0
stop = archiveStartT.AddDays 2.0 }
}
{ {
fence = archive.polygon fence = archive.polygon
start = false, None start = false, None
@@ -309,11 +299,21 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
SetXtractModel (Some newModel) |> dispatch SetXtractModel (Some newModel) |> dispatch
) )
let setStartDateTime (dt: DateTime) = 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) = 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 setName (s: string) =
let currentModel = modelRef.contents |> Option.defaultValue xmodel' let currentModel = modelRef.contents |> Option.defaultValue xmodel'
@@ -322,7 +322,10 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
let minStartDate = archiveStartUTC let minStartDate = archiveStartUTC
let maxStartDate = archiveEndT let maxStartDate = archiveEndT
let minEndDate = archiveStartUTC 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 = let metaControls =
html html
@@ -579,4 +582,4 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
</div> </div>
</div> </div>
{submitButtons} {submitButtons}
""" """

View File

@@ -43,16 +43,16 @@ let inboxDialog
let table = document.getElementById "inbox-table" let table = document.getElementById "inbox-table"
async { async {
let! mbox = Remoting.inboxApi().getMessages () let! mbox = Remoting.inboxApi().getMessages ()
// if mbox.Length = 0 then if mbox.Length = 0 then
// table?items <- [| { table?items <- [| {
// id = Guid.Empty id = Guid.Empty
// content = "" content = ""
// unread = false unread = false
// type' = MessageType.Note type' = MessageType.Note
// created = DateTime.Now created = DateTime.Now
// } |] } |]
// else else
table?items <- mbox table?items <- mbox
} |> Async.StartImmediate } |> Async.StartImmediate
Hook.useEffectOnChange(arg.unread, loadMessages) Hook.useEffectOnChange(arg.unread, loadMessages)
@@ -65,30 +65,18 @@ let inboxDialog
|> Set.ofSeq |> Set.ofSeq
|> setSelected |> setSelected
let doDelete _ = let doDelete selected _ =
let table = document.getElementById "inbox-table" let table = document.getElementById "inbox-table"
let selectedSet : Guid JS.Set = table?selectedSet
let items: InboxItem array = table?items 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 loadMessages ()
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
let doRead selected _ = let doRead selected _ =
let table = document.getElementById "inbox-table" let table = document.getElementById "inbox-table"
@@ -118,7 +106,7 @@ let inboxDialog
html $""" html $"""
<sp-field-group horizontal> <sp-field-group horizontal>
<sp-action-button <sp-action-button
@click={Ev(doDelete)} @click={Ev(doDelete selected)}
?disabled={selected.Count = 0}> ?disabled={selected.Count = 0}>
<sp-icon-delete slot="icon"></sp-icon-delete> Delete selected <sp-icon-delete slot="icon"></sp-icon-delete> Delete selected
</sp-action-button> </sp-action-button>
@@ -308,11 +296,8 @@ let inboxDialog
let sortFn = if sortDir = "asc" then Array.sortBy else Array.sortByDescending let sortFn = if sortDir = "asc" then Array.sortBy else Array.sortByDescending
table?items table?items
|> sortFn (fun item -> JS.expr_js $"{item}[{sortKey}]") |> sortFn (fun item -> JS.expr_js $"{item}[{sortKey}]")
|> fun items -> table?items <- items)) |> fun items -> table?items <- items)
)
let table =
html $"""
"""
html $""" html $"""
<div class="inbox-dialog"> <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)) polygon = a.polygon |> Option.map (Array.map (fun (x, y) -> float x, float y))
frames = archiveFrames frames = archiveFrames
} }
| Result.Error err -> | Error err ->
console.error $"Could not retrieve the selected archive!: {err}" console.error $"Could not retrieve the selected archive!: {err}"
return ArchiveInfo.empty return ArchiveInfo.empty
} }

View File

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

View File

@@ -100,7 +100,7 @@ let private simAccordion (dispatch: Msg -> unit) model =
console.debug $"policies: %A{model.simPolicies}" console.debug $"policies: %A{model.simPolicies}"
let disabled = model.archive.id = Guid.Empty 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 = let disabledPlume =
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false) model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
let disabledXtract = let disabledXtract =
@@ -189,7 +189,7 @@ let private simAccordion (dispatch: Msg -> unit) model =
<sp-action-button <sp-action-button
static="primary" static="primary"
style="flex-grow: 1" style="flex-grow: 1"
?disabled={disabled} ?disabled={disabledXtract }
@click={Ev (chooseMode (DataExtraction DefaultXtract))} @click={Ev (chooseMode (DataExtraction DefaultXtract))}
> >
Extract Data Extract Data

View File

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

View File

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

View File

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

View File

@@ -25,37 +25,37 @@
min-width: 400px; 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> </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> <script>
// NOTE: This should only be sent when we mount the script after confirming the id exists in sessionStorage // NOTE: This should only be sent when we mount the script after confirming the id exists in sessionStorage
function beforeSendHandler(type, payload) { function beforeSendHandler(type, payload) {
@@ -65,26 +65,26 @@
return payload; return payload;
} }
</script> </script>
<div id="winterContainer" class="winterContainer" aria-hidden="true"></div> <!-- <div id="winterContainer" class="winterContainer" aria-hidden="true"></div> -->
<script> <!-- <script> -->
// NOTE(mrtz): Add some snowflakes <!-- // NOTE(mrtz): Add some snowflakes -->
const container = document.getElementById('winterContainer'); <!-- const container = document.getElementById('winterContainer'); -->
const snowflakeCount = 150; <!-- const snowflakeCount = 150; -->
const snowflakeSymbols = ['❄', '❅', '❆', '❇', '❈', '❉', '❊', '❋'] <!-- const snowflakeSymbols = ['❄', '❅', '❆', '❇', '❈', '❉', '❊', '❋'] -->
for (let i = 0; i < snowflakeCount; i++) { <!-- for (let i = 0; i < snowflakeCount; i++) { -->
const snowflake = document.createElement('div'); <!-- const snowflake = document.createElement('div'); -->
const randomSymbol = snowflakeSymbols[Math.floor(Math.random() * snowflakeSymbols.length)]; <!-- const randomSymbol = snowflakeSymbols[Math.floor(Math.random() * snowflakeSymbols.length)]; -->
snowflake.className = 'winterSnowflake'; <!-- snowflake.className = 'winterSnowflake'; -->
snowflake.style.left = `${Math.random() * 100}%`; <!-- snowflake.style.left = `${Math.random() * 100}%`; -->
snowflake.style.animationDuration = `${10 + Math.random() * 10}s`; <!-- snowflake.style.animationDuration = `${10 + Math.random() * 10}s`; -->
snowflake.style.animationDelay = `${Math.random() * 10}s`; <!-- snowflake.style.animationDelay = `${Math.random() * 10}s`; -->
snowflake.style.setProperty('--swirl', `${Math.random() * 20 - 10}vw`); <!-- snowflake.style.setProperty('--swirl', `${Math.random() * 20 - 10}vw`); -->
snowflake.style.setProperty('--rot', `${Math.random() * 720 + 360}deg`); <!-- snowflake.style.setProperty('--rot', `${Math.random() * 720 + 360}deg`); -->
snowflake.textContent = randomSymbol; <!-- snowflake.textContent = randomSymbol; -->
container.appendChild(snowflake); <!-- container.appendChild(snowflake); -->
} <!-- } -->
</script> <!-- </script> -->
</head> </head>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Utils.fs" /> <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 grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
let dep = None let dep = None
let part = job.partition |> Option.defaultValue "long" let part = job.partition |> Option.defaultValue "long"
let cpt = 32
task { task {
try try
@@ -101,7 +102,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
Sites = job.input.release.groups |> Seq.sumBy _.sites.Length 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 () let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
match Decode.Auto.fromString<SlurmSubmissionResponse> s with 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 grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
let dep = job.dependency |> Option.map (fun jobId -> $"afterok:{jobId}") let dep = job.dependency |> Option.map (fun jobId -> $"afterok:{jobId}")
let part = job.partition |> Option.defaultValue "long" let part = job.partition |> Option.defaultValue "long"
let cpt = 32
task { task {
try try
@@ -179,7 +181,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
SimType = job.model.ToString() 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 () let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
match Decode.Auto.fromString<SlurmSubmissionResponse> s with match Decode.Auto.fromString<SlurmSubmissionResponse> s with

View File

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

View File

@@ -286,31 +286,35 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
} }
member this.getActiveJobs aid = member this.getActiveJobs aid =
Log.Debug $"get active jobs for: {this.myId}" task {
Log.Debug $"get active jobs for: {this.myId}"
let active = [| let active = [|
for v in this.jobs do for v in this.jobs do
Log.Debug $"job: {aid} -> %A{v.Value}" Log.Debug $"job: {aid} -> %A{v.Value}"
if v.Value.refId = aid then if v.Value.refId = aid then
match v.Value.status with match v.Value.status with
| JobStatus.New | JobStatus.New
| JobStatus.Waiting | JobStatus.Waiting
| JobStatus.Failed | JobStatus.Failed
| JobStatus.Running -> v.Value | JobStatus.Running -> v.Value
| _ -> () | _ -> ()
else else
() ()
|] |]
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}" Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
task { return active } return active
}
member this.getFenceRadius() = member this.getFenceRadius() =
Log.Information "fence?" task {
let r = settings.file.fenceRadius Log.Information "fence?"
Log.Information $"fence: {r}" let r = settings.file.fenceRadius
Task.FromResult r Log.Information $"fence: {r}"
return r
}
member this.checkFence aid (pts: (float * float) list) = member this.checkFence aid (pts: (float * float) list) =
task { return this.validatePoints aid pts } task { return this.validatePoints aid pts }

View File

@@ -149,7 +149,7 @@ type SlurmClient(client: HttpClient, settings: ISlurmClientSettings) =
Log.Information $"base: {client.BaseAddress}" Log.Information $"base: {client.BaseAddress}"
client.GetStringAsync "diag" 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 = { let jobProps = {
SlurmJobProps.empty with SlurmJobProps.empty with
name = jobName name = jobName
@@ -162,6 +162,7 @@ type SlurmClient(client: HttpClient, settings: ISlurmClientSettings) =
GROUP_ID = group GROUP_ID = group
env = env env = env
} }
cpus_per_task = cpu_per_task
partition = partition partition = partition
account = Some group[1..] // stripping leading '/' in group name account = Some group[1..] // stripping leading '/' in group name
comment = comment comment = comment

View File

@@ -66,6 +66,7 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
let xtractScript = let xtractScript =
let env = settings.appEnv |> AppEnv.FromString let env = settings.appEnv |> AppEnv.FromString
// TODO(mrtz): Add additional environments once excavator is migrated to atlas
match env with match env with
| Production -> "#!/bin/sh\nexec /opt/bin/excavator-production.run" | Production -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
| PreProd -> "#!/bin/sh\nexec /opt/bin/excavator-production.run" | PreProd -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
@@ -82,15 +83,22 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
let g = defaultArg groups [||] let g = defaultArg groups [||]
let grp = if g.Length > 0 then g[0] else "" let grp = if g.Length > 0 then g[0] else ""
let dep = None let dep = None
let part = partition |> Option.defaultValue "long" let part = partition |> Option.defaultValue "short"
let cpt = 4
taskResult { taskResult {
let! archiveProps = let! archiveName =
job.archiveId this.archivist.GetArchiveName job.archiveId
|> this.archivist.GetArchiveProps
|> TaskResult.mapError (fun e -> |> TaskResult.mapError (fun e ->
Log.Error $"[XtractActor.SubmitXtract] Failed to get archive props: %s{e}" Log.Error $"[XtractActor.SubmitXtract] Failed to get archive name: %s{e}"
$"Failed to get archive props: {e}" $"Failed to get archive name: {e}"
)
let! projection =
this.archivist.GetProjection job.archiveId
|> TaskResult.mapError (fun e ->
Log.Error $"[XtractActor.SubmitXtract] Failed to get projection: %s{e}"
$"Failed to get archive projection: {e}"
) )
let! basePath = let! basePath =
@@ -108,7 +116,7 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
) )
Log.Debug Log.Debug
$"[XtractActor] Got archive name: '{archiveProps.name}', basePath: '{basePath}', projection: '{archiveProps.projection}'" $"[XtractActor.SubmitXtract] Got archive name: '{archiveName}', basePath: '{basePath}', projection: '{projection}'"
let uniqueOutputDirs = let uniqueOutputDirs =
archiveFiles.series archiveFiles.series
@@ -126,8 +134,8 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
let enrichedJob = { let enrichedJob = {
job with job with
basePath = basePath basePath = basePath
caseName = archiveProps.name caseName = archiveName
projection = archiveProps.projection projection = projection
files = uniqueOutputDirs files = uniqueOutputDirs
} }
@@ -149,7 +157,7 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
Positions = 1 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 s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
let! slurmResponse = let! slurmResponse =

View File

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

View File

@@ -1,33 +1,35 @@
module Main module Main
open System open System
open System.Text.Json open System.Text.Json
open System.Text.Json.Serialization 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 Argu
open Dapr.Actors open Dapr.Actors
open Dapr.Actors.Client open Dapr.Actors.Client
open Dapr.Client open Dapr.Client
open FSharp.Data open FSharp.Data
open FsToolkit.ErrorHandling
open Fable.SignalR open Fable.SignalR
open Giraffe 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 Polly
open Prometheus open Prometheus
open Saturn open Saturn
open Saturn.Dapr open Saturn.Dapr
open Saturn.OpenTelemetry
open Saturn.Observer open Saturn.Observer
open Saturn.OpenTelemetry
open Sentry open Sentry
open Sentry.AspNetCore open Sentry.AspNetCore
open Sentry.Extensibility open Sentry.Extensibility
@@ -37,6 +39,7 @@ open Serilog.Sinks.OpenTelemetry
open Atlantis open Atlantis
open Atlantis.Shared open Atlantis.Shared
open Oceanbox.DataAgent
open Oceanbox.ServerPack.MultiAuth open Oceanbox.ServerPack.MultiAuth
open Saturn.OpenFga open Saturn.OpenFga
open Settings open Settings
@@ -82,7 +85,8 @@ let configureSerilog () =
let configureLogging (builder: ILoggingBuilder) = let configureLogging (builder: ILoggingBuilder) =
builder builder
.ClearProviders() .ClearProviders()
.AddSerilog() |> ignore .AddSerilog()
|> ignore
let corsPolicy (policy: CorsPolicyBuilder) = let corsPolicy (policy: CorsPolicyBuilder) =
policy policy
@@ -91,7 +95,7 @@ let corsPolicy (policy: CorsPolicyBuilder) =
.AllowAnyMethod() .AllowAnyMethod()
.WithOrigins(appsettings.file.allowedOrigins) .WithOrigins(appsettings.file.allowedOrigins)
.SetIsOriginAllowedToAllowWildcardSubdomains() .SetIsOriginAllowedToAllowWildcardSubdomains()
|> ignore |> ignore
type UserIdProvider() = type UserIdProvider() =
interface IUserIdProvider with interface IUserIdProvider with
@@ -254,12 +258,12 @@ let stopImpersonating (next: HttpFunc) (ctx: HttpContext) =
let getBarentsWatchToken (next: HttpFunc) (ctx: HttpContext) = let getBarentsWatchToken (next: HttpFunc) (ctx: HttpContext) =
task { task {
let! tokenRes = let! tokenRes =
tryGetEnv "BARENTSWATCH_CLIENT_ID" taskResult {
|> Option.bind (fun id -> let! id = tryGetEnv "BARENTSWATCH_CLIENT_ID" |> Result.requireSome "Missing barentswatch client id"
tryGetEnv "BARENTSWATCH_SECRET" let! secret = tryGetEnv "BARENTSWATCH_SECRET" |> Result.requireSome "Missing barentswatch secret"
|> Option.map (BarentsWatch.getToken appsettings.redis id)) let! token = BarentsWatch.getToken appsettings.redis id secret
|> Option.defaultValue (Error "Secret or client id missing" |> async.Return) return token
|> Async.StartAsTask }
match tokenRes with match tokenRes with
| Error err -> | Error err ->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ variables:
include: include:
- project: oceanbox/gitlab-ci - project: oceanbox/gitlab-ci
ref: v4.4 ref: v4.5
file: DotnetDeployment.gitlab-ci.yml file: DotnetDeployment.gitlab-ci.yml
inputs: inputs:
project-name: codex 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 WORKDIR /app
COPY dist /app COPY dist /app

View File

@@ -2,6 +2,7 @@ name='codex'
cluster='oceanbox' cluster='oceanbox'
env=os.getenv('APP_ENV') env=os.getenv('APP_ENV')
username=os.getenv('USER')
namespace=os.getenv('APP_NAMESPACE') namespace=os.getenv('APP_NAMESPACE')
app = '{}-{}'.format(env, name) app = '{}-{}'.format(env, name)
@@ -39,7 +40,9 @@ docker_build_with_restart(
ignore = [ 'src/Client' ] 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') k8s_resource(app, port_forwards='8085:8085')
# vim:ft=python # vim:ft=python

View File

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

View File

@@ -5,6 +5,7 @@ open Fable.Core
open Fable.Core.JsInterop open Fable.Core.JsInterop
open Feliz open Feliz
open Feliz.Router open Feliz.Router
open FS.FluentUI
module Archive = module Archive =
let private fetchArchiveAcl (id: System.Guid) : JS.Promise<Result<Archmaester.Dto.ArchiveAcl, string>> = let private fetchArchiveAcl (id: System.Guid) : JS.Promise<Result<Archmaester.Dto.ArchiveAcl, string>> =
@@ -29,16 +30,6 @@ module Archive =
return subs 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) = let private addArchiveGroup (archiveId: System.Guid) (group: string) =
promise { promise {
try try
@@ -227,9 +218,6 @@ module Archive =
let View (archiveId: System.Guid) = let View (archiveId: System.Guid) =
let loading, setLoading = React.useState true let loading, setLoading = React.useState true
let error, setError = React.useState<string option> None 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 selectedGroup, setSelectedGroup = 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 aclOpt, setAcl = React.useState<Archmaester.Dto.ArchiveAcl option> None let aclOpt, setAcl = React.useState<Archmaester.Dto.ArchiveAcl option> None
@@ -257,21 +245,6 @@ module Archive =
| None -> | None ->
console.warn("ACL has not been downloaded") 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) = let handleSelectedGroupChange (groupOpt: string option) =
console.debug("Selected group: %s", groupOpt) console.debug("Selected group: %s", groupOpt)
setSelectedGroup groupOpt setSelectedGroup groupOpt
@@ -319,94 +292,27 @@ module Archive =
| None -> | None ->
match archiveOpt with match archiveOpt with
| Some archive -> | Some archive ->
Html.h1 (sprintf "Archive %s" archive.name) Fui.text.title1 (sprintf "Archive %s" archive.name)
if deleting then Html.div [
Html.h2 "Deleting archive ..." prop.classes [ "flex-row"; "gap-8" ]
else prop.children [
Html.none Archives.EditArchiveDialog archive (fun edited ->
Some
if deleted then {archive with
Html.div [ name = edited.Name
prop.children [ startTime = edited.StartTime
Html.h2 "Archive successfully deleted" endTime = edited.StartTime.AddHours(edited.Frames)
Html.a [ frames = edited.Frames
prop.href (Router.format "archives") isPublished = edited.Published
prop.text "Return to archives listing" 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 Archives.InfoSection archive
@@ -744,4 +650,4 @@ module Archive =
| None -> | None ->
Html.h1 "Archive not found" Html.h1 "Archive not found"
] ]

View File

@@ -1,6 +1,8 @@
namespace Oceanbox.Codex namespace Oceanbox.Codex
open Fable.Core
open Feliz open Feliz
open FS.FluentUI
type Archives = type Archives =
[<ReactComponent>] [<ReactComponent>]
@@ -13,49 +15,131 @@ type Archives =
Html.section [ Html.section [
prop.classes [ "flex-row"; "flex-wrap"; "gap-16" ] prop.classes [ "flex-row"; "flex-wrap"; "gap-16" ]
prop.children [ prop.children [
Html.ul [ Fui.table [
prop.children [ table.classes [ "flex-basis-7"; "flex-grow" ]
Html.li [ table.children [
prop.text (sprintf "Description: %s" archive.description) Fui.tableBody [
] Fui.tableRow [
Html.li [ tableRow.children [
prop.text (sprintf "Archive type: %s" (string archive.archiveType)) Fui.tableCell [
] Fui.tableCellLayout [
Html.li [ tableCellLayout.media (Fui.icon.textDescriptionRegular [])
prop.text (sprintf "Projection: %s" archive.projection) tableCellLayout.children [ Fui.text "Description" ]
] ]
Html.li [ ]
prop.text (sprintf "Frequency: %d" archive.freq) Fui.tableCell [ Fui.text archive.description ]
] ]
Html.li [ ]
prop.text (sprintf "Frames: %d" archive.frames) Fui.tableRow [
] tableRow.children [
Html.li [ Fui.tableCell [
prop.text (sprintf "Created: %s" (archive.created.ToLongDateString())) Fui.tableCellLayout [
] tableCellLayout.media (Fui.icon.contactCardGenericRegular [])
Html.li [ tableCellLayout.children [ Fui.text "Archive type" ]
prop.text (sprintf "Start time: %s" (archive.startTime.ToLongDateString())) ]
] ]
Html.li [ Fui.tableCell [ Fui.text (string archive.archiveType) ]
prop.text (sprintf "End time: %s" (archive.endTime.ToLongDateString())) ]
] ]
Html.li [ Fui.tableRow [
prop.text (sprintf "Length: %d days %d hours" archiveLength.Days archiveLength.Hours) tableRow.children [
] Fui.tableCell [
Html.li [ Fui.tableCellLayout [
prop.text (sprintf "Owner: %s" archive.owner) tableCellLayout.media (Fui.icon.globeSurfaceRegular [])
] tableCellLayout.children [ Fui.text "Projection" ]
Html.li [ ]
prop.text (sprintf "Expires: %s" (archive.expires |> Option.map string |> Option.defaultValue "")) ]
] Fui.tableCell [ Fui.text archive.projection ]
Html.li [ ]
prop.text (sprintf "Publised: %b" archive.isPublished) ]
] Fui.tableRow [
Html.li [ tableRow.children [
prop.text (sprintf "Public: %b" archive.isPublic) Fui.tableCell [
] Fui.tableCellLayout [
Html.li [ tableCellLayout.media (Fui.icon.timerRegular [])
prop.text (sprintf "Location: %s" "tos") 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 -> | Ok archives ->
setArchives archives setArchives archives
setError None setError None
| Error err -> setError (Some "Error fetching archives") | Error err ->
setError (Some "Error fetching archives")
setLoading false setLoading false
) )
@@ -196,4 +197,4 @@ type Archives =
] ]
] ]
] ]
] ]

View File

@@ -5,6 +5,7 @@ module Utils =
open Fable.Core open Fable.Core
open Fable.Core.JsInterop open Fable.Core.JsInterop
open Fable.Remoting.Client open Fable.Remoting.Client
open FsToolkit.ErrorHandling
open Oceanbox.Codex open Oceanbox.Codex
open Oceanbox.Codex.Types open Oceanbox.Codex.Types
@@ -71,3 +72,52 @@ module Utils =
return Error "Error fetching archive count" 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"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<RootNamespace>Oceanbox</RootNamespace> <RootNamespace>Oceanbox</RootNamespace>
<Version>0.0.0-alpha.1</Version> <Version>0.0.1</Version>
<DefineConstants>FABLE_COMPILER</DefineConstants> <DefineConstants>FABLE_COMPILER</DefineConstants>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="../Shared/Remoting.fs" /> <Compile Include="../Shared/Remoting.fs" />
<Compile Include="Types.fs" /> <Compile Include="Types.fs" />
<Compile Include="Utils.fs" />
<Compile Include="Extensions.fs" /> <Compile Include="Extensions.fs" />
<Compile Include="Remoting.fs" /> <Compile Include="Remoting.fs" />
<Compile Include="Map.fs" /> <Compile Include="Map.fs" />
@@ -18,6 +19,8 @@
<Compile Include="OpenFGA/useReadTuples.fs" /> <Compile Include="OpenFGA/useReadTuples.fs" />
<Compile Include="OpenFGA/Checkbox.fs" /> <Compile Include="OpenFGA/Checkbox.fs" />
<Compile Include="OpenFGA/ArchiveOwnerList.fs" /> <Compile Include="OpenFGA/ArchiveOwnerList.fs" />
<Compile Include="Users/DeleteForm.fs" />
<Compile Include="Users/OpenFgaList.fs" />
<Compile Include="Groups/Utils.fs" /> <Compile Include="Groups/Utils.fs" />
<Compile Include="Groups/useGroups.fs" /> <Compile Include="Groups/useGroups.fs" />
<Compile Include="Groups/List.fs" /> <Compile Include="Groups/List.fs" />
@@ -34,6 +37,7 @@
<Compile Include="Groups.fs" /> <Compile Include="Groups.fs" />
<Compile Include="GroupArchiveAddForm.fs" /> <Compile Include="GroupArchiveAddForm.fs" />
<Compile Include="GroupArchive.fs" /> <Compile Include="GroupArchive.fs" />
<Compile Include="GroupUser.fs" />
<Compile Include="Group.fs" /> <Compile Include="Group.fs" />
<Compile Include="ArchivesList.fs" /> <Compile Include="ArchivesList.fs" />
<Compile Include="Archives.fs" /> <Compile Include="Archives.fs" />
@@ -54,6 +58,7 @@
<PackageReference Include="Fable.Remoting.Client" /> <PackageReference Include="Fable.Remoting.Client" />
<PackageReference Include="Feliz" /> <PackageReference Include="Feliz" />
<PackageReference Include="Feliz.Router" /> <PackageReference Include="Feliz.Router" />
<PackageReference Include="FS.FluentUI" />
<PackageReference Include="Feliz.UseElmish" /> <PackageReference Include="Feliz.UseElmish" />
<PackageReference Include="FsToolkit.ErrorHandling" /> <PackageReference Include="FsToolkit.ErrorHandling" />
</ItemGroup> </ItemGroup>

View File

@@ -4,8 +4,108 @@ open Browser
open Fable.Core open Fable.Core
open Feliz open Feliz
open Feliz.Router 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 = 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> /// <summary>
/// A React component that uses Feliz.Router to determine what to show based on the current URL /// A React component that uses Feliz.Router to determine what to show based on the current URL
/// </summary> /// </summary>
@@ -39,19 +139,30 @@ type Components =
elif not authed then elif not authed then
Html.h1 "Redirecting to sign-in..." Html.h1 "Redirecting to sign-in..."
else else
match currentUrl with Html.div [
| [ ] -> Index.View() prop.classes ["flex-row"; "gap-16"]
| [ "archives" ] -> Archives.View () prop.children [
| [ "archives"; archive ] -> Archive.View (System.Guid archive) Components.Sidebar currentUrl
| [ "model-areas" ] -> ModelAreas.List () Html.div [
| [ "model-areas"; id ] -> ModelArea.View (System.Guid id) prop.classes ["grow"]
| [ "groups" ] -> Groups.View () prop.children [
| [ "groups"; group ] -> Group.View group match currentUrl with
| [ "groups"; group; "archives"; id ] -> GroupArchive.View group (System.Guid id) | [ ] -> Index.View()
| [ "groups"; group; "users"; user ] -> User.View user | [ "archives" ] -> Archives.View ()
| [ "users"; user ] -> User.View user | [ "archives"; Route.Guid archive ] -> Archive.View archive
| [ "organizations" ] -> Organizations.List () | [ "model-areas" ] -> ModelAreas.List ()
| [ "organizations"; org ] -> Organization.View org | [ "model-areas"; Route.Guid id ] -> ModelArea.View id
| otherwise -> Html.h1 "Not found" | [ "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 = module Group =
open Browser open Browser
open Fable.Core open Fable.Core
open FsToolkit.ErrorHandling
open Oceanbox.Codex.Types open Oceanbox.Codex.Types
@@ -17,59 +16,9 @@ module Group =
return Error (sprintf "Error fetching users for group %s" 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 = module private Elmish =
open 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 = type Msg =
| FetchArchives of string | FetchArchives of string
| SetArchiveAdding of bool | SetArchiveAdding of bool
@@ -97,7 +46,7 @@ module Group =
let update msg model = let update msg model =
match msg with match msg with
| FetchArchives group -> | FetchArchives group ->
model, Cmd.OfPromise.either fetchArchiveProps group SetArchives HandleExn model, Cmd.OfPromise.either Archives.Utils.fetchGroupArchiveProps group SetArchives HandleExn
| HandleExn ex -> | HandleExn ex ->
let msg = let msg =
match ex with match ex with
@@ -470,4 +419,4 @@ module Group =
] ]
] ]
] ]
] ]

View File

@@ -1,10 +1,40 @@
namespace Oceanbox.Codex namespace Oceanbox.Codex
type private Permission = {
Tuple: Remoting.Tuple
Relation: Remoting.ArchiveRelation
}
module GroupArchive = module GroupArchive =
open Browser open Browser
open Fable.Core open Fable.Core
open Feliz open Feliz
open Feliz.Router 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>] [<ReactComponent>]
let private DeleteRelationButton onDelete (tuple: Remoting.Tuple) = let private DeleteRelationButton onDelete (tuple: Remoting.Tuple) =
@@ -20,74 +50,137 @@ module GroupArchive =
else else
// TODO: Should probably just return unit and error if not deleted // TODO: Should probably just return unit and error if not deleted
console.warn ("[Group] Tuple was not deleted: %o", tuple) 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>] [<ReactComponent>]
let private ViewTerm group archiveId (onDelete: Remoting.Tuple -> unit) (viewTerm: Remoting.ViewTerm) = let private PermissionCard (title: string) onDelete (tuple: Remoting.Tuple) (children: ReactElement array) =
let tuple =
Remoting.Tuple.delete (
user = Groups.Utils.fgaMember group,
relation = "view",
object = sprintf "archive:%O" archiveId
)
Html.div [ Html.div [
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ] 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 [ prop.children [
Html.div [ Html.div [
prop.classes [ "flex-row-center" ] prop.classes [ "flex-row-center" ]
prop.children [ 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 DeleteRelationButton onDelete tuple
] ]
] ]
Html.div [ Html.div [
prop.classes [ "ml-16" ] prop.children children
prop.children [
Html.div (sprintf "Start time: %s" (Intl.shortDateTime viewTerm.StartTime))
Html.div (sprintf "End time: %s" (Intl.shortDateTime viewTerm.EndTime))
]
] ]
] ]
] ]
[<ReactComponent>] [<ReactComponent>]
let private ExecTicket (group: string) (archiveId: System.Guid) onDelete (ticket: Remoting.ExecTicket) = let private ViewTerm
let tuple = key
Remoting.Tuple.delete ( (onUpdate: Permission -> unit)
user = Groups.Utils.fgaMember group, (permission: Permission)
relation = "exec", (viewTerm: Remoting.ViewTerm)
object = sprintf "archive:%O" archiveId =
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 [ Fui.table [
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ] table.size.medium
prop.style [ style.flexBasis (length.px 320) ] table.children [
prop.children [ Fui.tableBody [
Html.div [ tableBody.children [
prop.classes [ "flex-row-center" ] Fui.tableRow [
prop.children [ tableRow.key "view-term-start-time"
Html.div [ prop.classes [ "grow" ]; prop.children [ Html.b "Exec Ticket" ] ] tableRow.children [
Fui.tableCell [
tableCell.text "Start time"
]
DeleteRelationButton onDelete tuple Fui.tableCell [
] tableCell.children (
] Fui.tableCellLayout [
Html.div [ tableCellLayout.children (
prop.classes [ "ml-16" ] Fui.datePicker [
prop.children [ datePicker.size.small
Html.div (sprintf "Start time: %s" (Intl.shortDateTime ticket.StartTime)) datePicker.onSelectDate handleStartChange
Html.div (sprintf "End time: %s" (Intl.shortDateTime ticket.EndTime)) datePicker.value (Some viewTerm.StartTime)
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.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>] [<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 adding, setAdding = React.useState false
let loading, setLoading = React.useState false
let success, setSuccess = React.useState false
let handleAddClick (ev: Types.Event) = setAdding true // Create a list of permissions missing from the archive
let handleCancelClick (ev: Types.Event) = setAdding false 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 if exists then
let hasExecTicket = permissions |> Array.exists _.IsExecTicket 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 [ React.fragment [
Html.div [ Html.div [
@@ -113,52 +470,39 @@ module GroupArchive =
"gap-8" "gap-8"
] ]
prop.children [ prop.children [
if adding then if loading then
Html.button [ prop.onClick handleAddClick; prop.text "Save" ] Html.p "Loading ..."
Html.button [
prop.onClick handleCancelClick
prop.text "Cancel"
]
else 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 if adding then
Html.div [ Html.div [
prop.id "group-archive-exec-form" prop.id "group-archive-exec-form"
prop.classes [ prop.classes [
"flex-row" "flex-row-start"
"gap-32" "gap-32"
] ]
prop.children [ prop.children [
if not hasViewTerm then availablePermissions
Html.div [ |> Array.map (fun permission ->
prop.children [ PermissionCreateCard group archiveId handlePermissionAdd permission
Html.b "View" )
Groups.ViewForm (Remoting.ViewTerm.empty, ignore) |> unbox
]
]
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)
]
]
] ]
] ]
] ]
@@ -167,22 +511,77 @@ module GroupArchive =
let View (group: string) (archiveId: System.Guid) = let View (group: string) (archiveId: System.Guid) =
let loading, setLoading = React.useState true let loading, setLoading = React.useState true
let error, setError = React.useState<string option> None let error, setError = React.useState<string option> None
let archiveOpt, setArchive = let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
React.useState<Archmaester.Dto.ArchiveProps option> None
let fgaUser = Groups.Utils.fgaMember group let fgaUser = Groups.Utils.fgaMember group
let tuples = let tuples = OpenFGA.useReadTuples (fgaUser, object = sprintf "archive:%O" archiveId)
OpenFGA.useReadTuples (fgaUser, object = sprintf "archive:%O" archiveId)
let relations: OpenFGA.Types.ArchiveRelation array = let permissions: Permission array =
tuples.Tuples 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) = let handlePermissionDelete (tuple: Remoting.Tuple) =
console.debug("Deleting %o from %o", tuple, tuples)
tuples.Tuples tuples.Tuples
|> Array.filter (fun existing -> existing.Relation <> tuple.Relation) |> Array.filter (fun existing -> existing.Relation <> tuple.Relation)
|> tuples.SetTuples |> 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 ( React.useEffect (
(fun () -> (fun () ->
setLoading true setLoading true
@@ -210,7 +609,10 @@ module GroupArchive =
Html.h1 [ Html.h1 [
prop.children [ prop.children [
Html.text "Group " 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 " / "
Html.text "Archive " Html.text "Archive "
Html.a [ 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 Archives.InfoSection archive
@@ -228,9 +637,13 @@ module GroupArchive =
prop.children [ prop.children [
Html.h2 "Permissions" Html.h2 "Permissions"
if not tuples.Loading then if tuples.Loading then
Html.p "Loading ..."
else
Html.div [ Html.div [
prop.children [ PermissionForm relations group ] prop.children [
PermissionForm group archive.archiveId handleAddPermission permissions
]
] ]
Html.div [ Html.div [
@@ -240,23 +653,28 @@ module GroupArchive =
] ]
] ]
if Array.isEmpty relations then if Array.isEmpty permissions then
Html.p "No permissions" Html.p "No permissions"
else else
Html.div [ Html.div [
prop.classes [ "flex-row"; "gap-32" ] prop.classes [ "flex-row-start"; "gap-32" ]
prop.children ( prop.children (
relations permissions
|> Array.map ( |> Array.map (fun permission ->
function match permission.Relation with
| OpenFGA.Types.ArchiveRelation.ViewTerm term -> | Remoting.ArchiveRelation.ViewTerm term ->
ViewTerm group archive.archiveId handlePermissionDelete term PermissionCard "View Term" handlePermissionDelete permission.Tuple [|
| OpenFGA.Types.ArchiveRelation.ExecTicket ticket -> ViewTerm "view-term-table" handlePermissionUpdate permission term
ExecTicket group archive.archiveId handlePermissionDelete ticket |]
| 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 } onChange { view with EndTime = endTime }
Html.div [ Html.div [
prop.classes [ "flex-column"; "gap-8"]
prop.children [ prop.children [
Html.div [ Html.div [
prop.classes [ "flex-row-start"]
prop.children [ prop.children [
Html.label [ Html.label [
prop.text "Start date" prop.style [
style.flexBasis (length.px 128)
]
prop.text "Start date:"
] ]
Html.input [ Html.input [
@@ -36,9 +41,13 @@ type Groups =
] ]
Html.div [ Html.div [
prop.classes [ "flex-row-start"]
prop.children [ prop.children [
Html.label [ Html.label [
prop.text "End date" prop.style [
style.flexBasis (length.px 128)
]
prop.text "End date:"
] ]
Html.input [ 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." 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 namespace Oceanbox.Codex
open Fable.Core
open Feliz open Feliz
open Feliz.Router open Feliz.Router
open FS.FluentUI
open Browser
module Index = module Index =
[<Literal>]
let private noGroup = "Select Group..."
type private User = { Name: string; Group: string }
// TODO: Elmish?
[<ReactComponent>] [<ReactComponent>]
let View () = let private UserAddForm onAdd =
Html.main [ let email, setEmail = React.useState ""
Html.h1 "Codex" let selectedGroup, setSelectedGroup = React.useState noGroup
Html.h2 "Index" let recentlyAddedUsers, setRecentlyAddedUsers = React.useState<User array> [||]
Html.div [ let groups = Groups.useGroups ()
prop.children [ // TODO: Fui Toast?
Html.ul [ let errMsg, setErrMsg = React.useState None
Html.li [
Html.a [ let handleAddUser () =
prop.href (Router.format "archives") let newUser = {
prop.text "archives" 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 [ Fui.field [
Html.a [ field.label (
prop.href (Router.format "model-areas") Fui.label [
prop.text "model areas" 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.div [
Html.a [ prop.classes [ "flex-row"; "gap-8"; ]
prop.href (Router.format "groups") prop.children [
prop.text "groups" Fui.button [
] button.onClick (fun _ -> handleAddUser ())
button.text "Save"
button.icon (Fui.icon.saveRegular [])
button.appearance.primary
button.disabled <| (email = "" || selectedGroup = noGroup)
] ]
Fui.button [
Html.li [ button.onClick (fun _ ->
Html.a [ setEmail ""
prop.href (Router.format "organizations") setSelectedGroup noGroup
prop.text "organizations" )
] 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 = module Main =
open Browser open Browser
open Feliz 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") 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 Browser
open Fable.Core open Fable.Core
open Feliz open Feliz
open FS.FluentUI
[<Erase>] [<Erase>]
type OpenFGA = type OpenFGA =
[<ReactComponent>] [<ReactComponent>]
static member Checkbox(key, label: string, user: string, relation: string, object: string) = 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 isChecked, setChecked = React.useState false
let handleChange (ev: Types.Event) = 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 // 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 ( React.useEffect (
(fun () -> (fun () ->
setLoading true
Remoting.openFgaApi.Check { Remoting.openFgaApi.Check {
User = user User = user
Relation = relation Relation = relation
@@ -30,6 +52,8 @@ type OpenFGA =
setChecked hasRelation setChecked hasRelation
| Error err -> | Error err ->
console.error("[OpenFGA.Checkbox] Error checking user %s has relation %s to %s", user, relation, object) 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 [ Html.div [
prop.classes [ "flex-row-center" ] prop.classes [ "flex-row-center" ]
prop.children [ prop.children [
Html.input [ if isLoading then
prop.id (sprintf "openfga-checkbox-%s" key) Fui.spinner [
prop.type'.checkbox spinner.size.tiny
prop.onChange handleChange ]
prop.custom("checked", isChecked) else
] Html.input [
prop.id (sprintf "openfga-checkbox-%s" key)
prop.type'.checkbox
prop.onChange handleChange
prop.custom("checked", isChecked)
]
Html.label [ Html.label [
prop.htmlFor (sprintf "openfga-checkbox-%s" key) prop.htmlFor (sprintf "openfga-checkbox-%s" key)
prop.text label prop.text label
] ]
] ]
] ]

View File

@@ -31,6 +31,8 @@ module Types =
let decode = Decode.fromString decoder let decode = Decode.fromString decoder
let encode : Remoting.ViewTerm -> string = Encode.Auto.toString
module ExecTicket = module ExecTicket =
open Thoth.Json open Thoth.Json
@@ -55,12 +57,59 @@ module Types =
let decode = Decode.fromString decoder let decode = Decode.fromString decoder
[<RequireQualifiedAccess>] module ArchiveRelation =
type ArchiveRelation = open Browser
| ViewTerm of Remoting.ViewTerm open Fable.Core
| ExecTicket of Remoting.ExecTicket
static member tryOfCondition(cond: Remoting.Condition) = let tryOfCondition(cond: Remoting.Condition) =
match cond.Name with match cond.Name with
| "term" -> cond.Context |> ViewTerm.decode |> Result.toOption |> Option.map ViewTerm | "term" ->
| "ticket" -> cond.Context |> ExecTicket.decode |> Result.toOption |> Option.map ExecTicket match ViewTerm.decode cond.Context with
| _ -> None | 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>] [<Hook>]
static member useObjects(user: string, relation: string, objectType: string, ?context: obj) : Objects = 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 ( React.useEffect (
(fun () -> (fun () ->
setObjects { objects with Loading = true; Error = None } setLoading true
setError None
OpenFGA.fetchObjects(user, relation, objectType, context) OpenFGA.fetchObjects(user, relation, objectType, context)
|> Promise.iter (fun res -> |> Promise.iter (fun res ->
match res with match res with
| Ok newObjects -> | Ok newObjects ->
setObjects { objects with Loading = false; Objects = newObjects } setObjects newObjects
| Error err -> | Error err ->
console.error("[OpenFGA] Error loading user objects %s", err) console.error("[OpenFGA] Error loading user objects: %s", err)
setObjects { objects with Loading = false; Error = Some err } setError (Some err)
setLoading false
) )
), ),
[| box user |] [| box user |]
) )
objects let props = {
Loading = isLoading
Error = error
Objects = objects
}
props

View File

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

View File

@@ -1,136 +1,61 @@
namespace Oceanbox.Codex namespace Oceanbox.Codex
open Browser
open Fable.Core
open Fable.Remoting.Client
open Feliz open Feliz
open Feliz.Router 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 = 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>] [<ReactComponent>]
let View (user: string) = let View (user: string) =
let fgaUser = sprintf "user:%s" user 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.main [
Html.h1 user Html.h1 user
Html.section [ Html.section [
prop.children [ 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 [ Html.section [
prop.children [ prop.children [
Html.h2 "Archmaester" Html.h2 "Archmaester"
@@ -170,7 +95,7 @@ module User =
style.maxHeight (length.px 512) style.maxHeight (length.px 512)
] ]
prop.children [ prop.children [
User.List(fgaUser, "owner", "archive") Users.OpenFgaList(fgaUser, "owner", "archive")
] ]
] ]
] ]
@@ -192,7 +117,7 @@ module User =
style.maxHeight (length.px 512) style.maxHeight (length.px 512)
] ]
prop.children [ 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) style.maxHeight (length.px 512)
] ]
prop.children [ 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 echo Building Codex frontend
fable -e .jsx -o build --verbose --run \ fable -e .jsx -o build --verbose --test:MSBuildCracker --run \
bunx --bun vite build -d -c ../../vite.config.js --mode development --minify false --outDir /home/simkir/oceanbox/poseidon/src/Codex/dist/WebRoot bunx --bun \
vite build -d -c ../../vite.config.js --mode development --minify false --outDir ../../dist/WebRoot

View File

@@ -1,15 +1,52 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Feliz App</title> <title>Codex</title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'> <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1"> <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-32x32.png" sizes="32x32" />
<link rel="shortcut icon" type="image/png" href="/img/favicon-16x16.png" sizes="16x16" /> <link rel="shortcut icon" type="image/png" href="/img/favicon-16x16.png" sizes="16x16" />
<link rel="stylesheet" href="/main.css" /> <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> </head>
<body> <body style="margin: 0;">
<div id="feliz-app"></div> <div id="feliz-app"></div>
<script type="module" src="/build/Main.jsx"></script> <script type="module" src="/build/Main.jsx"></script>
</body> </body>
</html> </html>

View File

@@ -1,7 +1,7 @@
{ {
"version": 2, "version": 2,
"dependencies": { "dependencies": {
"net9.0": { "net10.0": {
"Fable.Core": { "Fable.Core": {
"type": "Direct", "type": "Direct",
"requested": "[4.4.0, )", "requested": "[4.4.0, )",
@@ -75,6 +75,16 @@
"Fable.Elmish": "4.0.0" "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": { "FsToolkit.ErrorHandling": {
"type": "Direct", "type": "Direct",
"requested": "[5.0.1, )", "requested": "[5.0.1, )",

View File

@@ -116,6 +116,15 @@ h1 {
border-radius: 8px; border-radius: 8px;
} }
.bcol-neutral-stroke-1 {
border-color: var(--colorNeutralStroke1);
}
.br-solid {
border-right-width: 2px;
border-right-style: solid;
}
.text-overflow { .text-overflow {
overflow: hidden; overflow: hidden;
white-space: nowrap; 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; 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 */ /* special stuff */
.archives-list { .archives-list {
@@ -147,4 +176,4 @@ h1 {
flex: 2 1 512px; flex: 2 1 512px;
min-width: 512px; min-width: 512px;
max-width: 576px; max-width: 576px;
} }

View File

@@ -13,6 +13,19 @@ module Admin =
open Oceanbox 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 = module private Handler =
let addUsers (ctx: HttpContext) (req: Remoting.AddUsersRequest) : Async<Result<unit, string>> = let addUsers (ctx: HttpContext) (req: Remoting.AddUsersRequest) : Async<Result<unit, string>> =
let archmaesterAdd (db: Entity.ArchiveContext) = let archmaesterAdd (db: Entity.ArchiveContext) =
@@ -63,7 +76,7 @@ module Admin =
with e -> with e ->
do logger.LogError ("OpenFGA write errored with: {Msg}. Rolling back archmaester.", e.Message) do logger.LogError ("OpenFGA write errored with: {Msg}. Rolling back archmaester.", e.Message)
do! tr.RollbackAsync () 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 -> with e ->
do logger.LogError (e, "Failed connecting to database") do logger.LogError (e, "Failed connecting to database")
return! Error (sprintf "Error deleting users from OpenFGA: %s" e.Message) 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) do logger.LogInformation ("Add archive groups from {User}: {Request}", user, req)
try try
let db = ctx.GetService<Entity.ArchiveContext> () let db = ctx.GetService<Entity.ArchiveContext> ()
let fga = ctx.GetService<OpenFgaClient> ()
let! created = Archmaester.EFCore.addArchiveGroups db req.Id req.Groups let! created = Archmaester.EFCore.addArchiveGroups db req.Id req.Groups
do logger.LogInformation ("Added {CreatedCount} archive group entries", created) do logger.LogInformation ("Added {CreatedCount} archive group entries", created)
let fga = ctx.GetService<OpenFgaClient> ()
let req = OpenFGA.Group.addArchive req let req = OpenFGA.Group.addArchive req
let! fgaResp = fga.Write req |> Async.AwaitTask let! fgaResp = fga.Write req |> Async.AwaitTask
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaResp.ToJson ()) do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaResp.ToJson ())
return Ok () return Ok ()
with e -> 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) 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>> = let deleteArchive (ctx: HttpContext) (archiveId: System.Guid) : Async<Result<bool, string>> =
async { async {
let user = ctx.User.Identity.Name let user = ctx.User.Identity.Name
@@ -179,7 +221,7 @@ module Admin =
else else
return Error "Filter must include archive id" return Error "Filter must include archive id"
with e -> 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" return Error "Error fetching archive count"
} }
@@ -235,6 +277,186 @@ module Admin =
return Error "Error fetching archive count" 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) = let getArchiveTypes (ctx: HttpContext) =
async { async {
let user = ctx.User.Identity.Name let user = ctx.User.Identity.Name
@@ -306,18 +528,101 @@ module Admin =
return () 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 = { let private impl (ctx: HttpContext) : Remoting.Api.Admin = {
addUsers = Handler.addUsers ctx addArchive = Handler.addArchive ctx
addArchiveGroups = Handler.addArchiveGroups ctx addArchiveGroups = Handler.addArchiveGroups ctx
addGroupPermissions = Handler.addGroupPermissions ctx
addUsers = Handler.addUsers ctx
deleteArchive = Handler.deleteArchive ctx deleteArchive = Handler.deleteArchive ctx
getAllGroups = Handler.getAllGroups ctx getAllGroups = Handler.getAllGroups ctx
getArchive = Handler.getArchive ctx getArchive = Handler.getArchive ctx
getArchiveCount = Handler.getArchiveCount ctx getArchiveCount = Handler.getArchiveCount ctx
getArchiveDataSet = Handler.getArchiveDataSet ctx
getArchiveRefs = Handler.getArchiveRefs ctx getArchiveRefs = Handler.getArchiveRefs ctx
getArchiveTypes = fun () -> Handler.getArchiveTypes ctx getArchiveTypes = fun () -> Handler.getArchiveTypes ctx
getArchives = Handler.getArchives ctx getArchives = Handler.getArchives ctx
getDataSets = fun () -> Handler.getAllDataSets ctx
getGroupUsers = Handler.getGroupUsers ctx getGroupUsers = Handler.getGroupUsers ctx
removeUsers = Handler.removeUsers ctx removeUsers = Handler.removeUsers ctx
setUserPermissions = Handler.setUserPermissions ctx
updateArchive = Handler.updateArchive ctx
updateGroupPermissions = Handler.updateGroupPermissions ctx
} }
let endpoints: HttpHandler = let endpoints: HttpHandler =
@@ -325,4 +630,4 @@ module Admin =
|> Remoting.withErrorHandler Utils.rpcErrorHandler |> Remoting.withErrorHandler Utils.rpcErrorHandler
|> Remoting.fromContext impl |> Remoting.fromContext impl
|> Remoting.withRouteBuilder Remoting.routeBuilder |> Remoting.withRouteBuilder Remoting.routeBuilder
|> Remoting.buildHttpHandler |> Remoting.buildHttpHandler

View File

@@ -5,6 +5,7 @@ module Archmaester =
open Archmaester open Archmaester
let getDataSource (connStr: string) : NpgsqlDataSource = let getDataSource (connStr: string) : NpgsqlDataSource =
let dataSourceBuilder = NpgsqlDataSourceBuilder connStr let dataSourceBuilder = NpgsqlDataSourceBuilder connStr
let _mapper = dataSourceBuilder.UseNetTopologySuite () let _mapper = dataSourceBuilder.UseNetTopologySuite ()
@@ -18,6 +19,16 @@ module Archmaester =
open Oceanbox.DataAgent.Dapper 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 = let private canonicalizeGroupNames (groups: string array option) : string array =
groups groups
|> Option.defaultValue [||] |> Option.defaultValue [||]
@@ -138,6 +149,116 @@ module Archmaester =
return count 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 = module EFCore =
open System.Linq open System.Linq
@@ -180,6 +301,114 @@ module Archmaester =
return created 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> = let deleteArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<int> =
async { async {
let! entity = let! entity =
@@ -420,4 +649,4 @@ module Archmaester =
|> Async.AwaitTask |> Async.AwaitTask
return entities return entities
} }

View File

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

View File

@@ -71,7 +71,8 @@ module OpenFGA =
|> Seq.toArray |> Seq.toArray
|> Array.map (fun t -> |> Array.map (fun t ->
let condition : Remoting.Condition option = let condition : Remoting.Condition option =
Option.ofObj t.Key.Condition t.Key.Condition
|> Option.ofObj
|> Option.map (fun cond -> { |> Option.map (fun cond -> {
Name = cond.Name Name = cond.Name
Context = JsonSerializer.Serialize cond.Context Context = JsonSerializer.Serialize cond.Context
@@ -148,6 +149,15 @@ module OpenFGA =
result 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 delete (tuples: Remoting.Tuple array) =
let result = ClientWriteRequest () let result = ClientWriteRequest ()
@@ -167,6 +177,57 @@ module OpenFGA =
result 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 write (tuples: Remoting.Tuple array) =
let result = ClientWriteRequest () let result = ClientWriteRequest ()
@@ -236,6 +297,28 @@ module OpenFGA =
|]) |])
|> Queries.write' |> 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 = module private Handlers =
let check (ctx: HttpContext) (req: Remoting.CheckRequest) = let check (ctx: HttpContext) (req: Remoting.CheckRequest) =
async { async {
@@ -260,9 +343,10 @@ module OpenFGA =
let logger = ctx.GetLogger<Remoting.Api.OpenFGA> () let logger = ctx.GetLogger<Remoting.Api.OpenFGA> ()
let fga = ctx.GetService<OpenFgaClient> () let fga = ctx.GetService<OpenFgaClient> ()
try try
let deleteRequest = Queries.delete [| tuple |] let deleteRequest = Queries.deleteTuples [| tuple |]
do logger.LogInformation ("Delete req: {Request}", deleteRequest.ToJson ()) let json = JsonSerializer.Serialize deleteRequest
let! resp = fga.Write deleteRequest |> Async.AwaitTask do logger.LogInformation ("Delete req: {Request}", json)
let! resp = fga.DeleteTuples deleteRequest |> Async.AwaitTask
do logger.LogInformation ("Delete resp: {Response}", resp.ToJson ()) do logger.LogInformation ("Delete resp: {Response}", resp.ToJson ())
return Ok (resp.Deletes.Count >= 1) return Ok (resp.Deletes.Count >= 1)
with e -> with e ->
@@ -329,4 +413,4 @@ module OpenFGA =
Remoting.createApi () Remoting.createApi ()
|> Remoting.withRouteBuilder Remoting.routeBuilder |> Remoting.withRouteBuilder Remoting.routeBuilder
|> Remoting.fromContext impl |> Remoting.fromContext impl
|> Remoting.buildHttpHandler |> Remoting.buildHttpHandler

View File

@@ -155,7 +155,11 @@ let webApp: HttpHandler =
>=> choose [ >=> choose [
Auth.endpoints 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() .UseStaticFiles()
.UseGiraffe webApp .UseGiraffe webApp
let private getIp (uri: Uri) =
uri.DnsSafeHost
|> Net.Dns.GetHostAddresses
|> Array.head
let configureServices (settings: Settings) (services: IServiceCollection) = let configureServices (settings: Settings) (services: IServiceCollection) =
let authSettings = settings.Auth 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 fga: OpenFgaClient = Fga.newFgaClient settings.Fga
let archmaesterDatasource = Archmaester.getDataSource settings.DbConnectionString let archmaesterDatasource = Archmaester.getDataSource settings.DbConnectionString
do Oceanbox.DataAgent.Dapper.register () do Oceanbox.DataAgent.Dapper.register ()
@@ -293,4 +305,4 @@ let main args =
return 0 return 0
} }
) )

View File

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

View File

@@ -1,17 +1,14 @@
{ {
"version": 2, "version": 2,
"dependencies": { "dependencies": {
"net9.0": { "net10.0": {
"Azure.Security.KeyVault.Secrets": { "Azure.Security.KeyVault.Secrets": {
"type": "Direct", "type": "Direct",
"requested": "[4.7.0, )", "requested": "[4.7.0, )",
"resolved": "4.7.0", "resolved": "4.7.0",
"contentHash": "uOPCojkm41V4dKTORyGzl3/f/lriKpxSQ43fWDn4StRJBVmbF1F/DNWJhwm207kCnqgE/W9+tskJSimIKHCZkw==", "contentHash": "uOPCojkm41V4dKTORyGzl3/f/lriKpxSQ43fWDn4StRJBVmbF1F/DNWJhwm207kCnqgE/W9+tskJSimIKHCZkw==",
"dependencies": { "dependencies": {
"Azure.Core": "1.44.1", "Azure.Core": "1.44.1"
"System.Memory": "4.5.5",
"System.Text.Json": "6.0.10",
"System.Threading.Tasks.Extensions": "4.5.4"
} }
}, },
"Fable.Remoting.Giraffe": { "Fable.Remoting.Giraffe": {
@@ -53,8 +50,7 @@
"FSharp.Core": "6.0.0", "FSharp.Core": "6.0.0",
"FSharp.SystemTextJson": "1.3.13", "FSharp.SystemTextJson": "1.3.13",
"Giraffe.ViewEngine": "1.4.0", "Giraffe.ViewEngine": "1.4.0",
"Microsoft.IO.RecyclableMemoryStream": "3.0.1", "Microsoft.IO.RecyclableMemoryStream": "3.0.1"
"System.Text.Json": "8.0.5"
} }
}, },
"Azure.Core": { "Azure.Core": {
@@ -64,12 +60,7 @@
"dependencies": { "dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "6.0.0", "Microsoft.Bcl.AsyncInterfaces": "6.0.0",
"System.ClientModel": "1.1.0", "System.ClientModel": "1.1.0",
"System.Diagnostics.DiagnosticSource": "6.0.1", "System.Memory.Data": "6.0.0"
"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"
} }
}, },
"Azure.Security.KeyVault.Keys": { "Azure.Security.KeyVault.Keys": {
@@ -77,10 +68,7 @@
"resolved": "4.6.0", "resolved": "4.6.0",
"contentHash": "1KbCIkXmLaj+kDDNm1Va5rNlzgcJ/fVtnsoVmzZPKa38jz6DXhPyojdvGaOX8AdupGJceg0X1vrsGvZKN79Qzw==", "contentHash": "1KbCIkXmLaj+kDDNm1Va5rNlzgcJ/fVtnsoVmzZPKa38jz6DXhPyojdvGaOX8AdupGJceg0X1vrsGvZKN79Qzw==",
"dependencies": { "dependencies": {
"Azure.Core": "1.37.0", "Azure.Core": "1.37.0"
"System.Memory": "4.5.4",
"System.Text.Json": "4.7.2",
"System.Threading.Tasks.Extensions": "4.5.4"
} }
}, },
"Azure.Storage.Blobs": { "Azure.Storage.Blobs": {
@@ -88,8 +76,7 @@
"resolved": "12.23.0", "resolved": "12.23.0",
"contentHash": "wokJ5KX/iViQQ32xyCu69+Ter0aR4B9QQ+oR9NCpc/WPIanxnDErrmFfdmE7K8ZdccjHkvE/wEnqJxaF1+5wFg==", "contentHash": "wokJ5KX/iViQQ32xyCu69+Ter0aR4B9QQ+oR9NCpc/WPIanxnDErrmFfdmE7K8ZdccjHkvE/wEnqJxaF1+5wFg==",
"dependencies": { "dependencies": {
"Azure.Storage.Common": "12.22.0", "Azure.Storage.Common": "12.22.0"
"System.Text.Json": "6.0.10"
} }
}, },
"Azure.Storage.Common": { "Azure.Storage.Common": {
@@ -233,8 +220,7 @@
"resolved": "5.3.2", "resolved": "5.3.2",
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==", "contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
"dependencies": { "dependencies": {
"FSharp.Core": "4.3.2", "FSharp.Core": "4.3.2"
"System.Reflection.Emit.Lightweight": "4.3.0"
} }
}, },
"Giraffe.ViewEngine": { "Giraffe.ViewEngine": {
@@ -527,8 +513,7 @@
"resolved": "4.67.2", "resolved": "4.67.2",
"contentHash": "37t0TfekfG6XM8kue/xNaA66Qjtti5Qe1xA41CK+bEd8VD76/oXJc+meFJHGzygIC485dCpKoamG/pDfb9Qd7Q==", "contentHash": "37t0TfekfG6XM8kue/xNaA66Qjtti5Qe1xA41CK+bEd8VD76/oXJc+meFJHGzygIC485dCpKoamG/pDfb9Qd7Q==",
"dependencies": { "dependencies": {
"Microsoft.IdentityModel.Abstractions": "6.35.0", "Microsoft.IdentityModel.Abstractions": "6.35.0"
"System.Diagnostics.DiagnosticSource": "6.0.1"
} }
}, },
"Microsoft.Identity.Client.Extensions.Msal": { "Microsoft.Identity.Client.Extensions.Msal": {
@@ -617,10 +602,7 @@
"OpenTelemetry.Api": { "OpenTelemetry.Api": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.10.0", "resolved": "1.10.0",
"contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w==", "contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w=="
"dependencies": {
"System.Diagnostics.DiagnosticSource": "9.0.0"
}
}, },
"OpenTelemetry.Api.ProviderBuilderExtensions": { "OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive", "type": "Transitive",
@@ -701,26 +683,18 @@
"dependencies": { "dependencies": {
"Microsoft.Extensions.Options": "9.0.0", "Microsoft.Extensions.Options": "9.0.0",
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.10.0, 2.0.0)", "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.10.0, 2.0.0)",
"StackExchange.Redis": "[2.6.122, 3.0.0)", "StackExchange.Redis": "[2.6.122, 3.0.0)"
"System.Reflection.Emit.Lightweight": "4.7.0"
} }
}, },
"Pipelines.Sockets.Unofficial": { "Pipelines.Sockets.Unofficial": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.2.8", "resolved": "2.2.8",
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
"dependencies": {
"System.IO.Pipelines": "5.0.1"
}
}, },
"ProjNET": { "ProjNET": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.0.0", "resolved": "2.0.0",
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==", "contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
"dependencies": {
"System.Memory": "4.5.3",
"System.Numerics.Vectors": "4.5.0"
}
}, },
"Sentry": { "Sentry": {
"type": "Transitive", "type": "Transitive",
@@ -769,8 +743,7 @@
"resolved": "1.1.0", "resolved": "1.1.0",
"contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==", "contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==",
"dependencies": { "dependencies": {
"System.Memory.Data": "1.0.2", "System.Memory.Data": "1.0.2"
"System.Text.Json": "6.0.9"
} }
}, },
"System.ComponentModel.Annotations": { "System.ComponentModel.Annotations": {
@@ -778,11 +751,6 @@
"resolved": "5.0.0", "resolved": "5.0.0",
"contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg=="
}, },
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "8hy61dsFYYSDjT9iTAfygGMU3A0EAnG69x5FUXeKsCjMhBmtTBt4UMUEW3ipprFoorOW6Jw/7hDMjXtlrsOvVQ=="
},
"System.IdentityModel.Tokens.Jwt": { "System.IdentityModel.Tokens.Jwt": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.1", "resolved": "8.0.1",
@@ -797,38 +765,10 @@
"resolved": "6.0.0", "resolved": "6.0.0",
"contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g==" "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": { "System.Memory.Data": {
"type": "Transitive", "type": "Transitive",
"resolved": "6.0.0", "resolved": "6.0.0",
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ==", "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=="
}, },
"System.Security.Cryptography.Pkcs": { "System.Security.Cryptography.Pkcs": {
"type": "Transitive", "type": "Transitive",
@@ -848,16 +788,6 @@
"System.Security.Cryptography.Pkcs": "9.0.2" "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": { "entity": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
@@ -953,9 +883,7 @@
"dependencies": { "dependencies": {
"Azure.Core": "1.44.1", "Azure.Core": "1.44.1",
"Microsoft.Identity.Client": "4.67.2", "Microsoft.Identity.Client": "4.67.2",
"Microsoft.Identity.Client.Extensions.Msal": "4.67.2", "Microsoft.Identity.Client.Extensions.Msal": "4.67.2"
"System.Memory": "4.5.5",
"System.Threading.Tasks.Extensions": "4.5.4"
} }
}, },
"Dapper.FSharp": { "Dapper.FSharp": {
@@ -1121,8 +1049,7 @@
"resolved": "1.3.13", "resolved": "1.3.13",
"contentHash": "znp8odpdkVGKVX0AvbhiXdmeMi0KJ+A4AyAQWSkfAEAe4Z4clRE+rVhrLnAGrFD1VEIUX2lsQ4o84ywpWZUSGw==", "contentHash": "znp8odpdkVGKVX0AvbhiXdmeMi0KJ+A4AyAQWSkfAEAe4Z4clRE+rVhrLnAGrFD1VEIUX2lsQ4o84ywpWZUSGw==",
"dependencies": { "dependencies": {
"FSharp.Core": "4.7.0", "FSharp.Core": "4.7.0"
"System.Text.Json": "6.0.0"
} }
}, },
"FSharpPlus": { "FSharpPlus": {
@@ -1211,10 +1138,7 @@
"type": "CentralTransitive", "type": "CentralTransitive",
"requested": "[2.5.0, )", "requested": "[2.5.0, )",
"resolved": "2.5.0", "resolved": "2.5.0",
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==", "contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
"dependencies": {
"System.Memory": "4.5.4"
}
}, },
"Newtonsoft.Json": { "Newtonsoft.Json": {
"type": "CentralTransitive", "type": "CentralTransitive",
@@ -1300,8 +1224,7 @@
"contentHash": "vlOKvmigJ3Sumoulp1HwCTFXgX4KuERVGIIw4ZqmhgUJnSiApDmY183ddzzHo2FIdIJ8vGwrMGx98v9cLAezFA==", "contentHash": "vlOKvmigJ3Sumoulp1HwCTFXgX4KuERVGIIw4ZqmhgUJnSiApDmY183ddzzHo2FIdIJ8vGwrMGx98v9cLAezFA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Http": "9.0.9", "Microsoft.Extensions.Http": "9.0.9",
"System.ComponentModel.Annotations": "5.0.0", "System.ComponentModel.Annotations": "5.0.0"
"System.Diagnostics.DiagnosticSource": "9.0.9"
} }
}, },
"ProjNet.FSharp": { "ProjNet.FSharp": {
@@ -1372,15 +1295,6 @@
"Serilog": "4.0.0" "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": { "Thoth.Json.Net": {
"type": "CentralTransitive", "type": "CentralTransitive",
"requested": "[12.0.0, )", "requested": "[12.0.0, )",

View File

@@ -122,6 +122,16 @@ module Remoting =
EndTime = System.DateTime.Now 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>] [<Struct>]
type AddArchiveGroupsRequest = { type AddArchiveGroupsRequest = {
Id: Archmaester.Dto.ArchiveId Id: Archmaester.Dto.ArchiveId
@@ -130,28 +140,102 @@ module Remoting =
Exec: ExecTicket option 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>] [<Struct>]
type AddUsersRequest = { type AddUsersRequest = {
Group: string Group: string
Users: string array 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>] [<RequireQualifiedAccess>]
module Api = module Api =
type Auth = { IsAuthenticated: Async<bool> } type Auth = { IsAuthenticated: Async<bool> }
type Admin = { type Admin = {
addUsers: AddUsersRequest -> Async<Result<unit, string>> addArchive: AddArchiveRequest -> Async<Result<Archive, string>>
addArchiveGroups: AddArchiveGroupsRequest -> Async<Result<unit, 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>> deleteArchive: Archmaester.Dto.ArchiveId -> Async<Result<bool, string>>
getAllGroups: Async<string array> getAllGroups: Async<string array>
getArchive: Archmaester.Dto.ArchiveId -> Async<Result<Archmaester.Dto.ArchiveProps, string>> getArchive: Archmaester.Dto.ArchiveId -> Async<Result<Archmaester.Dto.ArchiveProps, string>>
getArchiveCount: Archmaester.Dto.ArchiveFilter -> Async<Result<int, 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>> getArchiveRefs: Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
getArchiveTypes: unit -> Async<Archmaester.Dto.ArchiveType array> getArchiveTypes: unit -> Async<Archmaester.Dto.ArchiveType array>
getArchives: int -> int -> Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>> 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> getGroupUsers: string -> Async<string array>
removeUsers: string array -> Async<Result<unit, string>> 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 = { type OpenFGA = {
@@ -160,4 +244,4 @@ module Remoting =
ListObjects: ListObjectsRequest -> Async<Result<string array, string>> ListObjects: ListObjectsRequest -> Async<Result<string array, string>>
ListUsers: ListUsersRequest -> Async<Result<string array, string>> ListUsers: ListUsersRequest -> Async<Result<string array, string>>
Read: ReadRequest -> Async<Result<ReadResponse, string>> Read: ReadRequest -> Async<Result<ReadResponse, string>>
} }

View File

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

View File

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

View File

@@ -101,10 +101,12 @@ let private archiveToDto (ctx: Entity.ArchiveContext) (a: Entity.Archive) =
.ToArray () .ToArray ()
let files = let files =
ctx.Files a.Attribs.Files
.Where(fun f -> a.Files.Select(_.FileId).Contains f.FileId) |> Array.ofSeq
.OrderBy(_.Ordering) |> Array.filter (fun file ->
.ToArray () 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 = let modelArea =
ctx.ModelAreas.SingleOrDefault (fun y -> y.Attribs.Select(_.AttribsId).Contains a.AttribsId) 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) = type Archivist(dataSource: NpgsqlDataSource) =
let withDb qry = let withDb qry =
try try
let ctx = new Entity.ArchiveContext(dataSource, true) use ctx = new Entity.ArchiveContext(dataSource, true)
qry ctx |> Ok qry ctx |> Ok
with e -> with e ->
Log.Error $"DataAgent.Archives.Archivist.withDb: {e}" Log.Error $"DataAgent.Archives.Archivist.withDb: {e}"
@@ -844,6 +846,12 @@ type Archivist(dataSource: NpgsqlDataSource) =
ctx.SaveChanges () |> ignore ctx.SaveChanges () |> ignore
} }
member _.startConnection () =
new Entity.ArchiveContext (dataSource)
member _.startTransaction (ctx: Entity.ArchiveContext) =
ctx.Database.BeginTransaction ()
member x.tryAddArchive(item: ArchiveDto) = member x.tryAddArchive(item: ArchiveDto) =
monad { monad {
do! do!
@@ -886,6 +894,45 @@ type Archivist(dataSource: NpgsqlDataSource) =
return! Error e 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) = member x.tryAddSubArchive(item: SubArchiveDef) =
let ctx = new Entity.ArchiveContext (dataSource) let ctx = new Entity.ArchiveContext (dataSource)
let transaction = ctx.Database.BeginTransaction () let transaction = ctx.Database.BeginTransaction ()
@@ -1330,11 +1377,12 @@ type Archivist(dataSource: NpgsqlDataSource) =
.Where(fun y -> y.ArchiveId = aid) .Where(fun y -> y.ArchiveId = aid)
.Include(_.Attribs) .Include(_.Attribs)
.ThenInclude(_.Type) .ThenInclude(_.Type)
.Include(_.Attribs)
.ThenInclude(_.Files)
.Include(_.Ref) .Include(_.Ref)
.Include(_.Owners) .Include(_.Owners)
.Include(_.Users) .Include(_.Users)
.Include(_.Groups) .Include(_.Groups)
.Include(fun y -> y.Files.OrderBy _.File.Ordering)
.ToArray () .ToArray ()
|> fun y -> |> fun y ->
if y.Length = 0 then if y.Length = 0 then

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"version": 2, "version": 2,
"dependencies": { "dependencies": {
"net9.0": { "net10.0": {
"Dapper.FSharp": { "Dapper.FSharp": {
"type": "Direct", "type": "Direct",
"requested": "[4.9.0, )", "requested": "[4.9.0, )",
@@ -103,10 +103,7 @@
"type": "Direct", "type": "Direct",
"requested": "[2.5.0, )", "requested": "[2.5.0, )",
"resolved": "2.5.0", "resolved": "2.5.0",
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==", "contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
"dependencies": {
"System.Memory": "4.5.4"
}
}, },
"Npgsql.EntityFrameworkCore.PostgreSQL": { "Npgsql.EntityFrameworkCore.PostgreSQL": {
"type": "Direct", "type": "Direct",
@@ -313,8 +310,7 @@
"resolved": "5.3.2", "resolved": "5.3.2",
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==", "contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
"dependencies": { "dependencies": {
"FSharp.Core": "4.3.2", "FSharp.Core": "4.3.2"
"System.Reflection.Emit.Lightweight": "4.3.0"
} }
}, },
"Google.Api.CommonProtos": { "Google.Api.CommonProtos": {
@@ -530,16 +526,6 @@
"resolved": "17.11.4", "resolved": "17.11.4",
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA==" "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": { "NetTopologySuite.IO.PostGis": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.0", "resolved": "2.1.0",
@@ -551,11 +537,7 @@
"ProjNET": { "ProjNET": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.0.0", "resolved": "2.0.0",
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==", "contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
"dependencies": {
"System.Memory": "4.5.3",
"System.Numerics.Vectors": "4.5.0"
}
}, },
"Serilog.Sinks.File": { "Serilog.Sinks.File": {
"type": "Transitive", "type": "Transitive",
@@ -574,100 +556,6 @@
"Serilog.Sinks.File": "6.0.0" "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": { "entity": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
@@ -767,136 +655,6 @@
"contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA==" "contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA=="
} }
}, },
"net9.0/linux-x64": { "net10.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"
}
}
}
} }
} }

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