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
|
indent_size = 2
|
||||||
max_line_length = 80
|
max_line_length = 80
|
||||||
|
|
||||||
|
[*.yaml]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
[*.fs]
|
[*.fs]
|
||||||
max_line_length= 120
|
max_line_length= 120
|
||||||
|
|
||||||
|
|||||||
37
.envrc
37
.envrc
@@ -1,4 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
export NPINS_DIRECTORY="nix"
|
||||||
|
export APP_ENV=$USER
|
||||||
|
|
||||||
# the shebang is ignored, but nice for editors
|
# the shebang is ignored, but nice for editors
|
||||||
watch_file nix/sources.json
|
watch_file nix/sources.json
|
||||||
|
|
||||||
@@ -9,35 +13,4 @@ dotenv_if_exists
|
|||||||
use nix
|
use nix
|
||||||
|
|
||||||
# HACK: Workaround for direnv bug
|
# HACK: Workaround for direnv bug
|
||||||
unset TMP TMPDIR TEMP TEMPDIR
|
unset TMP TMPDIR TEMP TEMPDIR
|
||||||
|
|
||||||
export NPINS_DIRECTORY="nix"
|
|
||||||
|
|
||||||
# HACK: Configure Rider to use the correct .NET paths from an ambient .NET
|
|
||||||
use_rider_dotnet() {
|
|
||||||
# Get paths
|
|
||||||
DOTNET_PATH=$(readlink "$(which dotnet)")
|
|
||||||
SETTINGS_FILE=$(find . -maxdepth 1 -type f -name '*.sln.DotSettings.user')
|
|
||||||
MSBUILD=$(realpath "$(find "$(dirname "$DOTNET_PATH")/../share/dotnet/sdk" -maxdepth 2 -type f -name MSBuild.dll)")
|
|
||||||
|
|
||||||
# Update Rider settings if they exist
|
|
||||||
if [ -f "$SETTINGS_FILE" ] ; then
|
|
||||||
xmlstarlet ed --inplace \
|
|
||||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
|
||||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
|
||||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
|
||||||
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
|
|
||||||
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" \
|
|
||||||
--value "$(realpath "$(dirname "$DOTNET_PATH")/../share/dotnet/dotnet")" \
|
|
||||||
"$SETTINGS_FILE"
|
|
||||||
|
|
||||||
xmlstarlet ed --inplace \
|
|
||||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
|
||||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
|
||||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
|
||||||
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
|
|
||||||
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" \
|
|
||||||
--value "$MSBUILD" \
|
|
||||||
"$SETTINGS_FILE"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
@@ -12,8 +12,6 @@ include:
|
|||||||
file: template/Base.gitlab-ci.yml
|
file: template/Base.gitlab-ci.yml
|
||||||
- local: "/src/Atlantis/.gitlab-ci.yml"
|
- local: "/src/Atlantis/.gitlab-ci.yml"
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
||||||
when: never
|
|
||||||
- changes:
|
- changes:
|
||||||
- "src/Atlantis/**/*"
|
- "src/Atlantis/**/*"
|
||||||
- "nix/packages/atlantis.nix"
|
- "nix/packages/atlantis.nix"
|
||||||
@@ -21,44 +19,32 @@ include:
|
|||||||
- "nix/containers.nix"
|
- "nix/containers.nix"
|
||||||
- local: "/src/Sorcerer/.gitlab-ci.yml"
|
- local: "/src/Sorcerer/.gitlab-ci.yml"
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
||||||
when: never
|
|
||||||
- changes:
|
- changes:
|
||||||
- "src/Sorcerer/**/*"
|
- "src/Sorcerer/**/*"
|
||||||
- "nix/packages/sorcerer.nix"
|
- "nix/packages/sorcerer.nix"
|
||||||
- "nix/containers.nix"
|
- "nix/containers.nix"
|
||||||
- local: "/src/Archivist/.gitlab-ci.yml"
|
- local: "/src/Archivist/.gitlab-ci.yml"
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
||||||
when: never
|
|
||||||
- changes:
|
- changes:
|
||||||
- "src/Archivist/**/*"
|
- "src/Archivist/**/*"
|
||||||
- "nix/packages/archivist.nix"
|
- "nix/packages/archivist.nix"
|
||||||
- local: "/src/Interfaces/.gitlab-ci.yml"
|
- local: "/src/Interfaces/.gitlab-ci.yml"
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
||||||
when: never
|
|
||||||
- changes:
|
- changes:
|
||||||
- "src/Interfaces/**/*"
|
- "src/Interfaces/**/*"
|
||||||
- "nix/packages/api.nix"
|
- "nix/packages/api.nix"
|
||||||
- local: "/src/DataAgent/.gitlab-ci.yml"
|
- local: "/src/DataAgent/.gitlab-ci.yml"
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
||||||
when: never
|
|
||||||
- changes:
|
- changes:
|
||||||
- "src/DataAgent/**/*"
|
- "src/DataAgent/**/*"
|
||||||
- "nix/packages/dataagent.nix"
|
- "nix/packages/dataagent.nix"
|
||||||
- local: "/src/ServerPack/.gitlab-ci.yml"
|
- local: "/src/ServerPack/.gitlab-ci.yml"
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
||||||
when: never
|
|
||||||
- changes:
|
- changes:
|
||||||
- "src/ServerPack/**/*"
|
- "src/ServerPack/**/*"
|
||||||
- "nix/packages/serverpack.nix"
|
- "nix/packages/serverpack.nix"
|
||||||
- local: "/src/Codex/.gitlab-ci.yml"
|
- local: "/src/Codex/.gitlab-ci.yml"
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
||||||
when: never
|
|
||||||
- changes:
|
- changes:
|
||||||
- "src/Codex/**/*"
|
- "src/Codex/**/*"
|
||||||
- "nix/packages/node-modules.nix"
|
- "nix/packages/node-modules.nix"
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<PackageVersion Include="Matplotlib.ColorMaps" Version="3.0.1" />
|
<PackageVersion Include="Matplotlib.ColorMaps" Version="3.0.1" />
|
||||||
<PackageVersion Include="Thoth.Fetch" Version="3.0.1" />
|
<PackageVersion Include="Thoth.Fetch" Version="3.0.1" />
|
||||||
<PackageVersion Include="Thoth.Json" Version="10.4.1"/>
|
<PackageVersion Include="Thoth.Json" Version="10.4.1"/>
|
||||||
|
<PackageVersion Include="FS.FluentUI" Version="3.0.0"/>
|
||||||
<!-- Serverpack -->
|
<!-- Serverpack -->
|
||||||
<PackageVersion Include="OpenFga.Sdk" Version="0.7.0"/>
|
<PackageVersion Include="OpenFga.Sdk" Version="0.7.0"/>
|
||||||
<PackageVersion Include="FSharp.SystemTextJson" Version="1.3.13"/>
|
<PackageVersion Include="FSharp.SystemTextJson" Version="1.3.13"/>
|
||||||
|
|||||||
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>.
|
Required helm manifests are hosted in a separate repository: <https://gitlab.com/oceanbox/manifests>.
|
||||||
Clone it into a directory _in the same parent directory as this repository._
|
Clone it into a directory _in the same parent directory as this repository._
|
||||||
|
|
||||||
The Bitnami respository must also be added to helm:
|
You'll have to run `helm dependency update` in the atlantis directory within the manifest repo to download the charts.
|
||||||
|
|
||||||
```shell
|
|
||||||
helm repo add bitnami https://charts.bitnami.com/bitnami
|
|
||||||
```
|
|
||||||
|
|
||||||
### DNS
|
|
||||||
|
|
||||||
Some DNS masking is required. Add the following to your NixOS configuration:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
services.dnsmasq = {
|
|
||||||
enable = true;
|
|
||||||
settings.address = [
|
|
||||||
"/.local/127.0.0.1"
|
|
||||||
"/.local.oceanbox.io/127.0.0.1"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### NuGet
|
### NuGet
|
||||||
|
|
||||||
@@ -102,14 +84,30 @@ To retrieve packages from the private Oceanbox nuget registry, configure it with
|
|||||||
</packageSources>
|
</packageSources>
|
||||||
<packageSourceCredentials>
|
<packageSourceCredentials>
|
||||||
<oceanbox>
|
<oceanbox>
|
||||||
<add key="Username" value="oceanbox-nuget" />
|
<add key="Username" value="<Your-GitLab-Username>" />
|
||||||
<add key="ClearTextPassword" value="<...>" />
|
<add key="ClearTextPassword" value="<Your-GitLab-PAT>" />
|
||||||
</oceanbox>
|
</oceanbox>
|
||||||
</packageSourceCredentials>
|
</packageSourceCredentials>
|
||||||
|
<packageSourceMapping>
|
||||||
|
<packageSource key="nuget.org">
|
||||||
|
<package pattern="*" />
|
||||||
|
</packageSource>
|
||||||
|
<packageSource key="oceanbox">
|
||||||
|
<package pattern="Oceanbox.*" />
|
||||||
|
<package pattern="ProjNet.FSharp" />
|
||||||
|
<package pattern="Drifters.Api" />
|
||||||
|
<package pattern="Fable.Lit" />
|
||||||
|
<package pattern="Fable.Lit.*" />
|
||||||
|
<package pattern="Fable.SignalR" />
|
||||||
|
<package pattern="Fable.SignalR.*" />
|
||||||
|
<package pattern="Fable.OpenLayers" />
|
||||||
|
<package pattern="Matplotlib.*" />
|
||||||
|
</packageSource>
|
||||||
|
</packageSourceMapping>
|
||||||
</configuration>
|
</configuration>
|
||||||
```
|
```
|
||||||
|
|
||||||
Substitute `<...>` for the corresponding secret.
|
Substitute with your own gitlab username and PAT in the credentials.
|
||||||
|
|
||||||
Now, we should be able to `restore`:
|
Now, we should be able to `restore`:
|
||||||
|
|
||||||
@@ -168,7 +166,7 @@ You should now be able to access the Atlantis client (with HMR) on <atlantis.loc
|
|||||||
### Trust Root Certificate
|
### Trust Root Certificate
|
||||||
|
|
||||||
> [!note]
|
> [!note]
|
||||||
> You'll need to run `dotnet run bundle` in `src/Atlantis` to generate the `/certs` directory
|
> You'll need to run `just run-client` in `src/Atlantis` to generate the certificates in `~/.vite-plugin-mkcert/certs`
|
||||||
|
|
||||||
In order for your browser to allow you to access the web application, you must add the root certificate generated by `mkcert` to the list of trusted authorities in your browser:
|
In order for your browser to allow you to access the web application, you must add the root certificate generated by `mkcert` to the list of trusted authorities in your browser:
|
||||||
|
|
||||||
@@ -179,9 +177,9 @@ In order for your browser to allow you to access the web application, you must a
|
|||||||
|
|
||||||
### Add `user` to OpenFGA
|
### Add `user` to OpenFGA
|
||||||
|
|
||||||
Ask [sales](moritz.jorg@oceanbox.io) to add your `azure-ad-user` to OpenFGA.
|
Ask [sales](support@oceanbox.io) to add your `azure-ad-user` to OpenFGA.
|
||||||
|
|
||||||
### CORS for Sorcerer
|
### CORS for Sorcerer
|
||||||
|
|
||||||
Add the `url` of your instance to the CORS list of Sorcerer
|
Add the `url` of your instance to the CORS list of Sorcerer
|
||||||
[here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52).
|
[here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52).
|
||||||
132
RELEASE_NOTES.md
132
RELEASE_NOTES.md
@@ -1,5 +1,137 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.40.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.4...v1.40.5) (2026-01-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **xtractor:** Reduce to 4 cores per task ([8e824d4](https://gitlab.com/oceanbox/Poseidon/commit/8e824d4afa0b03f59e006d3a0d50fb216e71483e))
|
||||||
|
|
||||||
|
## [1.40.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.3...v1.40.4) (2026-01-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **xtractor:** Reduce core requirement to 8 ([efacb2a](https://gitlab.com/oceanbox/Poseidon/commit/efacb2a3322de0ced45db4eec240846f4e371a75))
|
||||||
|
|
||||||
|
## [1.40.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.2...v1.40.3) (2026-01-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **inbox|xtracto:** Delete/Read msg and allow non-ascii xtractor names ([d8d5e07](https://gitlab.com/oceanbox/Poseidon/commit/d8d5e076baf8b559200f2da91237f9874678b216))
|
||||||
|
* **multiauth:** Add clientId to redirect on signout ([54c40d7](https://gitlab.com/oceanbox/Poseidon/commit/54c40d7accc4bbc43f66dda0df647ccac482a2b0))
|
||||||
|
|
||||||
|
## [1.40.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.1...v1.40.2) (2026-01-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **xtract:** Disabled if not allowed to simulate transport ([e429a85](https://gitlab.com/oceanbox/Poseidon/commit/e429a855e5bd00493e2f99647092aebce9c99a2a))
|
||||||
|
|
||||||
|
## [1.40.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.40.0...v1.40.1) (2026-01-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix tilt build on net10 ([d5cde19](https://gitlab.com/oceanbox/Poseidon/commit/d5cde19250847f7b091cfa5f65eb703405c202b6))
|
||||||
|
|
||||||
|
# [1.40.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.2...v1.40.0) (2026-01-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Codex:** expose days instead of frames in arcive form ([6cf5262](https://gitlab.com/oceanbox/Poseidon/commit/6cf5262dd5c98517a3c767f410c858fe32c07bd5))
|
||||||
|
* **Codex:** only allow inbounds time intervals on edit archive ([eaea4b2](https://gitlab.com/oceanbox/Poseidon/commit/eaea4b2e215669cec19f2a8cec122ba670a7a202))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Codex:** edit archives ([ec10932](https://gitlab.com/oceanbox/Poseidon/commit/ec109328fbf5f237a52ef77cbd44dff571deee5f))
|
||||||
|
|
||||||
|
## [1.39.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.1...v1.39.2) (2026-01-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix net10 issues ([f4943a1](https://gitlab.com/oceanbox/Poseidon/commit/f4943a148b72fb7e10a745cc3e806b9c4bdd76d8))
|
||||||
|
|
||||||
|
## [1.39.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.39.0...v1.39.1) (2026-01-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ci:** Remove schedule check ([ab37e88](https://gitlab.com/oceanbox/Poseidon/commit/ab37e88bb0f669a7aa94bf831f95f8c60dc28804))
|
||||||
|
|
||||||
|
# [1.39.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.5...v1.39.0) (2026-01-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Codex:** add * user in archmeister on public archives ([cd678a4](https://gitlab.com/oceanbox/Poseidon/commit/cd678a41f64a64c6f3616f32bafeaae6715c08a4))
|
||||||
|
* **Codex:** use feliz router guid matching ([7182f7c](https://gitlab.com/oceanbox/Poseidon/commit/7182f7c9f094d65c884e8e02d4aaa89561ca5e82))
|
||||||
|
* **Codex:** utc start_time ([492651e](https://gitlab.com/oceanbox/Poseidon/commit/492651e0f34035d8c61174aa50336222bcfd979c))
|
||||||
|
* **DataAgent:** use files from parent attribs instead of archive_files ([eac23e7](https://gitlab.com/oceanbox/Poseidon/commit/eac23e7d1a541f2e90374a2add689846d9e7b642))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Codex:** ability to add FVCOM archives ([d86db7a](https://gitlab.com/oceanbox/Poseidon/commit/d86db7a66ca5ecb6a9ad45ce3d47be3a98d56bb8))
|
||||||
|
|
||||||
|
## [1.38.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.4...v1.38.5) (2026-01-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Archmaester:** Rollback add archive if openfga fails ([46e86eb](https://gitlab.com/oceanbox/Poseidon/commit/46e86eb5f961a45fba2da1525c1472bdca79ab47))
|
||||||
|
|
||||||
|
## [1.38.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.3...v1.38.4) (2026-01-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **nix:** Bump node deps ([e513d87](https://gitlab.com/oceanbox/Poseidon/commit/e513d87d249843423f0e0a62275afe45e6c73a46))
|
||||||
|
|
||||||
|
## [1.38.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.2...v1.38.3) (2026-01-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **xtractor:** Set maxEndDate to 1 year and format ([5315a05](https://gitlab.com/oceanbox/Poseidon/commit/5315a05fa255c6164d3ef73c0f5e20cdb4c632d0))
|
||||||
|
|
||||||
|
## [1.38.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.1...v1.38.2) (2026-01-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **atlas:** Disable Snowflakes ([4cd3673](https://gitlab.com/oceanbox/Poseidon/commit/4cd3673d15783afa72ca3358e6bd8c3a8cfbfd16))
|
||||||
|
|
||||||
|
## [1.38.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.38.0...v1.38.1) (2026-01-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **xtractor:** Move to short partition and 16 CPU, also set max duration to 1 year ([56d3476](https://gitlab.com/oceanbox/Poseidon/commit/56d34767d7a2e0bc6aadaa5987344c6e7a58698a))
|
||||||
|
|
||||||
|
# [1.38.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.37.1...v1.38.0) (2026-01-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* client layout ([efb3292](https://gitlab.com/oceanbox/Poseidon/commit/efb3292d9f4a8fccf2cebbdd75b3d3ffc186fa11))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add fluent ui to codex ([d8bf174](https://gitlab.com/oceanbox/Poseidon/commit/d8bf174d3aa181169365c178b4335052e13eabc5))
|
||||||
|
|
||||||
|
## [1.37.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.37.0...v1.37.1) (2026-01-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **xtractor:** Avoid using union types in Dapr Actors ([14c1a57](https://gitlab.com/oceanbox/Poseidon/commit/14c1a57331f981b1e1e0793426448ea261002e6d))
|
||||||
|
|
||||||
# [1.37.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.36.0...v1.37.0) (2025-12-22)
|
# [1.37.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.36.0...v1.37.0) (2025-12-22)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ let
|
|||||||
in
|
in
|
||||||
clean version;
|
clean version;
|
||||||
|
|
||||||
dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0;
|
dotnet-sdk = pkgs.dotnetCorePackages.sdk_10_0;
|
||||||
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_9_0;
|
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_10_0;
|
||||||
deps = nix-utils.output.lib.nuget.deps;
|
deps = nix-utils.output.lib.nuget.deps;
|
||||||
|
|
||||||
# Usage: export NETRC="$(agenix -d netrc.age)" in `./nix/secrets`
|
# Usage: export NETRC="$(agenix -d netrc.age)" in `./nix/secrets`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "9.0.0",
|
"version": "10.0.100",
|
||||||
"rollForward": "latestMinor"
|
"rollForward": "latestMinor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ buildDotnetModule rec {
|
|||||||
;
|
;
|
||||||
name = "Archivist";
|
name = "Archivist";
|
||||||
pname = name;
|
pname = name;
|
||||||
dotnet-runtime = pkgs.dotnetCorePackages.runtime_9_0;
|
dotnet-runtime = pkgs.dotnetCorePackages.runtime_10_0;
|
||||||
dotnetRestoreFlags = "--force-evaluate";
|
dotnetRestoreFlags = "--force-evaluate";
|
||||||
nugetDeps = deps {
|
nugetDeps = deps {
|
||||||
inherit
|
inherit
|
||||||
|
|||||||
@@ -48,5 +48,5 @@ stdenvNoCC.mkDerivation {
|
|||||||
outputHashMode = "recursive";
|
outputHashMode = "recursive";
|
||||||
outputHashAlgo = "sha256";
|
outputHashAlgo = "sha256";
|
||||||
# NOTE: Empty this when a new dependency is added
|
# NOTE: Empty this when a new dependency is added
|
||||||
outputHash = "sha256-9XLCFORr+StPsCLdanXATD+vmIOptvy3Xhr7O34qzZc=";
|
outputHash = "sha256-bbCaGoZRE7vRuAS3eRyP8yHANYXBJVaHmuL99BAovjY=";
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,13 @@
|
|||||||
"vite-plugin-mkcert": "^1.17.8"
|
"vite-plugin-mkcert": "^1.17.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.72.2",
|
"@fluentui/react-components": "^9.72.9",
|
||||||
"@fluentui/react-datepicker-compat": "^0.6.20",
|
"@fluentui/react-datepicker-compat": "^0.6.20",
|
||||||
|
"@fluentui/react-calendar-compat": "^0.3.15",
|
||||||
|
"@fluentui/react-timepicker-compat": "^0.4.26",
|
||||||
|
"@fluentui-contrib/react-data-grid-react-window": "^1.4.2",
|
||||||
|
"@fluentui/react-icons": "^2.0.316",
|
||||||
|
"react-window": "^2.2.3",
|
||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@lit/context": "^1.1.6",
|
"@lit/context": "^1.1.6",
|
||||||
"@microsoft/signalr": "^8.0.17",
|
"@microsoft/signalr": "^8.0.17",
|
||||||
|
|||||||
101
scripts/update-rider.sh
Executable file
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" { };
|
agenix = pkgs.callPackage "${sources.agenix}/pkgs/agenix.nix" { };
|
||||||
fable = pkgs.buildDotnetGlobalTool {
|
fable = pkgs.buildDotnetGlobalTool {
|
||||||
pname = "fable";
|
pname = "fable";
|
||||||
version = "4.28.0";
|
version = "4.24.0";
|
||||||
nugetHash = "sha256-t5Kex6sVe1B/xErMfDav+WGEjeZjndRNQA2r0FvL92g=";
|
nugetHash = "sha256-ERewWqfEyyZKpHFFALpMGJT0fDWywBYY5buU/wTZZTg=";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
pkgs.mkShellNoCC {
|
pkgs.mkShellNoCC {
|
||||||
@@ -24,7 +24,7 @@ pkgs.mkShellNoCC {
|
|||||||
|
|
||||||
# JavaScript
|
# JavaScript
|
||||||
pkgs.bun
|
pkgs.bun
|
||||||
pkgs.nodejs
|
pkgs.nodejs_25
|
||||||
|
|
||||||
# Devlopment tools
|
# Devlopment tools
|
||||||
pkgs.npins
|
pkgs.npins
|
||||||
@@ -32,6 +32,7 @@ pkgs.mkShellNoCC {
|
|||||||
pkgs.dive
|
pkgs.dive
|
||||||
pkgs.nix-output-monitor
|
pkgs.nix-output-monitor
|
||||||
pkgs.just
|
pkgs.just
|
||||||
|
pkgs.skopeo
|
||||||
|
|
||||||
# Secret management with agenix
|
# Secret management with agenix
|
||||||
agenix
|
agenix
|
||||||
@@ -47,6 +48,10 @@ pkgs.mkShellNoCC {
|
|||||||
DOTNET_ROOT = "${dotnet-sdk}/share/dotnet";
|
DOTNET_ROOT = "${dotnet-sdk}/share/dotnet";
|
||||||
LOG_LEVEL = "verbose";
|
LOG_LEVEL = "verbose";
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
scripts/update-rider.sh ${dotnet-sdk}/bin/dotnet
|
||||||
|
'';
|
||||||
|
|
||||||
# Alternative shells
|
# Alternative shells
|
||||||
passthru = pkgs.lib.mapAttrs (name: value: pkgs.mkShellNoCC (value // { inherit name; })) {
|
passthru = pkgs.lib.mapAttrs (name: value: pkgs.mkShellNoCC (value // { inherit name; })) {
|
||||||
pre-commit.shellHook = pre-commit.shellHook;
|
pre-commit.shellHook = pre-commit.shellHook;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ variables:
|
|||||||
|
|
||||||
include:
|
include:
|
||||||
- project: oceanbox/gitlab-ci
|
- project: oceanbox/gitlab-ci
|
||||||
ref: v4.4
|
ref: v4.5
|
||||||
file: DotnetDeployment.gitlab-ci.yml
|
file: DotnetDeployment.gitlab-ci.yml
|
||||||
inputs:
|
inputs:
|
||||||
project-name: archivist
|
project-name: archivist
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ pkgs.mkShellNoCC {
|
|||||||
SERVER_PORT = port + 85;
|
SERVER_PORT = port + 85;
|
||||||
TILT_PORT = port + 50;
|
TILT_PORT = port + 50;
|
||||||
|
|
||||||
DOTNET_ROOT = "${pkgs.dotnetCorePackages.sdk_9_0}/share/dotnet";
|
DOTNET_ROOT = "${pkgs.dotnetCorePackages.sdk_10_0}/share/dotnet";
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export PATH="$PWD/src/Cli/bin/Release/net9.0/linux-x64/:$PATH"
|
export PATH="$PWD/src/Cli/bin/Release/net9.0/linux-x64/:$PATH"
|
||||||
|
|||||||
@@ -631,7 +631,6 @@ let instantiateArchiveDto (idx, modelArea, basePath, files, reverse, json, publi
|
|||||||
}
|
}
|
||||||
|
|
||||||
let retireArchive (archive: string) =
|
let retireArchive (archive: string) =
|
||||||
// TODO: retire all dependent archies
|
|
||||||
let aid =
|
let aid =
|
||||||
try
|
try
|
||||||
Guid.Parse archive
|
Guid.Parse archive
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||||
<AssemblyName>archivist</AssemblyName>
|
<AssemblyName>archivist</AssemblyName>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"Fargo.CmdLine": {
|
"Fargo.CmdLine": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[1.7.5, )",
|
"requested": "[1.7.5, )",
|
||||||
@@ -71,8 +71,7 @@
|
|||||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyModel": "9.0.1",
|
"Microsoft.Extensions.DependencyModel": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging": "9.0.1",
|
"Microsoft.Extensions.Logging": "9.0.1",
|
||||||
"Mono.TextTemplating": "3.0.0",
|
"Mono.TextTemplating": "3.0.0"
|
||||||
"System.Text.Json": "9.0.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.EntityFrameworkCore.Tools": {
|
"Microsoft.EntityFrameworkCore.Tools": {
|
||||||
@@ -233,8 +232,7 @@
|
|||||||
"resolved": "5.3.2",
|
"resolved": "5.3.2",
|
||||||
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"FSharp.Core": "4.3.2",
|
"FSharp.Core": "4.3.2"
|
||||||
"System.Reflection.Emit.Lightweight": "4.3.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Google.Api.CommonProtos": {
|
"Google.Api.CommonProtos": {
|
||||||
@@ -331,10 +329,7 @@
|
|||||||
"resolved": "4.8.0",
|
"resolved": "4.8.0",
|
||||||
"contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
|
"contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
|
"Microsoft.CodeAnalysis.Analyzers": "3.3.4"
|
||||||
"System.Collections.Immutable": "7.0.0",
|
|
||||||
"System.Reflection.Metadata": "7.0.0",
|
|
||||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.CodeAnalysis.CSharp": {
|
"Microsoft.CodeAnalysis.CSharp": {
|
||||||
@@ -364,9 +359,7 @@
|
|||||||
"Humanizer.Core": "2.14.1",
|
"Humanizer.Core": "2.14.1",
|
||||||
"Microsoft.Bcl.AsyncInterfaces": "7.0.0",
|
"Microsoft.Bcl.AsyncInterfaces": "7.0.0",
|
||||||
"Microsoft.CodeAnalysis.Common": "[4.8.0]",
|
"Microsoft.CodeAnalysis.Common": "[4.8.0]",
|
||||||
"System.Composition": "7.0.0",
|
"System.Composition": "7.0.0"
|
||||||
"System.IO.Pipelines": "7.0.0",
|
|
||||||
"System.Threading.Channels": "7.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.CodeAnalysis.Workspaces.MSBuild": {
|
"Microsoft.CodeAnalysis.Workspaces.MSBuild": {
|
||||||
@@ -376,8 +369,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Build.Framework": "16.10.0",
|
"Microsoft.Build.Framework": "16.10.0",
|
||||||
"Microsoft.CodeAnalysis.Common": "[4.8.0]",
|
"Microsoft.CodeAnalysis.Common": "[4.8.0]",
|
||||||
"Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]",
|
"Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]"
|
||||||
"System.Text.Json": "7.0.3"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions": {
|
"Microsoft.EntityFrameworkCore.Abstractions": {
|
||||||
@@ -534,16 +526,6 @@
|
|||||||
"resolved": "17.11.4",
|
"resolved": "17.11.4",
|
||||||
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
||||||
},
|
},
|
||||||
"Microsoft.NETCore.Platforms": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.1.0",
|
|
||||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
|
||||||
},
|
|
||||||
"Microsoft.NETCore.Targets": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.1.0",
|
|
||||||
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
|
|
||||||
},
|
|
||||||
"Mono.TextTemplating": {
|
"Mono.TextTemplating": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0",
|
"resolved": "3.0.0",
|
||||||
@@ -563,11 +545,7 @@
|
|||||||
"ProjNET": {
|
"ProjNET": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.0.0",
|
"resolved": "2.0.0",
|
||||||
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
|
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
|
||||||
"dependencies": {
|
|
||||||
"System.Memory": "4.5.3",
|
|
||||||
"System.Numerics.Vectors": "4.5.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Serilog.Sinks.File": {
|
"Serilog.Sinks.File": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -591,11 +569,6 @@
|
|||||||
"resolved": "6.0.0",
|
"resolved": "6.0.0",
|
||||||
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
|
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
|
||||||
},
|
},
|
||||||
"System.Collections.Immutable": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "7.0.0",
|
|
||||||
"contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ=="
|
|
||||||
},
|
|
||||||
"System.Composition": {
|
"System.Composition": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "7.0.0",
|
||||||
@@ -644,128 +617,6 @@
|
|||||||
"System.Composition.Runtime": "7.0.0"
|
"System.Composition.Runtime": "7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.IO": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"System.Text.Encoding": "4.3.0",
|
|
||||||
"System.Threading.Tasks": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.IO.Pipelines": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "7.0.0",
|
|
||||||
"contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg=="
|
|
||||||
},
|
|
||||||
"System.Memory": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.4",
|
|
||||||
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw=="
|
|
||||||
},
|
|
||||||
"System.Numerics.Vectors": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.0",
|
|
||||||
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
|
|
||||||
},
|
|
||||||
"System.Reflection": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.IO": "4.3.0",
|
|
||||||
"System.Reflection.Primitives": "4.3.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection.Emit.ILGeneration": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Reflection": "4.3.0",
|
|
||||||
"System.Reflection.Primitives": "4.3.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection.Emit.Lightweight": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Reflection": "4.3.0",
|
|
||||||
"System.Reflection.Emit.ILGeneration": "4.3.0",
|
|
||||||
"System.Reflection.Primitives": "4.3.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection.Metadata": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "7.0.0",
|
|
||||||
"contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Collections.Immutable": "7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection.Primitives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Runtime": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Runtime.CompilerServices.Unsafe": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "6.0.0",
|
|
||||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
|
||||||
},
|
|
||||||
"System.Text.Encoding": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Text.Json": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "eqWHDZqYPv1PvuvoIIx5pF74plL3iEOZOl/0kQP+Y0TEbtgNnM2W6k8h8EPYs+LTJZsXuWa92n5W5sHTWvE3VA=="
|
|
||||||
},
|
|
||||||
"System.Threading.Channels": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "7.0.0",
|
|
||||||
"contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA=="
|
|
||||||
},
|
|
||||||
"System.Threading.Tasks": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -911,10 +762,7 @@
|
|||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.5.0, )",
|
"requested": "[2.5.0, )",
|
||||||
"resolved": "2.5.0",
|
"resolved": "2.5.0",
|
||||||
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
|
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
|
||||||
"dependencies": {
|
|
||||||
"System.Memory": "4.5.4"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Newtonsoft.Json": {
|
"Newtonsoft.Json": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
@@ -1006,136 +854,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net9.0/linux-x64": {
|
"net10.0/linux-x64": {}
|
||||||
"runtime.any.System.IO": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "SDZ5AD1DtyRoxYtEcqQ3HDlcrorMYXZeCt7ZhG9US9I5Vva+gpIWDGMkcwa5XiKL0ceQKRZIX2x0XEjLX7PDzQ=="
|
|
||||||
},
|
|
||||||
"runtime.any.System.Reflection": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "hLC3A3rI8jipR5d9k7+f0MgRCW6texsAp0MWkN/ci18FMtQ9KH7E2vDn/DH2LkxsszlpJpOn9qy6Z6/69rH6eQ=="
|
|
||||||
},
|
|
||||||
"runtime.any.System.Reflection.Primitives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "Nrm1p3armp6TTf2xuvaa+jGTTmncALWFq22CpmwRvhDf6dE9ZmH40EbOswD4GnFLrMRS0Ki6Kx5aUPmKK/hZBg=="
|
|
||||||
},
|
|
||||||
"runtime.any.System.Runtime": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "fRS7zJgaG9NkifaAxGGclDDoRn9HC7hXACl52Or06a/fxdzDajWb5wov3c6a+gVSlekRoexfjwQSK9sh5um5LQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Private.Uri": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtime.any.System.Text.Encoding": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "+ihI5VaXFCMVPJNstG4O4eo1CfbrByLxRrQQTqOTp1ttK0kUKDqOdBSTaCB2IBk/QtjDrs6+x4xuezyMXdm0HQ=="
|
|
||||||
},
|
|
||||||
"runtime.any.System.Threading.Tasks": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "OhBAVBQG5kFj1S+hCEQ3TUHBAEtZ3fbEMgZMRNdN8A0Pj4x+5nTELEqL59DU0TjKVE6II3dqKw4Dklb3szT65w=="
|
|
||||||
},
|
|
||||||
"runtime.native.System": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtime.unix.System.Private.Uri": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "ooWzobr5RAq34r9uan1r/WPXJYG1XWy9KanrxNvEnBzbFdQbMG7Y3bVi4QxR7xZMNLOxLLTAyXvnSkfj5boZSg==",
|
|
||||||
"dependencies": {
|
|
||||||
"runtime.native.System": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.IO": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"System.Text.Encoding": "4.3.0",
|
|
||||||
"System.Threading.Tasks": "4.3.0",
|
|
||||||
"runtime.any.System.IO": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Private.Uri": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "I4SwANiUGho1esj4V4oSlPllXjzCZDE+5XXso2P03LW2vOda2Enzh8DWOxwN6hnrJyp314c7KuVu31QYhRzOGg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"runtime.unix.System.Private.Uri": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.IO": "4.3.0",
|
|
||||||
"System.Reflection.Primitives": "4.3.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"runtime.any.System.Reflection": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection.Primitives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"runtime.any.System.Reflection.Primitives": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Runtime": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"runtime.any.System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Text.Encoding": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"runtime.any.System.Text.Encoding": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Threading.Tasks": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"runtime.any.System.Threading.Tasks": "4.3.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>7.1.0</Version>
|
<Version>7.1.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>7.1.0</Version>
|
<Version>7.1.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ variables:
|
|||||||
|
|
||||||
include:
|
include:
|
||||||
- project: oceanbox/gitlab-ci
|
- project: oceanbox/gitlab-ci
|
||||||
ref: v4.4
|
ref: v4.5
|
||||||
file: DotnetDeployment.gitlab-ci.yml
|
file: DotnetDeployment.gitlab-ci.yml
|
||||||
inputs:
|
inputs:
|
||||||
project-name: atlantis
|
project-name: atlantis
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0.6
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev
|
&& apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev
|
||||||
@@ -12,4 +12,4 @@ ENV SERVER_CONTENT_ROOT=/app/public
|
|||||||
COPY dist/ /app
|
COPY dist/ /app
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD [ "dotnet", "/app/Server.dll" ]
|
CMD [ "dotnet", "/app/Server.dll" ]
|
||||||
@@ -74,6 +74,10 @@ local_resource(
|
|||||||
ignore=[
|
ignore=[
|
||||||
'src/Server/bin',
|
'src/Server/bin',
|
||||||
'src/Server/obj',
|
'src/Server/obj',
|
||||||
|
'src/Server/Archmaester/obj',
|
||||||
|
'src/Server/Hipster/obj',
|
||||||
|
'src/Server/Petimeter/obj',
|
||||||
|
'src/Server/Common/obj',
|
||||||
'src/Shared/bin',
|
'src/Shared/bin',
|
||||||
'src/Shared/obj',
|
'src/Shared/obj',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ client_path := "src/Client"
|
|||||||
test_path := "test"
|
test_path := "test"
|
||||||
lib_path := "src/Interfaces"
|
lib_path := "src/Interfaces"
|
||||||
|
|
||||||
dist_path := "dist"
|
dist_path := "../../dist"
|
||||||
pack_path := "packages"
|
pack_path := "../../packages"
|
||||||
|
|
||||||
vite_prod := "bunx --bun vite build -c ../../vite.config.js -m production --emptyOutDir --outDir " + "../../dist/public"
|
vite_prod := "bunx --bun vite build -c ../../vite.config.js -m production --emptyOutDir --outDir " + "../../dist/public"
|
||||||
vite_dev := "bunx --bun vite build -c ../../vite.config.js -m development --minify false --sourcemap true --emptyOutDir --outDir " + "../../dist/public"
|
vite_dev := "bunx --bun vite build -c ../../vite.config.js -m development --minify false --sourcemap true --emptyOutDir --outDir " + "../../dist/public"
|
||||||
vite := "vite -c ../../vite.config.js"
|
vite := "bunx vite -c ../../vite.config.js -m development "
|
||||||
|
|
||||||
# Default recipe - show available commands
|
# Default recipe - show available commands
|
||||||
default:
|
default:
|
||||||
@@ -28,23 +28,31 @@ clean:
|
|||||||
[parallel]
|
[parallel]
|
||||||
bundle: clean bundle-server bundle-client
|
bundle: clean bundle-server bundle-client
|
||||||
|
|
||||||
|
[working-directory: 'src/Server']
|
||||||
bundle-server:
|
bundle-server:
|
||||||
dotnet build -tl -c Release -o {{dist_path}} {{server_path}}
|
dotnet build -tl -c Release -o {{dist_path}}
|
||||||
|
|
||||||
bundle-client:
|
[working-directory: 'src/Client']
|
||||||
fable --cwd {{client_path}} -e .jsx -o build --test:MSBuildCracker --run {{vite_prod}}
|
install-client:
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
|
||||||
|
[working-directory: 'src/Client']
|
||||||
|
bundle-client: install-client
|
||||||
|
|
||||||
# Build debug bundle (server + client)
|
# Build debug bundle (server + client)
|
||||||
[parallel]
|
[parallel]
|
||||||
bundle-debug: clean bundle-debug-server bundle-debug-client
|
bundle-debug: clean bundle-debug-server bundle-debug-client
|
||||||
|
|
||||||
|
[working-directory: 'src/Server']
|
||||||
bundle-debug-server:
|
bundle-debug-server:
|
||||||
dotnet build -tl -c Debug -o {{dist_path}} {{server_path}}
|
dotnet build -tl -c Debug -o {{dist_path}}
|
||||||
|
|
||||||
|
[working-directory: 'src/Client']
|
||||||
bundle-debug-client:
|
bundle-debug-client:
|
||||||
fable --cwd {{client_path}} -e .jsx -o build --test:MSBuildCracker --run {{vite_dev}}
|
fable -e .jsx -o build --test:MSBuildCracker --run {{vite_dev}}
|
||||||
|
|
||||||
# Create NuGet package
|
# Create NuGet package
|
||||||
|
[working-directory: 'src/Server']
|
||||||
pack: clean
|
pack: clean
|
||||||
dotnet pack -c Release -o "{{pack_path}}" {{lib_path}}
|
dotnet pack -c Release -o "{{pack_path}}" {{lib_path}}
|
||||||
|
|
||||||
@@ -52,12 +60,14 @@ pack: clean
|
|||||||
[parallel]
|
[parallel]
|
||||||
run: clean run-server run-client
|
run: clean run-server run-client
|
||||||
|
|
||||||
|
[working-directory: 'src/Server']
|
||||||
run-server:
|
run-server:
|
||||||
dotnet watch run {{server_path}}
|
dotnet watch run
|
||||||
|
|
||||||
# Run client only in watch mode
|
# Run client only in watch mode
|
||||||
run-client:
|
[working-directory: 'src/Client']
|
||||||
fable watch --cwd {{client_path}} -e .jsx -o build --run {{vite}}
|
run-client: install-client
|
||||||
|
fable watch -e .jsx -o build --test:MSBuildCracker --run {{vite}}
|
||||||
|
|
||||||
# Format code with Fantomas
|
# Format code with Fantomas
|
||||||
format:
|
format:
|
||||||
@@ -67,8 +77,10 @@ format:
|
|||||||
[parallel]
|
[parallel]
|
||||||
test: clean test-server test-client
|
test: clean test-server test-client
|
||||||
|
|
||||||
|
[working-directory: 'src']
|
||||||
test-server:
|
test-server:
|
||||||
dotnet run {{test_path}}/Server
|
dotnet run {{test_path}}/Server
|
||||||
|
|
||||||
test-client:
|
[working-directory: 'src/Client']
|
||||||
fable --cwd {{test_path}}/Client -e .jsx -o build --run {{vite}}
|
test-client: install-client
|
||||||
|
fable -e .jsx -o build --run {{vite}}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<Version>2.87.0</Version>
|
<Version>2.87.0</Version>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"Fable.Browser.IndexedDB": {
|
"Fable.Browser.IndexedDB": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.2.0, )",
|
"requested": "[2.2.0, )",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>6.20.0</Version>
|
<Version>6.20.0</Version>
|
||||||
<RootNamespace>Archivist</RootNamespace>
|
<RootNamespace>Archivist</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<Version>2.102.0</Version>
|
<Version>2.102.0</Version>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<Version>2.87.0</Version>
|
<Version>2.87.0</Version>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"Fable.Browser.IndexedDB": {
|
"Fable.Browser.IndexedDB": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.2.0, )",
|
"requested": "[2.2.0, )",
|
||||||
|
|||||||
@@ -46,12 +46,9 @@ let private update (msg: XtractMsg) (model: XtractModel) =
|
|||||||
{ model with position = pos }, Elmish.Cmd.none
|
{ model with position = pos }, Elmish.Cmd.none
|
||||||
| SetData s ->
|
| SetData s ->
|
||||||
console.debug ("[DataExtraction] SetData msg:", s)
|
console.debug ("[DataExtraction] SetData msg:", s)
|
||||||
let data' = { model.data with name = s.name; fvcom = s.fvcom;}
|
{ model with data = s }, Elmish.Cmd.none
|
||||||
{ model with data = data' }, Elmish.Cmd.none
|
| XtractMsg.SetStarted (started, jobIdOpt) -> { model with start = started, jobIdOpt }, Elmish.Cmd.none
|
||||||
| XtractMsg.SetStarted (started, jobIdOpt) ->
|
| XtractMsg.ResetModel m -> { m with data.name = model.data.name }, Elmish.Cmd.none
|
||||||
{ model with start = started, jobIdOpt }, Elmish.Cmd.none
|
|
||||||
| XtractMsg.ResetModel m ->
|
|
||||||
{ m with data.name = model.data.name }, Elmish.Cmd.none
|
|
||||||
| XtractMsg.Noop () -> model, Elmish.Cmd.none
|
| XtractMsg.Noop () -> model, Elmish.Cmd.none
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -105,7 +102,8 @@ let updateExtractionSite (posOpt: (float * float) option, map) =
|
|||||||
match posOpt with
|
match posOpt with
|
||||||
| Some pos ->
|
| Some pos ->
|
||||||
let p' = pos |> posToCoord
|
let p' = pos |> posToCoord
|
||||||
let point = Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
|
let point =
|
||||||
|
Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
|
||||||
let feature = Feature.feature [ feature.geometryOrProperties point ]
|
let feature = Feature.feature [ feature.geometryOrProperties point ]
|
||||||
source.addFeature (feature)
|
source.addFeature (feature)
|
||||||
| _ -> ()
|
| _ -> ()
|
||||||
@@ -124,28 +122,19 @@ let private extractionSiteControls (dispatch': XtractMsg -> unit) (xmodel': Xtra
|
|||||||
if circle.intersectsCoordinate (pos |> posToCoord) then
|
if circle.intersectsCoordinate (pos |> posToCoord) then
|
||||||
Some pos
|
Some pos
|
||||||
else
|
else
|
||||||
console.error("[DataExtraction] Trying to place extraction point outside of fencing radius")
|
console.error ("[DataExtraction] Trying to place extraction point outside of fencing radius")
|
||||||
None
|
None
|
||||||
|
|
||||||
let handleMapPlaceExtraction (coords: Coordinate) =
|
let handleMapPlaceExtraction (coords: Coordinate) =
|
||||||
console.debug ($"[DataExtraction] Click add site: %s{coords.ToString ()}")
|
console.debug ($"[DataExtraction] Click add site: %s{coords.ToString ()}")
|
||||||
coordToPos coords
|
coordToPos coords |> tryFence |> SetExtractionSite |> dispatch'
|
||||||
|> tryFence
|
|
||||||
|> SetExtractionSite
|
|
||||||
|> dispatch'
|
|
||||||
|
|
||||||
let setPosition (pos: float * float) : unit =
|
let setPosition (pos: float * float) : unit =
|
||||||
Some pos
|
Some pos |> SetExtractionSite |> dispatch'
|
||||||
|> SetExtractionSite
|
|
||||||
|> dispatch'
|
|
||||||
|
|
||||||
let selectedPos =
|
let selectedPos = xmodel'.position |> Option.defaultValue (0.0, 0.0) |> toWgs84'
|
||||||
xmodel'.position
|
|
||||||
|> Option.defaultValue (0.0, 0.0)
|
|
||||||
|> toWgs84'
|
|
||||||
|
|
||||||
let deleteSite (_: Browser.Types.Event) =
|
let deleteSite (_: Browser.Types.Event) = None |> SetExtractionSite |> dispatch'
|
||||||
None |> SetExtractionSite |> dispatch'
|
|
||||||
|
|
||||||
let latitudeBox =
|
let latitudeBox =
|
||||||
let latitude = snd selectedPos
|
let latitude = snd selectedPos
|
||||||
@@ -264,21 +253,22 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
|
|||||||
let map = model.map
|
let map = model.map
|
||||||
let currentFrame = model.frame
|
let currentFrame = model.frame
|
||||||
let archiveStartUTC = archive.startTime.ToUniversalTime ()
|
let archiveStartUTC = archive.startTime.ToUniversalTime ()
|
||||||
let archiveEndT = archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
|
let archiveEndT =
|
||||||
let archiveStartT = archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
|
archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
|
||||||
|
let archiveStartT =
|
||||||
|
archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
|
||||||
let submitted, setSubmitted = Hook.useState false
|
let submitted, setSubmitted = Hook.useState false
|
||||||
|
|
||||||
let createNewModel () : XtractModel =
|
let createNewModel () : XtractModel =
|
||||||
let data =
|
let data =
|
||||||
match xtractModelOpt with
|
match xtractModelOpt with
|
||||||
| Some existing -> { existing.data with fvcom = archive.id }
|
| Some existing -> { existing.data with fvcom = archive.id }
|
||||||
| None ->
|
| None -> {
|
||||||
{
|
XtractData.empty with
|
||||||
XtractData.empty with
|
fvcom = archive.id
|
||||||
fvcom = archive.id
|
start = archiveStartT
|
||||||
start = archiveStartT
|
stop = archiveStartT.AddDays 2.0
|
||||||
stop = archiveStartT.AddDays 2.0
|
}
|
||||||
}
|
|
||||||
{
|
{
|
||||||
fence = archive.polygon
|
fence = archive.polygon
|
||||||
start = false, None
|
start = false, None
|
||||||
@@ -309,11 +299,21 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
|
|||||||
SetXtractModel (Some newModel) |> dispatch
|
SetXtractModel (Some newModel) |> dispatch
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
let setStartDateTime (dt: DateTime) =
|
let setStartDateTime (dt: DateTime) =
|
||||||
SetData { xmodel'.data with start = dt } |> dispatch'
|
// Set time to midday (12:00)
|
||||||
|
let startDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
|
||||||
|
let stopDate = xmodel'.data.stop
|
||||||
|
let newStop = if startDate >= stopDate then startDate.AddDays(1.0) else stopDate
|
||||||
|
console.debug ("[DataExtraction] setStartDateTime:", startDate, "stop:", newStop)
|
||||||
|
SetData { xmodel'.data with start = startDate; stop = newStop } |> dispatch'
|
||||||
|
|
||||||
let setStopDateTime (dt: DateTime) =
|
let setStopDateTime (dt: DateTime) =
|
||||||
SetData { xmodel'.data with stop = dt } |> dispatch'
|
let endDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
|
||||||
|
let startDate = xmodel'.data.start
|
||||||
|
let newStart = if endDate <= startDate then endDate.AddDays(-1.0) else startDate
|
||||||
|
console.debug ("[DataExtraction] setStopDateTime:", endDate, "start:", newStart)
|
||||||
|
SetData { xmodel'.data with stop = endDate; start = newStart } |> dispatch'
|
||||||
|
|
||||||
let setName (s: string) =
|
let setName (s: string) =
|
||||||
let currentModel = modelRef.contents |> Option.defaultValue xmodel'
|
let currentModel = modelRef.contents |> Option.defaultValue xmodel'
|
||||||
@@ -322,7 +322,10 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
|
|||||||
let minStartDate = archiveStartUTC
|
let minStartDate = archiveStartUTC
|
||||||
let maxStartDate = archiveEndT
|
let maxStartDate = archiveEndT
|
||||||
let minEndDate = archiveStartUTC
|
let minEndDate = archiveStartUTC
|
||||||
let maxEndDate = archiveEndT
|
// maxEndDate is one year after the selected start date
|
||||||
|
let maxEndDate =
|
||||||
|
let startDate = xmodel'.data.start
|
||||||
|
startDate.AddYears(1)
|
||||||
|
|
||||||
let metaControls =
|
let metaControls =
|
||||||
html
|
html
|
||||||
@@ -579,4 +582,4 @@ let controls xtractType (dispatch: Msg -> unit) (model: Model) =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{submitButtons}
|
{submitButtons}
|
||||||
"""
|
"""
|
||||||
@@ -43,16 +43,16 @@ let inboxDialog
|
|||||||
let table = document.getElementById "inbox-table"
|
let table = document.getElementById "inbox-table"
|
||||||
async {
|
async {
|
||||||
let! mbox = Remoting.inboxApi().getMessages ()
|
let! mbox = Remoting.inboxApi().getMessages ()
|
||||||
// if mbox.Length = 0 then
|
if mbox.Length = 0 then
|
||||||
// table?items <- [| {
|
table?items <- [| {
|
||||||
// id = Guid.Empty
|
id = Guid.Empty
|
||||||
// content = ""
|
content = ""
|
||||||
// unread = false
|
unread = false
|
||||||
// type' = MessageType.Note
|
type' = MessageType.Note
|
||||||
// created = DateTime.Now
|
created = DateTime.Now
|
||||||
// } |]
|
} |]
|
||||||
// else
|
else
|
||||||
table?items <- mbox
|
table?items <- mbox
|
||||||
} |> Async.StartImmediate
|
} |> Async.StartImmediate
|
||||||
|
|
||||||
Hook.useEffectOnChange(arg.unread, loadMessages)
|
Hook.useEffectOnChange(arg.unread, loadMessages)
|
||||||
@@ -65,30 +65,18 @@ let inboxDialog
|
|||||||
|> Set.ofSeq
|
|> Set.ofSeq
|
||||||
|> setSelected
|
|> setSelected
|
||||||
|
|
||||||
let doDelete _ =
|
let doDelete selected _ =
|
||||||
let table = document.getElementById "inbox-table"
|
let table = document.getElementById "inbox-table"
|
||||||
let selectedSet : Guid JS.Set = table?selectedSet
|
|
||||||
let items: InboxItem array = table?items
|
let items: InboxItem array = table?items
|
||||||
async {
|
|
||||||
let toDelete =
|
|
||||||
items
|
|
||||||
|> Array.filter (fun item -> selectedSet.has(item.id))
|
|
||||||
|> Array.map (fun item -> item.id)
|
|
||||||
|
|
||||||
console.debug("Deleting", toDelete.Length, "messages")
|
items
|
||||||
|
|> Array.filter (fun item -> Set.contains item.id selected)
|
||||||
|
|> Array.iter (fun item ->
|
||||||
|
console.log("Delete: %A", item.content)
|
||||||
|
do arg.deleteMessage item.id
|
||||||
|
)
|
||||||
|
|
||||||
for id in toDelete do
|
loadMessages ()
|
||||||
arg.deleteMessage id
|
|
||||||
|
|
||||||
// Clear selection immediately
|
|
||||||
selectedSet.clear()
|
|
||||||
setSelected Set.empty
|
|
||||||
|
|
||||||
// Wait a bit for server to process, then reload
|
|
||||||
do! Async.Sleep 200
|
|
||||||
let! mbox = Remoting.inboxApi().getMessages ()
|
|
||||||
table?items <- mbox
|
|
||||||
} |> Async.StartImmediate
|
|
||||||
|
|
||||||
let doRead selected _ =
|
let doRead selected _ =
|
||||||
let table = document.getElementById "inbox-table"
|
let table = document.getElementById "inbox-table"
|
||||||
@@ -118,7 +106,7 @@ let inboxDialog
|
|||||||
html $"""
|
html $"""
|
||||||
<sp-field-group horizontal>
|
<sp-field-group horizontal>
|
||||||
<sp-action-button
|
<sp-action-button
|
||||||
@click={Ev(doDelete)}
|
@click={Ev(doDelete selected)}
|
||||||
?disabled={selected.Count = 0}>
|
?disabled={selected.Count = 0}>
|
||||||
<sp-icon-delete slot="icon"></sp-icon-delete> Delete selected
|
<sp-icon-delete slot="icon"></sp-icon-delete> Delete selected
|
||||||
</sp-action-button>
|
</sp-action-button>
|
||||||
@@ -308,11 +296,8 @@ let inboxDialog
|
|||||||
let sortFn = if sortDir = "asc" then Array.sortBy else Array.sortByDescending
|
let sortFn = if sortDir = "asc" then Array.sortBy else Array.sortByDescending
|
||||||
table?items
|
table?items
|
||||||
|> sortFn (fun item -> JS.expr_js $"{item}[{sortKey}]")
|
|> sortFn (fun item -> JS.expr_js $"{item}[{sortKey}]")
|
||||||
|> fun items -> table?items <- items))
|
|> fun items -> table?items <- items)
|
||||||
|
)
|
||||||
let table =
|
|
||||||
html $"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
html $"""
|
html $"""
|
||||||
<div class="inbox-dialog">
|
<div class="inbox-dialog">
|
||||||
|
|||||||
@@ -1893,7 +1893,7 @@ let fetchArchive (archiveId: System.Guid) : ArchiveInfo Async =
|
|||||||
polygon = a.polygon |> Option.map (Array.map (fun (x, y) -> float x, float y))
|
polygon = a.polygon |> Option.map (Array.map (fun (x, y) -> float x, float y))
|
||||||
frames = archiveFrames
|
frames = archiveFrames
|
||||||
}
|
}
|
||||||
| Result.Error err ->
|
| Error err ->
|
||||||
console.error $"Could not retrieve the selected archive!: {err}"
|
console.error $"Could not retrieve the selected archive!: {err}"
|
||||||
return ArchiveInfo.empty
|
return ArchiveInfo.empty
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<Version>2.87.0</Version>
|
<Version>2.87.0</Version>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ let private simAccordion (dispatch: Msg -> unit) model =
|
|||||||
console.debug $"policies: %A{model.simPolicies}"
|
console.debug $"policies: %A{model.simPolicies}"
|
||||||
let disabled = model.archive.id = Guid.Empty
|
let disabled = model.archive.id = Guid.Empty
|
||||||
|
|
||||||
// TODO(mrtz): Create custom policy for plumes, for now just inherit from drifters.
|
// TODO(mrtz): Create custom policy for plume and xtract, for now just inherit from drifters.
|
||||||
let disabledPlume =
|
let disabledPlume =
|
||||||
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
|
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
|
||||||
let disabledXtract =
|
let disabledXtract =
|
||||||
@@ -189,7 +189,7 @@ let private simAccordion (dispatch: Msg -> unit) model =
|
|||||||
<sp-action-button
|
<sp-action-button
|
||||||
static="primary"
|
static="primary"
|
||||||
style="flex-grow: 1"
|
style="flex-grow: 1"
|
||||||
?disabled={disabled}
|
?disabled={disabledXtract }
|
||||||
@click={Ev (chooseMode (DataExtraction DefaultXtract))}
|
@click={Ev (chooseMode (DataExtraction DefaultXtract))}
|
||||||
>
|
>
|
||||||
Extract Data
|
Extract Data
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"Fable.Browser.IndexedDB": {
|
"Fable.Browser.IndexedDB": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.2.0, )",
|
"requested": "[2.2.0, )",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<Version>1.9.8</Version>
|
<Version>1.9.8</Version>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"Fable.Core": {
|
"Fable.Core": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[4.4.0, )",
|
"requested": "[4.4.0, )",
|
||||||
|
|||||||
@@ -25,37 +25,37 @@
|
|||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.winterContainer {
|
|
||||||
position:fixed;
|
|
||||||
top:0;
|
|
||||||
left:0;
|
|
||||||
width:100%;
|
|
||||||
height:100%;
|
|
||||||
pointer-events:none;
|
|
||||||
overflow:hidden;
|
|
||||||
z-index:10;
|
|
||||||
}
|
|
||||||
@keyframes snowfall {
|
|
||||||
0% {
|
|
||||||
transform: translateY(0) translateX(0) rotate(0deg);
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(100vh) translateX(var(--swirl)) rotate(var(--rot));
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.winterSnowflake {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: white;
|
|
||||||
opacity: 0;
|
|
||||||
animation: snowfall linear infinite;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
<!-- .winterContainer { -->
|
||||||
|
<!-- position:fixed; -->
|
||||||
|
<!-- top:0; -->
|
||||||
|
<!-- left:0; -->
|
||||||
|
<!-- width:100%; -->
|
||||||
|
<!-- height:100%; -->
|
||||||
|
<!-- pointer-events:none; -->
|
||||||
|
<!-- overflow:hidden; -->
|
||||||
|
<!-- z-index:10; -->
|
||||||
|
<!-- } -->
|
||||||
|
<!-- @keyframes snowfall { -->
|
||||||
|
<!-- 0% { -->
|
||||||
|
<!-- transform: translateY(0) translateX(0) rotate(0deg); -->
|
||||||
|
<!-- opacity: 0.9; -->
|
||||||
|
<!-- } -->
|
||||||
|
<!-- 100% { -->
|
||||||
|
<!-- transform: translateY(100vh) translateX(var(--swirl)) rotate(var(--rot)); -->
|
||||||
|
<!-- opacity: 0.9; -->
|
||||||
|
<!-- } -->
|
||||||
|
<!-- } -->
|
||||||
|
<!-- .winterSnowflake { -->
|
||||||
|
<!-- position: absolute; -->
|
||||||
|
<!-- top: 0; -->
|
||||||
|
<!-- left: 0; -->
|
||||||
|
<!-- font-size: 1rem; -->
|
||||||
|
<!-- color: white; -->
|
||||||
|
<!-- opacity: 0; -->
|
||||||
|
<!-- animation: snowfall linear infinite; -->
|
||||||
|
<!-- will-change: transform; -->
|
||||||
|
<!-- } -->
|
||||||
<script>
|
<script>
|
||||||
// NOTE: This should only be sent when we mount the script after confirming the id exists in sessionStorage
|
// NOTE: This should only be sent when we mount the script after confirming the id exists in sessionStorage
|
||||||
function beforeSendHandler(type, payload) {
|
function beforeSendHandler(type, payload) {
|
||||||
@@ -65,26 +65,26 @@
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<div id="winterContainer" class="winterContainer" aria-hidden="true"></div>
|
<!-- <div id="winterContainer" class="winterContainer" aria-hidden="true"></div> -->
|
||||||
<script>
|
<!-- <script> -->
|
||||||
// NOTE(mrtz): Add some snowflakes
|
<!-- // NOTE(mrtz): Add some snowflakes -->
|
||||||
const container = document.getElementById('winterContainer');
|
<!-- const container = document.getElementById('winterContainer'); -->
|
||||||
const snowflakeCount = 150;
|
<!-- const snowflakeCount = 150; -->
|
||||||
const snowflakeSymbols = ['❄', '❅', '❆', '❇', '❈', '❉', '❊', '❋']
|
<!-- const snowflakeSymbols = ['❄', '❅', '❆', '❇', '❈', '❉', '❊', '❋'] -->
|
||||||
|
|
||||||
for (let i = 0; i < snowflakeCount; i++) {
|
<!-- for (let i = 0; i < snowflakeCount; i++) { -->
|
||||||
const snowflake = document.createElement('div');
|
<!-- const snowflake = document.createElement('div'); -->
|
||||||
const randomSymbol = snowflakeSymbols[Math.floor(Math.random() * snowflakeSymbols.length)];
|
<!-- const randomSymbol = snowflakeSymbols[Math.floor(Math.random() * snowflakeSymbols.length)]; -->
|
||||||
snowflake.className = 'winterSnowflake';
|
<!-- snowflake.className = 'winterSnowflake'; -->
|
||||||
snowflake.style.left = `${Math.random() * 100}%`;
|
<!-- snowflake.style.left = `${Math.random() * 100}%`; -->
|
||||||
snowflake.style.animationDuration = `${10 + Math.random() * 10}s`;
|
<!-- snowflake.style.animationDuration = `${10 + Math.random() * 10}s`; -->
|
||||||
snowflake.style.animationDelay = `${Math.random() * 10}s`;
|
<!-- snowflake.style.animationDelay = `${Math.random() * 10}s`; -->
|
||||||
snowflake.style.setProperty('--swirl', `${Math.random() * 20 - 10}vw`);
|
<!-- snowflake.style.setProperty('--swirl', `${Math.random() * 20 - 10}vw`); -->
|
||||||
snowflake.style.setProperty('--rot', `${Math.random() * 720 + 360}deg`);
|
<!-- snowflake.style.setProperty('--rot', `${Math.random() * 720 + 360}deg`); -->
|
||||||
snowflake.textContent = randomSymbol;
|
<!-- snowflake.textContent = randomSymbol; -->
|
||||||
container.appendChild(snowflake);
|
<!-- container.appendChild(snowflake); -->
|
||||||
}
|
<!-- } -->
|
||||||
</script>
|
<!-- </script> -->
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"FSharp.Core": {
|
"FSharp.Core": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[9.0.303, )",
|
"requested": "[9.0.303, )",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ open System
|
|||||||
open System.Data
|
open System.Data
|
||||||
open System.Linq
|
open System.Linq
|
||||||
open System.Security.Claims
|
open System.Security.Claims
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
open Archmaester.Actors
|
open Archmaester.Actors
|
||||||
open Archmaester.Dto
|
open Archmaester.Dto
|
||||||
open Dapper.FSharp.PostgreSQL
|
open Dapper.FSharp.PostgreSQL
|
||||||
@@ -145,46 +147,47 @@ module Handlers =
|
|||||||
}
|
}
|
||||||
|> Async.AwaitTask
|
|> Async.AwaitTask
|
||||||
|
|
||||||
let private setNewArchivePermissions (p: Permission) =
|
let private setNewArchivePermissions (p: Permission) : Async<Result<bool, exn>> =
|
||||||
let id = ActorId p.uid
|
|
||||||
let t = DateTime.UtcNow
|
|
||||||
let term: Term = { start_time = t; end_time = t }
|
|
||||||
|
|
||||||
let ticket: Ticket = {
|
|
||||||
task = JobType.Any
|
|
||||||
quota = 100.0
|
|
||||||
start_time = t
|
|
||||||
end_time = t
|
|
||||||
}
|
|
||||||
|
|
||||||
task {
|
task {
|
||||||
let proxy = ActorProxy.Create<IArchiveAccessActor> (id, nameof ArchiveAccessActor)
|
try
|
||||||
let! (o: bool seq) = p.owners |> Array.map (fun x -> proxy.AddOwner (p.aid, x)) |> sequence
|
let id = ActorId p.uid
|
||||||
let! uv = p.users |> Array.map (fun x -> proxy.AllowUserView (p.aid, x, term)) |> sequence
|
let t = DateTime.UtcNow
|
||||||
let! ux =
|
let term: Term = { start_time = t; end_time = t }
|
||||||
p.users
|
|
||||||
|> Array.map (fun x -> proxy.AllowUserExec (p.aid, x, ticket))
|
|
||||||
|> sequence
|
|
||||||
let! gv =
|
|
||||||
p.groups
|
|
||||||
|> Array.map (fun x -> proxy.AllowGroupView (p.aid, x, term))
|
|
||||||
|> sequence
|
|
||||||
|
|
||||||
|
let ticket: Ticket = {
|
||||||
|
task = JobType.Any
|
||||||
|
quota = 100.0
|
||||||
|
start_time = t
|
||||||
|
end_time = t
|
||||||
|
}
|
||||||
|
let proxy = ActorProxy.Create<IArchiveAccessActor> (id, nameof ArchiveAccessActor)
|
||||||
|
let! (o: bool seq) = p.owners |> Array.map (fun x -> proxy.AddOwner (p.aid, x)) |> sequence
|
||||||
|
let! uv = p.users |> Array.map (fun x -> proxy.AllowUserView (p.aid, x, term)) |> sequence
|
||||||
|
let! ux =
|
||||||
|
p.users
|
||||||
|
|> Array.map (fun x -> proxy.AllowUserExec (p.aid, x, ticket))
|
||||||
|
|> sequence
|
||||||
|
let! gv =
|
||||||
|
p.groups
|
||||||
|
|> Array.map (fun x -> proxy.AllowGroupView (p.aid, x, term))
|
||||||
|
|> sequence
|
||||||
|
|
||||||
if p.ref.IsSome then
|
if p.ref.IsSome then
|
||||||
let proxy =
|
let proxy =
|
||||||
ActorProxy.Create<IArchiveAccessActor> (ActorId p.uid, nameof ArchiveAccessActor)
|
ActorProxy.Create<IArchiveAccessActor> (ActorId p.uid, nameof ArchiveAccessActor)
|
||||||
|
|
||||||
let! _ = proxy.AddParent (p.aid, p.ref.Value) |> Async.AwaitTask
|
let! _ = proxy.AddParent (p.aid, p.ref.Value) |> Async.AwaitTask
|
||||||
()
|
()
|
||||||
|
|
||||||
let all = Seq.concat [ o; uv; ux; gv ]
|
let all = Seq.concat [ o; uv; ux; gv ]
|
||||||
let res = all |> Seq.reduce (&&)
|
let res = all |> Seq.reduce (&&)
|
||||||
|
|
||||||
if not res then
|
if not res then
|
||||||
Log.Warning $"Archmaester.setArchivePermissions returned false: %A{all}"
|
Log.Warning $"Archmaester.setArchivePermissions returned false: %A{all}"
|
||||||
|
|
||||||
return res
|
return Ok res
|
||||||
|
with ex ->
|
||||||
|
return Error ex
|
||||||
}
|
}
|
||||||
|> Async.AwaitTask
|
|> Async.AwaitTask
|
||||||
|
|
||||||
@@ -219,10 +222,12 @@ module Handlers =
|
|||||||
Log.Information $"Adding archive: {item.props.name}"
|
Log.Information $"Adding archive: {item.props.name}"
|
||||||
|
|
||||||
async {
|
async {
|
||||||
let db = Archives.Archivist (Db.getDataSource ())
|
let archivist = Archives.Archivist (Db.getDataSource ())
|
||||||
|
use db = archivist.startConnection ()
|
||||||
|
let tr = archivist.startTransaction db
|
||||||
|
|
||||||
try
|
try
|
||||||
let saveRes = db.tryAddArchive item
|
let saveRes = archivist.tryAddArchive(db, item)
|
||||||
Log.Debug $"saveRes %A{saveRes}"
|
Log.Debug $"saveRes %A{saveRes}"
|
||||||
|
|
||||||
match saveRes with
|
match saveRes with
|
||||||
@@ -241,7 +246,7 @@ module Handlers =
|
|||||||
|
|
||||||
let aid = archive.ArchiveId
|
let aid = archive.ArchiveId
|
||||||
|
|
||||||
let! _ =
|
let! res =
|
||||||
setNewArchivePermissions {
|
setNewArchivePermissions {
|
||||||
uid = uid
|
uid = uid
|
||||||
gid = gid
|
gid = gid
|
||||||
@@ -252,9 +257,22 @@ module Handlers =
|
|||||||
ref = item.props.reference
|
ref = item.props.reference
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetStatusCode 201
|
match res with
|
||||||
return Ok ()
|
| Ok ok ->
|
||||||
|
if ok then
|
||||||
|
tr.Commit ()
|
||||||
|
ctx.SetStatusCode 201
|
||||||
|
return Ok ()
|
||||||
|
else
|
||||||
|
tr.Rollback ()
|
||||||
|
Log.Error("addArchive: error: one of the permissions failed")
|
||||||
|
return Error "Error adding archive"
|
||||||
|
| Error ex ->
|
||||||
|
tr.Rollback ()
|
||||||
|
Log.Error(ex, "addArchive: error")
|
||||||
|
return Error "Error adding archive"
|
||||||
with exn ->
|
with exn ->
|
||||||
|
tr.Rollback ()
|
||||||
Log.Error $"addArchive: error: {exn}"
|
Log.Error $"addArchive: error: {exn}"
|
||||||
ctx.SetStatusCode 500
|
ctx.SetStatusCode 500
|
||||||
return Error $"Could not add Archive: {exn.Message}"
|
return Error $"Could not add Archive: {exn.Message}"
|
||||||
@@ -413,6 +431,21 @@ module Handlers =
|
|||||||
return Error $"Could not retrieve archive {aid}: {err}"
|
return Error $"Could not retrieve archive {aid}: {err}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let getBasePath (aid: ArchiveId) =
|
||||||
|
Log.Information $"Getting archive basePath: {aid}"
|
||||||
|
|
||||||
|
async {
|
||||||
|
let db = Archives.Archivist (Db.getDataSource ())
|
||||||
|
|
||||||
|
match db.getBasePath aid with
|
||||||
|
| Ok path ->
|
||||||
|
Log.Debug $"getBasePath: {path}"
|
||||||
|
return Ok path
|
||||||
|
| Error err ->
|
||||||
|
Log.Error $"getBasePath: archive with id {aid} not found"
|
||||||
|
return Error $"Could not retrieve archive {aid}: {err}"
|
||||||
|
}
|
||||||
|
|
||||||
let getFiles (aid: ArchiveId) =
|
let getFiles (aid: ArchiveId) =
|
||||||
Log.Information $"Getting archive files: {aid}"
|
Log.Information $"Getting archive files: {aid}"
|
||||||
|
|
||||||
@@ -436,10 +469,10 @@ module Handlers =
|
|||||||
|
|
||||||
match db.getAllArchiveFiles aid with
|
match db.getAllArchiveFiles aid with
|
||||||
| Ok files ->
|
| Ok files ->
|
||||||
Log.Debug $"getFiles: {files.basePath} {files.series.Length}"
|
Log.Debug $"getAllFiles: {files.basePath} {files.series.Length}"
|
||||||
return Ok files
|
return Ok files
|
||||||
| Error err ->
|
| Error err ->
|
||||||
Log.Error $"getFiles: archive with id {aid} not found"
|
Log.Error $"getAllFiles: archive with id {aid} not found"
|
||||||
return Error $"Could not retrieve archive {aid}: {err}"
|
return Error $"Could not retrieve archive {aid}: {err}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ open Dapr.Actors.Runtime
|
|||||||
open Oceanbox.ServerPack
|
open Oceanbox.ServerPack
|
||||||
|
|
||||||
open Archmaester.Actors
|
open Archmaester.Actors
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
type ArchivistActor(host: ActorHost, observatory: Observer.ObserverFactory) =
|
type ArchivistActor(host: ActorHost, observatory: Observer.ObserverFactory) =
|
||||||
inherit Actor(host)
|
inherit Actor(host)
|
||||||
@@ -53,14 +54,14 @@ type ArchivistActor(host: ActorHost, observatory: Observer.ObserverFactory) =
|
|||||||
Api.Handlers.getArchivePropsById aid |> Result.map _.json |> Task.FromResult
|
Api.Handlers.getArchivePropsById aid |> Result.map _.json |> Task.FromResult
|
||||||
|
|
||||||
member this.GetBasePath(aid) =
|
member this.GetBasePath(aid) =
|
||||||
let db = Oceanbox.DataAgent.Archives.Archivist (Db.getDataSource ())
|
Api.Handlers.getBasePath aid |> Async.StartAsTask
|
||||||
db.getBasePath aid |> Task.FromResult
|
|
||||||
|
|
||||||
member this.GetProjection(aid) =
|
member this.GetProjection(aid) =
|
||||||
Api.Handlers.getArchivePropsById aid |> Result.map _.projection |> Task.FromResult
|
Api.Handlers.getArchivePropsById aid
|
||||||
|
|> Result.map _.projection
|
||||||
|
|> Task.FromResult
|
||||||
|
|
||||||
member this.GetArchiveFiles(aid) =
|
member this.GetArchiveFiles(aid) =
|
||||||
let db = Oceanbox.DataAgent.Archives.Archivist (Db.getDataSource ())
|
Api.Handlers.getFiles aid |> Async.StartAsTask
|
||||||
db.getArchiveFiles aid |> Task.FromResult
|
|
||||||
|
|
||||||
override this.OnActivateAsync() = task { return () }
|
override this.OnActivateAsync() = task { return () }
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>6.20.0</Version>
|
<Version>6.20.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Utils.fs" />
|
<Compile Include="Utils.fs" />
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
|
|||||||
let grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
|
let grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
|
||||||
let dep = None
|
let dep = None
|
||||||
let part = job.partition |> Option.defaultValue "long"
|
let part = job.partition |> Option.defaultValue "long"
|
||||||
|
let cpt = 32
|
||||||
|
|
||||||
task {
|
task {
|
||||||
try
|
try
|
||||||
@@ -101,7 +102,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
|
|||||||
Sites = job.input.release.groups |> Seq.sumBy _.sites.Length
|
Sites = job.input.release.groups |> Seq.sumBy _.sites.Length
|
||||||
|}
|
|}
|
||||||
|
|
||||||
let! r = slurm.Submit (job.name, this.myId, driftersScript, env, grp, part, dep, Some comment)
|
let! r = slurm.Submit (job.name, this.myId, driftersScript, env, grp, part, cpt, dep, Some comment)
|
||||||
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
|
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
|
||||||
|
|
||||||
match Decode.Auto.fromString<SlurmSubmissionResponse> s with
|
match Decode.Auto.fromString<SlurmSubmissionResponse> s with
|
||||||
@@ -163,6 +164,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
|
|||||||
let grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
|
let grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
|
||||||
let dep = job.dependency |> Option.map (fun jobId -> $"afterok:{jobId}")
|
let dep = job.dependency |> Option.map (fun jobId -> $"afterok:{jobId}")
|
||||||
let part = job.partition |> Option.defaultValue "long"
|
let part = job.partition |> Option.defaultValue "long"
|
||||||
|
let cpt = 32
|
||||||
|
|
||||||
task {
|
task {
|
||||||
try
|
try
|
||||||
@@ -179,7 +181,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
|
|||||||
SimType = job.model.ToString()
|
SimType = job.model.ToString()
|
||||||
|}
|
|}
|
||||||
|
|
||||||
let! r = slurm.Submit (job.name, this.myId, postdriftScript, env, grp, part, dep, Some comment)
|
let! r = slurm.Submit (job.name, this.myId, postdriftScript, env, grp, part, cpt, dep, Some comment)
|
||||||
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
|
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
|
||||||
|
|
||||||
match Decode.Auto.fromString<SlurmSubmissionResponse> s with
|
match Decode.Auto.fromString<SlurmSubmissionResponse> s with
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>2.6.6</Version>
|
<Version>2.6.6</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -286,31 +286,35 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
member this.getActiveJobs aid =
|
member this.getActiveJobs aid =
|
||||||
Log.Debug $"get active jobs for: {this.myId}"
|
task {
|
||||||
|
Log.Debug $"get active jobs for: {this.myId}"
|
||||||
|
|
||||||
let active = [|
|
let active = [|
|
||||||
for v in this.jobs do
|
for v in this.jobs do
|
||||||
Log.Debug $"job: {aid} -> %A{v.Value}"
|
Log.Debug $"job: {aid} -> %A{v.Value}"
|
||||||
|
|
||||||
if v.Value.refId = aid then
|
if v.Value.refId = aid then
|
||||||
match v.Value.status with
|
match v.Value.status with
|
||||||
| JobStatus.New
|
| JobStatus.New
|
||||||
| JobStatus.Waiting
|
| JobStatus.Waiting
|
||||||
| JobStatus.Failed
|
| JobStatus.Failed
|
||||||
| JobStatus.Running -> v.Value
|
| JobStatus.Running -> v.Value
|
||||||
| _ -> ()
|
| _ -> ()
|
||||||
else
|
else
|
||||||
()
|
()
|
||||||
|]
|
|]
|
||||||
|
|
||||||
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
|
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
|
||||||
task { return active }
|
return active
|
||||||
|
}
|
||||||
|
|
||||||
member this.getFenceRadius() =
|
member this.getFenceRadius() =
|
||||||
Log.Information "fence?"
|
task {
|
||||||
let r = settings.file.fenceRadius
|
Log.Information "fence?"
|
||||||
Log.Information $"fence: {r}"
|
let r = settings.file.fenceRadius
|
||||||
Task.FromResult r
|
Log.Information $"fence: {r}"
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
member this.checkFence aid (pts: (float * float) list) =
|
member this.checkFence aid (pts: (float * float) list) =
|
||||||
task { return this.validatePoints aid pts }
|
task { return this.validatePoints aid pts }
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ type SlurmClient(client: HttpClient, settings: ISlurmClientSettings) =
|
|||||||
Log.Information $"base: {client.BaseAddress}"
|
Log.Information $"base: {client.BaseAddress}"
|
||||||
client.GetStringAsync "diag"
|
client.GetStringAsync "diag"
|
||||||
|
|
||||||
member this.Submit(jobName, agentId, exec: string, env: IJobEnv, group, partition, dependency, comment) =
|
member this.Submit(jobName, agentId, exec: string, env: IJobEnv, group, partition, cpu_per_task, dependency, comment) =
|
||||||
let jobProps = {
|
let jobProps = {
|
||||||
SlurmJobProps.empty with
|
SlurmJobProps.empty with
|
||||||
name = jobName
|
name = jobName
|
||||||
@@ -162,6 +162,7 @@ type SlurmClient(client: HttpClient, settings: ISlurmClientSettings) =
|
|||||||
GROUP_ID = group
|
GROUP_ID = group
|
||||||
env = env
|
env = env
|
||||||
}
|
}
|
||||||
|
cpus_per_task = cpu_per_task
|
||||||
partition = partition
|
partition = partition
|
||||||
account = Some group[1..] // stripping leading '/' in group name
|
account = Some group[1..] // stripping leading '/' in group name
|
||||||
comment = comment
|
comment = comment
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
|
|||||||
|
|
||||||
let xtractScript =
|
let xtractScript =
|
||||||
let env = settings.appEnv |> AppEnv.FromString
|
let env = settings.appEnv |> AppEnv.FromString
|
||||||
|
// TODO(mrtz): Add additional environments once excavator is migrated to atlas
|
||||||
match env with
|
match env with
|
||||||
| Production -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
|
| Production -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
|
||||||
| PreProd -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
|
| PreProd -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
|
||||||
@@ -82,15 +83,22 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
|
|||||||
let g = defaultArg groups [||]
|
let g = defaultArg groups [||]
|
||||||
let grp = if g.Length > 0 then g[0] else ""
|
let grp = if g.Length > 0 then g[0] else ""
|
||||||
let dep = None
|
let dep = None
|
||||||
let part = partition |> Option.defaultValue "long"
|
let part = partition |> Option.defaultValue "short"
|
||||||
|
let cpt = 4
|
||||||
|
|
||||||
taskResult {
|
taskResult {
|
||||||
let! archiveProps =
|
let! archiveName =
|
||||||
job.archiveId
|
this.archivist.GetArchiveName job.archiveId
|
||||||
|> this.archivist.GetArchiveProps
|
|
||||||
|> TaskResult.mapError (fun e ->
|
|> TaskResult.mapError (fun e ->
|
||||||
Log.Error $"[XtractActor.SubmitXtract] Failed to get archive props: %s{e}"
|
Log.Error $"[XtractActor.SubmitXtract] Failed to get archive name: %s{e}"
|
||||||
$"Failed to get archive props: {e}"
|
$"Failed to get archive name: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
let! projection =
|
||||||
|
this.archivist.GetProjection job.archiveId
|
||||||
|
|> TaskResult.mapError (fun e ->
|
||||||
|
Log.Error $"[XtractActor.SubmitXtract] Failed to get projection: %s{e}"
|
||||||
|
$"Failed to get archive projection: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
let! basePath =
|
let! basePath =
|
||||||
@@ -108,7 +116,7 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
|
|||||||
)
|
)
|
||||||
|
|
||||||
Log.Debug
|
Log.Debug
|
||||||
$"[XtractActor] Got archive name: '{archiveProps.name}', basePath: '{basePath}', projection: '{archiveProps.projection}'"
|
$"[XtractActor.SubmitXtract] Got archive name: '{archiveName}', basePath: '{basePath}', projection: '{projection}'"
|
||||||
|
|
||||||
let uniqueOutputDirs =
|
let uniqueOutputDirs =
|
||||||
archiveFiles.series
|
archiveFiles.series
|
||||||
@@ -126,8 +134,8 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
|
|||||||
let enrichedJob = {
|
let enrichedJob = {
|
||||||
job with
|
job with
|
||||||
basePath = basePath
|
basePath = basePath
|
||||||
caseName = archiveProps.name
|
caseName = archiveName
|
||||||
projection = archiveProps.projection
|
projection = projection
|
||||||
files = uniqueOutputDirs
|
files = uniqueOutputDirs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +157,7 @@ type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings,
|
|||||||
Positions = 1
|
Positions = 1
|
||||||
|}
|
|}
|
||||||
|
|
||||||
let! r = slurm.Submit (job.name, this.myId, xtractScript, env, grp, part, dep, Some comment)
|
let! r = slurm.Submit (job.name, this.myId, xtractScript, env, grp, part, cpt, dep, Some comment)
|
||||||
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
|
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
|
||||||
|
|
||||||
let! slurmResponse =
|
let! slurmResponse =
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>1.9.8</Version>
|
<Version>1.9.8</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
module Main
|
module Main
|
||||||
|
|
||||||
open System
|
open System
|
||||||
open System.Text.Json
|
open System.Text.Json
|
||||||
open System.Text.Json.Serialization
|
open System.Text.Json.Serialization
|
||||||
|
open System.Net.Http
|
||||||
|
|
||||||
|
open Microsoft.AspNetCore.Authentication
|
||||||
|
open Microsoft.AspNetCore.Builder
|
||||||
|
open Microsoft.AspNetCore.Cors.Infrastructure
|
||||||
|
open Microsoft.AspNetCore.Hosting
|
||||||
|
open Microsoft.AspNetCore.Http
|
||||||
|
open Microsoft.AspNetCore.SignalR
|
||||||
|
open Microsoft.Extensions.DependencyInjection
|
||||||
|
open Microsoft.Extensions.FileProviders
|
||||||
|
open Microsoft.Extensions.Hosting
|
||||||
|
open Microsoft.Extensions.Logging
|
||||||
|
|
||||||
open Argu
|
open Argu
|
||||||
open Dapr.Actors
|
open Dapr.Actors
|
||||||
open Dapr.Actors.Client
|
open Dapr.Actors.Client
|
||||||
open Dapr.Client
|
open Dapr.Client
|
||||||
open FSharp.Data
|
open FSharp.Data
|
||||||
|
open FsToolkit.ErrorHandling
|
||||||
open Fable.SignalR
|
open Fable.SignalR
|
||||||
open Giraffe
|
open Giraffe
|
||||||
open Microsoft.AspNetCore.Authentication
|
|
||||||
open Microsoft.AspNetCore.Builder
|
|
||||||
open Microsoft.AspNetCore.Hosting
|
|
||||||
open Microsoft.AspNetCore.Cors.Infrastructure
|
|
||||||
open Microsoft.AspNetCore.Http
|
|
||||||
open Microsoft.AspNetCore.SignalR
|
|
||||||
open Microsoft.Extensions.DependencyInjection
|
|
||||||
open Microsoft.Extensions.Logging
|
|
||||||
open Microsoft.Extensions.FileProviders
|
|
||||||
open Microsoft.Extensions.Hosting
|
|
||||||
open System.Net.Http
|
|
||||||
open Oceanbox.DataAgent
|
|
||||||
open Polly
|
open Polly
|
||||||
open Prometheus
|
open Prometheus
|
||||||
open Saturn
|
open Saturn
|
||||||
open Saturn.Dapr
|
open Saturn.Dapr
|
||||||
open Saturn.OpenTelemetry
|
|
||||||
open Saturn.Observer
|
open Saturn.Observer
|
||||||
|
open Saturn.OpenTelemetry
|
||||||
open Sentry
|
open Sentry
|
||||||
open Sentry.AspNetCore
|
open Sentry.AspNetCore
|
||||||
open Sentry.Extensibility
|
open Sentry.Extensibility
|
||||||
@@ -37,6 +39,7 @@ open Serilog.Sinks.OpenTelemetry
|
|||||||
|
|
||||||
open Atlantis
|
open Atlantis
|
||||||
open Atlantis.Shared
|
open Atlantis.Shared
|
||||||
|
open Oceanbox.DataAgent
|
||||||
open Oceanbox.ServerPack.MultiAuth
|
open Oceanbox.ServerPack.MultiAuth
|
||||||
open Saturn.OpenFga
|
open Saturn.OpenFga
|
||||||
open Settings
|
open Settings
|
||||||
@@ -82,7 +85,8 @@ let configureSerilog () =
|
|||||||
let configureLogging (builder: ILoggingBuilder) =
|
let configureLogging (builder: ILoggingBuilder) =
|
||||||
builder
|
builder
|
||||||
.ClearProviders()
|
.ClearProviders()
|
||||||
.AddSerilog() |> ignore
|
.AddSerilog()
|
||||||
|
|> ignore
|
||||||
|
|
||||||
let corsPolicy (policy: CorsPolicyBuilder) =
|
let corsPolicy (policy: CorsPolicyBuilder) =
|
||||||
policy
|
policy
|
||||||
@@ -91,7 +95,7 @@ let corsPolicy (policy: CorsPolicyBuilder) =
|
|||||||
.AllowAnyMethod()
|
.AllowAnyMethod()
|
||||||
.WithOrigins(appsettings.file.allowedOrigins)
|
.WithOrigins(appsettings.file.allowedOrigins)
|
||||||
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
||||||
|> ignore
|
|> ignore
|
||||||
|
|
||||||
type UserIdProvider() =
|
type UserIdProvider() =
|
||||||
interface IUserIdProvider with
|
interface IUserIdProvider with
|
||||||
@@ -254,12 +258,12 @@ let stopImpersonating (next: HttpFunc) (ctx: HttpContext) =
|
|||||||
let getBarentsWatchToken (next: HttpFunc) (ctx: HttpContext) =
|
let getBarentsWatchToken (next: HttpFunc) (ctx: HttpContext) =
|
||||||
task {
|
task {
|
||||||
let! tokenRes =
|
let! tokenRes =
|
||||||
tryGetEnv "BARENTSWATCH_CLIENT_ID"
|
taskResult {
|
||||||
|> Option.bind (fun id ->
|
let! id = tryGetEnv "BARENTSWATCH_CLIENT_ID" |> Result.requireSome "Missing barentswatch client id"
|
||||||
tryGetEnv "BARENTSWATCH_SECRET"
|
let! secret = tryGetEnv "BARENTSWATCH_SECRET" |> Result.requireSome "Missing barentswatch secret"
|
||||||
|> Option.map (BarentsWatch.getToken appsettings.redis id))
|
let! token = BarentsWatch.getToken appsettings.redis id secret
|
||||||
|> Option.defaultValue (Error "Secret or client id missing" |> async.Return)
|
return token
|
||||||
|> Async.StartAsTask
|
}
|
||||||
|
|
||||||
match tokenRes with
|
match tokenRes with
|
||||||
| Error err ->
|
| Error err ->
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<ParallelCompilation>true</ParallelCompilation>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<Version>2.102.0</Version>
|
<Version>2.102.0</Version>
|
||||||
<RootNamespace>Server</RootNamespace>
|
<RootNamespace>Server</RootNamespace>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ let configureEnv () : Async<unit> =
|
|||||||
| Some x when x = "1" || x = "true" ->
|
| Some x when x = "1" || x = "true" ->
|
||||||
Log.Information "[Settings] > Waiting for Dapr..."
|
Log.Information "[Settings] > Waiting for Dapr..."
|
||||||
Threading.Thread.Sleep 2000
|
Threading.Thread.Sleep 2000
|
||||||
do! Async.Sleep (TimeSpan.FromMilliseconds 2000)
|
do! Async.Sleep (TimeSpan.FromMilliseconds 2000.0)
|
||||||
do! setupAzureEnv ()
|
do! setupAzureEnv ()
|
||||||
Log.Information $"[Settings] > Azure Keyvault credentials in {appsettings.keyVault}"
|
Log.Information $"[Settings] > Azure Keyvault credentials in {appsettings.keyVault}"
|
||||||
do! setupDbEnv ()
|
do! setupDbEnv ()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"Argu": {
|
"Argu": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[6.2.5, )",
|
"requested": "[6.2.5, )",
|
||||||
@@ -56,9 +56,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Azure.Core": "1.44.1",
|
"Azure.Core": "1.44.1",
|
||||||
"Microsoft.Identity.Client": "4.67.2",
|
"Microsoft.Identity.Client": "4.67.2",
|
||||||
"Microsoft.Identity.Client.Extensions.Msal": "4.67.2",
|
"Microsoft.Identity.Client.Extensions.Msal": "4.67.2"
|
||||||
"System.Memory": "4.5.5",
|
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Azure.Security.KeyVault.Secrets": {
|
"Azure.Security.KeyVault.Secrets": {
|
||||||
@@ -67,10 +65,7 @@
|
|||||||
"resolved": "4.7.0",
|
"resolved": "4.7.0",
|
||||||
"contentHash": "uOPCojkm41V4dKTORyGzl3/f/lriKpxSQ43fWDn4StRJBVmbF1F/DNWJhwm207kCnqgE/W9+tskJSimIKHCZkw==",
|
"contentHash": "uOPCojkm41V4dKTORyGzl3/f/lriKpxSQ43fWDn4StRJBVmbF1F/DNWJhwm207kCnqgE/W9+tskJSimIKHCZkw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Azure.Core": "1.44.1",
|
"Azure.Core": "1.44.1"
|
||||||
"System.Memory": "4.5.5",
|
|
||||||
"System.Text.Json": "6.0.10",
|
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Dapr.Actors": {
|
"Dapr.Actors": {
|
||||||
@@ -251,8 +246,7 @@
|
|||||||
"resolved": "1.3.13",
|
"resolved": "1.3.13",
|
||||||
"contentHash": "znp8odpdkVGKVX0AvbhiXdmeMi0KJ+A4AyAQWSkfAEAe4Z4clRE+rVhrLnAGrFD1VEIUX2lsQ4o84ywpWZUSGw==",
|
"contentHash": "znp8odpdkVGKVX0AvbhiXdmeMi0KJ+A4AyAQWSkfAEAe4Z4clRE+rVhrLnAGrFD1VEIUX2lsQ4o84ywpWZUSGw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"FSharp.Core": "4.7.0",
|
"FSharp.Core": "4.7.0"
|
||||||
"System.Text.Json": "6.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"FSharpPlus": {
|
"FSharpPlus": {
|
||||||
@@ -273,8 +267,7 @@
|
|||||||
"FSharp.Core": "6.0.0",
|
"FSharp.Core": "6.0.0",
|
||||||
"FSharp.SystemTextJson": "1.3.13",
|
"FSharp.SystemTextJson": "1.3.13",
|
||||||
"Giraffe.ViewEngine": "1.4.0",
|
"Giraffe.ViewEngine": "1.4.0",
|
||||||
"Microsoft.IO.RecyclableMemoryStream": "3.0.1",
|
"Microsoft.IO.RecyclableMemoryStream": "3.0.1"
|
||||||
"System.Text.Json": "8.0.5"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"IdentityModel.AspNetCore": {
|
"IdentityModel.AspNetCore": {
|
||||||
@@ -489,12 +482,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
|
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
|
||||||
"System.ClientModel": "1.1.0",
|
"System.ClientModel": "1.1.0",
|
||||||
"System.Diagnostics.DiagnosticSource": "6.0.1",
|
"System.Memory.Data": "6.0.0"
|
||||||
"System.Memory.Data": "6.0.0",
|
|
||||||
"System.Numerics.Vectors": "4.5.0",
|
|
||||||
"System.Text.Encodings.Web": "6.0.0",
|
|
||||||
"System.Text.Json": "6.0.10",
|
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Azure.Security.KeyVault.Keys": {
|
"Azure.Security.KeyVault.Keys": {
|
||||||
@@ -502,10 +490,7 @@
|
|||||||
"resolved": "4.6.0",
|
"resolved": "4.6.0",
|
||||||
"contentHash": "1KbCIkXmLaj+kDDNm1Va5rNlzgcJ/fVtnsoVmzZPKa38jz6DXhPyojdvGaOX8AdupGJceg0X1vrsGvZKN79Qzw==",
|
"contentHash": "1KbCIkXmLaj+kDDNm1Va5rNlzgcJ/fVtnsoVmzZPKa38jz6DXhPyojdvGaOX8AdupGJceg0X1vrsGvZKN79Qzw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Azure.Core": "1.37.0",
|
"Azure.Core": "1.37.0"
|
||||||
"System.Memory": "4.5.4",
|
|
||||||
"System.Text.Json": "4.7.2",
|
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Azure.Storage.Blobs": {
|
"Azure.Storage.Blobs": {
|
||||||
@@ -513,8 +498,7 @@
|
|||||||
"resolved": "12.23.0",
|
"resolved": "12.23.0",
|
||||||
"contentHash": "wokJ5KX/iViQQ32xyCu69+Ter0aR4B9QQ+oR9NCpc/WPIanxnDErrmFfdmE7K8ZdccjHkvE/wEnqJxaF1+5wFg==",
|
"contentHash": "wokJ5KX/iViQQ32xyCu69+Ter0aR4B9QQ+oR9NCpc/WPIanxnDErrmFfdmE7K8ZdccjHkvE/wEnqJxaF1+5wFg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Azure.Storage.Common": "12.22.0",
|
"Azure.Storage.Common": "12.22.0"
|
||||||
"System.Text.Json": "6.0.10"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Azure.Storage.Common": {
|
"Azure.Storage.Common": {
|
||||||
@@ -586,8 +570,7 @@
|
|||||||
"Fable.Remoting.MsgPack": "1.24.0",
|
"Fable.Remoting.MsgPack": "1.24.0",
|
||||||
"Fable.SignalR.Shared": "2.1.0",
|
"Fable.SignalR.Shared": "2.1.0",
|
||||||
"Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson": "9.0.0",
|
"Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson": "9.0.0",
|
||||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis": "9.0.0",
|
"Microsoft.AspNetCore.SignalR.StackExchangeRedis": "9.0.0"
|
||||||
"System.Text.Encodings.Web": "9.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Fable.SignalR.Shared": {
|
"Fable.SignalR.Shared": {
|
||||||
@@ -683,8 +666,7 @@
|
|||||||
"resolved": "5.3.2",
|
"resolved": "5.3.2",
|
||||||
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"FSharp.Core": "4.3.2",
|
"FSharp.Core": "4.3.2"
|
||||||
"System.Reflection.Emit.Lightweight": "4.3.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Giraffe.ViewEngine": {
|
"Giraffe.ViewEngine": {
|
||||||
@@ -836,8 +818,7 @@
|
|||||||
"resolved": "2.2.0",
|
"resolved": "2.2.0",
|
||||||
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
|
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNetCore.Http.Features": "2.2.0",
|
"Microsoft.AspNetCore.Http.Features": "2.2.0"
|
||||||
"System.Text.Encodings.Web": "4.5.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.AspNetCore.Http.Features": {
|
"Microsoft.AspNetCore.Http.Features": {
|
||||||
@@ -881,8 +862,7 @@
|
|||||||
"resolved": "2.2.0",
|
"resolved": "2.2.0",
|
||||||
"contentHash": "9ErxAAKaDzxXASB/b5uLEkLgUWv1QbeVxyJYEHQwMaxXOeFFVkQxiq8RyfVcifLU7NR0QY0p3acqx4ZpYfhHDg==",
|
"contentHash": "9ErxAAKaDzxXASB/b5uLEkLgUWv1QbeVxyJYEHQwMaxXOeFFVkQxiq8RyfVcifLU7NR0QY0p3acqx4ZpYfhHDg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Net.Http.Headers": "2.2.0",
|
"Microsoft.Net.Http.Headers": "2.2.0"
|
||||||
"System.Text.Encodings.Web": "4.5.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Bcl.AsyncInterfaces": {
|
"Microsoft.Bcl.AsyncInterfaces": {
|
||||||
@@ -1089,8 +1069,7 @@
|
|||||||
"resolved": "4.67.2",
|
"resolved": "4.67.2",
|
||||||
"contentHash": "37t0TfekfG6XM8kue/xNaA66Qjtti5Qe1xA41CK+bEd8VD76/oXJc+meFJHGzygIC485dCpKoamG/pDfb9Qd7Q==",
|
"contentHash": "37t0TfekfG6XM8kue/xNaA66Qjtti5Qe1xA41CK+bEd8VD76/oXJc+meFJHGzygIC485dCpKoamG/pDfb9Qd7Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.IdentityModel.Abstractions": "6.35.0",
|
"Microsoft.IdentityModel.Abstractions": "6.35.0"
|
||||||
"System.Diagnostics.DiagnosticSource": "6.0.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Identity.Client.Extensions.Msal": {
|
"Microsoft.Identity.Client.Extensions.Msal": {
|
||||||
@@ -1158,8 +1137,7 @@
|
|||||||
"resolved": "2.2.0",
|
"resolved": "2.2.0",
|
||||||
"contentHash": "iZNkjYqlo8sIOI0bQfpsSoMTmB/kyvmV2h225ihyZT33aTp48ZpF6qYnXxzSXmHt8DpBAwBTX+1s1UFLbYfZKg==",
|
"contentHash": "iZNkjYqlo8sIOI0bQfpsSoMTmB/kyvmV2h225ihyZT33aTp48ZpF6qYnXxzSXmHt8DpBAwBTX+1s1UFLbYfZKg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Primitives": "2.2.0",
|
"Microsoft.Extensions.Primitives": "2.2.0"
|
||||||
"System.Buffers": "4.5.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.NET.StringTools": {
|
"Microsoft.NET.StringTools": {
|
||||||
@@ -1188,10 +1166,7 @@
|
|||||||
"OpenTelemetry.Api": {
|
"OpenTelemetry.Api": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "1.10.0",
|
"resolved": "1.10.0",
|
||||||
"contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w==",
|
"contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w=="
|
||||||
"dependencies": {
|
|
||||||
"System.Diagnostics.DiagnosticSource": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"OpenTelemetry.Api.ProviderBuilderExtensions": {
|
"OpenTelemetry.Api.ProviderBuilderExtensions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -1272,17 +1247,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Options": "9.0.0",
|
"Microsoft.Extensions.Options": "9.0.0",
|
||||||
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.10.0, 2.0.0)",
|
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.10.0, 2.0.0)",
|
||||||
"StackExchange.Redis": "[2.6.122, 3.0.0)",
|
"StackExchange.Redis": "[2.6.122, 3.0.0)"
|
||||||
"System.Reflection.Emit.Lightweight": "4.7.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Pipelines.Sockets.Unofficial": {
|
"Pipelines.Sockets.Unofficial": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.2.8",
|
"resolved": "2.2.8",
|
||||||
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==",
|
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
|
||||||
"dependencies": {
|
|
||||||
"System.IO.Pipelines": "5.0.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Polly": {
|
"Polly": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -1300,11 +1271,7 @@
|
|||||||
"ProjNET": {
|
"ProjNET": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.0.0",
|
"resolved": "2.0.0",
|
||||||
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
|
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
|
||||||
"dependencies": {
|
|
||||||
"System.Memory": "4.5.3",
|
|
||||||
"System.Numerics.Vectors": "4.5.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"prometheus-net": {
|
"prometheus-net": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -1404,18 +1371,12 @@
|
|||||||
"Pipelines.Sockets.Unofficial": "2.2.8"
|
"Pipelines.Sockets.Unofficial": "2.2.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.Buffers": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.0",
|
|
||||||
"contentHash": "pL2ChpaRRWI/p4LXyy4RgeWlYF2sgfj/pnVMvBqwNFr5cXg7CXNnWZWxrOONLg8VGdFB8oB+EG2Qw4MLgTOe+A=="
|
|
||||||
},
|
|
||||||
"System.ClientModel": {
|
"System.ClientModel": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "1.1.0",
|
"resolved": "1.1.0",
|
||||||
"contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==",
|
"contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Memory.Data": "1.0.2",
|
"System.Memory.Data": "1.0.2"
|
||||||
"System.Text.Json": "6.0.9"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.ComponentModel.Annotations": {
|
"System.ComponentModel.Annotations": {
|
||||||
@@ -1431,11 +1392,6 @@
|
|||||||
"System.Security.Cryptography.ProtectedData": "4.4.0"
|
"System.Security.Cryptography.ProtectedData": "4.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.Diagnostics.DiagnosticSource": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.9",
|
|
||||||
"contentHash": "8hy61dsFYYSDjT9iTAfygGMU3A0EAnG69x5FUXeKsCjMhBmtTBt4UMUEW3ipprFoorOW6Jw/7hDMjXtlrsOvVQ=="
|
|
||||||
},
|
|
||||||
"System.IdentityModel.Tokens.Jwt": {
|
"System.IdentityModel.Tokens.Jwt": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.1",
|
"resolved": "8.0.1",
|
||||||
@@ -1450,33 +1406,10 @@
|
|||||||
"resolved": "6.0.0",
|
"resolved": "6.0.0",
|
||||||
"contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g=="
|
"contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g=="
|
||||||
},
|
},
|
||||||
"System.IO.Pipelines": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "5.0.1",
|
|
||||||
"contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg=="
|
|
||||||
},
|
|
||||||
"System.Memory": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.5",
|
|
||||||
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw=="
|
|
||||||
},
|
|
||||||
"System.Memory.Data": {
|
"System.Memory.Data": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "6.0.0",
|
"resolved": "6.0.0",
|
||||||
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ==",
|
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ=="
|
||||||
"dependencies": {
|
|
||||||
"System.Text.Json": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Numerics.Vectors": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.0",
|
|
||||||
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
|
|
||||||
},
|
|
||||||
"System.Reflection.Emit.Lightweight": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.7.0",
|
|
||||||
"contentHash": "a4OLB4IITxAXJeV74MDx49Oq2+PsF6Sml54XAFv+2RyWwtDBcabzoxiiJRhdhx+gaohLh4hEGCLQyBozXoQPqA=="
|
|
||||||
},
|
},
|
||||||
"System.Security.Cryptography.Pkcs": {
|
"System.Security.Cryptography.Pkcs": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -1496,16 +1429,6 @@
|
|||||||
"System.Security.Cryptography.Pkcs": "9.0.2"
|
"System.Security.Cryptography.Pkcs": "9.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.Text.Json": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.5",
|
|
||||||
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg=="
|
|
||||||
},
|
|
||||||
"System.Threading.Tasks.Extensions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.4",
|
|
||||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
|
|
||||||
},
|
|
||||||
"archmaester": {
|
"archmaester": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1545,7 +1468,7 @@
|
|||||||
"Hipster.Api": "[1.0.1, )",
|
"Hipster.Api": "[1.0.1, )",
|
||||||
"Oceanbox.DataAgent": "[7.3.0, )",
|
"Oceanbox.DataAgent": "[7.3.0, )",
|
||||||
"Oceanbox.DataAgent.Api": "[7.2.1, )",
|
"Oceanbox.DataAgent.Api": "[7.2.1, )",
|
||||||
"Oceanbox.ServerPack": "[1.36.0, )",
|
"Oceanbox.ServerPack": "[1.40.0, )",
|
||||||
"Petimeter.Api": "[1.0.0, )"
|
"Petimeter.Api": "[1.0.0, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1731,10 +1654,7 @@
|
|||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.5.0, )",
|
"requested": "[2.5.0, )",
|
||||||
"resolved": "2.5.0",
|
"resolved": "2.5.0",
|
||||||
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
|
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
|
||||||
"dependencies": {
|
|
||||||
"System.Memory": "4.5.4"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Newtonsoft.Json": {
|
"Newtonsoft.Json": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
@@ -1799,8 +1719,7 @@
|
|||||||
"contentHash": "vlOKvmigJ3Sumoulp1HwCTFXgX4KuERVGIIw4ZqmhgUJnSiApDmY183ddzzHo2FIdIJ8vGwrMGx98v9cLAezFA==",
|
"contentHash": "vlOKvmigJ3Sumoulp1HwCTFXgX4KuERVGIIw4ZqmhgUJnSiApDmY183ddzzHo2FIdIJ8vGwrMGx98v9cLAezFA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Http": "9.0.9",
|
"Microsoft.Extensions.Http": "9.0.9",
|
||||||
"System.ComponentModel.Annotations": "5.0.0",
|
"System.ComponentModel.Annotations": "5.0.0"
|
||||||
"System.Diagnostics.DiagnosticSource": "9.0.9"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ProjNet.FSharp": {
|
"ProjNet.FSharp": {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fga": {
|
"fga": {
|
||||||
"apiUrl": "http://prod-openfga.openfga.svc.cluster.local:8080",
|
"apiUrl": "http://staging-openfga.staging-openfga.svc.cluster.local:8080",
|
||||||
"apiKey": "",
|
"apiKey": "",
|
||||||
"storeId": "01JKTZXMP7ANN4GG2P5W8Y56M6",
|
"storeId": "01JKTZXMP7ANN4GG2P5W8Y56M6",
|
||||||
"modelId": "01JKTZYMCZZBVSBG66W27XMW0A"
|
"modelId": "01JKTZYMCZZBVSBG66W27XMW0A"
|
||||||
@@ -63,6 +63,8 @@
|
|||||||
"allowedOrigins": [
|
"allowedOrigins": [
|
||||||
"http://*.oceanbox.io",
|
"http://*.oceanbox.io",
|
||||||
"https://*.oceanbox.io",
|
"https://*.oceanbox.io",
|
||||||
|
"https://*.oceanbox.io:8080",
|
||||||
|
"https://*.oceanbox.io:10380",
|
||||||
"https://*.vtn.obx",
|
"https://*.vtn.obx",
|
||||||
"https://*.tox.obx",
|
"https://*.tox.obx",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
replicaCount: 1
|
||||||
|
|
||||||
image:
|
image:
|
||||||
tag: latest
|
tag: latest
|
||||||
|
|
||||||
podAnnotations:
|
podAnnotations:
|
||||||
dapr.io/enabled: "true"
|
dapr.io/enabled: "true"
|
||||||
dapr.io/app-id: "<x>-atlantis"
|
dapr.io/app-id: "<x>-atlantis"
|
||||||
dapr.io/app-port: "8085"
|
dapr.io/app-port: "8085"
|
||||||
dapr.io/api-token-secret: "dapr-api-token"
|
dapr.io/api-token-secret: "dapr-api-token"
|
||||||
dapr.io/config: "tracing"
|
dapr.io/config: "tracing"
|
||||||
dapr.io/app-protocol: "http"
|
dapr.io/app-protocol: "http"
|
||||||
dapr.io/log-as-json: "true"
|
dapr.io/log-as-json: "true"
|
||||||
dapr.io/sidecar-cpu-request: "10m"
|
dapr.io/sidecar-cpu-request: "10m"
|
||||||
dapr.io/sidecar-memory-request: "50Mi"
|
dapr.io/sidecar-memory-request: "50Mi"
|
||||||
# dapr.io/sidecar-cpu-limit: "300m"
|
# dapr.io/sidecar-cpu-limit: "300m"
|
||||||
# dapr.io/sidecar-memory-limit: "1000Mi"
|
# dapr.io/sidecar-memory-limit: "1000Mi"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- name: APP_NAMESPACE
|
- name: APP_NAMESPACE
|
||||||
value: <x>-atlantis
|
value: <x>-atlantis
|
||||||
- name: APP_VERSION
|
- name: APP_VERSION
|
||||||
value: "<x>-tilt"
|
value: "<x>-tilt"
|
||||||
- name: LOG_LEVEL
|
- name: LOG_LEVEL
|
||||||
value: "verbose"
|
value: "verbose"
|
||||||
- name: REDIS_USER
|
- name: REDIS_USER
|
||||||
value: default
|
value: default
|
||||||
- name: REDIS_PASSWORD
|
- name: REDIS_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: <x>-atlantis-redis
|
name: <x>-atlantis-redis
|
||||||
key: redis-password
|
key: redis-password
|
||||||
- name: DB_HOST
|
- name: DB_HOST
|
||||||
value: <x>-atlantis-db-rw
|
value: <x>-atlantis-db-rw
|
||||||
- name: DB_PORT
|
- name: DB_PORT
|
||||||
value: "5432"
|
value: "5432"
|
||||||
- name: DB_USER
|
- name: DB_USER
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: <x>-atlantis-db-app
|
name: <x>-atlantis-db-app
|
||||||
key: username
|
key: username
|
||||||
- name: DB_PASSWORD
|
- name: DB_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: <x>-atlantis-db-app
|
name: <x>-atlantis-db-app
|
||||||
key: password
|
key: password
|
||||||
- name: DAPR_API_TOKEN
|
- name: DAPR_API_TOKEN
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: dapr-api-token
|
name: dapr-api-token
|
||||||
key: token
|
key: token
|
||||||
- name: ANALYTICS_WEB_ID
|
- name: ANALYTICS_WEB_ID
|
||||||
value: 6f26c702-2c6d-46ea-8122-ffcedda5f762
|
value: 6f26c702-2c6d-46ea-8122-ffcedda5f762
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
annotations:
|
annotations:
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-staging
|
cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||||
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
|
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
|
||||||
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||||
hosts:
|
hosts:
|
||||||
- host: <x>-atlantis.dev.oceanbox.io
|
- host: <x>-atlantis.dev.oceanbox.io
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
pathType: ImplementationSpecific
|
pathType: ImplementationSpecific
|
||||||
internal:
|
internal:
|
||||||
- path: /internal
|
- path: /internal
|
||||||
pathType: ImplementationSpecific
|
pathType: ImplementationSpecific
|
||||||
- path: /dapr
|
- path: /dapr
|
||||||
pathType: ImplementationSpecific
|
pathType: ImplementationSpecific
|
||||||
- path: /actors
|
- path: /actors
|
||||||
pathType: ImplementationSpecific
|
pathType: ImplementationSpecific
|
||||||
- path: /job
|
- path: /job
|
||||||
pathType: ImplementationSpecific
|
pathType: ImplementationSpecific
|
||||||
- path: /events
|
- path: /events
|
||||||
pathType: ImplementationSpecific
|
pathType: ImplementationSpecific
|
||||||
- path: /metrics
|
- path: /metrics
|
||||||
pathType: ImplementationSpecific
|
pathType: ImplementationSpecific
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- <x>-atlantis.dev.oceanbox.io
|
- <x>-atlantis.dev.oceanbox.io
|
||||||
secretName: <x>-atlantis-tls
|
secretName: <x>-atlantis-tls
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
enabled: true
|
enabled: true
|
||||||
size: 1G
|
size: 1G
|
||||||
accessMode: ReadWriteOnce
|
accessMode: ReadWriteOnce
|
||||||
storageClass: ceph-rdb
|
storageClass: ceph-rdb
|
||||||
|
|
||||||
securityContext:
|
securityContext:
|
||||||
capabilities:
|
capabilities:
|
||||||
drop:
|
drop:
|
||||||
- ALL
|
- ALL
|
||||||
readOnlyRootFilesystem: false
|
readOnlyRootFilesystem: false
|
||||||
runAsNonRoot: false
|
runAsNonRoot: false
|
||||||
runAsUser: 0
|
runAsUser: 0
|
||||||
|
|
||||||
cluster:
|
cluster:
|
||||||
backup:
|
backup:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ variables:
|
|||||||
|
|
||||||
include:
|
include:
|
||||||
- project: oceanbox/gitlab-ci
|
- project: oceanbox/gitlab-ci
|
||||||
ref: v4.4
|
ref: v4.5
|
||||||
file: DotnetDeployment.gitlab-ci.yml
|
file: DotnetDeployment.gitlab-ci.yml
|
||||||
inputs:
|
inputs:
|
||||||
project-name: codex
|
project-name: codex
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY dist /app
|
COPY dist /app
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name='codex'
|
|||||||
cluster='oceanbox'
|
cluster='oceanbox'
|
||||||
|
|
||||||
env=os.getenv('APP_ENV')
|
env=os.getenv('APP_ENV')
|
||||||
|
username=os.getenv('USER')
|
||||||
namespace=os.getenv('APP_NAMESPACE')
|
namespace=os.getenv('APP_NAMESPACE')
|
||||||
|
|
||||||
app = '{}-{}'.format(env, name)
|
app = '{}-{}'.format(env, name)
|
||||||
@@ -39,7 +40,9 @@ docker_build_with_restart(
|
|||||||
ignore = [ 'src/Client' ]
|
ignore = [ 'src/Client' ]
|
||||||
)
|
)
|
||||||
|
|
||||||
k8s_yaml('tilt/k8s.yaml')
|
local('cat tilt/k8s.yaml | sed "s/<x>/{}/" > tilt/_k8s.yaml'.format(username))
|
||||||
|
|
||||||
|
k8s_yaml('tilt/_k8s.yaml')
|
||||||
k8s_resource(app, port_forwards='8085:8085')
|
k8s_resource(app, port_forwards='8085:8085')
|
||||||
|
|
||||||
# vim:ft=python
|
# vim:ft=python
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
}:
|
}:
|
||||||
dockerTools.buildLayeredImage {
|
dockerTools.buildLayeredImage {
|
||||||
name = "Codex";
|
name = "Codex";
|
||||||
tag = "0.0.0-alpha.1";
|
tag = "0.0.1";
|
||||||
created = "now";
|
created = "now";
|
||||||
|
|
||||||
contents = [
|
contents = [
|
||||||
@@ -24,4 +24,4 @@ dockerTools.buildLayeredImage {
|
|||||||
cmd = [ "Codex.Server" ];
|
cmd = [ "Codex.Server" ];
|
||||||
workingDir = "/app";
|
workingDir = "/app";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ open Fable.Core
|
|||||||
open Fable.Core.JsInterop
|
open Fable.Core.JsInterop
|
||||||
open Feliz
|
open Feliz
|
||||||
open Feliz.Router
|
open Feliz.Router
|
||||||
|
open FS.FluentUI
|
||||||
|
|
||||||
module Archive =
|
module Archive =
|
||||||
let private fetchArchiveAcl (id: System.Guid) : JS.Promise<Result<Archmaester.Dto.ArchiveAcl, string>> =
|
let private fetchArchiveAcl (id: System.Guid) : JS.Promise<Result<Archmaester.Dto.ArchiveAcl, string>> =
|
||||||
@@ -29,16 +30,6 @@ module Archive =
|
|||||||
return subs
|
return subs
|
||||||
}
|
}
|
||||||
|
|
||||||
let private deleteArchive (id: System.Guid) =
|
|
||||||
promise {
|
|
||||||
try
|
|
||||||
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
|
|
||||||
return res
|
|
||||||
with e ->
|
|
||||||
console.error("Error deleting archive: %o", e)
|
|
||||||
return Error "Error deleting archive"
|
|
||||||
}
|
|
||||||
|
|
||||||
let private addArchiveGroup (archiveId: System.Guid) (group: string) =
|
let private addArchiveGroup (archiveId: System.Guid) (group: string) =
|
||||||
promise {
|
promise {
|
||||||
try
|
try
|
||||||
@@ -227,9 +218,6 @@ module Archive =
|
|||||||
let View (archiveId: System.Guid) =
|
let View (archiveId: System.Guid) =
|
||||||
let loading, setLoading = React.useState true
|
let loading, setLoading = React.useState true
|
||||||
let error, setError = React.useState<string option> None
|
let error, setError = React.useState<string option> None
|
||||||
let editing, setEditing = React.useState false
|
|
||||||
let deleting, setDeleting = React.useState false
|
|
||||||
let deleted, setDeleted = React.useState false
|
|
||||||
let selectedGroup, setSelectedGroup = React.useState<string option> None
|
let selectedGroup, setSelectedGroup = React.useState<string option> None
|
||||||
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
|
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
|
||||||
let aclOpt, setAcl = React.useState<Archmaester.Dto.ArchiveAcl option> None
|
let aclOpt, setAcl = React.useState<Archmaester.Dto.ArchiveAcl option> None
|
||||||
@@ -257,21 +245,6 @@ module Archive =
|
|||||||
| None ->
|
| None ->
|
||||||
console.warn("ACL has not been downloaded")
|
console.warn("ACL has not been downloaded")
|
||||||
|
|
||||||
let handleDeleteArchive (id: System.Guid) =
|
|
||||||
deleteArchive id
|
|
||||||
|> Promise.iter (fun res ->
|
|
||||||
match res with
|
|
||||||
| Ok deleted ->
|
|
||||||
if deleted then
|
|
||||||
console.info("Archive deleted successfully")
|
|
||||||
setDeleted true
|
|
||||||
else
|
|
||||||
setError (Some "Failed to delete archive")
|
|
||||||
| Error err ->
|
|
||||||
console.error("Error deleting archive: %s", err)
|
|
||||||
setError (Some err)
|
|
||||||
)
|
|
||||||
|
|
||||||
let handleSelectedGroupChange (groupOpt: string option) =
|
let handleSelectedGroupChange (groupOpt: string option) =
|
||||||
console.debug("Selected group: %s", groupOpt)
|
console.debug("Selected group: %s", groupOpt)
|
||||||
setSelectedGroup groupOpt
|
setSelectedGroup groupOpt
|
||||||
@@ -319,94 +292,27 @@ module Archive =
|
|||||||
| None ->
|
| None ->
|
||||||
match archiveOpt with
|
match archiveOpt with
|
||||||
| Some archive ->
|
| Some archive ->
|
||||||
Html.h1 (sprintf "Archive %s" archive.name)
|
Fui.text.title1 (sprintf "Archive %s" archive.name)
|
||||||
|
|
||||||
if deleting then
|
Html.div [
|
||||||
Html.h2 "Deleting archive ..."
|
prop.classes [ "flex-row"; "gap-8" ]
|
||||||
else
|
prop.children [
|
||||||
Html.none
|
Archives.EditArchiveDialog archive (fun edited ->
|
||||||
|
Some
|
||||||
if deleted then
|
{archive with
|
||||||
Html.div [
|
name = edited.Name
|
||||||
prop.children [
|
startTime = edited.StartTime
|
||||||
Html.h2 "Archive successfully deleted"
|
endTime = edited.StartTime.AddHours(edited.Frames)
|
||||||
Html.a [
|
frames = edited.Frames
|
||||||
prop.href (Router.format "archives")
|
isPublished = edited.Published
|
||||||
prop.text "Return to archives listing"
|
isPublic = edited.Public
|
||||||
]
|
}
|
||||||
]
|
|> setArchive
|
||||||
|
console.debug ("response: ", edited)
|
||||||
|
)
|
||||||
|
Archives.DeleteArchiveDialog archive
|
||||||
]
|
]
|
||||||
else
|
]
|
||||||
Html.div [
|
|
||||||
prop.classes [ "flex-row"; "gap-8" ]
|
|
||||||
prop.children [
|
|
||||||
if deleting then
|
|
||||||
Html.button [
|
|
||||||
prop.onClick (fun ev ->
|
|
||||||
setDeleting false
|
|
||||||
handleDeleteArchive archive.archiveId
|
|
||||||
)
|
|
||||||
prop.text "Save"
|
|
||||||
]
|
|
||||||
|
|
||||||
Html.button [
|
|
||||||
prop.onClick (fun ev ->
|
|
||||||
setDeleting false
|
|
||||||
)
|
|
||||||
prop.text "Cancel"
|
|
||||||
]
|
|
||||||
elif editing then
|
|
||||||
Html.button [
|
|
||||||
prop.onClick (fun ev ->
|
|
||||||
setEditing false
|
|
||||||
)
|
|
||||||
prop.text "Save"
|
|
||||||
]
|
|
||||||
|
|
||||||
Html.button [
|
|
||||||
prop.onClick (fun ev ->
|
|
||||||
setEditing false
|
|
||||||
)
|
|
||||||
prop.text "Cancel"
|
|
||||||
]
|
|
||||||
else
|
|
||||||
Html.button [
|
|
||||||
prop.onClick (fun ev ->
|
|
||||||
setEditing true
|
|
||||||
)
|
|
||||||
prop.text "Edit"
|
|
||||||
]
|
|
||||||
|
|
||||||
Html.button [
|
|
||||||
prop.onClick (fun ev ->
|
|
||||||
setDeleting true
|
|
||||||
)
|
|
||||||
prop.text "Delete"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
if editing then
|
|
||||||
Html.form [
|
|
||||||
prop.children [
|
|
||||||
Html.div [
|
|
||||||
prop.children [
|
|
||||||
Html.label [
|
|
||||||
prop.htmlFor "published-checkbox"
|
|
||||||
prop.text "Published: "
|
|
||||||
]
|
|
||||||
|
|
||||||
Html.input [
|
|
||||||
prop.id "published-checkbox"
|
|
||||||
prop.type' "checkbox"
|
|
||||||
prop.custom ("checked", archive.isPublished)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
else
|
|
||||||
Html.none
|
|
||||||
|
|
||||||
Archives.InfoSection archive
|
Archives.InfoSection archive
|
||||||
|
|
||||||
@@ -744,4 +650,4 @@ module Archive =
|
|||||||
|
|
||||||
| None ->
|
| None ->
|
||||||
Html.h1 "Archive not found"
|
Html.h1 "Archive not found"
|
||||||
]
|
]
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
namespace Oceanbox.Codex
|
namespace Oceanbox.Codex
|
||||||
|
|
||||||
|
open Fable.Core
|
||||||
open Feliz
|
open Feliz
|
||||||
|
open FS.FluentUI
|
||||||
|
|
||||||
type Archives =
|
type Archives =
|
||||||
[<ReactComponent>]
|
[<ReactComponent>]
|
||||||
@@ -13,49 +15,131 @@ type Archives =
|
|||||||
Html.section [
|
Html.section [
|
||||||
prop.classes [ "flex-row"; "flex-wrap"; "gap-16" ]
|
prop.classes [ "flex-row"; "flex-wrap"; "gap-16" ]
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.ul [
|
Fui.table [
|
||||||
prop.children [
|
table.classes [ "flex-basis-7"; "flex-grow" ]
|
||||||
Html.li [
|
table.children [
|
||||||
prop.text (sprintf "Description: %s" archive.description)
|
Fui.tableBody [
|
||||||
]
|
Fui.tableRow [
|
||||||
Html.li [
|
tableRow.children [
|
||||||
prop.text (sprintf "Archive type: %s" (string archive.archiveType))
|
Fui.tableCell [
|
||||||
]
|
Fui.tableCellLayout [
|
||||||
Html.li [
|
tableCellLayout.media (Fui.icon.textDescriptionRegular [])
|
||||||
prop.text (sprintf "Projection: %s" archive.projection)
|
tableCellLayout.children [ Fui.text "Description" ]
|
||||||
]
|
]
|
||||||
Html.li [
|
]
|
||||||
prop.text (sprintf "Frequency: %d" archive.freq)
|
Fui.tableCell [ Fui.text archive.description ]
|
||||||
]
|
]
|
||||||
Html.li [
|
]
|
||||||
prop.text (sprintf "Frames: %d" archive.frames)
|
Fui.tableRow [
|
||||||
]
|
tableRow.children [
|
||||||
Html.li [
|
Fui.tableCell [
|
||||||
prop.text (sprintf "Created: %s" (archive.created.ToLongDateString()))
|
Fui.tableCellLayout [
|
||||||
]
|
tableCellLayout.media (Fui.icon.contactCardGenericRegular [])
|
||||||
Html.li [
|
tableCellLayout.children [ Fui.text "Archive type" ]
|
||||||
prop.text (sprintf "Start time: %s" (archive.startTime.ToLongDateString()))
|
]
|
||||||
]
|
]
|
||||||
Html.li [
|
Fui.tableCell [ Fui.text (string archive.archiveType) ]
|
||||||
prop.text (sprintf "End time: %s" (archive.endTime.ToLongDateString()))
|
]
|
||||||
]
|
]
|
||||||
Html.li [
|
Fui.tableRow [
|
||||||
prop.text (sprintf "Length: %d days %d hours" archiveLength.Days archiveLength.Hours)
|
tableRow.children [
|
||||||
]
|
Fui.tableCell [
|
||||||
Html.li [
|
Fui.tableCellLayout [
|
||||||
prop.text (sprintf "Owner: %s" archive.owner)
|
tableCellLayout.media (Fui.icon.globeSurfaceRegular [])
|
||||||
]
|
tableCellLayout.children [ Fui.text "Projection" ]
|
||||||
Html.li [
|
]
|
||||||
prop.text (sprintf "Expires: %s" (archive.expires |> Option.map string |> Option.defaultValue ""))
|
]
|
||||||
]
|
Fui.tableCell [ Fui.text archive.projection ]
|
||||||
Html.li [
|
]
|
||||||
prop.text (sprintf "Publised: %b" archive.isPublished)
|
]
|
||||||
]
|
Fui.tableRow [
|
||||||
Html.li [
|
tableRow.children [
|
||||||
prop.text (sprintf "Public: %b" archive.isPublic)
|
Fui.tableCell [
|
||||||
]
|
Fui.tableCellLayout [
|
||||||
Html.li [
|
tableCellLayout.media (Fui.icon.timerRegular [])
|
||||||
prop.text (sprintf "Location: %s" "tos")
|
tableCellLayout.children [ Fui.text "Frequency" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [ Fui.text (string archive.freq) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.media (Fui.icon.filmstripRegular [])
|
||||||
|
tableCellLayout.children [ Fui.text "Frames" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [ Fui.text (string archive.frames) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.media (Fui.icon.calendarAddRegular [])
|
||||||
|
tableCellLayout.children [ Fui.text "Time created" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [ Fui.text (archive.created.ToLongDateString()) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.media (Fui.icon.playRegular [])
|
||||||
|
tableCellLayout.children [ Fui.text "Start time" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [ Fui.text (archive.startTime.ToLongDateString()) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.media (Fui.icon.stopRegular [])
|
||||||
|
tableCellLayout.children [ Fui.text "End time" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [ Fui.text (archive.endTime.ToLongDateString()) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.media (Fui.icon.autoFitWidthRegular [])
|
||||||
|
tableCellLayout.children [ Fui.text "Length" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [ Fui.text (sprintf "%d days %d hours" archiveLength.Days archiveLength.Hours) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.media (Fui.icon.eyeRegular [])
|
||||||
|
tableCellLayout.children [ Fui.text "Published" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [ Fui.text (string archive.isPublished) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.media (Fui.icon.peopleEyeRegular [])
|
||||||
|
tableCellLayout.children [ Fui.text "Public" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [ Fui.text (string archive.isPublic) ]
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -70,3 +154,342 @@ type Archives =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[<ReactComponent>]
|
||||||
|
static member EditArchiveDialog(archive: Archmaester.Dto.ArchiveProps) onEdit =
|
||||||
|
let initForm : Remoting.EditArchiveRequest =
|
||||||
|
{
|
||||||
|
Name = archive.name
|
||||||
|
StartTime = archive.startTime
|
||||||
|
Frames = archive.frames
|
||||||
|
Published = archive.isPublished
|
||||||
|
Public = archive.isPublic
|
||||||
|
}
|
||||||
|
let form, setForm = React.useState initForm
|
||||||
|
let isOpen, setIsOpen = React.useState false
|
||||||
|
let errMsg, setErrMsg = React.useState None
|
||||||
|
let dataSet, setDataSet = React.useState None
|
||||||
|
|
||||||
|
React.useEffectOnce (fun _ ->
|
||||||
|
Remoting.adminApi.getArchiveDataSet archive.archiveId
|
||||||
|
|> Async.StartAsPromise
|
||||||
|
|> Promise.iter (fun res ->
|
||||||
|
match res with
|
||||||
|
| Ok ds -> setDataSet (Some ds)
|
||||||
|
| Error msg ->
|
||||||
|
Browser.Dom.console.error("Error fetching dataset: %s", msg)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let handleEditArchive () =
|
||||||
|
let utcTime = System.DateTime(form.StartTime.Year, form.StartTime.Month, form.StartTime.Day, 0, 0, 0, System.DateTimeKind.Utc)
|
||||||
|
Remoting.adminApi.updateArchive archive.archiveId {form with StartTime = utcTime}
|
||||||
|
|> Async.StartAsPromise
|
||||||
|
|> Promise.iter (fun res ->
|
||||||
|
match res with
|
||||||
|
| Ok newArchive ->
|
||||||
|
Browser.Dom.console.info("Added archive %s with id %s", newArchive.Name, string newArchive.Id)
|
||||||
|
onEdit newArchive
|
||||||
|
setIsOpen false
|
||||||
|
| Error msg ->
|
||||||
|
Browser.Dom.console.error("Error adding archive %s: %s", form.Name, msg)
|
||||||
|
setErrMsg (Some msg)
|
||||||
|
)
|
||||||
|
|
||||||
|
let framesExceedEnd =
|
||||||
|
dataSet
|
||||||
|
|> Option.map (fun ds ->
|
||||||
|
form.StartTime.AddHours(form.Frames) > ds.EndTime
|
||||||
|
)
|
||||||
|
|> Option.defaultValue false
|
||||||
|
|
||||||
|
Fui.dialog [
|
||||||
|
dialog.open' isOpen
|
||||||
|
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
|
||||||
|
setIsOpen d.``open``
|
||||||
|
// NOTE: Reset form on open, so it's not noticeable in the UI
|
||||||
|
if d.``open`` then
|
||||||
|
setForm initForm
|
||||||
|
)
|
||||||
|
dialog.children [
|
||||||
|
Fui.dialogTrigger [
|
||||||
|
dialogTrigger.children (
|
||||||
|
Fui.button [
|
||||||
|
button.icon (Fui.icon.editRegular [])
|
||||||
|
button.text "Edit"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.dialogSurface [
|
||||||
|
dialogSurface.classes []
|
||||||
|
dialogSurface.children [
|
||||||
|
Fui.dialogBody [
|
||||||
|
dialogBody.classes []
|
||||||
|
dialogBody.children [
|
||||||
|
Fui.dialogTitle [
|
||||||
|
dialogTitle.text (sprintf "Edit archive %s" archive.name)
|
||||||
|
dialogTitle.action (
|
||||||
|
Fui.dialogTrigger [
|
||||||
|
dialogTrigger.action.close
|
||||||
|
dialogTrigger.children (
|
||||||
|
Fui.button [
|
||||||
|
button.appearance.transparent
|
||||||
|
button.icon (Fui.icon.dismissRegular [])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.dialogContent [
|
||||||
|
dialogContent.classes ["flex-column"; "gap-8"]
|
||||||
|
dialogContent.children [
|
||||||
|
Fui.field [
|
||||||
|
field.label (
|
||||||
|
Fui.label [
|
||||||
|
label.required true
|
||||||
|
label.text "Name"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
field.children (
|
||||||
|
Fui.input [
|
||||||
|
input.value form.Name
|
||||||
|
input.onChange (fun (v: ValueProp<string>) ->
|
||||||
|
if v.value.Length <= 80 then
|
||||||
|
setForm {form with Name = v.value}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
field.hint $"{form.Name.Length}/80"
|
||||||
|
]
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["flex-row"; "gap-8"]
|
||||||
|
prop.children [
|
||||||
|
Fui.field [
|
||||||
|
field.label (
|
||||||
|
Fui.label [
|
||||||
|
label.required true
|
||||||
|
label.text "Start Date"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
field.children (
|
||||||
|
Fui.datePicker [
|
||||||
|
datePicker.placeholder "Select a date..."
|
||||||
|
datePicker.showWeekNumbers true
|
||||||
|
datePicker.formatDate (fun d -> d.ToShortDateString())
|
||||||
|
match dataSet with
|
||||||
|
| None -> ()
|
||||||
|
| Some ds ->
|
||||||
|
datePicker.minDate ds.StartTime
|
||||||
|
datePicker.maxDate ds.EndTime
|
||||||
|
datePicker.value (Some form.StartTime)
|
||||||
|
datePicker.onSelectDate (fun d ->
|
||||||
|
d |> Option.iter (fun d' ->
|
||||||
|
setForm {form with StartTime = d'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.field [
|
||||||
|
field.label (
|
||||||
|
Fui.label [
|
||||||
|
label.required true
|
||||||
|
label.text "Days"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
field.children (
|
||||||
|
Fui.spinButton [
|
||||||
|
spinButton.value (form.Frames / 24)
|
||||||
|
spinButton.min 1
|
||||||
|
spinButton.onChange (fun (d: SpinButtonOnChangeData) ->
|
||||||
|
match d.value with
|
||||||
|
| Some v ->
|
||||||
|
setForm {form with Frames = v * 24}
|
||||||
|
| None ->
|
||||||
|
if d.displayValue.ToCharArray() |> Array.forall System.Char.IsDigit then
|
||||||
|
setForm {form with Frames = int d.displayValue * 24}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.text.caption1 [
|
||||||
|
if framesExceedEnd then
|
||||||
|
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||||
|
|
||||||
|
let endDate = form.StartTime.AddHours(form.Frames).ToShortDateString()
|
||||||
|
text.text
|
||||||
|
($"End Date: {endDate}, {form.Frames} frames"
|
||||||
|
+
|
||||||
|
if framesExceedEnd then
|
||||||
|
" (Exceeds DataSet Bounds!)"
|
||||||
|
else
|
||||||
|
"")
|
||||||
|
]
|
||||||
|
Fui.checkbox [
|
||||||
|
checkbox.label "Published"
|
||||||
|
checkbox.checked' form.Published
|
||||||
|
checkbox.onCheckedChange (fun c ->
|
||||||
|
if not c then
|
||||||
|
setForm {form with Published = c; Public = c}
|
||||||
|
else
|
||||||
|
setForm {form with Published = c}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.checkbox [
|
||||||
|
checkbox.label "Public"
|
||||||
|
checkbox.checked' form.Public
|
||||||
|
checkbox.onCheckedChange (fun c -> setForm {form with Public = c})
|
||||||
|
]
|
||||||
|
match errMsg with
|
||||||
|
| None -> Html.none
|
||||||
|
| Some msg ->
|
||||||
|
Fui.text [
|
||||||
|
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||||
|
text.text msg
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.dialogActions [
|
||||||
|
dialogActions.position.end'
|
||||||
|
dialogActions.children [
|
||||||
|
Fui.dialogTrigger [
|
||||||
|
dialogTrigger.action.close
|
||||||
|
dialogTrigger.children (
|
||||||
|
Fui.button [
|
||||||
|
button.icon (Fui.icon.dismissRegular [])
|
||||||
|
button.text "Cancel"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.button [
|
||||||
|
button.appearance.primary
|
||||||
|
button.icon (Fui.icon.saveRegular [])
|
||||||
|
button.text "Save changes"
|
||||||
|
button.disabled <| (form = initForm)
|
||||||
|
button.onClick (fun _ -> handleEditArchive ())
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[<ReactComponent>]
|
||||||
|
static member DeleteArchiveDialog(archive: Archmaester.Dto.ArchiveProps) =
|
||||||
|
let isOpen, setIsOpen = React.useState false
|
||||||
|
let userConfirmed, setUserConfirmed = React.useState false
|
||||||
|
let errMsg, setErrMsg = React.useState None
|
||||||
|
|
||||||
|
let deleteArchive (id: System.Guid) =
|
||||||
|
promise {
|
||||||
|
try
|
||||||
|
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
|
||||||
|
return res
|
||||||
|
with e ->
|
||||||
|
Browser.Dom.console.error("Error deleting archive: %o", e)
|
||||||
|
return Error "Error deleting archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
let handleDeleteArchive () =
|
||||||
|
deleteArchive archive.archiveId
|
||||||
|
|> Promise.iter (fun res ->
|
||||||
|
match res with
|
||||||
|
| Ok deleted ->
|
||||||
|
if deleted then
|
||||||
|
Browser.Dom.console.info("Archive deleted successfully")
|
||||||
|
setIsOpen false
|
||||||
|
Router.Router.navigateBack ()
|
||||||
|
else
|
||||||
|
setErrMsg (Some "Failed to delete archive")
|
||||||
|
| Error err ->
|
||||||
|
Browser.Dom.console.error("Error deleting archive: %s", err)
|
||||||
|
setErrMsg (Some err)
|
||||||
|
)
|
||||||
|
|
||||||
|
Fui.dialog [
|
||||||
|
dialog.open' isOpen
|
||||||
|
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
|
||||||
|
setIsOpen d.``open``
|
||||||
|
if d.``open`` then
|
||||||
|
setUserConfirmed false
|
||||||
|
setErrMsg None
|
||||||
|
)
|
||||||
|
dialog.children [
|
||||||
|
Fui.dialogTrigger [
|
||||||
|
dialogTrigger.children (
|
||||||
|
Fui.button [
|
||||||
|
button.icon (Fui.icon.deleteRegular [])
|
||||||
|
button.text "Delete"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.dialogSurface [
|
||||||
|
dialogSurface.classes []
|
||||||
|
dialogSurface.children [
|
||||||
|
Fui.dialogBody [
|
||||||
|
dialogBody.classes []
|
||||||
|
dialogBody.children [
|
||||||
|
Fui.dialogTitle [
|
||||||
|
dialogTitle.text (sprintf "Delete archive %s" archive.name)
|
||||||
|
dialogTitle.action (
|
||||||
|
Fui.dialogTrigger [
|
||||||
|
dialogTrigger.action.close
|
||||||
|
dialogTrigger.children (
|
||||||
|
Fui.button [
|
||||||
|
button.appearance.transparent
|
||||||
|
button.icon (Fui.icon.dismissRegular [])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.dialogContent [
|
||||||
|
dialogContent.classes ["flex-column"; "gap-8"]
|
||||||
|
dialogContent.children [
|
||||||
|
Fui.text "Are you sure you want to delete the archive? This action cannot be reverted!"
|
||||||
|
Fui.checkbox [
|
||||||
|
checkbox.label "Yes, I am sure"
|
||||||
|
checkbox.checked' userConfirmed
|
||||||
|
checkbox.onCheckedChange setUserConfirmed
|
||||||
|
]
|
||||||
|
match errMsg with
|
||||||
|
| None -> Html.none
|
||||||
|
| Some msg ->
|
||||||
|
Fui.text [
|
||||||
|
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||||
|
text.text msg
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.dialogActions [
|
||||||
|
dialogActions.position.end'
|
||||||
|
dialogActions.children [
|
||||||
|
Fui.dialogTrigger [
|
||||||
|
dialogTrigger.action.close
|
||||||
|
dialogTrigger.children (
|
||||||
|
Fui.button [
|
||||||
|
button.icon (Fui.icon.dismissRegular [])
|
||||||
|
button.text "Cancel"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.button [
|
||||||
|
button.appearance.primary
|
||||||
|
button.icon (Fui.icon.deleteRegular [])
|
||||||
|
button.text "Delete"
|
||||||
|
button.disabled (not userConfirmed)
|
||||||
|
button.onClick (fun _ -> handleDeleteArchive ())
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
@@ -97,7 +97,8 @@ type Archives =
|
|||||||
| Ok archives ->
|
| Ok archives ->
|
||||||
setArchives archives
|
setArchives archives
|
||||||
setError None
|
setError None
|
||||||
| Error err -> setError (Some "Error fetching archives")
|
| Error err ->
|
||||||
|
setError (Some "Error fetching archives")
|
||||||
|
|
||||||
setLoading false
|
setLoading false
|
||||||
)
|
)
|
||||||
@@ -196,4 +197,4 @@ type Archives =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -5,6 +5,7 @@ module Utils =
|
|||||||
open Fable.Core
|
open Fable.Core
|
||||||
open Fable.Core.JsInterop
|
open Fable.Core.JsInterop
|
||||||
open Fable.Remoting.Client
|
open Fable.Remoting.Client
|
||||||
|
open FsToolkit.ErrorHandling
|
||||||
|
|
||||||
open Oceanbox.Codex
|
open Oceanbox.Codex
|
||||||
open Oceanbox.Codex.Types
|
open Oceanbox.Codex.Types
|
||||||
@@ -71,3 +72,52 @@ module Utils =
|
|||||||
return Error "Error fetching archive count"
|
return Error "Error fetching archive count"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let private fetchArchmaesterArchives (group: string) : JS.Promise<Result<Archmaester.Dto.ArchiveProps array, string>> =
|
||||||
|
promise {
|
||||||
|
let filter : Archmaester.Dto.ArchiveFilter = {
|
||||||
|
id = None
|
||||||
|
searchTerm = None
|
||||||
|
archiveType = None
|
||||||
|
owner = None
|
||||||
|
user = None
|
||||||
|
groups = Some [| group |]
|
||||||
|
}
|
||||||
|
let! result = Remoting.adminApi.getArchives 0 -1 filter |> Async.StartAsPromise
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchGroupArchiveProps (group: string) : JS.Promise<Types.Archive array> =
|
||||||
|
promise {
|
||||||
|
let fgaUser = sprintf "group:%s#member" (Groups.Utils.canonicalizeName group)
|
||||||
|
let! archivesRes = fetchArchmaesterArchives group
|
||||||
|
let! viewsRes =
|
||||||
|
OpenFGA.fetchObjects(fgaUser, "view", "archive", context = {| time = System.DateTime.Now |})
|
||||||
|
let! execsRes =
|
||||||
|
OpenFGA.fetchObjects(fgaUser, "exec", "archive", context = {| task = "*"; usage = -1; time = System.DateTime.Now |})
|
||||||
|
|
||||||
|
let res =
|
||||||
|
result {
|
||||||
|
// TODO: Create specific exception
|
||||||
|
let! props = archivesRes |> Result.mapError System.Exception
|
||||||
|
let! views = viewsRes |> Result.mapError System.Exception
|
||||||
|
let! execs = execsRes |> Result.mapError System.Exception
|
||||||
|
let viewArchiveIds = views |> Array.choose extractFgaArchiveId
|
||||||
|
let execArchiveIds = execs |> Array.choose extractFgaArchiveId
|
||||||
|
let archives =
|
||||||
|
props
|
||||||
|
|> Array.map (fun prop -> {
|
||||||
|
Props = prop
|
||||||
|
CanView = viewArchiveIds |> Array.contains prop.archiveId
|
||||||
|
CanExec = execArchiveIds |> Array.contains prop.archiveId
|
||||||
|
})
|
||||||
|
|
||||||
|
return archives
|
||||||
|
}
|
||||||
|
|
||||||
|
match res with
|
||||||
|
| Ok archives ->
|
||||||
|
return archives
|
||||||
|
| Error ex ->
|
||||||
|
raise ex
|
||||||
|
return [||]
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RootNamespace>Oceanbox</RootNamespace>
|
<RootNamespace>Oceanbox</RootNamespace>
|
||||||
<Version>0.0.0-alpha.1</Version>
|
<Version>0.0.1</Version>
|
||||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="../Shared/Remoting.fs" />
|
<Compile Include="../Shared/Remoting.fs" />
|
||||||
<Compile Include="Types.fs" />
|
<Compile Include="Types.fs" />
|
||||||
|
<Compile Include="Utils.fs" />
|
||||||
<Compile Include="Extensions.fs" />
|
<Compile Include="Extensions.fs" />
|
||||||
<Compile Include="Remoting.fs" />
|
<Compile Include="Remoting.fs" />
|
||||||
<Compile Include="Map.fs" />
|
<Compile Include="Map.fs" />
|
||||||
@@ -18,6 +19,8 @@
|
|||||||
<Compile Include="OpenFGA/useReadTuples.fs" />
|
<Compile Include="OpenFGA/useReadTuples.fs" />
|
||||||
<Compile Include="OpenFGA/Checkbox.fs" />
|
<Compile Include="OpenFGA/Checkbox.fs" />
|
||||||
<Compile Include="OpenFGA/ArchiveOwnerList.fs" />
|
<Compile Include="OpenFGA/ArchiveOwnerList.fs" />
|
||||||
|
<Compile Include="Users/DeleteForm.fs" />
|
||||||
|
<Compile Include="Users/OpenFgaList.fs" />
|
||||||
<Compile Include="Groups/Utils.fs" />
|
<Compile Include="Groups/Utils.fs" />
|
||||||
<Compile Include="Groups/useGroups.fs" />
|
<Compile Include="Groups/useGroups.fs" />
|
||||||
<Compile Include="Groups/List.fs" />
|
<Compile Include="Groups/List.fs" />
|
||||||
@@ -34,6 +37,7 @@
|
|||||||
<Compile Include="Groups.fs" />
|
<Compile Include="Groups.fs" />
|
||||||
<Compile Include="GroupArchiveAddForm.fs" />
|
<Compile Include="GroupArchiveAddForm.fs" />
|
||||||
<Compile Include="GroupArchive.fs" />
|
<Compile Include="GroupArchive.fs" />
|
||||||
|
<Compile Include="GroupUser.fs" />
|
||||||
<Compile Include="Group.fs" />
|
<Compile Include="Group.fs" />
|
||||||
<Compile Include="ArchivesList.fs" />
|
<Compile Include="ArchivesList.fs" />
|
||||||
<Compile Include="Archives.fs" />
|
<Compile Include="Archives.fs" />
|
||||||
@@ -54,6 +58,7 @@
|
|||||||
<PackageReference Include="Fable.Remoting.Client" />
|
<PackageReference Include="Fable.Remoting.Client" />
|
||||||
<PackageReference Include="Feliz" />
|
<PackageReference Include="Feliz" />
|
||||||
<PackageReference Include="Feliz.Router" />
|
<PackageReference Include="Feliz.Router" />
|
||||||
|
<PackageReference Include="FS.FluentUI" />
|
||||||
<PackageReference Include="Feliz.UseElmish" />
|
<PackageReference Include="Feliz.UseElmish" />
|
||||||
<PackageReference Include="FsToolkit.ErrorHandling" />
|
<PackageReference Include="FsToolkit.ErrorHandling" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -4,8 +4,108 @@ open Browser
|
|||||||
open Fable.Core
|
open Fable.Core
|
||||||
open Feliz
|
open Feliz
|
||||||
open Feliz.Router
|
open Feliz.Router
|
||||||
|
open FS.FluentUI
|
||||||
|
|
||||||
|
type private NavTab =
|
||||||
|
| Index
|
||||||
|
| Archives
|
||||||
|
| ModelAreas
|
||||||
|
| Groups
|
||||||
|
| Organizations
|
||||||
|
|
||||||
|
override this.ToString () =
|
||||||
|
match this with
|
||||||
|
| Index -> ""
|
||||||
|
| Archives -> "archives"
|
||||||
|
| ModelAreas -> "model-areas"
|
||||||
|
| Groups -> "groups"
|
||||||
|
| Organizations -> "organizations"
|
||||||
|
|
||||||
|
static member FromString str =
|
||||||
|
match str with
|
||||||
|
| "archives" -> Archives
|
||||||
|
| "model-areas" -> ModelAreas
|
||||||
|
| "groups" -> Groups
|
||||||
|
| "organizations" -> Organizations
|
||||||
|
| _ -> Index
|
||||||
|
|
||||||
|
member this.ToPath () =
|
||||||
|
[this.ToString ()] |> Router.format
|
||||||
|
|
||||||
|
static member FromPath path =
|
||||||
|
match path with
|
||||||
|
| [] -> NavTab.Index
|
||||||
|
| list -> list |> List.head |> NavTab.FromString
|
||||||
|
|
||||||
type Components =
|
type Components =
|
||||||
|
/// <summary>
|
||||||
|
/// Navigation sidebar
|
||||||
|
/// </summary>
|
||||||
|
[<ReactComponent>]
|
||||||
|
static member Sidebar currentUrl : ReactElement =
|
||||||
|
let currentTab = NavTab.FromPath currentUrl
|
||||||
|
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["vh-100"; "w-256"; "br-solid"; "bcol-neutral-stroke-1"]
|
||||||
|
prop.children [
|
||||||
|
Fui.navDrawer [
|
||||||
|
navDrawer.style [style.height (length.perc 100); style.width (length.perc 100)]
|
||||||
|
navDrawer.selectedValue (currentTab.ToString())
|
||||||
|
navDrawer.type'.inline'
|
||||||
|
navDrawer.open' true
|
||||||
|
navDrawer.children [
|
||||||
|
Fui.navDrawerHeader [
|
||||||
|
Fui.link [
|
||||||
|
link.style [style.textDecoration.none]
|
||||||
|
link.href (NavTab.Index.ToPath ())
|
||||||
|
link.children [
|
||||||
|
Fui.text.title1 [
|
||||||
|
text.text "Codex"
|
||||||
|
text.style [style.color Theme.tokens.colorBrandForeground1]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.navDivider []
|
||||||
|
Fui.navDrawerBody [
|
||||||
|
navDrawerBody.children [
|
||||||
|
Fui.navItem [
|
||||||
|
navItem.icon (Fui.bundleIcon bundleIcons.home [icon.size.``20``])
|
||||||
|
navItem.href (NavTab.Index.ToPath ())
|
||||||
|
navItem.value (NavTab.Index.ToString ())
|
||||||
|
navItem.children [Html.text "Index"]
|
||||||
|
]
|
||||||
|
Fui.navItem [
|
||||||
|
navItem.icon (Fui.bundleIcon bundleIcons.archive <| [icon.size.``20``])
|
||||||
|
navItem.href (NavTab.Archives.ToPath ())
|
||||||
|
navItem.value (NavTab.Archives.ToString ())
|
||||||
|
navItem.children [Html.text "Archives"]
|
||||||
|
]
|
||||||
|
Fui.navItem [
|
||||||
|
navItem.icon (Fui.bundleIcon bundleIcons.map <| [icon.size.``20``])
|
||||||
|
navItem.href (NavTab.ModelAreas.ToPath ())
|
||||||
|
navItem.value (NavTab.ModelAreas.ToString ())
|
||||||
|
navItem.children [Html.text "Model Areas"]
|
||||||
|
]
|
||||||
|
Fui.navItem [
|
||||||
|
navItem.icon (Fui.bundleIcon bundleIcons.people <| [icon.size.``20``])
|
||||||
|
navItem.href (NavTab.Groups.ToPath ())
|
||||||
|
navItem.value (NavTab.Groups.ToString ())
|
||||||
|
navItem.children [Html.text "Groups"]
|
||||||
|
]
|
||||||
|
Fui.navItem [
|
||||||
|
navItem.icon (Fui.bundleIcon bundleIcons.organization <| [icon.size.``20``])
|
||||||
|
navItem.href (NavTab.Organizations.ToPath ())
|
||||||
|
navItem.value (NavTab.Organizations.ToString ())
|
||||||
|
navItem.children [Html.text "Organizations"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A React component that uses Feliz.Router to determine what to show based on the current URL
|
/// A React component that uses Feliz.Router to determine what to show based on the current URL
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -39,19 +139,30 @@ type Components =
|
|||||||
elif not authed then
|
elif not authed then
|
||||||
Html.h1 "Redirecting to sign-in..."
|
Html.h1 "Redirecting to sign-in..."
|
||||||
else
|
else
|
||||||
match currentUrl with
|
Html.div [
|
||||||
| [ ] -> Index.View()
|
prop.classes ["flex-row"; "gap-16"]
|
||||||
| [ "archives" ] -> Archives.View ()
|
prop.children [
|
||||||
| [ "archives"; archive ] -> Archive.View (System.Guid archive)
|
Components.Sidebar currentUrl
|
||||||
| [ "model-areas" ] -> ModelAreas.List ()
|
Html.div [
|
||||||
| [ "model-areas"; id ] -> ModelArea.View (System.Guid id)
|
prop.classes ["grow"]
|
||||||
| [ "groups" ] -> Groups.View ()
|
prop.children [
|
||||||
| [ "groups"; group ] -> Group.View group
|
match currentUrl with
|
||||||
| [ "groups"; group; "archives"; id ] -> GroupArchive.View group (System.Guid id)
|
| [ ] -> Index.View()
|
||||||
| [ "groups"; group; "users"; user ] -> User.View user
|
| [ "archives" ] -> Archives.View ()
|
||||||
| [ "users"; user ] -> User.View user
|
| [ "archives"; Route.Guid archive ] -> Archive.View archive
|
||||||
| [ "organizations" ] -> Organizations.List ()
|
| [ "model-areas" ] -> ModelAreas.List ()
|
||||||
| [ "organizations"; org ] -> Organization.View org
|
| [ "model-areas"; Route.Guid id ] -> ModelArea.View id
|
||||||
| otherwise -> Html.h1 "Not found"
|
| [ "groups" ] -> Groups.View ()
|
||||||
|
| [ "groups"; group ] -> Group.View group
|
||||||
|
| [ "groups"; group; "archives"; Route.Guid id ] -> GroupArchive.View group id
|
||||||
|
| [ "groups"; group; "users"; user ] -> GroupUser.View group user
|
||||||
|
| [ "users"; user ] -> User.View user
|
||||||
|
| [ "organizations" ] -> Organizations.List ()
|
||||||
|
| [ "organizations"; org ] -> Organization.View org
|
||||||
|
| otherwise -> Html.h1 "Not found"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -3,7 +3,6 @@ namespace Oceanbox.Codex
|
|||||||
module Group =
|
module Group =
|
||||||
open Browser
|
open Browser
|
||||||
open Fable.Core
|
open Fable.Core
|
||||||
open FsToolkit.ErrorHandling
|
|
||||||
|
|
||||||
open Oceanbox.Codex.Types
|
open Oceanbox.Codex.Types
|
||||||
|
|
||||||
@@ -17,59 +16,9 @@ module Group =
|
|||||||
return Error (sprintf "Error fetching users for group %s" group)
|
return Error (sprintf "Error fetching users for group %s" group)
|
||||||
}
|
}
|
||||||
|
|
||||||
let private fetchArchmaesterArchives (group: string) : JS.Promise<Result<Archmaester.Dto.ArchiveProps array, string>> =
|
|
||||||
promise {
|
|
||||||
let filter : Archmaester.Dto.ArchiveFilter = {
|
|
||||||
id = None
|
|
||||||
searchTerm = None
|
|
||||||
archiveType = None
|
|
||||||
owner = None
|
|
||||||
user = None
|
|
||||||
groups = Some [| group |]
|
|
||||||
}
|
|
||||||
let! result = Remoting.adminApi.getArchives 0 -1 filter |> Async.StartAsPromise
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
module private Elmish =
|
module private Elmish =
|
||||||
open Elmish
|
open Elmish
|
||||||
|
|
||||||
let fetchArchiveProps (group: string) : JS.Promise<Types.Archive array> =
|
|
||||||
promise {
|
|
||||||
let fgaUser = sprintf "group:%s#member" (Groups.Utils.canonicalizeName group)
|
|
||||||
let! archivesRes = fetchArchmaesterArchives group
|
|
||||||
let! viewsRes =
|
|
||||||
OpenFGA.fetchObjects(fgaUser, "view", "archive", context = {| time = System.DateTime.Now |})
|
|
||||||
let! execsRes =
|
|
||||||
OpenFGA.fetchObjects(fgaUser, "exec", "archive", context = {| task = "*"; usage = -1; time = System.DateTime.Now |})
|
|
||||||
|
|
||||||
let res =
|
|
||||||
result {
|
|
||||||
// TODO: Create specific exception
|
|
||||||
let! props = archivesRes |> Result.mapError System.Exception
|
|
||||||
let! views = viewsRes |> Result.mapError System.Exception
|
|
||||||
let! execs = execsRes |> Result.mapError System.Exception
|
|
||||||
let viewArchiveIds = views |> Array.choose Archives.Utils.extractFgaArchiveId
|
|
||||||
let execArchiveIds = execs |> Array.choose Archives.Utils.extractFgaArchiveId
|
|
||||||
let archives =
|
|
||||||
props
|
|
||||||
|> Array.map (fun prop -> {
|
|
||||||
Props = prop
|
|
||||||
CanView = viewArchiveIds |> Array.contains prop.archiveId
|
|
||||||
CanExec = execArchiveIds |> Array.contains prop.archiveId
|
|
||||||
})
|
|
||||||
|
|
||||||
return archives
|
|
||||||
}
|
|
||||||
|
|
||||||
match res with
|
|
||||||
| Ok archives ->
|
|
||||||
return archives
|
|
||||||
| Error ex ->
|
|
||||||
raise ex
|
|
||||||
return [||]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Msg =
|
type Msg =
|
||||||
| FetchArchives of string
|
| FetchArchives of string
|
||||||
| SetArchiveAdding of bool
|
| SetArchiveAdding of bool
|
||||||
@@ -97,7 +46,7 @@ module Group =
|
|||||||
let update msg model =
|
let update msg model =
|
||||||
match msg with
|
match msg with
|
||||||
| FetchArchives group ->
|
| FetchArchives group ->
|
||||||
model, Cmd.OfPromise.either fetchArchiveProps group SetArchives HandleExn
|
model, Cmd.OfPromise.either Archives.Utils.fetchGroupArchiveProps group SetArchives HandleExn
|
||||||
| HandleExn ex ->
|
| HandleExn ex ->
|
||||||
let msg =
|
let msg =
|
||||||
match ex with
|
match ex with
|
||||||
@@ -470,4 +419,4 @@ module Group =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -1,10 +1,40 @@
|
|||||||
namespace Oceanbox.Codex
|
namespace Oceanbox.Codex
|
||||||
|
|
||||||
|
type private Permission = {
|
||||||
|
Tuple: Remoting.Tuple
|
||||||
|
Relation: Remoting.ArchiveRelation
|
||||||
|
}
|
||||||
|
|
||||||
module GroupArchive =
|
module GroupArchive =
|
||||||
open Browser
|
open Browser
|
||||||
open Fable.Core
|
open Fable.Core
|
||||||
open Feliz
|
open Feliz
|
||||||
open Feliz.Router
|
open Feliz.Router
|
||||||
|
open FS.FluentUI
|
||||||
|
|
||||||
|
let private postPermissions (group: string) (archiveId: System.Guid) (permissions: Remoting.ArchiveRelation array) =
|
||||||
|
promise {
|
||||||
|
console.debug("Posting new relations: %o", permissions)
|
||||||
|
let req : Remoting.AddGroupPermissionsRequest = {
|
||||||
|
Group = Groups.Utils.canonicalizeName group
|
||||||
|
ArchiveId = archiveId
|
||||||
|
Permissions = permissions
|
||||||
|
}
|
||||||
|
let! res = Remoting.adminApi.addGroupPermissions req |> Async.StartAsPromise
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
let private putPermissions (group: string) (archiveId: System.Guid) (permissions: Remoting.ArchiveRelation array) =
|
||||||
|
promise {
|
||||||
|
console.debug("Updating existing relations: %o", permissions)
|
||||||
|
let req : Remoting.AddGroupPermissionsRequest = {
|
||||||
|
Group = Groups.Utils.canonicalizeName group
|
||||||
|
ArchiveId = archiveId
|
||||||
|
Permissions = permissions
|
||||||
|
}
|
||||||
|
let! res = Remoting.adminApi.updateGroupPermissions req |> Async.StartAsPromise
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
[<ReactComponent>]
|
[<ReactComponent>]
|
||||||
let private DeleteRelationButton onDelete (tuple: Remoting.Tuple) =
|
let private DeleteRelationButton onDelete (tuple: Remoting.Tuple) =
|
||||||
@@ -20,74 +50,137 @@ module GroupArchive =
|
|||||||
else
|
else
|
||||||
// TODO: Should probably just return unit and error if not deleted
|
// TODO: Should probably just return unit and error if not deleted
|
||||||
console.warn ("[Group] Tuple was not deleted: %o", tuple)
|
console.warn ("[Group] Tuple was not deleted: %o", tuple)
|
||||||
| Error err -> console.error ("[Group] Error deleting tuple: %s\n%o", err, tuple)
|
| Error err ->
|
||||||
|
console.error ("[Group] Error deleting tuple: %s\n%o", err, tuple)
|
||||||
)
|
)
|
||||||
|
|
||||||
Html.button [ prop.onClick handleDelete; prop.text "Delete" ]
|
Fui.button [
|
||||||
|
button.onClick handleDelete
|
||||||
|
button.icon (Fui.icon.deleteRegular [])
|
||||||
|
]
|
||||||
|
|
||||||
[<ReactComponent>]
|
[<ReactComponent>]
|
||||||
let private ViewTerm group archiveId (onDelete: Remoting.Tuple -> unit) (viewTerm: Remoting.ViewTerm) =
|
let private PermissionCard (title: string) onDelete (tuple: Remoting.Tuple) (children: ReactElement array) =
|
||||||
let tuple =
|
|
||||||
Remoting.Tuple.delete (
|
|
||||||
user = Groups.Utils.fgaMember group,
|
|
||||||
relation = "view",
|
|
||||||
object = sprintf "archive:%O" archiveId
|
|
||||||
)
|
|
||||||
|
|
||||||
Html.div [
|
Html.div [
|
||||||
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
||||||
prop.style [ style.flexBasis (length.px 320) ]
|
prop.style [ style.flexBasis (length.px 384) ]
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.div [
|
Html.div [
|
||||||
prop.classes [ "flex-row-center" ]
|
prop.classes [ "flex-row-center" ]
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.div [ prop.classes [ "grow" ]; prop.children [ Html.b "View Term" ] ]
|
Html.div [
|
||||||
|
prop.classes [ "grow" ]
|
||||||
|
prop.children [
|
||||||
|
Html.b [
|
||||||
|
prop.style [
|
||||||
|
style.fontSize(length.px 16)
|
||||||
|
]
|
||||||
|
prop.text title
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
DeleteRelationButton onDelete tuple
|
DeleteRelationButton onDelete tuple
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
Html.div [
|
Html.div [
|
||||||
prop.classes [ "ml-16" ]
|
prop.children children
|
||||||
prop.children [
|
|
||||||
Html.div (sprintf "Start time: %s" (Intl.shortDateTime viewTerm.StartTime))
|
|
||||||
Html.div (sprintf "End time: %s" (Intl.shortDateTime viewTerm.EndTime))
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
[<ReactComponent>]
|
[<ReactComponent>]
|
||||||
let private ExecTicket (group: string) (archiveId: System.Guid) onDelete (ticket: Remoting.ExecTicket) =
|
let private ViewTerm
|
||||||
let tuple =
|
key
|
||||||
Remoting.Tuple.delete (
|
(onUpdate: Permission -> unit)
|
||||||
user = Groups.Utils.fgaMember group,
|
(permission: Permission)
|
||||||
relation = "exec",
|
(viewTerm: Remoting.ViewTerm)
|
||||||
object = sprintf "archive:%O" archiveId
|
=
|
||||||
|
let updateCond (newCond: Remoting.ViewTerm) =
|
||||||
|
let updatedCond =
|
||||||
|
permission.Tuple.Condition
|
||||||
|
|> Option.map (fun cond ->
|
||||||
|
{ cond with Context = JS.JSON.stringify newCond.JsonObj }
|
||||||
|
)
|
||||||
|
onUpdate {
|
||||||
|
permission with
|
||||||
|
Tuple.Condition = updatedCond
|
||||||
|
Relation = Remoting.ArchiveRelation.ViewTerm newCond
|
||||||
|
}
|
||||||
|
|
||||||
|
let handleStartChange =
|
||||||
|
React.useCallback (
|
||||||
|
(fun (newStartOpt: System.DateTime option) ->
|
||||||
|
match newStartOpt with
|
||||||
|
| Some newStart ->
|
||||||
|
let updated = { viewTerm with StartTime = newStart }
|
||||||
|
updateCond updated
|
||||||
|
| None ->
|
||||||
|
console.error("Got no date from date picker")
|
||||||
|
),
|
||||||
|
[| permission |]
|
||||||
|
)
|
||||||
|
let handleEndChange =
|
||||||
|
React.useCallback (
|
||||||
|
(fun (newEndOpt: System.DateTime option) ->
|
||||||
|
match newEndOpt with
|
||||||
|
| Some newEnd ->
|
||||||
|
let updated = { viewTerm with EndTime = newEnd }
|
||||||
|
updateCond updated
|
||||||
|
| None ->
|
||||||
|
console.error("Got no date from date picker")
|
||||||
|
),
|
||||||
|
[| permission |]
|
||||||
)
|
)
|
||||||
|
|
||||||
Html.div [
|
Fui.table [
|
||||||
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
table.size.medium
|
||||||
prop.style [ style.flexBasis (length.px 320) ]
|
table.children [
|
||||||
prop.children [
|
Fui.tableBody [
|
||||||
Html.div [
|
tableBody.children [
|
||||||
prop.classes [ "flex-row-center" ]
|
Fui.tableRow [
|
||||||
prop.children [
|
tableRow.key "view-term-start-time"
|
||||||
Html.div [ prop.classes [ "grow" ]; prop.children [ Html.b "Exec Ticket" ] ]
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.text "Start time"
|
||||||
|
]
|
||||||
|
|
||||||
DeleteRelationButton onDelete tuple
|
Fui.tableCell [
|
||||||
]
|
tableCell.children (
|
||||||
]
|
Fui.tableCellLayout [
|
||||||
Html.div [
|
tableCellLayout.children (
|
||||||
prop.classes [ "ml-16" ]
|
Fui.datePicker [
|
||||||
prop.children [
|
datePicker.size.small
|
||||||
Html.div (sprintf "Start time: %s" (Intl.shortDateTime ticket.StartTime))
|
datePicker.onSelectDate handleStartChange
|
||||||
Html.div (sprintf "End time: %s" (Intl.shortDateTime ticket.EndTime))
|
datePicker.value (Some viewTerm.StartTime)
|
||||||
Html.div (sprintf "Quota: %.1f" ticket.Quota)
|
]
|
||||||
Html.div [
|
)
|
||||||
prop.children [
|
]
|
||||||
Html.span "Tasks:"
|
)
|
||||||
Html.ul [
|
]
|
||||||
prop.children (ticket.Tasks |> Array.map (fun task -> Html.li task))
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.key "view-term-end-time"
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.text "End time"
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.children (
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.children (
|
||||||
|
Fui.datePicker [
|
||||||
|
datePicker.size.small
|
||||||
|
datePicker.onSelectDate handleEndChange
|
||||||
|
datePicker.value (Some viewTerm.EndTime)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -97,14 +190,278 @@ module GroupArchive =
|
|||||||
]
|
]
|
||||||
|
|
||||||
[<ReactComponent>]
|
[<ReactComponent>]
|
||||||
let private PermissionForm (permissions: OpenFGA.Types.ArchiveRelation array) (group: string) =
|
let private ExecTicket
|
||||||
|
(key: string)
|
||||||
|
(onUpdate: Permission -> unit)
|
||||||
|
(permission: Permission)
|
||||||
|
(ticket: Remoting.ExecTicket)
|
||||||
|
=
|
||||||
|
let updateCond (newCond: Remoting.ExecTicket) =
|
||||||
|
let updatedCond =
|
||||||
|
permission.Tuple.Condition
|
||||||
|
|> Option.map (fun cond ->
|
||||||
|
{ cond with Context = JS.JSON.stringify newCond.JsonObj }
|
||||||
|
)
|
||||||
|
onUpdate {
|
||||||
|
permission with
|
||||||
|
Tuple.Condition = updatedCond
|
||||||
|
Relation = Remoting.ArchiveRelation.ExecTicket newCond
|
||||||
|
}
|
||||||
|
|
||||||
|
let handleStartChange =
|
||||||
|
React.useCallback (
|
||||||
|
(fun (newStartOpt: System.DateTime option) ->
|
||||||
|
match newStartOpt with
|
||||||
|
| Some newStart ->
|
||||||
|
let updated = { ticket with StartTime = newStart }
|
||||||
|
updateCond updated
|
||||||
|
| None ->
|
||||||
|
console.error("Got no date from date picker")
|
||||||
|
),
|
||||||
|
[| ticket |]
|
||||||
|
)
|
||||||
|
let handleEndChange =
|
||||||
|
React.useCallback (
|
||||||
|
(fun (newEndOpt: System.DateTime option) ->
|
||||||
|
match newEndOpt with
|
||||||
|
| Some newEnd ->
|
||||||
|
let updated = { ticket with EndTime = newEnd }
|
||||||
|
updateCond updated
|
||||||
|
| None ->
|
||||||
|
console.error("Got no date from date picker")
|
||||||
|
),
|
||||||
|
[| ticket |]
|
||||||
|
)
|
||||||
|
|
||||||
|
Fui.table [
|
||||||
|
table.size.medium
|
||||||
|
table.children [
|
||||||
|
Fui.tableBody [
|
||||||
|
tableBody.children [
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.key "exec-ticket-start-time"
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.text "Start time"
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.children (
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.children (
|
||||||
|
Fui.datePicker [
|
||||||
|
datePicker.size.small
|
||||||
|
datePicker.onSelectDate handleStartChange
|
||||||
|
datePicker.value (Some ticket.StartTime)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.key "exec-ticket-end-time"
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.text "End time"
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.children (
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.children (
|
||||||
|
Fui.datePicker [
|
||||||
|
datePicker.size.small
|
||||||
|
datePicker.onSelectDate handleEndChange
|
||||||
|
datePicker.value (Some ticket.EndTime)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.key "exec-ticket-quota"
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.text "Quota"
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.children (
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.children (
|
||||||
|
Fui.input [
|
||||||
|
input.size.small
|
||||||
|
input.type'.number
|
||||||
|
input.value ticket.Quota
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.key "exec-ticket-tasks"
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.text "Tasks"
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.tableCell [
|
||||||
|
tableCell.children (
|
||||||
|
Fui.tableCellLayout [
|
||||||
|
tableCellLayout.children (
|
||||||
|
Fui.text (
|
||||||
|
ticket.Tasks
|
||||||
|
|> String.concat ", "
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
[<ReactComponent>]
|
||||||
|
let private PermissionCreateCard group archiveId (onCreate: Permission -> unit) (defaultPermission: Permission) =
|
||||||
|
let isLoading, setLoading = React.useState false
|
||||||
|
let permission, setPermission = React.useState defaultPermission
|
||||||
|
|
||||||
|
let handleCreate (ev: Types.Event) =
|
||||||
|
setLoading true
|
||||||
|
postPermissions group archiveId [| permission.Relation |]
|
||||||
|
|> Promise.iter (fun res ->
|
||||||
|
match res with
|
||||||
|
| Ok () ->
|
||||||
|
console.info("Success.")
|
||||||
|
onCreate permission
|
||||||
|
| Error msg ->
|
||||||
|
console.error("Error adding permissions %s.", msg)
|
||||||
|
|
||||||
|
setLoading false
|
||||||
|
)
|
||||||
|
|
||||||
|
let handleUpdateRelation (permission: Permission) =
|
||||||
|
setPermission permission
|
||||||
|
|
||||||
|
console.debug("Permission: %o", permission)
|
||||||
|
|
||||||
|
Html.div [
|
||||||
|
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
||||||
|
prop.style [ style.flexBasis (length.px 384) ]
|
||||||
|
prop.children [
|
||||||
|
Html.div [
|
||||||
|
prop.classes [ "flex-row-center" ]
|
||||||
|
prop.children [
|
||||||
|
Html.div [
|
||||||
|
prop.classes [ "grow" ]
|
||||||
|
prop.children [
|
||||||
|
Html.b [
|
||||||
|
prop.style [
|
||||||
|
style.fontSize(length.px 16)
|
||||||
|
]
|
||||||
|
prop.text (
|
||||||
|
match permission.Relation with
|
||||||
|
| Remoting.ArchiveRelation.ViewTerm _ -> "View Term"
|
||||||
|
| Remoting.ArchiveRelation.ExecTicket _ -> "Exec Ticket"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.button [
|
||||||
|
button.onClick handleCreate
|
||||||
|
button.icon (
|
||||||
|
if isLoading then
|
||||||
|
Fui.spinner [ spinner.size.tiny ]
|
||||||
|
else
|
||||||
|
Fui.icon.addRegular []
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Html.div [
|
||||||
|
prop.children [|
|
||||||
|
match permission.Relation with
|
||||||
|
| Remoting.ArchiveRelation.ViewTerm term ->
|
||||||
|
ViewTerm
|
||||||
|
"create-permission-view-term"
|
||||||
|
handleUpdateRelation
|
||||||
|
permission
|
||||||
|
term
|
||||||
|
| Remoting.ArchiveRelation.ExecTicket ticket ->
|
||||||
|
ExecTicket
|
||||||
|
"create-permission-exec-ticket"
|
||||||
|
handleUpdateRelation
|
||||||
|
permission
|
||||||
|
ticket
|
||||||
|
|]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let private allRelations = [|
|
||||||
|
Remoting.ArchiveRelation.ViewTerm Remoting.ViewTerm.empty
|
||||||
|
Remoting.ArchiveRelation.ExecTicket Remoting.ExecTicket.empty
|
||||||
|
|]
|
||||||
|
|
||||||
|
[<ReactComponent>]
|
||||||
|
let private PermissionForm
|
||||||
|
(group: string)
|
||||||
|
(archiveId: System.Guid)
|
||||||
|
(onAdd: Permission -> unit)
|
||||||
|
(permissions: Permission array)
|
||||||
|
=
|
||||||
let adding, setAdding = React.useState false
|
let adding, setAdding = React.useState false
|
||||||
|
let loading, setLoading = React.useState false
|
||||||
|
let success, setSuccess = React.useState false
|
||||||
|
|
||||||
let handleAddClick (ev: Types.Event) = setAdding true
|
// Create a list of permissions missing from the archive
|
||||||
let handleCancelClick (ev: Types.Event) = setAdding false
|
let availablePermissions : Permission array =
|
||||||
|
allRelations
|
||||||
|
|> Array.choose (fun relation ->
|
||||||
|
let exists =
|
||||||
|
permissions
|
||||||
|
|> Array.exists (fun permission ->
|
||||||
|
let name = Remoting.ArchiveRelation.ConditionName relation
|
||||||
|
permission.Tuple.Condition
|
||||||
|
|> Option.map (fun cond -> cond.Name = name)
|
||||||
|
|> Option.defaultValue false
|
||||||
|
)
|
||||||
|
|
||||||
let hasViewTerm = permissions |> Array.exists _.IsViewTerm
|
if exists then
|
||||||
let hasExecTicket = permissions |> Array.exists _.IsExecTicket
|
None
|
||||||
|
else
|
||||||
|
let user = Groups.Utils.fgaMember group
|
||||||
|
let object = sprintf "archive:%O" archiveId
|
||||||
|
Some {
|
||||||
|
Tuple = OpenFGA.Types.ArchiveRelation.toTuple user object relation
|
||||||
|
Relation = relation
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let handleAddClick (ev: Types.Event) =
|
||||||
|
setAdding true
|
||||||
|
|
||||||
|
let handleCancelClick (ev: Types.Event) =
|
||||||
|
setAdding false
|
||||||
|
|
||||||
|
let handlePermissionAdd (newPermission: Permission) =
|
||||||
|
console.debug("Added new permission: %o", newPermission)
|
||||||
|
onAdd newPermission
|
||||||
|
|
||||||
React.fragment [
|
React.fragment [
|
||||||
Html.div [
|
Html.div [
|
||||||
@@ -113,52 +470,39 @@ module GroupArchive =
|
|||||||
"gap-8"
|
"gap-8"
|
||||||
]
|
]
|
||||||
prop.children [
|
prop.children [
|
||||||
if adding then
|
if loading then
|
||||||
Html.button [ prop.onClick handleAddClick; prop.text "Save" ]
|
Html.p "Loading ..."
|
||||||
|
|
||||||
Html.button [
|
|
||||||
prop.onClick handleCancelClick
|
|
||||||
prop.text "Cancel"
|
|
||||||
]
|
|
||||||
else
|
else
|
||||||
Html.button [ prop.onClick handleAddClick; prop.text "Add" ]
|
if adding then
|
||||||
|
Html.button [
|
||||||
|
prop.onClick handleCancelClick
|
||||||
|
prop.text "Cancel"
|
||||||
|
]
|
||||||
|
else
|
||||||
|
Html.button [
|
||||||
|
prop.disabled (Array.isEmpty availablePermissions)
|
||||||
|
prop.onClick handleAddClick
|
||||||
|
prop.text "Add"
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if not loading && success then
|
||||||
|
Html.p "Success."
|
||||||
|
|
||||||
if adding then
|
if adding then
|
||||||
Html.div [
|
Html.div [
|
||||||
prop.id "group-archive-exec-form"
|
prop.id "group-archive-exec-form"
|
||||||
prop.classes [
|
prop.classes [
|
||||||
"flex-row"
|
"flex-row-start"
|
||||||
"gap-32"
|
"gap-32"
|
||||||
]
|
]
|
||||||
prop.children [
|
prop.children [
|
||||||
if not hasViewTerm then
|
availablePermissions
|
||||||
Html.div [
|
|> Array.map (fun permission ->
|
||||||
prop.children [
|
PermissionCreateCard group archiveId handlePermissionAdd permission
|
||||||
Html.b "View"
|
)
|
||||||
Groups.ViewForm (Remoting.ViewTerm.empty, ignore)
|
|> unbox
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
if not hasExecTicket then
|
|
||||||
Html.div [
|
|
||||||
prop.classes [
|
|
||||||
"flex-column"
|
|
||||||
"gap-8"
|
|
||||||
"shadow"
|
|
||||||
"brad-8"
|
|
||||||
"m-8"
|
|
||||||
"p-16"
|
|
||||||
]
|
|
||||||
prop.style [
|
|
||||||
style.flexBasis (length.px 512)
|
|
||||||
]
|
|
||||||
prop.children [
|
|
||||||
Html.b "Exec"
|
|
||||||
Groups.ExecForm (Remoting.ExecTicket.empty, ignore)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -167,22 +511,77 @@ module GroupArchive =
|
|||||||
let View (group: string) (archiveId: System.Guid) =
|
let View (group: string) (archiveId: System.Guid) =
|
||||||
let loading, setLoading = React.useState true
|
let loading, setLoading = React.useState true
|
||||||
let error, setError = React.useState<string option> None
|
let error, setError = React.useState<string option> None
|
||||||
let archiveOpt, setArchive =
|
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
|
||||||
React.useState<Archmaester.Dto.ArchiveProps option> None
|
|
||||||
|
|
||||||
let fgaUser = Groups.Utils.fgaMember group
|
let fgaUser = Groups.Utils.fgaMember group
|
||||||
let tuples =
|
let tuples = OpenFGA.useReadTuples (fgaUser, object = sprintf "archive:%O" archiveId)
|
||||||
OpenFGA.useReadTuples (fgaUser, object = sprintf "archive:%O" archiveId)
|
|
||||||
|
|
||||||
let relations: OpenFGA.Types.ArchiveRelation array =
|
let permissions: Permission array =
|
||||||
tuples.Tuples
|
tuples.Tuples
|
||||||
|> Array.choose (fun tuple -> tuple.Condition |> Option.bind OpenFGA.Types.ArchiveRelation.tryOfCondition)
|
|> Array.choose (fun tuple ->
|
||||||
|
tuple.Condition
|
||||||
|
|> Option.bind (fun cond ->
|
||||||
|
cond
|
||||||
|
|> OpenFGA.Types.ArchiveRelation.tryOfCondition
|
||||||
|
|> Option.map (fun rel -> {
|
||||||
|
Tuple = tuple
|
||||||
|
Relation = rel
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
let handlePermissionDelete (tuple: Remoting.Tuple) =
|
let handlePermissionDelete (tuple: Remoting.Tuple) =
|
||||||
|
console.debug("Deleting %o from %o", tuple, tuples)
|
||||||
tuples.Tuples
|
tuples.Tuples
|
||||||
|> Array.filter (fun existing -> existing.Relation <> tuple.Relation)
|
|> Array.filter (fun existing -> existing.Relation <> tuple.Relation)
|
||||||
|> tuples.SetTuples
|
|> tuples.SetTuples
|
||||||
|
|
||||||
|
let handlePermissionUpdate (updated: Permission) =
|
||||||
|
console.debug("Updated permission tuple %o", updated)
|
||||||
|
putPermissions group archiveId [| updated.Relation |]
|
||||||
|
|> Promise.iter (fun res ->
|
||||||
|
match res with
|
||||||
|
| Ok () ->
|
||||||
|
tuples.Tuples
|
||||||
|
|> Array.map (fun existing ->
|
||||||
|
let equal =
|
||||||
|
existing.Object = updated.Tuple.Object
|
||||||
|
&& existing.Relation = updated.Tuple.Relation
|
||||||
|
&& existing.User = updated.Tuple.User
|
||||||
|
|
||||||
|
if equal then
|
||||||
|
updated.Tuple
|
||||||
|
else
|
||||||
|
existing
|
||||||
|
)
|
||||||
|
|> tuples.SetTuples
|
||||||
|
| Error msg ->
|
||||||
|
setError (Some msg)
|
||||||
|
)
|
||||||
|
|
||||||
|
let handleUpdateRelations =
|
||||||
|
React.useCallback (
|
||||||
|
(fun (updated: Remoting.Tuple array) ->
|
||||||
|
console.debug("Relations updated: %o with current: %o", updated, tuples.Tuples)
|
||||||
|
|
||||||
|
tuples.SetTuples updated
|
||||||
|
),
|
||||||
|
[| box tuples |]
|
||||||
|
)
|
||||||
|
|
||||||
|
let handleAddPermission =
|
||||||
|
React.useCallback (
|
||||||
|
(fun (newPermission: Permission) ->
|
||||||
|
console.debug("New relation added: %o with current: %o", newPermission, tuples.Tuples)
|
||||||
|
|
||||||
|
newPermission.Tuple
|
||||||
|
|> Array.singleton
|
||||||
|
|> Array.append tuples.Tuples
|
||||||
|
|> tuples.SetTuples
|
||||||
|
),
|
||||||
|
[| box tuples |]
|
||||||
|
)
|
||||||
|
|
||||||
React.useEffect (
|
React.useEffect (
|
||||||
(fun () ->
|
(fun () ->
|
||||||
setLoading true
|
setLoading true
|
||||||
@@ -210,7 +609,10 @@ module GroupArchive =
|
|||||||
Html.h1 [
|
Html.h1 [
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.text "Group "
|
Html.text "Group "
|
||||||
Html.a [ prop.href (Router.format ("groups", group)); prop.text group ]
|
Html.a [
|
||||||
|
prop.href (Router.format ("groups", group))
|
||||||
|
prop.text group
|
||||||
|
]
|
||||||
Html.text " / "
|
Html.text " / "
|
||||||
Html.text "Archive "
|
Html.text "Archive "
|
||||||
Html.a [
|
Html.a [
|
||||||
@@ -220,7 +622,14 @@ module GroupArchive =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
Html.div [ prop.children [ Html.button [ prop.text "Remove" ] ] ]
|
Html.div [
|
||||||
|
prop.children [
|
||||||
|
Html.button [
|
||||||
|
prop.disabled true
|
||||||
|
prop.text "Remove (TODO)"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
Archives.InfoSection archive
|
Archives.InfoSection archive
|
||||||
|
|
||||||
@@ -228,9 +637,13 @@ module GroupArchive =
|
|||||||
prop.children [
|
prop.children [
|
||||||
Html.h2 "Permissions"
|
Html.h2 "Permissions"
|
||||||
|
|
||||||
if not tuples.Loading then
|
if tuples.Loading then
|
||||||
|
Html.p "Loading ..."
|
||||||
|
else
|
||||||
Html.div [
|
Html.div [
|
||||||
prop.children [ PermissionForm relations group ]
|
prop.children [
|
||||||
|
PermissionForm group archive.archiveId handleAddPermission permissions
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
Html.div [
|
Html.div [
|
||||||
@@ -240,23 +653,28 @@ module GroupArchive =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
if Array.isEmpty relations then
|
if Array.isEmpty permissions then
|
||||||
Html.p "No permissions"
|
Html.p "No permissions"
|
||||||
else
|
else
|
||||||
Html.div [
|
Html.div [
|
||||||
prop.classes [ "flex-row"; "gap-32" ]
|
prop.classes [ "flex-row-start"; "gap-32" ]
|
||||||
prop.children (
|
prop.children (
|
||||||
relations
|
permissions
|
||||||
|> Array.map (
|
|> Array.map (fun permission ->
|
||||||
function
|
match permission.Relation with
|
||||||
| OpenFGA.Types.ArchiveRelation.ViewTerm term ->
|
| Remoting.ArchiveRelation.ViewTerm term ->
|
||||||
ViewTerm group archive.archiveId handlePermissionDelete term
|
PermissionCard "View Term" handlePermissionDelete permission.Tuple [|
|
||||||
| OpenFGA.Types.ArchiveRelation.ExecTicket ticket ->
|
ViewTerm "view-term-table" handlePermissionUpdate permission term
|
||||||
ExecTicket group archive.archiveId handlePermissionDelete ticket
|
|]
|
||||||
|
| Remoting.ArchiveRelation.ExecTicket ticket ->
|
||||||
|
PermissionCard "Exec Ticket" handlePermissionDelete permission.Tuple [|
|
||||||
|
ExecTicket "exec-ticket-table" handlePermissionUpdate permission ticket
|
||||||
|
|]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
| None -> Html.h1 (sprintf "Group %s / Archive %O not found" group archiveId)
|
| None ->
|
||||||
]
|
Html.h1 (sprintf "Group %s / Archive %O not found" group archiveId)
|
||||||
|
]
|
||||||
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 }
|
onChange { view with EndTime = endTime }
|
||||||
|
|
||||||
Html.div [
|
Html.div [
|
||||||
|
prop.classes [ "flex-column"; "gap-8"]
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.div [
|
Html.div [
|
||||||
|
prop.classes [ "flex-row-start"]
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.label [
|
Html.label [
|
||||||
prop.text "Start date"
|
prop.style [
|
||||||
|
style.flexBasis (length.px 128)
|
||||||
|
]
|
||||||
|
prop.text "Start date:"
|
||||||
]
|
]
|
||||||
|
|
||||||
Html.input [
|
Html.input [
|
||||||
@@ -36,9 +41,13 @@ type Groups =
|
|||||||
]
|
]
|
||||||
|
|
||||||
Html.div [
|
Html.div [
|
||||||
|
prop.classes [ "flex-row-start"]
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.label [
|
Html.label [
|
||||||
prop.text "End date"
|
prop.style [
|
||||||
|
style.flexBasis (length.px 128)
|
||||||
|
]
|
||||||
|
prop.text "End date:"
|
||||||
]
|
]
|
||||||
|
|
||||||
Html.input [
|
Html.input [
|
||||||
@@ -52,4 +61,3 @@ type Groups =
|
|||||||
Html.p "NB: If the start date is the same or after the end date, there is no time restriction."
|
Html.p "NB: If the start date is the same or after the end date, there is no time restriction."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,569 @@
|
|||||||
namespace Oceanbox.Codex
|
namespace Oceanbox.Codex
|
||||||
|
|
||||||
|
open Fable.Core
|
||||||
open Feliz
|
open Feliz
|
||||||
open Feliz.Router
|
open Feliz.Router
|
||||||
|
open FS.FluentUI
|
||||||
|
open Browser
|
||||||
|
|
||||||
module Index =
|
module Index =
|
||||||
|
|
||||||
|
[<Literal>]
|
||||||
|
let private noGroup = "Select Group..."
|
||||||
|
|
||||||
|
type private User = { Name: string; Group: string }
|
||||||
|
|
||||||
|
// TODO: Elmish?
|
||||||
[<ReactComponent>]
|
[<ReactComponent>]
|
||||||
let View () =
|
let private UserAddForm onAdd =
|
||||||
Html.main [
|
let email, setEmail = React.useState ""
|
||||||
Html.h1 "Codex"
|
let selectedGroup, setSelectedGroup = React.useState noGroup
|
||||||
Html.h2 "Index"
|
let recentlyAddedUsers, setRecentlyAddedUsers = React.useState<User array> [||]
|
||||||
Html.div [
|
let groups = Groups.useGroups ()
|
||||||
prop.children [
|
// TODO: Fui Toast?
|
||||||
Html.ul [
|
let errMsg, setErrMsg = React.useState None
|
||||||
Html.li [
|
|
||||||
Html.a [
|
let handleAddUser () =
|
||||||
prop.href (Router.format "archives")
|
let newUser = {
|
||||||
prop.text "archives"
|
Name = email
|
||||||
|
Group = Groups.Utils.canonicalizeName selectedGroup
|
||||||
|
}
|
||||||
|
Remoting.adminApi.addUsers { Group = newUser.Group; Users = [| newUser.Name |] }
|
||||||
|
|> Async.StartAsPromise
|
||||||
|
|> Promise.iter (fun res ->
|
||||||
|
match res with
|
||||||
|
| Ok () ->
|
||||||
|
console.info("Added user %s to selectedGroup %s", newUser.Name, newUser.Group)
|
||||||
|
onAdd (Some (email, selectedGroup))
|
||||||
|
recentlyAddedUsers |> Array.append [|newUser|] |> setRecentlyAddedUsers
|
||||||
|
| Error msg ->
|
||||||
|
setErrMsg (Some msg)
|
||||||
|
console.error("Error adding user %s to group %s: %s", newUser.Name, newUser.Group, msg)
|
||||||
|
)
|
||||||
|
|
||||||
|
Fui.card [
|
||||||
|
card.classes [ "flex-column"; "gap-16"; "w-512"; "minh-512"]
|
||||||
|
card.children [
|
||||||
|
React.fragment [
|
||||||
|
Fui.cardHeader [
|
||||||
|
cardHeader.image (Fui.icon.personAddRegular [icon.size.``32``])
|
||||||
|
cardHeader.header (Fui.text.title3 [ text.weight.semibold; text.text "Add new users"])
|
||||||
|
]
|
||||||
|
if groups.Loading then
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["flex-row"]
|
||||||
|
prop.children [
|
||||||
|
Fui.spinner []
|
||||||
|
Fui.text.body1 "Loading groups..."
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
else
|
||||||
|
match groups.Error with
|
||||||
|
| Some _error ->
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["flex-row"]
|
||||||
|
prop.children [
|
||||||
|
Fui.icon.warningRegular []
|
||||||
|
Fui.text.body1 "Failed to load groups"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
| None ->
|
||||||
|
Fui.field [
|
||||||
|
field.label (
|
||||||
|
Fui.label [
|
||||||
|
label.required true
|
||||||
|
label.text "Group"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
// field.style [style.maxWidth 256]
|
||||||
|
field.children (
|
||||||
|
Fui.dropdown [
|
||||||
|
dropdown.value selectedGroup
|
||||||
|
dropdown.selectedOptions [|selectedGroup|]
|
||||||
|
dropdown.onOptionSelect (fun (d: OptionOnSelectData) ->
|
||||||
|
d.optionText
|
||||||
|
|> Option.iter setSelectedGroup
|
||||||
|
)
|
||||||
|
dropdown.children [
|
||||||
|
yield!
|
||||||
|
groups.Groups
|
||||||
|
|> Array.sort
|
||||||
|
|> Array.map (fun group ->
|
||||||
|
Fui.option [
|
||||||
|
option.key group
|
||||||
|
option.text group
|
||||||
|
option.children (Fui.text group)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
Html.li [
|
Fui.field [
|
||||||
Html.a [
|
field.label (
|
||||||
prop.href (Router.format "model-areas")
|
Fui.label [
|
||||||
prop.text "model areas"
|
label.required true
|
||||||
|
label.text "Email"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
// field.style [style.maxWidth 256]
|
||||||
|
field.children (
|
||||||
|
Fui.input [
|
||||||
|
input.contentBefore (Fui.icon.mailRegular [])
|
||||||
|
input.type'.email
|
||||||
|
input.onChange (fun (v: ValueProp<string>) -> setEmail v.value)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
match errMsg with
|
||||||
|
| None -> Html.none
|
||||||
|
| Some msg ->
|
||||||
|
Html.div [
|
||||||
|
prop.classes [ "flex-row"; "gap-8"]
|
||||||
|
prop.children [
|
||||||
|
Fui.text [
|
||||||
|
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||||
|
text.text msg
|
||||||
|
]
|
||||||
|
Fui.button [
|
||||||
|
button.appearance.subtle
|
||||||
|
button.icon (Fui.icon.dismissRegular [])
|
||||||
|
button.onClick (fun _ -> setErrMsg None)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
]
|
||||||
|
|
||||||
Html.li [
|
Html.div [
|
||||||
Html.a [
|
prop.classes [ "flex-row"; "gap-8"; ]
|
||||||
prop.href (Router.format "groups")
|
prop.children [
|
||||||
prop.text "groups"
|
Fui.button [
|
||||||
]
|
button.onClick (fun _ -> handleAddUser ())
|
||||||
|
button.text "Save"
|
||||||
|
button.icon (Fui.icon.saveRegular [])
|
||||||
|
button.appearance.primary
|
||||||
|
button.disabled <| (email = "" || selectedGroup = noGroup)
|
||||||
]
|
]
|
||||||
|
Fui.button [
|
||||||
Html.li [
|
button.onClick (fun _ ->
|
||||||
Html.a [
|
setEmail ""
|
||||||
prop.href (Router.format "organizations")
|
setSelectedGroup noGroup
|
||||||
prop.text "organizations"
|
)
|
||||||
]
|
button.text "Clear"
|
||||||
|
button.icon (Fui.icon.arrowResetRegular [])
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
match recentlyAddedUsers with
|
||||||
|
| [||] -> Html.none
|
||||||
|
| users ->
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["flex-column"; "gap-16"]
|
||||||
|
prop.children [
|
||||||
|
Fui.text.title3 "Recently added"
|
||||||
|
Fui.table [
|
||||||
|
Fui.tableHeader [
|
||||||
|
Fui.tableRow [
|
||||||
|
Fui.tableHeaderCell [
|
||||||
|
tableHeaderCell.children [ Fui.text "Name"]
|
||||||
|
]
|
||||||
|
Fui.tableHeaderCell [
|
||||||
|
tableHeaderCell.children [ Fui.text "Group"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableBody [
|
||||||
|
yield!
|
||||||
|
users |> Array.map (fun user ->
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.key user.Name
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.link [
|
||||||
|
link.href (Router.format ["users"; user.Name])
|
||||||
|
link.text user.Name
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.link [
|
||||||
|
link.href (Router.format ["groups"; user.Group])
|
||||||
|
link.text user.Group
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[<Literal>]
|
||||||
|
let private noDataSet = "Select DataSet..."
|
||||||
|
|
||||||
|
[<ReactComponent>]
|
||||||
|
let private ArchiveAddForm () =
|
||||||
|
let dataSets, setDataSets = React.useState [||]
|
||||||
|
let isLoading, setIsLoading = React.useState true
|
||||||
|
let recentlyAddedArchives, setRecentlyAddedArchives = React.useState [||]
|
||||||
|
let errMsg, setErrMsg = React.useState None
|
||||||
|
|
||||||
|
let form, setForm = React.useState (Remoting.AddArchiveRequest.empty ())
|
||||||
|
|
||||||
|
let fetchDataSets () : JS.Promise<Result<Remoting.DataSet array, string>> =
|
||||||
|
promise {
|
||||||
|
try
|
||||||
|
console.debug("Fetching all dataSets")
|
||||||
|
let! res = Remoting.adminApi.getDataSets () |> Async.StartAsPromise
|
||||||
|
return res
|
||||||
|
with ex ->
|
||||||
|
console.error("Error fetching dataSets: %s", ex.Message)
|
||||||
|
return Error "Error fetching dataSets"
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffectOnce (fun _ ->
|
||||||
|
fetchDataSets ()
|
||||||
|
|> Promise.iter (fun res ->
|
||||||
|
setIsLoading false
|
||||||
|
match res with
|
||||||
|
| Ok r ->
|
||||||
|
setDataSets r
|
||||||
|
| Error e -> setErrMsg (Some e)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let handleAddArchive () =
|
||||||
|
let utcTime = System.DateTime(form.StartTime.Year, form.StartTime.Month, form.StartTime.Day, 0, 0, 0, System.DateTimeKind.Utc)
|
||||||
|
Remoting.adminApi.addArchive {form with StartTime = utcTime}
|
||||||
|
|> Async.StartAsPromise
|
||||||
|
|> Promise.iter (fun res ->
|
||||||
|
match res with
|
||||||
|
| Ok newArchive ->
|
||||||
|
console.info("Added archive %s with id %s", newArchive.Name, string newArchive.Id)
|
||||||
|
recentlyAddedArchives |> Array.append [|newArchive|] |> setRecentlyAddedArchives
|
||||||
|
setForm (Remoting.AddArchiveRequest.empty())
|
||||||
|
| Error msg ->
|
||||||
|
console.error("Error adding archive %s: %s", form.Name, msg)
|
||||||
|
setErrMsg (Some msg)
|
||||||
|
)
|
||||||
|
|
||||||
|
let selectedDataSet =
|
||||||
|
dataSets
|
||||||
|
|> Array.tryFind (fun ds -> ds.Id = form.DataSetId)
|
||||||
|
|
||||||
|
let framesExceedEnd =
|
||||||
|
selectedDataSet
|
||||||
|
|> Option.map (fun ds ->
|
||||||
|
form.StartTime.AddHours(form.Frames) > ds.EndTime
|
||||||
|
)
|
||||||
|
|> Option.defaultValue false
|
||||||
|
|
||||||
|
Fui.card [
|
||||||
|
card.classes [ "flex-column"; "gap-16"; "w-512"; "minh-512"]
|
||||||
|
card.children [
|
||||||
|
React.fragment [
|
||||||
|
Fui.cardHeader [
|
||||||
|
cardHeader.image (Fui.icon.archiveRegular [icon.size.``32``])
|
||||||
|
cardHeader.header (Fui.text.title3 [ text.weight.semibold; text.text "Add new archives"])
|
||||||
|
]
|
||||||
|
match dataSets with
|
||||||
|
| [||] ->
|
||||||
|
if isLoading then
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["flex-row"]
|
||||||
|
prop.children [
|
||||||
|
Fui.spinner []
|
||||||
|
Fui.text.body1 "Loading groups..."
|
||||||
|
]
|
||||||
|
]
|
||||||
|
else
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["flex-row"]
|
||||||
|
prop.children [
|
||||||
|
Fui.icon.warningRegular []
|
||||||
|
Fui.text.body1 "Failed to load groups"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
| dataSets' ->
|
||||||
|
Fui.field [
|
||||||
|
field.label (
|
||||||
|
Fui.label [
|
||||||
|
label.required true
|
||||||
|
label.text "DataSet"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
// field.style [style.maxWidth 256]
|
||||||
|
field.children (
|
||||||
|
Fui.dropdown [
|
||||||
|
let selectedPath = selectedDataSet |> Option.map _.BasePath |> Option.defaultValue noDataSet
|
||||||
|
dropdown.value selectedPath
|
||||||
|
dropdown.selectedOptions [|selectedPath|]
|
||||||
|
dropdown.onOptionSelect (fun (d: OptionOnSelectData) ->
|
||||||
|
d.optionText
|
||||||
|
|> Option.bind (fun path -> dataSets |> Array.tryFind (fun ds -> ds.BasePath = path))
|
||||||
|
|> Option.iter (fun ds ->
|
||||||
|
setForm {form with DataSetId = ds.Id; StartTime = ds.StartTime}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dropdown.children [
|
||||||
|
yield!
|
||||||
|
dataSets'
|
||||||
|
|> Array.groupBy _.ModelAreaName
|
||||||
|
|> Array.sortBy fst
|
||||||
|
|> Array.map (fun (modelArea, dataSetsInArea) ->
|
||||||
|
Fui.optionGroup [
|
||||||
|
optionGroup.key modelArea
|
||||||
|
optionGroup.label modelArea
|
||||||
|
optionGroup.children [
|
||||||
|
yield!
|
||||||
|
dataSetsInArea
|
||||||
|
|> Array.sort
|
||||||
|
|> Array.map (fun dataSet ->
|
||||||
|
Fui.option [
|
||||||
|
option.key dataSet.Id
|
||||||
|
option.text dataSet.BasePath
|
||||||
|
option.children (Fui.text dataSet.BasePath)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
Fui.field [
|
||||||
|
field.label (
|
||||||
|
Fui.label [
|
||||||
|
label.required true
|
||||||
|
label.text "Name"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
// field.style [style.maxWidth 256]
|
||||||
|
field.children (
|
||||||
|
Fui.input [
|
||||||
|
input.value form.Name
|
||||||
|
input.onChange (fun (v: ValueProp<string>) ->
|
||||||
|
if v.value.Length <= 80 then
|
||||||
|
setForm {form with Name = v.value}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
field.hint $"{form.Name.Length}/80"
|
||||||
|
]
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["flex-row"; "gap-8"]
|
||||||
|
prop.children [
|
||||||
|
Fui.field [
|
||||||
|
field.label (
|
||||||
|
Fui.label [
|
||||||
|
label.required true
|
||||||
|
label.text "Start Date"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
field.children (
|
||||||
|
Fui.datePicker [
|
||||||
|
datePicker.placeholder "Select a date..."
|
||||||
|
datePicker.showWeekNumbers true
|
||||||
|
match selectedDataSet with
|
||||||
|
| None -> ()
|
||||||
|
| Some ds ->
|
||||||
|
datePicker.maxDate ds.EndTime
|
||||||
|
datePicker.minDate ds.StartTime
|
||||||
|
datePicker.formatDate (fun d -> d.ToShortDateString())
|
||||||
|
datePicker.value (Some form.StartTime)
|
||||||
|
datePicker.onSelectDate (fun d ->
|
||||||
|
d |> Option.iter (fun d' ->
|
||||||
|
setForm {form with StartTime = d'}
|
||||||
|
console.debug("d': %s", d'.ToString())
|
||||||
|
console.debug("d'.Kind: %s", d'.Kind)
|
||||||
|
console.debug("utc: %s", d'.ToUniversalTime().ToString())
|
||||||
|
console.debug("utc.Kind: %s", d'.ToUniversalTime().Kind)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.field [
|
||||||
|
field.label (
|
||||||
|
Fui.label [
|
||||||
|
label.required true
|
||||||
|
label.text "Days"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
field.children (
|
||||||
|
Fui.spinButton [
|
||||||
|
spinButton.value (form.Frames / 24)
|
||||||
|
spinButton.min 1
|
||||||
|
spinButton.onChange (fun (d: SpinButtonOnChangeData) ->
|
||||||
|
match d.value with
|
||||||
|
| Some v ->
|
||||||
|
setForm {form with Frames = v * 24}
|
||||||
|
| None ->
|
||||||
|
if d.displayValue.ToCharArray() |> Array.forall System.Char.IsDigit then
|
||||||
|
setForm {form with Frames = int d.displayValue * 24}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.text.caption1 [
|
||||||
|
if framesExceedEnd then
|
||||||
|
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||||
|
|
||||||
|
let endDate = form.StartTime.AddHours(form.Frames).ToShortDateString()
|
||||||
|
text.text
|
||||||
|
($"End Date: {endDate}, {form.Frames} frames"
|
||||||
|
+
|
||||||
|
if framesExceedEnd then
|
||||||
|
" (Exceeds DataSet Bounds!)"
|
||||||
|
else
|
||||||
|
"")
|
||||||
|
]
|
||||||
|
Fui.checkbox [
|
||||||
|
checkbox.label "Published"
|
||||||
|
checkbox.checked' form.Published
|
||||||
|
checkbox.onCheckedChange (fun c -> setForm {form with Published = c})
|
||||||
|
]
|
||||||
|
Fui.checkbox [
|
||||||
|
checkbox.label "Public"
|
||||||
|
checkbox.checked' form.Public
|
||||||
|
checkbox.onCheckedChange (fun c -> setForm {form with Public = c})
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Html.div [
|
||||||
|
prop.classes [ "flex-row"; "gap-8"; ]
|
||||||
|
prop.children [
|
||||||
|
Fui.button [
|
||||||
|
button.onClick (fun _ -> handleAddArchive ())
|
||||||
|
button.text "Save"
|
||||||
|
button.icon (Fui.icon.saveRegular [])
|
||||||
|
button.appearance.primary
|
||||||
|
button.disabled (
|
||||||
|
selectedDataSet.IsNone
|
||||||
|
|| form.Frames < 24
|
||||||
|
|| form.Name |> System.String.IsNullOrWhiteSpace
|
||||||
|
|| framesExceedEnd
|
||||||
|
)
|
||||||
|
]
|
||||||
|
Fui.button [
|
||||||
|
button.onClick (fun _ ->
|
||||||
|
setForm (Remoting.AddArchiveRequest.empty ())
|
||||||
|
)
|
||||||
|
button.text "Clear"
|
||||||
|
button.icon (Fui.icon.arrowResetRegular [])
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
match errMsg with
|
||||||
|
| None -> Html.none
|
||||||
|
| Some msg ->
|
||||||
|
Html.div [
|
||||||
|
prop.classes [ "flex-row"; "gap-8"]
|
||||||
|
prop.children [
|
||||||
|
Fui.text [
|
||||||
|
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||||
|
text.text msg
|
||||||
|
]
|
||||||
|
Fui.button [
|
||||||
|
button.appearance.subtle
|
||||||
|
button.icon (Fui.icon.dismissRegular [])
|
||||||
|
button.onClick (fun _ -> setErrMsg None)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
match recentlyAddedArchives with
|
||||||
|
| [||] -> Html.none
|
||||||
|
| archives ->
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["flex-column"; "gap-16"]
|
||||||
|
prop.children [
|
||||||
|
Fui.text.title3 "Recently added"
|
||||||
|
Fui.table [
|
||||||
|
Fui.tableHeader [
|
||||||
|
Fui.tableRow [
|
||||||
|
Fui.tableHeaderCell [
|
||||||
|
tableHeaderCell.children [ Fui.text "Name"]
|
||||||
|
]
|
||||||
|
Fui.tableHeaderCell [
|
||||||
|
tableHeaderCell.children [ Fui.text "Start Time"]
|
||||||
|
]
|
||||||
|
Fui.tableHeaderCell [
|
||||||
|
tableHeaderCell.children [ Fui.text "Frames"]
|
||||||
|
]
|
||||||
|
Fui.tableHeaderCell [
|
||||||
|
tableHeaderCell.children [ Fui.text "Published"]
|
||||||
|
]
|
||||||
|
Fui.tableHeaderCell [
|
||||||
|
tableHeaderCell.children [ Fui.text "Public"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableBody [
|
||||||
|
yield!
|
||||||
|
archives |> Array.map (fun archive ->
|
||||||
|
Fui.tableRow [
|
||||||
|
tableRow.key archive.Name
|
||||||
|
tableRow.children [
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.link [
|
||||||
|
link.href (Router.format ["archives"; string archive.Id])
|
||||||
|
link.text archive.Name
|
||||||
|
]
|
||||||
|
]
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.text (archive.StartTime.ToShortDateString())
|
||||||
|
]
|
||||||
|
Fui.tableCell [
|
||||||
|
Fui.text (string archive.Frames)
|
||||||
|
]
|
||||||
|
Fui.tableCell [
|
||||||
|
if archive.Published then
|
||||||
|
Fui.icon.checkmarkRegular []
|
||||||
|
else
|
||||||
|
Fui.icon.dismissRegular []
|
||||||
|
]
|
||||||
|
Fui.tableCell [
|
||||||
|
if archive.Public then
|
||||||
|
Fui.icon.checkmarkRegular []
|
||||||
|
else
|
||||||
|
Fui.icon.dismissRegular []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
[<ReactComponent>]
|
||||||
|
let View () =
|
||||||
|
let message, setMessage = React.useState None
|
||||||
|
Html.main [
|
||||||
|
prop.classes ["flex-column"; "gap-16"]
|
||||||
|
prop.children [
|
||||||
|
Fui.text.title2 "Actions"
|
||||||
|
Html.div [
|
||||||
|
prop.classes ["flex-row"; "flex-wrap";"gap-16"]
|
||||||
|
prop.children [
|
||||||
|
UserAddForm (Option.map (fun email group -> $"User {email} was added to group {group}") >> setMessage)
|
||||||
|
ArchiveAddForm ()
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
@@ -3,6 +3,33 @@ namespace Oceanbox.Codex
|
|||||||
module Main =
|
module Main =
|
||||||
open Browser
|
open Browser
|
||||||
open Feliz
|
open Feliz
|
||||||
|
open FS.FluentUI
|
||||||
|
|
||||||
|
let maritimeBlueBrands = {
|
||||||
|
``10`` = "#020304"
|
||||||
|
``20`` = "#11181F"
|
||||||
|
``30`` = "#172736"
|
||||||
|
``40`` = "#1B3449"
|
||||||
|
``50`` = "#20405B"
|
||||||
|
``60`` = "#314D66"
|
||||||
|
``70`` = "#415972"
|
||||||
|
``80`` = "#50667D"
|
||||||
|
``90`` = "#607489"
|
||||||
|
``100`` = "#708195"
|
||||||
|
``110`` = "#808FA1"
|
||||||
|
``120`` = "#909DAD"
|
||||||
|
``130`` = "#A0ACB9"
|
||||||
|
``140`` = "#B1BAC5"
|
||||||
|
``150`` = "#C2C9D2"
|
||||||
|
``160`` = "#D3D8DE"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<ReactComponent>]
|
||||||
|
let FluentProvider () =
|
||||||
|
Fui.fluentProvider [
|
||||||
|
fluentProvider.theme.createLightTheme maritimeBlueBrands
|
||||||
|
fluentProvider.children [Components.Router()]
|
||||||
|
]
|
||||||
|
|
||||||
let root = ReactDOM.createRoot(document.getElementById "feliz-app")
|
let root = ReactDOM.createRoot(document.getElementById "feliz-app")
|
||||||
root.render(Components.Router())
|
root.render(FluentProvider())
|
||||||
@@ -3,20 +3,42 @@ namespace Oceanbox.Codex
|
|||||||
open Browser
|
open Browser
|
||||||
open Fable.Core
|
open Fable.Core
|
||||||
open Feliz
|
open Feliz
|
||||||
|
open FS.FluentUI
|
||||||
|
|
||||||
[<Erase>]
|
[<Erase>]
|
||||||
type OpenFGA =
|
type OpenFGA =
|
||||||
[<ReactComponent>]
|
[<ReactComponent>]
|
||||||
static member Checkbox(key, label: string, user: string, relation: string, object: string) =
|
static member Checkbox(key, label: string, user: string, relation: string, object: string) =
|
||||||
|
let isLoading, setLoading = React.useState false
|
||||||
let isChecked, setChecked = React.useState false
|
let isChecked, setChecked = React.useState false
|
||||||
|
|
||||||
let handleChange (ev: Types.Event) =
|
let handleChange (ev: Types.Event) =
|
||||||
console.debug("[OpenFGA.Checkbox] Checkbox %s changed to %o", key, not isChecked)
|
console.debug("[OpenFGA.Checkbox] Checkbox %s for user %s rel %s changed to %o", key, user, relation, not isChecked)
|
||||||
// TODO: Write to OpenFGA
|
// TODO: Write to OpenFGA
|
||||||
setChecked(not isChecked)
|
let newChecked = not isChecked
|
||||||
|
// setChecked
|
||||||
|
setLoading true
|
||||||
|
Remoting.adminApi.setUserPermissions {
|
||||||
|
User = user
|
||||||
|
Permissions = [|
|
||||||
|
{ Name = relation; Enabled = newChecked }
|
||||||
|
|]
|
||||||
|
}
|
||||||
|
|> Async.StartAsPromise
|
||||||
|
|> Promise.iter (fun res ->
|
||||||
|
match res with
|
||||||
|
| Ok () ->
|
||||||
|
console.debug("Success.")
|
||||||
|
setChecked newChecked
|
||||||
|
| Error err ->
|
||||||
|
console.error("Error: %s", err)
|
||||||
|
setLoading false
|
||||||
|
)
|
||||||
|
|
||||||
React.useEffect (
|
React.useEffect (
|
||||||
(fun () ->
|
(fun () ->
|
||||||
|
setLoading true
|
||||||
|
|
||||||
Remoting.openFgaApi.Check {
|
Remoting.openFgaApi.Check {
|
||||||
User = user
|
User = user
|
||||||
Relation = relation
|
Relation = relation
|
||||||
@@ -30,6 +52,8 @@ type OpenFGA =
|
|||||||
setChecked hasRelation
|
setChecked hasRelation
|
||||||
| Error err ->
|
| Error err ->
|
||||||
console.error("[OpenFGA.Checkbox] Error checking user %s has relation %s to %s", user, relation, object)
|
console.error("[OpenFGA.Checkbox] Error checking user %s has relation %s to %s", user, relation, object)
|
||||||
|
|
||||||
|
setLoading false
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
[| |]
|
[| |]
|
||||||
@@ -38,17 +62,21 @@ type OpenFGA =
|
|||||||
Html.div [
|
Html.div [
|
||||||
prop.classes [ "flex-row-center" ]
|
prop.classes [ "flex-row-center" ]
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.input [
|
if isLoading then
|
||||||
prop.id (sprintf "openfga-checkbox-%s" key)
|
Fui.spinner [
|
||||||
prop.type'.checkbox
|
spinner.size.tiny
|
||||||
prop.onChange handleChange
|
]
|
||||||
prop.custom("checked", isChecked)
|
else
|
||||||
]
|
Html.input [
|
||||||
|
prop.id (sprintf "openfga-checkbox-%s" key)
|
||||||
|
prop.type'.checkbox
|
||||||
|
prop.onChange handleChange
|
||||||
|
prop.custom("checked", isChecked)
|
||||||
|
]
|
||||||
|
|
||||||
Html.label [
|
Html.label [
|
||||||
prop.htmlFor (sprintf "openfga-checkbox-%s" key)
|
prop.htmlFor (sprintf "openfga-checkbox-%s" key)
|
||||||
prop.text label
|
prop.text label
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -31,6 +31,8 @@ module Types =
|
|||||||
|
|
||||||
let decode = Decode.fromString decoder
|
let decode = Decode.fromString decoder
|
||||||
|
|
||||||
|
let encode : Remoting.ViewTerm -> string = Encode.Auto.toString
|
||||||
|
|
||||||
module ExecTicket =
|
module ExecTicket =
|
||||||
open Thoth.Json
|
open Thoth.Json
|
||||||
|
|
||||||
@@ -55,12 +57,59 @@ module Types =
|
|||||||
|
|
||||||
let decode = Decode.fromString decoder
|
let decode = Decode.fromString decoder
|
||||||
|
|
||||||
[<RequireQualifiedAccess>]
|
module ArchiveRelation =
|
||||||
type ArchiveRelation =
|
open Browser
|
||||||
| ViewTerm of Remoting.ViewTerm
|
open Fable.Core
|
||||||
| ExecTicket of Remoting.ExecTicket
|
|
||||||
static member tryOfCondition(cond: Remoting.Condition) =
|
let tryOfCondition(cond: Remoting.Condition) =
|
||||||
match cond.Name with
|
match cond.Name with
|
||||||
| "term" -> cond.Context |> ViewTerm.decode |> Result.toOption |> Option.map ViewTerm
|
| "term" ->
|
||||||
| "ticket" -> cond.Context |> ExecTicket.decode |> Result.toOption |> Option.map ExecTicket
|
match ViewTerm.decode cond.Context with
|
||||||
| _ -> None
|
| Ok ctx -> Remoting.ArchiveRelation.ViewTerm ctx |> Some
|
||||||
|
| Error err ->
|
||||||
|
console.error("Error decoding term: %s", err)
|
||||||
|
None
|
||||||
|
| "ticket" ->
|
||||||
|
match ExecTicket.decode cond.Context with
|
||||||
|
| Ok ctx -> Remoting.ArchiveRelation.ExecTicket ctx |> Some
|
||||||
|
| Error err ->
|
||||||
|
console.error("Error decoding ticket: %s", err)
|
||||||
|
None
|
||||||
|
| _ ->
|
||||||
|
console.error("Got unknown condition: %s", cond.Name)
|
||||||
|
None
|
||||||
|
|
||||||
|
let toCond (relation: Remoting.ArchiveRelation) : Remoting.Condition =
|
||||||
|
match relation with
|
||||||
|
| Remoting.ArchiveRelation.ViewTerm term -> {
|
||||||
|
Name = "term"
|
||||||
|
Context = JS.JSON.stringify term.JsonObj
|
||||||
|
}
|
||||||
|
| Remoting.ArchiveRelation.ExecTicket ticket -> {
|
||||||
|
Name = "ticket"
|
||||||
|
Context = JS.JSON.stringify ticket.JsonObj
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
let toTuple user object (relation: Remoting.ArchiveRelation) : Remoting.Tuple =
|
||||||
|
match relation with
|
||||||
|
| Remoting.ArchiveRelation.ViewTerm term ->
|
||||||
|
{
|
||||||
|
User = user
|
||||||
|
Relation = "view"
|
||||||
|
Object = object
|
||||||
|
Condition = Some {
|
||||||
|
Name = "term"
|
||||||
|
Context = JS.JSON.stringify term.JsonObj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| Remoting.ArchiveRelation.ExecTicket ticket ->
|
||||||
|
{
|
||||||
|
User = user
|
||||||
|
Relation = "exec"
|
||||||
|
Object = object
|
||||||
|
Condition = Some {
|
||||||
|
Name = "ticket"
|
||||||
|
Context = JS.JSON.stringify ticket.JsonObj
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,23 +29,34 @@ type OpenFGA =
|
|||||||
|
|
||||||
[<Hook>]
|
[<Hook>]
|
||||||
static member useObjects(user: string, relation: string, objectType: string, ?context: obj) : Objects =
|
static member useObjects(user: string, relation: string, objectType: string, ?context: obj) : Objects =
|
||||||
let objects, setObjects = React.useState<Objects> Objects.Empty
|
let isLoading, setLoading = React.useState true
|
||||||
|
let error, setError = React.useState<string option> None
|
||||||
|
let objects, setObjects = React.useState<string array> Array.empty
|
||||||
|
|
||||||
React.useEffect (
|
React.useEffect (
|
||||||
(fun () ->
|
(fun () ->
|
||||||
setObjects { objects with Loading = true; Error = None }
|
setLoading true
|
||||||
|
setError None
|
||||||
|
|
||||||
OpenFGA.fetchObjects(user, relation, objectType, context)
|
OpenFGA.fetchObjects(user, relation, objectType, context)
|
||||||
|> Promise.iter (fun res ->
|
|> Promise.iter (fun res ->
|
||||||
match res with
|
match res with
|
||||||
| Ok newObjects ->
|
| Ok newObjects ->
|
||||||
setObjects { objects with Loading = false; Objects = newObjects }
|
setObjects newObjects
|
||||||
| Error err ->
|
| Error err ->
|
||||||
console.error("[OpenFGA] Error loading user objects %s", err)
|
console.error("[OpenFGA] Error loading user objects: %s", err)
|
||||||
setObjects { objects with Loading = false; Error = Some err }
|
setError (Some err)
|
||||||
|
|
||||||
|
setLoading false
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
[| box user |]
|
[| box user |]
|
||||||
)
|
)
|
||||||
|
|
||||||
objects
|
let props = {
|
||||||
|
Loading = isLoading
|
||||||
|
Error = error
|
||||||
|
Objects = objects
|
||||||
|
}
|
||||||
|
|
||||||
|
props
|
||||||
@@ -25,32 +25,37 @@ type OpenFGA =
|
|||||||
|
|
||||||
[<Hook>]
|
[<Hook>]
|
||||||
static member useReadTuples(?user: string, ?relation: string, ?object: string) : Tuples =
|
static member useReadTuples(?user: string, ?relation: string, ?object: string) : Tuples =
|
||||||
let tuples, setTuples = React.useState<Tuples> Tuples.Empty
|
let isLoading, setLoading = React.useState true
|
||||||
|
let error, setError = React.useState None
|
||||||
|
let tuples, setTuples = React.useState<Remoting.Tuple array> [||]
|
||||||
|
|
||||||
let handleSetTuples (newTuples: Remoting.Tuple array) =
|
let handleSetTuples (newTuples: Remoting.Tuple array) =
|
||||||
setTuples { tuples with Tuples = newTuples }
|
console.debug("[OpenFGA] Read tuples set by outside: %o", newTuples)
|
||||||
|
setTuples newTuples
|
||||||
|
|
||||||
React.useEffect (
|
React.useEffect (
|
||||||
(fun () ->
|
(fun () ->
|
||||||
setTuples { tuples with Loading = true; Error = None }
|
setLoading true
|
||||||
|
|
||||||
OpenFGA.fetchTuples(?user = user, ?relation = relation, ?object = object)
|
OpenFGA.fetchTuples(?user = user, ?relation = relation, ?object = object)
|
||||||
|> Promise.iter (fun res ->
|
|> Promise.iter (fun res ->
|
||||||
match res with
|
match res with
|
||||||
| Ok resp ->
|
| Ok resp ->
|
||||||
let newTuples = resp.Tuples |> Array.map _.Key
|
let newTuples = resp.Tuples |> Array.map _.Key
|
||||||
setTuples {
|
setTuples newTuples
|
||||||
tuples with
|
|
||||||
Loading = false
|
|
||||||
SetTuples = handleSetTuples
|
|
||||||
Tuples = newTuples
|
|
||||||
}
|
|
||||||
| Error err ->
|
| Error err ->
|
||||||
console.error("[OpenFGA] Error loading user objects %s", err)
|
console.error("[OpenFGA] Error loading user objects %s", err)
|
||||||
setTuples { tuples with Loading = false; Error = Some err }
|
setError (Some err)
|
||||||
|
|
||||||
|
setLoading false
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
[| box user |]
|
[| box user |]
|
||||||
)
|
)
|
||||||
|
|
||||||
tuples
|
{
|
||||||
|
Loading = isLoading
|
||||||
|
Error = error
|
||||||
|
SetTuples = handleSetTuples
|
||||||
|
Tuples = tuples
|
||||||
|
}
|
||||||
@@ -1,136 +1,61 @@
|
|||||||
namespace Oceanbox.Codex
|
namespace Oceanbox.Codex
|
||||||
|
|
||||||
open Browser
|
|
||||||
open Fable.Core
|
|
||||||
open Fable.Remoting.Client
|
|
||||||
open Feliz
|
open Feliz
|
||||||
open Feliz.Router
|
open Feliz.Router
|
||||||
|
|
||||||
[<Erase>]
|
|
||||||
type User =
|
|
||||||
[<ReactComponent>]
|
|
||||||
static member List(user: string, relation: string, objectType: string, ?context: obj) =
|
|
||||||
let objects = OpenFGA.useObjects(user, relation, objectType, context)
|
|
||||||
|
|
||||||
if objects.Loading then
|
|
||||||
Html.p "Loading ..."
|
|
||||||
else
|
|
||||||
if Array.isEmpty objects.Objects then
|
|
||||||
Html.p (sprintf "No objects with user %s relation %s of type %s" user relation objectType)
|
|
||||||
else
|
|
||||||
Html.ul [
|
|
||||||
prop.children (
|
|
||||||
objects.Objects
|
|
||||||
|> Array.sort
|
|
||||||
|> Array.map (fun object ->
|
|
||||||
let split = object.Split ':'
|
|
||||||
match split with
|
|
||||||
| [| objectType; id |] ->
|
|
||||||
Html.li [
|
|
||||||
prop.key id
|
|
||||||
prop.children [
|
|
||||||
Html.a [
|
|
||||||
prop.href (Router.format("archives", id))
|
|
||||||
prop.text id
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
| _ ->
|
|
||||||
Html.li [
|
|
||||||
prop.text "Invalid object format"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
module User =
|
module User =
|
||||||
[<ReactComponent>]
|
|
||||||
let private DeleteForm (user: string) =
|
|
||||||
let deleted, setDeleted = React.useState<Result<unit, string> option> None
|
|
||||||
let deleting, setDeleting = React.useState false
|
|
||||||
|
|
||||||
let handleDelete =
|
|
||||||
React.useCallback (
|
|
||||||
(fun () ->
|
|
||||||
setDeleting true
|
|
||||||
console.info("[User] Deleting user %s", user)
|
|
||||||
Remoting.adminApi.removeUsers [| user |]
|
|
||||||
|> Async.StartAsPromise
|
|
||||||
|> Promise.catch (fun ex ->
|
|
||||||
match ex with
|
|
||||||
| :? ProxyRequestException as e ->
|
|
||||||
let proxyError : Types.ProxyError = JS.JSON.parse e.ResponseText |> unbox
|
|
||||||
let msg = proxyError.error.errorMsg
|
|
||||||
Error msg
|
|
||||||
| ex ->
|
|
||||||
Error ex.Message
|
|
||||||
)
|
|
||||||
|> Promise.iter (fun res ->
|
|
||||||
match res with
|
|
||||||
| Ok () ->
|
|
||||||
console.info("[User] Successfully deleted user %s", user)
|
|
||||||
setDeleted (Some (Ok ()))
|
|
||||||
| Error err ->
|
|
||||||
console.error("[User] Error deleting user %s: %s", user, err)
|
|
||||||
setDeleted (Some (Error err))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
[| box user |]
|
|
||||||
)
|
|
||||||
|
|
||||||
React.fragment [
|
|
||||||
match deleted with
|
|
||||||
| Some (Ok ()) ->
|
|
||||||
Html.p "User successfully deleted."
|
|
||||||
Html.a [
|
|
||||||
prop.onClick (fun ev ->
|
|
||||||
ev.preventDefault ()
|
|
||||||
Router.navigateBack ()
|
|
||||||
)
|
|
||||||
prop.href (Router.format "")
|
|
||||||
prop.text "Back"
|
|
||||||
]
|
|
||||||
| Some (Error err) ->
|
|
||||||
Html.p (sprintf "Error deleting user: %s" err)
|
|
||||||
| None ->
|
|
||||||
if deleting then
|
|
||||||
Html.div [
|
|
||||||
prop.classes [ "flex-row-center"; "gap-8" ]
|
|
||||||
prop.children [
|
|
||||||
Html.button [
|
|
||||||
prop.onClick (fun _ -> handleDelete ())
|
|
||||||
prop.text "Are you sure?"
|
|
||||||
]
|
|
||||||
|
|
||||||
Html.button [
|
|
||||||
prop.onClick (fun _ -> setDeleting false)
|
|
||||||
prop.text "Cancel"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
Html.p "This will delete the user from the databases. Not disable the user."
|
|
||||||
else
|
|
||||||
Html.button [
|
|
||||||
prop.onClick (fun _ -> setDeleting true)
|
|
||||||
prop.text "Delete"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
[<ReactComponent>]
|
[<ReactComponent>]
|
||||||
let View (user: string) =
|
let View (user: string) =
|
||||||
let fgaUser = sprintf "user:%s" user
|
let fgaUser = sprintf "user:%s" user
|
||||||
|
let execCtx = {|
|
||||||
|
time = System.DateTime.Now
|
||||||
|
task = "*"
|
||||||
|
usage = "-1"
|
||||||
|
|}
|
||||||
|
|
||||||
|
let groups = OpenFGA.useObjects(fgaUser, "member", "group")
|
||||||
|
|
||||||
Html.main [
|
Html.main [
|
||||||
Html.h1 user
|
Html.h1 user
|
||||||
|
|
||||||
Html.section [
|
Html.section [
|
||||||
prop.children [
|
prop.children [
|
||||||
DeleteForm user
|
Users.DeleteForm user
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Html.section [
|
||||||
|
prop.children [
|
||||||
|
Html.h2 "Groups"
|
||||||
|
|
||||||
|
Html.div [
|
||||||
|
prop.children [
|
||||||
|
Html.ul [
|
||||||
|
prop.children (
|
||||||
|
groups.Objects
|
||||||
|
|> Array.map (fun group ->
|
||||||
|
let split = group.Split ':'
|
||||||
|
match split with
|
||||||
|
| [| "group"; groupName |] ->
|
||||||
|
Html.li [
|
||||||
|
prop.children [
|
||||||
|
Html.a [
|
||||||
|
prop.href (Router.format("groups", groupName))
|
||||||
|
prop.text groupName
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
| _ ->
|
||||||
|
Html.none
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
Html.section [
|
Html.section [
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.h2 "Archmaester"
|
Html.h2 "Archmaester"
|
||||||
@@ -170,7 +95,7 @@ module User =
|
|||||||
style.maxHeight (length.px 512)
|
style.maxHeight (length.px 512)
|
||||||
]
|
]
|
||||||
prop.children [
|
prop.children [
|
||||||
User.List(fgaUser, "owner", "archive")
|
Users.OpenFgaList(fgaUser, "owner", "archive")
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -192,7 +117,7 @@ module User =
|
|||||||
style.maxHeight (length.px 512)
|
style.maxHeight (length.px 512)
|
||||||
]
|
]
|
||||||
prop.children [
|
prop.children [
|
||||||
User.List(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
|
Users.OpenFgaList(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -214,7 +139,7 @@ module User =
|
|||||||
style.maxHeight (length.px 512)
|
style.maxHeight (length.px 512)
|
||||||
]
|
]
|
||||||
prop.children [
|
prop.children [
|
||||||
User.List(fgaUser, "exec", "archive", {| time = System.DateTime.Now |})
|
Users.OpenFgaList(fgaUser, "exec", "archive", execCtx)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -223,4 +148,4 @@ module User =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
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
|
echo Building Codex frontend
|
||||||
|
|
||||||
fable -e .jsx -o build --verbose --run \
|
fable -e .jsx -o build --verbose --test:MSBuildCracker --run \
|
||||||
bunx --bun vite build -d -c ../../vite.config.js --mode development --minify false --outDir /home/simkir/oceanbox/poseidon/src/Codex/dist/WebRoot
|
bunx --bun \
|
||||||
|
vite build -d -c ../../vite.config.js --mode development --minify false --outDir ../../dist/WebRoot
|
||||||
@@ -1,15 +1,52 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Feliz App</title>
|
<title>Codex</title>
|
||||||
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
|
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="shortcut icon" type="image/png" href="/img/favicon-32x32.png" sizes="32x32" />
|
<link rel="shortcut icon" type="image/png" href="/img/favicon-32x32.png" sizes="32x32" />
|
||||||
<link rel="shortcut icon" type="image/png" href="/img/favicon-16x16.png" sizes="16x16" />
|
<link rel="shortcut icon" type="image/png" href="/img/favicon-16x16.png" sizes="16x16" />
|
||||||
<link rel="stylesheet" href="/main.css" />
|
<link rel="stylesheet" href="/main.css" />
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Segoe UI Web (West European)';
|
||||||
|
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-light.woff2);
|
||||||
|
font-weight: 100;
|
||||||
|
font-style: 'normal';
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Segoe UI Web (West European)';
|
||||||
|
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-semilight.woff2);
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: 'normal';
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Segoe UI Web (West European)';
|
||||||
|
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-regular.woff2);
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: 'normal';
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Segoe UI Web (West European)';
|
||||||
|
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-semibold.woff2);
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: 'normal';
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Segoe UI Web (West European)';
|
||||||
|
src: url(https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/fonts/segoeui-westeuropean/segoeui-bold.woff2);
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: 'normal';
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="margin: 0;">
|
||||||
<div id="feliz-app"></div>
|
<div id="feliz-app"></div>
|
||||||
<script type="module" src="/build/Main.jsx"></script>
|
<script type="module" src="/build/Main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"Fable.Core": {
|
"Fable.Core": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[4.4.0, )",
|
"requested": "[4.4.0, )",
|
||||||
@@ -75,6 +75,16 @@
|
|||||||
"Fable.Elmish": "4.0.0"
|
"Fable.Elmish": "4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"FS.FluentUI": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.0.0, )",
|
||||||
|
"resolved": "3.0.0",
|
||||||
|
"contentHash": "QXFfClv7q7UGYRJJ5K5Gz9B2QvxNFNn5sLSbojLb0rITbQE8BfkyJEYaY3MmRyQgAsbWu3Xr62AR+W2fJbcfsA==",
|
||||||
|
"dependencies": {
|
||||||
|
"FSharp.Core": "6.0.1",
|
||||||
|
"Feliz": "2.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"FsToolkit.ErrorHandling": {
|
"FsToolkit.ErrorHandling": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[5.0.1, )",
|
"requested": "[5.0.1, )",
|
||||||
|
|||||||
@@ -116,6 +116,15 @@ h1 {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bcol-neutral-stroke-1 {
|
||||||
|
border-color: var(--colorNeutralStroke1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.br-solid {
|
||||||
|
border-right-width: 2px;
|
||||||
|
border-right-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
.text-overflow {
|
.text-overflow {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -126,6 +135,26 @@ h1 {
|
|||||||
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vh-100 {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-256 {
|
||||||
|
width: 256px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-512 {
|
||||||
|
width: 512px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minh-512 {
|
||||||
|
min-height: 512px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mw-128 {
|
||||||
|
max-width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
/* special stuff */
|
/* special stuff */
|
||||||
|
|
||||||
.archives-list {
|
.archives-list {
|
||||||
@@ -147,4 +176,4 @@ h1 {
|
|||||||
flex: 2 1 512px;
|
flex: 2 1 512px;
|
||||||
min-width: 512px;
|
min-width: 512px;
|
||||||
max-width: 576px;
|
max-width: 576px;
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,19 @@ module Admin =
|
|||||||
|
|
||||||
open Oceanbox
|
open Oceanbox
|
||||||
|
|
||||||
|
// TODO: Better name and/or place for this?
|
||||||
|
module private EFType =
|
||||||
|
let toArchive (efArchive: Entity.Archive) : Remoting.Archive = {
|
||||||
|
Id = efArchive.ArchiveId
|
||||||
|
Name = efArchive.Name
|
||||||
|
DataSetId = efArchive.AttribsId
|
||||||
|
StartTime = efArchive.StartTime
|
||||||
|
Frames = efArchive.Frames
|
||||||
|
Published = efArchive.Published
|
||||||
|
Public = efArchive.Public
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
module private Handler =
|
module private Handler =
|
||||||
let addUsers (ctx: HttpContext) (req: Remoting.AddUsersRequest) : Async<Result<unit, string>> =
|
let addUsers (ctx: HttpContext) (req: Remoting.AddUsersRequest) : Async<Result<unit, string>> =
|
||||||
let archmaesterAdd (db: Entity.ArchiveContext) =
|
let archmaesterAdd (db: Entity.ArchiveContext) =
|
||||||
@@ -63,7 +76,7 @@ module Admin =
|
|||||||
with e ->
|
with e ->
|
||||||
do logger.LogError ("OpenFGA write errored with: {Msg}. Rolling back archmaester.", e.Message)
|
do logger.LogError ("OpenFGA write errored with: {Msg}. Rolling back archmaester.", e.Message)
|
||||||
do! tr.RollbackAsync ()
|
do! tr.RollbackAsync ()
|
||||||
return! Error (sprintf "Error deleting users from OpenFGA: %s" e.Message)
|
return! Error (sprintf "Error adding users to OpenFGA: %s" e.Message)
|
||||||
with e ->
|
with e ->
|
||||||
do logger.LogError (e, "Failed connecting to database")
|
do logger.LogError (e, "Failed connecting to database")
|
||||||
return! Error (sprintf "Error deleting users from OpenFGA: %s" e.Message)
|
return! Error (sprintf "Error deleting users from OpenFGA: %s" e.Message)
|
||||||
@@ -76,21 +89,50 @@ module Admin =
|
|||||||
do logger.LogInformation ("Add archive groups from {User}: {Request}", user, req)
|
do logger.LogInformation ("Add archive groups from {User}: {Request}", user, req)
|
||||||
try
|
try
|
||||||
let db = ctx.GetService<Entity.ArchiveContext> ()
|
let db = ctx.GetService<Entity.ArchiveContext> ()
|
||||||
let fga = ctx.GetService<OpenFgaClient> ()
|
|
||||||
|
|
||||||
let! created = Archmaester.EFCore.addArchiveGroups db req.Id req.Groups
|
let! created = Archmaester.EFCore.addArchiveGroups db req.Id req.Groups
|
||||||
do logger.LogInformation ("Added {CreatedCount} archive group entries", created)
|
do logger.LogInformation ("Added {CreatedCount} archive group entries", created)
|
||||||
|
|
||||||
|
let fga = ctx.GetService<OpenFgaClient> ()
|
||||||
let req = OpenFGA.Group.addArchive req
|
let req = OpenFGA.Group.addArchive req
|
||||||
let! fgaResp = fga.Write req |> Async.AwaitTask
|
let! fgaResp = fga.Write req |> Async.AwaitTask
|
||||||
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaResp.ToJson ())
|
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaResp.ToJson ())
|
||||||
|
|
||||||
return Ok ()
|
return Ok ()
|
||||||
with e ->
|
with e ->
|
||||||
do logger.LogError(e, "Error adding group to archive")
|
do logger.LogError (e, "Error adding group to archive")
|
||||||
return Error (sprintf "Error adding archive groups: %s" e.Message)
|
return Error (sprintf "Error adding archive groups: %s" e.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let private permissionToTuple
|
||||||
|
(archiveId: System.Guid)
|
||||||
|
(groupName: string)
|
||||||
|
(relation: Remoting.ArchiveRelation)
|
||||||
|
: Model.ClientTupleKey =
|
||||||
|
match relation with
|
||||||
|
| Remoting.ArchiveRelation.ViewTerm term -> OpenFGA.Group.viewArchive archiveId term groupName
|
||||||
|
| Remoting.ArchiveRelation.ExecTicket ticket -> OpenFGA.Group.execArchive archiveId ticket groupName
|
||||||
|
|
||||||
|
let addGroupPermissions (ctx: HttpContext) (req: Remoting.AddGroupPermissionsRequest) =
|
||||||
|
async {
|
||||||
|
let user = ctx.User.Identity.Name
|
||||||
|
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||||
|
do logger.LogInformation ("Add group archive permissions from {User}: {Request}", user, req)
|
||||||
|
try
|
||||||
|
let fga = ctx.GetService<OpenFgaClient> ()
|
||||||
|
let fgaReq: Model.ClientWriteRequest =
|
||||||
|
req.Permissions
|
||||||
|
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|
||||||
|
|> OpenFGA.Queries.write'
|
||||||
|
let! fgaResp = fga.Write fgaReq |> Async.AwaitTask
|
||||||
|
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaResp.ToJson ())
|
||||||
|
|
||||||
|
return Ok ()
|
||||||
|
with e ->
|
||||||
|
do logger.LogError (e, "Error adding archive permission to group")
|
||||||
|
return Error (sprintf "Error adding group permissions: %s" e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
let deleteArchive (ctx: HttpContext) (archiveId: System.Guid) : Async<Result<bool, string>> =
|
let deleteArchive (ctx: HttpContext) (archiveId: System.Guid) : Async<Result<bool, string>> =
|
||||||
async {
|
async {
|
||||||
let user = ctx.User.Identity.Name
|
let user = ctx.User.Identity.Name
|
||||||
@@ -179,7 +221,7 @@ module Admin =
|
|||||||
else
|
else
|
||||||
return Error "Filter must include archive id"
|
return Error "Filter must include archive id"
|
||||||
with e ->
|
with e ->
|
||||||
do logger.LogError (e, "Error in getArchives from {User}", user)
|
do logger.LogError (e, "Error in getArchiveRefs from {User}", user)
|
||||||
return Error "Error fetching archive count"
|
return Error "Error fetching archive count"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +277,186 @@ module Admin =
|
|||||||
return Error "Error fetching archive count"
|
return Error "Error fetching archive count"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let getAllDataSets (ctx: HttpContext) =
|
||||||
|
async {
|
||||||
|
let user = ctx.User.Identity.Name
|
||||||
|
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||||
|
try
|
||||||
|
do logger.LogInformation("getAllDataSets from {User}", user)
|
||||||
|
let db = ctx.GetService<NpgsqlDataSource> ()
|
||||||
|
let! dataSets =
|
||||||
|
Archmaester.Dapper.queryDataSets db
|
||||||
|
return Ok dataSets
|
||||||
|
with e ->
|
||||||
|
do logger.LogError (e, "getAllDataSets from {User}", user)
|
||||||
|
return Error "Error fetching dataSets"
|
||||||
|
}
|
||||||
|
|
||||||
|
let getArchiveDataSet (ctx: HttpContext) (archiveId: System.Guid) =
|
||||||
|
async {
|
||||||
|
let user = ctx.User.Identity.Name
|
||||||
|
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||||
|
try
|
||||||
|
do logger.LogInformation("getArchiveDataSet {Archive} from {User}", archiveId, user)
|
||||||
|
let db = ctx.GetService<NpgsqlDataSource> ()
|
||||||
|
let! dataSet = Archmaester.Dapper.queryArchiveDataSet db archiveId
|
||||||
|
return Ok dataSet
|
||||||
|
with e ->
|
||||||
|
do logger.LogError (e, "getArchiveDataSet {Archive} from {User}", archiveId, user)
|
||||||
|
return Error "Error fetching dataset"
|
||||||
|
}
|
||||||
|
|
||||||
|
let addArchive (ctx: HttpContext) (archive: Remoting.AddArchiveRequest) =
|
||||||
|
let archmaesterAdd (db: Entity.ArchiveContext) =
|
||||||
|
async {
|
||||||
|
try
|
||||||
|
let! nameTaken = Archmaester.EFCore.checkArchiveNameTaken db archive.Name
|
||||||
|
if nameTaken then
|
||||||
|
return Error "Error adding archive: an archive with that name already exists"
|
||||||
|
else
|
||||||
|
let! newArchive = Archmaester.EFCore.addArchive db archive
|
||||||
|
return Ok newArchive
|
||||||
|
with e ->
|
||||||
|
return Error (sprintf "Error adding archive: %s" e.Message)
|
||||||
|
}
|
||||||
|
let archmaesterAddPublic (db: Entity.ArchiveContext) (archiveId: System.Guid) =
|
||||||
|
async {
|
||||||
|
try
|
||||||
|
let! success = Archmaester.EFCore.setArchivePublic db archiveId true
|
||||||
|
if success > 0 then
|
||||||
|
return Ok ()
|
||||||
|
else
|
||||||
|
return Error "Failed to add * user to public archive"
|
||||||
|
with e ->
|
||||||
|
return Error (sprintf "Error adding * user to archive: %s" e.Message)
|
||||||
|
}
|
||||||
|
asyncResult {
|
||||||
|
let user = ctx.User.Identity.Name
|
||||||
|
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||||
|
try
|
||||||
|
do logger.LogInformation("addArchive {Archive} from {User}", archive.Name, user)
|
||||||
|
let fga = ctx.GetService<OpenFgaClient> ()
|
||||||
|
let db = ctx.GetService<Entity.ArchiveContext> ()
|
||||||
|
let tr = db.Database.BeginTransaction()
|
||||||
|
|
||||||
|
let! newArchive = archmaesterAdd db
|
||||||
|
try
|
||||||
|
if newArchive.Public then
|
||||||
|
do! archmaesterAddPublic db newArchive.ArchiveId
|
||||||
|
|
||||||
|
let tuples =
|
||||||
|
if newArchive.Public then
|
||||||
|
[|
|
||||||
|
OpenFGA.Archive.defaultPrincipal newArchive.ArchiveId
|
||||||
|
OpenFGA.Archive.publicArchive newArchive.ArchiveId
|
||||||
|
|]
|
||||||
|
else
|
||||||
|
[|OpenFGA.Archive.defaultPrincipal newArchive.ArchiveId|]
|
||||||
|
|
||||||
|
let! fgaResp =
|
||||||
|
let req = OpenFGA.Queries.write' tuples
|
||||||
|
fga.Write req |> Async.AwaitTask
|
||||||
|
|
||||||
|
logger.LogInformation (
|
||||||
|
"addArchive: {Archive} openFGA write responded with {JSON}",
|
||||||
|
archive.Name,
|
||||||
|
fgaResp.ToJson ()
|
||||||
|
)
|
||||||
|
|
||||||
|
do! tr.CommitAsync ()
|
||||||
|
|
||||||
|
return! newArchive |> EFType.toArchive |> Ok
|
||||||
|
with e ->
|
||||||
|
// TODO: Ideally, we should rollback fga too
|
||||||
|
do logger.LogError (e, "addArchive OpenFGA failed, rolling back Archmaester")
|
||||||
|
do! tr.RollbackAsync ()
|
||||||
|
return! Error (sprintf "Error adding archive: %s" e.Message)
|
||||||
|
with e ->
|
||||||
|
do logger.LogError (e, "addArchive {Archive} from {User}", archive.Name, user)
|
||||||
|
return! Error "Error adding archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateArchive (ctx: HttpContext) (archiveId: System.Guid) (archive: Remoting.EditArchiveRequest) =
|
||||||
|
let archmaesterEdit (db: Entity.ArchiveContext) =
|
||||||
|
async {
|
||||||
|
try
|
||||||
|
let! existingName = Archmaester.EFCore.getArchiveName db archiveId
|
||||||
|
let! nameTaken = Archmaester.EFCore.checkArchiveNameTaken db archive.Name
|
||||||
|
if nameTaken && not (existingName = archive.Name) then
|
||||||
|
return Error "Error updating archive: an archive with that name already exists"
|
||||||
|
else
|
||||||
|
let! newArchive = Archmaester.EFCore.editArchive db archiveId archive
|
||||||
|
return Ok newArchive
|
||||||
|
with e ->
|
||||||
|
return Error (sprintf "Error updating archive: %s" e.Message)
|
||||||
|
}
|
||||||
|
let archmaesterSetPublic (db: Entity.ArchiveContext) (setPublic: bool) =
|
||||||
|
async {
|
||||||
|
try
|
||||||
|
let! success = Archmaester.EFCore.setArchivePublic db archiveId setPublic
|
||||||
|
if success > 0 then
|
||||||
|
return Ok ()
|
||||||
|
else
|
||||||
|
return Error "Failed to add * user to public archive"
|
||||||
|
with e ->
|
||||||
|
return Error (sprintf "Error adding/removing * user to/from archive: %s" e.Message)
|
||||||
|
}
|
||||||
|
let fgaSetPublic (fga: OpenFgaClient) (setPublic: bool) =
|
||||||
|
async {
|
||||||
|
try
|
||||||
|
let publicTuple = OpenFGA.Archive.publicArchive archiveId
|
||||||
|
let checkReq =
|
||||||
|
OpenFGA.Queries.check { User = publicTuple.User; Relation = publicTuple.Relation; Object = publicTuple.Object}
|
||||||
|
let! publicResp = fga.Check checkReq |> Async.AwaitTask
|
||||||
|
let publicTupleExists = publicResp.Allowed |> Option.ofNullable |> Option.defaultValue false
|
||||||
|
|
||||||
|
if setPublic && not publicTupleExists then
|
||||||
|
let writeReq = OpenFGA.Queries.write' [|publicTuple|]
|
||||||
|
let! writeResp = fga.Write writeReq |> Async.AwaitTask
|
||||||
|
match writeResp.Writes |> Array.ofSeq with
|
||||||
|
| [||] -> return Error (sprintf "Error writing public tuple to fga: %s" (writeResp.ToJson()))
|
||||||
|
| _ -> return Ok ()
|
||||||
|
elif not setPublic && publicTupleExists then
|
||||||
|
let deleteReq = OpenFGA.Queries.delete' [|publicTuple|]
|
||||||
|
let! deleteResp = fga.Write deleteReq |> Async.AwaitTask
|
||||||
|
match deleteResp.Deletes |> Array.ofSeq with
|
||||||
|
| [||] -> return Error (sprintf "Error deleting public tuple from fga: %s" (deleteResp.ToJson()))
|
||||||
|
| _ -> return Ok ()
|
||||||
|
else
|
||||||
|
return Ok ()
|
||||||
|
with e ->
|
||||||
|
return Error (sprintf "Error adding/removing fga user:* view relation to/from archive: %s" e.Message)
|
||||||
|
}
|
||||||
|
asyncResult {
|
||||||
|
let user = ctx.User.Identity.Name
|
||||||
|
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||||
|
try
|
||||||
|
do logger.LogInformation("updateArchive {Archive} from {User}", archive.Name, user)
|
||||||
|
let fga = ctx.GetService<OpenFgaClient> ()
|
||||||
|
let db = ctx.GetService<Entity.ArchiveContext> ()
|
||||||
|
let tr = db.Database.BeginTransaction()
|
||||||
|
|
||||||
|
let! newArchive = archmaesterEdit db
|
||||||
|
try
|
||||||
|
if newArchive.Public && not archive.Public then
|
||||||
|
do! archmaesterSetPublic db true
|
||||||
|
do! fgaSetPublic fga true
|
||||||
|
elif not newArchive.Public && archive.Public then
|
||||||
|
do! archmaesterSetPublic db false
|
||||||
|
do! fgaSetPublic fga false
|
||||||
|
|
||||||
|
do! tr.CommitAsync ()
|
||||||
|
|
||||||
|
return! newArchive |> EFType.toArchive |> Ok
|
||||||
|
with e ->
|
||||||
|
do logger.LogError (e, "updateArchive setting public failed, rolling back Archmaester")
|
||||||
|
do! tr.RollbackAsync ()
|
||||||
|
return! Error (sprintf "Error updating archive: %s" e.Message)
|
||||||
|
with e ->
|
||||||
|
do logger.LogError (e, "updateArchive {Archive} from {User}", archive.Name, user)
|
||||||
|
return! Error (sprintf "Error updating archive: %s" e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
let getArchiveTypes (ctx: HttpContext) =
|
let getArchiveTypes (ctx: HttpContext) =
|
||||||
async {
|
async {
|
||||||
let user = ctx.User.Identity.Name
|
let user = ctx.User.Identity.Name
|
||||||
@@ -306,18 +528,101 @@ module Admin =
|
|||||||
return ()
|
return ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let setUserPermissions (ctx: HttpContext) (req: Remoting.UserPermissionRequest) =
|
||||||
|
async {
|
||||||
|
let user = ctx.User.Identity.Name
|
||||||
|
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||||
|
do logger.LogInformation ("setUserPermissions from {User}: {@Req}", user, req)
|
||||||
|
// TODO(simkir): Sanitize/check the request, aka turn the dto to an internal type
|
||||||
|
try
|
||||||
|
let fga = ctx.GetService<OpenFgaClient> ()
|
||||||
|
let writes: Model.ClientWriteRequest =
|
||||||
|
req.Permissions
|
||||||
|
|> Array.choose (fun permission ->
|
||||||
|
if permission.Enabled then
|
||||||
|
Some {
|
||||||
|
Remoting.Tuple.empty with
|
||||||
|
User = req.User
|
||||||
|
Relation = permission.Name
|
||||||
|
Object = req.User
|
||||||
|
}
|
||||||
|
else
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|> OpenFGA.Queries.write
|
||||||
|
if writes.Writes.Count > 0 then
|
||||||
|
let! fgaWriteResp = fga.Write writes |> Async.AwaitTask
|
||||||
|
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaWriteResp.ToJson ())
|
||||||
|
|
||||||
|
let deletes =
|
||||||
|
req.Permissions
|
||||||
|
|> Array.choose (fun permission ->
|
||||||
|
if permission.Enabled then
|
||||||
|
None
|
||||||
|
else
|
||||||
|
Remoting.Tuple.delete(req.User, permission.Name, req.User)
|
||||||
|
|> Some
|
||||||
|
)
|
||||||
|
|> OpenFGA.Queries.deleteTuples
|
||||||
|
if deletes.Count > 0 then
|
||||||
|
let! fgaDeleteResp = fga.DeleteTuples deletes |> Async.AwaitTask
|
||||||
|
do logger.LogInformation ("OpenFGA delete responded with: {JSON}", fgaDeleteResp.ToJson ())
|
||||||
|
|
||||||
|
return Ok ()
|
||||||
|
with e ->
|
||||||
|
do logger.LogError (e, "Error setting user permissions")
|
||||||
|
// TODO: Maybe do not send exn message
|
||||||
|
return Error (sprintf "Error setting user permissions: %s" e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateGroupPermissions (ctx: HttpContext) (req: Remoting.AddGroupPermissionsRequest) =
|
||||||
|
async {
|
||||||
|
let user = ctx.User.Identity.Name
|
||||||
|
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||||
|
do logger.LogInformation ("updateGroupPermissions from {User}: {@Req}", user, req)
|
||||||
|
try
|
||||||
|
let fga = ctx.GetService<OpenFgaClient> ()
|
||||||
|
|
||||||
|
let deletes =
|
||||||
|
req.Permissions
|
||||||
|
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|
||||||
|
|> OpenFGA.Queries.deleteTuples'
|
||||||
|
let! deleteResp = fga.DeleteTuples deletes |> Async.AwaitTask
|
||||||
|
do logger.LogInformation ("OpenFGA delete responded with: {JSON}", deleteResp.ToJson ())
|
||||||
|
|
||||||
|
let writes =
|
||||||
|
req.Permissions
|
||||||
|
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|
||||||
|
|> ResizeArray
|
||||||
|
let! writeResp = fga.WriteTuples writes |> Async.AwaitTask
|
||||||
|
do logger.LogInformation ("OpenFGA write responded with: {JSON}", writeResp.ToJson ())
|
||||||
|
|
||||||
|
return Ok ()
|
||||||
|
with e ->
|
||||||
|
do logger.LogError (e, "Error updating group permissions")
|
||||||
|
// TODO: Maybe do not send exn message
|
||||||
|
return Error (sprintf "Error updating group permissions: %s" e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
let private impl (ctx: HttpContext) : Remoting.Api.Admin = {
|
let private impl (ctx: HttpContext) : Remoting.Api.Admin = {
|
||||||
addUsers = Handler.addUsers ctx
|
addArchive = Handler.addArchive ctx
|
||||||
addArchiveGroups = Handler.addArchiveGroups ctx
|
addArchiveGroups = Handler.addArchiveGroups ctx
|
||||||
|
addGroupPermissions = Handler.addGroupPermissions ctx
|
||||||
|
addUsers = Handler.addUsers ctx
|
||||||
deleteArchive = Handler.deleteArchive ctx
|
deleteArchive = Handler.deleteArchive ctx
|
||||||
getAllGroups = Handler.getAllGroups ctx
|
getAllGroups = Handler.getAllGroups ctx
|
||||||
getArchive = Handler.getArchive ctx
|
getArchive = Handler.getArchive ctx
|
||||||
getArchiveCount = Handler.getArchiveCount ctx
|
getArchiveCount = Handler.getArchiveCount ctx
|
||||||
|
getArchiveDataSet = Handler.getArchiveDataSet ctx
|
||||||
getArchiveRefs = Handler.getArchiveRefs ctx
|
getArchiveRefs = Handler.getArchiveRefs ctx
|
||||||
getArchiveTypes = fun () -> Handler.getArchiveTypes ctx
|
getArchiveTypes = fun () -> Handler.getArchiveTypes ctx
|
||||||
getArchives = Handler.getArchives ctx
|
getArchives = Handler.getArchives ctx
|
||||||
|
getDataSets = fun () -> Handler.getAllDataSets ctx
|
||||||
getGroupUsers = Handler.getGroupUsers ctx
|
getGroupUsers = Handler.getGroupUsers ctx
|
||||||
removeUsers = Handler.removeUsers ctx
|
removeUsers = Handler.removeUsers ctx
|
||||||
|
setUserPermissions = Handler.setUserPermissions ctx
|
||||||
|
updateArchive = Handler.updateArchive ctx
|
||||||
|
updateGroupPermissions = Handler.updateGroupPermissions ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
let endpoints: HttpHandler =
|
let endpoints: HttpHandler =
|
||||||
@@ -325,4 +630,4 @@ module Admin =
|
|||||||
|> Remoting.withErrorHandler Utils.rpcErrorHandler
|
|> Remoting.withErrorHandler Utils.rpcErrorHandler
|
||||||
|> Remoting.fromContext impl
|
|> Remoting.fromContext impl
|
||||||
|> Remoting.withRouteBuilder Remoting.routeBuilder
|
|> Remoting.withRouteBuilder Remoting.routeBuilder
|
||||||
|> Remoting.buildHttpHandler
|
|> Remoting.buildHttpHandler
|
||||||
@@ -5,6 +5,7 @@ module Archmaester =
|
|||||||
|
|
||||||
open Archmaester
|
open Archmaester
|
||||||
|
|
||||||
|
|
||||||
let getDataSource (connStr: string) : NpgsqlDataSource =
|
let getDataSource (connStr: string) : NpgsqlDataSource =
|
||||||
let dataSourceBuilder = NpgsqlDataSourceBuilder connStr
|
let dataSourceBuilder = NpgsqlDataSourceBuilder connStr
|
||||||
let _mapper = dataSourceBuilder.UseNetTopologySuite ()
|
let _mapper = dataSourceBuilder.UseNetTopologySuite ()
|
||||||
@@ -18,6 +19,16 @@ module Archmaester =
|
|||||||
|
|
||||||
open Oceanbox.DataAgent.Dapper
|
open Oceanbox.DataAgent.Dapper
|
||||||
|
|
||||||
|
// TODO: Is there another, better way to do this?
|
||||||
|
type DataSetTable = {
|
||||||
|
a_id: int
|
||||||
|
base_path: string
|
||||||
|
m_id: System.Guid
|
||||||
|
m_name: string
|
||||||
|
start_time: System.DateTime
|
||||||
|
end_time: System.DateTime
|
||||||
|
}
|
||||||
|
|
||||||
let private canonicalizeGroupNames (groups: string array option) : string array =
|
let private canonicalizeGroupNames (groups: string array option) : string array =
|
||||||
groups
|
groups
|
||||||
|> Option.defaultValue [||]
|
|> Option.defaultValue [||]
|
||||||
@@ -138,6 +149,116 @@ module Archmaester =
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Currently called attribs, but soon to be renamed
|
||||||
|
let queryDataSets (db: NpgsqlDataSource) =
|
||||||
|
async {
|
||||||
|
use conn = db.OpenConnection ()
|
||||||
|
let query =
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
a.id AS a_id,
|
||||||
|
a.base_path,
|
||||||
|
m.id AS m_id,
|
||||||
|
m.name AS m_name,
|
||||||
|
MIN(f.start_time) AS start_time,
|
||||||
|
MAX(f.start_time) AS end_time
|
||||||
|
FROM
|
||||||
|
attribs AS a
|
||||||
|
JOIN
|
||||||
|
model_areas AS m
|
||||||
|
ON m.id = a.model_area_id
|
||||||
|
LEFT JOIN
|
||||||
|
files AS f
|
||||||
|
ON f.attribs_id = a.id
|
||||||
|
WHERE
|
||||||
|
-- Type 1 is FVCOM
|
||||||
|
a.type_id = 1
|
||||||
|
AND NOT a.retired
|
||||||
|
GROUP BY
|
||||||
|
a.id,
|
||||||
|
a.base_path,
|
||||||
|
m.id,
|
||||||
|
m.name
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
|
||||||
|
let! res =
|
||||||
|
conn.QueryAsync<DataSetTable>(query)
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
return
|
||||||
|
res
|
||||||
|
|> Array.ofSeq
|
||||||
|
|> Array.map (fun x ->
|
||||||
|
{
|
||||||
|
Id = x.a_id
|
||||||
|
BasePath = x.base_path
|
||||||
|
ModelAreaId = x.m_id
|
||||||
|
ModelAreaName = x.m_name
|
||||||
|
StartTime = x.start_time
|
||||||
|
EndTime = x.end_time.AddHours(24)
|
||||||
|
} : Remoting.DataSet
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let queryArchiveDataSet (db: NpgsqlDataSource) (archiveId: System.Guid) =
|
||||||
|
async {
|
||||||
|
use conn = db.OpenConnection ()
|
||||||
|
let query =
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
a.id AS a_id,
|
||||||
|
a.base_path,
|
||||||
|
m.id AS m_id,
|
||||||
|
m.name AS m_name,
|
||||||
|
MIN(f.start_time) AS start_time,
|
||||||
|
MAX(f.start_time) AS end_time
|
||||||
|
FROM
|
||||||
|
attribs AS a
|
||||||
|
JOIN
|
||||||
|
model_areas AS m
|
||||||
|
ON m.id = a.model_area_id
|
||||||
|
LEFT JOIN
|
||||||
|
files AS f
|
||||||
|
ON f.attribs_id = a.id
|
||||||
|
LEFT JOIN
|
||||||
|
archives as ar
|
||||||
|
ON ar.attribs_id = a.id
|
||||||
|
WHERE
|
||||||
|
ar.id = @archive_id
|
||||||
|
GROUP BY
|
||||||
|
a.id,
|
||||||
|
a.base_path,
|
||||||
|
m.id,
|
||||||
|
m.name
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
|
||||||
|
let param =
|
||||||
|
dict [
|
||||||
|
"archive_id", box archiveId
|
||||||
|
]
|
||||||
|
|
||||||
|
let! res =
|
||||||
|
conn.QueryAsync<DataSetTable>(query, param)
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
return
|
||||||
|
res
|
||||||
|
|> Array.ofSeq
|
||||||
|
|> Array.map (fun x ->
|
||||||
|
{
|
||||||
|
Id = x.a_id
|
||||||
|
BasePath = x.base_path
|
||||||
|
ModelAreaId = x.m_id
|
||||||
|
ModelAreaName = x.m_name
|
||||||
|
StartTime = x.start_time
|
||||||
|
EndTime = x.end_time.AddHours(24)
|
||||||
|
} : Remoting.DataSet
|
||||||
|
) // TODO: is there a better way to select single rows?
|
||||||
|
|> Array.head
|
||||||
|
}
|
||||||
|
|
||||||
module EFCore =
|
module EFCore =
|
||||||
open System.Linq
|
open System.Linq
|
||||||
|
|
||||||
@@ -180,6 +301,114 @@ module Archmaester =
|
|||||||
return created
|
return created
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let checkArchiveNameTaken (db: Entity.ArchiveContext) (name: string) : Async<bool> =
|
||||||
|
db.Archives
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(fun archive -> archive.Name = name)
|
||||||
|
.AnyAsync()
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
let getArchiveName (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<string> =
|
||||||
|
db.Archives
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(fun archive -> archive.ArchiveId = archiveId)
|
||||||
|
.Select(_.Name)
|
||||||
|
.SingleOrDefaultAsync()
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
/// Align archive start time with the actual files in the dataset
|
||||||
|
let private getAlignedStartTime (db: Entity.ArchiveContext) (dataSetId: int) (archiveStartTime: System.DateTime) =
|
||||||
|
let utcStartTime = archiveStartTime.ToUniversalTime()
|
||||||
|
db.Files
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(fun file ->
|
||||||
|
file.AttribsId = dataSetId
|
||||||
|
&& file.StartTime >= utcStartTime
|
||||||
|
)
|
||||||
|
.OrderBy(_.StartTime)
|
||||||
|
.Select(_.StartTime)
|
||||||
|
.FirstAsync()
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
let addArchive (db: Entity.ArchiveContext) (archive: Remoting.AddArchiveRequest) : Async<Entity.Archive> =
|
||||||
|
async {
|
||||||
|
let! alignedStartTime = getAlignedStartTime db archive.DataSetId archive.StartTime
|
||||||
|
|
||||||
|
let newArchive =
|
||||||
|
Entity.Archive(
|
||||||
|
Name = archive.Name,
|
||||||
|
Frames = archive.Frames,
|
||||||
|
StartTime = alignedStartTime,
|
||||||
|
Published = archive.Published,
|
||||||
|
Public = archive.Public,
|
||||||
|
AttribsId = archive.DataSetId
|
||||||
|
)
|
||||||
|
|
||||||
|
db.Add newArchive |> ignore
|
||||||
|
|
||||||
|
let! obxGroupId =
|
||||||
|
db.Groups
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(fun group -> group.Name = "/oceanbox")
|
||||||
|
.Select(_.GroupId)
|
||||||
|
.SingleAsync()
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
let defaultGroup = Entity.ArchiveGroup(ArchiveId = newArchive.ArchiveId, GroupId = obxGroupId)
|
||||||
|
db.Add defaultGroup |> ignore
|
||||||
|
|
||||||
|
do! db.SaveChangesAsync () |> Async.AwaitTask |> Async.Ignore
|
||||||
|
|
||||||
|
return newArchive
|
||||||
|
}
|
||||||
|
|
||||||
|
let editArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) (archive: Remoting.EditArchiveRequest) : Async<Entity.Archive> =
|
||||||
|
async {
|
||||||
|
let! existingArchive =
|
||||||
|
db.Archives
|
||||||
|
.Where(fun archive -> archive.ArchiveId = archiveId)
|
||||||
|
.SingleAsync()
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
let! alignedStartTime = getAlignedStartTime db existingArchive.AttribsId archive.StartTime
|
||||||
|
|
||||||
|
existingArchive.Name <- archive.Name
|
||||||
|
existingArchive.StartTime <- alignedStartTime
|
||||||
|
existingArchive.Frames <- archive.Frames
|
||||||
|
existingArchive.Published <- archive.Published
|
||||||
|
existingArchive.Public <- archive.Public
|
||||||
|
|
||||||
|
do db.Update existingArchive |> ignore
|
||||||
|
do! db.SaveChangesAsync () |> Async.AwaitTask |> Async.Ignore
|
||||||
|
|
||||||
|
return existingArchive
|
||||||
|
}
|
||||||
|
|
||||||
|
let setArchivePublic (db: Entity.ArchiveContext) (archiveId: System.Guid) (setPublic: bool) : Async<int> =
|
||||||
|
async {
|
||||||
|
let! starUserId =
|
||||||
|
db.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(fun user -> user.Name = "*")
|
||||||
|
.Select(_.UserId)
|
||||||
|
.SingleAsync()
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
if setPublic then
|
||||||
|
let starArchiveUser = Entity.ArchiveUser(UserId = starUserId, ArchiveId = archiveId)
|
||||||
|
db.Add starArchiveUser |> ignore
|
||||||
|
else
|
||||||
|
let! starArchiveUser =
|
||||||
|
db.ArchiveUsers
|
||||||
|
.Where(fun au -> au.UserId = starUserId && au.ArchiveId = archiveId)
|
||||||
|
.SingleOrDefaultAsync()
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|
||||||
|
starArchiveUser |> Option.ofObj |> Option.iter (db.Remove >> ignore)
|
||||||
|
|
||||||
|
return! db.SaveChangesAsync () |> Async.AwaitTask
|
||||||
|
}
|
||||||
|
|
||||||
let deleteArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<int> =
|
let deleteArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<int> =
|
||||||
async {
|
async {
|
||||||
let! entity =
|
let! entity =
|
||||||
@@ -420,4 +649,4 @@ module Archmaester =
|
|||||||
|> Async.AwaitTask
|
|> Async.AwaitTask
|
||||||
|
|
||||||
return entities
|
return entities
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<PackageId>Codex.Server</PackageId>
|
<PackageId>Codex.Server</PackageId>
|
||||||
<RootNamespace>Oceanbox</RootNamespace>
|
<RootNamespace>Oceanbox</RootNamespace>
|
||||||
<Version>0.0.0-alpha.1</Version>
|
<Version>0.0.1</Version>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ module OpenFGA =
|
|||||||
|> Seq.toArray
|
|> Seq.toArray
|
||||||
|> Array.map (fun t ->
|
|> Array.map (fun t ->
|
||||||
let condition : Remoting.Condition option =
|
let condition : Remoting.Condition option =
|
||||||
Option.ofObj t.Key.Condition
|
t.Key.Condition
|
||||||
|
|> Option.ofObj
|
||||||
|> Option.map (fun cond -> {
|
|> Option.map (fun cond -> {
|
||||||
Name = cond.Name
|
Name = cond.Name
|
||||||
Context = JsonSerializer.Serialize cond.Context
|
Context = JsonSerializer.Serialize cond.Context
|
||||||
@@ -148,6 +149,15 @@ module OpenFGA =
|
|||||||
|
|
||||||
result
|
result
|
||||||
|
|
||||||
|
let read' (tuple: ClientTupleKey) : ClientReadRequest =
|
||||||
|
let result = ClientReadRequest ()
|
||||||
|
|
||||||
|
do result.User <- tuple.User
|
||||||
|
do result.Relation <- tuple.Relation
|
||||||
|
do result.Object <- tuple.Object
|
||||||
|
|
||||||
|
result
|
||||||
|
|
||||||
let delete (tuples: Remoting.Tuple array) =
|
let delete (tuples: Remoting.Tuple array) =
|
||||||
let result = ClientWriteRequest ()
|
let result = ClientWriteRequest ()
|
||||||
|
|
||||||
@@ -167,6 +177,57 @@ module OpenFGA =
|
|||||||
|
|
||||||
result
|
result
|
||||||
|
|
||||||
|
/// To be used with OpenFga.Sdk.Client.OpenFgaClient.DeleteTuples
|
||||||
|
let deleteTuples (tuples: Remoting.Tuple array) : ResizeArray<ClientTupleKeyWithoutCondition> =
|
||||||
|
let deletes: ClientTupleKeyWithoutCondition array =
|
||||||
|
tuples
|
||||||
|
|> Array.map (fun tuple ->
|
||||||
|
let result = ClientTupleKeyWithoutCondition ()
|
||||||
|
|
||||||
|
do result.Object <- tuple.Object
|
||||||
|
do result.Relation <- tuple.Relation
|
||||||
|
do result.User <- tuple.User
|
||||||
|
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
ResizeArray deletes
|
||||||
|
|
||||||
|
let delete' (tuples: ClientTupleKey array) : ClientWriteRequest =
|
||||||
|
let result = ClientWriteRequest ()
|
||||||
|
|
||||||
|
let deletes: ClientTupleKeyWithoutCondition array =
|
||||||
|
tuples
|
||||||
|
|> Array.map (fun tuple ->
|
||||||
|
let result = ClientTupleKeyWithoutCondition ()
|
||||||
|
|
||||||
|
do result.Object <- tuple.Object
|
||||||
|
do result.Relation <- tuple.Relation
|
||||||
|
do result.User <- tuple.User
|
||||||
|
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
do result.Deletes <- ResizeArray deletes
|
||||||
|
|
||||||
|
result
|
||||||
|
|
||||||
|
/// To be used with OpenFga.Sdk.Client.OpenFgaClient.DeleteTuples
|
||||||
|
let deleteTuples' (tuples: ClientTupleKey array) : ResizeArray<ClientTupleKeyWithoutCondition> =
|
||||||
|
let deletes: ClientTupleKeyWithoutCondition array =
|
||||||
|
tuples
|
||||||
|
|> Array.map (fun tuple ->
|
||||||
|
let result = ClientTupleKeyWithoutCondition ()
|
||||||
|
|
||||||
|
do result.Object <- tuple.Object
|
||||||
|
do result.Relation <- tuple.Relation
|
||||||
|
do result.User <- tuple.User
|
||||||
|
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
ResizeArray deletes
|
||||||
|
|
||||||
let write (tuples: Remoting.Tuple array) =
|
let write (tuples: Remoting.Tuple array) =
|
||||||
let result = ClientWriteRequest ()
|
let result = ClientWriteRequest ()
|
||||||
|
|
||||||
@@ -236,6 +297,28 @@ module OpenFGA =
|
|||||||
|])
|
|])
|
||||||
|> Queries.write'
|
|> Queries.write'
|
||||||
|
|
||||||
|
module Archive =
|
||||||
|
/// Creates a default principal tuple for the oceanbox group
|
||||||
|
let defaultPrincipal (id: Guid) : ClientTupleKey =
|
||||||
|
let tuple = ClientTupleKey ()
|
||||||
|
|
||||||
|
do tuple.Object <- sprintf "archive:%s" (string id)
|
||||||
|
do tuple.Relation <- "principal"
|
||||||
|
do tuple.User <- "group:/oceanbox"
|
||||||
|
|
||||||
|
tuple
|
||||||
|
|
||||||
|
/// Gives view permission to all users. This tuple should be added to public archives
|
||||||
|
let publicArchive (id: Guid) : ClientTupleKey =
|
||||||
|
let tuple = ClientTupleKey ()
|
||||||
|
|
||||||
|
do tuple.Object <- sprintf "archive:%s" (string id)
|
||||||
|
do tuple.Relation <- "view"
|
||||||
|
do tuple.User <- "user:*"
|
||||||
|
|
||||||
|
tuple
|
||||||
|
|
||||||
|
|
||||||
module private Handlers =
|
module private Handlers =
|
||||||
let check (ctx: HttpContext) (req: Remoting.CheckRequest) =
|
let check (ctx: HttpContext) (req: Remoting.CheckRequest) =
|
||||||
async {
|
async {
|
||||||
@@ -260,9 +343,10 @@ module OpenFGA =
|
|||||||
let logger = ctx.GetLogger<Remoting.Api.OpenFGA> ()
|
let logger = ctx.GetLogger<Remoting.Api.OpenFGA> ()
|
||||||
let fga = ctx.GetService<OpenFgaClient> ()
|
let fga = ctx.GetService<OpenFgaClient> ()
|
||||||
try
|
try
|
||||||
let deleteRequest = Queries.delete [| tuple |]
|
let deleteRequest = Queries.deleteTuples [| tuple |]
|
||||||
do logger.LogInformation ("Delete req: {Request}", deleteRequest.ToJson ())
|
let json = JsonSerializer.Serialize deleteRequest
|
||||||
let! resp = fga.Write deleteRequest |> Async.AwaitTask
|
do logger.LogInformation ("Delete req: {Request}", json)
|
||||||
|
let! resp = fga.DeleteTuples deleteRequest |> Async.AwaitTask
|
||||||
do logger.LogInformation ("Delete resp: {Response}", resp.ToJson ())
|
do logger.LogInformation ("Delete resp: {Response}", resp.ToJson ())
|
||||||
return Ok (resp.Deletes.Count >= 1)
|
return Ok (resp.Deletes.Count >= 1)
|
||||||
with e ->
|
with e ->
|
||||||
@@ -329,4 +413,4 @@ module OpenFGA =
|
|||||||
Remoting.createApi ()
|
Remoting.createApi ()
|
||||||
|> Remoting.withRouteBuilder Remoting.routeBuilder
|
|> Remoting.withRouteBuilder Remoting.routeBuilder
|
||||||
|> Remoting.fromContext impl
|
|> Remoting.fromContext impl
|
||||||
|> Remoting.buildHttpHandler
|
|> Remoting.buildHttpHandler
|
||||||
@@ -155,7 +155,11 @@ let webApp: HttpHandler =
|
|||||||
>=> choose [
|
>=> choose [
|
||||||
Auth.endpoints
|
Auth.endpoints
|
||||||
|
|
||||||
authorize >=> choose [ Acl.endpoints; Admin.endpoints; OpenFGA.endpoints ]
|
authorize >=> choose [
|
||||||
|
Acl.endpoints
|
||||||
|
Admin.endpoints
|
||||||
|
OpenFGA.endpoints
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -203,8 +207,16 @@ let configureApp (app: IApplicationBuilder) =
|
|||||||
.UseStaticFiles()
|
.UseStaticFiles()
|
||||||
.UseGiraffe webApp
|
.UseGiraffe webApp
|
||||||
|
|
||||||
|
let private getIp (uri: Uri) =
|
||||||
|
uri.DnsSafeHost
|
||||||
|
|> Net.Dns.GetHostAddresses
|
||||||
|
|> Array.head
|
||||||
|
|
||||||
let configureServices (settings: Settings) (services: IServiceCollection) =
|
let configureServices (settings: Settings) (services: IServiceCollection) =
|
||||||
let authSettings = settings.Auth
|
let authSettings = settings.Auth
|
||||||
|
let uri = Uri settings.Fga.apiUrl
|
||||||
|
let ip = getIp uri
|
||||||
|
eprintfn "OpenFGA uri is %s (%s)" uri.DnsSafeHost (string ip)
|
||||||
let fga: OpenFgaClient = Fga.newFgaClient settings.Fga
|
let fga: OpenFgaClient = Fga.newFgaClient settings.Fga
|
||||||
let archmaesterDatasource = Archmaester.getDataSource settings.DbConnectionString
|
let archmaesterDatasource = Archmaester.getDataSource settings.DbConnectionString
|
||||||
do Oceanbox.DataAgent.Dapper.register ()
|
do Oceanbox.DataAgent.Dapper.register ()
|
||||||
@@ -293,4 +305,4 @@ let main args =
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -14,7 +14,7 @@ let
|
|||||||
in
|
in
|
||||||
buildDotnetModule {
|
buildDotnetModule {
|
||||||
pname = name;
|
pname = name;
|
||||||
version = "0.0.0-alpha.1";
|
version = "0.0.1";
|
||||||
|
|
||||||
inherit dotnet-sdk dotnet-runtime;
|
inherit dotnet-sdk dotnet-runtime;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"Azure.Security.KeyVault.Secrets": {
|
"Azure.Security.KeyVault.Secrets": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[4.7.0, )",
|
"requested": "[4.7.0, )",
|
||||||
"resolved": "4.7.0",
|
"resolved": "4.7.0",
|
||||||
"contentHash": "uOPCojkm41V4dKTORyGzl3/f/lriKpxSQ43fWDn4StRJBVmbF1F/DNWJhwm207kCnqgE/W9+tskJSimIKHCZkw==",
|
"contentHash": "uOPCojkm41V4dKTORyGzl3/f/lriKpxSQ43fWDn4StRJBVmbF1F/DNWJhwm207kCnqgE/W9+tskJSimIKHCZkw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Azure.Core": "1.44.1",
|
"Azure.Core": "1.44.1"
|
||||||
"System.Memory": "4.5.5",
|
|
||||||
"System.Text.Json": "6.0.10",
|
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Fable.Remoting.Giraffe": {
|
"Fable.Remoting.Giraffe": {
|
||||||
@@ -53,8 +50,7 @@
|
|||||||
"FSharp.Core": "6.0.0",
|
"FSharp.Core": "6.0.0",
|
||||||
"FSharp.SystemTextJson": "1.3.13",
|
"FSharp.SystemTextJson": "1.3.13",
|
||||||
"Giraffe.ViewEngine": "1.4.0",
|
"Giraffe.ViewEngine": "1.4.0",
|
||||||
"Microsoft.IO.RecyclableMemoryStream": "3.0.1",
|
"Microsoft.IO.RecyclableMemoryStream": "3.0.1"
|
||||||
"System.Text.Json": "8.0.5"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Azure.Core": {
|
"Azure.Core": {
|
||||||
@@ -64,12 +60,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
|
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
|
||||||
"System.ClientModel": "1.1.0",
|
"System.ClientModel": "1.1.0",
|
||||||
"System.Diagnostics.DiagnosticSource": "6.0.1",
|
"System.Memory.Data": "6.0.0"
|
||||||
"System.Memory.Data": "6.0.0",
|
|
||||||
"System.Numerics.Vectors": "4.5.0",
|
|
||||||
"System.Text.Encodings.Web": "6.0.0",
|
|
||||||
"System.Text.Json": "6.0.10",
|
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Azure.Security.KeyVault.Keys": {
|
"Azure.Security.KeyVault.Keys": {
|
||||||
@@ -77,10 +68,7 @@
|
|||||||
"resolved": "4.6.0",
|
"resolved": "4.6.0",
|
||||||
"contentHash": "1KbCIkXmLaj+kDDNm1Va5rNlzgcJ/fVtnsoVmzZPKa38jz6DXhPyojdvGaOX8AdupGJceg0X1vrsGvZKN79Qzw==",
|
"contentHash": "1KbCIkXmLaj+kDDNm1Va5rNlzgcJ/fVtnsoVmzZPKa38jz6DXhPyojdvGaOX8AdupGJceg0X1vrsGvZKN79Qzw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Azure.Core": "1.37.0",
|
"Azure.Core": "1.37.0"
|
||||||
"System.Memory": "4.5.4",
|
|
||||||
"System.Text.Json": "4.7.2",
|
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Azure.Storage.Blobs": {
|
"Azure.Storage.Blobs": {
|
||||||
@@ -88,8 +76,7 @@
|
|||||||
"resolved": "12.23.0",
|
"resolved": "12.23.0",
|
||||||
"contentHash": "wokJ5KX/iViQQ32xyCu69+Ter0aR4B9QQ+oR9NCpc/WPIanxnDErrmFfdmE7K8ZdccjHkvE/wEnqJxaF1+5wFg==",
|
"contentHash": "wokJ5KX/iViQQ32xyCu69+Ter0aR4B9QQ+oR9NCpc/WPIanxnDErrmFfdmE7K8ZdccjHkvE/wEnqJxaF1+5wFg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Azure.Storage.Common": "12.22.0",
|
"Azure.Storage.Common": "12.22.0"
|
||||||
"System.Text.Json": "6.0.10"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Azure.Storage.Common": {
|
"Azure.Storage.Common": {
|
||||||
@@ -233,8 +220,7 @@
|
|||||||
"resolved": "5.3.2",
|
"resolved": "5.3.2",
|
||||||
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"FSharp.Core": "4.3.2",
|
"FSharp.Core": "4.3.2"
|
||||||
"System.Reflection.Emit.Lightweight": "4.3.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Giraffe.ViewEngine": {
|
"Giraffe.ViewEngine": {
|
||||||
@@ -527,8 +513,7 @@
|
|||||||
"resolved": "4.67.2",
|
"resolved": "4.67.2",
|
||||||
"contentHash": "37t0TfekfG6XM8kue/xNaA66Qjtti5Qe1xA41CK+bEd8VD76/oXJc+meFJHGzygIC485dCpKoamG/pDfb9Qd7Q==",
|
"contentHash": "37t0TfekfG6XM8kue/xNaA66Qjtti5Qe1xA41CK+bEd8VD76/oXJc+meFJHGzygIC485dCpKoamG/pDfb9Qd7Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.IdentityModel.Abstractions": "6.35.0",
|
"Microsoft.IdentityModel.Abstractions": "6.35.0"
|
||||||
"System.Diagnostics.DiagnosticSource": "6.0.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Identity.Client.Extensions.Msal": {
|
"Microsoft.Identity.Client.Extensions.Msal": {
|
||||||
@@ -617,10 +602,7 @@
|
|||||||
"OpenTelemetry.Api": {
|
"OpenTelemetry.Api": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "1.10.0",
|
"resolved": "1.10.0",
|
||||||
"contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w==",
|
"contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w=="
|
||||||
"dependencies": {
|
|
||||||
"System.Diagnostics.DiagnosticSource": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"OpenTelemetry.Api.ProviderBuilderExtensions": {
|
"OpenTelemetry.Api.ProviderBuilderExtensions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -701,26 +683,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Options": "9.0.0",
|
"Microsoft.Extensions.Options": "9.0.0",
|
||||||
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.10.0, 2.0.0)",
|
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.10.0, 2.0.0)",
|
||||||
"StackExchange.Redis": "[2.6.122, 3.0.0)",
|
"StackExchange.Redis": "[2.6.122, 3.0.0)"
|
||||||
"System.Reflection.Emit.Lightweight": "4.7.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Pipelines.Sockets.Unofficial": {
|
"Pipelines.Sockets.Unofficial": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.2.8",
|
"resolved": "2.2.8",
|
||||||
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==",
|
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
|
||||||
"dependencies": {
|
|
||||||
"System.IO.Pipelines": "5.0.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"ProjNET": {
|
"ProjNET": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.0.0",
|
"resolved": "2.0.0",
|
||||||
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
|
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
|
||||||
"dependencies": {
|
|
||||||
"System.Memory": "4.5.3",
|
|
||||||
"System.Numerics.Vectors": "4.5.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Sentry": {
|
"Sentry": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -769,8 +743,7 @@
|
|||||||
"resolved": "1.1.0",
|
"resolved": "1.1.0",
|
||||||
"contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==",
|
"contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Memory.Data": "1.0.2",
|
"System.Memory.Data": "1.0.2"
|
||||||
"System.Text.Json": "6.0.9"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.ComponentModel.Annotations": {
|
"System.ComponentModel.Annotations": {
|
||||||
@@ -778,11 +751,6 @@
|
|||||||
"resolved": "5.0.0",
|
"resolved": "5.0.0",
|
||||||
"contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg=="
|
"contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg=="
|
||||||
},
|
},
|
||||||
"System.Diagnostics.DiagnosticSource": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.9",
|
|
||||||
"contentHash": "8hy61dsFYYSDjT9iTAfygGMU3A0EAnG69x5FUXeKsCjMhBmtTBt4UMUEW3ipprFoorOW6Jw/7hDMjXtlrsOvVQ=="
|
|
||||||
},
|
|
||||||
"System.IdentityModel.Tokens.Jwt": {
|
"System.IdentityModel.Tokens.Jwt": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.1",
|
"resolved": "8.0.1",
|
||||||
@@ -797,38 +765,10 @@
|
|||||||
"resolved": "6.0.0",
|
"resolved": "6.0.0",
|
||||||
"contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g=="
|
"contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g=="
|
||||||
},
|
},
|
||||||
"System.IO.Pipelines": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "5.0.1",
|
|
||||||
"contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg=="
|
|
||||||
},
|
|
||||||
"System.Memory": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.5",
|
|
||||||
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw=="
|
|
||||||
},
|
|
||||||
"System.Memory.Data": {
|
"System.Memory.Data": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "6.0.0",
|
"resolved": "6.0.0",
|
||||||
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ==",
|
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ=="
|
||||||
"dependencies": {
|
|
||||||
"System.Text.Json": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Numerics.Vectors": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.0",
|
|
||||||
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
|
|
||||||
},
|
|
||||||
"System.Reflection.Emit.Lightweight": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.7.0",
|
|
||||||
"contentHash": "a4OLB4IITxAXJeV74MDx49Oq2+PsF6Sml54XAFv+2RyWwtDBcabzoxiiJRhdhx+gaohLh4hEGCLQyBozXoQPqA=="
|
|
||||||
},
|
|
||||||
"System.Runtime.CompilerServices.Unsafe": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "6.0.0",
|
|
||||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
|
||||||
},
|
},
|
||||||
"System.Security.Cryptography.Pkcs": {
|
"System.Security.Cryptography.Pkcs": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -848,16 +788,6 @@
|
|||||||
"System.Security.Cryptography.Pkcs": "9.0.2"
|
"System.Security.Cryptography.Pkcs": "9.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.Text.Json": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.5",
|
|
||||||
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg=="
|
|
||||||
},
|
|
||||||
"System.Threading.Tasks.Extensions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.4",
|
|
||||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
|
|
||||||
},
|
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -953,9 +883,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Azure.Core": "1.44.1",
|
"Azure.Core": "1.44.1",
|
||||||
"Microsoft.Identity.Client": "4.67.2",
|
"Microsoft.Identity.Client": "4.67.2",
|
||||||
"Microsoft.Identity.Client.Extensions.Msal": "4.67.2",
|
"Microsoft.Identity.Client.Extensions.Msal": "4.67.2"
|
||||||
"System.Memory": "4.5.5",
|
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Dapper.FSharp": {
|
"Dapper.FSharp": {
|
||||||
@@ -1121,8 +1049,7 @@
|
|||||||
"resolved": "1.3.13",
|
"resolved": "1.3.13",
|
||||||
"contentHash": "znp8odpdkVGKVX0AvbhiXdmeMi0KJ+A4AyAQWSkfAEAe4Z4clRE+rVhrLnAGrFD1VEIUX2lsQ4o84ywpWZUSGw==",
|
"contentHash": "znp8odpdkVGKVX0AvbhiXdmeMi0KJ+A4AyAQWSkfAEAe4Z4clRE+rVhrLnAGrFD1VEIUX2lsQ4o84ywpWZUSGw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"FSharp.Core": "4.7.0",
|
"FSharp.Core": "4.7.0"
|
||||||
"System.Text.Json": "6.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"FSharpPlus": {
|
"FSharpPlus": {
|
||||||
@@ -1211,10 +1138,7 @@
|
|||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.5.0, )",
|
"requested": "[2.5.0, )",
|
||||||
"resolved": "2.5.0",
|
"resolved": "2.5.0",
|
||||||
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
|
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
|
||||||
"dependencies": {
|
|
||||||
"System.Memory": "4.5.4"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Newtonsoft.Json": {
|
"Newtonsoft.Json": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
@@ -1300,8 +1224,7 @@
|
|||||||
"contentHash": "vlOKvmigJ3Sumoulp1HwCTFXgX4KuERVGIIw4ZqmhgUJnSiApDmY183ddzzHo2FIdIJ8vGwrMGx98v9cLAezFA==",
|
"contentHash": "vlOKvmigJ3Sumoulp1HwCTFXgX4KuERVGIIw4ZqmhgUJnSiApDmY183ddzzHo2FIdIJ8vGwrMGx98v9cLAezFA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Http": "9.0.9",
|
"Microsoft.Extensions.Http": "9.0.9",
|
||||||
"System.ComponentModel.Annotations": "5.0.0",
|
"System.ComponentModel.Annotations": "5.0.0"
|
||||||
"System.Diagnostics.DiagnosticSource": "9.0.9"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ProjNet.FSharp": {
|
"ProjNet.FSharp": {
|
||||||
@@ -1372,15 +1295,6 @@
|
|||||||
"Serilog": "4.0.0"
|
"Serilog": "4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.Text.Encodings.Web": {
|
|
||||||
"type": "CentralTransitive",
|
|
||||||
"requested": "[9.0.2, )",
|
|
||||||
"resolved": "6.0.0",
|
|
||||||
"contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Thoth.Json.Net": {
|
"Thoth.Json.Net": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[12.0.0, )",
|
"requested": "[12.0.0, )",
|
||||||
|
|||||||
@@ -122,6 +122,16 @@ module Remoting =
|
|||||||
EndTime = System.DateTime.Now
|
EndTime = System.DateTime.Now
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
type ArchiveRelation =
|
||||||
|
| ViewTerm of ViewTerm
|
||||||
|
| ExecTicket of ExecTicket
|
||||||
|
with
|
||||||
|
static member ConditionName (permission: ArchiveRelation) =
|
||||||
|
match permission with
|
||||||
|
| ViewTerm _ -> "term"
|
||||||
|
| ExecTicket _ -> "ticket"
|
||||||
|
|
||||||
[<Struct>]
|
[<Struct>]
|
||||||
type AddArchiveGroupsRequest = {
|
type AddArchiveGroupsRequest = {
|
||||||
Id: Archmaester.Dto.ArchiveId
|
Id: Archmaester.Dto.ArchiveId
|
||||||
@@ -130,28 +140,102 @@ module Remoting =
|
|||||||
Exec: ExecTicket option
|
Exec: ExecTicket option
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[<Struct>]
|
||||||
|
type DataSet = {
|
||||||
|
Id: int
|
||||||
|
BasePath: string
|
||||||
|
ModelAreaId: System.Guid
|
||||||
|
ModelAreaName: string
|
||||||
|
StartTime: System.DateTime
|
||||||
|
EndTime: System.DateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Struct>]
|
||||||
|
type Archive = {
|
||||||
|
Id: System.Guid
|
||||||
|
Name: string
|
||||||
|
DataSetId: int
|
||||||
|
StartTime: System.DateTime
|
||||||
|
Frames: int
|
||||||
|
Published: bool
|
||||||
|
Public: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Struct>]
|
||||||
|
type AddArchiveRequest = {
|
||||||
|
Name: string
|
||||||
|
DataSetId: int
|
||||||
|
StartTime: System.DateTime
|
||||||
|
Frames: int
|
||||||
|
Published: bool
|
||||||
|
Public: bool
|
||||||
|
} with
|
||||||
|
static member empty () = {
|
||||||
|
Name = ""
|
||||||
|
DataSetId = -1
|
||||||
|
StartTime = System.DateTime.Now.AddDays(-14.)
|
||||||
|
Frames = 48
|
||||||
|
Published = true
|
||||||
|
Public = false
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Struct>]
|
||||||
|
type EditArchiveRequest = {
|
||||||
|
Name: string
|
||||||
|
StartTime: System.DateTime
|
||||||
|
Frames: int
|
||||||
|
Published: bool
|
||||||
|
Public: bool
|
||||||
|
}
|
||||||
|
|
||||||
[<Struct>]
|
[<Struct>]
|
||||||
type AddUsersRequest = {
|
type AddUsersRequest = {
|
||||||
Group: string
|
Group: string
|
||||||
Users: string array
|
Users: string array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[<Struct>]
|
||||||
|
type AddGroupPermissionsRequest = {
|
||||||
|
Group: string
|
||||||
|
ArchiveId: System.Guid
|
||||||
|
Permissions: ArchiveRelation array
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Struct>]
|
||||||
|
type Permission = {
|
||||||
|
Name: string
|
||||||
|
Enabled: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Struct>]
|
||||||
|
type UserPermissionRequest = {
|
||||||
|
User: string
|
||||||
|
Permissions: Permission array
|
||||||
|
}
|
||||||
|
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Api =
|
module Api =
|
||||||
type Auth = { IsAuthenticated: Async<bool> }
|
type Auth = { IsAuthenticated: Async<bool> }
|
||||||
|
|
||||||
type Admin = {
|
type Admin = {
|
||||||
addUsers: AddUsersRequest -> Async<Result<unit, string>>
|
addArchive: AddArchiveRequest -> Async<Result<Archive, string>>
|
||||||
addArchiveGroups: AddArchiveGroupsRequest -> Async<Result<unit, string>>
|
addArchiveGroups: AddArchiveGroupsRequest -> Async<Result<unit, string>>
|
||||||
|
addGroupPermissions: AddGroupPermissionsRequest -> Async<Result<unit, string>>
|
||||||
|
addUsers: AddUsersRequest -> Async<Result<unit, string>>
|
||||||
deleteArchive: Archmaester.Dto.ArchiveId -> Async<Result<bool, string>>
|
deleteArchive: Archmaester.Dto.ArchiveId -> Async<Result<bool, string>>
|
||||||
getAllGroups: Async<string array>
|
getAllGroups: Async<string array>
|
||||||
getArchive: Archmaester.Dto.ArchiveId -> Async<Result<Archmaester.Dto.ArchiveProps, string>>
|
getArchive: Archmaester.Dto.ArchiveId -> Async<Result<Archmaester.Dto.ArchiveProps, string>>
|
||||||
getArchiveCount: Archmaester.Dto.ArchiveFilter -> Async<Result<int, string>>
|
getArchiveCount: Archmaester.Dto.ArchiveFilter -> Async<Result<int, string>>
|
||||||
|
getArchiveDataSet: System.Guid -> Async<Result<DataSet, string>>
|
||||||
getArchiveRefs: Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
|
getArchiveRefs: Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
|
||||||
getArchiveTypes: unit -> Async<Archmaester.Dto.ArchiveType array>
|
getArchiveTypes: unit -> Async<Archmaester.Dto.ArchiveType array>
|
||||||
getArchives: int -> int -> Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
|
getArchives: int -> int -> Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
|
||||||
|
getDataSets: unit -> Async<Result<DataSet array, string>>
|
||||||
getGroupUsers: string -> Async<string array>
|
getGroupUsers: string -> Async<string array>
|
||||||
removeUsers: string array -> Async<Result<unit, string>>
|
removeUsers: string array -> Async<Result<unit, string>>
|
||||||
|
setUserPermissions: UserPermissionRequest -> Async<Result<unit, string>>
|
||||||
|
updateArchive: System.Guid -> EditArchiveRequest -> Async<Result<Archive, string>>
|
||||||
|
updateGroupPermissions: AddGroupPermissionsRequest -> Async<Result<unit, string>>
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenFGA = {
|
type OpenFGA = {
|
||||||
@@ -160,4 +244,4 @@ module Remoting =
|
|||||||
ListObjects: ListObjectsRequest -> Async<Result<string array, string>>
|
ListObjects: ListObjectsRequest -> Async<Result<string array, string>>
|
||||||
ListUsers: ListUsersRequest -> Async<Result<string array, string>>
|
ListUsers: ListUsersRequest -> Async<Result<string array, string>>
|
||||||
Read: ReadRequest -> Async<Result<ReadResponse, string>>
|
Read: ReadRequest -> Async<Result<ReadResponse, string>>
|
||||||
}
|
}
|
||||||
@@ -2,21 +2,21 @@ apiVersion: apps/v1
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: simkir-codex
|
app: <x>-codex
|
||||||
name: simkir-codex
|
name: <x>-codex
|
||||||
namespace: simkir-atlantis
|
namespace: <x>-atlantis
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: simkir-codex
|
app: <x>-codex
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: simkir-codex
|
app: <x>-codex
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- image: yolo-registry.dev.oceanbox.io/simkir/codex
|
- image: yolo-registry.dev.oceanbox.io/<x>/codex
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: codex
|
name: codex
|
||||||
ports:
|
ports:
|
||||||
@@ -33,27 +33,27 @@ spec:
|
|||||||
- name: DB_HOST
|
- name: DB_HOST
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: simkir-atlantis-db-app
|
name: <x>-atlantis-db-app
|
||||||
key: host
|
key: host
|
||||||
- name: DB_PORT
|
- name: DB_PORT
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: simkir-atlantis-db-app
|
name: <x>-atlantis-db-app
|
||||||
key: port
|
key: port
|
||||||
- name: DB_DATABASE
|
- name: DB_DATABASE
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: simkir-atlantis-db-app
|
name: <x>-atlantis-db-app
|
||||||
key: dbname
|
key: dbname
|
||||||
- name: DB_USER
|
- name: DB_USER
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: simkir-atlantis-db-app
|
name: <x>-atlantis-db-app
|
||||||
key: user
|
key: user
|
||||||
- name: DB_PASSWORD
|
- name: DB_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: simkir-atlantis-db-app
|
name: <x>-atlantis-db-app
|
||||||
key: password
|
key: password
|
||||||
- name: FGA_DB_HOST
|
- name: FGA_DB_HOST
|
||||||
valueFrom:
|
valueFrom:
|
||||||
@@ -83,6 +83,15 @@ spec:
|
|||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: azure-keyvault
|
name: azure-keyvault
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- "ALL"
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
seccompProfile:
|
||||||
|
type: "RuntimeDefault"
|
||||||
dnsPolicy: ClusterFirst
|
dnsPolicy: ClusterFirst
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
schedulerName: default-scheduler
|
schedulerName: default-scheduler
|
||||||
@@ -97,33 +106,33 @@ metadata:
|
|||||||
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
|
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
|
||||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,213.239.94.191/32
|
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,213.239.94.191/32
|
||||||
name: simkir-codex
|
name: <x>-codex
|
||||||
namespace: simkir-atlantis
|
namespace: <x>-atlantis
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: nginx
|
ingressClassName: nginx
|
||||||
rules:
|
rules:
|
||||||
- host: simkir-codex.dev.oceanbox.io
|
- host: <x>-codex.dev.oceanbox.io
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- backend:
|
- backend:
|
||||||
service:
|
service:
|
||||||
name: simkir-codex
|
name: <x>-codex
|
||||||
port:
|
port:
|
||||||
name: http
|
name: http
|
||||||
path: /
|
path: /
|
||||||
pathType: ImplementationSpecific
|
pathType: ImplementationSpecific
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- simkir-codex.dev.oceanbox.io
|
- <x>-codex.dev.oceanbox.io
|
||||||
secretName: simkir-codex-tls
|
secretName: <x>-codex-tls
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: simkir-codex
|
app: <x>-codex
|
||||||
name: simkir-codex
|
name: <x>-codex
|
||||||
namespace: simkir-atlantis
|
namespace: <x>-atlantis
|
||||||
spec:
|
spec:
|
||||||
internalTrafficPolicy: Cluster
|
internalTrafficPolicy: Cluster
|
||||||
ipFamilies:
|
ipFamilies:
|
||||||
@@ -135,4 +144,4 @@ spec:
|
|||||||
protocol: TCP
|
protocol: TCP
|
||||||
targetPort: http
|
targetPort: http
|
||||||
selector:
|
selector:
|
||||||
app: simkir-codex
|
app: <x>-codex
|
||||||
@@ -5,7 +5,7 @@ variables:
|
|||||||
|
|
||||||
include:
|
include:
|
||||||
- project: oceanbox/gitlab-ci
|
- project: oceanbox/gitlab-ci
|
||||||
ref: v4.4
|
ref: v4.5
|
||||||
file: DotnetPackage.gitlab-ci.yml
|
file: DotnetPackage.gitlab-ci.yml
|
||||||
inputs:
|
inputs:
|
||||||
project-name: archmaester
|
project-name: archmaester
|
||||||
|
|||||||
@@ -101,10 +101,12 @@ let private archiveToDto (ctx: Entity.ArchiveContext) (a: Entity.Archive) =
|
|||||||
.ToArray ()
|
.ToArray ()
|
||||||
|
|
||||||
let files =
|
let files =
|
||||||
ctx.Files
|
a.Attribs.Files
|
||||||
.Where(fun f -> a.Files.Select(_.FileId).Contains f.FileId)
|
|> Array.ofSeq
|
||||||
.OrderBy(_.Ordering)
|
|> Array.filter (fun file ->
|
||||||
.ToArray ()
|
file.StartTime >= a.StartTime && file.StartTime.AddSeconds(float (file.Frames * a.Attribs.Freq)) <= a.StartTime.AddSeconds(float (a.Frames * a.Attribs.Freq))
|
||||||
|
)
|
||||||
|
|> Array.sortBy _.Ordering
|
||||||
|
|
||||||
let modelArea =
|
let modelArea =
|
||||||
ctx.ModelAreas.SingleOrDefault (fun y -> y.Attribs.Select(_.AttribsId).Contains a.AttribsId)
|
ctx.ModelAreas.SingleOrDefault (fun y -> y.Attribs.Select(_.AttribsId).Contains a.AttribsId)
|
||||||
@@ -230,7 +232,7 @@ let retireDanglingAttribs (ctx: Entity.ArchiveContext) =
|
|||||||
type Archivist(dataSource: NpgsqlDataSource) =
|
type Archivist(dataSource: NpgsqlDataSource) =
|
||||||
let withDb qry =
|
let withDb qry =
|
||||||
try
|
try
|
||||||
let ctx = new Entity.ArchiveContext(dataSource, true)
|
use ctx = new Entity.ArchiveContext(dataSource, true)
|
||||||
qry ctx |> Ok
|
qry ctx |> Ok
|
||||||
with e ->
|
with e ->
|
||||||
Log.Error $"DataAgent.Archives.Archivist.withDb: {e}"
|
Log.Error $"DataAgent.Archives.Archivist.withDb: {e}"
|
||||||
@@ -844,6 +846,12 @@ type Archivist(dataSource: NpgsqlDataSource) =
|
|||||||
ctx.SaveChanges () |> ignore
|
ctx.SaveChanges () |> ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
member _.startConnection () =
|
||||||
|
new Entity.ArchiveContext (dataSource)
|
||||||
|
|
||||||
|
member _.startTransaction (ctx: Entity.ArchiveContext) =
|
||||||
|
ctx.Database.BeginTransaction ()
|
||||||
|
|
||||||
member x.tryAddArchive(item: ArchiveDto) =
|
member x.tryAddArchive(item: ArchiveDto) =
|
||||||
monad {
|
monad {
|
||||||
do!
|
do!
|
||||||
@@ -886,6 +894,45 @@ type Archivist(dataSource: NpgsqlDataSource) =
|
|||||||
return! Error e
|
return! Error e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
member x.tryAddArchive(ctx: Entity.ArchiveContext, item: ArchiveDto) =
|
||||||
|
monad {
|
||||||
|
do!
|
||||||
|
if not (x.modelAreaExists item.props.modelArea) then
|
||||||
|
let msg = $"ModelArea {item.props.modelArea} does not exist"
|
||||||
|
Log.Error msg
|
||||||
|
Error msg
|
||||||
|
else
|
||||||
|
Ok ()
|
||||||
|
|
||||||
|
do!
|
||||||
|
if x.archiveExists item.props.archiveId then
|
||||||
|
let msg = $"Archive {item.props.name} already exists with id {item.props.archiveId}"
|
||||||
|
Log.Error msg
|
||||||
|
Error msg
|
||||||
|
else
|
||||||
|
Ok ()
|
||||||
|
|
||||||
|
let p = item.props
|
||||||
|
let files = item.files.series
|
||||||
|
|
||||||
|
if verifyStartAndEndTimes p.startTime p.endTime files |> not then
|
||||||
|
do! Error "Start and end times don't match file series"
|
||||||
|
|
||||||
|
let! _ = verifyContiguousSeries files
|
||||||
|
|
||||||
|
Log.Information $"Adding new archive with Guid: {p.archiveId}"
|
||||||
|
|
||||||
|
do! x.addAcl' (ctx, item.acl)
|
||||||
|
Log.Information $"Adding new archive item: {(p.name, item.files.basePath)}"
|
||||||
|
Log.Debug $"Adding new archive item\n%A{item}"
|
||||||
|
|
||||||
|
match x.addArchive' (ctx, item) with
|
||||||
|
| Ok a -> return! Ok a
|
||||||
|
| Error e ->
|
||||||
|
Log.Error e
|
||||||
|
return! Error e
|
||||||
|
}
|
||||||
|
|
||||||
member x.tryAddSubArchive(item: SubArchiveDef) =
|
member x.tryAddSubArchive(item: SubArchiveDef) =
|
||||||
let ctx = new Entity.ArchiveContext (dataSource)
|
let ctx = new Entity.ArchiveContext (dataSource)
|
||||||
let transaction = ctx.Database.BeginTransaction ()
|
let transaction = ctx.Database.BeginTransaction ()
|
||||||
@@ -1330,11 +1377,12 @@ type Archivist(dataSource: NpgsqlDataSource) =
|
|||||||
.Where(fun y -> y.ArchiveId = aid)
|
.Where(fun y -> y.ArchiveId = aid)
|
||||||
.Include(_.Attribs)
|
.Include(_.Attribs)
|
||||||
.ThenInclude(_.Type)
|
.ThenInclude(_.Type)
|
||||||
|
.Include(_.Attribs)
|
||||||
|
.ThenInclude(_.Files)
|
||||||
.Include(_.Ref)
|
.Include(_.Ref)
|
||||||
.Include(_.Owners)
|
.Include(_.Owners)
|
||||||
.Include(_.Users)
|
.Include(_.Users)
|
||||||
.Include(_.Groups)
|
.Include(_.Groups)
|
||||||
.Include(fun y -> y.Files.OrderBy _.File.Ordering)
|
|
||||||
.ToArray ()
|
.ToArray ()
|
||||||
|> fun y ->
|
|> fun y ->
|
||||||
if y.Length = 0 then
|
if y.Length = 0 then
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
<PackageId>Oceanbox.DataAgent</PackageId>
|
<PackageId>Oceanbox.DataAgent</PackageId>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"Dapper.FSharp": {
|
"Dapper.FSharp": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[4.9.0, )",
|
"requested": "[4.9.0, )",
|
||||||
@@ -103,10 +103,7 @@
|
|||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.5.0, )",
|
"requested": "[2.5.0, )",
|
||||||
"resolved": "2.5.0",
|
"resolved": "2.5.0",
|
||||||
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
|
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
|
||||||
"dependencies": {
|
|
||||||
"System.Memory": "4.5.4"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Npgsql.EntityFrameworkCore.PostgreSQL": {
|
"Npgsql.EntityFrameworkCore.PostgreSQL": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
@@ -313,8 +310,7 @@
|
|||||||
"resolved": "5.3.2",
|
"resolved": "5.3.2",
|
||||||
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"FSharp.Core": "4.3.2",
|
"FSharp.Core": "4.3.2"
|
||||||
"System.Reflection.Emit.Lightweight": "4.3.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Google.Api.CommonProtos": {
|
"Google.Api.CommonProtos": {
|
||||||
@@ -530,16 +526,6 @@
|
|||||||
"resolved": "17.11.4",
|
"resolved": "17.11.4",
|
||||||
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
||||||
},
|
},
|
||||||
"Microsoft.NETCore.Platforms": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.1.0",
|
|
||||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
|
||||||
},
|
|
||||||
"Microsoft.NETCore.Targets": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.1.0",
|
|
||||||
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
|
|
||||||
},
|
|
||||||
"NetTopologySuite.IO.PostGis": {
|
"NetTopologySuite.IO.PostGis": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.1.0",
|
"resolved": "2.1.0",
|
||||||
@@ -551,11 +537,7 @@
|
|||||||
"ProjNET": {
|
"ProjNET": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.0.0",
|
"resolved": "2.0.0",
|
||||||
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
|
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
|
||||||
"dependencies": {
|
|
||||||
"System.Memory": "4.5.3",
|
|
||||||
"System.Numerics.Vectors": "4.5.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Serilog.Sinks.File": {
|
"Serilog.Sinks.File": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -574,100 +556,6 @@
|
|||||||
"Serilog.Sinks.File": "6.0.0"
|
"Serilog.Sinks.File": "6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System.IO": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"System.Text.Encoding": "4.3.0",
|
|
||||||
"System.Threading.Tasks": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Memory": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.4",
|
|
||||||
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw=="
|
|
||||||
},
|
|
||||||
"System.Numerics.Vectors": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.0",
|
|
||||||
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
|
|
||||||
},
|
|
||||||
"System.Reflection": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.IO": "4.3.0",
|
|
||||||
"System.Reflection.Primitives": "4.3.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection.Emit.ILGeneration": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Reflection": "4.3.0",
|
|
||||||
"System.Reflection.Primitives": "4.3.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection.Emit.Lightweight": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Reflection": "4.3.0",
|
|
||||||
"System.Reflection.Emit.ILGeneration": "4.3.0",
|
|
||||||
"System.Reflection.Primitives": "4.3.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection.Primitives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Runtime": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Text.Encoding": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Threading.Tasks": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
"entity": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -767,136 +655,6 @@
|
|||||||
"contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA=="
|
"contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net9.0/linux-x64": {
|
"net10.0/linux-x64": {}
|
||||||
"runtime.any.System.IO": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "SDZ5AD1DtyRoxYtEcqQ3HDlcrorMYXZeCt7ZhG9US9I5Vva+gpIWDGMkcwa5XiKL0ceQKRZIX2x0XEjLX7PDzQ=="
|
|
||||||
},
|
|
||||||
"runtime.any.System.Reflection": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "hLC3A3rI8jipR5d9k7+f0MgRCW6texsAp0MWkN/ci18FMtQ9KH7E2vDn/DH2LkxsszlpJpOn9qy6Z6/69rH6eQ=="
|
|
||||||
},
|
|
||||||
"runtime.any.System.Reflection.Primitives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "Nrm1p3armp6TTf2xuvaa+jGTTmncALWFq22CpmwRvhDf6dE9ZmH40EbOswD4GnFLrMRS0Ki6Kx5aUPmKK/hZBg=="
|
|
||||||
},
|
|
||||||
"runtime.any.System.Runtime": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "fRS7zJgaG9NkifaAxGGclDDoRn9HC7hXACl52Or06a/fxdzDajWb5wov3c6a+gVSlekRoexfjwQSK9sh5um5LQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Private.Uri": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtime.any.System.Text.Encoding": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "+ihI5VaXFCMVPJNstG4O4eo1CfbrByLxRrQQTqOTp1ttK0kUKDqOdBSTaCB2IBk/QtjDrs6+x4xuezyMXdm0HQ=="
|
|
||||||
},
|
|
||||||
"runtime.any.System.Threading.Tasks": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "OhBAVBQG5kFj1S+hCEQ3TUHBAEtZ3fbEMgZMRNdN8A0Pj4x+5nTELEqL59DU0TjKVE6II3dqKw4Dklb3szT65w=="
|
|
||||||
},
|
|
||||||
"runtime.native.System": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtime.unix.System.Private.Uri": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "ooWzobr5RAq34r9uan1r/WPXJYG1XWy9KanrxNvEnBzbFdQbMG7Y3bVi4QxR7xZMNLOxLLTAyXvnSkfj5boZSg==",
|
|
||||||
"dependencies": {
|
|
||||||
"runtime.native.System": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.IO": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"System.Text.Encoding": "4.3.0",
|
|
||||||
"System.Threading.Tasks": "4.3.0",
|
|
||||||
"runtime.any.System.IO": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Private.Uri": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "I4SwANiUGho1esj4V4oSlPllXjzCZDE+5XXso2P03LW2vOda2Enzh8DWOxwN6hnrJyp314c7KuVu31QYhRzOGg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"runtime.unix.System.Private.Uri": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.IO": "4.3.0",
|
|
||||||
"System.Reflection.Primitives": "4.3.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"runtime.any.System.Reflection": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Reflection.Primitives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"runtime.any.System.Reflection.Primitives": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Runtime": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"runtime.any.System.Runtime": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Text.Encoding": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"runtime.any.System.Text.Encoding": "4.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Threading.Tasks": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.3.0",
|
|
||||||
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
|
||||||
"Microsoft.NETCore.Targets": "1.1.0",
|
|
||||||
"System.Runtime": "4.3.0",
|
|
||||||
"runtime.any.System.Threading.Tasks": "4.3.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user