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
max_line_length = 80
[*.yaml]
indent_size = 2
[*.fs]
max_line_length= 120

35
.envrc
View File

@@ -1,4 +1,8 @@
#!/usr/bin/env bash
export NPINS_DIRECTORY="nix"
export APP_ENV=$USER
# the shebang is ignored, but nice for editors
watch_file nix/sources.json
@@ -10,34 +14,3 @@ use nix
# HACK: Workaround for direnv bug
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
- local: "/src/Atlantis/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Atlantis/**/*"
- "nix/packages/atlantis.nix"
@@ -21,44 +19,32 @@ include:
- "nix/containers.nix"
- local: "/src/Sorcerer/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Sorcerer/**/*"
- "nix/packages/sorcerer.nix"
- "nix/containers.nix"
- local: "/src/Archivist/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Archivist/**/*"
- "nix/packages/archivist.nix"
- local: "/src/Interfaces/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Interfaces/**/*"
- "nix/packages/api.nix"
- local: "/src/DataAgent/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/DataAgent/**/*"
- "nix/packages/dataagent.nix"
- local: "/src/ServerPack/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/ServerPack/**/*"
- "nix/packages/serverpack.nix"
- local: "/src/Codex/.gitlab-ci.yml"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- changes:
- "src/Codex/**/*"
- "nix/packages/node-modules.nix"

View File

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

View File

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

