Merge branch 'main' into 'automated/npins-update-20260123'
# Conflicts: # nix/sources.json
This commit is contained in:
@@ -23,6 +23,9 @@ max_line_length= 80
|
||||
indent_size = 2
|
||||
max_line_length = 80
|
||||
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
|
||||
[*.fs]
|
||||
max_line_length= 120
|
||||
|
||||
|
||||
37
.envrc
37
.envrc
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export NPINS_DIRECTORY="nix"
|
||||
export APP_ENV=$USER
|
||||
|
||||
# the shebang is ignored, but nice for editors
|
||||
watch_file nix/sources.json
|
||||
|
||||
@@ -9,35 +13,4 @@ dotenv_if_exists
|
||||
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
|
||||
}
|
||||
unset TMP TMPDIR TEMP TEMPDIR
|
||||
@@ -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"
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
48
README.md
48
README.md
@@ -69,25 +69,7 @@ kubectl --context oceanbox -n default get pods
|
||||
Required helm manifests are hosted in a separate repository: <https://gitlab.com/oceanbox/manifests>.
|
||||
Clone it into a directory _in the same parent directory as this repository._
|
||||
|
||||
The Bitnami respository must also be added to helm:
|
||||
|
||||
```shell
|
||||
helm repo add bitnami https://charts.bitnami.com/bitnami
|
||||
```
|
||||
|
||||
### DNS
|
||||
|
||||
Some DNS masking is required. Add the following to your NixOS configuration:
|
||||
|
||||
```nix
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
settings.address = [
|
||||
"/.local/127.0.0.1"
|
||||
"/.local.oceanbox.io/127.0.0.1"
|
||||
];
|
||||
};
|
||||
```
|
||||
You'll have to run `helm dependency update` in the atlantis directory within the manifest repo to download the charts.
|
||||
|
||||
### NuGet
|
||||
|
||||
@@ -102,14 +84,30 @@ To retrieve packages from the private Oceanbox nuget registry, configure it with
|
||||
</packageSources>
|
||||
<packageSourceCredentials>
|
||||
<oceanbox>
|
||||
<add key="Username" value="oceanbox-nuget" />
|
||||
<add key="ClearTextPassword" value="<...>" />
|
||||
<add key="Username" value="<Your-GitLab-Username>" />
|
||||
<add key="ClearTextPassword" value="<Your-GitLab-PAT>" />
|
||||
</oceanbox>
|
||||
</packageSourceCredentials>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="oceanbox">
|
||||
<package pattern="Oceanbox.*" />
|
||||
<package pattern="ProjNet.FSharp" />
|
||||
<package pattern="Drifters.Api" />
|
||||
<package pattern="Fable.Lit" />
|
||||
<package pattern="Fable.Lit.*" />
|
||||
<package pattern="Fable.SignalR" />
|
||||
<package pattern="Fable.SignalR.*" />
|
||||
<package pattern="Fable.OpenLayers" />
|
||||
<package pattern="Matplotlib.*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
Substitute `<...>` for the corresponding secret.
|
||||
Substitute with your own gitlab username and PAT in the credentials.
|
||||
|
||||
Now, we should be able to `restore`:
|
||||
|
||||
@@ -168,7 +166,7 @@ You should now be able to access the Atlantis client (with HMR) on <atlantis.loc
|
||||
### Trust Root Certificate
|
||||
|
||||
> [!note]
|
||||
> You'll need to run `dotnet run bundle` in `src/Atlantis` to generate the `/certs` directory
|
||||
> You'll need to run `just run-client` in `src/Atlantis` to generate the certificates in `~/.vite-plugin-mkcert/certs`
|
||||
|
||||
In order for your browser to allow you to access the web application, you must add the root certificate generated by `mkcert` to the list of trusted authorities in your browser:
|
||||
|
||||
@@ -179,9 +177,9 @@ In order for your browser to allow you to access the web application, you must a
|
||||
|
||||
### Add `user` to OpenFGA
|
||||
|
||||
Ask [sales](moritz.jorg@oceanbox.io) to add your `azure-ad-user` to OpenFGA.
|
||||
Ask [sales](support@oceanbox.io) to add your `azure-ad-user` to OpenFGA.
|
||||
|
||||
### CORS for Sorcerer
|
||||
|
||||
Add the `url` of your instance to the CORS list of Sorcerer
|
||||
[here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52).
|
||||
[here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52).
|
||||
132
RELEASE_NOTES.md
132
RELEASE_NOTES.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.0",
|
||||
"version": "10.0.100",
|
||||
"rollForward": "latestMinor"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=";
|
||||
}
|
||||
@@ -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
101
scripts/update-rider.sh
Executable 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
|
||||
11
shell.nix
11
shell.nix
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0.6
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev
|
||||
@@ -12,4 +12,4 @@ ENV SERVER_CONTENT_ROOT=/app/public
|
||||
COPY dist/ /app
|
||||
|
||||
WORKDIR /app
|
||||
CMD [ "dotnet", "/app/Server.dll" ]
|
||||
CMD [ "dotnet", "/app/Server.dll" ]
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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}}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Fable.Browser.IndexedDB": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.2.0, )",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Fable.Browser.IndexedDB": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.2.0, )",
|
||||
|
||||
@@ -46,12 +46,9 @@ let private update (msg: XtractMsg) (model: XtractModel) =
|
||||
{ model with position = pos }, Elmish.Cmd.none
|
||||
| SetData s ->
|
||||
console.debug ("[DataExtraction] SetData msg:", s)
|
||||
let data' = { model.data with name = s.name; fvcom = s.fvcom;}
|
||||
{ model with data = data' }, Elmish.Cmd.none
|
||||
| XtractMsg.SetStarted (started, jobIdOpt) ->
|
||||
{ model with start = started, jobIdOpt }, Elmish.Cmd.none
|
||||
| XtractMsg.ResetModel m ->
|
||||
{ m with data.name = model.data.name }, Elmish.Cmd.none
|
||||
{ model with data = s }, Elmish.Cmd.none
|
||||
| XtractMsg.SetStarted (started, jobIdOpt) -> { model with start = started, jobIdOpt }, Elmish.Cmd.none
|
||||
| XtractMsg.ResetModel m -> { m with data.name = model.data.name }, Elmish.Cmd.none
|
||||
| XtractMsg.Noop () -> model, Elmish.Cmd.none
|
||||
|
||||
//
|
||||
@@ -105,7 +102,8 @@ let updateExtractionSite (posOpt: (float * float) option, map) =
|
||||
match posOpt with
|
||||
| Some pos ->
|
||||
let p' = pos |> posToCoord
|
||||
let point = Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
|
||||
let point =
|
||||
Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
|
||||
let feature = Feature.feature [ feature.geometryOrProperties point ]
|
||||
source.addFeature (feature)
|
||||
| _ -> ()
|
||||
@@ -124,28 +122,19 @@ let private extractionSiteControls (dispatch': XtractMsg -> unit) (xmodel': Xtra
|
||||
if circle.intersectsCoordinate (pos |> posToCoord) then
|
||||
Some pos
|
||||
else
|
||||
console.error("[DataExtraction] Trying to place extraction point outside of fencing radius")
|
||||
console.error ("[DataExtraction] Trying to place extraction point outside of fencing radius")
|
||||
None
|
||||
|
||||
let handleMapPlaceExtraction (coords: Coordinate) =
|
||||
console.debug ($"[DataExtraction] Click add site: %s{coords.ToString ()}")
|
||||
coordToPos coords
|
||||
|> tryFence
|
||||
|> SetExtractionSite
|
||||
|> dispatch'
|
||||
coordToPos coords |> tryFence |> SetExtractionSite |> dispatch'
|
||||
|
||||
let setPosition (pos: float * float) : unit =
|
||||
Some pos
|
||||
|> SetExtractionSite
|
||||
|> dispatch'
|
||||
Some pos |> SetExtractionSite |> dispatch'
|
||||
|
||||
let selectedPos =
|
||||
xmodel'.position
|
||||
|> Option.defaultValue (0.0, 0.0)
|
||||
|> toWgs84'
|
||||
let selectedPos = xmodel'.position |> Option.defaultValue (0.0, 0.0) |> toWgs84'
|
||||
|
||||
let deleteSite (_: Browser.Types.Event) =
|
||||
None |> SetExtractionSite |> dispatch'
|
||||
let deleteSite (_: Browser.Types.Event) = None |> SetExtractionSite |> dispatch'
|
||||
|
||||
let latitudeBox =
|
||||
let latitude = snd selectedPos
|
||||
@@ -264,21 +253,22 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
|
||||
let map = model.map
|
||||
let currentFrame = model.frame
|
||||
let archiveStartUTC = archive.startTime.ToUniversalTime ()
|
||||
let archiveEndT = archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
|
||||
let archiveStartT = archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
|
||||
let archiveEndT =
|
||||
archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
|
||||
let archiveStartT =
|
||||
archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
|
||||
let submitted, setSubmitted = Hook.useState false
|
||||
|
||||
let createNewModel () : XtractModel =
|
||||
let data =
|
||||
match xtractModelOpt with
|
||||
| Some existing -> { existing.data with fvcom = archive.id }
|
||||
| None ->
|
||||
{
|
||||
XtractData.empty with
|
||||
fvcom = archive.id
|
||||
start = archiveStartT
|
||||
stop = archiveStartT.AddDays 2.0
|
||||
}
|
||||
| None -> {
|
||||
XtractData.empty with
|
||||
fvcom = archive.id
|
||||
start = archiveStartT
|
||||
stop = archiveStartT.AddDays 2.0
|
||||
}
|
||||
{
|
||||
fence = archive.polygon
|
||||
start = false, None
|
||||
@@ -309,11 +299,21 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
|
||||
SetXtractModel (Some newModel) |> dispatch
|
||||
)
|
||||
|
||||
|
||||
let setStartDateTime (dt: DateTime) =
|
||||
SetData { xmodel'.data with start = dt } |> dispatch'
|
||||
// Set time to midday (12:00)
|
||||
let startDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
|
||||
let stopDate = xmodel'.data.stop
|
||||
let newStop = if startDate >= stopDate then startDate.AddDays(1.0) else stopDate
|
||||
console.debug ("[DataExtraction] setStartDateTime:", startDate, "stop:", newStop)
|
||||
SetData { xmodel'.data with start = startDate; stop = newStop } |> dispatch'
|
||||
|
||||
let setStopDateTime (dt: DateTime) =
|
||||
SetData { xmodel'.data with stop = dt } |> dispatch'
|
||||
let endDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
|
||||
let startDate = xmodel'.data.start
|
||||
let newStart = if endDate <= startDate then endDate.AddDays(-1.0) else startDate
|
||||
console.debug ("[DataExtraction] setStopDateTime:", endDate, "start:", newStart)
|
||||
SetData { xmodel'.data with stop = endDate; start = newStart } |> dispatch'
|
||||
|
||||
let setName (s: string) =
|
||||
let currentModel = modelRef.contents |> Option.defaultValue xmodel'
|
||||
@@ -322,7 +322,10 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
|
||||
let minStartDate = archiveStartUTC
|
||||
let maxStartDate = archiveEndT
|
||||
let minEndDate = archiveStartUTC
|
||||
let maxEndDate = archiveEndT
|
||||
// maxEndDate is one year after the selected start date
|
||||
let maxEndDate =
|
||||
let startDate = xmodel'.data.start
|
||||
startDate.AddYears(1)
|
||||
|
||||
let metaControls =
|
||||
html
|
||||
@@ -579,4 +582,4 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
|
||||
</div>
|
||||
</div>
|
||||
{submitButtons}
|
||||
"""
|
||||
"""
|
||||
@@ -43,16 +43,16 @@ let inboxDialog
|
||||
let table = document.getElementById "inbox-table"
|
||||
async {
|
||||
let! mbox = Remoting.inboxApi().getMessages ()
|
||||
// if mbox.Length = 0 then
|
||||
// table?items <- [| {
|
||||
// id = Guid.Empty
|
||||
// content = ""
|
||||
// unread = false
|
||||
// type' = MessageType.Note
|
||||
// created = DateTime.Now
|
||||
// } |]
|
||||
// else
|
||||
table?items <- mbox
|
||||
if mbox.Length = 0 then
|
||||
table?items <- [| {
|
||||
id = Guid.Empty
|
||||
content = ""
|
||||
unread = false
|
||||
type' = MessageType.Note
|
||||
created = DateTime.Now
|
||||
} |]
|
||||
else
|
||||
table?items <- mbox
|
||||
} |> Async.StartImmediate
|
||||
|
||||
Hook.useEffectOnChange(arg.unread, loadMessages)
|
||||
@@ -65,30 +65,18 @@ let inboxDialog
|
||||
|> Set.ofSeq
|
||||
|> setSelected
|
||||
|
||||
let doDelete _ =
|
||||
let doDelete selected _ =
|
||||
let table = document.getElementById "inbox-table"
|
||||
let selectedSet : Guid JS.Set = table?selectedSet
|
||||
let items: InboxItem array = table?items
|
||||
async {
|
||||
let toDelete =
|
||||
items
|
||||
|> Array.filter (fun item -> selectedSet.has(item.id))
|
||||
|> Array.map (fun item -> item.id)
|
||||
|
||||
console.debug("Deleting", toDelete.Length, "messages")
|
||||
items
|
||||
|> Array.filter (fun item -> Set.contains item.id selected)
|
||||
|> Array.iter (fun item ->
|
||||
console.log("Delete: %A", item.content)
|
||||
do arg.deleteMessage item.id
|
||||
)
|
||||
|
||||
for id in toDelete do
|
||||
arg.deleteMessage id
|
||||
|
||||
// Clear selection immediately
|
||||
selectedSet.clear()
|
||||
setSelected Set.empty
|
||||
|
||||
// Wait a bit for server to process, then reload
|
||||
do! Async.Sleep 200
|
||||
let! mbox = Remoting.inboxApi().getMessages ()
|
||||
table?items <- mbox
|
||||
} |> Async.StartImmediate
|
||||
loadMessages ()
|
||||
|
||||
let doRead selected _ =
|
||||
let table = document.getElementById "inbox-table"
|
||||
@@ -118,7 +106,7 @@ let inboxDialog
|
||||
html $"""
|
||||
<sp-field-group horizontal>
|
||||
<sp-action-button
|
||||
@click={Ev(doDelete)}
|
||||
@click={Ev(doDelete selected)}
|
||||
?disabled={selected.Count = 0}>
|
||||
<sp-icon-delete slot="icon"></sp-icon-delete> Delete selected
|
||||
</sp-action-button>
|
||||
@@ -308,11 +296,8 @@ let inboxDialog
|
||||
let sortFn = if sortDir = "asc" then Array.sortBy else Array.sortByDescending
|
||||
table?items
|
||||
|> sortFn (fun item -> JS.expr_js $"{item}[{sortKey}]")
|
||||
|> fun items -> table?items <- items))
|
||||
|
||||
let table =
|
||||
html $"""
|
||||
"""
|
||||
|> fun items -> table?items <- items)
|
||||
)
|
||||
|
||||
html $"""
|
||||
<div class="inbox-dialog">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Fable.Browser.IndexedDB": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.2.0, )",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Fable.Core": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.4.0, )",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"FSharp.Core": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.303, )",
|
||||
|
||||
@@ -4,6 +4,8 @@ open System
|
||||
open System.Data
|
||||
open System.Linq
|
||||
open System.Security.Claims
|
||||
open System.Threading.Tasks
|
||||
|
||||
open Archmaester.Actors
|
||||
open Archmaester.Dto
|
||||
open Dapper.FSharp.PostgreSQL
|
||||
@@ -145,46 +147,47 @@ module Handlers =
|
||||
}
|
||||
|> Async.AwaitTask
|
||||
|
||||
let private setNewArchivePermissions (p: Permission) =
|
||||
let id = ActorId p.uid
|
||||
let t = DateTime.UtcNow
|
||||
let term: Term = { start_time = t; end_time = t }
|
||||
|
||||
let ticket: Ticket = {
|
||||
task = JobType.Any
|
||||
quota = 100.0
|
||||
start_time = t
|
||||
end_time = t
|
||||
}
|
||||
|
||||
let private setNewArchivePermissions (p: Permission) : Async<Result<bool, exn>> =
|
||||
task {
|
||||
let proxy = ActorProxy.Create<IArchiveAccessActor> (id, nameof ArchiveAccessActor)
|
||||
let! (o: bool seq) = p.owners |> Array.map (fun x -> proxy.AddOwner (p.aid, x)) |> sequence
|
||||
let! uv = p.users |> Array.map (fun x -> proxy.AllowUserView (p.aid, x, term)) |> sequence
|
||||
let! ux =
|
||||
p.users
|
||||
|> Array.map (fun x -> proxy.AllowUserExec (p.aid, x, ticket))
|
||||
|> sequence
|
||||
let! gv =
|
||||
p.groups
|
||||
|> Array.map (fun x -> proxy.AllowGroupView (p.aid, x, term))
|
||||
|> sequence
|
||||
try
|
||||
let id = ActorId p.uid
|
||||
let t = DateTime.UtcNow
|
||||
let term: Term = { start_time = t; end_time = t }
|
||||
|
||||
let ticket: Ticket = {
|
||||
task = JobType.Any
|
||||
quota = 100.0
|
||||
start_time = t
|
||||
end_time = t
|
||||
}
|
||||
let proxy = ActorProxy.Create<IArchiveAccessActor> (id, nameof ArchiveAccessActor)
|
||||
let! (o: bool seq) = p.owners |> Array.map (fun x -> proxy.AddOwner (p.aid, x)) |> sequence
|
||||
let! uv = p.users |> Array.map (fun x -> proxy.AllowUserView (p.aid, x, term)) |> sequence
|
||||
let! ux =
|
||||
p.users
|
||||
|> Array.map (fun x -> proxy.AllowUserExec (p.aid, x, ticket))
|
||||
|> sequence
|
||||
let! gv =
|
||||
p.groups
|
||||
|> Array.map (fun x -> proxy.AllowGroupView (p.aid, x, term))
|
||||
|> sequence
|
||||
|
||||
if p.ref.IsSome then
|
||||
let proxy =
|
||||
ActorProxy.Create<IArchiveAccessActor> (ActorId p.uid, nameof ArchiveAccessActor)
|
||||
if p.ref.IsSome then
|
||||
let proxy =
|
||||
ActorProxy.Create<IArchiveAccessActor> (ActorId p.uid, nameof ArchiveAccessActor)
|
||||
|
||||
let! _ = proxy.AddParent (p.aid, p.ref.Value) |> Async.AwaitTask
|
||||
()
|
||||
let! _ = proxy.AddParent (p.aid, p.ref.Value) |> Async.AwaitTask
|
||||
()
|
||||
|
||||
let all = Seq.concat [ o; uv; ux; gv ]
|
||||
let res = all |> Seq.reduce (&&)
|
||||
let all = Seq.concat [ o; uv; ux; gv ]
|
||||
let res = all |> Seq.reduce (&&)
|
||||
|
||||
if not res then
|
||||
Log.Warning $"Archmaester.setArchivePermissions returned false: %A{all}"
|
||||
if not res then
|
||||
Log.Warning $"Archmaester.setArchivePermissions returned false: %A{all}"
|
||||
|
||||
return res
|
||||
return Ok res
|
||||
with ex ->
|
||||
return Error ex
|
||||
}
|
||||
|> Async.AwaitTask
|
||||
|
||||
@@ -219,10 +222,12 @@ module Handlers =
|
||||
Log.Information $"Adding archive: {item.props.name}"
|
||||
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
let archivist = Archives.Archivist (Db.getDataSource ())
|
||||
use db = archivist.startConnection ()
|
||||
let tr = archivist.startTransaction db
|
||||
|
||||
try
|
||||
let saveRes = db.tryAddArchive item
|
||||
let saveRes = archivist.tryAddArchive(db, item)
|
||||
Log.Debug $"saveRes %A{saveRes}"
|
||||
|
||||
match saveRes with
|
||||
@@ -241,7 +246,7 @@ module Handlers =
|
||||
|
||||
let aid = archive.ArchiveId
|
||||
|
||||
let! _ =
|
||||
let! res =
|
||||
setNewArchivePermissions {
|
||||
uid = uid
|
||||
gid = gid
|
||||
@@ -252,9 +257,22 @@ module Handlers =
|
||||
ref = item.props.reference
|
||||
}
|
||||
|
||||
ctx.SetStatusCode 201
|
||||
return Ok ()
|
||||
match res with
|
||||
| Ok ok ->
|
||||
if ok then
|
||||
tr.Commit ()
|
||||
ctx.SetStatusCode 201
|
||||
return Ok ()
|
||||
else
|
||||
tr.Rollback ()
|
||||
Log.Error("addArchive: error: one of the permissions failed")
|
||||
return Error "Error adding archive"
|
||||
| Error ex ->
|
||||
tr.Rollback ()
|
||||
Log.Error(ex, "addArchive: error")
|
||||
return Error "Error adding archive"
|
||||
with exn ->
|
||||
tr.Rollback ()
|
||||
Log.Error $"addArchive: error: {exn}"
|
||||
ctx.SetStatusCode 500
|
||||
return Error $"Could not add Archive: {exn.Message}"
|
||||
@@ -413,6 +431,21 @@ module Handlers =
|
||||
return Error $"Could not retrieve archive {aid}: {err}"
|
||||
}
|
||||
|
||||
let getBasePath (aid: ArchiveId) =
|
||||
Log.Information $"Getting archive basePath: {aid}"
|
||||
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
|
||||
match db.getBasePath aid with
|
||||
| Ok path ->
|
||||
Log.Debug $"getBasePath: {path}"
|
||||
return Ok path
|
||||
| Error err ->
|
||||
Log.Error $"getBasePath: archive with id {aid} not found"
|
||||
return Error $"Could not retrieve archive {aid}: {err}"
|
||||
}
|
||||
|
||||
let getFiles (aid: ArchiveId) =
|
||||
Log.Information $"Getting archive files: {aid}"
|
||||
|
||||
@@ -436,10 +469,10 @@ module Handlers =
|
||||
|
||||
match db.getAllArchiveFiles aid with
|
||||
| Ok files ->
|
||||
Log.Debug $"getFiles: {files.basePath} {files.series.Length}"
|
||||
Log.Debug $"getAllFiles: {files.basePath} {files.series.Length}"
|
||||
return Ok files
|
||||
| Error err ->
|
||||
Log.Error $"getFiles: archive with id {aid} not found"
|
||||
Log.Error $"getAllFiles: archive with id {aid} not found"
|
||||
return Error $"Could not retrieve archive {aid}: {err}"
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () }
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -286,31 +286,35 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
|
||||
}
|
||||
|
||||
member this.getActiveJobs aid =
|
||||
Log.Debug $"get active jobs for: {this.myId}"
|
||||
task {
|
||||
Log.Debug $"get active jobs for: {this.myId}"
|
||||
|
||||
let active = [|
|
||||
for v in this.jobs do
|
||||
Log.Debug $"job: {aid} -> %A{v.Value}"
|
||||
let active = [|
|
||||
for v in this.jobs do
|
||||
Log.Debug $"job: {aid} -> %A{v.Value}"
|
||||
|
||||
if v.Value.refId = aid then
|
||||
match v.Value.status with
|
||||
| JobStatus.New
|
||||
| JobStatus.Waiting
|
||||
| JobStatus.Failed
|
||||
| JobStatus.Running -> v.Value
|
||||
| _ -> ()
|
||||
else
|
||||
()
|
||||
|]
|
||||
if v.Value.refId = aid then
|
||||
match v.Value.status with
|
||||
| JobStatus.New
|
||||
| JobStatus.Waiting
|
||||
| JobStatus.Failed
|
||||
| JobStatus.Running -> v.Value
|
||||
| _ -> ()
|
||||
else
|
||||
()
|
||||
|]
|
||||
|
||||
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
|
||||
task { return active }
|
||||
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
|
||||
return active
|
||||
}
|
||||
|
||||
member this.getFenceRadius() =
|
||||
Log.Information "fence?"
|
||||
let r = settings.file.fenceRadius
|
||||
Log.Information $"fence: {r}"
|
||||
Task.FromResult r
|
||||
task {
|
||||
Log.Information "fence?"
|
||||
let r = settings.file.fenceRadius
|
||||
Log.Information $"fence: {r}"
|
||||
return r
|
||||
}
|
||||
|
||||
member this.checkFence aid (pts: (float * float) list) =
|
||||
task { return this.validatePoints aid pts }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
module Main
|
||||
|
||||
open System
|
||||
open System.Text.Json
|
||||
open System.Text.Json.Serialization
|
||||
open System.Net.Http
|
||||
|
||||
open Microsoft.AspNetCore.Authentication
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.Cors.Infrastructure
|
||||
open Microsoft.AspNetCore.Hosting
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.AspNetCore.SignalR
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Microsoft.Extensions.FileProviders
|
||||
open Microsoft.Extensions.Hosting
|
||||
open Microsoft.Extensions.Logging
|
||||
|
||||
open Argu
|
||||
open Dapr.Actors
|
||||
open Dapr.Actors.Client
|
||||
open Dapr.Client
|
||||
open FSharp.Data
|
||||
open FsToolkit.ErrorHandling
|
||||
open Fable.SignalR
|
||||
open Giraffe
|
||||
open Microsoft.AspNetCore.Authentication
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.Hosting
|
||||
open Microsoft.AspNetCore.Cors.Infrastructure
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.AspNetCore.SignalR
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Microsoft.Extensions.Logging
|
||||
open Microsoft.Extensions.FileProviders
|
||||
open Microsoft.Extensions.Hosting
|
||||
open System.Net.Http
|
||||
open Oceanbox.DataAgent
|
||||
open Polly
|
||||
open Prometheus
|
||||
open Saturn
|
||||
open Saturn.Dapr
|
||||
open Saturn.OpenTelemetry
|
||||
open Saturn.Observer
|
||||
open Saturn.OpenTelemetry
|
||||
open Sentry
|
||||
open Sentry.AspNetCore
|
||||
open Sentry.Extensibility
|
||||
@@ -37,6 +39,7 @@ open Serilog.Sinks.OpenTelemetry
|
||||
|
||||
open Atlantis
|
||||
open Atlantis.Shared
|
||||
open Oceanbox.DataAgent
|
||||
open Oceanbox.ServerPack.MultiAuth
|
||||
open Saturn.OpenFga
|
||||
open Settings
|
||||
@@ -82,7 +85,8 @@ let configureSerilog () =
|
||||
let configureLogging (builder: ILoggingBuilder) =
|
||||
builder
|
||||
.ClearProviders()
|
||||
.AddSerilog() |> ignore
|
||||
.AddSerilog()
|
||||
|> ignore
|
||||
|
||||
let corsPolicy (policy: CorsPolicyBuilder) =
|
||||
policy
|
||||
@@ -91,7 +95,7 @@ let corsPolicy (policy: CorsPolicyBuilder) =
|
||||
.AllowAnyMethod()
|
||||
.WithOrigins(appsettings.file.allowedOrigins)
|
||||
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
||||
|> ignore
|
||||
|> ignore
|
||||
|
||||
type UserIdProvider() =
|
||||
interface IUserIdProvider with
|
||||
@@ -254,12 +258,12 @@ let stopImpersonating (next: HttpFunc) (ctx: HttpContext) =
|
||||
let getBarentsWatchToken (next: HttpFunc) (ctx: HttpContext) =
|
||||
task {
|
||||
let! tokenRes =
|
||||
tryGetEnv "BARENTSWATCH_CLIENT_ID"
|
||||
|> Option.bind (fun id ->
|
||||
tryGetEnv "BARENTSWATCH_SECRET"
|
||||
|> Option.map (BarentsWatch.getToken appsettings.redis id))
|
||||
|> Option.defaultValue (Error "Secret or client id missing" |> async.Return)
|
||||
|> Async.StartAsTask
|
||||
taskResult {
|
||||
let! id = tryGetEnv "BARENTSWATCH_CLIENT_ID" |> Result.requireSome "Missing barentswatch client id"
|
||||
let! secret = tryGetEnv "BARENTSWATCH_SECRET" |> Result.requireSome "Missing barentswatch secret"
|
||||
let! token = BarentsWatch.getToken appsettings.redis id secret
|
||||
return token
|
||||
}
|
||||
|
||||
match tokenRes with
|
||||
| Error err ->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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
|
||||
@@ -1,103 +1,103 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
tag: latest
|
||||
tag: latest
|
||||
|
||||
podAnnotations:
|
||||
dapr.io/enabled: "true"
|
||||
dapr.io/app-id: "<x>-atlantis"
|
||||
dapr.io/app-port: "8085"
|
||||
dapr.io/api-token-secret: "dapr-api-token"
|
||||
dapr.io/config: "tracing"
|
||||
dapr.io/app-protocol: "http"
|
||||
dapr.io/log-as-json: "true"
|
||||
dapr.io/sidecar-cpu-request: "10m"
|
||||
dapr.io/sidecar-memory-request: "50Mi"
|
||||
# dapr.io/sidecar-cpu-limit: "300m"
|
||||
# dapr.io/sidecar-memory-limit: "1000Mi"
|
||||
dapr.io/enabled: "true"
|
||||
dapr.io/app-id: "<x>-atlantis"
|
||||
dapr.io/app-port: "8085"
|
||||
dapr.io/api-token-secret: "dapr-api-token"
|
||||
dapr.io/config: "tracing"
|
||||
dapr.io/app-protocol: "http"
|
||||
dapr.io/log-as-json: "true"
|
||||
dapr.io/sidecar-cpu-request: "10m"
|
||||
dapr.io/sidecar-memory-request: "50Mi"
|
||||
# dapr.io/sidecar-cpu-limit: "300m"
|
||||
# dapr.io/sidecar-memory-limit: "1000Mi"
|
||||
|
||||
env:
|
||||
- name: APP_NAMESPACE
|
||||
value: <x>-atlantis
|
||||
- name: APP_VERSION
|
||||
value: "<x>-tilt"
|
||||
- name: LOG_LEVEL
|
||||
value: "verbose"
|
||||
- name: REDIS_USER
|
||||
value: default
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-redis
|
||||
key: redis-password
|
||||
- name: DB_HOST
|
||||
value: <x>-atlantis-db-rw
|
||||
- name: DB_PORT
|
||||
value: "5432"
|
||||
- name: DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-db-app
|
||||
key: username
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-db-app
|
||||
key: password
|
||||
- name: DAPR_API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dapr-api-token
|
||||
key: token
|
||||
- name: ANALYTICS_WEB_ID
|
||||
value: 6f26c702-2c6d-46ea-8122-ffcedda5f762
|
||||
- name: APP_NAMESPACE
|
||||
value: <x>-atlantis
|
||||
- name: APP_VERSION
|
||||
value: "<x>-tilt"
|
||||
- name: LOG_LEVEL
|
||||
value: "verbose"
|
||||
- name: REDIS_USER
|
||||
value: default
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-redis
|
||||
key: redis-password
|
||||
- name: DB_HOST
|
||||
value: <x>-atlantis-db-rw
|
||||
- name: DB_PORT
|
||||
value: "5432"
|
||||
- name: DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-db-app
|
||||
key: username
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-db-app
|
||||
key: password
|
||||
- name: DAPR_API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dapr-api-token
|
||||
key: token
|
||||
- name: ANALYTICS_WEB_ID
|
||||
value: 6f26c702-2c6d-46ea-8122-ffcedda5f762
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
|
||||
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
hosts:
|
||||
- host: <x>-atlantis.dev.oceanbox.io
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
internal:
|
||||
- path: /internal
|
||||
pathType: ImplementationSpecific
|
||||
- path: /dapr
|
||||
pathType: ImplementationSpecific
|
||||
- path: /actors
|
||||
pathType: ImplementationSpecific
|
||||
- path: /job
|
||||
pathType: ImplementationSpecific
|
||||
- path: /events
|
||||
pathType: ImplementationSpecific
|
||||
- path: /metrics
|
||||
pathType: ImplementationSpecific
|
||||
tls:
|
||||
- hosts:
|
||||
- <x>-atlantis.dev.oceanbox.io
|
||||
secretName: <x>-atlantis-tls
|
||||
enabled: true
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
|
||||
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
hosts:
|
||||
- host: <x>-atlantis.dev.oceanbox.io
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
internal:
|
||||
- path: /internal
|
||||
pathType: ImplementationSpecific
|
||||
- path: /dapr
|
||||
pathType: ImplementationSpecific
|
||||
- path: /actors
|
||||
pathType: ImplementationSpecific
|
||||
- path: /job
|
||||
pathType: ImplementationSpecific
|
||||
- path: /events
|
||||
pathType: ImplementationSpecific
|
||||
- path: /metrics
|
||||
pathType: ImplementationSpecific
|
||||
tls:
|
||||
- hosts:
|
||||
- <x>-atlantis.dev.oceanbox.io
|
||||
secretName: <x>-atlantis-tls
|
||||
|
||||
storage:
|
||||
enabled: true
|
||||
size: 1G
|
||||
accessMode: ReadWriteOnce
|
||||
storageClass: ceph-rdb
|
||||
enabled: true
|
||||
size: 1G
|
||||
accessMode: ReadWriteOnce
|
||||
storageClass: ceph-rdb
|
||||
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
|
||||
cluster:
|
||||
backup:
|
||||
enabled: false
|
||||
backup:
|
||||
enabled: false
|
||||
|
||||
redis:
|
||||
enabled: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -5,7 +5,7 @@
|
||||
}:
|
||||
dockerTools.buildLayeredImage {
|
||||
name = "Codex";
|
||||
tag = "0.0.0-alpha.1";
|
||||
tag = "0.0.1";
|
||||
created = "now";
|
||||
|
||||
contents = [
|
||||
@@ -24,4 +24,4 @@ dockerTools.buildLayeredImage {
|
||||
cmd = [ "Codex.Server" ];
|
||||
workingDir = "/app";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ open Fable.Core
|
||||
open Fable.Core.JsInterop
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
open FS.FluentUI
|
||||
|
||||
module Archive =
|
||||
let private fetchArchiveAcl (id: System.Guid) : JS.Promise<Result<Archmaester.Dto.ArchiveAcl, string>> =
|
||||
@@ -29,16 +30,6 @@ module Archive =
|
||||
return subs
|
||||
}
|
||||
|
||||
let private deleteArchive (id: System.Guid) =
|
||||
promise {
|
||||
try
|
||||
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
|
||||
return res
|
||||
with e ->
|
||||
console.error("Error deleting archive: %o", e)
|
||||
return Error "Error deleting archive"
|
||||
}
|
||||
|
||||
let private addArchiveGroup (archiveId: System.Guid) (group: string) =
|
||||
promise {
|
||||
try
|
||||
@@ -227,9 +218,6 @@ module Archive =
|
||||
let View (archiveId: System.Guid) =
|
||||
let loading, setLoading = React.useState true
|
||||
let error, setError = React.useState<string option> None
|
||||
let editing, setEditing = React.useState false
|
||||
let deleting, setDeleting = React.useState false
|
||||
let deleted, setDeleted = React.useState false
|
||||
let selectedGroup, setSelectedGroup = React.useState<string option> None
|
||||
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
|
||||
let aclOpt, setAcl = React.useState<Archmaester.Dto.ArchiveAcl option> None
|
||||
@@ -257,21 +245,6 @@ module Archive =
|
||||
| None ->
|
||||
console.warn("ACL has not been downloaded")
|
||||
|
||||
let handleDeleteArchive (id: System.Guid) =
|
||||
deleteArchive id
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok deleted ->
|
||||
if deleted then
|
||||
console.info("Archive deleted successfully")
|
||||
setDeleted true
|
||||
else
|
||||
setError (Some "Failed to delete archive")
|
||||
| Error err ->
|
||||
console.error("Error deleting archive: %s", err)
|
||||
setError (Some err)
|
||||
)
|
||||
|
||||
let handleSelectedGroupChange (groupOpt: string option) =
|
||||
console.debug("Selected group: %s", groupOpt)
|
||||
setSelectedGroup groupOpt
|
||||
@@ -319,94 +292,27 @@ module Archive =
|
||||
| None ->
|
||||
match archiveOpt with
|
||||
| Some archive ->
|
||||
Html.h1 (sprintf "Archive %s" archive.name)
|
||||
Fui.text.title1 (sprintf "Archive %s" archive.name)
|
||||
|
||||
if deleting then
|
||||
Html.h2 "Deleting archive ..."
|
||||
else
|
||||
Html.none
|
||||
|
||||
if deleted then
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.h2 "Archive successfully deleted"
|
||||
Html.a [
|
||||
prop.href (Router.format "archives")
|
||||
prop.text "Return to archives listing"
|
||||
]
|
||||
]
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "gap-8" ]
|
||||
prop.children [
|
||||
Archives.EditArchiveDialog archive (fun edited ->
|
||||
Some
|
||||
{archive with
|
||||
name = edited.Name
|
||||
startTime = edited.StartTime
|
||||
endTime = edited.StartTime.AddHours(edited.Frames)
|
||||
frames = edited.Frames
|
||||
isPublished = edited.Published
|
||||
isPublic = edited.Public
|
||||
}
|
||||
|> setArchive
|
||||
console.debug ("response: ", edited)
|
||||
)
|
||||
Archives.DeleteArchiveDialog archive
|
||||
]
|
||||
else
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "gap-8" ]
|
||||
prop.children [
|
||||
if deleting then
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setDeleting false
|
||||
handleDeleteArchive archive.archiveId
|
||||
)
|
||||
prop.text "Save"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setDeleting false
|
||||
)
|
||||
prop.text "Cancel"
|
||||
]
|
||||
elif editing then
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setEditing false
|
||||
)
|
||||
prop.text "Save"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setEditing false
|
||||
)
|
||||
prop.text "Cancel"
|
||||
]
|
||||
else
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setEditing true
|
||||
)
|
||||
prop.text "Edit"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setDeleting true
|
||||
)
|
||||
prop.text "Delete"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
if editing then
|
||||
Html.form [
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.label [
|
||||
prop.htmlFor "published-checkbox"
|
||||
prop.text "Published: "
|
||||
]
|
||||
|
||||
Html.input [
|
||||
prop.id "published-checkbox"
|
||||
prop.type' "checkbox"
|
||||
prop.custom ("checked", archive.isPublished)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
else
|
||||
Html.none
|
||||
]
|
||||
|
||||
Archives.InfoSection archive
|
||||
|
||||
@@ -744,4 +650,4 @@ module Archive =
|
||||
|
||||
| None ->
|
||||
Html.h1 "Archive not found"
|
||||
]
|
||||
]
|
||||
@@ -1,6 +1,8 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
open Fable.Core
|
||||
open Feliz
|
||||
open FS.FluentUI
|
||||
|
||||
type Archives =
|
||||
[<ReactComponent>]
|
||||
@@ -13,49 +15,131 @@ type Archives =
|
||||
Html.section [
|
||||
prop.classes [ "flex-row"; "flex-wrap"; "gap-16" ]
|
||||
prop.children [
|
||||
Html.ul [
|
||||
prop.children [
|
||||
Html.li [
|
||||
prop.text (sprintf "Description: %s" archive.description)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Archive type: %s" (string archive.archiveType))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Projection: %s" archive.projection)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Frequency: %d" archive.freq)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Frames: %d" archive.frames)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Created: %s" (archive.created.ToLongDateString()))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Start time: %s" (archive.startTime.ToLongDateString()))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "End time: %s" (archive.endTime.ToLongDateString()))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Length: %d days %d hours" archiveLength.Days archiveLength.Hours)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Owner: %s" archive.owner)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Expires: %s" (archive.expires |> Option.map string |> Option.defaultValue ""))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Publised: %b" archive.isPublished)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Public: %b" archive.isPublic)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Location: %s" "tos")
|
||||
Fui.table [
|
||||
table.classes [ "flex-basis-7"; "flex-grow" ]
|
||||
table.children [
|
||||
Fui.tableBody [
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.textDescriptionRegular [])
|
||||
tableCellLayout.children [ Fui.text "Description" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text archive.description ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.contactCardGenericRegular [])
|
||||
tableCellLayout.children [ Fui.text "Archive type" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.archiveType) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.globeSurfaceRegular [])
|
||||
tableCellLayout.children [ Fui.text "Projection" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text archive.projection ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.timerRegular [])
|
||||
tableCellLayout.children [ Fui.text "Frequency" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.freq) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.filmstripRegular [])
|
||||
tableCellLayout.children [ Fui.text "Frames" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.frames) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.calendarAddRegular [])
|
||||
tableCellLayout.children [ Fui.text "Time created" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (archive.created.ToLongDateString()) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.playRegular [])
|
||||
tableCellLayout.children [ Fui.text "Start time" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (archive.startTime.ToLongDateString()) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.stopRegular [])
|
||||
tableCellLayout.children [ Fui.text "End time" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (archive.endTime.ToLongDateString()) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.autoFitWidthRegular [])
|
||||
tableCellLayout.children [ Fui.text "Length" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (sprintf "%d days %d hours" archiveLength.Days archiveLength.Hours) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.eyeRegular [])
|
||||
tableCellLayout.children [ Fui.text "Published" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.isPublished) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.peopleEyeRegular [])
|
||||
tableCellLayout.children [ Fui.text "Public" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.isPublic) ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -70,3 +154,342 @@ type Archives =
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
static member EditArchiveDialog(archive: Archmaester.Dto.ArchiveProps) onEdit =
|
||||
let initForm : Remoting.EditArchiveRequest =
|
||||
{
|
||||
Name = archive.name
|
||||
StartTime = archive.startTime
|
||||
Frames = archive.frames
|
||||
Published = archive.isPublished
|
||||
Public = archive.isPublic
|
||||
}
|
||||
let form, setForm = React.useState initForm
|
||||
let isOpen, setIsOpen = React.useState false
|
||||
let errMsg, setErrMsg = React.useState None
|
||||
let dataSet, setDataSet = React.useState None
|
||||
|
||||
React.useEffectOnce (fun _ ->
|
||||
Remoting.adminApi.getArchiveDataSet archive.archiveId
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok ds -> setDataSet (Some ds)
|
||||
| Error msg ->
|
||||
Browser.Dom.console.error("Error fetching dataset: %s", msg)
|
||||
)
|
||||
)
|
||||
|
||||
let handleEditArchive () =
|
||||
let utcTime = System.DateTime(form.StartTime.Year, form.StartTime.Month, form.StartTime.Day, 0, 0, 0, System.DateTimeKind.Utc)
|
||||
Remoting.adminApi.updateArchive archive.archiveId {form with StartTime = utcTime}
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok newArchive ->
|
||||
Browser.Dom.console.info("Added archive %s with id %s", newArchive.Name, string newArchive.Id)
|
||||
onEdit newArchive
|
||||
setIsOpen false
|
||||
| Error msg ->
|
||||
Browser.Dom.console.error("Error adding archive %s: %s", form.Name, msg)
|
||||
setErrMsg (Some msg)
|
||||
)
|
||||
|
||||
let framesExceedEnd =
|
||||
dataSet
|
||||
|> Option.map (fun ds ->
|
||||
form.StartTime.AddHours(form.Frames) > ds.EndTime
|
||||
)
|
||||
|> Option.defaultValue false
|
||||
|
||||
Fui.dialog [
|
||||
dialog.open' isOpen
|
||||
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
|
||||
setIsOpen d.``open``
|
||||
// NOTE: Reset form on open, so it's not noticeable in the UI
|
||||
if d.``open`` then
|
||||
setForm initForm
|
||||
)
|
||||
dialog.children [
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.icon (Fui.icon.editRegular [])
|
||||
button.text "Edit"
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.dialogSurface [
|
||||
dialogSurface.classes []
|
||||
dialogSurface.children [
|
||||
Fui.dialogBody [
|
||||
dialogBody.classes []
|
||||
dialogBody.children [
|
||||
Fui.dialogTitle [
|
||||
dialogTitle.text (sprintf "Edit archive %s" archive.name)
|
||||
dialogTitle.action (
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.action.close
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.appearance.transparent
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.dialogContent [
|
||||
dialogContent.classes ["flex-column"; "gap-8"]
|
||||
dialogContent.children [
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Name"
|
||||
]
|
||||
)
|
||||
field.children (
|
||||
Fui.input [
|
||||
input.value form.Name
|
||||
input.onChange (fun (v: ValueProp<string>) ->
|
||||
if v.value.Length <= 80 then
|
||||
setForm {form with Name = v.value}
|
||||
)
|
||||
]
|
||||
)
|
||||
field.hint $"{form.Name.Length}/80"
|
||||
]
|
||||
Html.div [
|
||||
prop.classes ["flex-row"; "gap-8"]
|
||||
prop.children [
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Start Date"
|
||||
]
|
||||
)
|
||||
|
||||
field.children (
|
||||
Fui.datePicker [
|
||||
datePicker.placeholder "Select a date..."
|
||||
datePicker.showWeekNumbers true
|
||||
datePicker.formatDate (fun d -> d.ToShortDateString())
|
||||
match dataSet with
|
||||
| None -> ()
|
||||
| Some ds ->
|
||||
datePicker.minDate ds.StartTime
|
||||
datePicker.maxDate ds.EndTime
|
||||
datePicker.value (Some form.StartTime)
|
||||
datePicker.onSelectDate (fun d ->
|
||||
d |> Option.iter (fun d' ->
|
||||
setForm {form with StartTime = d'}
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Days"
|
||||
]
|
||||
)
|
||||
field.children (
|
||||
Fui.spinButton [
|
||||
spinButton.value (form.Frames / 24)
|
||||
spinButton.min 1
|
||||
spinButton.onChange (fun (d: SpinButtonOnChangeData) ->
|
||||
match d.value with
|
||||
| Some v ->
|
||||
setForm {form with Frames = v * 24}
|
||||
| None ->
|
||||
if d.displayValue.ToCharArray() |> Array.forall System.Char.IsDigit then
|
||||
setForm {form with Frames = int d.displayValue * 24}
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.text.caption1 [
|
||||
if framesExceedEnd then
|
||||
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||
|
||||
let endDate = form.StartTime.AddHours(form.Frames).ToShortDateString()
|
||||
text.text
|
||||
($"End Date: {endDate}, {form.Frames} frames"
|
||||
+
|
||||
if framesExceedEnd then
|
||||
" (Exceeds DataSet Bounds!)"
|
||||
else
|
||||
"")
|
||||
]
|
||||
Fui.checkbox [
|
||||
checkbox.label "Published"
|
||||
checkbox.checked' form.Published
|
||||
checkbox.onCheckedChange (fun c ->
|
||||
if not c then
|
||||
setForm {form with Published = c; Public = c}
|
||||
else
|
||||
setForm {form with Published = c}
|
||||
)
|
||||
]
|
||||
Fui.checkbox [
|
||||
checkbox.label "Public"
|
||||
checkbox.checked' form.Public
|
||||
checkbox.onCheckedChange (fun c -> setForm {form with Public = c})
|
||||
]
|
||||
match errMsg with
|
||||
| None -> Html.none
|
||||
| Some msg ->
|
||||
Fui.text [
|
||||
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||
text.text msg
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.dialogActions [
|
||||
dialogActions.position.end'
|
||||
dialogActions.children [
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.action.close
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
button.text "Cancel"
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.button [
|
||||
button.appearance.primary
|
||||
button.icon (Fui.icon.saveRegular [])
|
||||
button.text "Save changes"
|
||||
button.disabled <| (form = initForm)
|
||||
button.onClick (fun _ -> handleEditArchive ())
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
[<ReactComponent>]
|
||||
static member DeleteArchiveDialog(archive: Archmaester.Dto.ArchiveProps) =
|
||||
let isOpen, setIsOpen = React.useState false
|
||||
let userConfirmed, setUserConfirmed = React.useState false
|
||||
let errMsg, setErrMsg = React.useState None
|
||||
|
||||
let deleteArchive (id: System.Guid) =
|
||||
promise {
|
||||
try
|
||||
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
|
||||
return res
|
||||
with e ->
|
||||
Browser.Dom.console.error("Error deleting archive: %o", e)
|
||||
return Error "Error deleting archive"
|
||||
}
|
||||
|
||||
let handleDeleteArchive () =
|
||||
deleteArchive archive.archiveId
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok deleted ->
|
||||
if deleted then
|
||||
Browser.Dom.console.info("Archive deleted successfully")
|
||||
setIsOpen false
|
||||
Router.Router.navigateBack ()
|
||||
else
|
||||
setErrMsg (Some "Failed to delete archive")
|
||||
| Error err ->
|
||||
Browser.Dom.console.error("Error deleting archive: %s", err)
|
||||
setErrMsg (Some err)
|
||||
)
|
||||
|
||||
Fui.dialog [
|
||||
dialog.open' isOpen
|
||||
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
|
||||
setIsOpen d.``open``
|
||||
if d.``open`` then
|
||||
setUserConfirmed false
|
||||
setErrMsg None
|
||||
)
|
||||
dialog.children [
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.icon (Fui.icon.deleteRegular [])
|
||||
button.text "Delete"
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.dialogSurface [
|
||||
dialogSurface.classes []
|
||||
dialogSurface.children [
|
||||
Fui.dialogBody [
|
||||
dialogBody.classes []
|
||||
dialogBody.children [
|
||||
Fui.dialogTitle [
|
||||
dialogTitle.text (sprintf "Delete archive %s" archive.name)
|
||||
dialogTitle.action (
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.action.close
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.appearance.transparent
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.dialogContent [
|
||||
dialogContent.classes ["flex-column"; "gap-8"]
|
||||
dialogContent.children [
|
||||
Fui.text "Are you sure you want to delete the archive? This action cannot be reverted!"
|
||||
Fui.checkbox [
|
||||
checkbox.label "Yes, I am sure"
|
||||
checkbox.checked' userConfirmed
|
||||
checkbox.onCheckedChange setUserConfirmed
|
||||
]
|
||||
match errMsg with
|
||||
| None -> Html.none
|
||||
| Some msg ->
|
||||
Fui.text [
|
||||
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||
text.text msg
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.dialogActions [
|
||||
dialogActions.position.end'
|
||||
dialogActions.children [
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.action.close
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
button.text "Cancel"
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.button [
|
||||
button.appearance.primary
|
||||
button.icon (Fui.icon.deleteRegular [])
|
||||
button.text "Delete"
|
||||
button.disabled (not userConfirmed)
|
||||
button.onClick (fun _ -> handleDeleteArchive ())
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -97,7 +97,8 @@ type Archives =
|
||||
| Ok archives ->
|
||||
setArchives archives
|
||||
setError None
|
||||
| Error err -> setError (Some "Error fetching archives")
|
||||
| Error err ->
|
||||
setError (Some "Error fetching archives")
|
||||
|
||||
setLoading false
|
||||
)
|
||||
@@ -196,4 +197,4 @@ type Archives =
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -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 [||]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,8 +4,108 @@ open Browser
|
||||
open Fable.Core
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
open FS.FluentUI
|
||||
|
||||
type private NavTab =
|
||||
| Index
|
||||
| Archives
|
||||
| ModelAreas
|
||||
| Groups
|
||||
| Organizations
|
||||
|
||||
override this.ToString () =
|
||||
match this with
|
||||
| Index -> ""
|
||||
| Archives -> "archives"
|
||||
| ModelAreas -> "model-areas"
|
||||
| Groups -> "groups"
|
||||
| Organizations -> "organizations"
|
||||
|
||||
static member FromString str =
|
||||
match str with
|
||||
| "archives" -> Archives
|
||||
| "model-areas" -> ModelAreas
|
||||
| "groups" -> Groups
|
||||
| "organizations" -> Organizations
|
||||
| _ -> Index
|
||||
|
||||
member this.ToPath () =
|
||||
[this.ToString ()] |> Router.format
|
||||
|
||||
static member FromPath path =
|
||||
match path with
|
||||
| [] -> NavTab.Index
|
||||
| list -> list |> List.head |> NavTab.FromString
|
||||
|
||||
type Components =
|
||||
/// <summary>
|
||||
/// Navigation sidebar
|
||||
/// </summary>
|
||||
[<ReactComponent>]
|
||||
static member Sidebar currentUrl : ReactElement =
|
||||
let currentTab = NavTab.FromPath currentUrl
|
||||
|
||||
Html.div [
|
||||
prop.classes ["vh-100"; "w-256"; "br-solid"; "bcol-neutral-stroke-1"]
|
||||
prop.children [
|
||||
Fui.navDrawer [
|
||||
navDrawer.style [style.height (length.perc 100); style.width (length.perc 100)]
|
||||
navDrawer.selectedValue (currentTab.ToString())
|
||||
navDrawer.type'.inline'
|
||||
navDrawer.open' true
|
||||
navDrawer.children [
|
||||
Fui.navDrawerHeader [
|
||||
Fui.link [
|
||||
link.style [style.textDecoration.none]
|
||||
link.href (NavTab.Index.ToPath ())
|
||||
link.children [
|
||||
Fui.text.title1 [
|
||||
text.text "Codex"
|
||||
text.style [style.color Theme.tokens.colorBrandForeground1]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.navDivider []
|
||||
Fui.navDrawerBody [
|
||||
navDrawerBody.children [
|
||||
Fui.navItem [
|
||||
navItem.icon (Fui.bundleIcon bundleIcons.home [icon.size.``20``])
|
||||
navItem.href (NavTab.Index.ToPath ())
|
||||
navItem.value (NavTab.Index.ToString ())
|
||||
navItem.children [Html.text "Index"]
|
||||
]
|
||||
Fui.navItem [
|
||||
navItem.icon (Fui.bundleIcon bundleIcons.archive <| [icon.size.``20``])
|
||||
navItem.href (NavTab.Archives.ToPath ())
|
||||
navItem.value (NavTab.Archives.ToString ())
|
||||
navItem.children [Html.text "Archives"]
|
||||
]
|
||||
Fui.navItem [
|
||||
navItem.icon (Fui.bundleIcon bundleIcons.map <| [icon.size.``20``])
|
||||
navItem.href (NavTab.ModelAreas.ToPath ())
|
||||
navItem.value (NavTab.ModelAreas.ToString ())
|
||||
navItem.children [Html.text "Model Areas"]
|
||||
]
|
||||
Fui.navItem [
|
||||
navItem.icon (Fui.bundleIcon bundleIcons.people <| [icon.size.``20``])
|
||||
navItem.href (NavTab.Groups.ToPath ())
|
||||
navItem.value (NavTab.Groups.ToString ())
|
||||
navItem.children [Html.text "Groups"]
|
||||
]
|
||||
Fui.navItem [
|
||||
navItem.icon (Fui.bundleIcon bundleIcons.organization <| [icon.size.``20``])
|
||||
navItem.href (NavTab.Organizations.ToPath ())
|
||||
navItem.value (NavTab.Organizations.ToString ())
|
||||
navItem.children [Html.text "Organizations"]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
/// <summary>
|
||||
/// A React component that uses Feliz.Router to determine what to show based on the current URL
|
||||
/// </summary>
|
||||
@@ -39,19 +139,30 @@ type Components =
|
||||
elif not authed then
|
||||
Html.h1 "Redirecting to sign-in..."
|
||||
else
|
||||
match currentUrl with
|
||||
| [ ] -> Index.View()
|
||||
| [ "archives" ] -> Archives.View ()
|
||||
| [ "archives"; archive ] -> Archive.View (System.Guid archive)
|
||||
| [ "model-areas" ] -> ModelAreas.List ()
|
||||
| [ "model-areas"; id ] -> ModelArea.View (System.Guid id)
|
||||
| [ "groups" ] -> Groups.View ()
|
||||
| [ "groups"; group ] -> Group.View group
|
||||
| [ "groups"; group; "archives"; id ] -> GroupArchive.View group (System.Guid id)
|
||||
| [ "groups"; group; "users"; user ] -> User.View user
|
||||
| [ "users"; user ] -> User.View user
|
||||
| [ "organizations" ] -> Organizations.List ()
|
||||
| [ "organizations"; org ] -> Organization.View org
|
||||
| otherwise -> Html.h1 "Not found"
|
||||
Html.div [
|
||||
prop.classes ["flex-row"; "gap-16"]
|
||||
prop.children [
|
||||
Components.Sidebar currentUrl
|
||||
Html.div [
|
||||
prop.classes ["grow"]
|
||||
prop.children [
|
||||
match currentUrl with
|
||||
| [ ] -> Index.View()
|
||||
| [ "archives" ] -> Archives.View ()
|
||||
| [ "archives"; Route.Guid archive ] -> Archive.View archive
|
||||
| [ "model-areas" ] -> ModelAreas.List ()
|
||||
| [ "model-areas"; Route.Guid id ] -> ModelArea.View id
|
||||
| [ "groups" ] -> Groups.View ()
|
||||
| [ "groups"; group ] -> Group.View group
|
||||
| [ "groups"; group; "archives"; Route.Guid id ] -> GroupArchive.View group id
|
||||
| [ "groups"; group; "users"; user ] -> GroupUser.View group user
|
||||
| [ "users"; user ] -> User.View user
|
||||
| [ "organizations" ] -> Organizations.List ()
|
||||
| [ "organizations"; org ] -> Organization.View org
|
||||
| otherwise -> Html.h1 "Not found"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -3,7 +3,6 @@ namespace Oceanbox.Codex
|
||||
module Group =
|
||||
open Browser
|
||||
open Fable.Core
|
||||
open FsToolkit.ErrorHandling
|
||||
|
||||
open Oceanbox.Codex.Types
|
||||
|
||||
@@ -17,59 +16,9 @@ module Group =
|
||||
return Error (sprintf "Error fetching users for group %s" group)
|
||||
}
|
||||
|
||||
let private fetchArchmaesterArchives (group: string) : JS.Promise<Result<Archmaester.Dto.ArchiveProps array, string>> =
|
||||
promise {
|
||||
let filter : Archmaester.Dto.ArchiveFilter = {
|
||||
id = None
|
||||
searchTerm = None
|
||||
archiveType = None
|
||||
owner = None
|
||||
user = None
|
||||
groups = Some [| group |]
|
||||
}
|
||||
let! result = Remoting.adminApi.getArchives 0 -1 filter |> Async.StartAsPromise
|
||||
return result
|
||||
}
|
||||
|
||||
module private Elmish =
|
||||
open Elmish
|
||||
|
||||
let fetchArchiveProps (group: string) : JS.Promise<Types.Archive array> =
|
||||
promise {
|
||||
let fgaUser = sprintf "group:%s#member" (Groups.Utils.canonicalizeName group)
|
||||
let! archivesRes = fetchArchmaesterArchives group
|
||||
let! viewsRes =
|
||||
OpenFGA.fetchObjects(fgaUser, "view", "archive", context = {| time = System.DateTime.Now |})
|
||||
let! execsRes =
|
||||
OpenFGA.fetchObjects(fgaUser, "exec", "archive", context = {| task = "*"; usage = -1; time = System.DateTime.Now |})
|
||||
|
||||
let res =
|
||||
result {
|
||||
// TODO: Create specific exception
|
||||
let! props = archivesRes |> Result.mapError System.Exception
|
||||
let! views = viewsRes |> Result.mapError System.Exception
|
||||
let! execs = execsRes |> Result.mapError System.Exception
|
||||
let viewArchiveIds = views |> Array.choose Archives.Utils.extractFgaArchiveId
|
||||
let execArchiveIds = execs |> Array.choose Archives.Utils.extractFgaArchiveId
|
||||
let archives =
|
||||
props
|
||||
|> Array.map (fun prop -> {
|
||||
Props = prop
|
||||
CanView = viewArchiveIds |> Array.contains prop.archiveId
|
||||
CanExec = execArchiveIds |> Array.contains prop.archiveId
|
||||
})
|
||||
|
||||
return archives
|
||||
}
|
||||
|
||||
match res with
|
||||
| Ok archives ->
|
||||
return archives
|
||||
| Error ex ->
|
||||
raise ex
|
||||
return [||]
|
||||
}
|
||||
|
||||
type Msg =
|
||||
| FetchArchives of string
|
||||
| SetArchiveAdding of bool
|
||||
@@ -97,7 +46,7 @@ module Group =
|
||||
let update msg model =
|
||||
match msg with
|
||||
| FetchArchives group ->
|
||||
model, Cmd.OfPromise.either fetchArchiveProps group SetArchives HandleExn
|
||||
model, Cmd.OfPromise.either Archives.Utils.fetchGroupArchiveProps group SetArchives HandleExn
|
||||
| HandleExn ex ->
|
||||
let msg =
|
||||
match ex with
|
||||
@@ -470,4 +419,4 @@ module Group =
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -1,10 +1,40 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
type private Permission = {
|
||||
Tuple: Remoting.Tuple
|
||||
Relation: Remoting.ArchiveRelation
|
||||
}
|
||||
|
||||
module GroupArchive =
|
||||
open Browser
|
||||
open Fable.Core
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
open FS.FluentUI
|
||||
|
||||
let private postPermissions (group: string) (archiveId: System.Guid) (permissions: Remoting.ArchiveRelation array) =
|
||||
promise {
|
||||
console.debug("Posting new relations: %o", permissions)
|
||||
let req : Remoting.AddGroupPermissionsRequest = {
|
||||
Group = Groups.Utils.canonicalizeName group
|
||||
ArchiveId = archiveId
|
||||
Permissions = permissions
|
||||
}
|
||||
let! res = Remoting.adminApi.addGroupPermissions req |> Async.StartAsPromise
|
||||
return res
|
||||
}
|
||||
|
||||
let private putPermissions (group: string) (archiveId: System.Guid) (permissions: Remoting.ArchiveRelation array) =
|
||||
promise {
|
||||
console.debug("Updating existing relations: %o", permissions)
|
||||
let req : Remoting.AddGroupPermissionsRequest = {
|
||||
Group = Groups.Utils.canonicalizeName group
|
||||
ArchiveId = archiveId
|
||||
Permissions = permissions
|
||||
}
|
||||
let! res = Remoting.adminApi.updateGroupPermissions req |> Async.StartAsPromise
|
||||
return res
|
||||
}
|
||||
|
||||
[<ReactComponent>]
|
||||
let private DeleteRelationButton onDelete (tuple: Remoting.Tuple) =
|
||||
@@ -20,74 +50,137 @@ module GroupArchive =
|
||||
else
|
||||
// TODO: Should probably just return unit and error if not deleted
|
||||
console.warn ("[Group] Tuple was not deleted: %o", tuple)
|
||||
| Error err -> console.error ("[Group] Error deleting tuple: %s\n%o", err, tuple)
|
||||
| Error err ->
|
||||
console.error ("[Group] Error deleting tuple: %s\n%o", err, tuple)
|
||||
)
|
||||
|
||||
Html.button [ prop.onClick handleDelete; prop.text "Delete" ]
|
||||
Fui.button [
|
||||
button.onClick handleDelete
|
||||
button.icon (Fui.icon.deleteRegular [])
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private ViewTerm group archiveId (onDelete: Remoting.Tuple -> unit) (viewTerm: Remoting.ViewTerm) =
|
||||
let tuple =
|
||||
Remoting.Tuple.delete (
|
||||
user = Groups.Utils.fgaMember group,
|
||||
relation = "view",
|
||||
object = sprintf "archive:%O" archiveId
|
||||
)
|
||||
|
||||
let private PermissionCard (title: string) onDelete (tuple: Remoting.Tuple) (children: ReactElement array) =
|
||||
Html.div [
|
||||
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
||||
prop.style [ style.flexBasis (length.px 320) ]
|
||||
prop.style [ style.flexBasis (length.px 384) ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center" ]
|
||||
prop.children [
|
||||
Html.div [ prop.classes [ "grow" ]; prop.children [ Html.b "View Term" ] ]
|
||||
Html.div [
|
||||
prop.classes [ "grow" ]
|
||||
prop.children [
|
||||
Html.b [
|
||||
prop.style [
|
||||
style.fontSize(length.px 16)
|
||||
]
|
||||
prop.text title
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
DeleteRelationButton onDelete tuple
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "ml-16" ]
|
||||
prop.children [
|
||||
Html.div (sprintf "Start time: %s" (Intl.shortDateTime viewTerm.StartTime))
|
||||
Html.div (sprintf "End time: %s" (Intl.shortDateTime viewTerm.EndTime))
|
||||
]
|
||||
prop.children children
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private ExecTicket (group: string) (archiveId: System.Guid) onDelete (ticket: Remoting.ExecTicket) =
|
||||
let tuple =
|
||||
Remoting.Tuple.delete (
|
||||
user = Groups.Utils.fgaMember group,
|
||||
relation = "exec",
|
||||
object = sprintf "archive:%O" archiveId
|
||||
let private ViewTerm
|
||||
key
|
||||
(onUpdate: Permission -> unit)
|
||||
(permission: Permission)
|
||||
(viewTerm: Remoting.ViewTerm)
|
||||
=
|
||||
let updateCond (newCond: Remoting.ViewTerm) =
|
||||
let updatedCond =
|
||||
permission.Tuple.Condition
|
||||
|> Option.map (fun cond ->
|
||||
{ cond with Context = JS.JSON.stringify newCond.JsonObj }
|
||||
)
|
||||
onUpdate {
|
||||
permission with
|
||||
Tuple.Condition = updatedCond
|
||||
Relation = Remoting.ArchiveRelation.ViewTerm newCond
|
||||
}
|
||||
|
||||
let handleStartChange =
|
||||
React.useCallback (
|
||||
(fun (newStartOpt: System.DateTime option) ->
|
||||
match newStartOpt with
|
||||
| Some newStart ->
|
||||
let updated = { viewTerm with StartTime = newStart }
|
||||
updateCond updated
|
||||
| None ->
|
||||
console.error("Got no date from date picker")
|
||||
),
|
||||
[| permission |]
|
||||
)
|
||||
let handleEndChange =
|
||||
React.useCallback (
|
||||
(fun (newEndOpt: System.DateTime option) ->
|
||||
match newEndOpt with
|
||||
| Some newEnd ->
|
||||
let updated = { viewTerm with EndTime = newEnd }
|
||||
updateCond updated
|
||||
| None ->
|
||||
console.error("Got no date from date picker")
|
||||
),
|
||||
[| permission |]
|
||||
)
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
||||
prop.style [ style.flexBasis (length.px 320) ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center" ]
|
||||
prop.children [
|
||||
Html.div [ prop.classes [ "grow" ]; prop.children [ Html.b "Exec Ticket" ] ]
|
||||
Fui.table [
|
||||
table.size.medium
|
||||
table.children [
|
||||
Fui.tableBody [
|
||||
tableBody.children [
|
||||
Fui.tableRow [
|
||||
tableRow.key "view-term-start-time"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "Start time"
|
||||
]
|
||||
|
||||
DeleteRelationButton onDelete tuple
|
||||
]
|
||||
]
|
||||
Html.div [
|
||||
prop.classes [ "ml-16" ]
|
||||
prop.children [
|
||||
Html.div (sprintf "Start time: %s" (Intl.shortDateTime ticket.StartTime))
|
||||
Html.div (sprintf "End time: %s" (Intl.shortDateTime ticket.EndTime))
|
||||
Html.div (sprintf "Quota: %.1f" ticket.Quota)
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.span "Tasks:"
|
||||
Html.ul [
|
||||
prop.children (ticket.Tasks |> Array.map (fun task -> Html.li task))
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.datePicker [
|
||||
datePicker.size.small
|
||||
datePicker.onSelectDate handleStartChange
|
||||
datePicker.value (Some viewTerm.StartTime)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.tableRow [
|
||||
tableRow.key "view-term-end-time"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "End time"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.datePicker [
|
||||
datePicker.size.small
|
||||
datePicker.onSelectDate handleEndChange
|
||||
datePicker.value (Some viewTerm.EndTime)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -97,14 +190,278 @@ module GroupArchive =
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private PermissionForm (permissions: OpenFGA.Types.ArchiveRelation array) (group: string) =
|
||||
let private ExecTicket
|
||||
(key: string)
|
||||
(onUpdate: Permission -> unit)
|
||||
(permission: Permission)
|
||||
(ticket: Remoting.ExecTicket)
|
||||
=
|
||||
let updateCond (newCond: Remoting.ExecTicket) =
|
||||
let updatedCond =
|
||||
permission.Tuple.Condition
|
||||
|> Option.map (fun cond ->
|
||||
{ cond with Context = JS.JSON.stringify newCond.JsonObj }
|
||||
)
|
||||
onUpdate {
|
||||
permission with
|
||||
Tuple.Condition = updatedCond
|
||||
Relation = Remoting.ArchiveRelation.ExecTicket newCond
|
||||
}
|
||||
|
||||
let handleStartChange =
|
||||
React.useCallback (
|
||||
(fun (newStartOpt: System.DateTime option) ->
|
||||
match newStartOpt with
|
||||
| Some newStart ->
|
||||
let updated = { ticket with StartTime = newStart }
|
||||
updateCond updated
|
||||
| None ->
|
||||
console.error("Got no date from date picker")
|
||||
),
|
||||
[| ticket |]
|
||||
)
|
||||
let handleEndChange =
|
||||
React.useCallback (
|
||||
(fun (newEndOpt: System.DateTime option) ->
|
||||
match newEndOpt with
|
||||
| Some newEnd ->
|
||||
let updated = { ticket with EndTime = newEnd }
|
||||
updateCond updated
|
||||
| None ->
|
||||
console.error("Got no date from date picker")
|
||||
),
|
||||
[| ticket |]
|
||||
)
|
||||
|
||||
Fui.table [
|
||||
table.size.medium
|
||||
table.children [
|
||||
Fui.tableBody [
|
||||
tableBody.children [
|
||||
Fui.tableRow [
|
||||
tableRow.key "exec-ticket-start-time"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "Start time"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.datePicker [
|
||||
datePicker.size.small
|
||||
datePicker.onSelectDate handleStartChange
|
||||
datePicker.value (Some ticket.StartTime)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.tableRow [
|
||||
tableRow.key "exec-ticket-end-time"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "End time"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.datePicker [
|
||||
datePicker.size.small
|
||||
datePicker.onSelectDate handleEndChange
|
||||
datePicker.value (Some ticket.EndTime)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.tableRow [
|
||||
tableRow.key "exec-ticket-quota"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "Quota"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.input [
|
||||
input.size.small
|
||||
input.type'.number
|
||||
input.value ticket.Quota
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.tableRow [
|
||||
tableRow.key "exec-ticket-tasks"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "Tasks"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.text (
|
||||
ticket.Tasks
|
||||
|> String.concat ", "
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private PermissionCreateCard group archiveId (onCreate: Permission -> unit) (defaultPermission: Permission) =
|
||||
let isLoading, setLoading = React.useState false
|
||||
let permission, setPermission = React.useState defaultPermission
|
||||
|
||||
let handleCreate (ev: Types.Event) =
|
||||
setLoading true
|
||||
postPermissions group archiveId [| permission.Relation |]
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
console.info("Success.")
|
||||
onCreate permission
|
||||
| Error msg ->
|
||||
console.error("Error adding permissions %s.", msg)
|
||||
|
||||
setLoading false
|
||||
)
|
||||
|
||||
let handleUpdateRelation (permission: Permission) =
|
||||
setPermission permission
|
||||
|
||||
console.debug("Permission: %o", permission)
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
||||
prop.style [ style.flexBasis (length.px 384) ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center" ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "grow" ]
|
||||
prop.children [
|
||||
Html.b [
|
||||
prop.style [
|
||||
style.fontSize(length.px 16)
|
||||
]
|
||||
prop.text (
|
||||
match permission.Relation with
|
||||
| Remoting.ArchiveRelation.ViewTerm _ -> "View Term"
|
||||
| Remoting.ArchiveRelation.ExecTicket _ -> "Exec Ticket"
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.button [
|
||||
button.onClick handleCreate
|
||||
button.icon (
|
||||
if isLoading then
|
||||
Fui.spinner [ spinner.size.tiny ]
|
||||
else
|
||||
Fui.icon.addRegular []
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [
|
||||
prop.children [|
|
||||
match permission.Relation with
|
||||
| Remoting.ArchiveRelation.ViewTerm term ->
|
||||
ViewTerm
|
||||
"create-permission-view-term"
|
||||
handleUpdateRelation
|
||||
permission
|
||||
term
|
||||
| Remoting.ArchiveRelation.ExecTicket ticket ->
|
||||
ExecTicket
|
||||
"create-permission-exec-ticket"
|
||||
handleUpdateRelation
|
||||
permission
|
||||
ticket
|
||||
|]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
let private allRelations = [|
|
||||
Remoting.ArchiveRelation.ViewTerm Remoting.ViewTerm.empty
|
||||
Remoting.ArchiveRelation.ExecTicket Remoting.ExecTicket.empty
|
||||
|]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private PermissionForm
|
||||
(group: string)
|
||||
(archiveId: System.Guid)
|
||||
(onAdd: Permission -> unit)
|
||||
(permissions: Permission array)
|
||||
=
|
||||
let adding, setAdding = React.useState false
|
||||
let loading, setLoading = React.useState false
|
||||
let success, setSuccess = React.useState false
|
||||
|
||||
let handleAddClick (ev: Types.Event) = setAdding true
|
||||
let handleCancelClick (ev: Types.Event) = setAdding false
|
||||
// Create a list of permissions missing from the archive
|
||||
let availablePermissions : Permission array =
|
||||
allRelations
|
||||
|> Array.choose (fun relation ->
|
||||
let exists =
|
||||
permissions
|
||||
|> Array.exists (fun permission ->
|
||||
let name = Remoting.ArchiveRelation.ConditionName relation
|
||||
permission.Tuple.Condition
|
||||
|> Option.map (fun cond -> cond.Name = name)
|
||||
|> Option.defaultValue false
|
||||
)
|
||||
|
||||
let hasViewTerm = permissions |> Array.exists _.IsViewTerm
|
||||
let hasExecTicket = permissions |> Array.exists _.IsExecTicket
|
||||
if exists then
|
||||
None
|
||||
else
|
||||
let user = Groups.Utils.fgaMember group
|
||||
let object = sprintf "archive:%O" archiveId
|
||||
Some {
|
||||
Tuple = OpenFGA.Types.ArchiveRelation.toTuple user object relation
|
||||
Relation = relation
|
||||
}
|
||||
)
|
||||
|
||||
let handleAddClick (ev: Types.Event) =
|
||||
setAdding true
|
||||
|
||||
let handleCancelClick (ev: Types.Event) =
|
||||
setAdding false
|
||||
|
||||
let handlePermissionAdd (newPermission: Permission) =
|
||||
console.debug("Added new permission: %o", newPermission)
|
||||
onAdd newPermission
|
||||
|
||||
React.fragment [
|
||||
Html.div [
|
||||
@@ -113,52 +470,39 @@ module GroupArchive =
|
||||
"gap-8"
|
||||
]
|
||||
prop.children [
|
||||
if adding then
|
||||
Html.button [ prop.onClick handleAddClick; prop.text "Save" ]
|
||||
|
||||
Html.button [
|
||||
prop.onClick handleCancelClick
|
||||
prop.text "Cancel"
|
||||
]
|
||||
if loading then
|
||||
Html.p "Loading ..."
|
||||
else
|
||||
Html.button [ prop.onClick handleAddClick; prop.text "Add" ]
|
||||
if adding then
|
||||
Html.button [
|
||||
prop.onClick handleCancelClick
|
||||
prop.text "Cancel"
|
||||
]
|
||||
else
|
||||
Html.button [
|
||||
prop.disabled (Array.isEmpty availablePermissions)
|
||||
prop.onClick handleAddClick
|
||||
prop.text "Add"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
if not loading && success then
|
||||
Html.p "Success."
|
||||
|
||||
if adding then
|
||||
Html.div [
|
||||
prop.id "group-archive-exec-form"
|
||||
prop.classes [
|
||||
"flex-row"
|
||||
"flex-row-start"
|
||||
"gap-32"
|
||||
]
|
||||
prop.children [
|
||||
if not hasViewTerm then
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.b "View"
|
||||
Groups.ViewForm (Remoting.ViewTerm.empty, ignore)
|
||||
]
|
||||
]
|
||||
|
||||
if not hasExecTicket then
|
||||
Html.div [
|
||||
prop.classes [
|
||||
"flex-column"
|
||||
"gap-8"
|
||||
"shadow"
|
||||
"brad-8"
|
||||
"m-8"
|
||||
"p-16"
|
||||
]
|
||||
prop.style [
|
||||
style.flexBasis (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
Html.b "Exec"
|
||||
Groups.ExecForm (Remoting.ExecTicket.empty, ignore)
|
||||
]
|
||||
]
|
||||
availablePermissions
|
||||
|> Array.map (fun permission ->
|
||||
PermissionCreateCard group archiveId handlePermissionAdd permission
|
||||
)
|
||||
|> unbox
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -167,22 +511,77 @@ module GroupArchive =
|
||||
let View (group: string) (archiveId: System.Guid) =
|
||||
let loading, setLoading = React.useState true
|
||||
let error, setError = React.useState<string option> None
|
||||
let archiveOpt, setArchive =
|
||||
React.useState<Archmaester.Dto.ArchiveProps option> None
|
||||
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
|
||||
|
||||
let fgaUser = Groups.Utils.fgaMember group
|
||||
let tuples =
|
||||
OpenFGA.useReadTuples (fgaUser, object = sprintf "archive:%O" archiveId)
|
||||
let tuples = OpenFGA.useReadTuples (fgaUser, object = sprintf "archive:%O" archiveId)
|
||||
|
||||
let relations: OpenFGA.Types.ArchiveRelation array =
|
||||
let permissions: Permission array =
|
||||
tuples.Tuples
|
||||
|> Array.choose (fun tuple -> tuple.Condition |> Option.bind OpenFGA.Types.ArchiveRelation.tryOfCondition)
|
||||
|> Array.choose (fun tuple ->
|
||||
tuple.Condition
|
||||
|> Option.bind (fun cond ->
|
||||
cond
|
||||
|> OpenFGA.Types.ArchiveRelation.tryOfCondition
|
||||
|> Option.map (fun rel -> {
|
||||
Tuple = tuple
|
||||
Relation = rel
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
let handlePermissionDelete (tuple: Remoting.Tuple) =
|
||||
console.debug("Deleting %o from %o", tuple, tuples)
|
||||
tuples.Tuples
|
||||
|> Array.filter (fun existing -> existing.Relation <> tuple.Relation)
|
||||
|> tuples.SetTuples
|
||||
|
||||
let handlePermissionUpdate (updated: Permission) =
|
||||
console.debug("Updated permission tuple %o", updated)
|
||||
putPermissions group archiveId [| updated.Relation |]
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
tuples.Tuples
|
||||
|> Array.map (fun existing ->
|
||||
let equal =
|
||||
existing.Object = updated.Tuple.Object
|
||||
&& existing.Relation = updated.Tuple.Relation
|
||||
&& existing.User = updated.Tuple.User
|
||||
|
||||
if equal then
|
||||
updated.Tuple
|
||||
else
|
||||
existing
|
||||
)
|
||||
|> tuples.SetTuples
|
||||
| Error msg ->
|
||||
setError (Some msg)
|
||||
)
|
||||
|
||||
let handleUpdateRelations =
|
||||
React.useCallback (
|
||||
(fun (updated: Remoting.Tuple array) ->
|
||||
console.debug("Relations updated: %o with current: %o", updated, tuples.Tuples)
|
||||
|
||||
tuples.SetTuples updated
|
||||
),
|
||||
[| box tuples |]
|
||||
)
|
||||
|
||||
let handleAddPermission =
|
||||
React.useCallback (
|
||||
(fun (newPermission: Permission) ->
|
||||
console.debug("New relation added: %o with current: %o", newPermission, tuples.Tuples)
|
||||
|
||||
newPermission.Tuple
|
||||
|> Array.singleton
|
||||
|> Array.append tuples.Tuples
|
||||
|> tuples.SetTuples
|
||||
),
|
||||
[| box tuples |]
|
||||
)
|
||||
|
||||
React.useEffect (
|
||||
(fun () ->
|
||||
setLoading true
|
||||
@@ -210,7 +609,10 @@ module GroupArchive =
|
||||
Html.h1 [
|
||||
prop.children [
|
||||
Html.text "Group "
|
||||
Html.a [ prop.href (Router.format ("groups", group)); prop.text group ]
|
||||
Html.a [
|
||||
prop.href (Router.format ("groups", group))
|
||||
prop.text group
|
||||
]
|
||||
Html.text " / "
|
||||
Html.text "Archive "
|
||||
Html.a [
|
||||
@@ -220,7 +622,14 @@ module GroupArchive =
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [ prop.children [ Html.button [ prop.text "Remove" ] ] ]
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.button [
|
||||
prop.disabled true
|
||||
prop.text "Remove (TODO)"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Archives.InfoSection archive
|
||||
|
||||
@@ -228,9 +637,13 @@ module GroupArchive =
|
||||
prop.children [
|
||||
Html.h2 "Permissions"
|
||||
|
||||
if not tuples.Loading then
|
||||
if tuples.Loading then
|
||||
Html.p "Loading ..."
|
||||
else
|
||||
Html.div [
|
||||
prop.children [ PermissionForm relations group ]
|
||||
prop.children [
|
||||
PermissionForm group archive.archiveId handleAddPermission permissions
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [
|
||||
@@ -240,23 +653,28 @@ module GroupArchive =
|
||||
]
|
||||
]
|
||||
|
||||
if Array.isEmpty relations then
|
||||
if Array.isEmpty permissions then
|
||||
Html.p "No permissions"
|
||||
else
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "gap-32" ]
|
||||
prop.classes [ "flex-row-start"; "gap-32" ]
|
||||
prop.children (
|
||||
relations
|
||||
|> Array.map (
|
||||
function
|
||||
| OpenFGA.Types.ArchiveRelation.ViewTerm term ->
|
||||
ViewTerm group archive.archiveId handlePermissionDelete term
|
||||
| OpenFGA.Types.ArchiveRelation.ExecTicket ticket ->
|
||||
ExecTicket group archive.archiveId handlePermissionDelete ticket
|
||||
permissions
|
||||
|> Array.map (fun permission ->
|
||||
match permission.Relation with
|
||||
| Remoting.ArchiveRelation.ViewTerm term ->
|
||||
PermissionCard "View Term" handlePermissionDelete permission.Tuple [|
|
||||
ViewTerm "view-term-table" handlePermissionUpdate permission term
|
||||
|]
|
||||
| Remoting.ArchiveRelation.ExecTicket ticket ->
|
||||
PermissionCard "Exec Ticket" handlePermissionDelete permission.Tuple [|
|
||||
ExecTicket "exec-ticket-table" handlePermissionUpdate permission ticket
|
||||
|]
|
||||
)
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
| None -> Html.h1 (sprintf "Group %s / Archive %O not found" group archiveId)
|
||||
]
|
||||
| None ->
|
||||
Html.h1 (sprintf "Group %s / Archive %O not found" group archiveId)
|
||||
]
|
||||
204
src/Codex/src/Client/GroupUser.fs
Normal file
204
src/Codex/src/Client/GroupUser.fs
Normal 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
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -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."
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@@ -1,45 +1,569 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
open Fable.Core
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
open FS.FluentUI
|
||||
open Browser
|
||||
|
||||
module Index =
|
||||
|
||||
[<Literal>]
|
||||
let private noGroup = "Select Group..."
|
||||
|
||||
type private User = { Name: string; Group: string }
|
||||
|
||||
// TODO: Elmish?
|
||||
[<ReactComponent>]
|
||||
let View () =
|
||||
Html.main [
|
||||
Html.h1 "Codex"
|
||||
Html.h2 "Index"
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.ul [
|
||||
Html.li [
|
||||
Html.a [
|
||||
prop.href (Router.format "archives")
|
||||
prop.text "archives"
|
||||
let private UserAddForm onAdd =
|
||||
let email, setEmail = React.useState ""
|
||||
let selectedGroup, setSelectedGroup = React.useState noGroup
|
||||
let recentlyAddedUsers, setRecentlyAddedUsers = React.useState<User array> [||]
|
||||
let groups = Groups.useGroups ()
|
||||
// TODO: Fui Toast?
|
||||
let errMsg, setErrMsg = React.useState None
|
||||
|
||||
let handleAddUser () =
|
||||
let newUser = {
|
||||
Name = email
|
||||
Group = Groups.Utils.canonicalizeName selectedGroup
|
||||
}
|
||||
Remoting.adminApi.addUsers { Group = newUser.Group; Users = [| newUser.Name |] }
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
console.info("Added user %s to selectedGroup %s", newUser.Name, newUser.Group)
|
||||
onAdd (Some (email, selectedGroup))
|
||||
recentlyAddedUsers |> Array.append [|newUser|] |> setRecentlyAddedUsers
|
||||
| Error msg ->
|
||||
setErrMsg (Some msg)
|
||||
console.error("Error adding user %s to group %s: %s", newUser.Name, newUser.Group, msg)
|
||||
)
|
||||
|
||||
Fui.card [
|
||||
card.classes [ "flex-column"; "gap-16"; "w-512"; "minh-512"]
|
||||
card.children [
|
||||
React.fragment [
|
||||
Fui.cardHeader [
|
||||
cardHeader.image (Fui.icon.personAddRegular [icon.size.``32``])
|
||||
cardHeader.header (Fui.text.title3 [ text.weight.semibold; text.text "Add new users"])
|
||||
]
|
||||
if groups.Loading then
|
||||
Html.div [
|
||||
prop.classes ["flex-row"]
|
||||
prop.children [
|
||||
Fui.spinner []
|
||||
Fui.text.body1 "Loading groups..."
|
||||
]
|
||||
]
|
||||
else
|
||||
match groups.Error with
|
||||
| Some _error ->
|
||||
Html.div [
|
||||
prop.classes ["flex-row"]
|
||||
prop.children [
|
||||
Fui.icon.warningRegular []
|
||||
Fui.text.body1 "Failed to load groups"
|
||||
]
|
||||
]
|
||||
| None ->
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Group"
|
||||
]
|
||||
)
|
||||
// field.style [style.maxWidth 256]
|
||||
field.children (
|
||||
Fui.dropdown [
|
||||
dropdown.value selectedGroup
|
||||
dropdown.selectedOptions [|selectedGroup|]
|
||||
dropdown.onOptionSelect (fun (d: OptionOnSelectData) ->
|
||||
d.optionText
|
||||
|> Option.iter setSelectedGroup
|
||||
)
|
||||
dropdown.children [
|
||||
yield!
|
||||
groups.Groups
|
||||
|> Array.sort
|
||||
|> Array.map (fun group ->
|
||||
Fui.option [
|
||||
option.key group
|
||||
option.text group
|
||||
option.children (Fui.text group)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
Html.li [
|
||||
Html.a [
|
||||
prop.href (Router.format "model-areas")
|
||||
prop.text "model areas"
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Email"
|
||||
]
|
||||
)
|
||||
// field.style [style.maxWidth 256]
|
||||
field.children (
|
||||
Fui.input [
|
||||
input.contentBefore (Fui.icon.mailRegular [])
|
||||
input.type'.email
|
||||
input.onChange (fun (v: ValueProp<string>) -> setEmail v.value)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
match errMsg with
|
||||
| None -> Html.none
|
||||
| Some msg ->
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "gap-8"]
|
||||
prop.children [
|
||||
Fui.text [
|
||||
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||
text.text msg
|
||||
]
|
||||
Fui.button [
|
||||
button.appearance.subtle
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
button.onClick (fun _ -> setErrMsg None)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Html.li [
|
||||
Html.a [
|
||||
prop.href (Router.format "groups")
|
||||
prop.text "groups"
|
||||
]
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "gap-8"; ]
|
||||
prop.children [
|
||||
Fui.button [
|
||||
button.onClick (fun _ -> handleAddUser ())
|
||||
button.text "Save"
|
||||
button.icon (Fui.icon.saveRegular [])
|
||||
button.appearance.primary
|
||||
button.disabled <| (email = "" || selectedGroup = noGroup)
|
||||
]
|
||||
|
||||
Html.li [
|
||||
Html.a [
|
||||
prop.href (Router.format "organizations")
|
||||
prop.text "organizations"
|
||||
]
|
||||
Fui.button [
|
||||
button.onClick (fun _ ->
|
||||
setEmail ""
|
||||
setSelectedGroup noGroup
|
||||
)
|
||||
button.text "Clear"
|
||||
button.icon (Fui.icon.arrowResetRegular [])
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
match recentlyAddedUsers with
|
||||
| [||] -> Html.none
|
||||
| users ->
|
||||
Html.div [
|
||||
prop.classes ["flex-column"; "gap-16"]
|
||||
prop.children [
|
||||
Fui.text.title3 "Recently added"
|
||||
Fui.table [
|
||||
Fui.tableHeader [
|
||||
Fui.tableRow [
|
||||
Fui.tableHeaderCell [
|
||||
tableHeaderCell.children [ Fui.text "Name"]
|
||||
]
|
||||
Fui.tableHeaderCell [
|
||||
tableHeaderCell.children [ Fui.text "Group"]
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.tableBody [
|
||||
yield!
|
||||
users |> Array.map (fun user ->
|
||||
Fui.tableRow [
|
||||
tableRow.key user.Name
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.link [
|
||||
link.href (Router.format ["users"; user.Name])
|
||||
link.text user.Name
|
||||
]
|
||||
]
|
||||
Fui.tableCell [
|
||||
Fui.link [
|
||||
link.href (Router.format ["groups"; user.Group])
|
||||
link.text user.Group
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
[<Literal>]
|
||||
let private noDataSet = "Select DataSet..."
|
||||
|
||||
[<ReactComponent>]
|
||||
let private ArchiveAddForm () =
|
||||
let dataSets, setDataSets = React.useState [||]
|
||||
let isLoading, setIsLoading = React.useState true
|
||||
let recentlyAddedArchives, setRecentlyAddedArchives = React.useState [||]
|
||||
let errMsg, setErrMsg = React.useState None
|
||||
|
||||
let form, setForm = React.useState (Remoting.AddArchiveRequest.empty ())
|
||||
|
||||
let fetchDataSets () : JS.Promise<Result<Remoting.DataSet array, string>> =
|
||||
promise {
|
||||
try
|
||||
console.debug("Fetching all dataSets")
|
||||
let! res = Remoting.adminApi.getDataSets () |> Async.StartAsPromise
|
||||
return res
|
||||
with ex ->
|
||||
console.error("Error fetching dataSets: %s", ex.Message)
|
||||
return Error "Error fetching dataSets"
|
||||
}
|
||||
|
||||
React.useEffectOnce (fun _ ->
|
||||
fetchDataSets ()
|
||||
|> Promise.iter (fun res ->
|
||||
setIsLoading false
|
||||
match res with
|
||||
| Ok r ->
|
||||
setDataSets r
|
||||
| Error e -> setErrMsg (Some e)
|
||||
)
|
||||
)
|
||||
|
||||
let handleAddArchive () =
|
||||
let utcTime = System.DateTime(form.StartTime.Year, form.StartTime.Month, form.StartTime.Day, 0, 0, 0, System.DateTimeKind.Utc)
|
||||
Remoting.adminApi.addArchive {form with StartTime = utcTime}
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok newArchive ->
|
||||
console.info("Added archive %s with id %s", newArchive.Name, string newArchive.Id)
|
||||
recentlyAddedArchives |> Array.append [|newArchive|] |> setRecentlyAddedArchives
|
||||
setForm (Remoting.AddArchiveRequest.empty())
|
||||
| Error msg ->
|
||||
console.error("Error adding archive %s: %s", form.Name, msg)
|
||||
setErrMsg (Some msg)
|
||||
)
|
||||
|
||||
let selectedDataSet =
|
||||
dataSets
|
||||
|> Array.tryFind (fun ds -> ds.Id = form.DataSetId)
|
||||
|
||||
let framesExceedEnd =
|
||||
selectedDataSet
|
||||
|> Option.map (fun ds ->
|
||||
form.StartTime.AddHours(form.Frames) > ds.EndTime
|
||||
)
|
||||
|> Option.defaultValue false
|
||||
|
||||
Fui.card [
|
||||
card.classes [ "flex-column"; "gap-16"; "w-512"; "minh-512"]
|
||||
card.children [
|
||||
React.fragment [
|
||||
Fui.cardHeader [
|
||||
cardHeader.image (Fui.icon.archiveRegular [icon.size.``32``])
|
||||
cardHeader.header (Fui.text.title3 [ text.weight.semibold; text.text "Add new archives"])
|
||||
]
|
||||
match dataSets with
|
||||
| [||] ->
|
||||
if isLoading then
|
||||
Html.div [
|
||||
prop.classes ["flex-row"]
|
||||
prop.children [
|
||||
Fui.spinner []
|
||||
Fui.text.body1 "Loading groups..."
|
||||
]
|
||||
]
|
||||
else
|
||||
Html.div [
|
||||
prop.classes ["flex-row"]
|
||||
prop.children [
|
||||
Fui.icon.warningRegular []
|
||||
Fui.text.body1 "Failed to load groups"
|
||||
]
|
||||
]
|
||||
| dataSets' ->
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "DataSet"
|
||||
]
|
||||
)
|
||||
// field.style [style.maxWidth 256]
|
||||
field.children (
|
||||
Fui.dropdown [
|
||||
let selectedPath = selectedDataSet |> Option.map _.BasePath |> Option.defaultValue noDataSet
|
||||
dropdown.value selectedPath
|
||||
dropdown.selectedOptions [|selectedPath|]
|
||||
dropdown.onOptionSelect (fun (d: OptionOnSelectData) ->
|
||||
d.optionText
|
||||
|> Option.bind (fun path -> dataSets |> Array.tryFind (fun ds -> ds.BasePath = path))
|
||||
|> Option.iter (fun ds ->
|
||||
setForm {form with DataSetId = ds.Id; StartTime = ds.StartTime}
|
||||
)
|
||||
)
|
||||
dropdown.children [
|
||||
yield!
|
||||
dataSets'
|
||||
|> Array.groupBy _.ModelAreaName
|
||||
|> Array.sortBy fst
|
||||
|> Array.map (fun (modelArea, dataSetsInArea) ->
|
||||
Fui.optionGroup [
|
||||
optionGroup.key modelArea
|
||||
optionGroup.label modelArea
|
||||
optionGroup.children [
|
||||
yield!
|
||||
dataSetsInArea
|
||||
|> Array.sort
|
||||
|> Array.map (fun dataSet ->
|
||||
Fui.option [
|
||||
option.key dataSet.Id
|
||||
option.text dataSet.BasePath
|
||||
option.children (Fui.text dataSet.BasePath)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Name"
|
||||
]
|
||||
)
|
||||
// field.style [style.maxWidth 256]
|
||||
field.children (
|
||||
Fui.input [
|
||||
input.value form.Name
|
||||
input.onChange (fun (v: ValueProp<string>) ->
|
||||
if v.value.Length <= 80 then
|
||||
setForm {form with Name = v.value}
|
||||
)
|
||||
]
|
||||
)
|
||||
field.hint $"{form.Name.Length}/80"
|
||||
]
|
||||
Html.div [
|
||||
prop.classes ["flex-row"; "gap-8"]
|
||||
prop.children [
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Start Date"
|
||||
]
|
||||
)
|
||||
|
||||
field.children (
|
||||
Fui.datePicker [
|
||||
datePicker.placeholder "Select a date..."
|
||||
datePicker.showWeekNumbers true
|
||||
match selectedDataSet with
|
||||
| None -> ()
|
||||
| Some ds ->
|
||||
datePicker.maxDate ds.EndTime
|
||||
datePicker.minDate ds.StartTime
|
||||
datePicker.formatDate (fun d -> d.ToShortDateString())
|
||||
datePicker.value (Some form.StartTime)
|
||||
datePicker.onSelectDate (fun d ->
|
||||
d |> Option.iter (fun d' ->
|
||||
setForm {form with StartTime = d'}
|
||||
console.debug("d': %s", d'.ToString())
|
||||
console.debug("d'.Kind: %s", d'.Kind)
|
||||
console.debug("utc: %s", d'.ToUniversalTime().ToString())
|
||||
console.debug("utc.Kind: %s", d'.ToUniversalTime().Kind)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Days"
|
||||
]
|
||||
)
|
||||
field.children (
|
||||
Fui.spinButton [
|
||||
spinButton.value (form.Frames / 24)
|
||||
spinButton.min 1
|
||||
spinButton.onChange (fun (d: SpinButtonOnChangeData) ->
|
||||
match d.value with
|
||||
| Some v ->
|
||||
setForm {form with Frames = v * 24}
|
||||
| None ->
|
||||
if d.displayValue.ToCharArray() |> Array.forall System.Char.IsDigit then
|
||||
setForm {form with Frames = int d.displayValue * 24}
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.text.caption1 [
|
||||
if framesExceedEnd then
|
||||
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||
|
||||
let endDate = form.StartTime.AddHours(form.Frames).ToShortDateString()
|
||||
text.text
|
||||
($"End Date: {endDate}, {form.Frames} frames"
|
||||
+
|
||||
if framesExceedEnd then
|
||||
" (Exceeds DataSet Bounds!)"
|
||||
else
|
||||
"")
|
||||
]
|
||||
Fui.checkbox [
|
||||
checkbox.label "Published"
|
||||
checkbox.checked' form.Published
|
||||
checkbox.onCheckedChange (fun c -> setForm {form with Published = c})
|
||||
]
|
||||
Fui.checkbox [
|
||||
checkbox.label "Public"
|
||||
checkbox.checked' form.Public
|
||||
checkbox.onCheckedChange (fun c -> setForm {form with Public = c})
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "gap-8"; ]
|
||||
prop.children [
|
||||
Fui.button [
|
||||
button.onClick (fun _ -> handleAddArchive ())
|
||||
button.text "Save"
|
||||
button.icon (Fui.icon.saveRegular [])
|
||||
button.appearance.primary
|
||||
button.disabled (
|
||||
selectedDataSet.IsNone
|
||||
|| form.Frames < 24
|
||||
|| form.Name |> System.String.IsNullOrWhiteSpace
|
||||
|| framesExceedEnd
|
||||
)
|
||||
]
|
||||
Fui.button [
|
||||
button.onClick (fun _ ->
|
||||
setForm (Remoting.AddArchiveRequest.empty ())
|
||||
)
|
||||
button.text "Clear"
|
||||
button.icon (Fui.icon.arrowResetRegular [])
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
match errMsg with
|
||||
| None -> Html.none
|
||||
| Some msg ->
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "gap-8"]
|
||||
prop.children [
|
||||
Fui.text [
|
||||
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||
text.text msg
|
||||
]
|
||||
Fui.button [
|
||||
button.appearance.subtle
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
button.onClick (fun _ -> setErrMsg None)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
match recentlyAddedArchives with
|
||||
| [||] -> Html.none
|
||||
| archives ->
|
||||
Html.div [
|
||||
prop.classes ["flex-column"; "gap-16"]
|
||||
prop.children [
|
||||
Fui.text.title3 "Recently added"
|
||||
Fui.table [
|
||||
Fui.tableHeader [
|
||||
Fui.tableRow [
|
||||
Fui.tableHeaderCell [
|
||||
tableHeaderCell.children [ Fui.text "Name"]
|
||||
]
|
||||
Fui.tableHeaderCell [
|
||||
tableHeaderCell.children [ Fui.text "Start Time"]
|
||||
]
|
||||
Fui.tableHeaderCell [
|
||||
tableHeaderCell.children [ Fui.text "Frames"]
|
||||
]
|
||||
Fui.tableHeaderCell [
|
||||
tableHeaderCell.children [ Fui.text "Published"]
|
||||
]
|
||||
Fui.tableHeaderCell [
|
||||
tableHeaderCell.children [ Fui.text "Public"]
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.tableBody [
|
||||
yield!
|
||||
archives |> Array.map (fun archive ->
|
||||
Fui.tableRow [
|
||||
tableRow.key archive.Name
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.link [
|
||||
link.href (Router.format ["archives"; string archive.Id])
|
||||
link.text archive.Name
|
||||
]
|
||||
]
|
||||
Fui.tableCell [
|
||||
Fui.text (archive.StartTime.ToShortDateString())
|
||||
]
|
||||
Fui.tableCell [
|
||||
Fui.text (string archive.Frames)
|
||||
]
|
||||
Fui.tableCell [
|
||||
if archive.Published then
|
||||
Fui.icon.checkmarkRegular []
|
||||
else
|
||||
Fui.icon.dismissRegular []
|
||||
]
|
||||
Fui.tableCell [
|
||||
if archive.Public then
|
||||
Fui.icon.checkmarkRegular []
|
||||
else
|
||||
Fui.icon.dismissRegular []
|
||||
]
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let View () =
|
||||
let message, setMessage = React.useState None
|
||||
Html.main [
|
||||
prop.classes ["flex-column"; "gap-16"]
|
||||
prop.children [
|
||||
Fui.text.title2 "Actions"
|
||||
Html.div [
|
||||
prop.classes ["flex-row"; "flex-wrap";"gap-16"]
|
||||
prop.children [
|
||||
UserAddForm (Option.map (fun email group -> $"User {email} was added to group {group}") >> setMessage)
|
||||
ArchiveAddForm ()
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -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())
|
||||
@@ -3,20 +3,42 @@ namespace Oceanbox.Codex
|
||||
open Browser
|
||||
open Fable.Core
|
||||
open Feliz
|
||||
open FS.FluentUI
|
||||
|
||||
[<Erase>]
|
||||
type OpenFGA =
|
||||
[<ReactComponent>]
|
||||
static member Checkbox(key, label: string, user: string, relation: string, object: string) =
|
||||
let isLoading, setLoading = React.useState false
|
||||
let isChecked, setChecked = React.useState false
|
||||
|
||||
let handleChange (ev: Types.Event) =
|
||||
console.debug("[OpenFGA.Checkbox] Checkbox %s changed to %o", key, not isChecked)
|
||||
console.debug("[OpenFGA.Checkbox] Checkbox %s for user %s rel %s changed to %o", key, user, relation, not isChecked)
|
||||
// TODO: Write to OpenFGA
|
||||
setChecked(not isChecked)
|
||||
let newChecked = not isChecked
|
||||
// setChecked
|
||||
setLoading true
|
||||
Remoting.adminApi.setUserPermissions {
|
||||
User = user
|
||||
Permissions = [|
|
||||
{ Name = relation; Enabled = newChecked }
|
||||
|]
|
||||
}
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
console.debug("Success.")
|
||||
setChecked newChecked
|
||||
| Error err ->
|
||||
console.error("Error: %s", err)
|
||||
setLoading false
|
||||
)
|
||||
|
||||
React.useEffect (
|
||||
(fun () ->
|
||||
setLoading true
|
||||
|
||||
Remoting.openFgaApi.Check {
|
||||
User = user
|
||||
Relation = relation
|
||||
@@ -30,6 +52,8 @@ type OpenFGA =
|
||||
setChecked hasRelation
|
||||
| Error err ->
|
||||
console.error("[OpenFGA.Checkbox] Error checking user %s has relation %s to %s", user, relation, object)
|
||||
|
||||
setLoading false
|
||||
)
|
||||
),
|
||||
[| |]
|
||||
@@ -38,17 +62,21 @@ type OpenFGA =
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center" ]
|
||||
prop.children [
|
||||
Html.input [
|
||||
prop.id (sprintf "openfga-checkbox-%s" key)
|
||||
prop.type'.checkbox
|
||||
prop.onChange handleChange
|
||||
prop.custom("checked", isChecked)
|
||||
]
|
||||
if isLoading then
|
||||
Fui.spinner [
|
||||
spinner.size.tiny
|
||||
]
|
||||
else
|
||||
Html.input [
|
||||
prop.id (sprintf "openfga-checkbox-%s" key)
|
||||
prop.type'.checkbox
|
||||
prop.onChange handleChange
|
||||
prop.custom("checked", isChecked)
|
||||
]
|
||||
|
||||
Html.label [
|
||||
prop.htmlFor (sprintf "openfga-checkbox-%s" key)
|
||||
prop.text label
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,136 +1,61 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
open Browser
|
||||
open Fable.Core
|
||||
open Fable.Remoting.Client
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
|
||||
[<Erase>]
|
||||
type User =
|
||||
[<ReactComponent>]
|
||||
static member List(user: string, relation: string, objectType: string, ?context: obj) =
|
||||
let objects = OpenFGA.useObjects(user, relation, objectType, context)
|
||||
|
||||
if objects.Loading then
|
||||
Html.p "Loading ..."
|
||||
else
|
||||
if Array.isEmpty objects.Objects then
|
||||
Html.p (sprintf "No objects with user %s relation %s of type %s" user relation objectType)
|
||||
else
|
||||
Html.ul [
|
||||
prop.children (
|
||||
objects.Objects
|
||||
|> Array.sort
|
||||
|> Array.map (fun object ->
|
||||
let split = object.Split ':'
|
||||
match split with
|
||||
| [| objectType; id |] ->
|
||||
Html.li [
|
||||
prop.key id
|
||||
prop.children [
|
||||
Html.a [
|
||||
prop.href (Router.format("archives", id))
|
||||
prop.text id
|
||||
]
|
||||
]
|
||||
]
|
||||
| _ ->
|
||||
Html.li [
|
||||
prop.text "Invalid object format"
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
module User =
|
||||
[<ReactComponent>]
|
||||
let private DeleteForm (user: string) =
|
||||
let deleted, setDeleted = React.useState<Result<unit, string> option> None
|
||||
let deleting, setDeleting = React.useState false
|
||||
|
||||
let handleDelete =
|
||||
React.useCallback (
|
||||
(fun () ->
|
||||
setDeleting true
|
||||
console.info("[User] Deleting user %s", user)
|
||||
Remoting.adminApi.removeUsers [| user |]
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.catch (fun ex ->
|
||||
match ex with
|
||||
| :? ProxyRequestException as e ->
|
||||
let proxyError : Types.ProxyError = JS.JSON.parse e.ResponseText |> unbox
|
||||
let msg = proxyError.error.errorMsg
|
||||
Error msg
|
||||
| ex ->
|
||||
Error ex.Message
|
||||
)
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
console.info("[User] Successfully deleted user %s", user)
|
||||
setDeleted (Some (Ok ()))
|
||||
| Error err ->
|
||||
console.error("[User] Error deleting user %s: %s", user, err)
|
||||
setDeleted (Some (Error err))
|
||||
)
|
||||
),
|
||||
[| box user |]
|
||||
)
|
||||
|
||||
React.fragment [
|
||||
match deleted with
|
||||
| Some (Ok ()) ->
|
||||
Html.p "User successfully deleted."
|
||||
Html.a [
|
||||
prop.onClick (fun ev ->
|
||||
ev.preventDefault ()
|
||||
Router.navigateBack ()
|
||||
)
|
||||
prop.href (Router.format "")
|
||||
prop.text "Back"
|
||||
]
|
||||
| Some (Error err) ->
|
||||
Html.p (sprintf "Error deleting user: %s" err)
|
||||
| None ->
|
||||
if deleting then
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center"; "gap-8" ]
|
||||
prop.children [
|
||||
Html.button [
|
||||
prop.onClick (fun _ -> handleDelete ())
|
||||
prop.text "Are you sure?"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick (fun _ -> setDeleting false)
|
||||
prop.text "Cancel"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Html.p "This will delete the user from the databases. Not disable the user."
|
||||
else
|
||||
Html.button [
|
||||
prop.onClick (fun _ -> setDeleting true)
|
||||
prop.text "Delete"
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let View (user: string) =
|
||||
let fgaUser = sprintf "user:%s" user
|
||||
let execCtx = {|
|
||||
time = System.DateTime.Now
|
||||
task = "*"
|
||||
usage = "-1"
|
||||
|}
|
||||
|
||||
let groups = OpenFGA.useObjects(fgaUser, "member", "group")
|
||||
|
||||
Html.main [
|
||||
Html.h1 user
|
||||
|
||||
Html.section [
|
||||
prop.children [
|
||||
DeleteForm user
|
||||
Users.DeleteForm user
|
||||
]
|
||||
]
|
||||
|
||||
Html.section [
|
||||
prop.children [
|
||||
Html.h2 "Groups"
|
||||
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.ul [
|
||||
prop.children (
|
||||
groups.Objects
|
||||
|> Array.map (fun group ->
|
||||
let split = group.Split ':'
|
||||
match split with
|
||||
| [| "group"; groupName |] ->
|
||||
Html.li [
|
||||
prop.children [
|
||||
Html.a [
|
||||
prop.href (Router.format("groups", groupName))
|
||||
prop.text groupName
|
||||
]
|
||||
]
|
||||
]
|
||||
| _ ->
|
||||
Html.none
|
||||
)
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
Html.section [
|
||||
prop.children [
|
||||
Html.h2 "Archmaester"
|
||||
@@ -170,7 +95,7 @@ module User =
|
||||
style.maxHeight (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
User.List(fgaUser, "owner", "archive")
|
||||
Users.OpenFgaList(fgaUser, "owner", "archive")
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -192,7 +117,7 @@ module User =
|
||||
style.maxHeight (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
User.List(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
|
||||
Users.OpenFgaList(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -214,7 +139,7 @@ module User =
|
||||
style.maxHeight (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
User.List(fgaUser, "exec", "archive", {| time = System.DateTime.Now |})
|
||||
Users.OpenFgaList(fgaUser, "exec", "archive", execCtx)
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -223,4 +148,4 @@ module User =
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
81
src/Codex/src/Client/Users/DeleteForm.fs
Normal file
81
src/Codex/src/Client/Users/DeleteForm.fs
Normal 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"
|
||||
]
|
||||
]
|
||||
42
src/Codex/src/Client/Users/OpenFgaList.fs
Normal file
42
src/Codex/src/Client/Users/OpenFgaList.fs
Normal 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"
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
7
src/Codex/src/Client/Utils.fs
Normal file
7
src/Codex/src/Client/Utils.fs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
module Utils =
|
||||
open Fable.Core
|
||||
open Feliz
|
||||
|
||||
let toReact (el: JSX.Element) : ReactElement = unbox el
|
||||
@@ -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
|
||||
@@ -1,15 +1,52 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Feliz App</title>
|
||||
<title>Codex</title>
|
||||
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" type="image/png" href="/img/favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="shortcut icon" type="image/png" href="/img/favicon-16x16.png" sizes="16x16" />
|
||||
<link rel="stylesheet" href="/main.css" />
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Segoe UI Web (West European)';
|
||||
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-light.woff2);
|
||||
font-weight: 100;
|
||||
font-style: 'normal';
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Segoe UI Web (West European)';
|
||||
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-semilight.woff2);
|
||||
font-weight: 300;
|
||||
font-style: 'normal';
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Segoe UI Web (West European)';
|
||||
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-regular.woff2);
|
||||
font-weight: 400;
|
||||
font-style: 'normal';
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Segoe UI Web (West European)';
|
||||
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-semibold.woff2);
|
||||
font-weight: 600;
|
||||
font-style: 'normal';
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Segoe UI Web (West European)';
|
||||
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-bold.woff2);
|
||||
font-weight: 700;
|
||||
font-style: 'normal';
|
||||
font-display: swap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body style="margin: 0;">
|
||||
<div id="feliz-app"></div>
|
||||
<script type="module" src="/build/Main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -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, )",
|
||||
|
||||
@@ -116,6 +116,15 @@ h1 {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bcol-neutral-stroke-1 {
|
||||
border-color: var(--colorNeutralStroke1);
|
||||
}
|
||||
|
||||
.br-solid {
|
||||
border-right-width: 2px;
|
||||
border-right-style: solid;
|
||||
}
|
||||
|
||||
.text-overflow {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@@ -126,6 +135,26 @@ h1 {
|
||||
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
||||
}
|
||||
|
||||
.vh-100 {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.w-256 {
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
.w-512 {
|
||||
width: 512px;
|
||||
}
|
||||
|
||||
.minh-512 {
|
||||
min-height: 512px;
|
||||
}
|
||||
|
||||
.mw-128 {
|
||||
max-width: 128px;
|
||||
}
|
||||
|
||||
/* special stuff */
|
||||
|
||||
.archives-list {
|
||||
@@ -147,4 +176,4 @@ h1 {
|
||||
flex: 2 1 512px;
|
||||
min-width: 512px;
|
||||
max-width: 576px;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,19 @@ module Admin =
|
||||
|
||||
open Oceanbox
|
||||
|
||||
// TODO: Better name and/or place for this?
|
||||
module private EFType =
|
||||
let toArchive (efArchive: Entity.Archive) : Remoting.Archive = {
|
||||
Id = efArchive.ArchiveId
|
||||
Name = efArchive.Name
|
||||
DataSetId = efArchive.AttribsId
|
||||
StartTime = efArchive.StartTime
|
||||
Frames = efArchive.Frames
|
||||
Published = efArchive.Published
|
||||
Public = efArchive.Public
|
||||
}
|
||||
|
||||
|
||||
module private Handler =
|
||||
let addUsers (ctx: HttpContext) (req: Remoting.AddUsersRequest) : Async<Result<unit, string>> =
|
||||
let archmaesterAdd (db: Entity.ArchiveContext) =
|
||||
@@ -63,7 +76,7 @@ module Admin =
|
||||
with e ->
|
||||
do logger.LogError ("OpenFGA write errored with: {Msg}. Rolling back archmaester.", e.Message)
|
||||
do! tr.RollbackAsync ()
|
||||
return! Error (sprintf "Error deleting users from OpenFGA: %s" e.Message)
|
||||
return! Error (sprintf "Error adding users to OpenFGA: %s" e.Message)
|
||||
with e ->
|
||||
do logger.LogError (e, "Failed connecting to database")
|
||||
return! Error (sprintf "Error deleting users from OpenFGA: %s" e.Message)
|
||||
@@ -76,21 +89,50 @@ module Admin =
|
||||
do logger.LogInformation ("Add archive groups from {User}: {Request}", user, req)
|
||||
try
|
||||
let db = ctx.GetService<Entity.ArchiveContext> ()
|
||||
let fga = ctx.GetService<OpenFgaClient> ()
|
||||
|
||||
let! created = Archmaester.EFCore.addArchiveGroups db req.Id req.Groups
|
||||
do logger.LogInformation ("Added {CreatedCount} archive group entries", created)
|
||||
|
||||
let fga = ctx.GetService<OpenFgaClient> ()
|
||||
let req = OpenFGA.Group.addArchive req
|
||||
let! fgaResp = fga.Write req |> Async.AwaitTask
|
||||
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaResp.ToJson ())
|
||||
|
||||
return Ok ()
|
||||
with e ->
|
||||
do logger.LogError(e, "Error adding group to archive")
|
||||
do logger.LogError (e, "Error adding group to archive")
|
||||
return Error (sprintf "Error adding archive groups: %s" e.Message)
|
||||
}
|
||||
|
||||
let private permissionToTuple
|
||||
(archiveId: System.Guid)
|
||||
(groupName: string)
|
||||
(relation: Remoting.ArchiveRelation)
|
||||
: Model.ClientTupleKey =
|
||||
match relation with
|
||||
| Remoting.ArchiveRelation.ViewTerm term -> OpenFGA.Group.viewArchive archiveId term groupName
|
||||
| Remoting.ArchiveRelation.ExecTicket ticket -> OpenFGA.Group.execArchive archiveId ticket groupName
|
||||
|
||||
let addGroupPermissions (ctx: HttpContext) (req: Remoting.AddGroupPermissionsRequest) =
|
||||
async {
|
||||
let user = ctx.User.Identity.Name
|
||||
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||
do logger.LogInformation ("Add group archive permissions from {User}: {Request}", user, req)
|
||||
try
|
||||
let fga = ctx.GetService<OpenFgaClient> ()
|
||||
let fgaReq: Model.ClientWriteRequest =
|
||||
req.Permissions
|
||||
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|
||||
|> OpenFGA.Queries.write'
|
||||
let! fgaResp = fga.Write fgaReq |> Async.AwaitTask
|
||||
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaResp.ToJson ())
|
||||
|
||||
return Ok ()
|
||||
with e ->
|
||||
do logger.LogError (e, "Error adding archive permission to group")
|
||||
return Error (sprintf "Error adding group permissions: %s" e.Message)
|
||||
}
|
||||
|
||||
let deleteArchive (ctx: HttpContext) (archiveId: System.Guid) : Async<Result<bool, string>> =
|
||||
async {
|
||||
let user = ctx.User.Identity.Name
|
||||
@@ -179,7 +221,7 @@ module Admin =
|
||||
else
|
||||
return Error "Filter must include archive id"
|
||||
with e ->
|
||||
do logger.LogError (e, "Error in getArchives from {User}", user)
|
||||
do logger.LogError (e, "Error in getArchiveRefs from {User}", user)
|
||||
return Error "Error fetching archive count"
|
||||
}
|
||||
|
||||
@@ -235,6 +277,186 @@ module Admin =
|
||||
return Error "Error fetching archive count"
|
||||
}
|
||||
|
||||
let getAllDataSets (ctx: HttpContext) =
|
||||
async {
|
||||
let user = ctx.User.Identity.Name
|
||||
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||
try
|
||||
do logger.LogInformation("getAllDataSets from {User}", user)
|
||||
let db = ctx.GetService<NpgsqlDataSource> ()
|
||||
let! dataSets =
|
||||
Archmaester.Dapper.queryDataSets db
|
||||
return Ok dataSets
|
||||
with e ->
|
||||
do logger.LogError (e, "getAllDataSets from {User}", user)
|
||||
return Error "Error fetching dataSets"
|
||||
}
|
||||
|
||||
let getArchiveDataSet (ctx: HttpContext) (archiveId: System.Guid) =
|
||||
async {
|
||||
let user = ctx.User.Identity.Name
|
||||
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||
try
|
||||
do logger.LogInformation("getArchiveDataSet {Archive} from {User}", archiveId, user)
|
||||
let db = ctx.GetService<NpgsqlDataSource> ()
|
||||
let! dataSet = Archmaester.Dapper.queryArchiveDataSet db archiveId
|
||||
return Ok dataSet
|
||||
with e ->
|
||||
do logger.LogError (e, "getArchiveDataSet {Archive} from {User}", archiveId, user)
|
||||
return Error "Error fetching dataset"
|
||||
}
|
||||
|
||||
let addArchive (ctx: HttpContext) (archive: Remoting.AddArchiveRequest) =
|
||||
let archmaesterAdd (db: Entity.ArchiveContext) =
|
||||
async {
|
||||
try
|
||||
let! nameTaken = Archmaester.EFCore.checkArchiveNameTaken db archive.Name
|
||||
if nameTaken then
|
||||
return Error "Error adding archive: an archive with that name already exists"
|
||||
else
|
||||
let! newArchive = Archmaester.EFCore.addArchive db archive
|
||||
return Ok newArchive
|
||||
with e ->
|
||||
return Error (sprintf "Error adding archive: %s" e.Message)
|
||||
}
|
||||
let archmaesterAddPublic (db: Entity.ArchiveContext) (archiveId: System.Guid) =
|
||||
async {
|
||||
try
|
||||
let! success = Archmaester.EFCore.setArchivePublic db archiveId true
|
||||
if success > 0 then
|
||||
return Ok ()
|
||||
else
|
||||
return Error "Failed to add * user to public archive"
|
||||
with e ->
|
||||
return Error (sprintf "Error adding * user to archive: %s" e.Message)
|
||||
}
|
||||
asyncResult {
|
||||
let user = ctx.User.Identity.Name
|
||||
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||
try
|
||||
do logger.LogInformation("addArchive {Archive} from {User}", archive.Name, user)
|
||||
let fga = ctx.GetService<OpenFgaClient> ()
|
||||
let db = ctx.GetService<Entity.ArchiveContext> ()
|
||||
let tr = db.Database.BeginTransaction()
|
||||
|
||||
let! newArchive = archmaesterAdd db
|
||||
try
|
||||
if newArchive.Public then
|
||||
do! archmaesterAddPublic db newArchive.ArchiveId
|
||||
|
||||
let tuples =
|
||||
if newArchive.Public then
|
||||
[|
|
||||
OpenFGA.Archive.defaultPrincipal newArchive.ArchiveId
|
||||
OpenFGA.Archive.publicArchive newArchive.ArchiveId
|
||||
|]
|
||||
else
|
||||
[|OpenFGA.Archive.defaultPrincipal newArchive.ArchiveId|]
|
||||
|
||||
let! fgaResp =
|
||||
let req = OpenFGA.Queries.write' tuples
|
||||
fga.Write req |> Async.AwaitTask
|
||||
|
||||
logger.LogInformation (
|
||||
"addArchive: {Archive} openFGA write responded with {JSON}",
|
||||
archive.Name,
|
||||
fgaResp.ToJson ()
|
||||
)
|
||||
|
||||
do! tr.CommitAsync ()
|
||||
|
||||
return! newArchive |> EFType.toArchive |> Ok
|
||||
with e ->
|
||||
// TODO: Ideally, we should rollback fga too
|
||||
do logger.LogError (e, "addArchive OpenFGA failed, rolling back Archmaester")
|
||||
do! tr.RollbackAsync ()
|
||||
return! Error (sprintf "Error adding archive: %s" e.Message)
|
||||
with e ->
|
||||
do logger.LogError (e, "addArchive {Archive} from {User}", archive.Name, user)
|
||||
return! Error "Error adding archive"
|
||||
}
|
||||
|
||||
let updateArchive (ctx: HttpContext) (archiveId: System.Guid) (archive: Remoting.EditArchiveRequest) =
|
||||
let archmaesterEdit (db: Entity.ArchiveContext) =
|
||||
async {
|
||||
try
|
||||
let! existingName = Archmaester.EFCore.getArchiveName db archiveId
|
||||
let! nameTaken = Archmaester.EFCore.checkArchiveNameTaken db archive.Name
|
||||
if nameTaken && not (existingName = archive.Name) then
|
||||
return Error "Error updating archive: an archive with that name already exists"
|
||||
else
|
||||
let! newArchive = Archmaester.EFCore.editArchive db archiveId archive
|
||||
return Ok newArchive
|
||||
with e ->
|
||||
return Error (sprintf "Error updating archive: %s" e.Message)
|
||||
}
|
||||
let archmaesterSetPublic (db: Entity.ArchiveContext) (setPublic: bool) =
|
||||
async {
|
||||
try
|
||||
let! success = Archmaester.EFCore.setArchivePublic db archiveId setPublic
|
||||
if success > 0 then
|
||||
return Ok ()
|
||||
else
|
||||
return Error "Failed to add * user to public archive"
|
||||
with e ->
|
||||
return Error (sprintf "Error adding/removing * user to/from archive: %s" e.Message)
|
||||
}
|
||||
let fgaSetPublic (fga: OpenFgaClient) (setPublic: bool) =
|
||||
async {
|
||||
try
|
||||
let publicTuple = OpenFGA.Archive.publicArchive archiveId
|
||||
let checkReq =
|
||||
OpenFGA.Queries.check { User = publicTuple.User; Relation = publicTuple.Relation; Object = publicTuple.Object}
|
||||
let! publicResp = fga.Check checkReq |> Async.AwaitTask
|
||||
let publicTupleExists = publicResp.Allowed |> Option.ofNullable |> Option.defaultValue false
|
||||
|
||||
if setPublic && not publicTupleExists then
|
||||
let writeReq = OpenFGA.Queries.write' [|publicTuple|]
|
||||
let! writeResp = fga.Write writeReq |> Async.AwaitTask
|
||||
match writeResp.Writes |> Array.ofSeq with
|
||||
| [||] -> return Error (sprintf "Error writing public tuple to fga: %s" (writeResp.ToJson()))
|
||||
| _ -> return Ok ()
|
||||
elif not setPublic && publicTupleExists then
|
||||
let deleteReq = OpenFGA.Queries.delete' [|publicTuple|]
|
||||
let! deleteResp = fga.Write deleteReq |> Async.AwaitTask
|
||||
match deleteResp.Deletes |> Array.ofSeq with
|
||||
| [||] -> return Error (sprintf "Error deleting public tuple from fga: %s" (deleteResp.ToJson()))
|
||||
| _ -> return Ok ()
|
||||
else
|
||||
return Ok ()
|
||||
with e ->
|
||||
return Error (sprintf "Error adding/removing fga user:* view relation to/from archive: %s" e.Message)
|
||||
}
|
||||
asyncResult {
|
||||
let user = ctx.User.Identity.Name
|
||||
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||
try
|
||||
do logger.LogInformation("updateArchive {Archive} from {User}", archive.Name, user)
|
||||
let fga = ctx.GetService<OpenFgaClient> ()
|
||||
let db = ctx.GetService<Entity.ArchiveContext> ()
|
||||
let tr = db.Database.BeginTransaction()
|
||||
|
||||
let! newArchive = archmaesterEdit db
|
||||
try
|
||||
if newArchive.Public && not archive.Public then
|
||||
do! archmaesterSetPublic db true
|
||||
do! fgaSetPublic fga true
|
||||
elif not newArchive.Public && archive.Public then
|
||||
do! archmaesterSetPublic db false
|
||||
do! fgaSetPublic fga false
|
||||
|
||||
do! tr.CommitAsync ()
|
||||
|
||||
return! newArchive |> EFType.toArchive |> Ok
|
||||
with e ->
|
||||
do logger.LogError (e, "updateArchive setting public failed, rolling back Archmaester")
|
||||
do! tr.RollbackAsync ()
|
||||
return! Error (sprintf "Error updating archive: %s" e.Message)
|
||||
with e ->
|
||||
do logger.LogError (e, "updateArchive {Archive} from {User}", archive.Name, user)
|
||||
return! Error (sprintf "Error updating archive: %s" e.Message)
|
||||
}
|
||||
|
||||
let getArchiveTypes (ctx: HttpContext) =
|
||||
async {
|
||||
let user = ctx.User.Identity.Name
|
||||
@@ -306,18 +528,101 @@ module Admin =
|
||||
return ()
|
||||
}
|
||||
|
||||
let setUserPermissions (ctx: HttpContext) (req: Remoting.UserPermissionRequest) =
|
||||
async {
|
||||
let user = ctx.User.Identity.Name
|
||||
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||
do logger.LogInformation ("setUserPermissions from {User}: {@Req}", user, req)
|
||||
// TODO(simkir): Sanitize/check the request, aka turn the dto to an internal type
|
||||
try
|
||||
let fga = ctx.GetService<OpenFgaClient> ()
|
||||
let writes: Model.ClientWriteRequest =
|
||||
req.Permissions
|
||||
|> Array.choose (fun permission ->
|
||||
if permission.Enabled then
|
||||
Some {
|
||||
Remoting.Tuple.empty with
|
||||
User = req.User
|
||||
Relation = permission.Name
|
||||
Object = req.User
|
||||
}
|
||||
else
|
||||
None
|
||||
)
|
||||
|> OpenFGA.Queries.write
|
||||
if writes.Writes.Count > 0 then
|
||||
let! fgaWriteResp = fga.Write writes |> Async.AwaitTask
|
||||
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaWriteResp.ToJson ())
|
||||
|
||||
let deletes =
|
||||
req.Permissions
|
||||
|> Array.choose (fun permission ->
|
||||
if permission.Enabled then
|
||||
None
|
||||
else
|
||||
Remoting.Tuple.delete(req.User, permission.Name, req.User)
|
||||
|> Some
|
||||
)
|
||||
|> OpenFGA.Queries.deleteTuples
|
||||
if deletes.Count > 0 then
|
||||
let! fgaDeleteResp = fga.DeleteTuples deletes |> Async.AwaitTask
|
||||
do logger.LogInformation ("OpenFGA delete responded with: {JSON}", fgaDeleteResp.ToJson ())
|
||||
|
||||
return Ok ()
|
||||
with e ->
|
||||
do logger.LogError (e, "Error setting user permissions")
|
||||
// TODO: Maybe do not send exn message
|
||||
return Error (sprintf "Error setting user permissions: %s" e.Message)
|
||||
}
|
||||
|
||||
let updateGroupPermissions (ctx: HttpContext) (req: Remoting.AddGroupPermissionsRequest) =
|
||||
async {
|
||||
let user = ctx.User.Identity.Name
|
||||
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||
do logger.LogInformation ("updateGroupPermissions from {User}: {@Req}", user, req)
|
||||
try
|
||||
let fga = ctx.GetService<OpenFgaClient> ()
|
||||
|
||||
let deletes =
|
||||
req.Permissions
|
||||
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|
||||
|> OpenFGA.Queries.deleteTuples'
|
||||
let! deleteResp = fga.DeleteTuples deletes |> Async.AwaitTask
|
||||
do logger.LogInformation ("OpenFGA delete responded with: {JSON}", deleteResp.ToJson ())
|
||||
|
||||
let writes =
|
||||
req.Permissions
|
||||
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|
||||
|> ResizeArray
|
||||
let! writeResp = fga.WriteTuples writes |> Async.AwaitTask
|
||||
do logger.LogInformation ("OpenFGA write responded with: {JSON}", writeResp.ToJson ())
|
||||
|
||||
return Ok ()
|
||||
with e ->
|
||||
do logger.LogError (e, "Error updating group permissions")
|
||||
// TODO: Maybe do not send exn message
|
||||
return Error (sprintf "Error updating group permissions: %s" e.Message)
|
||||
}
|
||||
|
||||
let private impl (ctx: HttpContext) : Remoting.Api.Admin = {
|
||||
addUsers = Handler.addUsers ctx
|
||||
addArchive = Handler.addArchive ctx
|
||||
addArchiveGroups = Handler.addArchiveGroups ctx
|
||||
addGroupPermissions = Handler.addGroupPermissions ctx
|
||||
addUsers = Handler.addUsers ctx
|
||||
deleteArchive = Handler.deleteArchive ctx
|
||||
getAllGroups = Handler.getAllGroups ctx
|
||||
getArchive = Handler.getArchive ctx
|
||||
getArchiveCount = Handler.getArchiveCount ctx
|
||||
getArchiveDataSet = Handler.getArchiveDataSet ctx
|
||||
getArchiveRefs = Handler.getArchiveRefs ctx
|
||||
getArchiveTypes = fun () -> Handler.getArchiveTypes ctx
|
||||
getArchives = Handler.getArchives ctx
|
||||
getDataSets = fun () -> Handler.getAllDataSets ctx
|
||||
getGroupUsers = Handler.getGroupUsers ctx
|
||||
removeUsers = Handler.removeUsers ctx
|
||||
setUserPermissions = Handler.setUserPermissions ctx
|
||||
updateArchive = Handler.updateArchive ctx
|
||||
updateGroupPermissions = Handler.updateGroupPermissions ctx
|
||||
}
|
||||
|
||||
let endpoints: HttpHandler =
|
||||
@@ -325,4 +630,4 @@ module Admin =
|
||||
|> Remoting.withErrorHandler Utils.rpcErrorHandler
|
||||
|> Remoting.fromContext impl
|
||||
|> Remoting.withRouteBuilder Remoting.routeBuilder
|
||||
|> Remoting.buildHttpHandler
|
||||
|> Remoting.buildHttpHandler
|
||||
@@ -5,6 +5,7 @@ module Archmaester =
|
||||
|
||||
open Archmaester
|
||||
|
||||
|
||||
let getDataSource (connStr: string) : NpgsqlDataSource =
|
||||
let dataSourceBuilder = NpgsqlDataSourceBuilder connStr
|
||||
let _mapper = dataSourceBuilder.UseNetTopologySuite ()
|
||||
@@ -18,6 +19,16 @@ module Archmaester =
|
||||
|
||||
open Oceanbox.DataAgent.Dapper
|
||||
|
||||
// TODO: Is there another, better way to do this?
|
||||
type DataSetTable = {
|
||||
a_id: int
|
||||
base_path: string
|
||||
m_id: System.Guid
|
||||
m_name: string
|
||||
start_time: System.DateTime
|
||||
end_time: System.DateTime
|
||||
}
|
||||
|
||||
let private canonicalizeGroupNames (groups: string array option) : string array =
|
||||
groups
|
||||
|> Option.defaultValue [||]
|
||||
@@ -138,6 +149,116 @@ module Archmaester =
|
||||
return count
|
||||
}
|
||||
|
||||
// NOTE: Currently called attribs, but soon to be renamed
|
||||
let queryDataSets (db: NpgsqlDataSource) =
|
||||
async {
|
||||
use conn = db.OpenConnection ()
|
||||
let query =
|
||||
"""
|
||||
SELECT
|
||||
a.id AS a_id,
|
||||
a.base_path,
|
||||
m.id AS m_id,
|
||||
m.name AS m_name,
|
||||
MIN(f.start_time) AS start_time,
|
||||
MAX(f.start_time) AS end_time
|
||||
FROM
|
||||
attribs AS a
|
||||
JOIN
|
||||
model_areas AS m
|
||||
ON m.id = a.model_area_id
|
||||
LEFT JOIN
|
||||
files AS f
|
||||
ON f.attribs_id = a.id
|
||||
WHERE
|
||||
-- Type 1 is FVCOM
|
||||
a.type_id = 1
|
||||
AND NOT a.retired
|
||||
GROUP BY
|
||||
a.id,
|
||||
a.base_path,
|
||||
m.id,
|
||||
m.name
|
||||
;
|
||||
"""
|
||||
|
||||
let! res =
|
||||
conn.QueryAsync<DataSetTable>(query)
|
||||
|> Async.AwaitTask
|
||||
|
||||
return
|
||||
res
|
||||
|> Array.ofSeq
|
||||
|> Array.map (fun x ->
|
||||
{
|
||||
Id = x.a_id
|
||||
BasePath = x.base_path
|
||||
ModelAreaId = x.m_id
|
||||
ModelAreaName = x.m_name
|
||||
StartTime = x.start_time
|
||||
EndTime = x.end_time.AddHours(24)
|
||||
} : Remoting.DataSet
|
||||
)
|
||||
}
|
||||
|
||||
let queryArchiveDataSet (db: NpgsqlDataSource) (archiveId: System.Guid) =
|
||||
async {
|
||||
use conn = db.OpenConnection ()
|
||||
let query =
|
||||
"""
|
||||
SELECT
|
||||
a.id AS a_id,
|
||||
a.base_path,
|
||||
m.id AS m_id,
|
||||
m.name AS m_name,
|
||||
MIN(f.start_time) AS start_time,
|
||||
MAX(f.start_time) AS end_time
|
||||
FROM
|
||||
attribs AS a
|
||||
JOIN
|
||||
model_areas AS m
|
||||
ON m.id = a.model_area_id
|
||||
LEFT JOIN
|
||||
files AS f
|
||||
ON f.attribs_id = a.id
|
||||
LEFT JOIN
|
||||
archives as ar
|
||||
ON ar.attribs_id = a.id
|
||||
WHERE
|
||||
ar.id = @archive_id
|
||||
GROUP BY
|
||||
a.id,
|
||||
a.base_path,
|
||||
m.id,
|
||||
m.name
|
||||
;
|
||||
"""
|
||||
|
||||
let param =
|
||||
dict [
|
||||
"archive_id", box archiveId
|
||||
]
|
||||
|
||||
let! res =
|
||||
conn.QueryAsync<DataSetTable>(query, param)
|
||||
|> Async.AwaitTask
|
||||
|
||||
return
|
||||
res
|
||||
|> Array.ofSeq
|
||||
|> Array.map (fun x ->
|
||||
{
|
||||
Id = x.a_id
|
||||
BasePath = x.base_path
|
||||
ModelAreaId = x.m_id
|
||||
ModelAreaName = x.m_name
|
||||
StartTime = x.start_time
|
||||
EndTime = x.end_time.AddHours(24)
|
||||
} : Remoting.DataSet
|
||||
) // TODO: is there a better way to select single rows?
|
||||
|> Array.head
|
||||
}
|
||||
|
||||
module EFCore =
|
||||
open System.Linq
|
||||
|
||||
@@ -180,6 +301,114 @@ module Archmaester =
|
||||
return created
|
||||
}
|
||||
|
||||
let checkArchiveNameTaken (db: Entity.ArchiveContext) (name: string) : Async<bool> =
|
||||
db.Archives
|
||||
.AsNoTracking()
|
||||
.Where(fun archive -> archive.Name = name)
|
||||
.AnyAsync()
|
||||
|> Async.AwaitTask
|
||||
|
||||
let getArchiveName (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<string> =
|
||||
db.Archives
|
||||
.AsNoTracking()
|
||||
.Where(fun archive -> archive.ArchiveId = archiveId)
|
||||
.Select(_.Name)
|
||||
.SingleOrDefaultAsync()
|
||||
|> Async.AwaitTask
|
||||
|
||||
/// Align archive start time with the actual files in the dataset
|
||||
let private getAlignedStartTime (db: Entity.ArchiveContext) (dataSetId: int) (archiveStartTime: System.DateTime) =
|
||||
let utcStartTime = archiveStartTime.ToUniversalTime()
|
||||
db.Files
|
||||
.AsNoTracking()
|
||||
.Where(fun file ->
|
||||
file.AttribsId = dataSetId
|
||||
&& file.StartTime >= utcStartTime
|
||||
)
|
||||
.OrderBy(_.StartTime)
|
||||
.Select(_.StartTime)
|
||||
.FirstAsync()
|
||||
|> Async.AwaitTask
|
||||
|
||||
let addArchive (db: Entity.ArchiveContext) (archive: Remoting.AddArchiveRequest) : Async<Entity.Archive> =
|
||||
async {
|
||||
let! alignedStartTime = getAlignedStartTime db archive.DataSetId archive.StartTime
|
||||
|
||||
let newArchive =
|
||||
Entity.Archive(
|
||||
Name = archive.Name,
|
||||
Frames = archive.Frames,
|
||||
StartTime = alignedStartTime,
|
||||
Published = archive.Published,
|
||||
Public = archive.Public,
|
||||
AttribsId = archive.DataSetId
|
||||
)
|
||||
|
||||
db.Add newArchive |> ignore
|
||||
|
||||
let! obxGroupId =
|
||||
db.Groups
|
||||
.AsNoTracking()
|
||||
.Where(fun group -> group.Name = "/oceanbox")
|
||||
.Select(_.GroupId)
|
||||
.SingleAsync()
|
||||
|> Async.AwaitTask
|
||||
|
||||
let defaultGroup = Entity.ArchiveGroup(ArchiveId = newArchive.ArchiveId, GroupId = obxGroupId)
|
||||
db.Add defaultGroup |> ignore
|
||||
|
||||
do! db.SaveChangesAsync () |> Async.AwaitTask |> Async.Ignore
|
||||
|
||||
return newArchive
|
||||
}
|
||||
|
||||
let editArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) (archive: Remoting.EditArchiveRequest) : Async<Entity.Archive> =
|
||||
async {
|
||||
let! existingArchive =
|
||||
db.Archives
|
||||
.Where(fun archive -> archive.ArchiveId = archiveId)
|
||||
.SingleAsync()
|
||||
|> Async.AwaitTask
|
||||
|
||||
let! alignedStartTime = getAlignedStartTime db existingArchive.AttribsId archive.StartTime
|
||||
|
||||
existingArchive.Name <- archive.Name
|
||||
existingArchive.StartTime <- alignedStartTime
|
||||
existingArchive.Frames <- archive.Frames
|
||||
existingArchive.Published <- archive.Published
|
||||
existingArchive.Public <- archive.Public
|
||||
|
||||
do db.Update existingArchive |> ignore
|
||||
do! db.SaveChangesAsync () |> Async.AwaitTask |> Async.Ignore
|
||||
|
||||
return existingArchive
|
||||
}
|
||||
|
||||
let setArchivePublic (db: Entity.ArchiveContext) (archiveId: System.Guid) (setPublic: bool) : Async<int> =
|
||||
async {
|
||||
let! starUserId =
|
||||
db.Users
|
||||
.AsNoTracking()
|
||||
.Where(fun user -> user.Name = "*")
|
||||
.Select(_.UserId)
|
||||
.SingleAsync()
|
||||
|> Async.AwaitTask
|
||||
|
||||
if setPublic then
|
||||
let starArchiveUser = Entity.ArchiveUser(UserId = starUserId, ArchiveId = archiveId)
|
||||
db.Add starArchiveUser |> ignore
|
||||
else
|
||||
let! starArchiveUser =
|
||||
db.ArchiveUsers
|
||||
.Where(fun au -> au.UserId = starUserId && au.ArchiveId = archiveId)
|
||||
.SingleOrDefaultAsync()
|
||||
|> Async.AwaitTask
|
||||
|
||||
starArchiveUser |> Option.ofObj |> Option.iter (db.Remove >> ignore)
|
||||
|
||||
return! db.SaveChangesAsync () |> Async.AwaitTask
|
||||
}
|
||||
|
||||
let deleteArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<int> =
|
||||
async {
|
||||
let! entity =
|
||||
@@ -420,4 +649,4 @@ module Archmaester =
|
||||
|> Async.AwaitTask
|
||||
|
||||
return entities
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -71,7 +71,8 @@ module OpenFGA =
|
||||
|> Seq.toArray
|
||||
|> Array.map (fun t ->
|
||||
let condition : Remoting.Condition option =
|
||||
Option.ofObj t.Key.Condition
|
||||
t.Key.Condition
|
||||
|> Option.ofObj
|
||||
|> Option.map (fun cond -> {
|
||||
Name = cond.Name
|
||||
Context = JsonSerializer.Serialize cond.Context
|
||||
@@ -148,6 +149,15 @@ module OpenFGA =
|
||||
|
||||
result
|
||||
|
||||
let read' (tuple: ClientTupleKey) : ClientReadRequest =
|
||||
let result = ClientReadRequest ()
|
||||
|
||||
do result.User <- tuple.User
|
||||
do result.Relation <- tuple.Relation
|
||||
do result.Object <- tuple.Object
|
||||
|
||||
result
|
||||
|
||||
let delete (tuples: Remoting.Tuple array) =
|
||||
let result = ClientWriteRequest ()
|
||||
|
||||
@@ -167,6 +177,57 @@ module OpenFGA =
|
||||
|
||||
result
|
||||
|
||||
/// To be used with OpenFga.Sdk.Client.OpenFgaClient.DeleteTuples
|
||||
let deleteTuples (tuples: Remoting.Tuple array) : ResizeArray<ClientTupleKeyWithoutCondition> =
|
||||
let deletes: ClientTupleKeyWithoutCondition array =
|
||||
tuples
|
||||
|> Array.map (fun tuple ->
|
||||
let result = ClientTupleKeyWithoutCondition ()
|
||||
|
||||
do result.Object <- tuple.Object
|
||||
do result.Relation <- tuple.Relation
|
||||
do result.User <- tuple.User
|
||||
|
||||
result
|
||||
)
|
||||
|
||||
ResizeArray deletes
|
||||
|
||||
let delete' (tuples: ClientTupleKey array) : ClientWriteRequest =
|
||||
let result = ClientWriteRequest ()
|
||||
|
||||
let deletes: ClientTupleKeyWithoutCondition array =
|
||||
tuples
|
||||
|> Array.map (fun tuple ->
|
||||
let result = ClientTupleKeyWithoutCondition ()
|
||||
|
||||
do result.Object <- tuple.Object
|
||||
do result.Relation <- tuple.Relation
|
||||
do result.User <- tuple.User
|
||||
|
||||
result
|
||||
)
|
||||
|
||||
do result.Deletes <- ResizeArray deletes
|
||||
|
||||
result
|
||||
|
||||
/// To be used with OpenFga.Sdk.Client.OpenFgaClient.DeleteTuples
|
||||
let deleteTuples' (tuples: ClientTupleKey array) : ResizeArray<ClientTupleKeyWithoutCondition> =
|
||||
let deletes: ClientTupleKeyWithoutCondition array =
|
||||
tuples
|
||||
|> Array.map (fun tuple ->
|
||||
let result = ClientTupleKeyWithoutCondition ()
|
||||
|
||||
do result.Object <- tuple.Object
|
||||
do result.Relation <- tuple.Relation
|
||||
do result.User <- tuple.User
|
||||
|
||||
result
|
||||
)
|
||||
|
||||
ResizeArray deletes
|
||||
|
||||
let write (tuples: Remoting.Tuple array) =
|
||||
let result = ClientWriteRequest ()
|
||||
|
||||
@@ -236,6 +297,28 @@ module OpenFGA =
|
||||
|])
|
||||
|> Queries.write'
|
||||
|
||||
module Archive =
|
||||
/// Creates a default principal tuple for the oceanbox group
|
||||
let defaultPrincipal (id: Guid) : ClientTupleKey =
|
||||
let tuple = ClientTupleKey ()
|
||||
|
||||
do tuple.Object <- sprintf "archive:%s" (string id)
|
||||
do tuple.Relation <- "principal"
|
||||
do tuple.User <- "group:/oceanbox"
|
||||
|
||||
tuple
|
||||
|
||||
/// Gives view permission to all users. This tuple should be added to public archives
|
||||
let publicArchive (id: Guid) : ClientTupleKey =
|
||||
let tuple = ClientTupleKey ()
|
||||
|
||||
do tuple.Object <- sprintf "archive:%s" (string id)
|
||||
do tuple.Relation <- "view"
|
||||
do tuple.User <- "user:*"
|
||||
|
||||
tuple
|
||||
|
||||
|
||||
module private Handlers =
|
||||
let check (ctx: HttpContext) (req: Remoting.CheckRequest) =
|
||||
async {
|
||||
@@ -260,9 +343,10 @@ module OpenFGA =
|
||||
let logger = ctx.GetLogger<Remoting.Api.OpenFGA> ()
|
||||
let fga = ctx.GetService<OpenFgaClient> ()
|
||||
try
|
||||
let deleteRequest = Queries.delete [| tuple |]
|
||||
do logger.LogInformation ("Delete req: {Request}", deleteRequest.ToJson ())
|
||||
let! resp = fga.Write deleteRequest |> Async.AwaitTask
|
||||
let deleteRequest = Queries.deleteTuples [| tuple |]
|
||||
let json = JsonSerializer.Serialize deleteRequest
|
||||
do logger.LogInformation ("Delete req: {Request}", json)
|
||||
let! resp = fga.DeleteTuples deleteRequest |> Async.AwaitTask
|
||||
do logger.LogInformation ("Delete resp: {Response}", resp.ToJson ())
|
||||
return Ok (resp.Deletes.Count >= 1)
|
||||
with e ->
|
||||
@@ -329,4 +413,4 @@ module OpenFGA =
|
||||
Remoting.createApi ()
|
||||
|> Remoting.withRouteBuilder Remoting.routeBuilder
|
||||
|> Remoting.fromContext impl
|
||||
|> Remoting.buildHttpHandler
|
||||
|> Remoting.buildHttpHandler
|
||||
@@ -155,7 +155,11 @@ let webApp: HttpHandler =
|
||||
>=> choose [
|
||||
Auth.endpoints
|
||||
|
||||
authorize >=> choose [ Acl.endpoints; Admin.endpoints; OpenFGA.endpoints ]
|
||||
authorize >=> choose [
|
||||
Acl.endpoints
|
||||
Admin.endpoints
|
||||
OpenFGA.endpoints
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -203,8 +207,16 @@ let configureApp (app: IApplicationBuilder) =
|
||||
.UseStaticFiles()
|
||||
.UseGiraffe webApp
|
||||
|
||||
let private getIp (uri: Uri) =
|
||||
uri.DnsSafeHost
|
||||
|> Net.Dns.GetHostAddresses
|
||||
|> Array.head
|
||||
|
||||
let configureServices (settings: Settings) (services: IServiceCollection) =
|
||||
let authSettings = settings.Auth
|
||||
let uri = Uri settings.Fga.apiUrl
|
||||
let ip = getIp uri
|
||||
eprintfn "OpenFGA uri is %s (%s)" uri.DnsSafeHost (string ip)
|
||||
let fga: OpenFgaClient = Fga.newFgaClient settings.Fga
|
||||
let archmaesterDatasource = Archmaester.getDataSource settings.DbConnectionString
|
||||
do Oceanbox.DataAgent.Dapper.register ()
|
||||
@@ -293,4 +305,4 @@ let main args =
|
||||
|
||||
return 0
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -14,7 +14,7 @@ let
|
||||
in
|
||||
buildDotnetModule {
|
||||
pname = name;
|
||||
version = "0.0.0-alpha.1";
|
||||
version = "0.0.1";
|
||||
|
||||
inherit dotnet-sdk dotnet-runtime;
|
||||
|
||||
|
||||
@@ -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, )",
|
||||
|
||||
@@ -122,6 +122,16 @@ module Remoting =
|
||||
EndTime = System.DateTime.Now
|
||||
}
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
type ArchiveRelation =
|
||||
| ViewTerm of ViewTerm
|
||||
| ExecTicket of ExecTicket
|
||||
with
|
||||
static member ConditionName (permission: ArchiveRelation) =
|
||||
match permission with
|
||||
| ViewTerm _ -> "term"
|
||||
| ExecTicket _ -> "ticket"
|
||||
|
||||
[<Struct>]
|
||||
type AddArchiveGroupsRequest = {
|
||||
Id: Archmaester.Dto.ArchiveId
|
||||
@@ -130,28 +140,102 @@ module Remoting =
|
||||
Exec: ExecTicket option
|
||||
}
|
||||
|
||||
[<Struct>]
|
||||
type DataSet = {
|
||||
Id: int
|
||||
BasePath: string
|
||||
ModelAreaId: System.Guid
|
||||
ModelAreaName: string
|
||||
StartTime: System.DateTime
|
||||
EndTime: System.DateTime
|
||||
}
|
||||
|
||||
[<Struct>]
|
||||
type Archive = {
|
||||
Id: System.Guid
|
||||
Name: string
|
||||
DataSetId: int
|
||||
StartTime: System.DateTime
|
||||
Frames: int
|
||||
Published: bool
|
||||
Public: bool
|
||||
}
|
||||
|
||||
[<Struct>]
|
||||
type AddArchiveRequest = {
|
||||
Name: string
|
||||
DataSetId: int
|
||||
StartTime: System.DateTime
|
||||
Frames: int
|
||||
Published: bool
|
||||
Public: bool
|
||||
} with
|
||||
static member empty () = {
|
||||
Name = ""
|
||||
DataSetId = -1
|
||||
StartTime = System.DateTime.Now.AddDays(-14.)
|
||||
Frames = 48
|
||||
Published = true
|
||||
Public = false
|
||||
}
|
||||
|
||||
[<Struct>]
|
||||
type EditArchiveRequest = {
|
||||
Name: string
|
||||
StartTime: System.DateTime
|
||||
Frames: int
|
||||
Published: bool
|
||||
Public: bool
|
||||
}
|
||||
|
||||
[<Struct>]
|
||||
type AddUsersRequest = {
|
||||
Group: string
|
||||
Users: string array
|
||||
}
|
||||
|
||||
[<Struct>]
|
||||
type AddGroupPermissionsRequest = {
|
||||
Group: string
|
||||
ArchiveId: System.Guid
|
||||
Permissions: ArchiveRelation array
|
||||
}
|
||||
|
||||
[<Struct>]
|
||||
type Permission = {
|
||||
Name: string
|
||||
Enabled: bool
|
||||
}
|
||||
|
||||
[<Struct>]
|
||||
type UserPermissionRequest = {
|
||||
User: string
|
||||
Permissions: Permission array
|
||||
}
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module Api =
|
||||
type Auth = { IsAuthenticated: Async<bool> }
|
||||
|
||||
type Admin = {
|
||||
addUsers: AddUsersRequest -> Async<Result<unit, string>>
|
||||
addArchive: AddArchiveRequest -> Async<Result<Archive, string>>
|
||||
addArchiveGroups: AddArchiveGroupsRequest -> Async<Result<unit, string>>
|
||||
addGroupPermissions: AddGroupPermissionsRequest -> Async<Result<unit, string>>
|
||||
addUsers: AddUsersRequest -> Async<Result<unit, string>>
|
||||
deleteArchive: Archmaester.Dto.ArchiveId -> Async<Result<bool, string>>
|
||||
getAllGroups: Async<string array>
|
||||
getArchive: Archmaester.Dto.ArchiveId -> Async<Result<Archmaester.Dto.ArchiveProps, string>>
|
||||
getArchiveCount: Archmaester.Dto.ArchiveFilter -> Async<Result<int, string>>
|
||||
getArchiveDataSet: System.Guid -> Async<Result<DataSet, string>>
|
||||
getArchiveRefs: Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
|
||||
getArchiveTypes: unit -> Async<Archmaester.Dto.ArchiveType array>
|
||||
getArchives: int -> int -> Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
|
||||
getDataSets: unit -> Async<Result<DataSet array, string>>
|
||||
getGroupUsers: string -> Async<string array>
|
||||
removeUsers: string array -> Async<Result<unit, string>>
|
||||
setUserPermissions: UserPermissionRequest -> Async<Result<unit, string>>
|
||||
updateArchive: System.Guid -> EditArchiveRequest -> Async<Result<Archive, string>>
|
||||
updateGroupPermissions: AddGroupPermissionsRequest -> Async<Result<unit, string>>
|
||||
}
|
||||
|
||||
type OpenFGA = {
|
||||
@@ -160,4 +244,4 @@ module Remoting =
|
||||
ListObjects: ListObjectsRequest -> Async<Result<string array, string>>
|
||||
ListUsers: ListUsersRequest -> Async<Result<string array, string>>
|
||||
Read: ReadRequest -> Async<Result<ReadResponse, string>>
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user