View File

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

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
clean version;
dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0;
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_9_0;
dotnet-sdk = pkgs.dotnetCorePackages.sdk_10_0;
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_10_0;
deps = nix-utils.output.lib.nuget.deps;
# Usage: export NETRC="$(agenix -d netrc.age)" in `./nix/secrets`

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0.6
FROM mcr.microsoft.com/dotnet/aspnet:10.0
RUN apt-get update \
&& apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,12 +46,9 @@ let private update (msg: XtractMsg) (model: XtractModel) =
{ model with position = pos }, Elmish.Cmd.none
| SetData s ->
console.debug ("[DataExtraction] SetData msg:", s)
let data' = { model.data with name = s.name; fvcom = s.fvcom;}
{ model with data = data' }, Elmish.Cmd.none
| XtractMsg.SetStarted (started, jobIdOpt) ->
{ model with start = started, jobIdOpt }, Elmish.Cmd.none
| XtractMsg.ResetModel m ->
{ m with data.name = model.data.name }, Elmish.Cmd.none
{ model with data = s }, Elmish.Cmd.none
| XtractMsg.SetStarted (started, jobIdOpt) -> { model with start = started, jobIdOpt }, Elmish.Cmd.none
| XtractMsg.ResetModel m -> { m with data.name = model.data.name }, Elmish.Cmd.none
| XtractMsg.Noop () -> model, Elmish.Cmd.none
//
@@ -105,7 +102,8 @@ let updateExtractionSite (posOpt: (float * float) option, map) =
match posOpt with
| Some pos ->
let p' = pos |> posToCoord
let point = Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
let point =
Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
let feature = Feature.feature [ feature.geometryOrProperties point ]
source.addFeature (feature)
| _ -> ()
@@ -129,23 +127,14 @@ let private extractionSiteControls (dispatch': XtractMsg -> unit) (xmodel': Xtra
let handleMapPlaceExtraction (coords: Coordinate) =
console.debug ($"[DataExtraction] Click add site: %s{coords.ToString ()}")
coordToPos coords
|> tryFence
|> SetExtractionSite
|> dispatch'
coordToPos coords |> tryFence |> SetExtractionSite |> dispatch'
let setPosition (pos: float * float) : unit =
Some pos
|> SetExtractionSite
|> dispatch'
Some pos |> SetExtractionSite |> dispatch'
let selectedPos =
xmodel'.position
|> Option.defaultValue (0.0, 0.0)
|> toWgs84'
let selectedPos = xmodel'.position |> Option.defaultValue (0.0, 0.0) |> toWgs84'
let deleteSite (_: Browser.Types.Event) =
None |> SetExtractionSite |> dispatch'
let deleteSite (_: Browser.Types.Event) = None |> SetExtractionSite |> dispatch'
let latitudeBox =
let latitude = snd selectedPos
@@ -264,16 +253,17 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
let map = model.map
let currentFrame = model.frame
let archiveStartUTC = archive.startTime.ToUniversalTime ()
let archiveEndT = archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
let archiveStartT = archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
let archiveEndT =
archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
let archiveStartT =
archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
let submitted, setSubmitted = Hook.useState false
let createNewModel () : XtractModel =
let data =
match xtractModelOpt with
| Some existing -> { existing.data with fvcom = archive.id }
| None ->
{
| None -> {
XtractData.empty with
fvcom = archive.id
start = archiveStartT
@@ -309,11 +299,21 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
SetXtractModel (Some newModel) |> dispatch
)
let setStartDateTime (dt: DateTime) =
SetData { xmodel'.data with start = dt } |> dispatch'
// Set time to midday (12:00)
let startDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
let stopDate = xmodel'.data.stop
let newStop = if startDate >= stopDate then startDate.AddDays(1.0) else stopDate
console.debug ("[DataExtraction] setStartDateTime:", startDate, "stop:", newStop)
SetData { xmodel'.data with start = startDate; stop = newStop } |> dispatch'
let setStopDateTime (dt: DateTime) =
SetData { xmodel'.data with stop = dt } |> dispatch'
let endDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
let startDate = xmodel'.data.start
let newStart = if endDate <= startDate then endDate.AddDays(-1.0) else startDate
console.debug ("[DataExtraction] setStopDateTime:", endDate, "start:", newStart)
SetData { xmodel'.data with stop = endDate; start = newStart } |> dispatch'
let setName (s: string) =
let currentModel = modelRef.contents |> Option.defaultValue xmodel'
@@ -322,7 +322,10 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
let minStartDate = archiveStartUTC
let maxStartDate = archiveEndT
let minEndDate = archiveStartUTC
let maxEndDate = archiveEndT
// maxEndDate is one year after the selected start date
let maxEndDate =
let startDate = xmodel'.data.start
startDate.AddYears(1)
let metaControls =
html

View File

@@ -43,15 +43,15 @@ let inboxDialog
let table = document.getElementById "inbox-table"
async {
let! mbox = Remoting.inboxApi().getMessages ()
// if mbox.Length = 0 then
// table?items <- [| {
// id = Guid.Empty
// content = ""
// unread = false
// type' = MessageType.Note
// created = DateTime.Now
// } |]
// else
if mbox.Length = 0 then
table?items <- [| {
id = Guid.Empty
content = ""
unread = false
type' = MessageType.Note
created = DateTime.Now
} |]
else
table?items <- mbox
} |> Async.StartImmediate
@@ -65,30 +65,18 @@ let inboxDialog
|> Set.ofSeq
|> setSelected
let doDelete _ =
let doDelete selected _ =
let table = document.getElementById "inbox-table"
let selectedSet : Guid JS.Set = table?selectedSet
let items: InboxItem array = table?items
async {
let toDelete =
items
|> Array.filter (fun item -> selectedSet.has(item.id))
|> Array.map (fun item -> item.id)
|> Array.filter (fun item -> Set.contains item.id selected)
|> Array.iter (fun item ->
console.log("Delete: %A", item.content)
do arg.deleteMessage item.id
)
console.debug("Deleting", toDelete.Length, "messages")
for id in toDelete do
arg.deleteMessage id
// Clear selection immediately
selectedSet.clear()
setSelected Set.empty
// Wait a bit for server to process, then reload
do! Async.Sleep 200
let! mbox = Remoting.inboxApi().getMessages ()
table?items <- mbox
} |> Async.StartImmediate
loadMessages ()
let doRead selected _ =
let table = document.getElementById "inbox-table"
@@ -118,7 +106,7 @@ let inboxDialog
html $"""
<sp-field-group horizontal>
<sp-action-button
@click={Ev(doDelete)}
@click={Ev(doDelete selected)}
?disabled={selected.Count = 0}>
<sp-icon-delete slot="icon"></sp-icon-delete> Delete selected
</sp-action-button>
@@ -308,11 +296,8 @@ let inboxDialog
let sortFn = if sortDir = "asc" then Array.sortBy else Array.sortByDescending
table?items
|> sortFn (fun item -> JS.expr_js $"{item}[{sortKey}]")
|> fun items -> table?items <- items))
let table =
html $"""
"""
|> fun items -> table?items <- items)
)
html $"""
<div class="inbox-dialog">

View File

@@ -1893,7 +1893,7 @@ let fetchArchive (archiveId: System.Guid) : ArchiveInfo Async =
polygon = a.polygon |> Option.map (Array.map (fun (x, y) -> float x, float y))
frames = archiveFrames
}
| Result.Error err ->
| Error err ->
console.error $"Could not retrieve the selected archive!: {err}"
return ArchiveInfo.empty
}

View File

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

View File

@@ -100,7 +100,7 @@ let private simAccordion (dispatch: Msg -> unit) model =
console.debug $"policies: %A{model.simPolicies}"
let disabled = model.archive.id = Guid.Empty
// TODO(mrtz): Create custom policy for plumes, for now just inherit from drifters.
// TODO(mrtz): Create custom policy for plume and xtract, for now just inherit from drifters.
let disabledPlume =
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
let disabledXtract =
@@ -189,7 +189,7 @@ let private simAccordion (dispatch: Msg -> unit) model =
<sp-action-button
static="primary"
style="flex-grow: 1"
?disabled={disabled}
?disabled={disabledXtract }
@click={Ev (chooseMode (DataExtraction DefaultXtract))}
>
Extract Data

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -286,6 +286,7 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
}
member this.getActiveJobs aid =
task {
Log.Debug $"get active jobs for: {this.myId}"
let active = [|
@@ -304,13 +305,16 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
|]
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
task { return active }
return active
}
member this.getFenceRadius() =
task {
Log.Information "fence?"
let r = settings.file.fenceRadius
Log.Information $"fence: {r}"
Task.FromResult r
return r
}
member this.checkFence aid (pts: (float * float) list) =
task { return this.validatePoints aid pts }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
}:
dockerTools.buildLayeredImage {
name = "Codex";
tag = "0.0.0-alpha.1";
tag = "0.0.1";
created = "now";
contents = [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"version": 2,
"dependencies": {
"net9.0": {
"net10.0": {
"Dapper.FSharp": {
"type": "Direct",
"requested": "[4.9.0, )",
@@ -103,10 +103,7 @@
"type": "Direct",
"requested": "[2.5.0, )",
"resolved": "2.5.0",
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
"dependencies": {
"System.Memory": "4.5.4"
}
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
},
"Npgsql.EntityFrameworkCore.PostgreSQL": {
"type": "Direct",
@@ -313,8 +310,7 @@
"resolved": "5.3.2",
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
"dependencies": {
"FSharp.Core": "4.3.2",
"System.Reflection.Emit.Lightweight": "4.3.0"
"FSharp.Core": "4.3.2"
}
},
"Google.Api.CommonProtos": {
@@ -530,16 +526,6 @@
"resolved": "17.11.4",
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
"Microsoft.NETCore.Targets": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
},
"NetTopologySuite.IO.PostGis": {
"type": "Transitive",
"resolved": "2.1.0",
@@ -551,11 +537,7 @@
"ProjNET": {
"type": "Transitive",
"resolved": "2.0.0",
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
"dependencies": {
"System.Memory": "4.5.3",
"System.Numerics.Vectors": "4.5.0"
}
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
},
"Serilog.Sinks.File": {
"type": "Transitive",
@@ -574,100 +556,6 @@
"Serilog.Sinks.File": "6.0.0"
}
},
"System.IO": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"System.Text.Encoding": "4.3.0",
"System.Threading.Tasks": "4.3.0"
}
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw=="
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
},
"System.Reflection": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.IO": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Emit.ILGeneration": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==",
"dependencies": {
"System.Reflection": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Emit.Lightweight": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==",
"dependencies": {
"System.Reflection": "4.3.0",
"System.Reflection.Emit.ILGeneration": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"System.Text.Encoding": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Threading.Tasks": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"entity": {
"type": "Project",
"dependencies": {
@@ -767,136 +655,6 @@
"contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA=="
}
},
"net9.0/linux-x64": {
"runtime.any.System.IO": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "SDZ5AD1DtyRoxYtEcqQ3HDlcrorMYXZeCt7ZhG9US9I5Vva+gpIWDGMkcwa5XiKL0ceQKRZIX2x0XEjLX7PDzQ=="
},
"runtime.any.System.Reflection": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "hLC3A3rI8jipR5d9k7+f0MgRCW6texsAp0MWkN/ci18FMtQ9KH7E2vDn/DH2LkxsszlpJpOn9qy6Z6/69rH6eQ=="
},
"runtime.any.System.Reflection.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "Nrm1p3armp6TTf2xuvaa+jGTTmncALWFq22CpmwRvhDf6dE9ZmH40EbOswD4GnFLrMRS0Ki6Kx5aUPmKK/hZBg=="
},
"runtime.any.System.Runtime": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "fRS7zJgaG9NkifaAxGGclDDoRn9HC7hXACl52Or06a/fxdzDajWb5wov3c6a+gVSlekRoexfjwQSK9sh5um5LQ==",
"dependencies": {
"System.Private.Uri": "4.3.0"
}
},
"runtime.any.System.Text.Encoding": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "+ihI5VaXFCMVPJNstG4O4eo1CfbrByLxRrQQTqOTp1ttK0kUKDqOdBSTaCB2IBk/QtjDrs6+x4xuezyMXdm0HQ=="
},
"runtime.any.System.Threading.Tasks": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "OhBAVBQG5kFj1S+hCEQ3TUHBAEtZ3fbEMgZMRNdN8A0Pj4x+5nTELEqL59DU0TjKVE6II3dqKw4Dklb3szT65w=="
},
"runtime.native.System": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"runtime.unix.System.Private.Uri": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "ooWzobr5RAq34r9uan1r/WPXJYG1XWy9KanrxNvEnBzbFdQbMG7Y3bVi4QxR7xZMNLOxLLTAyXvnSkfj5boZSg==",
"dependencies": {
"runtime.native.System": "4.3.0"
}
},
"System.IO": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"System.Text.Encoding": "4.3.0",
"System.Threading.Tasks": "4.3.0",
"runtime.any.System.IO": "4.3.0"
}
},
"System.Private.Uri": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "I4SwANiUGho1esj4V4oSlPllXjzCZDE+5XXso2P03LW2vOda2Enzh8DWOxwN6hnrJyp314c7KuVu31QYhRzOGg==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"runtime.unix.System.Private.Uri": "4.3.0"
}
},
"System.Reflection": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.IO": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0",
"runtime.any.System.Reflection": "4.3.0"
}
},
"System.Reflection.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"runtime.any.System.Reflection.Primitives": "4.3.0"
}
},
"System.Runtime": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"runtime.any.System.Runtime": "4.3.0"
}
},
"System.Text.Encoding": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"runtime.any.System.Text.Encoding": "4.3.0"
}
},
"System.Threading.Tasks": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"runtime.any.System.Threading.Tasks": "4.3.0"
}
}
}
"net10.0/linux-x64": {}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<!-- <RuntimeIdentifier>linux-x64</RuntimeIdentifier> -->
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>

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