Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9490a06303 | |||
| 5fb1ae0678 | |||
| 97c03e216b | |||
|
|
bab4490847 | ||
|
8e824d4afa
|
|||
|
|
777cf1a31d | ||
|
efacb2a332
|
|||
|
|
17c4e9dd22 | ||
| 503ccbb2ad | |||
|
54c40d7acc
|
|||
|
d8d5e076ba
|
|||
|
|
fd2b3fe691 | ||
| 6ae7a7dac8 | |||
|
e429a855e5
|
|||
|
|
9ed60b7cc8 | ||
|
|
175df0ce33 | ||
|
|
d5cde19250 | ||
| 2e1165d99c | |||
| a100ffa77b | |||
| 95c3608d85 | |||
| 15d91d87bf | |||
| f6ec692ebf | |||
| f41617c08e | |||
| 2bf0d82a5b | |||
| 3e61cfb939 | |||
| 21ec3a04ab | |||
| 5d6fe5572b | |||
| 0a543c7b21 | |||
| b1ba2effe3 | |||
| 626ce34dc0 | |||
| b879555e6a | |||
| ed08980df3 | |||
| 6aee2bbc60 | |||
| d4701c958c | |||
| 446d4f4171 | |||
| 1ed2a15c4c | |||
| 42b746871a | |||
| 6c20b01cc2 | |||
| 4f879252a0 | |||
|
|
11724987b0 | ||
| d90703453f | |||
|
eaea4b2e21
|
|||
|
|
68efc76e8e | ||
| 6df17c88c7 | |||
|
ec109328fb
|
|||
|
|
f4943a148b | ||
|
15e348c17b
|
|||
|
6cf5262dd5
|
|||
|
|
4b229cd7d7 | ||
| e372be192a | |||
|
ab37e88bb0
|
|||
|
|
d38a784326 | ||
| 4d18b105c8 | |||
|
492651e0f3
|
|||
|
cd678a41f6
|
|||
| 03fbc14b72 | |||
|
d86db7a66c
|
|||
|
7182f7c9f0
|
|||
|
eac23e7d1a
|
|||
|
b500fdb211
|
|||
|
180aba4fa5
|
|||
|
|
d9d7221e90 | ||
| b3fe6f8b70 | |||
| 4792720d74 | |||
| 46e86eb5f9 | |||
| 9a890f30fc | |||
| 66c44976d8 | |||
| 608caeeda2 | |||
| 55bcaaf963 | |||
|
|
ce10ea93db | ||
|
e513d87d24
|
|||
|
|
08061bc6ce | ||
| dd55a0c9df | |||
| 89f0f768e3 | |||
|
5315a05fa2
|
|||
| 57b28daf4e | |||
|
|
0ba060d78c | ||
|
|
b2077ae317 | ||
|
4cd3673d15
|
|||
|
|
771712ad9a | ||
| 1d6941ecc6 | |||
|
56d34767d7
|
|||
| 5fbd914e24 | |||
| 5f193c559f | |||
| a998483d2c | |||
|
|
a4159f0fff | ||
| 866f3a317b | |||
|
4eac05cbb7
|
|||
|
efb3292d9f
|
|||
|
6e822bd5d1
|
|||
|
d8bf174d3a
|
|||
|
2f7be7b051
|
|||
| 18bb207e4a | |||
|
c914f4a477
|
|||
|
2da1be0c6b
|
|||
|
eb00b8c19d
|
|||
|
09a9e47348
|
|||
|
156ae2315a
|
|||
|
|
cf6bedbd9b | ||
| 36aa90519e | |||
|
14c1a57331
|
|||
| d7f0630693 | |||
| 3b7149f161 | |||
| 5545e90160 | |||
| da5b38d1ea | |||
|
65928c4064
|
|||
|
|
033b61dd4f | ||
| 5725d43b11 | |||
|
a8a187a412
|
|||
|
|
f30e16b15e | ||
| bd745042df | |||
|
70878e1423
|
|||
|
937b2c367b
|
|||
| faa0a8533e | |||
|
1cb9d455db
|
|||
|
|
453c9d234c | ||
| 369127e081 | |||
|
|
563faa6c0b | ||
|
|
17163ab002 | ||
|
|
6293e9e67a | ||
|
|
a68ef32614 | ||
| 4de10614be | |||
|
|
2887e6a909 | ||
|
|
e75ffc41e5 | ||
|
|
a620c26812 | ||
|
|
f2bb57b50d | ||
| 8e0cb2105a | |||
|
|
048e80356b | ||
|
|
a47fb89143 | ||
|
|
1b8167c66e | ||
|
|
48d46eda62 | ||
|
|
36307c822e | ||
|
|
dca32db800 | ||
|
|
a153238f79 | ||
|
|
c1fa85fd1b | ||
|
|
69b380e665 | ||
|
|
afc888ab60 | ||
|
|
9a4ef08060 | ||
|
f68b7f68c8
|
|||
|
|
d8143a6b8d | ||
|
e04d36ca12
|
|||
|
|
275ec44a97 | ||
|
fff7913cd5
|
|||
|
|
23ba2efe96 | ||
|
759bbc6f60
|
|||
|
3140c07ad0
|
|||
|
b109dbdcbd
|
|||
|
c1be7c468d
|
|||
|
|
d3797115f7 | ||
|
6d04af6230
|
|||
|
7cf50641f9
|
|||
|
dd398bd96b
|
|||
|
|
63d782ade4 | ||
| e3a1f56b87 | |||
|
|
2f18948cce | ||
|
|
4786850431 | ||
|
|
7584bf661f | ||
|
|
f23b3f1821 | ||
|
|
5bb2ffd67c | ||
| c3c9e8e4e2 | |||
| b03908f93e | |||
| c2e7762df8 |
@@ -1,37 +0,0 @@
|
||||
open Fake.Core
|
||||
open Fake.IO
|
||||
open Farmer
|
||||
open Farmer.Builders
|
||||
|
||||
open Helpers
|
||||
|
||||
initializeContext()
|
||||
|
||||
let packPath = Path.getFullName "packages"
|
||||
|
||||
Target.create "Clean" (fun _ -> Shell.cleanDir packPath)
|
||||
|
||||
Target.create "InstallClient" (fun _ ->
|
||||
run dotnet "tool restore" "."
|
||||
)
|
||||
|
||||
Target.create "Run" ignore
|
||||
|
||||
Target.create "Format" (fun _ ->
|
||||
run dotnet "fantomas . -r" "src"
|
||||
)
|
||||
|
||||
open Fake.Core.TargetOperators
|
||||
|
||||
let dependencies = [
|
||||
"Clean"
|
||||
==> "InstallClient"
|
||||
|
||||
"Run"
|
||||
==> "InstallClient"
|
||||
|
||||
"Format"
|
||||
]
|
||||
|
||||
[<EntryPoint>]
|
||||
let main args = runOrDefault args
|
||||
@@ -1,129 +0,0 @@
|
||||
module Helpers
|
||||
|
||||
open Fake.Core
|
||||
|
||||
let initializeContext () =
|
||||
let execContext = Context.FakeExecutionContext.Create false "build.fsx" [ ]
|
||||
Context.setExecutionContext (Context.RuntimeContext.Fake execContext)
|
||||
|
||||
module Proc =
|
||||
module Parallel =
|
||||
open System
|
||||
|
||||
let locker = obj()
|
||||
|
||||
let colors =
|
||||
[| ConsoleColor.Blue
|
||||
ConsoleColor.Yellow
|
||||
ConsoleColor.Magenta
|
||||
ConsoleColor.Cyan
|
||||
ConsoleColor.DarkBlue
|
||||
ConsoleColor.DarkYellow
|
||||
ConsoleColor.DarkMagenta
|
||||
ConsoleColor.DarkCyan |]
|
||||
|
||||
let print color (colored: string) (line: string) =
|
||||
lock locker
|
||||
(fun () ->
|
||||
let currentColor = Console.ForegroundColor
|
||||
Console.ForegroundColor <- color
|
||||
Console.Write colored
|
||||
Console.ForegroundColor <- currentColor
|
||||
Console.WriteLine line)
|
||||
|
||||
let onStdout index name (line: string) =
|
||||
let color = colors.[index % colors.Length]
|
||||
if isNull line then
|
||||
print color $"{name}: --- END ---" ""
|
||||
else if String.isNotNullOrEmpty line then
|
||||
print color $"{name}: " line
|
||||
|
||||
let onStderr name (line: string) =
|
||||
let color = ConsoleColor.Red
|
||||
if isNull line |> not then
|
||||
print color $"{name}: " line
|
||||
|
||||
let redirect (index, (name, createProcess)) =
|
||||
createProcess
|
||||
|> CreateProcess.redirectOutputIfNotRedirected
|
||||
|> CreateProcess.withOutputEvents (onStdout index name) (onStderr name)
|
||||
|
||||
let printStarting indexed =
|
||||
for (index, (name, c: CreateProcess<_>)) in indexed do
|
||||
let color = colors.[index % colors.Length]
|
||||
let wd =
|
||||
c.WorkingDirectory
|
||||
|> Option.defaultValue ""
|
||||
let exe = c.Command.Executable
|
||||
let args = c.Command.Arguments.ToStartInfo
|
||||
print color $"{name}: {wd}> {exe} {args}" ""
|
||||
|
||||
let run cs =
|
||||
cs
|
||||
|> Seq.toArray
|
||||
|> Array.indexed
|
||||
|> fun x -> printStarting x; x
|
||||
|> Array.map redirect
|
||||
|> Array.Parallel.map Proc.run
|
||||
|
||||
let createProcess exe arg dir =
|
||||
CreateProcess.fromRawCommandLine exe arg
|
||||
|> CreateProcess.withWorkingDirectory dir
|
||||
|> CreateProcess.ensureExitCode
|
||||
|
||||
let dotnet = createProcess "dotnet"
|
||||
|
||||
// NOTE: Uses dotnet-tools from nixpkgs
|
||||
let fable = createProcess "fable"
|
||||
let fantomas = createProcess "fantomas"
|
||||
|
||||
let bun =
|
||||
let bunPath =
|
||||
match ProcessUtils.tryFindFileOnPath "bun" with
|
||||
| Some path -> path
|
||||
| None ->
|
||||
"bun was not found in path. Please install it and make sure it's available from your path. " +
|
||||
"See https://safe-stack.github.io/docs/quickstart/#install-pre-requisites for more info"
|
||||
|> failwith
|
||||
|
||||
createProcess bunPath
|
||||
|
||||
let bunx = createProcess "bunx"
|
||||
|
||||
type BundleMode =
|
||||
| Prod
|
||||
| Devel
|
||||
| Watch
|
||||
with
|
||||
override this.ToString() =
|
||||
match this with
|
||||
| Prod -> "production"
|
||||
| Devel -> "development"
|
||||
| Watch -> "watch"
|
||||
|
||||
let viteCmd (m: BundleMode) outDir =
|
||||
match m with
|
||||
| Prod -> $"vite build -c ../../vite.config.js -m {m} --emptyOutDir --outDir {outDir}/public"
|
||||
| Devel -> $"vite build -c ../../vite.config.js -m {m} --minify false --sourcemap true --emptyOutDir --outDir {outDir}/public"
|
||||
| Watch -> "vite -c ../../vite.config.js"
|
||||
|
||||
let run proc arg dir =
|
||||
proc arg dir
|
||||
|> Proc.run
|
||||
|> ignore
|
||||
|
||||
let runParallel processes =
|
||||
processes
|
||||
|> Proc.Parallel.run
|
||||
|> ignore
|
||||
|
||||
let runOrDefault args =
|
||||
try
|
||||
match args with
|
||||
| [| target |] -> Target.runOrDefault target
|
||||
| _ ->
|
||||
Target.runOrDefault "Run"
|
||||
0
|
||||
with e ->
|
||||
printfn "%A" e
|
||||
1
|
||||
@@ -23,6 +23,9 @@ max_line_length= 80
|
||||
indent_size = 2
|
||||
max_line_length = 80
|
||||
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
|
||||
[*.fs]
|
||||
max_line_length= 120
|
||||
|
||||
|
||||
35
.envrc
35
.envrc
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export NPINS_DIRECTORY="nix"
|
||||
export APP_ENV=$USER
|
||||
|
||||
# the shebang is ignored, but nice for editors
|
||||
watch_file nix/sources.json
|
||||
|
||||
@@ -9,33 +13,4 @@ dotenv_if_exists
|
||||
use nix
|
||||
|
||||
# HACK: Workaround for direnv bug
|
||||
unset TMP TMPDIR TEMP TEMPDIR
|
||||
|
||||
# HACK: Configure Rider to use the correct .NET paths from an ambient .NET
|
||||
use_rider_dotnet() {
|
||||
# Get paths
|
||||
DOTNET_PATH=$(readlink "$(which dotnet)")
|
||||
SETTINGS_FILE=$(find . -maxdepth 1 -type f -name '*.sln.DotSettings.user')
|
||||
MSBUILD=$(realpath "$(find "$(dirname "$DOTNET_PATH")/../share/dotnet/sdk" -maxdepth 2 -type f -name MSBuild.dll)")
|
||||
|
||||
# Update Rider settings if they exist
|
||||
if [ -f "$SETTINGS_FILE" ] ; then
|
||||
xmlstarlet ed --inplace \
|
||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
|
||||
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" \
|
||||
--value "$(realpath "$(dirname "$DOTNET_PATH")/../share/dotnet/dotnet")" \
|
||||
"$SETTINGS_FILE"
|
||||
|
||||
xmlstarlet ed --inplace \
|
||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||
-N ss="urn:shemas-jetbrains-com:settings-storage-xaml" \
|
||||
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" \
|
||||
--value "$MSBUILD" \
|
||||
"$SETTINGS_FILE"
|
||||
fi
|
||||
}
|
||||
unset TMP TMPDIR TEMP TEMPDIR
|
||||
@@ -1,49 +1,51 @@
|
||||
# yaml-language-server: $schema=https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json
|
||||
variables:
|
||||
SDK_VERSION: 9.0
|
||||
SKIP_TESTS: "true"
|
||||
SKIP_SINGULARITY: "true"
|
||||
SKIP_TESTS: "true"
|
||||
|
||||
default:
|
||||
tags:
|
||||
- nix
|
||||
|
||||
include:
|
||||
- project: oceanbox/gitlab-ci
|
||||
ref: v4.2
|
||||
file: template/Base.gitlab-ci.yml
|
||||
- local: '/src/Atlantis/.gitlab-ci.yml'
|
||||
rules:
|
||||
- changes:
|
||||
- 'src/Atlantis/**/*'
|
||||
- 'nix/packages/atlantis.nix'
|
||||
- 'nix/packages/atlantis-client.nix'
|
||||
- 'nix/containers.nix'
|
||||
- local: '/src/Sorcerer/.gitlab-ci.yml'
|
||||
rules:
|
||||
- changes:
|
||||
- 'src/Sorcerer/**/*'
|
||||
- 'nix/packages/sorcerer.nix'
|
||||
- 'nix/containers.nix'
|
||||
- local: '/src/Archivist/.gitlab-ci.yml'
|
||||
rules:
|
||||
- changes:
|
||||
- 'src/Archivist/**/*'
|
||||
- 'nix/packages/archivist.nix'
|
||||
- local: '/src/Interfaces/.gitlab-ci.yml'
|
||||
rules:
|
||||
- changes:
|
||||
- 'src/Interfaces/**/*'
|
||||
- 'nix/packages/api.nix'
|
||||
- local: '/src/DataAgent/.gitlab-ci.yml'
|
||||
rules:
|
||||
- changes:
|
||||
- 'src/DataAgent/**/*'
|
||||
- 'nix/packages/dataagent.nix'
|
||||
- local: '/src/ServerPack/.gitlab-ci.yml'
|
||||
rules:
|
||||
- changes:
|
||||
- 'src/ServerPack/**/*'
|
||||
- 'nix/packages/serverpack.nix'
|
||||
- local: '/src/Codex/.gitlab-ci.yml'
|
||||
rules:
|
||||
- changes:
|
||||
- 'src/Codex/**/*'
|
||||
- 'nix/packages/node-modules.nix'
|
||||
- 'nix/packages/sources.nix'
|
||||
- project: oceanbox/gitlab-ci
|
||||
ref: v4.5
|
||||
file: template/Base.gitlab-ci.yml
|
||||
- local: "/src/Atlantis/.gitlab-ci.yml"
|
||||
rules:
|
||||
- changes:
|
||||
- "src/Atlantis/**/*"
|
||||
- "nix/packages/atlantis.nix"
|
||||
- "nix/packages/atlantis-client.nix"
|
||||
- "nix/containers.nix"
|
||||
- local: "/src/Sorcerer/.gitlab-ci.yml"
|
||||
rules:
|
||||
- changes:
|
||||
- "src/Sorcerer/**/*"
|
||||
- "nix/packages/sorcerer.nix"
|
||||
- "nix/containers.nix"
|
||||
- local: "/src/Archivist/.gitlab-ci.yml"
|
||||
rules:
|
||||
- changes:
|
||||
- "src/Archivist/**/*"
|
||||
- "nix/packages/archivist.nix"
|
||||
- local: "/src/Interfaces/.gitlab-ci.yml"
|
||||
rules:
|
||||
- changes:
|
||||
- "src/Interfaces/**/*"
|
||||
- "nix/packages/api.nix"
|
||||
- local: "/src/DataAgent/.gitlab-ci.yml"
|
||||
rules:
|
||||
- changes:
|
||||
- "src/DataAgent/**/*"
|
||||
- "nix/packages/dataagent.nix"
|
||||
- local: "/src/ServerPack/.gitlab-ci.yml"
|
||||
rules:
|
||||
- changes:
|
||||
- "src/ServerPack/**/*"
|
||||
- "nix/packages/serverpack.nix"
|
||||
- local: "/src/Codex/.gitlab-ci.yml"
|
||||
rules:
|
||||
- changes:
|
||||
- "src/Codex/**/*"
|
||||
- "nix/packages/node-modules.nix"
|
||||
- "nix/packages/sources.nix"
|
||||
|
||||
17
Build.fsproj
17
Build.fsproj
@@ -1,17 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include=".build/Helpers.fs" />
|
||||
<Compile Include=".build/Build.fs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fake.Core.Target" />
|
||||
<PackageReference Include="Fake.DotNet.Cli" />
|
||||
<PackageReference Include="Fake.IO.FileSystem" />
|
||||
<PackageReference Include="Farmer" />
|
||||
<PackageReference Include="FSharp.Core" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -39,6 +39,7 @@
|
||||
<PackageVersion Include="Matplotlib.ColorMaps" Version="3.0.1" />
|
||||
<PackageVersion Include="Thoth.Fetch" Version="3.0.1" />
|
||||
<PackageVersion Include="Thoth.Json" Version="10.4.1"/>
|
||||
<PackageVersion Include="FS.FluentUI" Version="3.0.0"/>
|
||||
<!-- Serverpack -->
|
||||
<PackageVersion Include="OpenFga.Sdk" Version="0.7.0"/>
|
||||
<PackageVersion Include="FSharp.SystemTextJson" Version="1.3.13"/>
|
||||
@@ -81,11 +82,6 @@
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="ProjNet.FSharp" Version="5.2.0" />
|
||||
<!-- Build -->
|
||||
<PackageVersion Include="Fake.Core.Target" Version="6.1.3" />
|
||||
<PackageVersion Include="Fake.DotNet.Cli" Version="6.1.3" />
|
||||
<PackageVersion Include="Fake.IO.FileSystem" Version="6.1.3" />
|
||||
<PackageVersion Include="Farmer" Version="1.9.6" />
|
||||
<!-- Dapperizer -->
|
||||
<PackageVersion Include="Oceanbox.SDSLite" Version="2.8.0" />
|
||||
<PackageVersion Include="Dapper.FSharp" Version="4.9.0"/>
|
||||
|
||||
48
README.md
48
README.md
@@ -69,25 +69,7 @@ kubectl --context oceanbox -n default get pods
|
||||
Required helm manifests are hosted in a separate repository: <https://gitlab.com/oceanbox/manifests>.
|
||||
Clone it into a directory _in the same parent directory as this repository._
|
||||
|
||||
The Bitnami respository must also be added to helm:
|
||||
|
||||
```shell
|
||||
helm repo add bitnami https://charts.bitnami.com/bitnami
|
||||
```
|
||||
|
||||
### DNS
|
||||
|
||||
Some DNS masking is required. Add the following to your NixOS configuration:
|
||||
|
||||
```nix
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
settings.address = [
|
||||
"/.local/127.0.0.1"
|
||||
"/.local.oceanbox.io/127.0.0.1"
|
||||
];
|
||||
};
|
||||
```
|
||||
You'll have to run `helm dependency update` in the atlantis directory within the manifest repo to download the charts.
|
||||
|
||||
### NuGet
|
||||
|
||||
@@ -102,14 +84,30 @@ To retrieve packages from the private Oceanbox nuget registry, configure it with
|
||||
</packageSources>
|
||||
<packageSourceCredentials>
|
||||
<oceanbox>
|
||||
<add key="Username" value="oceanbox-nuget" />
|
||||
<add key="ClearTextPassword" value="<...>" />
|
||||
<add key="Username" value="<Your-GitLab-Username>" />
|
||||
<add key="ClearTextPassword" value="<Your-GitLab-PAT>" />
|
||||
</oceanbox>
|
||||
</packageSourceCredentials>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="oceanbox">
|
||||
<package pattern="Oceanbox.*" />
|
||||
<package pattern="ProjNet.FSharp" />
|
||||
<package pattern="Drifters.Api" />
|
||||
<package pattern="Fable.Lit" />
|
||||
<package pattern="Fable.Lit.*" />
|
||||
<package pattern="Fable.SignalR" />
|
||||
<package pattern="Fable.SignalR.*" />
|
||||
<package pattern="Fable.OpenLayers" />
|
||||
<package pattern="Matplotlib.*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
Substitute `<...>` for the corresponding secret.
|
||||
Substitute with your own gitlab username and PAT in the credentials.
|
||||
|
||||
Now, we should be able to `restore`:
|
||||
|
||||
@@ -168,7 +166,7 @@ You should now be able to access the Atlantis client (with HMR) on <atlantis.loc
|
||||
### Trust Root Certificate
|
||||
|
||||
> [!note]
|
||||
> You'll need to run `dotnet run bundle` in `src/Atlantis` to generate the `/certs` directory
|
||||
> You'll need to run `just run-client` in `src/Atlantis` to generate the certificates in `~/.vite-plugin-mkcert/certs`
|
||||
|
||||
In order for your browser to allow you to access the web application, you must add the root certificate generated by `mkcert` to the list of trusted authorities in your browser:
|
||||
|
||||
@@ -179,9 +177,9 @@ In order for your browser to allow you to access the web application, you must a
|
||||
|
||||
### Add `user` to OpenFGA
|
||||
|
||||
Ask [sales](moritz.jorg@oceanbox.io) to add your `azure-ad-user` to OpenFGA.
|
||||
Ask [sales](support@oceanbox.io) to add your `azure-ad-user` to OpenFGA.
|
||||
|
||||
### CORS for Sorcerer
|
||||
|
||||
Add the `url` of your instance to the CORS list of Sorcerer
|
||||
[here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52).
|
||||
[here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52).
|
||||
253
RELEASE_NOTES.md
253
RELEASE_NOTES.md
@@ -1,5 +1,258 @@
|
||||
# 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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add XtractActor ([a8a187a](https://gitlab.com/oceanbox/Poseidon/commit/a8a187a412c13d3e9d21cbbcfc2e1813c0e38dfe))
|
||||
|
||||
# [1.36.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.35.2...v1.36.0) (2025-12-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Saturn.ReverseProxy middleware ([bd74504](https://gitlab.com/oceanbox/Poseidon/commit/bd745042dfa51fbbef7bf7b55d31b8b57e6ad0a4))
|
||||
|
||||
## [1.35.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.35.1...v1.35.2) (2025-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **drifters:** reverse toggle on postdrift analysis ([563faa6](https://gitlab.com/oceanbox/Poseidon/commit/563faa6c0bd20a7d3f184bba66ae5df340e7ef4e))
|
||||
|
||||
## [1.35.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.35.0...v1.35.1) (2025-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **stats:** no stats banner showing when stats are available, closing [#87](https://gitlab.com/oceanbox/Poseidon/issues/87) ([e75ffc4](https://gitlab.com/oceanbox/Poseidon/commit/e75ffc41e5d298e2ecf92c5c4d11e0f930f633ab))
|
||||
* **stats:** set priority order of depth plots, closing [#88](https://gitlab.com/oceanbox/Poseidon/issues/88) ([2887e6a](https://gitlab.com/oceanbox/Poseidon/commit/2887e6a90951f4aaa088ad14b3d2bbcb6fd25b93))
|
||||
|
||||
# [1.35.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.34.2...v1.35.0) (2025-12-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ❄️ ([a620c26](https://gitlab.com/oceanbox/Poseidon/commit/a620c26812d3ec7517c34e7931e03b411f725907))
|
||||
|
||||
## [1.34.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.34.1...v1.34.2) (2025-11-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Temporarily use vtn as only source for wind barbs ([048e803](https://gitlab.com/oceanbox/Poseidon/commit/048e80356b59cfcd408bf6784bbc2e22aebe25c6))
|
||||
|
||||
## [1.34.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.34.0...v1.34.1) (2025-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **stats:** find available stats archives ([48d46ed](https://gitlab.com/oceanbox/Poseidon/commit/48d46eda62160d3efe1423238a25f26a439c6b88))
|
||||
|
||||
# [1.34.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.11...v1.34.0) (2025-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* extract data cage interaction matrix, closing [#84](https://gitlab.com/oceanbox/Poseidon/issues/84) ([afc888a](https://gitlab.com/oceanbox/Poseidon/commit/afc888ab604f4759e3787e3feab568a322109b34))
|
||||
* reintroduce active layer selector, closes [#74](https://gitlab.com/oceanbox/Poseidon/issues/74) ([c1fa85f](https://gitlab.com/oceanbox/Poseidon/commit/c1fa85fd1b955e810bd76973e5873a7ab4cb8f18))
|
||||
* remove parameter limitations on particle sims ([69b380e](https://gitlab.com/oceanbox/Poseidon/commit/69b380e6659521c9f9ab489fa75d1221cc9b7db2))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* fly-to coordinate button, closes [#85](https://gitlab.com/oceanbox/Poseidon/issues/85) ([a153238](https://gitlab.com/oceanbox/Poseidon/commit/a153238f79e2b2c87ac3553acce00bb17c1529f4))
|
||||
|
||||
## [1.33.11](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.10...v1.33.11) (2025-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** Run archivist ci on coffee ([f68b7f6](https://gitlab.com/oceanbox/Poseidon/commit/f68b7f68c8fd95d5d593702632cc2e9a7b36007a))
|
||||
|
||||
## [1.33.10](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.9...v1.33.10) (2025-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** Build on Coffee ([e04d36c](https://gitlab.com/oceanbox/Poseidon/commit/e04d36ca124693b471ac973cd4d0f39f42f0fec0))
|
||||
|
||||
## [1.33.9](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.8...v1.33.9) (2025-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nix:** Bump hash for node_modules ([fff7913](https://gitlab.com/oceanbox/Poseidon/commit/fff7913cd5280f0732bb23bc1cb6ed5282631b90))
|
||||
|
||||
## [1.33.8](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.7...v1.33.8) (2025-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** Format .gitlab-ci.yml and move codex to nix runner ([3140c07](https://gitlab.com/oceanbox/Poseidon/commit/3140c07ad078d3b8b3082d2f86eaa73f4ba08969))
|
||||
* **ci:** Remove unsed var ([759bbc6](https://gitlab.com/oceanbox/Poseidon/commit/759bbc6f60e5b364dd83653193d1501ce86c9eef))
|
||||
* **ci:** Try using v4.4 ([c1be7c4](https://gitlab.com/oceanbox/Poseidon/commit/c1be7c468dd9a689eb0b1b61797841c398fadc02))
|
||||
* **ci:** Use 4.4 for check ([b109dbd](https://gitlab.com/oceanbox/Poseidon/commit/b109dbdcbdfa3315e099b2edf72eb10518732c8f))
|
||||
|
||||
## [1.33.7](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.6...v1.33.7) (2025-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** Change name to codex ([6d04af6](https://gitlab.com/oceanbox/Poseidon/commit/6d04af6230b536c1cbadf9abd2b59cf13609b451))
|
||||
* **ci:** Correct tag for codex pipeline ([7cf5064](https://gitlab.com/oceanbox/Poseidon/commit/7cf50641f986ab81b030e678ff7db0de83acae98))
|
||||
* **ci:** Run Codex on Coffee ([dd398bd](https://gitlab.com/oceanbox/Poseidon/commit/dd398bd96b0d79c8585a3156edcd2158982744d9))
|
||||
|
||||
## [1.33.6](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.5...v1.33.6) (2025-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **maps:** Make FluentUI DatePicker popup inline; closes [#83](https://gitlab.com/oceanbox/Poseidon/issues/83) ([e3a1f56](https://gitlab.com/oceanbox/Poseidon/commit/e3a1f56b87c20ad9aada6108c92854b9d12671df))
|
||||
|
||||
## [1.33.5](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.4...v1.33.5) (2025-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* General fixes "stats" menu ([c3c9e8e](https://gitlab.com/oceanbox/Poseidon/commit/c3c9e8e4e2b473564d1f6434a28dd934457189e9)), closes [#79](https://gitlab.com/oceanbox/Poseidon/issues/79) [#80](https://gitlab.com/oceanbox/Poseidon/issues/80) [#81](https://gitlab.com/oceanbox/Poseidon/issues/81) [#82](https://gitlab.com/oceanbox/Poseidon/issues/82)
|
||||
* no more DateFlicker (thanks Simen) ([7584bf6](https://gitlab.com/oceanbox/Poseidon/commit/7584bf661f2fd1cbf95e473a3334efcd69c77392))
|
||||
* remove goto today in date picker ([f23b3f1](https://gitlab.com/oceanbox/Poseidon/commit/f23b3f18213682e0b41c1db13b72e3c0bb0534b6))
|
||||
* **stats:** alert banner on missing stats ([5bb2ffd](https://gitlab.com/oceanbox/Poseidon/commit/5bb2ffd67c16078854b9f86a6728958c3923626c))
|
||||
|
||||
## [1.33.4](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.3...v1.33.4) (2025-11-27)
|
||||
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ let
|
||||
in
|
||||
clean version;
|
||||
|
||||
dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0;
|
||||
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_9_0;
|
||||
dotnet-sdk = pkgs.dotnetCorePackages.sdk_10_0;
|
||||
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_10_0;
|
||||
deps = nix-utils.output.lib.nuget.deps;
|
||||
|
||||
# Usage: export NETRC="$(agenix -d netrc.age)" in `./nix/secrets`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.0",
|
||||
"version": "10.0.100",
|
||||
"rollForward": "latestMinor"
|
||||
}
|
||||
}
|
||||
42
justfile
Normal file
42
justfile
Normal file
@@ -0,0 +1,42 @@
|
||||
# Poseidon build commands
|
||||
# Install just: https://github.com/casey/just
|
||||
#
|
||||
# Sub-projects with justfiles:
|
||||
# - Atlantis (src/Atlantis) - Server + Client application with Fable/Vite
|
||||
# - ServerPack (src/ServerPack) - Server package library
|
||||
# - DataAgent (src/DataAgent) - Data agent library
|
||||
# - Interfaces (src/Interfaces) - API interfaces library
|
||||
# - Archivist (src/Archivist) - CLI tool with client
|
||||
# - Sorcerer (src/Sorcerer) - Server application with client
|
||||
#
|
||||
# Run 'just <project>' to see available commands for each project (e.g., 'just atlantis')
|
||||
|
||||
set dotenv-load
|
||||
|
||||
# Default recipe - show available commands
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Show available commands for Atlantis (src/Atlantis)
|
||||
atlantis:
|
||||
@just src/Atlantis/
|
||||
|
||||
# Show available commands for ServerPack (src/ServerPack)
|
||||
serverpack:
|
||||
@just src/ServerPack/
|
||||
|
||||
# Show available commands for DataAgent (src/DataAgent)
|
||||
dataagent:
|
||||
@just src/DataAgent/
|
||||
|
||||
# Show available commands for Interfaces (src/Interfaces)
|
||||
interfaces:
|
||||
@just src/Interfaces/
|
||||
|
||||
# Show available commands for Archivist (src/Archivist)
|
||||
archivist:
|
||||
@just src/Archivist/
|
||||
|
||||
# Show available commands for Sorcerer (src/Sorcerer)
|
||||
sorcerer:
|
||||
@just src/Sorcerer/
|
||||
151
nix/default.nix
151
nix/default.nix
@@ -9,8 +9,15 @@
|
||||
*/
|
||||
# Generated by npins. Do not modify; will be overwritten regularly
|
||||
let
|
||||
data = builtins.fromJSON (builtins.readFile ./sources.json);
|
||||
version = data.version;
|
||||
# Backwards-compatibly make something that previously didn't take any arguments take some
|
||||
# The function must return an attrset, and will unfortunately be eagerly evaluated
|
||||
# Same thing, but it catches eval errors on the default argument so that one may still call it with other arguments
|
||||
mkFunctor =
|
||||
fn:
|
||||
let
|
||||
e = builtins.tryEval (fn { });
|
||||
in
|
||||
(if e.success then e.value else { error = fn { }; }) // { __functor = _self: fn; };
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
|
||||
range =
|
||||
@@ -21,7 +28,6 @@ let
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
|
||||
stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
|
||||
concatMapStrings = f: list: concatStrings (map f list);
|
||||
concatStrings = builtins.concatStringsSep "";
|
||||
|
||||
# If the environment variable NPINS_OVERRIDE_${name} is set, then use
|
||||
@@ -48,41 +54,87 @@ let
|
||||
|
||||
mkSource =
|
||||
name: spec:
|
||||
{
|
||||
pkgs ? null,
|
||||
}:
|
||||
assert spec ? type;
|
||||
let
|
||||
# Unify across builtin and pkgs fetchers.
|
||||
# `fetchGit` requires a wrapper because of slight API differences.
|
||||
fetchers =
|
||||
if pkgs == null then
|
||||
{
|
||||
inherit (builtins) fetchTarball fetchurl;
|
||||
# For some fucking reason, fetchGit has a different signature than the other builtin fetchers …
|
||||
fetchGit = args: (builtins.fetchGit args).outPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
fetchTarball =
|
||||
{
|
||||
url,
|
||||
sha256,
|
||||
}:
|
||||
pkgs.fetchzip {
|
||||
inherit url sha256;
|
||||
extension = "tar";
|
||||
};
|
||||
inherit (pkgs) fetchurl;
|
||||
fetchGit =
|
||||
{
|
||||
url,
|
||||
submodules,
|
||||
rev,
|
||||
name,
|
||||
narHash,
|
||||
}:
|
||||
pkgs.fetchgit {
|
||||
inherit url rev name;
|
||||
fetchSubmodules = submodules;
|
||||
hash = narHash;
|
||||
};
|
||||
};
|
||||
|
||||
# Dispatch to the correct code path based on the type
|
||||
path =
|
||||
if spec.type == "Git" then
|
||||
mkGitSource spec
|
||||
mkGitSource fetchers spec
|
||||
else if spec.type == "GitRelease" then
|
||||
mkGitSource spec
|
||||
mkGitSource fetchers spec
|
||||
else if spec.type == "PyPi" then
|
||||
mkPyPiSource spec
|
||||
mkPyPiSource fetchers spec
|
||||
else if spec.type == "Channel" then
|
||||
mkChannelSource spec
|
||||
mkChannelSource fetchers spec
|
||||
else if spec.type == "Tarball" then
|
||||
mkTarballSource spec
|
||||
mkTarballSource fetchers spec
|
||||
else if spec.type == "Container" then
|
||||
mkContainerSource pkgs spec
|
||||
else
|
||||
builtins.throw "Unknown source type ${spec.type}";
|
||||
in
|
||||
spec // { outPath = mayOverride name path; };
|
||||
|
||||
mkGitSource =
|
||||
{
|
||||
fetchTarball,
|
||||
fetchGit,
|
||||
...
|
||||
}:
|
||||
{
|
||||
repository,
|
||||
revision,
|
||||
url ? null,
|
||||
submodules,
|
||||
hash,
|
||||
branch ? null,
|
||||
...
|
||||
}:
|
||||
assert repository ? type;
|
||||
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
|
||||
# In the latter case, there we will always be an url to the tarball
|
||||
if url != null && !submodules then
|
||||
builtins.fetchTarball {
|
||||
fetchTarball {
|
||||
inherit url;
|
||||
sha256 = hash; # FIXME: check nix version & use SRI hashes
|
||||
sha256 = hash;
|
||||
}
|
||||
else
|
||||
let
|
||||
@@ -93,6 +145,8 @@ let
|
||||
"https://github.com/${repository.owner}/${repository.repo}.git"
|
||||
else if repository.type == "GitLab" then
|
||||
"${repository.server}/${repository.repo_path}.git"
|
||||
else if repository.type == "Forgejo" then
|
||||
"${repository.server}/${repository.owner}/${repository.repo}.git"
|
||||
else
|
||||
throw "Unrecognized repository type ${repository.type}";
|
||||
urlToName =
|
||||
@@ -107,40 +161,89 @@ let
|
||||
"${if matched == null then "source" else builtins.head matched}${appendShort}";
|
||||
name = urlToName url revision;
|
||||
in
|
||||
builtins.fetchGit {
|
||||
fetchGit {
|
||||
rev = revision;
|
||||
inherit name;
|
||||
# hash = hash;
|
||||
inherit url submodules;
|
||||
narHash = hash;
|
||||
|
||||
inherit name submodules url;
|
||||
};
|
||||
|
||||
mkPyPiSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchurl {
|
||||
{ fetchurl, ... }:
|
||||
{
|
||||
url,
|
||||
hash,
|
||||
...
|
||||
}:
|
||||
fetchurl {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
};
|
||||
|
||||
mkChannelSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchTarball {
|
||||
{ fetchTarball, ... }:
|
||||
{
|
||||
url,
|
||||
hash,
|
||||
...
|
||||
}:
|
||||
fetchTarball {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
};
|
||||
|
||||
mkTarballSource =
|
||||
{ fetchTarball, ... }:
|
||||
{
|
||||
url,
|
||||
locked_url ? url,
|
||||
hash,
|
||||
...
|
||||
}:
|
||||
builtins.fetchTarball {
|
||||
fetchTarball {
|
||||
url = locked_url;
|
||||
sha256 = hash;
|
||||
};
|
||||
|
||||
mkContainerSource =
|
||||
pkgs:
|
||||
{
|
||||
image_name,
|
||||
image_tag,
|
||||
image_digest,
|
||||
...
|
||||
}:
|
||||
if pkgs == null then
|
||||
builtins.throw "container sources require passing in a Nixpkgs value: https://github.com/andir/npins/blob/master/README.md#using-the-nixpkgs-fetchers"
|
||||
else
|
||||
pkgs.dockerTools.pullImage {
|
||||
imageName = image_name;
|
||||
imageDigest = image_digest;
|
||||
finalImageTag = image_tag;
|
||||
};
|
||||
in
|
||||
if version == 5 then
|
||||
builtins.mapAttrs mkSource data.pins
|
||||
else
|
||||
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"
|
||||
mkFunctor (
|
||||
{
|
||||
input ? ./sources.json,
|
||||
}:
|
||||
let
|
||||
data =
|
||||
if builtins.isPath input then
|
||||
# while `readFile` will throw an error anyways if the path doesn't exist,
|
||||
# we still need to check beforehand because *our* error can be caught but not the one from the builtin
|
||||
# *piegames sighs*
|
||||
if builtins.pathExists input then
|
||||
builtins.fromJSON (builtins.readFile input)
|
||||
else
|
||||
throw "Input path ${toString input} does not exist"
|
||||
else if builtins.isAttrs input then
|
||||
input
|
||||
else
|
||||
throw "Unsupported input type ${builtins.typeOf input}, must be a path or an attrset";
|
||||
version = data.version;
|
||||
in
|
||||
if version == 7 then
|
||||
builtins.mapAttrs (name: spec: mkFunctor (mkSource name spec)) data.pins
|
||||
else
|
||||
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ buildDotnetModule rec {
|
||||
;
|
||||
name = "Archivist";
|
||||
pname = name;
|
||||
dotnet-runtime = pkgs.dotnetCorePackages.runtime_9_0;
|
||||
dotnet-runtime = pkgs.dotnetCorePackages.runtime_10_0;
|
||||
dotnetRestoreFlags = "--force-evaluate";
|
||||
nugetDeps = deps {
|
||||
inherit
|
||||
|
||||
@@ -48,5 +48,5 @@ stdenvNoCC.mkDerivation {
|
||||
outputHashMode = "recursive";
|
||||
outputHashAlgo = "sha256";
|
||||
# NOTE: Empty this when a new dependency is added
|
||||
outputHash = "sha256-V4uM+12m2nPTb9rdv1RjNheV1ipzW2gIH7/Fq+R5OJE=";
|
||||
}
|
||||
outputHash = "sha256-bbCaGoZRE7vRuAS3eRyP8yHANYXBJVaHmuL99BAovjY=";
|
||||
}
|
||||
@@ -9,9 +9,9 @@
|
||||
},
|
||||
"branch": "main",
|
||||
"submodules": false,
|
||||
"revision": "2f0f812f69f3eb4140157fe15e12739adf82e32a",
|
||||
"url": "https://github.com/ryantm/agenix/archive/2f0f812f69f3eb4140157fe15e12739adf82e32a.tar.gz",
|
||||
"hash": "1d4m7hsq727q7ndjqmgyl8vkbkqjwps962ygmv2mcc5dbqzgn963"
|
||||
"revision": "fcdea223397448d35d9b31f798479227e80183f6",
|
||||
"url": "https://github.com/ryantm/agenix/archive/fcdea223397448d35d9b31f798479227e80183f6.tar.gz",
|
||||
"hash": "sha256-wyT7Pl6tMFbFrs8Lk/TlEs81N6L+VSybPfiIgzU8lbQ="
|
||||
},
|
||||
"nix-utils": {
|
||||
"type": "Git",
|
||||
@@ -23,13 +23,13 @@
|
||||
"submodules": false,
|
||||
"revision": "098f594425d2b9dde0657becad0f6498d074f8b3",
|
||||
"url": null,
|
||||
"hash": "0hh52w1fkpr1xx6j8cjm6g88j2352yv2ysqm1q51j59y6f583vyb"
|
||||
"hash": "sha256-y++BijM+FRkKDhVrL7YXZQiJ0DNVMiRN7yHf6QIXBUI="
|
||||
},
|
||||
"nixpkgs": {
|
||||
"type": "Channel",
|
||||
"name": "nixpkgs-unstable",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre880343.87848bf0cc4f/nixexprs.tar.xz",
|
||||
"hash": "134c1sx06gxh7a4jnf618bi4c2wa949fm14w34cjhsryqjs3a8ha"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre930822.ed142ab1b3a0/nixexprs.tar.xz",
|
||||
"hash": "sha256-XH6awru9NnBc/m+2YhRNT8r1PAKEiPGF3gs//F3ods0="
|
||||
},
|
||||
"pre-commit": {
|
||||
"type": "Git",
|
||||
@@ -40,10 +40,10 @@
|
||||
},
|
||||
"branch": "master",
|
||||
"submodules": false,
|
||||
"revision": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
|
||||
"url": "https://github.com/cachix/git-hooks.nix/archive/ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37.tar.gz",
|
||||
"hash": "11r45r45qcfv77rx024mqpra2yixnc5g248kp7rmccq09vll1y85"
|
||||
"revision": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
|
||||
"url": "https://github.com/cachix/git-hooks.nix/archive/a1ef738813b15cf8ec759bdff5761b027e3e1d23.tar.gz",
|
||||
"hash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U="
|
||||
}
|
||||
},
|
||||
"version": 5
|
||||
"version": 7
|
||||
}
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
"vite-plugin-mkcert": "^1.17.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.72.2",
|
||||
"@fluentui/react-components": "^9.72.9",
|
||||
"@fluentui/react-datepicker-compat": "^0.6.20",
|
||||
"@fluentui/react-calendar-compat": "^0.3.15",
|
||||
"@fluentui/react-timepicker-compat": "^0.4.26",
|
||||
"@fluentui-contrib/react-data-grid-react-window": "^1.4.2",
|
||||
"@fluentui/react-icons": "^2.0.316",
|
||||
"react-window": "^2.2.3",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@lit/context": "^1.1.6",
|
||||
"@microsoft/signalr": "^8.0.17",
|
||||
|
||||
101
scripts/update-rider.sh
Executable file
101
scripts/update-rider.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#! nix-shell -i bash --pure
|
||||
#! nix-shell -p bash which xmlstarlet
|
||||
|
||||
if [[ ! $# -eq 1 ]]; then
|
||||
echo "Usage: $0 <dotnet-path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dotnet_path=$1
|
||||
|
||||
function stderr() {
|
||||
echo "$@" 1>&2;
|
||||
}
|
||||
|
||||
function create_settings_file() {
|
||||
cat << EOF
|
||||
<?xml version="1.0"?>
|
||||
<wpf:ResourceDictionary
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:s="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:ss="urn:schemas-jetbrains-com:settings-storage-xaml"
|
||||
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">${1}</s:String>
|
||||
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">${2}</s:String>
|
||||
</wpf:ResourceDictionary>
|
||||
EOF
|
||||
}
|
||||
|
||||
# HACK: Configure Rider to use the correct .NET paths from an ambient .NET
|
||||
function use_rider_dotnet() {
|
||||
local solution_file=$(find . -maxdepth 1 -type f -name '*.slnx' | cut -d'.' -f2 | cut -d'/' -f2)
|
||||
local settings_file=$(find . -maxdepth 1 -type f -name '*.sln.DotSettings.user')
|
||||
# Get paths
|
||||
local cli_path=$(realpath "$dotnet_path")
|
||||
local dir=$(dirname $cli_path)
|
||||
local msbuild_path=$(find "$dir" -maxdepth 3 -type f -name MSBuild.dll)
|
||||
|
||||
# stderr "dotnet path is $dir"
|
||||
# stderr "Found msbuild: $msbuild_path"
|
||||
|
||||
if [ -f "$settings_file" ] ; then
|
||||
# stderr "Updating rider settings file: $settings_file"
|
||||
# stderr "Setting DotNetCliExePath to $cli_path"
|
||||
|
||||
# NOTE: check if dotnet binary in share folder settings exists
|
||||
xml sel -t -v "wpf:ResourceDictionary/s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" "$settings_file"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
xml ed --inplace \
|
||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||
-N ss="urn:schemas-jetbrains-com:settings-storage-xaml" \
|
||||
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue']" \
|
||||
--value "$cli_path" \
|
||||
"$settings_file"
|
||||
else
|
||||
xml ed --inplace \
|
||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||
-N ss="urn:schemas-jetbrains-com:settings-storage-xaml" \
|
||||
-s /wpf:ResourceDictionary -t elem -n s:String -v "$cli_path" \
|
||||
--var new_node '$prev' \
|
||||
-i '$new_node' -t attr -n "x:Key" -v "/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue" \
|
||||
"$settings_file"
|
||||
fi
|
||||
|
||||
xml sel -t -v "wpf:ResourceDictionary/s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" "$settings_file"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
xmlstarlet ed --inplace \
|
||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||
-N ss="urn:schemas-jetbrains-com:settings-storage-xaml" \
|
||||
--update "//s:String[@x:Key='/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue']" \
|
||||
--value "$msbuild_path" \
|
||||
"$settings_file"
|
||||
else
|
||||
xml ed --inplace \
|
||||
-N wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" \
|
||||
-N x="http://schemas.microsoft.com/winfx/2006/xaml" \
|
||||
-N s="clr-namespace:System;assembly=mscorlib" \
|
||||
-N ss="urn:schemas-jetbrains-com:settings-storage-xaml" \
|
||||
-s /wpf:ResourceDictionary -t elem -n s:String -v "$cli_path" \
|
||||
--var new_node '$prev' \
|
||||
-i '$new_node' -t attr -n "x:Key" -v "/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue" \
|
||||
"$settings_file"
|
||||
fi
|
||||
else
|
||||
create_settings_file $cli_path $msbuild_path > "$solution_file.sln.DotSettings.user"
|
||||
fi
|
||||
}
|
||||
|
||||
function main() {
|
||||
use_rider_dotnet
|
||||
}
|
||||
|
||||
main
|
||||
53
shell.nix
53
shell.nix
@@ -4,48 +4,65 @@
|
||||
pre-commit ? import ./nix/pre-commit.nix,
|
||||
}:
|
||||
let
|
||||
dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0;
|
||||
dotnet-sdk = pkgs.dotnetCorePackages.sdk_10_0;
|
||||
agenix = pkgs.callPackage "${sources.agenix}/pkgs/agenix.nix" { };
|
||||
fable = pkgs.buildDotnetGlobalTool {
|
||||
pname = "fable";
|
||||
version = "4.24.0";
|
||||
nugetHash = "sha256-ERewWqfEyyZKpHFFALpMGJT0fDWywBYY5buU/wTZZTg=";
|
||||
};
|
||||
in
|
||||
pkgs.mkShellNoCC {
|
||||
buildInputs = [ dotnet-sdk ];
|
||||
|
||||
packages = with pkgs; [
|
||||
packages = [
|
||||
# F#
|
||||
fable
|
||||
dotnet-outdated
|
||||
fantomas
|
||||
fsautocomplete
|
||||
pkgs.dotnet-outdated
|
||||
pkgs.fantomas
|
||||
pkgs.fsautocomplete
|
||||
|
||||
# JavaScript
|
||||
bun
|
||||
nodejs
|
||||
pkgs.bun
|
||||
pkgs.nodejs_25
|
||||
|
||||
# Devlopment tools
|
||||
npins
|
||||
mkcert
|
||||
dive
|
||||
nix-output-monitor
|
||||
pkgs.npins
|
||||
pkgs.mkcert
|
||||
pkgs.dive
|
||||
pkgs.nix-output-monitor
|
||||
pkgs.just
|
||||
pkgs.skopeo
|
||||
|
||||
# Secret management with agenix
|
||||
agenix
|
||||
|
||||
# Kubernetes tools
|
||||
tilt
|
||||
dapr-cli
|
||||
kustomize
|
||||
kubernetes-helm
|
||||
pkgs.tilt
|
||||
pkgs.dapr-cli
|
||||
pkgs.kustomize
|
||||
pkgs.kubernetes-helm
|
||||
];
|
||||
|
||||
# Environment variables
|
||||
DOTNET_ROOT = "${dotnet-sdk}/share/dotnet";
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT = "true";
|
||||
LOG_LEVEL = "verbose";
|
||||
NPINS_DIRECTORY = "nix";
|
||||
|
||||
shellHook = ''
|
||||
scripts/update-rider.sh ${dotnet-sdk}/bin/dotnet
|
||||
'';
|
||||
|
||||
# Alternative shells
|
||||
passthru = pkgs.lib.mapAttrs (name: value: pkgs.mkShellNoCC (value // { inherit name; })) {
|
||||
pre-commit.shellHook = pre-commit.shellHook;
|
||||
ci-shell = {
|
||||
packages = [
|
||||
pkgs.npins
|
||||
];
|
||||
shellHook = ''
|
||||
export NPINS_DIRECTORY="nix"
|
||||
'';
|
||||
};
|
||||
agenix-gen = {
|
||||
packages = [ agenix ];
|
||||
shellHook = ''
|
||||
@@ -61,4 +78,4 @@ pkgs.mkShellNoCC {
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
open Fake.Core
|
||||
open Fake.IO
|
||||
open Farmer
|
||||
open Farmer.Builders
|
||||
|
||||
open Helpers
|
||||
|
||||
initializeContext()
|
||||
|
||||
let clientPath = Path.getFullName "src/Client"
|
||||
let cliPath = Path.getFullName "src/Cli"
|
||||
let testPath = Path.getFullName "tests"
|
||||
|
||||
let distPath = Path.getFullName "dist"
|
||||
|
||||
let vite = $"bunx --bun vite -c ../../vite.config.js"
|
||||
let viteBundle = $"{vite} build --outDir {distPath}/public"
|
||||
|
||||
Target.create "Clean" (fun _ -> Shell.cleanDir distPath)
|
||||
|
||||
// Target.create "Bundle" (fun _ ->
|
||||
// let vite = $"{viteBundle} -m production"
|
||||
// run dotnet $"publish -c Release -o \"{distPath}\"" serverPath
|
||||
// run fable $"-o build/client --run {vite}" clientPath
|
||||
// )
|
||||
|
||||
// Target.create "BundleDebug" (fun _ ->
|
||||
// let vite = $"{viteBundle} -m development --minify false"
|
||||
// run dotnet $"publish -c Debug -o \"{distPath}\"" serverPath
|
||||
// run fable $"-o build/client --run {vite}" clientPath
|
||||
// )
|
||||
|
||||
Target.create "Bundle" (fun _ ->
|
||||
run dotnet $"publish -c Release -o \"{distPath}\"" cliPath
|
||||
)
|
||||
|
||||
Target.create "BundleDebug" (fun _ ->
|
||||
run dotnet $"publish -c Debug -o \"{distPath}\"" cliPath
|
||||
)
|
||||
|
||||
Target.create "Format" (fun _ ->
|
||||
run fantomas ". -r" "src"
|
||||
)
|
||||
|
||||
Target.create "Test" (fun _ ->
|
||||
if System.IO.Directory.Exists testPath then
|
||||
run dotnet "run" testPath
|
||||
else ()
|
||||
)
|
||||
|
||||
Target.create "Run" (fun _ -> Target.runOrDefault "Bundle")
|
||||
|
||||
open Fake.Core.TargetOperators
|
||||
|
||||
let dependencies = [
|
||||
"Clean"
|
||||
==> "Bundle"
|
||||
|
||||
"Clean"
|
||||
==> "BundleDebug"
|
||||
|
||||
"Clean"
|
||||
==> "Test"
|
||||
]
|
||||
|
||||
[<EntryPoint>]
|
||||
let main args = runOrDefault args
|
||||
@@ -1,116 +0,0 @@
|
||||
module Helpers
|
||||
|
||||
open Fake.Core
|
||||
|
||||
let initializeContext () =
|
||||
let execContext = Context.FakeExecutionContext.Create false "build.fsx" [ ]
|
||||
Context.setExecutionContext (Context.RuntimeContext.Fake execContext)
|
||||
|
||||
module Proc =
|
||||
module Parallel =
|
||||
open System
|
||||
|
||||
let locker = obj()
|
||||
|
||||
let colors =
|
||||
[| ConsoleColor.Blue
|
||||
ConsoleColor.Yellow
|
||||
ConsoleColor.Magenta
|
||||
ConsoleColor.Cyan
|
||||
ConsoleColor.DarkBlue
|
||||
ConsoleColor.DarkYellow
|
||||
ConsoleColor.DarkMagenta
|
||||
ConsoleColor.DarkCyan |]
|
||||
|
||||
let print color (colored: string) (line: string) =
|
||||
lock locker
|
||||
(fun () ->
|
||||
let currentColor = Console.ForegroundColor
|
||||
Console.ForegroundColor <- color
|
||||
Console.Write colored
|
||||
Console.ForegroundColor <- currentColor
|
||||
Console.WriteLine line)
|
||||
|
||||
let onStdout index name (line: string) =
|
||||
let color = colors.[index % colors.Length]
|
||||
if isNull line then
|
||||
print color $"{name}: --- END ---" ""
|
||||
else if String.isNotNullOrEmpty line then
|
||||
print color $"{name}: " line
|
||||
|
||||
let onStderr name (line: string) =
|
||||
let color = ConsoleColor.Red
|
||||
if isNull line |> not then
|
||||
print color $"{name}: " line
|
||||
|
||||
let redirect (index, (name, createProcess)) =
|
||||
createProcess
|
||||
|> CreateProcess.redirectOutputIfNotRedirected
|
||||
|> CreateProcess.withOutputEvents (onStdout index name) (onStderr name)
|
||||
|
||||
let printStarting indexed =
|
||||
for (index, (name, c: CreateProcess<_>)) in indexed do
|
||||
let color = colors.[index % colors.Length]
|
||||
let wd =
|
||||
c.WorkingDirectory
|
||||
|> Option.defaultValue ""
|
||||
let exe = c.Command.Executable
|
||||
let args = c.Command.Arguments.ToStartInfo
|
||||
print color $"{name}: {wd}> {exe} {args}" ""
|
||||
|
||||
let run cs =
|
||||
cs
|
||||
|> Seq.toArray
|
||||
|> Array.indexed
|
||||
|> fun x -> printStarting x; x
|
||||
|> Array.map redirect
|
||||
|> Array.Parallel.map Proc.run
|
||||
|
||||
let createProcess exe arg dir =
|
||||
CreateProcess.fromRawCommandLine exe arg
|
||||
|> CreateProcess.withWorkingDirectory dir
|
||||
|> CreateProcess.ensureExitCode
|
||||
|
||||
let dotnet = createProcess "dotnet"
|
||||
|
||||
let fable = createProcess "fable"
|
||||
|
||||
let fantomas = createProcess "fantomas"
|
||||
|
||||
type BundleMode =
|
||||
| Prod
|
||||
| Devel
|
||||
| Watch
|
||||
with
|
||||
override this.ToString() =
|
||||
match this with
|
||||
| Prod -> "production"
|
||||
| Devel -> "development"
|
||||
| Watch -> "watch"
|
||||
|
||||
let viteCmd (m: BundleMode) outDir =
|
||||
match m with
|
||||
| Prod -> $"vite build -c ../../vite.config.js -m {m} --emptyOutDir --outDir {outDir}/public"
|
||||
| Devel -> $"vite build -c ../../vite.config.js -m {m} --minify false --sourcemap true --emptyOutDir --outDir {outDir}/public"
|
||||
| Watch -> "vite -c ../../vite.config.js"
|
||||
|
||||
let run proc arg dir =
|
||||
proc arg dir
|
||||
|> Proc.run
|
||||
|> ignore
|
||||
|
||||
let runParallel processes =
|
||||
processes
|
||||
|> Proc.Parallel.run
|
||||
|> ignore
|
||||
|
||||
let runOrDefault args =
|
||||
try
|
||||
match args with
|
||||
| [| target |] -> Target.runOrDefault target
|
||||
| _ ->
|
||||
Target.runOrDefault "Run"
|
||||
0
|
||||
with e ->
|
||||
printfn "%A" e
|
||||
1
|
||||
@@ -1,11 +1,15 @@
|
||||
# yaml-language-server: $schema=https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json
|
||||
variables:
|
||||
SKIP_TESTS: "true"
|
||||
SKIP_TESTS: "true"
|
||||
|
||||
include:
|
||||
- project: oceanbox/gitlab-ci
|
||||
ref: v4.2
|
||||
file: DotnetDeployment.gitlab-ci.yml
|
||||
inputs:
|
||||
project-name: archivist
|
||||
project-dir: src/Cli
|
||||
- project: oceanbox/gitlab-ci
|
||||
ref: v4.5
|
||||
file: DotnetDeployment.gitlab-ci.yml
|
||||
inputs:
|
||||
project-name: archivist
|
||||
project-dir: src/Cli
|
||||
|
||||
dockerize-archivist:
|
||||
tags:
|
||||
- nix
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include=".build/Helpers.fs" />
|
||||
<Compile Include=".build/Build.fs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fake.Core.Target" />
|
||||
<PackageReference Include="Fake.DotNet.Cli" />
|
||||
<PackageReference Include="Fake.IO.FileSystem" />
|
||||
<PackageReference Include="Farmer" />
|
||||
<PackageReference Include="FSharp.Core" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
40
src/Archivist/justfile
Normal file
40
src/Archivist/justfile
Normal file
@@ -0,0 +1,40 @@
|
||||
# Archivist build commands
|
||||
# Install just: https://github.com/casey/just
|
||||
|
||||
set dotenv-load
|
||||
|
||||
src_path := "src"
|
||||
client_path := "src/Client"
|
||||
cli_path := "src/Cli"
|
||||
test_path := "tests"
|
||||
dist_path := "dist"
|
||||
|
||||
# Default recipe - show available commands
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf {{dist_path}}
|
||||
|
||||
# Build production bundle
|
||||
bundle: clean
|
||||
dotnet publish -c Release -o {{dist_path}} {{cli_path}}
|
||||
|
||||
# Build debug bundle
|
||||
bundle-debug: clean
|
||||
dotnet publish -c Debug -o {{dist_path}} {{cli_path}}
|
||||
|
||||
# Format code with Fantomas
|
||||
format:
|
||||
fantomas {{src_path}} -r
|
||||
|
||||
# Run tests
|
||||
test: clean
|
||||
#!/usr/bin/env bash
|
||||
if [ -d "{{test_path}}" ]; then
|
||||
dotnet run {{test_path}}
|
||||
fi
|
||||
|
||||
# Run (builds bundle)
|
||||
run: bundle
|
||||
@@ -29,7 +29,7 @@ pkgs.mkShellNoCC {
|
||||
SERVER_PORT = port + 85;
|
||||
TILT_PORT = port + 50;
|
||||
|
||||
DOTNET_ROOT = "${pkgs.dotnetCorePackages.sdk_9_0}/share/dotnet";
|
||||
DOTNET_ROOT = "${pkgs.dotnetCorePackages.sdk_10_0}/share/dotnet";
|
||||
|
||||
shellHook = ''
|
||||
export PATH="$PWD/src/Cli/bin/Release/net9.0/linux-x64/:$PATH"
|
||||
|
||||
@@ -631,7 +631,6 @@ let instantiateArchiveDto (idx, modelArea, basePath, files, reverse, json, publi
|
||||
}
|
||||
|
||||
let retireArchive (archive: string) =
|
||||
// TODO: retire all dependent archies
|
||||
let aid =
|
||||
try
|
||||
Guid.Parse archive
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<AssemblyName>archivist</AssemblyName>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Fargo.CmdLine": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.7.5, )",
|
||||
@@ -71,8 +71,7 @@
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.1",
|
||||
"Microsoft.Extensions.Logging": "9.0.1",
|
||||
"Mono.TextTemplating": "3.0.0",
|
||||
"System.Text.Json": "9.0.1"
|
||||
"Mono.TextTemplating": "3.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Tools": {
|
||||
@@ -233,8 +232,7 @@
|
||||
"resolved": "5.3.2",
|
||||
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
||||
"dependencies": {
|
||||
"FSharp.Core": "4.3.2",
|
||||
"System.Reflection.Emit.Lightweight": "4.3.0"
|
||||
"FSharp.Core": "4.3.2"
|
||||
}
|
||||
},
|
||||
"Google.Api.CommonProtos": {
|
||||
@@ -331,10 +329,7 @@
|
||||
"resolved": "4.8.0",
|
||||
"contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
|
||||
"System.Collections.Immutable": "7.0.0",
|
||||
"System.Reflection.Metadata": "7.0.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.CSharp": {
|
||||
@@ -364,9 +359,7 @@
|
||||
"Humanizer.Core": "2.14.1",
|
||||
"Microsoft.Bcl.AsyncInterfaces": "7.0.0",
|
||||
"Microsoft.CodeAnalysis.Common": "[4.8.0]",
|
||||
"System.Composition": "7.0.0",
|
||||
"System.IO.Pipelines": "7.0.0",
|
||||
"System.Threading.Channels": "7.0.0"
|
||||
"System.Composition": "7.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Workspaces.MSBuild": {
|
||||
@@ -376,8 +369,7 @@
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Framework": "16.10.0",
|
||||
"Microsoft.CodeAnalysis.Common": "[4.8.0]",
|
||||
"Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]",
|
||||
"System.Text.Json": "7.0.3"
|
||||
"Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": {
|
||||
@@ -534,16 +526,6 @@
|
||||
"resolved": "17.11.4",
|
||||
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETCore.Targets": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg=="
|
||||
},
|
||||
"Mono.TextTemplating": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0",
|
||||
@@ -563,11 +545,7 @@
|
||||
"ProjNET": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.0",
|
||||
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.3",
|
||||
"System.Numerics.Vectors": "4.5.0"
|
||||
}
|
||||
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
|
||||
},
|
||||
"Serilog.Sinks.File": {
|
||||
"type": "Transitive",
|
||||
@@ -591,11 +569,6 @@
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
|
||||
},
|
||||
"System.Collections.Immutable": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ=="
|
||||
},
|
||||
"System.Composition": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
@@ -644,128 +617,6 @@
|
||||
"System.Composition.Runtime": "7.0.0"
|
||||
}
|
||||
},
|
||||
"System.IO": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.IO.Pipelines": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw=="
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
|
||||
},
|
||||
"System.Reflection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Emit.ILGeneration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==",
|
||||
"dependencies": {
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Emit.Lightweight": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==",
|
||||
"dependencies": {
|
||||
"System.Reflection": "4.3.0",
|
||||
"System.Reflection.Emit.ILGeneration": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
|
||||
"dependencies": {
|
||||
"System.Collections.Immutable": "7.0.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Text.Encoding": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.1",
|
||||
"contentHash": "eqWHDZqYPv1PvuvoIIx5pF74plL3iEOZOl/0kQP+Y0TEbtgNnM2W6k8h8EPYs+LTJZsXuWa92n5W5sHTWvE3VA=="
|
||||
},
|
||||
"System.Threading.Channels": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA=="
|
||||
},
|
||||
"System.Threading.Tasks": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
@@ -911,10 +762,7 @@
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.5.0, )",
|
||||
"resolved": "2.5.0",
|
||||
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.4"
|
||||
}
|
||||
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "CentralTransitive",
|
||||
@@ -1006,136 +854,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"net9.0/linux-x64": {
|
||||
"runtime.any.System.IO": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "SDZ5AD1DtyRoxYtEcqQ3HDlcrorMYXZeCt7ZhG9US9I5Vva+gpIWDGMkcwa5XiKL0ceQKRZIX2x0XEjLX7PDzQ=="
|
||||
},
|
||||
"runtime.any.System.Reflection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "hLC3A3rI8jipR5d9k7+f0MgRCW6texsAp0MWkN/ci18FMtQ9KH7E2vDn/DH2LkxsszlpJpOn9qy6Z6/69rH6eQ=="
|
||||
},
|
||||
"runtime.any.System.Reflection.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "Nrm1p3armp6TTf2xuvaa+jGTTmncALWFq22CpmwRvhDf6dE9ZmH40EbOswD4GnFLrMRS0Ki6Kx5aUPmKK/hZBg=="
|
||||
},
|
||||
"runtime.any.System.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "fRS7zJgaG9NkifaAxGGclDDoRn9HC7hXACl52Or06a/fxdzDajWb5wov3c6a+gVSlekRoexfjwQSK9sh5um5LQ==",
|
||||
"dependencies": {
|
||||
"System.Private.Uri": "4.3.0"
|
||||
}
|
||||
},
|
||||
"runtime.any.System.Text.Encoding": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "+ihI5VaXFCMVPJNstG4O4eo1CfbrByLxRrQQTqOTp1ttK0kUKDqOdBSTaCB2IBk/QtjDrs6+x4xuezyMXdm0HQ=="
|
||||
},
|
||||
"runtime.any.System.Threading.Tasks": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "OhBAVBQG5kFj1S+hCEQ3TUHBAEtZ3fbEMgZMRNdN8A0Pj4x+5nTELEqL59DU0TjKVE6II3dqKw4Dklb3szT65w=="
|
||||
},
|
||||
"runtime.native.System": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0"
|
||||
}
|
||||
},
|
||||
"runtime.unix.System.Private.Uri": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "ooWzobr5RAq34r9uan1r/WPXJYG1XWy9KanrxNvEnBzbFdQbMG7Y3bVi4QxR7xZMNLOxLLTAyXvnSkfj5boZSg==",
|
||||
"dependencies": {
|
||||
"runtime.native.System": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.IO": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"System.Text.Encoding": "4.3.0",
|
||||
"System.Threading.Tasks": "4.3.0",
|
||||
"runtime.any.System.IO": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Private.Uri": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "I4SwANiUGho1esj4V4oSlPllXjzCZDE+5XXso2P03LW2vOda2Enzh8DWOxwN6hnrJyp314c7KuVu31QYhRzOGg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"runtime.unix.System.Private.Uri": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.IO": "4.3.0",
|
||||
"System.Reflection.Primitives": "4.3.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"runtime.any.System.Reflection": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"runtime.any.System.Reflection.Primitives": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"runtime.any.System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Encoding": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"runtime.any.System.Text.Encoding": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0",
|
||||
"Microsoft.NETCore.Targets": "1.1.0",
|
||||
"System.Runtime": "4.3.0",
|
||||
"runtime.any.System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
"net10.0/linux-x64": {}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>7.1.0</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>7.1.0</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
open Fake.Core
|
||||
open Fake.IO
|
||||
open Farmer
|
||||
open Farmer.Builders
|
||||
|
||||
open Helpers
|
||||
|
||||
initializeContext()
|
||||
|
||||
let serverPath = Path.getFullName "src/Server"
|
||||
let clientPath = Path.getFullName "src/Client"
|
||||
let testPath = Path.getFullName "test"
|
||||
let libPath = Path.getFullName "src/Interfaces" |> Some
|
||||
|
||||
let distPath = Path.getFullName "dist"
|
||||
let packPath = Path.getFullName "packages"
|
||||
let versionFile = Path.getFullName ".version"
|
||||
|
||||
let vite = """bunx --bun vite -c ../../vite.config.js"""
|
||||
|
||||
let fableOpt opts =
|
||||
$"-e .jsx -o build --test:MSBuildCracker --run {vite} build --emptyOutDir --outDir {distPath}/public {opts}"
|
||||
|
||||
let fableWatch = $"watch -e .jsx -o build --run {vite}"
|
||||
|
||||
Target.create "Clean" (fun _ -> Shell.cleanDir distPath)
|
||||
|
||||
Target.create "Bundle" (fun _ ->
|
||||
[ "server", dotnet $"build -tl -c Release -o {distPath} -p:DefineConstants=" serverPath
|
||||
"client", fable (fableOpt "-m production") clientPath ]
|
||||
|> runParallel
|
||||
)
|
||||
|
||||
Target.create "BundleDebug" (fun _ ->
|
||||
[ "server", dotnet $"build -tl -c Debug -o {distPath} -p:DefineConstants=" serverPath
|
||||
"client", fable (fableOpt "-m development --minify false --sourcemap true") clientPath ]
|
||||
|> runParallel
|
||||
)
|
||||
|
||||
Target.create "Pack" (fun _ ->
|
||||
match libPath with
|
||||
| Some p -> run dotnet $"pack -c Release -o \"{packPath}\"" p
|
||||
| None -> ()
|
||||
)
|
||||
|
||||
Target.create "Run" (fun _ ->
|
||||
[ "server", dotnet "watch run" serverPath
|
||||
"client", fable fableWatch clientPath ]
|
||||
|> runParallel
|
||||
)
|
||||
|
||||
Target.create "Client" (fun _ ->
|
||||
run fable fableWatch clientPath
|
||||
)
|
||||
|
||||
Target.create "Format" (fun _ ->
|
||||
run fantomas ". -r" "src"
|
||||
)
|
||||
|
||||
Target.create "Test" (fun _ ->
|
||||
if System.IO.Directory.Exists testPath then
|
||||
[ "server", dotnet "run" (testPath + "/Server")
|
||||
"client", fable $"-e .jsx -o build --run {vite}" (testPath + "/Client") ]
|
||||
|> runParallel
|
||||
else ()
|
||||
)
|
||||
|
||||
open Fake.Core.TargetOperators
|
||||
|
||||
let dependencies = [
|
||||
"Clean"
|
||||
==> "Bundle"
|
||||
|
||||
"Clean"
|
||||
==> "BundleDebug"
|
||||
|
||||
"Clean"
|
||||
==> "Test"
|
||||
|
||||
"Clean"
|
||||
==> "Run"
|
||||
|
||||
"Clean"
|
||||
==> "Pack"
|
||||
|
||||
"Client"
|
||||
]
|
||||
|
||||
[<EntryPoint>]
|
||||
let main args = runOrDefault args
|
||||
@@ -1,17 +1,15 @@
|
||||
# yaml-language-server: $schema=https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json
|
||||
variables:
|
||||
SKIP_TESTS: "true"
|
||||
SKIP_SINGULARITY: "true"
|
||||
SKIP_TESTS: "true"
|
||||
|
||||
include:
|
||||
- project: oceanbox/gitlab-ci
|
||||
ref: v4.2
|
||||
file: DotnetDeployment.gitlab-ci.yml
|
||||
inputs:
|
||||
project-name: atlantis
|
||||
project-dir: src/Atlantis
|
||||
- project: oceanbox/gitlab-ci
|
||||
ref: v4.5
|
||||
file: DotnetDeployment.gitlab-ci.yml
|
||||
inputs:
|
||||
project-name: atlantis
|
||||
project-dir: src/Atlantis
|
||||
|
||||
# TODO(mrtz): Create a nix-runner
|
||||
# dockerize-atlantis:
|
||||
# tags:
|
||||
# - saas-linux-large-amd64
|
||||
dockerize-atlantis:
|
||||
tags:
|
||||
- nix
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="../../.build/Helpers.fs" />
|
||||
<Compile Include=".build/Build.fs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fake.Core.Target" />
|
||||
<PackageReference Include="Fake.DotNet.Cli" />
|
||||
<PackageReference Include="Fake.IO.FileSystem" />
|
||||
<PackageReference Include="Farmer" />
|
||||
<PackageReference Include="FSharp.Core" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0.6
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev
|
||||
@@ -12,4 +12,4 @@ ENV SERVER_CONTENT_ROOT=/app/public
|
||||
COPY dist/ /app
|
||||
|
||||
WORKDIR /app
|
||||
CMD [ "dotnet", "/app/Server.dll" ]
|
||||
CMD [ "dotnet", "/app/Server.dll" ]
|
||||
@@ -60,13 +60,13 @@ k8s_yaml(namespace_inject(blob(kustomizations), namespace))
|
||||
|
||||
local_resource(
|
||||
'create-bundle',
|
||||
cmd='dotnet run bundledebug',
|
||||
cmd='just bundle-debug',
|
||||
trigger_mode=TRIGGER_MODE_MANUAL
|
||||
)
|
||||
|
||||
local_resource(
|
||||
'build-server',
|
||||
cmd='dotnet publish -o ./dist src/Server',
|
||||
cmd='just bundle-debug-server',
|
||||
deps=[
|
||||
'./src/Server',
|
||||
'./src/Shared'
|
||||
@@ -74,6 +74,10 @@ local_resource(
|
||||
ignore=[
|
||||
'src/Server/bin',
|
||||
'src/Server/obj',
|
||||
'src/Server/Archmaester/obj',
|
||||
'src/Server/Hipster/obj',
|
||||
'src/Server/Petimeter/obj',
|
||||
'src/Server/Common/obj',
|
||||
'src/Shared/bin',
|
||||
'src/Shared/obj',
|
||||
],
|
||||
@@ -84,7 +88,7 @@ local_resource(
|
||||
|
||||
local_resource(
|
||||
'run-client',
|
||||
serve_cmd='fable watch -e .jsx -o build --run vite -c ../../vite.config.js',
|
||||
serve_cmd='just run-client',
|
||||
serve_dir='./src/Client',
|
||||
links=['https://{name}.local.oceanbox.io:{port}'.format(name=name, port=clientPort)],
|
||||
resource_deps=['build-server'],
|
||||
|
||||
86
src/Atlantis/justfile
Normal file
86
src/Atlantis/justfile
Normal file
@@ -0,0 +1,86 @@
|
||||
# Atlantis build commands
|
||||
# Install just: https://github.com/casey/just
|
||||
|
||||
set dotenv-load
|
||||
|
||||
src_path := "src"
|
||||
server_path := "src/Server"
|
||||
client_path := "src/Client"
|
||||
test_path := "test"
|
||||
lib_path := "src/Interfaces"
|
||||
|
||||
dist_path := "../../dist"
|
||||
pack_path := "../../packages"
|
||||
|
||||
vite_prod := "bunx --bun vite build -c ../../vite.config.js -m production --emptyOutDir --outDir " + "../../dist/public"
|
||||
vite_dev := "bunx --bun vite build -c ../../vite.config.js -m development --minify false --sourcemap true --emptyOutDir --outDir " + "../../dist/public"
|
||||
vite := "bunx vite -c ../../vite.config.js -m development "
|
||||
|
||||
# Default recipe - show available commands
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf {{dist_path}}
|
||||
|
||||
# Build production bundle (server + client)
|
||||
[parallel]
|
||||
bundle: clean bundle-server bundle-client
|
||||
|
||||
[working-directory: 'src/Server']
|
||||
bundle-server:
|
||||
dotnet build -tl -c Release -o {{dist_path}}
|
||||
|
||||
[working-directory: 'src/Client']
|
||||
install-client:
|
||||
bun install --frozen-lockfile
|
||||
|
||||
[working-directory: 'src/Client']
|
||||
bundle-client: install-client
|
||||
|
||||
# Build debug bundle (server + client)
|
||||
[parallel]
|
||||
bundle-debug: clean bundle-debug-server bundle-debug-client
|
||||
|
||||
[working-directory: 'src/Server']
|
||||
bundle-debug-server:
|
||||
dotnet build -tl -c Debug -o {{dist_path}}
|
||||
|
||||
[working-directory: 'src/Client']
|
||||
bundle-debug-client:
|
||||
fable -e .jsx -o build --test:MSBuildCracker --run {{vite_dev}}
|
||||
|
||||
# Create NuGet package
|
||||
[working-directory: 'src/Server']
|
||||
pack: clean
|
||||
dotnet pack -c Release -o "{{pack_path}}" {{lib_path}}
|
||||
|
||||
# Run development server (watch mode)
|
||||
[parallel]
|
||||
run: clean run-server run-client
|
||||
|
||||
[working-directory: 'src/Server']
|
||||
run-server:
|
||||
dotnet watch run
|
||||
|
||||
# Run client only in watch mode
|
||||
[working-directory: 'src/Client']
|
||||
run-client: install-client
|
||||
fable watch -e .jsx -o build --test:MSBuildCracker --run {{vite}}
|
||||
|
||||
# Format code with Fantomas
|
||||
format:
|
||||
fantomas {{src_path}} -r
|
||||
|
||||
# Run tests
|
||||
[parallel]
|
||||
test: clean test-server test-client
|
||||
|
||||
[working-directory: 'src']
|
||||
test-server:
|
||||
dotnet run {{test_path}}/Server
|
||||
|
||||
[working-directory: 'src/Client']
|
||||
test-client: install-client
|
||||
fable -e .jsx -o build --run {{vite}}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<Version>2.87.0</Version>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Fable.Browser.IndexedDB": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.2.0, )",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>6.20.0</Version>
|
||||
<RootNamespace>Archivist</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<Version>2.102.0</Version>
|
||||
|
||||
@@ -74,6 +74,7 @@ importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-copy.js
|
||||
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-crosshairs.js"
|
||||
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-target.js"
|
||||
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-delete.js"
|
||||
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-color-harmony.js"
|
||||
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-deselect.js"
|
||||
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-erase.js"
|
||||
importSideEffects "@spectrum-web-components/icons-workflow/icons/sp-icon-reorder.js"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<Version>2.87.0</Version>
|
||||
|
||||
@@ -68,6 +68,7 @@ type barbTile =
|
||||
static member inline drawColor(value: string) = unbox ("drawColor", value)
|
||||
static member inline time(value: int) = unbox ("time", value)
|
||||
static member inline url(value: string) = unbox ("url", value)
|
||||
static member inline template(value: string) = unbox ("template", value)
|
||||
|
||||
module BarbTile =
|
||||
type BarbTile =
|
||||
|
||||
@@ -193,4 +193,10 @@ let plumeApi () =
|
||||
Remoting.createApi ()
|
||||
|> Remoting.withCredentials true
|
||||
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|
||||
|> Remoting.buildProxy<Api.Plume>
|
||||
|> Remoting.buildProxy<Api.Plume>
|
||||
|
||||
let xtractApi () =
|
||||
Remoting.createApi ()
|
||||
|> Remoting.withCredentials true
|
||||
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|
||||
|> Remoting.buildProxy<Api.Xtract>
|
||||
@@ -496,8 +496,8 @@ type MapLayer =
|
||||
| Networks -> "networks"
|
||||
member x.Label() =
|
||||
match x with
|
||||
| Ocean -> "Ocean"
|
||||
| Conc -> "Concentration"
|
||||
| Ocean -> "Oceanography"
|
||||
| Conc -> "Concentrations"
|
||||
| SeaDistance -> "SeaDistance"
|
||||
| GridCircle -> $"GridCircle"
|
||||
| AzeContour -> "AzeContour"
|
||||
@@ -768,21 +768,48 @@ type PlumeType =
|
||||
match this with
|
||||
| DefaultPlume -> "Plume"
|
||||
|
||||
type XtractType =
|
||||
| DefaultXtract
|
||||
override this.ToString (): string =
|
||||
match this with
|
||||
| DefaultXtract -> "xtract"
|
||||
|
||||
member this.ToLabel() =
|
||||
match this with
|
||||
| DefaultXtract -> "Xtract"
|
||||
|
||||
type XtractData = {
|
||||
name: string
|
||||
fvcom: System.Guid
|
||||
start: DateTime
|
||||
stop: DateTime
|
||||
} with
|
||||
static member empty = {
|
||||
name = ""
|
||||
fvcom = System.Guid.Empty
|
||||
start = DateTime.Now
|
||||
stop = DateTime.Now.AddDays (2)
|
||||
}
|
||||
|
||||
type SimControlKind =
|
||||
| Drifters of SimType
|
||||
| Plume of PlumeType
|
||||
| DataExtraction of XtractType
|
||||
override this.ToString (): string =
|
||||
match this with
|
||||
| Drifters simType -> string simType
|
||||
| Plume plumeType -> string plumeType
|
||||
| DataExtraction xtractType -> string xtractType
|
||||
member this.ToLabel() =
|
||||
match this with
|
||||
| Drifters simType -> simType.ToLabel ()
|
||||
| Plume plumeType -> plumeType.ToLabel ()
|
||||
| DataExtraction xtractType -> xtractType.ToLabel ()
|
||||
member this.simTypeOpt =
|
||||
match this with
|
||||
| Drifters simType -> Some simType
|
||||
| Plume plumeType -> None
|
||||
| DataExtraction xtractType -> None
|
||||
|
||||
// TODO: Not sure if anything but Mapster needs to know about this
|
||||
type SideNavMode =
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Fable.Browser.IndexedDB": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.2.0, )",
|
||||
|
||||
@@ -22,6 +22,7 @@ let getPropCM prop =
|
||||
match prop with
|
||||
| Prop.Temp -> ColorMap.Ocean "thermal"
|
||||
| Prop.Salt -> ColorMap.Ocean "haline"
|
||||
| Prop.Dens -> ColorMap.Ocean "thermal"
|
||||
| Prop.WC -> ColorMap.Ocean "curl"
|
||||
| Prop.DW _ -> ColorMap.Ocean "curl"
|
||||
| Prop.SedV2 -> ColorMap.Color16 "jet"
|
||||
@@ -41,6 +42,7 @@ let defaultColors =
|
||||
Prop.Bathy
|
||||
Prop.Temp
|
||||
Prop.Salt
|
||||
Prop.Dens
|
||||
Prop.Zeta
|
||||
Prop.Speed
|
||||
Prop.Conc2D
|
||||
|
||||
585
src/Atlantis/src/Client/Mapster/DataExtraction.fs
Normal file
585
src/Atlantis/src/Client/Mapster/DataExtraction.fs
Normal file
@@ -0,0 +1,585 @@
|
||||
module DataExtraction
|
||||
|
||||
open System
|
||||
open Browser
|
||||
open Fable.Core
|
||||
open Fable.Core.JsInterop
|
||||
open Fable.OpenLayers
|
||||
open Lit
|
||||
open Lit.Elmish
|
||||
open Maps
|
||||
open Remoting
|
||||
|
||||
open Atlantis.Types
|
||||
open Atlantis.Shared.Notification
|
||||
open Utils
|
||||
open Hipster.Job
|
||||
open Model
|
||||
open Layers
|
||||
|
||||
//
|
||||
// === Elmish ===
|
||||
//
|
||||
|
||||
type SiteIdx = int
|
||||
|
||||
type private XtractMsg =
|
||||
| SetExtractionSite of (float * float) option
|
||||
| SetData of XtractData
|
||||
| SetStarted of bool * int option
|
||||
| ResetModel of XtractModel
|
||||
| Noop of unit
|
||||
|
||||
let statusMessage (job: JobInfo) =
|
||||
match job.status with
|
||||
| JobStatus.New -> Note.info "New extraction"
|
||||
| JobStatus.Waiting -> Note.info "Waiting..."
|
||||
| JobStatus.Running -> Note.info "Running..."
|
||||
| JobStatus.Completed -> Note.success "Extraction finished"
|
||||
| JobStatus.Unknown -> Note.warn "Job status is unknown"
|
||||
| _ (*Failed*) -> Note.error "Extraction failed"
|
||||
|
||||
let private update (msg: XtractMsg) (model: XtractModel) =
|
||||
match msg with
|
||||
| SetExtractionSite pos ->
|
||||
console.debug ("[DataExtraction] SetExtractionSite msg:", pos)
|
||||
{ model with position = pos }, Elmish.Cmd.none
|
||||
| SetData s ->
|
||||
console.debug ("[DataExtraction] SetData msg:", s)
|
||||
{ model with data = s }, Elmish.Cmd.none
|
||||
| XtractMsg.SetStarted (started, jobIdOpt) -> { model with start = started, jobIdOpt }, Elmish.Cmd.none
|
||||
| XtractMsg.ResetModel m -> { m with data.name = model.data.name }, Elmish.Cmd.none
|
||||
| XtractMsg.Noop () -> model, Elmish.Cmd.none
|
||||
|
||||
//
|
||||
// === Views and components ===
|
||||
//
|
||||
|
||||
[<HookComponent>]
|
||||
let placingToggleButton (disabled: bool) (map: OlMap) (onPlace: Coordinate -> unit) =
|
||||
let placing, setPlacing = Hook.useState false
|
||||
let mapClickKey = Hook.useRef<Event.EventsKey> ()
|
||||
|
||||
let releaseClickHandler (e: Event.MapBrowserEvent) =
|
||||
onPlace e.coordinate
|
||||
setPlacing false
|
||||
|
||||
Hook.useEffectOnce (fun () ->
|
||||
Hook.createDisposable (fun () ->
|
||||
let elem = map.getTargetElement ()
|
||||
elem?style?cursor <- ""
|
||||
mapClickKey.contents |> Option.iter Observable.unByKey
|
||||
)
|
||||
)
|
||||
|
||||
Hook.useEffectOnChange (placing, crossHairSelect map mapClickKey releaseClickHandler)
|
||||
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
style="width: 300px"
|
||||
?disabled="{disabled}"
|
||||
?selected={placing}
|
||||
@click={Ev (fun _ -> setPlacing (not placing))}
|
||||
>
|
||||
<sp-icon-target slot="icon"></sp-icon-target>
|
||||
Add extraction point
|
||||
</sp-action-button>
|
||||
"""
|
||||
|
||||
/// <summary>
|
||||
/// Update the extraction site marker on the map
|
||||
/// </summary>
|
||||
let updateExtractionSite (posOpt: (float * float) option, map) =
|
||||
map
|
||||
|> updateBaseLayer
|
||||
MapLayer.SelectedReleaseGroup
|
||||
(fun baseLayer ->
|
||||
let layer = baseLayer :?> VectorLayer
|
||||
let source = layer.getSource () :?> VectorSource
|
||||
source.clear ()
|
||||
|
||||
match posOpt with
|
||||
| Some pos ->
|
||||
let p' = pos |> posToCoord
|
||||
let point =
|
||||
Geometry.point [ geometry.coordinates p'; geometry.layout GeometryLayout.XY ]
|
||||
let feature = Feature.feature [ feature.geometryOrProperties point ]
|
||||
source.addFeature (feature)
|
||||
| _ -> ()
|
||||
)
|
||||
|
||||
[<HookComponent>]
|
||||
let private extractionSiteControls (dispatch': XtractMsg -> unit) (xmodel': XtractModel) =
|
||||
let tryFence (pos: float * float) : (float * float) option =
|
||||
match xmodel'.fence with
|
||||
| None -> Some pos
|
||||
| Some pts ->
|
||||
let radius = sessionStorage["fence_radius"] |> float
|
||||
let coords' = toEpsg3857 pts[0]
|
||||
let radius' = radius * mercatorScaleFactor (snd pts[0])
|
||||
let circle = Geometry.circle coords' radius' GeometryLayout.XY
|
||||
if circle.intersectsCoordinate (pos |> posToCoord) then
|
||||
Some pos
|
||||
else
|
||||
console.error ("[DataExtraction] Trying to place extraction point outside of fencing radius")
|
||||
None
|
||||
|
||||
let handleMapPlaceExtraction (coords: Coordinate) =
|
||||
console.debug ($"[DataExtraction] Click add site: %s{coords.ToString ()}")
|
||||
coordToPos coords |> tryFence |> SetExtractionSite |> dispatch'
|
||||
|
||||
let setPosition (pos: float * float) : unit =
|
||||
Some pos |> SetExtractionSite |> dispatch'
|
||||
|
||||
let selectedPos = xmodel'.position |> Option.defaultValue (0.0, 0.0) |> toWgs84'
|
||||
|
||||
let deleteSite (_: Browser.Types.Event) = None |> SetExtractionSite |> dispatch'
|
||||
|
||||
let latitudeBox =
|
||||
let latitude = snd selectedPos
|
||||
let disabled = xmodel'.position.IsNone
|
||||
html
|
||||
$"""
|
||||
<sp-field-group vertical>
|
||||
<sp-field-label for="latitude">
|
||||
Latitude
|
||||
</sp-field-label>
|
||||
<sp-number-field
|
||||
id="latitude"
|
||||
style="width: 140px"
|
||||
size="m"
|
||||
step="0.000001"
|
||||
format-options="{formatDigits 6 6}"
|
||||
value={latitude}
|
||||
?disabled="{disabled}"
|
||||
@change={EvVal (
|
||||
unbox<float>
|
||||
>> fun v ->
|
||||
let newPos = toEpsg3857' (fst selectedPos, v)
|
||||
setPosition newPos
|
||||
)}
|
||||
>
|
||||
</sp-number-field>
|
||||
</sp-field-group>
|
||||
"""
|
||||
|
||||
let longitudeBox =
|
||||
let longitude = fst selectedPos
|
||||
let disabled = xmodel'.position.IsNone
|
||||
html
|
||||
$"""
|
||||
<sp-field-group vertical>
|
||||
<sp-field-label for="longitude">
|
||||
Longitude
|
||||
</sp-field-label>
|
||||
<sp-number-field
|
||||
id="longitude"
|
||||
style="width: 140px"
|
||||
size="m"
|
||||
step="0.000001"
|
||||
format-options="{formatDigits 6 6}"
|
||||
value={longitude}
|
||||
?disabled="{disabled}"
|
||||
@change={EvVal (
|
||||
unbox<float>
|
||||
>> fun v ->
|
||||
let newPos = toEpsg3857' (v, snd selectedPos)
|
||||
setPosition newPos
|
||||
)}
|
||||
></sp-number-field>
|
||||
</sp-field-group>
|
||||
"""
|
||||
|
||||
let siteDisplay =
|
||||
match xmodel'.position with
|
||||
| Some _ ->
|
||||
let lon, lat = selectedPos
|
||||
html
|
||||
$"""
|
||||
<div style="padding-top: 10px; padding-bottom: 10px">
|
||||
<sp-divider style="width: 300px"></sp-divider>
|
||||
</div>
|
||||
<div style="padding-top: 5px">
|
||||
<sp-field-label>Extraction Point</sp-field-label>
|
||||
<div style="display: flex; gap: 8px; padding-top: 5px">
|
||||
<sp-action-button style="width: 110px">
|
||||
{lat |> sprintf "%.6f"}
|
||||
</sp-action-button>
|
||||
<sp-action-button style="width: 110px">
|
||||
{lon |> sprintf "%.6f"}
|
||||
</sp-action-button>
|
||||
<sp-action-button
|
||||
style="width: 35px"
|
||||
@click={Ev deleteSite}
|
||||
>
|
||||
<sp-icon-delete slot="icon"></sp-icon-delete>
|
||||
<sp-tooltip placement="right" self-managed>Remove point</sp-tooltip>
|
||||
</sp-action-button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
| None -> Lit.nothing
|
||||
|
||||
html
|
||||
$"""
|
||||
<sp-field-group vertical>
|
||||
<sp-field-group horizontal style="padding-top: 5px; padding-bottom: 20px">
|
||||
{latitudeBox}
|
||||
{longitudeBox}
|
||||
</sp-field-group>
|
||||
</sp-field-group>
|
||||
|
||||
<div style="padding-top: 10px; padding-bottom: 5px">
|
||||
{placingToggleButton xmodel'.position.IsSome xmodel'.openLayersMap handleMapPlaceExtraction}
|
||||
</div>
|
||||
|
||||
{siteDisplay}
|
||||
"""
|
||||
|
||||
[<HookComponent>]
|
||||
let private extractionControls (dispatch': XtractMsg -> unit) (xmodel': XtractModel) =
|
||||
html
|
||||
$"""
|
||||
<sp-accordion-item class="extraction-site" ?open={true} label="Extraction point">
|
||||
{extractionSiteControls dispatch' xmodel'}
|
||||
</sp-accordion-item>
|
||||
"""
|
||||
|
||||
[<HookComponent>]
|
||||
let controls xtractType (dispatch: Msg -> unit) (model: Model) =
|
||||
let archive = model.archive
|
||||
let xtractModelOpt = model.xtractModelOpt
|
||||
let map = model.map
|
||||
let currentFrame = model.frame
|
||||
let archiveStartUTC = archive.startTime.ToUniversalTime ()
|
||||
let archiveEndT =
|
||||
archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
|
||||
let archiveStartT =
|
||||
archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
|
||||
let submitted, setSubmitted = Hook.useState false
|
||||
|
||||
let createNewModel () : XtractModel =
|
||||
let data =
|
||||
match xtractModelOpt with
|
||||
| Some existing -> { existing.data with fvcom = archive.id }
|
||||
| None -> {
|
||||
XtractData.empty with
|
||||
fvcom = archive.id
|
||||
start = archiveStartT
|
||||
stop = archiveStartT.AddDays 2.0
|
||||
}
|
||||
{
|
||||
fence = archive.polygon
|
||||
start = false, None
|
||||
kind = xtractType
|
||||
data = data
|
||||
position = None
|
||||
openLayersMap = map
|
||||
}
|
||||
|
||||
let xmodel', dispatch' =
|
||||
Hook.useElmish (
|
||||
(fun () ->
|
||||
let xmodel' =
|
||||
match xtractModelOpt with
|
||||
| Some existingModel -> existingModel
|
||||
| None -> createNewModel ()
|
||||
xmodel', Elmish.Cmd.none
|
||||
),
|
||||
update
|
||||
)
|
||||
|
||||
let modelRef = Hook.useRef (Some xmodel')
|
||||
|
||||
Hook.useEffectOnChange (
|
||||
xmodel',
|
||||
fun newModel ->
|
||||
modelRef.contents <- Some newModel
|
||||
SetXtractModel (Some newModel) |> dispatch
|
||||
)
|
||||
|
||||
|
||||
let setStartDateTime (dt: DateTime) =
|
||||
// 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 endDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
|
||||
let startDate = xmodel'.data.start
|
||||
let newStart = if endDate <= startDate then endDate.AddDays(-1.0) else startDate
|
||||
console.debug ("[DataExtraction] setStopDateTime:", endDate, "start:", newStart)
|
||||
SetData { xmodel'.data with stop = endDate; start = newStart } |> dispatch'
|
||||
|
||||
let setName (s: string) =
|
||||
let currentModel = modelRef.contents |> Option.defaultValue xmodel'
|
||||
SetData { currentModel.data with name = s } |> dispatch'
|
||||
|
||||
let minStartDate = archiveStartUTC
|
||||
let maxStartDate = archiveEndT
|
||||
let minEndDate = archiveStartUTC
|
||||
// maxEndDate is one year after the selected start date
|
||||
let maxEndDate =
|
||||
let startDate = xmodel'.data.start
|
||||
startDate.AddYears(1)
|
||||
|
||||
let metaControls =
|
||||
html
|
||||
$"""
|
||||
<div style="margin: 10px; padding-bottom: 5px;">
|
||||
<sp-field-label>
|
||||
Name (required)
|
||||
</sp-field-label>
|
||||
<sp-field-group horizontal id="vw" style="padding-bottom: 5px">
|
||||
<sp-textfield
|
||||
id="xtract-name"
|
||||
label="Name"
|
||||
placeholder="extraction-name"
|
||||
required="true"
|
||||
value="{xmodel'.data.name}"
|
||||
@change={EvVal (setName)}
|
||||
style="width: 300px"
|
||||
></sp-textfield>
|
||||
</sp-field-group>
|
||||
</div>
|
||||
|
||||
<div class="grow m-8">
|
||||
<div style="padding-bottom: 5px; padding-top: 20px">
|
||||
<sp-field-group style="flex-grow: 1;" vertical>
|
||||
<sp-field-label size="s" for="vertical">
|
||||
Start date
|
||||
</sp-field-label>
|
||||
<sp-field-group horizontal style="padding-bottom: 10px;">
|
||||
{FluentUI.Lit.DatePicker (false, xmodel'.data.start, setStartDateTime, minStartDate, maxStartDate)}
|
||||
</sp-field-group>
|
||||
</sp-field-group>
|
||||
|
||||
<sp-field-group style="flex-grow: 1;" vertical>
|
||||
<sp-field-label size="s" for="vertical">
|
||||
End date
|
||||
</sp-field-label>
|
||||
<sp-field-group horizontal style="padding-bottom: 10px;">
|
||||
{FluentUI.Lit.DatePicker (false, xmodel'.data.stop, setStopDateTime, minEndDate, maxEndDate)}
|
||||
</sp-field-group>
|
||||
</sp-field-group>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
let submit _ =
|
||||
if archive.id <> Guid.Empty && xmodel'.position.IsSome then
|
||||
let data' = xmodel'.data
|
||||
let pos' = xmodel'.position.Value |> toWgs84'
|
||||
let id = Guid.NewGuid ()
|
||||
|
||||
let payload: XtractPayload = {
|
||||
id = id
|
||||
name = data'.name
|
||||
archiveId = archive.id
|
||||
positions = { Lat = snd pos'; Long = fst pos' }
|
||||
start = data'.start
|
||||
stop = data'.stop
|
||||
basePath = "" // Set by server
|
||||
caseName = "" // Set by server
|
||||
projection = "" // Set by server
|
||||
files = [||] // Set by server
|
||||
}
|
||||
|
||||
console.log $"\n-------------- Data Extraction input ----------------"
|
||||
console.log $"Name: {data'.name}"
|
||||
console.log $"%A{payload}"
|
||||
|
||||
let api = xtractApi ()
|
||||
async {
|
||||
let! job = api.startXtract payload None
|
||||
setSubmitted true
|
||||
|
||||
do
|
||||
Lib.Umami.track (
|
||||
"mapster-submit-extraction",
|
||||
{|
|
||||
archiveId = string archive.id
|
||||
archiveName = string archive.name
|
||||
status = if job.IsSome then "success" else "failed"
|
||||
|}
|
||||
)
|
||||
|
||||
match job with
|
||||
| None ->
|
||||
Note.failure "[DataExtraction] Job submission failed"
|
||||
|> SetNotification
|
||||
|> dispatch
|
||||
|
||||
XtractMsg.SetStarted (true, Some 0) |> dispatch'
|
||||
| Some j ->
|
||||
j |> statusMessage |> SetNotification |> dispatch
|
||||
|
||||
if
|
||||
j.status = JobStatus.Waiting
|
||||
|| j.status = JobStatus.Running
|
||||
|| j.status = JobStatus.Completed
|
||||
|| j.status = JobStatus.New
|
||||
then
|
||||
XtractMsg.SetStarted (true, Some j.jobId) |> dispatch'
|
||||
|
||||
let msg: Petimeter.Inbox.InboxItem = {
|
||||
id = j.archiveId
|
||||
content =
|
||||
Thoth.Json.Encode.Auto.toString<JobMessage> (
|
||||
{
|
||||
aid = j.archiveId
|
||||
job = j.jobId
|
||||
name = j.name
|
||||
status = j.status
|
||||
}
|
||||
: JobMessage
|
||||
)
|
||||
unread = true
|
||||
type' = Petimeter.Inbox.MessageType.Xtract
|
||||
created = DateTime.Now
|
||||
}
|
||||
|
||||
msg
|
||||
|> Atlantis.Shared.Hub.InboxMsg.Post
|
||||
|> Atlantis.Shared.Hub.Action.Inbox
|
||||
|> HubMsg
|
||||
|> dispatch
|
||||
}
|
||||
|> Async.StartImmediate
|
||||
|
||||
let reset (_: Browser.Types.Event) =
|
||||
console.debug ("[DataExtraction] Reset extraction")
|
||||
clearFeatures map MapLayer.SelectedReleaseGroup
|
||||
|
||||
createNewModel () |> XtractMsg.ResetModel |> dispatch'
|
||||
|
||||
let cancel (_: Browser.Types.Event) =
|
||||
console.debug ("[DataExtraction] Cancel extraction")
|
||||
clearFeatures map MapLayer.SelectedReleaseGroup
|
||||
|
||||
modelRef.contents <- None
|
||||
SetXtractModel None |> dispatch
|
||||
SetMode Mode.Moot |> dispatch
|
||||
|
||||
let submitButtons =
|
||||
let noName = String.IsNullOrWhiteSpace (xmodel'.data.name)
|
||||
let noSite = xmodel'.position.IsNone
|
||||
|
||||
html
|
||||
$"""
|
||||
<div
|
||||
id="extraction-submit-controls"
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin: 2px;
|
||||
padding: 5px;
|
||||
"
|
||||
>
|
||||
<sp-action-group
|
||||
horizontal
|
||||
compact
|
||||
size="m"
|
||||
>
|
||||
<sp-action-button
|
||||
static="primary"
|
||||
style="width: 100px;"
|
||||
?disabled={noName || noSite || submitted}
|
||||
@click={Ev (submit)}
|
||||
>
|
||||
Submit
|
||||
</sp-action-button>
|
||||
<sp-action-button
|
||||
static="primary"
|
||||
style="width: 100px;"
|
||||
@click={Ev (reset)}
|
||||
>
|
||||
Reset
|
||||
</sp-action-button>
|
||||
<sp-action-button
|
||||
static="primary"
|
||||
style="width: 100px;"
|
||||
@click={Ev (cancel)}
|
||||
>
|
||||
Cancel
|
||||
</sp-action-button>
|
||||
</sp-action-group>
|
||||
</div>
|
||||
"""
|
||||
|
||||
Hook.useEffectOnce (fun () ->
|
||||
console.debug ("[DataExtraction] === mounting ===")
|
||||
|
||||
Hook.createDisposable (fun () ->
|
||||
console.log "[DataExtraction] Leaving extraction controls"
|
||||
modelRef.contents |> SetXtractModel |> dispatch
|
||||
)
|
||||
)
|
||||
|
||||
Hook.useEffectOnChange (
|
||||
xmodel',
|
||||
fun newModel ->
|
||||
console.debug ("[DataExtraction] Model changed", newModel)
|
||||
modelRef.contents <- Some newModel
|
||||
)
|
||||
|
||||
Hook.useEffectOnChange (
|
||||
xmodel'.position,
|
||||
fun posOpt ->
|
||||
console.debug ("[DataExtraction] Position changed", posOpt)
|
||||
updateExtractionSite (posOpt, xmodel'.openLayersMap) |> ignore
|
||||
)
|
||||
|
||||
Hook.useEffectOnChange (
|
||||
xmodel'.start,
|
||||
fun (updatedStarted, _) ->
|
||||
if updatedStarted then
|
||||
console.log ("[DataExtraction] Extraction started: resetting")
|
||||
do clearFeatures map MapLayer.SelectedReleaseGroup
|
||||
Msg.SetSideNavMode OceanControls |> dispatch
|
||||
modelRef.contents <- None
|
||||
)
|
||||
|
||||
let measuresHeight =
|
||||
tryGetElemRect "measures-controls"
|
||||
|> Option.map _.height
|
||||
|> Option.defaultValue 80
|
||||
|
||||
html
|
||||
$"""
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 95%%;
|
||||
"
|
||||
>
|
||||
<h3>Data Extraction</h3>
|
||||
<div
|
||||
style="
|
||||
width: 100%%;
|
||||
max-height: calc(100%% - ({measuresHeight}px));
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
border-top: 1px solid #eaeaea;
|
||||
"
|
||||
>
|
||||
<sp-accordion
|
||||
allow-multiple
|
||||
size="s"
|
||||
density="spacious"
|
||||
>
|
||||
{metaControls}
|
||||
{extractionControls dispatch' xmodel'}
|
||||
</sp-accordion>
|
||||
</div>
|
||||
</div>
|
||||
{submitButtons}
|
||||
"""
|
||||
@@ -183,7 +183,7 @@ let fetchDrifters (api: ArchivesApi) (aid: Guid) : SimArchive [] JS.Promise =
|
||||
let active = [||]
|
||||
let active' =
|
||||
active
|
||||
|> Array.map (fun x ->
|
||||
|> Array.map (fun (x: JobInfo) ->
|
||||
{
|
||||
Archive =
|
||||
{ ArchiveProps.empty with
|
||||
@@ -1863,7 +1863,7 @@ module private Deposition =
|
||||
let coordsString, setCoordsString = Hook.useState ""
|
||||
console.debug("[Drifters.SimControls] defaultSite :", defaultSite)
|
||||
|
||||
let maxSitesGroup = 20
|
||||
let maxSitesGroup = 1000
|
||||
|
||||
let nSitesGroup =
|
||||
xmodel'.selectedGroup
|
||||
@@ -2272,7 +2272,7 @@ module private Deposition =
|
||||
</div>
|
||||
"""
|
||||
|
||||
let sitesRow idx site =
|
||||
let sitesRow idx (site: ReleaseSite) =
|
||||
let lon, lat = site.position |> toWgs84'
|
||||
html
|
||||
$"""
|
||||
@@ -2781,9 +2781,6 @@ let private setupMetaParams disable (maxDurationHr: float) (simulation: Simulati
|
||||
let r' = not simulation.reverse
|
||||
{ simulation with reverse = r' } |> setSimModel
|
||||
|
||||
let startDateKey = "start-date"
|
||||
let endDateKey = "end-date"
|
||||
|
||||
// Calculate min/max dates with buffer
|
||||
let minStartDate = archiveStart
|
||||
let maxStartDate = archiveEnd - TimeSpan.FromDays(simulation.simDays)
|
||||
@@ -2817,7 +2814,7 @@ let private setupMetaParams disable (maxDurationHr: float) (simulation: Simulati
|
||||
Start date
|
||||
</sp-field-label>
|
||||
<sp-field-group horizontal style="padding-bottom: 10px;">
|
||||
{FluentUI.Lit.DatePicker (startDateKey, disable, simulation.startTime, setStartT, minStartDate, maxStartDate)}
|
||||
{FluentUI.Lit.DatePicker (disable, simulation.startTime, setStartT, minStartDate, maxStartDate)}
|
||||
</sp-field-group>
|
||||
</sp-field-group>
|
||||
|
||||
@@ -2826,7 +2823,7 @@ let private setupMetaParams disable (maxDurationHr: float) (simulation: Simulati
|
||||
End date
|
||||
</sp-field-label>
|
||||
<sp-field-group horizontal style="padding-bottom: 10px;">
|
||||
{FluentUI.Lit.DatePicker (endDateKey, disable, simulation.startTime.AddDays(simulation.simDays), setEndT, minEndDate, maxEndDate)}
|
||||
{FluentUI.Lit.DatePicker (disable, simulation.startTime.AddDays(simulation.simDays), setEndT, minEndDate, maxEndDate)}
|
||||
</sp-field-group>
|
||||
</sp-field-group>
|
||||
</div>
|
||||
@@ -2875,14 +2872,14 @@ let private setupReleaseSites dispatch' (xmodel': DrifterModel) =
|
||||
let coordsString, setCoordsString = Hook.useState ""
|
||||
console.debug("[Drifters.SimControls] defaultSite :", defaultSite)
|
||||
|
||||
let maxSites =
|
||||
match xmodel'.simulation.kind with
|
||||
| DepositionSim -> 40
|
||||
| WaterContactSim -> 10
|
||||
| LiceSim -> 10
|
||||
| VirusSim -> 10
|
||||
| TransportSim -> 1
|
||||
| DownwellingSim -> 1
|
||||
let maxSites = 1000
|
||||
// match xmodel'.simulation.kind with
|
||||
// | DepositionSim -> 40
|
||||
// | WaterContactSim -> 20
|
||||
// | LiceSim -> 10
|
||||
// | VirusSim -> 10
|
||||
// | TransportSim -> 10
|
||||
// | DownwellingSim -> 1
|
||||
|
||||
let nSites =
|
||||
xmodel'.release.groups.Values
|
||||
@@ -3468,7 +3465,7 @@ let private liceControls dispatch' xmodel' =
|
||||
|
||||
[<HookComponent>]
|
||||
let private depositionControls dispatch' xmodel' =
|
||||
let maxDays = (30.0, 730.0) // Max simdays for hourly/daily cohorts
|
||||
let maxDays = (30.0, 10_000.0) // Max simdays for hourly/daily cohorts
|
||||
html
|
||||
$"""
|
||||
<sp-accordion-item class="simulation-release-groups" ?open={true} label="Release groups">
|
||||
@@ -3489,7 +3486,7 @@ let private depositionControls dispatch' xmodel' =
|
||||
"""
|
||||
|
||||
let private waterContactControls dispatch' xmodel' =
|
||||
let maxDays = (30.0, 180.0) // Max simdays for hourly/daily cohorts
|
||||
let maxDays = (30.0, 10_000.0) // Max simdays for hourly/daily cohorts
|
||||
html
|
||||
$"""
|
||||
<sp-accordion-item class="simulation-release-groups" ?open={true} label="Release sites">
|
||||
@@ -3732,14 +3729,14 @@ let simulationControls (simType: SimType) (dispatch: Msg -> unit) (model: Model)
|
||||
///////////////
|
||||
|
||||
// Absolute max duration
|
||||
let maxDurationDays =
|
||||
match xmodel'.simulation.kind with
|
||||
| DepositionSim -> 731.
|
||||
| WaterContactSim -> 366.
|
||||
| DownwellingSim -> 366.
|
||||
| LiceSim -> 366.
|
||||
| VirusSim -> 366.
|
||||
| TransportSim -> 30.
|
||||
let maxDurationDays = 10_000.
|
||||
// match xmodel'.simulation.kind with
|
||||
// | DepositionSim -> 731.
|
||||
// | WaterContactSim -> 366.
|
||||
// | DownwellingSim -> 366.
|
||||
// | LiceSim -> 366.
|
||||
// | VirusSim -> 366.
|
||||
// | TransportSim -> 30.
|
||||
|
||||
let maxDurationHr =
|
||||
let buffer =
|
||||
@@ -3872,6 +3869,7 @@ let simulationControls (simType: SimType) (dispatch: Msg -> unit) (model: Model)
|
||||
name = j.name
|
||||
}
|
||||
Status = status
|
||||
Reverse = input.simulation.reverse |> Option.defaultValue false
|
||||
}
|
||||
)
|
||||
|> SetTmpDrifter
|
||||
|
||||
@@ -7,9 +7,6 @@ module private ReactLib =
|
||||
open Feliz
|
||||
open System
|
||||
|
||||
let webLightTheme: obj = import "webLightTheme" "@fluentui/react-components"
|
||||
|
||||
import "DayOfWeek" "@fluentui/react-calendar-compat"
|
||||
import "DatePicker" "@fluentui/react-datepicker-compat"
|
||||
|
||||
import "FluentProvider" "@fluentui/react-components"
|
||||
@@ -18,7 +15,6 @@ module private ReactLib =
|
||||
[<JSX.Component>]
|
||||
let DatePicker
|
||||
(
|
||||
componentKey: 'T,
|
||||
disabled: bool,
|
||||
value: DateTime,
|
||||
onSelectDate: DateTime -> unit,
|
||||
@@ -81,9 +77,12 @@ module private ReactLib =
|
||||
|
||||
JSX.html
|
||||
$"""
|
||||
<FluentProvider theme={webLightTheme}>
|
||||
<FluentProvider theme={Lib.webLightTheme}>
|
||||
<Field>
|
||||
<DatePicker
|
||||
firstDayOfWeek={DayOfWeek.Monday}
|
||||
inlinePopup
|
||||
firstDayOfWeek={1}
|
||||
showGoToToday={false}
|
||||
showWeekNumbers={true}
|
||||
disabled={disabled}
|
||||
value={internalValue}
|
||||
@@ -91,15 +90,11 @@ module private ReactLib =
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
/>
|
||||
</Field>
|
||||
</FluentProvider>
|
||||
"""
|
||||
|
||||
module Lit =
|
||||
open Lit
|
||||
open System
|
||||
|
||||
let DatePicker<'T> =
|
||||
React.toLit (fun (key, disabled, value, onSelectDate, minDate: DateTime, maxDate: DateTime) ->
|
||||
ReactLib.DatePicker (key, disabled, value, onSelectDate, minDate, maxDate)
|
||||
|> Lib.React.fromJsx
|
||||
)
|
||||
let DatePicker = React.toLit (ReactLib.DatePicker >> Lib.React.fromJsx)
|
||||
@@ -43,16 +43,16 @@ let inboxDialog
|
||||
let table = document.getElementById "inbox-table"
|
||||
async {
|
||||
let! mbox = Remoting.inboxApi().getMessages ()
|
||||
// if mbox.Length = 0 then
|
||||
// table?items <- [| {
|
||||
// id = Guid.Empty
|
||||
// content = ""
|
||||
// unread = false
|
||||
// type' = MessageType.Note
|
||||
// created = DateTime.Now
|
||||
// } |]
|
||||
// else
|
||||
table?items <- mbox
|
||||
if mbox.Length = 0 then
|
||||
table?items <- [| {
|
||||
id = Guid.Empty
|
||||
content = ""
|
||||
unread = false
|
||||
type' = MessageType.Note
|
||||
created = DateTime.Now
|
||||
} |]
|
||||
else
|
||||
table?items <- mbox
|
||||
} |> Async.StartImmediate
|
||||
|
||||
Hook.useEffectOnChange(arg.unread, loadMessages)
|
||||
@@ -65,30 +65,18 @@ let inboxDialog
|
||||
|> Set.ofSeq
|
||||
|> setSelected
|
||||
|
||||
let doDelete _ =
|
||||
let doDelete selected _ =
|
||||
let table = document.getElementById "inbox-table"
|
||||
let selectedSet : Guid JS.Set = table?selectedSet
|
||||
let items: InboxItem array = table?items
|
||||
async {
|
||||
let toDelete =
|
||||
items
|
||||
|> Array.filter (fun item -> selectedSet.has(item.id))
|
||||
|> Array.map (fun item -> item.id)
|
||||
|
||||
console.debug("Deleting", toDelete.Length, "messages")
|
||||
items
|
||||
|> Array.filter (fun item -> Set.contains item.id selected)
|
||||
|> Array.iter (fun item ->
|
||||
console.log("Delete: %A", item.content)
|
||||
do arg.deleteMessage item.id
|
||||
)
|
||||
|
||||
for id in toDelete do
|
||||
arg.deleteMessage id
|
||||
|
||||
// Clear selection immediately
|
||||
selectedSet.clear()
|
||||
setSelected Set.empty
|
||||
|
||||
// Wait a bit for server to process, then reload
|
||||
do! Async.Sleep 200
|
||||
let! mbox = Remoting.inboxApi().getMessages ()
|
||||
table?items <- mbox
|
||||
} |> Async.StartImmediate
|
||||
loadMessages ()
|
||||
|
||||
let doRead selected _ =
|
||||
let table = document.getElementById "inbox-table"
|
||||
@@ -118,7 +106,7 @@ let inboxDialog
|
||||
html $"""
|
||||
<sp-field-group horizontal>
|
||||
<sp-action-button
|
||||
@click={Ev(doDelete)}
|
||||
@click={Ev(doDelete selected)}
|
||||
?disabled={selected.Count = 0}>
|
||||
<sp-icon-delete slot="icon"></sp-icon-delete> Delete selected
|
||||
</sp-action-button>
|
||||
@@ -148,6 +136,7 @@ let inboxDialog
|
||||
match item.type' with
|
||||
| MessageType.Progress -> ()
|
||||
| MessageType.Plume
|
||||
| MessageType.Xtract
|
||||
| MessageType.Drifters ->
|
||||
let job = decodeJobMessage item.content
|
||||
doRead (Set.singleton item.id) ()
|
||||
@@ -180,6 +169,7 @@ let inboxDialog
|
||||
let renderItem item =
|
||||
let message, inactive =
|
||||
match item.type' with
|
||||
| MessageType.Xtract
|
||||
| MessageType.Plume ->
|
||||
let job = decodeJobMessage item.content
|
||||
renderJobMessage job |> formatMsg item.unread false, false
|
||||
@@ -227,11 +217,16 @@ let inboxDialog
|
||||
| _ -> formatMsg item.unread false item.content, false
|
||||
|
||||
let downloadButton =
|
||||
let disabled = not (item.type' = MessageType.Plume)
|
||||
let disabled = not (item.type' = MessageType.Plume || item.type' = MessageType.Xtract)
|
||||
let sorcerer = sessionStorage["sorcerer_url"]
|
||||
let name = (decodeJobMessage item.content).name
|
||||
let job = decodeJobMessage item.content
|
||||
let downloadUrl =
|
||||
match item.type' with
|
||||
| MessageType.Plume -> $"{sorcerer}/download/plume/{arg.aid}/{item.id}/plume"
|
||||
| MessageType.Xtract -> $"{sorcerer}/download/xtract/{arg.aid}/{job.job}/{job.name}"
|
||||
| _ -> ""
|
||||
html $"""
|
||||
<a href="{sorcerer}/download/plume/{arg.aid}/{item.id}/plume">
|
||||
<a href="{downloadUrl}">
|
||||
<sp-action-button ?disabled={disabled} @click={Ev(_.stopPropagation())}>
|
||||
<sp-icon-download slot="icon"></sp-icon-download>
|
||||
</sp-action-button>
|
||||
@@ -301,11 +296,8 @@ let inboxDialog
|
||||
let sortFn = if sortDir = "asc" then Array.sortBy else Array.sortByDescending
|
||||
table?items
|
||||
|> sortFn (fun item -> JS.expr_js $"{item}[{sortKey}]")
|
||||
|> fun items -> table?items <- items))
|
||||
|
||||
let table =
|
||||
html $"""
|
||||
"""
|
||||
|> fun items -> table?items <- items)
|
||||
)
|
||||
|
||||
html $"""
|
||||
<div class="inbox-dialog">
|
||||
|
||||
@@ -897,7 +897,12 @@ let reRenderStreams model : unit =
|
||||
[<HookComponent>]
|
||||
let oceanControls dispatch model =
|
||||
let disabled = model.archive.id = Guid.Empty
|
||||
|
||||
let stats = not model.stats.showInstant
|
||||
let noTemp = stats && (model.statsAvailable |> Array.contains StatProp.Temperature |> not)
|
||||
let noSalt = stats && (model.statsAvailable |> Array.contains StatProp.Salinity |> not)
|
||||
let noSpeed = stats && (model.statsAvailable |> Array.contains StatProp.Speed |> not)
|
||||
|
||||
let prop = model.glLayers[Ocean]
|
||||
let setProp p (ev: Types.Event) =
|
||||
ev.stopPropagation()
|
||||
@@ -956,9 +961,9 @@ let oceanControls dispatch model =
|
||||
</sp-field-label>
|
||||
<sp-radio-group id="model-views" selected="{string prop.PropType}" vertical>
|
||||
<sp-radio value="{Prop.Map}" @change={Ev(setProp Prop.Map)}>Map</sp-radio>
|
||||
<sp-radio ?disabled={disabled} value="{Prop.Temp}" @change={Ev(setProp Prop.Temp)}>Temperature</sp-radio>
|
||||
<sp-radio ?disabled={disabled} value="{Prop.Salt}" @change={Ev(setProp Prop.Salt)}>Salinity</sp-radio>
|
||||
<sp-radio ?disabled={disabled} value="{Prop.Speed}" @change={Ev(setProp Prop.Speed)}>Speed</sp-radio>
|
||||
<sp-radio ?disabled={disabled || noTemp} value="{Prop.Temp}" @change={Ev(setProp Prop.Temp)}>Temperature</sp-radio>
|
||||
<sp-radio ?disabled={disabled || noSalt} value="{Prop.Salt}" @change={Ev(setProp Prop.Salt)}>Salinity</sp-radio>
|
||||
<sp-radio ?disabled={disabled || noSpeed} value="{Prop.Speed}" @change={Ev(setProp Prop.Speed)}>Speed</sp-radio>
|
||||
<sp-radio ?disabled={disabled || stats} value="{Prop.Zeta}" @change={Ev(setProp Prop.Zeta)}>Elevation</sp-radio>
|
||||
<sp-radio ?disabled={disabled || stats} value="{Prop.Bathy}" @change={Ev(setProp Prop.Bathy)}>Depth</sp-radio>
|
||||
</sp-radio-group>
|
||||
@@ -1372,10 +1377,13 @@ let activeLayerSelector (dispatch: Msg -> unit) (model: Model) =
|
||||
|> Some
|
||||
else
|
||||
None)
|
||||
|
||||
html
|
||||
$"""
|
||||
<sp-radio-group vertical selected="{string model.activeLayer}">
|
||||
{layers}
|
||||
</sp-radio-group>
|
||||
"""
|
||||
if Seq.length layers > 1 then
|
||||
html
|
||||
$"""
|
||||
<sp-field-label for="model-color-options">Layer</sp-field-label>
|
||||
<sp-radio-group vertical selected="{string model.activeLayer}">
|
||||
{layers}
|
||||
</sp-radio-group>
|
||||
"""
|
||||
else
|
||||
Lit.nothing
|
||||
@@ -220,14 +220,14 @@ let initializeArchive model =
|
||||
|
||||
// Wind barbs
|
||||
let windBarbSource =
|
||||
let url = "/api/v2/atmo/Wind/WindTile"
|
||||
let url = "https://sorcerer.vtn.oceanbox.io/api/v2/atmo/Wind/WindTile"
|
||||
console.debug ("initializeArchive windbarb source url:", url)
|
||||
BarbTile.barbTile [
|
||||
barbTile.archive aid
|
||||
barbTile.arrows model.arrows
|
||||
barbTile.arrowsPerTile model.arrowsPerTile
|
||||
barbTile.time model.frame
|
||||
barbTile.url url
|
||||
barbTile.template url
|
||||
]
|
||||
let windBarbLayer =
|
||||
Layer.tileLayer [
|
||||
@@ -250,6 +250,9 @@ let initializeArchive model =
|
||||
// TODO(simkir): add existing release sites to release layer
|
||||
// #6 is the wireframe, but it is created after being loaded from indexeddb
|
||||
|
||||
let statsApi = StatsApi (getDataUrl ())
|
||||
let! stats = statsApi.FvStatsInfo.GetAvailableStats aid
|
||||
|
||||
let prop = {
|
||||
PropType = Prop.Map
|
||||
FieldKind = UndefinedField
|
||||
@@ -265,6 +268,7 @@ let initializeArchive model =
|
||||
grid = g
|
||||
uvs = uvFlat
|
||||
mode = Mode.Ocean
|
||||
statsAvailable = stats
|
||||
glLayers = Map.add Ocean prop model.glLayers
|
||||
}
|
||||
}
|
||||
@@ -1535,21 +1539,34 @@ let update cmd model =
|
||||
|
||||
updated, Cmd.none
|
||||
| TimeSeries ->
|
||||
let updated =
|
||||
let exists = ProbeView.props TimeSeries |> Array.contains probing.prop
|
||||
if exists then
|
||||
probing
|
||||
else
|
||||
{ probing with prop = Prop.Temp }
|
||||
|
||||
let stats = Stats.Utils.fromProp probing.prop
|
||||
|
||||
{
|
||||
model with
|
||||
isLoading = None
|
||||
probePoint = probing
|
||||
probePoint = updated
|
||||
stats.propType = stats
|
||||
stats.metrics = if resetMetrics then [||] else model.stats.metrics
|
||||
},
|
||||
Cmd.none
|
||||
| RosePlots ->
|
||||
let updated =
|
||||
let exists = ProbeView.props RosePlots |> Array.contains probing.prop
|
||||
if exists then
|
||||
probing
|
||||
else
|
||||
{ probing with prop = Prop.Speed }
|
||||
{
|
||||
model with
|
||||
isLoading = None
|
||||
probePoint = probing
|
||||
probePoint = updated
|
||||
stats.propType = Current
|
||||
stats.metrics = if resetMetrics then [||] else model.stats.metrics
|
||||
},
|
||||
@@ -1572,7 +1589,7 @@ let update cmd model =
|
||||
| SetStats stats ->
|
||||
let updated = { stats with propType = Stats.Utils.fromProp model.probePoint.prop }
|
||||
{ model with stats = updated }, Cmd.none
|
||||
| SetStatsAvailable stats -> { model with statsAvailable = Some stats }, Cmd.none
|
||||
| SetStatsAvailable stats -> { model with statsAvailable = stats }, Cmd.none
|
||||
| SetStatShowInstant showInstant ->
|
||||
let updated = { model with isLoading = Some MapLoading.Progress; stats.showInstant = showInstant }
|
||||
updated, Cmd.OfPromise.perform updateOceanProp updated SetProp
|
||||
@@ -1795,6 +1812,9 @@ let update cmd model =
|
||||
| SetPlumeModel modelOpt ->
|
||||
console.debug ("[Mapster] SetPlumeModel:", modelOpt)
|
||||
{ model with plumeModelOpt = modelOpt }, Cmd.none
|
||||
| SetXtractModel modelOpt ->
|
||||
console.debug ("[Mapster] SetXtractModel:", modelOpt)
|
||||
{ model with xtractModelOpt = modelOpt }, Cmd.none
|
||||
| HubMsg msg -> { model with hubAction = Some msg }, Cmd.none
|
||||
| Noop _ -> model, Cmd.none
|
||||
|
||||
@@ -1873,7 +1893,7 @@ let fetchArchive (archiveId: System.Guid) : ArchiveInfo Async =
|
||||
polygon = a.polygon |> Option.map (Array.map (fun (x, y) -> float x, float y))
|
||||
frames = archiveFrames
|
||||
}
|
||||
| Result.Error err ->
|
||||
| Error err ->
|
||||
console.error $"Could not retrieve the selected archive!: {err}"
|
||||
return ArchiveInfo.empty
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<Version>2.87.0</Version>
|
||||
@@ -37,6 +37,7 @@
|
||||
<Compile Include="Fiskeridir.fs" />
|
||||
<Compile Include="BarentsWatch.fs" />
|
||||
<Compile Include="Plume.fs" />
|
||||
<Compile Include="DataExtraction.fs" />
|
||||
<Compile Include="Drifters.fs" />
|
||||
<Compile Include="ContourModel.fs" />
|
||||
<Compile Include="Postdrift.fs" />
|
||||
|
||||
@@ -99,14 +99,14 @@ type ProbeView =
|
||||
static member props view : Prop array =
|
||||
match view with
|
||||
| DepthProfile -> [| Prop.Temp; Prop.Salt; Prop.Speed; Prop.Dens |]
|
||||
| TimeSeries -> [| Prop.Temp; Prop.Salt; Prop.Speed; Prop.Zeta |]
|
||||
| TimeSeries -> [| Prop.Temp; Prop.Salt; Prop.Speed; Prop.Zeta; Prop.Dens |]
|
||||
| RosePlots -> [| Prop.Speed |]
|
||||
|
||||
type Probing = {
|
||||
point: (float * float) option
|
||||
idx: GridIdx array option
|
||||
view: ProbeView
|
||||
// TODO: I don't think this is needed anymore
|
||||
/// TODO: I don't think this is needed anymore
|
||||
series: bool
|
||||
timeUnit: TimeUnit
|
||||
selectedDepths: Map<int, bool>
|
||||
@@ -242,6 +242,15 @@ type PlumeModel = {
|
||||
openLayersMap: OlMap
|
||||
}
|
||||
|
||||
type XtractModel = {
|
||||
fence: (float * float) array option
|
||||
start: bool * int option
|
||||
data: XtractData
|
||||
kind: XtractType
|
||||
position: (float * float) option
|
||||
openLayersMap: OlMap
|
||||
}
|
||||
|
||||
type ContourData = (float * float)[][]
|
||||
type ContourStyle = {
|
||||
lineWidth: float
|
||||
@@ -350,10 +359,11 @@ type Model = {
|
||||
// choose what stats to consider. Could of course just wrap these two in another structure. Or, perhaps of saving
|
||||
// the available stats here in the model, the sidebar could be responsible and store the avaiable stats in local
|
||||
// store instead.
|
||||
statsAvailable: StatProp array option
|
||||
statsAvailable: StatProp array
|
||||
stats: Stats
|
||||
|
||||
plumeModelOpt: PlumeModel option
|
||||
xtractModelOpt: XtractModel option
|
||||
infectionNetwork: NetworkState
|
||||
|
||||
customGrid: CircleGrid option
|
||||
@@ -455,8 +465,9 @@ type Model = {
|
||||
inboxUnread = 0
|
||||
hubAction = None
|
||||
plumeModelOpt = None
|
||||
xtractModelOpt = None
|
||||
infectionNetwork = NetworkState.empty
|
||||
statsAvailable = None
|
||||
statsAvailable = Array.empty
|
||||
stats = Stats.empty
|
||||
}
|
||||
|
||||
@@ -484,6 +495,7 @@ type Msg =
|
||||
| DeleteArchive of System.Guid
|
||||
| CancelJob of int
|
||||
| SetPlumeModel of PlumeModel option
|
||||
| SetXtractModel of XtractModel option
|
||||
| ShowReleases of bool
|
||||
|
||||
// Map / Layers
|
||||
|
||||
@@ -3,12 +3,15 @@ module Navigation
|
||||
open System
|
||||
|
||||
open Browser
|
||||
open Fable.Core
|
||||
open Fable.Core.JsInterop
|
||||
open Fable.OpenLayers
|
||||
open Lit
|
||||
|
||||
open Remoting
|
||||
open Archmaester.Dto
|
||||
open Atlantis.Types
|
||||
open Atlantis.Shared
|
||||
open Sorcerer.Types
|
||||
open Colors
|
||||
open Drifters.ApiTypes
|
||||
@@ -17,6 +20,13 @@ open Layers
|
||||
open Maps
|
||||
open Model
|
||||
|
||||
|
||||
let private noStatsBanner () =
|
||||
html
|
||||
$"""
|
||||
<sp-alert-banner ?open={true}>This archive has no available statistics</sp-alert-banner>
|
||||
"""
|
||||
|
||||
let private accountMenu dispatch model =
|
||||
let handleActionMenu (ev: Types.Event) =
|
||||
match ev.target.Value with
|
||||
@@ -90,28 +100,30 @@ let private simAccordion (dispatch: Msg -> unit) model =
|
||||
console.debug $"policies: %A{model.simPolicies}"
|
||||
let disabled = model.archive.id = Guid.Empty
|
||||
|
||||
// TODO(mrtz): Create custom policy for plumes, for now just inherit from drifters.
|
||||
let disabledPlume = model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
|
||||
let disabledTransport = model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
|
||||
let disabledDeposition = model.simPolicies |> Array.contains (DriftersPolicy.SubmitSedimentation false)
|
||||
// TODO(mrtz): Create custom policy for plume and xtract, for now just inherit from drifters.
|
||||
let disabledPlume =
|
||||
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
|
||||
let disabledXtract =
|
||||
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
|
||||
let disabledTransport =
|
||||
model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
|
||||
let disabledDeposition =
|
||||
model.simPolicies |> Array.contains (DriftersPolicy.SubmitSedimentation false)
|
||||
|
||||
let chooseMode (kind: SimControlKind) _ =
|
||||
// NOTE: Once you have commited to creating a new simulation, clear whatever sims you have chosen
|
||||
UnsetSelectedDrifter () |> dispatch
|
||||
|
||||
SimControls kind
|
||||
|> SetSideNavMode
|
||||
|> dispatch
|
||||
SimControls kind |> SetSideNavMode |> dispatch
|
||||
|
||||
do Lib.Umami.track $"mapster-enter-{string kind}-simulation"
|
||||
|
||||
Mode.Simulation Placing
|
||||
|> SetMode
|
||||
|> dispatch
|
||||
Mode.Simulation Placing |> SetMode |> dispatch
|
||||
|
||||
html
|
||||
$"""
|
||||
<div class="full-box flex-column" style="align-items: center;">
|
||||
<h3>Particle Simulations</h3>
|
||||
<h3>Simulations</h3>
|
||||
|
||||
<sp-action-group
|
||||
vertical
|
||||
@@ -167,6 +179,22 @@ let private simAccordion (dispatch: Msg -> unit) model =
|
||||
Plume
|
||||
</sp-action-button>
|
||||
</sp-action-group>
|
||||
|
||||
<h3>Data Extraction</h3>
|
||||
<sp-action-group
|
||||
vertical
|
||||
size="m"
|
||||
style="width: 90%%;"
|
||||
>
|
||||
<sp-action-button
|
||||
static="primary"
|
||||
style="flex-grow: 1"
|
||||
?disabled={disabledXtract }
|
||||
@click={Ev (chooseMode (DataExtraction DefaultXtract))}
|
||||
>
|
||||
Extract Data
|
||||
</sp-action-button>
|
||||
</sp-action-group>
|
||||
</div>
|
||||
"""
|
||||
|
||||
@@ -217,7 +245,7 @@ let private aquacultureLookup (onClick: int option -> unit) =
|
||||
"""
|
||||
|
||||
[<HookComponent>]
|
||||
let private mapsSelect (model: Model) (dispatch: Msg -> unit) =
|
||||
let private mapsSelect (model: Model) (dispatch: Msg -> unit) =
|
||||
Hook.useHmr hmr
|
||||
|
||||
let items =
|
||||
@@ -232,20 +260,20 @@ let private mapsSelect (model: Model) (dispatch: Msg -> unit) =
|
||||
NorgesKart BackgroundNorway
|
||||
OSM
|
||||
|]
|
||||
|> Array.map (fun map -> !!{| value = string map; label = map.ToLabel(); |})
|
||||
|> Array.map (fun map -> !!{| value = string map; label = map.ToLabel () |})
|
||||
|
||||
let handleChange (ev: Types.Event) (data: obj) =
|
||||
console.debug("[Nav] Map layer changed: %o", data)
|
||||
let value : string = data?value
|
||||
console.debug ("[Nav] Map layer changed: %o", data)
|
||||
let value: string = data?value
|
||||
match MapKind.OfString value with
|
||||
| Some mapKind -> SetMapKind mapKind |> dispatch
|
||||
| None -> console.error("[Nav] Got invalid map layer: %s", value)
|
||||
| None -> console.error ("[Nav] Got invalid map layer: %s", value)
|
||||
|
||||
html
|
||||
$"""
|
||||
<div style="padding-top: 10px">
|
||||
<sp-field-label for="map-layer-picker">Map type</sp-field-label>
|
||||
{FluentUI.Lit.Select(string model.mapKind, items, handleChange)}
|
||||
{FluentUI.Lit.Select (string model.mapKind, items, handleChange)}
|
||||
</div>
|
||||
"""
|
||||
|
||||
@@ -306,17 +334,17 @@ let private GraphRangeSlider (model: Model) dispatch =
|
||||
let rangeLow, rangeHigh = model.probePoint.propRange
|
||||
|
||||
let handleInputChange (ev: Types.Event) =
|
||||
ev.stopPropagation()
|
||||
console.debug("[Nav] Graph slider input: %o", ev)
|
||||
ev.stopPropagation ()
|
||||
console.debug ("[Nav] Graph slider input: %o", ev)
|
||||
let target = ev.target
|
||||
let value = target.Value
|
||||
if target?name = "high" then
|
||||
console.debug("[Nav] High value is: %s", value)
|
||||
console.debug ("[Nav] High value is: %s", value)
|
||||
let newHigh = fst model.probePoint.propRange, unbox value
|
||||
SetProbing { model.probePoint with propRange = newHigh } |> dispatch
|
||||
elif target?name = "low" then
|
||||
let value = target.Value
|
||||
console.debug("[Nav] Low value is: %s", value)
|
||||
console.debug ("[Nav] Low value is: %s", value)
|
||||
let newLow = unbox value, snd model.probePoint.propRange
|
||||
SetProbing { model.probePoint with propRange = newLow } |> dispatch
|
||||
else
|
||||
@@ -324,21 +352,21 @@ let private GraphRangeSlider (model: Model) dispatch =
|
||||
// console.error("[Nav] Error")
|
||||
()
|
||||
let handleChangeEvent (ev: Types.Event) =
|
||||
ev.stopPropagation()
|
||||
console.debug("[Nav] Graph slider change: %o", ev)
|
||||
ev.stopPropagation ()
|
||||
console.debug ("[Nav] Graph slider change: %o", ev)
|
||||
let target = ev.target
|
||||
let value = target.Value
|
||||
if target?name = "high" then
|
||||
console.debug("[Nav] High value changed: %s", value)
|
||||
console.debug ("[Nav] High value changed: %s", value)
|
||||
let newHigh = fst model.probePoint.propRange, unbox value
|
||||
SetProbing { model.probePoint with propRange = newHigh } |> dispatch
|
||||
elif target?name = "low" then
|
||||
let value = target.Value
|
||||
console.debug("[Nav] Low value changed: %s", value)
|
||||
console.debug ("[Nav] Low value changed: %s", value)
|
||||
let newLow = unbox value, snd model.probePoint.propRange
|
||||
SetProbing { model.probePoint with propRange = newLow } |> dispatch
|
||||
else
|
||||
console.error("[Nav] Error")
|
||||
console.error ("[Nav] Error")
|
||||
|
||||
html
|
||||
$"""
|
||||
@@ -368,14 +396,14 @@ let private GraphRangeSlider (model: Model) dispatch =
|
||||
"""
|
||||
|
||||
[<HookComponent>]
|
||||
let private RosePlotControls (probePoint: Probing) (stats: Stats) dispatch =
|
||||
let private RosePlotControls (statsAvailable: bool) (probePoint: Probing) (stats: Stats) dispatch =
|
||||
Hook.useHmr hmr
|
||||
|
||||
let handleDepthsChange (depths: Map<int, bool>) =
|
||||
console.debug("[RosePlotControls] Depths changed: %s", Map.toArray depths)
|
||||
console.debug ("[RosePlotControls] Depths changed: %s", Map.toArray depths)
|
||||
SetProbing { probePoint with selectedDepths = depths } |> dispatch
|
||||
let handlePeriodChange (period: Sorcerer.Types.Period) =
|
||||
console.debug("[RosePlotControls] period changed: %s", string period)
|
||||
console.debug ("[RosePlotControls] period changed: %s", string period)
|
||||
SetStatPeriod period |> dispatch
|
||||
|
||||
// TODO: Download button
|
||||
@@ -398,9 +426,13 @@ let private RosePlotControls (probePoint: Probing) (stats: Stats) dispatch =
|
||||
|
||||
<sp-accordion>
|
||||
<sp-accordion-item label="Statistics" open>
|
||||
{Stats.Controls.PeriodSelection stats.period handlePeriodChange}
|
||||
{if statsAvailable then
|
||||
Stats.Controls.PeriodSelection stats.period handlePeriodChange
|
||||
else
|
||||
noStatsBanner ()}
|
||||
</sp-accordion-item>
|
||||
</sp-accordion>
|
||||
|
||||
</div>
|
||||
"""
|
||||
|
||||
@@ -411,13 +443,14 @@ let private TimeSeriesControls (model: Model) dispatch =
|
||||
let selectedProp: string = model.probePoint.prop |> string
|
||||
|
||||
let selectedTimeUnit = string model.probePoint.timeUnit
|
||||
|
||||
let yearMeanSelected =
|
||||
match model.stats.metrics with
|
||||
| [| Mean |] -> true
|
||||
| _ -> false
|
||||
|
||||
let handlePropChange (ev: Types.Event) =
|
||||
// NOTE: Stop button group change event from propagating up to tab event handler......
|
||||
// NOTE: Stop button group change event from propagating up to tab event handler...
|
||||
ev.stopPropagation ()
|
||||
|
||||
let target = ev.target
|
||||
@@ -426,7 +459,10 @@ let private TimeSeriesControls (model: Model) dispatch =
|
||||
let propOpt = selected |> Array.tryHead |> Option.map Prop.fromString
|
||||
|
||||
propOpt
|
||||
|> Option.iter (fun prop -> SetProbing { model.probePoint with prop = prop } |> dispatch)
|
||||
|> Option.iter (fun prop ->
|
||||
SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange }
|
||||
|> dispatch
|
||||
)
|
||||
|
||||
let handleUnitChange (ev: Types.Event) =
|
||||
ev.stopPropagation ()
|
||||
@@ -439,11 +475,10 @@ let private TimeSeriesControls (model: Model) dispatch =
|
||||
if yearMeanExists then
|
||||
SetStatMetrics Mean |> dispatch
|
||||
|
||||
let handleDepthsChange newDepths =
|
||||
SetProbeDepths newDepths |> dispatch
|
||||
let handleDepthsChange newDepths = SetProbeDepths newDepths |> dispatch
|
||||
|
||||
let handleYearMeanChange (ev: Types.Event) =
|
||||
ev.stopPropagation()
|
||||
ev.stopPropagation ()
|
||||
SetStatMetrics Mean |> dispatch
|
||||
|
||||
let timeSeriesSelectors _ =
|
||||
@@ -499,6 +534,38 @@ let private TimeSeriesControls (model: Model) dispatch =
|
||||
</div>
|
||||
"""
|
||||
|
||||
let showDepth =
|
||||
match model.probePoint.prop with
|
||||
| Prop.Zeta -> false
|
||||
| _ -> true
|
||||
let disableMean =
|
||||
match model.probePoint.prop with
|
||||
| Prop.Zeta
|
||||
| Prop.Dens -> true
|
||||
| _ -> false
|
||||
let statsAvailable = (Array.isEmpty >> not) model.statsAvailable
|
||||
|
||||
let statsAccordion () =
|
||||
html
|
||||
$"""
|
||||
<sp-field-label for="metric-selector">
|
||||
Metrics
|
||||
</sp-field-label>
|
||||
<sp-field-group id="metric-selector" horizontal>
|
||||
<sp-checkbox
|
||||
value="year-mean"
|
||||
?disabled={disableMean}
|
||||
?checked={yearMeanSelected}
|
||||
@change={Ev handleYearMeanChange}
|
||||
>
|
||||
Year mean
|
||||
</sp-checkbox>
|
||||
<sp-help-text slot="help-text">
|
||||
Currently, only year averages are available for time series probing.
|
||||
</sp-help-text>
|
||||
</sp-field-group>
|
||||
"""
|
||||
|
||||
// TODO: Download button
|
||||
html
|
||||
$"""
|
||||
@@ -515,6 +582,9 @@ let private TimeSeriesControls (model: Model) dispatch =
|
||||
<sp-action-button value="{string Prop.Salt}">
|
||||
Salinity
|
||||
</sp-action-button>
|
||||
<sp-action-button value="{string Prop.Dens}">
|
||||
Density
|
||||
</sp-action-button>
|
||||
<sp-action-button value="{string Prop.Speed}">
|
||||
Speed
|
||||
</sp-action-button>
|
||||
@@ -526,29 +596,18 @@ let private TimeSeriesControls (model: Model) dispatch =
|
||||
<div class="flex-row" style="gap: 24px;">
|
||||
{timeSeriesSelectors ()}
|
||||
|
||||
{if model.probePoint.selectedDepths.IsEmpty then
|
||||
Lit.nothing
|
||||
{if showDepth then
|
||||
PropertyPlots.DepthSelectors false model.probePoint.selectedDepths handleDepthsChange
|
||||
else
|
||||
PropertyPlots.DepthSelectors false model.probePoint.selectedDepths handleDepthsChange}
|
||||
Lit.nothing}
|
||||
</div>
|
||||
|
||||
<sp-accordion>
|
||||
<sp-accordion-item label="Statistics">
|
||||
<sp-field-label for="metric-selector">
|
||||
Metrics
|
||||
</sp-field-label>
|
||||
<sp-field-group id="metric-selector" horizontal>
|
||||
<sp-checkbox
|
||||
value="year-mean"
|
||||
?checked={yearMeanSelected}
|
||||
@change={Ev handleYearMeanChange}
|
||||
>
|
||||
Year mean
|
||||
</sp-checkbox>
|
||||
<sp-help-text slot="help-text">
|
||||
Currently, only year averages are available for time series probing.
|
||||
</sp-help-text>
|
||||
</sp-field-group>
|
||||
{if statsAvailable then
|
||||
statsAccordion ()
|
||||
else
|
||||
noStatsBanner ()}
|
||||
</sp-accordion-item>
|
||||
</sp-accordion>
|
||||
</div>
|
||||
@@ -561,21 +620,18 @@ let private OceanPlotControls model dispatch =
|
||||
let selectedProp: string = model.probePoint.prop |> string
|
||||
|
||||
let handlePropChange (ev: Types.Event) =
|
||||
// NOTE: Stop button group change event from propagating up to tab event handler......
|
||||
// NOTE: Stop button group change event from propagating up to tab event handler...
|
||||
ev.stopPropagation ()
|
||||
let target = ev.target
|
||||
let selected: string array = target?selected
|
||||
console.debug ("[PropetryPlots] Selected Prop changed %o", selected)
|
||||
let propOpt = selected |> Array.tryHead |> Option.map Prop.fromString
|
||||
|
||||
if propOpt.IsSome then
|
||||
let prop = propOpt.Value
|
||||
SetProbing {
|
||||
model.probePoint with
|
||||
prop = prop
|
||||
propRange = prop.viewRange
|
||||
}
|
||||
propOpt
|
||||
|> Option.iter (fun prop ->
|
||||
SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange }
|
||||
|> dispatch
|
||||
)
|
||||
|
||||
html
|
||||
$"""
|
||||
@@ -592,45 +648,41 @@ let private OceanPlotControls model dispatch =
|
||||
<sp-action-button value="{string Prop.Salt}">
|
||||
Salinity
|
||||
</sp-action-button>
|
||||
<sp-action-button value="{string Prop.Speed}">
|
||||
Speed
|
||||
</sp-action-button>
|
||||
<sp-action-button value="{string Prop.Dens}">
|
||||
Density
|
||||
</sp-action-button>
|
||||
<sp-action-button value="{string Prop.Speed}">
|
||||
Speed
|
||||
</sp-action-button>
|
||||
</sp-action-group>
|
||||
|
||||
{GraphRangeSlider model dispatch}
|
||||
|
||||
<sp-accordion>
|
||||
<sp-accordion-item label="Statistics">
|
||||
{if model.archive.id <> Guid.Empty then Stats.Controls.View dispatch model else Lit.nothing}
|
||||
{if model.archive.id <> Guid.Empty then
|
||||
Stats.Controls.View dispatch model
|
||||
else
|
||||
Lit.nothing}
|
||||
</sp-accordion-item>
|
||||
</sp-accordion>
|
||||
|
||||
{PropertyPlots.dlButton model.archive.id model.frame model.probePoint}
|
||||
</div>
|
||||
"""
|
||||
|
||||
[<HookComponent>]
|
||||
let LinePlotControls dispatch (model: Model) =
|
||||
let handlePropChange (ev: Types.Event) =
|
||||
console.debug("[Nav] Line plot prop changed: %o", ev)
|
||||
let selected : string array = ev.target?selected
|
||||
console.debug ("[Nav] Line plot prop changed: %o", ev)
|
||||
let selected: string array = ev.target?selected
|
||||
let opt = selected |> Array.tryHead
|
||||
|
||||
match opt with
|
||||
| Some propStr ->
|
||||
let prop = Prop.fromString propStr
|
||||
console.debug("[Nav] Line plot selected new prop: %s", prop)
|
||||
SetProbing {
|
||||
model.probePoint with
|
||||
prop = prop
|
||||
propRange = prop.viewRange
|
||||
}
|
||||
console.debug ("[Nav] Line plot selected new prop: %s", prop)
|
||||
SetProbing { model.probePoint with prop = prop; propRange = prop.viewRange }
|
||||
|> dispatch
|
||||
| None ->
|
||||
console.error("[Nav] Unexpected selected from action-group")
|
||||
| None -> console.error ("[Nav] Unexpected selected from action-group")
|
||||
|
||||
html
|
||||
$"""
|
||||
@@ -734,22 +786,21 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
let selectedStats = if model.stats.showInstant then 1 else 2
|
||||
|
||||
let handleStatsTabChange (ev: Types.Event) =
|
||||
ev.stopPropagation()
|
||||
console.debug("[Stats] Stats tab changed: %o", ev)
|
||||
let selectedStr : string = ev.target?selected
|
||||
console.debug("[Stats] Selected is: %o", selectedStr)
|
||||
ev.stopPropagation ()
|
||||
console.debug ("[Stats] Stats tab changed: %o", ev)
|
||||
let selectedStr: string = ev.target?selected
|
||||
console.debug ("[Stats] Selected is: %o", selectedStr)
|
||||
let selected = int selectedStr
|
||||
|
||||
// TODO: Enum?
|
||||
console.debug("[Stats] Stats tab changed to: %d", selected)
|
||||
console.debug ("[Stats] Stats tab changed to: %d", selected)
|
||||
match selected with
|
||||
| 1 ->
|
||||
SetStatShowInstant true |> dispatch
|
||||
| 1 -> SetStatShowInstant true |> dispatch
|
||||
| 2 ->
|
||||
ShowStreams false |> dispatch
|
||||
ShowWindBarbs false |> dispatch
|
||||
SetStatShowInstant false |> dispatch
|
||||
| _ -> console.error("[Stats] Invalid stats tab (%d) selected. Somehow...", selected)
|
||||
| _ -> console.error ("[Stats] Invalid stats tab (%d) selected. Somehow...", selected)
|
||||
|
||||
let selectedTab =
|
||||
match model.probePoint.view with
|
||||
@@ -765,25 +816,31 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
| _ -> console.error "simControls: Coordinate in wrong format!"
|
||||
|
||||
let handleTabChange (ev: Types.Event) =
|
||||
console.debug("[Nav] Probe tab changed: %o", ev)
|
||||
let selectedStr : string = ev.target?selected
|
||||
console.debug("[Nav] Selected is: %o", selectedStr)
|
||||
console.debug ("[Nav] Probe tab changed: %o", ev)
|
||||
let selectedStr: string = ev.target?selected
|
||||
console.debug ("[Nav] Selected is: %o", selectedStr)
|
||||
let selected = int selectedStr
|
||||
|
||||
// TODO: Enum?
|
||||
console.debug("[Nav] Probe tab changed to: %d", selected)
|
||||
console.debug ("[Nav] Probe tab changed to: %d", selected)
|
||||
match selected with
|
||||
| 1 -> SetProbing { model.probePoint with view = DepthProfile; series = false } |> dispatch
|
||||
| 2 -> SetProbing { model.probePoint with view = TimeSeries; series = true } |> dispatch
|
||||
| 3 -> SetProbing { model.probePoint with view = RosePlots; series = false } |> dispatch
|
||||
| _ -> console.error("[Nav] Invalid probe tab (%d) selected. Somehow...", selected)
|
||||
| 1 ->
|
||||
SetProbing { model.probePoint with view = DepthProfile; series = false }
|
||||
|> dispatch
|
||||
| 2 ->
|
||||
SetProbing { model.probePoint with view = TimeSeries; series = true }
|
||||
|> dispatch
|
||||
| 3 ->
|
||||
SetProbing { model.probePoint with view = RosePlots; series = false }
|
||||
|> dispatch
|
||||
| _ -> console.error ("[Nav] Invalid probe tab (%d) selected. Somehow...", selected)
|
||||
|
||||
let handleStatPeriodChange (newPeriod: Period) =
|
||||
console.debug("[Nav] Stat period changed %s", string newPeriod)
|
||||
console.debug ("[Nav] Stat period changed %s", string newPeriod)
|
||||
SetStatPeriod newPeriod |> dispatch
|
||||
|
||||
let handleStatMetricChange (newMetric: StatMetric) =
|
||||
console.debug("[Nav] Stat metric changed %s", newMetric)
|
||||
console.debug ("[Nav] Stat metric changed %s", newMetric)
|
||||
// TODO: Uhm, yeah. Add a RemoveStatMetric msg, I guess. Or, StatMetricRemove and StatMetricAdd
|
||||
let existing = model.stats.metrics |> Array.tryHead
|
||||
if existing.IsSome then
|
||||
@@ -847,8 +904,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
match model.mode with
|
||||
| Mode.Simulation SimMode.Placing -> setGridding true
|
||||
| _ -> setGridding false
|
||||
| None ->
|
||||
setGridding false
|
||||
| None -> setGridding false
|
||||
)
|
||||
|
||||
// If you are hovering over a vector feature on the release layer, aka the release points, remove the event
|
||||
@@ -899,8 +955,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
let activeControls () =
|
||||
let pointPlotMode = model.probePoint.point.IsSome && model.probePoint.idx.IsSome
|
||||
let linePlotMode = (Array.isEmpty >> not) model.pickLine
|
||||
// TODO: This needs to be fetching in the model on loading archive
|
||||
let statsAvailable = model.statsAvailable |> Option.map (Array.isEmpty >> not) |> Option.defaultValue false
|
||||
let statsAvailable = (Array.isEmpty >> not) model.statsAvailable
|
||||
|
||||
match model.sideNavMode with
|
||||
| OceanControls ->
|
||||
@@ -914,7 +969,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
<sp-tab label="Rose plots" value="3"></sp-tab>
|
||||
<sp-tab-panel value="1">{OceanPlotControls model dispatch}</sp-tab-panel>
|
||||
<sp-tab-panel value="2">{TimeSeriesControls model dispatch}</sp-tab-panel>
|
||||
<sp-tab-panel value="3">{RosePlotControls model.probePoint model.stats dispatch}</sp-tab-panel>
|
||||
<sp-tab-panel value="3">{RosePlotControls statsAvailable model.probePoint model.stats dispatch}</sp-tab-panel>
|
||||
</sp-tabs>
|
||||
"""
|
||||
|
||||
@@ -924,7 +979,8 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
elif linePlotMode then
|
||||
LinePlotControls dispatch model
|
||||
else
|
||||
let divider () = html $"""<sp-divider size="s"></sp-divider>"""
|
||||
let divider () =
|
||||
html $"""<sp-divider size="s"></sp-divider>"""
|
||||
let stats () =
|
||||
match model.selectedDrifters with
|
||||
| Some _ -> divider ()
|
||||
@@ -932,6 +988,8 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
let metric = model.stats.metrics |> Array.tryHead |> Option.defaultValue Mean
|
||||
if model.stats.showInstant then
|
||||
Lit.nothing
|
||||
elif not statsAvailable then
|
||||
noStatsBanner ()
|
||||
else
|
||||
html
|
||||
$"""
|
||||
@@ -1009,7 +1067,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
</sp-tabs>
|
||||
</div>
|
||||
|
||||
{stats () (* TODO: if statsAvailable then stats () else divider () *)}
|
||||
{stats ()}
|
||||
|
||||
{noAnalysisText ()}
|
||||
|
||||
@@ -1023,13 +1081,16 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
let simSelector (dispatch: Msg -> unit) (model: Model) =
|
||||
match model.mode with
|
||||
| Mode.Simulation Placing ->
|
||||
match kind.simTypeOpt with
|
||||
| Some simType ->
|
||||
match kind with
|
||||
| Drifters simType ->
|
||||
console.log $"drifter simControls : {kind}"
|
||||
Drifters.simulationControls simType dispatch model
|
||||
| None ->
|
||||
| Plume _ ->
|
||||
console.log $"plume simControls : {kind}"
|
||||
Plume.simulationControls DefaultPlume dispatch model
|
||||
| DataExtraction xtractType ->
|
||||
console.log $"extraction simControls : {kind}"
|
||||
DataExtraction.controls xtractType dispatch model
|
||||
| Mode.Moot
|
||||
| Mode.Ocean
|
||||
| Mode.Stats _
|
||||
@@ -1058,8 +1119,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
| AnalysisControls archive ->
|
||||
let simSelector (dispatch: Msg -> unit) (model: Model) =
|
||||
match model.mode with
|
||||
| Mode.Simulation Placing ->
|
||||
Postdrift.analysisControls archive dispatch model
|
||||
| Mode.Simulation Placing -> Postdrift.analysisControls archive dispatch model
|
||||
| Mode.Moot
|
||||
| Mode.Ocean
|
||||
| Mode.Stats _
|
||||
@@ -1072,6 +1132,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
| ColorControls ->
|
||||
html
|
||||
$"""
|
||||
{activeLayerSelector dispatch model}
|
||||
{colorAccordion dispatch model}
|
||||
<sp-accordion size="s" allow-multiple>
|
||||
{colormapAccordion dispatch model}
|
||||
@@ -1084,8 +1145,7 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
{aquacultureLookup (SetAquaculture >> dispatch)}
|
||||
{mapsSelect model dispatch}
|
||||
"""
|
||||
| CropControls ->
|
||||
ProbingControls.cropControls dispatch model
|
||||
| CropControls -> ProbingControls.cropControls dispatch model
|
||||
|
||||
let timeControls () =
|
||||
let n =
|
||||
@@ -1184,18 +1244,23 @@ let toolboxNav setArchivesOpen setInboxOpen model dispatch =
|
||||
| Mode.Ocean -> ()
|
||||
| _ -> SetMode Mode.Ocean |> dispatch
|
||||
|
||||
let selectCompute (driftersModelOpt: DrifterModel option) (plumeModelOpt: PlumeModel option) ev =
|
||||
let selectCompute (driftersModelOpt: DrifterModel option) (plumeModelOpt: PlumeModel option) (xtractModelOpt: XtractModel option) ev =
|
||||
if canSubmit then
|
||||
match driftersModelOpt, plumeModelOpt with
|
||||
| Some d, _ ->
|
||||
console.debug $"We already have an ongoing driftser sim : {d.simulation.kind}"
|
||||
match driftersModelOpt, plumeModelOpt, xtractModelOpt with
|
||||
| Some d, _, _ ->
|
||||
console.debug $"We already have an ongoing drifters sim : {d.simulation.kind}"
|
||||
Drifters d.simulation.kind |> SimControls |> SetSideNavMode |> dispatch
|
||||
|
||||
SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch
|
||||
| None, Some p ->
|
||||
| None, Some p, _ ->
|
||||
console.debug $"We already have an ongoing plume sim"
|
||||
Plume p.kind |> SimControls |> SetSideNavMode |> dispatch
|
||||
|
||||
SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch
|
||||
| None, None, Some x ->
|
||||
console.debug $"We already have an ongoing extraction"
|
||||
DataExtraction x.kind |> SimControls |> SetSideNavMode |> dispatch
|
||||
|
||||
SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch
|
||||
| _ ->
|
||||
Drifters SimType.TransportSim |> SimControls |> SetSideNavMode |> dispatch
|
||||
@@ -1253,8 +1318,8 @@ let toolboxNav setArchivesOpen setInboxOpen model dispatch =
|
||||
<overlay-trigger id="trigger" placement="right" offset="6" triggered-by="hover">
|
||||
<sp-tooltip slot="hover-content">Compute</sp-tooltip>
|
||||
<div slot="trigger" class="toolbox-control{if canSubmit then " " + isSimControls else ".disabled"}">
|
||||
<div class="toolbox-icon" @click={Ev (selectCompute model.drifterModelOpt model.plumeModelOpt)}>
|
||||
{if model.drifterModelOpt.IsSome then
|
||||
<div class="toolbox-icon" @click={Ev (selectCompute model.drifterModelOpt model.plumeModelOpt model.xtractModelOpt)}>
|
||||
{if model.drifterModelOpt.IsSome || model.plumeModelOpt.IsSome || model.xtractModelOpt.IsSome then
|
||||
statusIcon
|
||||
else
|
||||
Lit.nothing}
|
||||
|
||||
@@ -389,6 +389,7 @@ let toTimeSeries3D (t0: DateTime) (freq: int) (first: int, last: int, stride) (d
|
||||
)
|
||||
nt
|
||||
data
|
||||
|> Array.sortByDescending fst
|
||||
|> Array.map (fun (d, ys) -> {
|
||||
PlotData.empty with
|
||||
x = ts |> Array.map float
|
||||
@@ -616,7 +617,7 @@ module ReactLib =
|
||||
"""
|
||||
|
||||
[<JSX.Component>]
|
||||
let HeatmapPlot (input: HeatmapInput) =
|
||||
let HeatmapPlot (showLabels, input) =
|
||||
let network = input.network
|
||||
let particleFilter = input.particleFilter
|
||||
let points = network.matrix
|
||||
@@ -655,9 +656,11 @@ module ReactLib =
|
||||
filteredPoints |> Array.map (fun (_, type', _) -> type'.name)
|
||||
let style = {| minHeight = "416px"; width = "100%" |}
|
||||
let config = {| responsive = true; editable = false |}
|
||||
let xlabel = if showLabels then "Receiver" else ""
|
||||
let ylabel = if showLabels then "Sender" else ""
|
||||
let layout = {|
|
||||
xaxis = {| side = "top"; title = {| text = "Receiver"; standoff = 120 |} |}
|
||||
yaxis = {| autorange = "reversed"; title = {| text = "Sender"; standoff = 120 |} |}
|
||||
xaxis = {| side = "top"; title = {| text = xlabel; standoff = 120 |} |}
|
||||
yaxis = {| autorange = "reversed"; title = {| text = ylabel; standoff = 120 |} |}
|
||||
margin = {| t = 100; b = 50; l = 100; r = 50 |}
|
||||
|}
|
||||
let traces = newHeatMap siteNames siteNames weights
|
||||
@@ -719,8 +722,8 @@ module ReactLib =
|
||||
editable = false
|
||||
|}
|
||||
let layout = {|
|
||||
xaxis = {| side = "top"; title = {| text = "Receiver"; standoff = 120 |} |}
|
||||
yaxis = {| title = {| text = "Sender"; standoff = 120 |} |}
|
||||
xaxis = {| side = "top"; title = {| standoff = 120 |} |}
|
||||
yaxis = {| title = {| standoff = 120 |} |}
|
||||
margin = {| t = 100; b = 50; l = 100; r = 50 |}
|
||||
|}
|
||||
let traces = newCageHeatMap groupNames' cageNames weights'
|
||||
|
||||
@@ -6,9 +6,35 @@ open FsToolkit.ErrorHandling
|
||||
|
||||
open Atlantis.Types
|
||||
open Sorcerer.Types
|
||||
open Stats.Controls
|
||||
|
||||
let private hmr = Lit.HMR.createToken ()
|
||||
|
||||
let private priorityMap =
|
||||
[|
|
||||
"instant"
|
||||
string StatMetric.Mean
|
||||
string StatMetric.Std
|
||||
string StatMetric.Q05
|
||||
string StatMetric.Q25
|
||||
string StatMetric.Q50
|
||||
string StatMetric.Q75
|
||||
string StatMetric.Q95
|
||||
string StatMetric.Q99
|
||||
|] |> Array.mapi (fun i s -> s, i) |> Map.ofArray
|
||||
|
||||
let private sortStats (inp: string array) : string array =
|
||||
let toPriority (s: string) : int =
|
||||
s.Split(' ')
|
||||
|> Array.last
|
||||
|> priorityMap.TryFind
|
||||
|> Option.defaultValue -1
|
||||
|
||||
inp
|
||||
|> Array.map (fun s -> toPriority s, s)
|
||||
|> Array.sortBy fst
|
||||
|> Array.map snd
|
||||
|
||||
let private fetchPropData (archiveId: System.Guid) (frame: FrameIdx) (gridIdx: GridIdx) (prop: Prop) : JS.Promise<single array> =
|
||||
promise {
|
||||
let dataUrl = Remoting.getDataUrl ()
|
||||
@@ -286,7 +312,9 @@ let View
|
||||
|
||||
let model, dispatch = Hook.useElmish ((fun () -> init archive.id gridIdx stats.showInstant probeProp), update)
|
||||
|
||||
let traceArray: Plotly.ITraces array = model.TraceMap |> Map.values |> Array.ofSeq
|
||||
let sortedKeys = model.TraceMap |> Map.keys |> Array.ofSeq |> sortStats
|
||||
let traceArray: Plotly.ITraces array = sortedKeys |> Array.choose (fun k -> Map.tryFind k model.TraceMap)
|
||||
|
||||
let plotInfo: Plotly.PlotInfoWithTraces = {
|
||||
title = $"{probeProp.ToLabel ()}"
|
||||
ylegend = "Depth [m]"
|
||||
|
||||
@@ -26,6 +26,7 @@ let private fetchSeries3D
|
||||
| Prop.Temp -> fvcom.TimeSeries.GetTemp aid frames vidx
|
||||
| Prop.Salt -> fvcom.TimeSeries.GetSalinity aid frames vidx
|
||||
| Prop.Speed -> fvcom.TimeSeries.GetSpeed aid frames vidx
|
||||
| Prop.Dens -> fvcom.TimeSeries.GetDensity aid frames vidx
|
||||
| _ -> fun _ -> async.Return [||]
|
||||
|
||||
let! x = Async.StartAsPromise (f idx)
|
||||
@@ -63,6 +64,7 @@ let private fetchStatSeries (archiveId: System.Guid) (propType: StatProp) (metri
|
||||
| Temperature -> stats.FvStatsSeries.GetTemperature archiveId
|
||||
| Salinity -> stats.FvStatsSeries.GetSalinity archiveId
|
||||
| Speed -> stats.FvStatsSeries.GetSpeed archiveId
|
||||
| Density
|
||||
| U
|
||||
| V
|
||||
| WaterTransport
|
||||
@@ -128,6 +130,7 @@ let fetchSeries frame archive probePoint gridIdx depthIndex : JS.Promise<single
|
||||
match probePoint.prop with
|
||||
| Prop.Temp
|
||||
| Prop.Salt
|
||||
| Prop.Dens
|
||||
| Prop.Speed -> fetchSeries3D frame archive probePoint.prop probePoint.timeUnit gridIdx depthIndex
|
||||
| Prop.Zeta -> fetchSeries2D frame archive probePoint.prop probePoint.timeUnit gridIdx
|
||||
| _ -> Promise.lift [||]
|
||||
@@ -211,6 +214,7 @@ let SeriesPlot
|
||||
match probePoint.prop with
|
||||
| Prop.Temp
|
||||
| Prop.Salt
|
||||
| Prop.Dens
|
||||
| Prop.Speed ->
|
||||
let p = data |> Plotly.toTimeSeries3D time archive.saveFreq frames
|
||||
setPlotData p
|
||||
|
||||
@@ -828,7 +828,7 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
Start date
|
||||
</sp-field-label>
|
||||
<sp-field-group horizontal style="padding-bottom: 10px;">
|
||||
{FluentUI.Lit.DatePicker (startDateKey, false, xmodel'.simulation.start, setStartDateTime, minStartDate, maxStartDate)}
|
||||
{FluentUI.Lit.DatePicker (false, xmodel'.simulation.start, setStartDateTime, minStartDate, maxStartDate)}
|
||||
</sp-field-label>
|
||||
</sp-field-group>
|
||||
|
||||
@@ -837,7 +837,7 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
End date
|
||||
</sp-field-label>
|
||||
<sp-field-group horizontal style="padding-bottom: 10px;">
|
||||
{FluentUI.Lit.DatePicker (endDateKey, false, xmodel'.simulation.stop, setStopDateTime, minEndDate, maxEndDate)}
|
||||
{FluentUI.Lit.DatePicker (false, xmodel'.simulation.stop, setStopDateTime, minEndDate, maxEndDate)}
|
||||
</sp-field-group>
|
||||
</sp-field-group>
|
||||
</div>
|
||||
|
||||
@@ -2294,8 +2294,25 @@ let private makeNetworkCircle (idx: int) radius (pos: float * float) =
|
||||
circle
|
||||
|
||||
[<HookComponent>]
|
||||
let CageNetworkDialogButton (downloadUrl: string) (archiveName: string) (input: Plotly.HeatmapInput) =
|
||||
let CageNetworkDialogButton (archiveName: string) (hmi: Plotly.HeatmapInput) =
|
||||
let isOpen, setOpen = Hook.useState false
|
||||
let showExtractedData, setDataExtracted = Hook.useState false
|
||||
let extractedData, setExtractedData = Hook.useState ""
|
||||
|
||||
let handleClearData () =
|
||||
setDataExtracted false
|
||||
setExtractedData ""
|
||||
|
||||
let handleExtractData () =
|
||||
setDataExtracted true
|
||||
hmi.network.matrix
|
||||
|> Array.map (fun (_, row) ->
|
||||
row
|
||||
|> Array.map (fun x -> $"%.3f{x}")
|
||||
|> String.concat ", "
|
||||
)
|
||||
|> String.concat "\n"
|
||||
|> setExtractedData
|
||||
|
||||
Hook.useEffectOnce (fun () ->
|
||||
let keyup (ev: Types.Event) =
|
||||
@@ -2307,30 +2324,127 @@ let CageNetworkDialogButton (downloadUrl: string) (archiveName: string) (input:
|
||||
|
||||
Hook.createDisposable (fun () -> document.removeEventListener("keyup", keyup)))
|
||||
|
||||
let extractedTextfield () =
|
||||
html
|
||||
$"""
|
||||
<textarea
|
||||
class="textarea grow"
|
||||
style="min-height: 500px; min-width: 1250px"
|
||||
readonly
|
||||
>{extractedData}</textarea>
|
||||
"""
|
||||
|
||||
let cancelButton () =
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
static="primary"
|
||||
@click={Ev(fun _ -> handleClearData ())}
|
||||
>
|
||||
Show heatmap
|
||||
</sp-action-button>
|
||||
"""
|
||||
|
||||
let extractButton () =
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
static="primary"
|
||||
@click={Ev(fun _ -> handleExtractData ())}
|
||||
>
|
||||
Show data
|
||||
</sp-action-button>
|
||||
"""
|
||||
|
||||
|
||||
html
|
||||
$"""
|
||||
<overlay-trigger type="modal">
|
||||
<overlay-trigger type="modal" class="flex-row">
|
||||
<sp-dialog-base slot="click-content" underlay responsive dismissable>
|
||||
<sp-dialog size="l" style="min-width: 1024px;" dismissable>
|
||||
<sp-dialog size="l" style="min-height: 800px; min-width: 1400px;" dismissable>
|
||||
<h2 slot="heading">Connection matrix for {archiveName}</h2>
|
||||
{Plotly.CageInteractionHeatmap input}
|
||||
<a slot="footer" href="{downloadUrl}">Download (CSV)</a>
|
||||
{if showExtractedData then extractedTextfield() else Plotly.CageInteractionHeatmap hmi}
|
||||
<div slot="footer">
|
||||
{if showExtractedData then cancelButton () else extractButton ()}
|
||||
</div>
|
||||
</sp-dialog>
|
||||
</sp-dialog-base>
|
||||
<sp-action-button
|
||||
id="heatmap-fullscreen-button"
|
||||
slot="trigger"
|
||||
size="s"
|
||||
>
|
||||
<sp-icon-maximize slot="icon"></sp-icon-maximize>
|
||||
<sp-tooltip self-managed placement="top">Fullscreen chart</sp-tooltip>
|
||||
style="flex-grow: 1; min-width: 150px"
|
||||
> Compute matrix
|
||||
<sp-icon-color-harmony slot="icon"></sp-icon-color-harmony>
|
||||
</sp-action-button>
|
||||
</overlay-trigger>
|
||||
"""
|
||||
|
||||
[<HookComponent>]
|
||||
let NetworkDialogButton (downloadUrl: string) (archiveName: string) color network particleFilter =
|
||||
let NetworkDialogButton (downloadUrl: string) (archiveName: string) (hmi: Plotly.HeatmapInput) =
|
||||
let isOpen, setOpen = Hook.useState false
|
||||
let showExtractedData, setDataExtracted = Hook.useState false
|
||||
let extractedData, setExtractedData = Hook.useState ""
|
||||
|
||||
let handleClearData () =
|
||||
setDataExtracted false
|
||||
setExtractedData ""
|
||||
|
||||
let handleExtractData () =
|
||||
setDataExtracted true
|
||||
hmi.network.matrix
|
||||
|> Array.map (fun (_, row) ->
|
||||
row
|
||||
|> Array.map (fun x -> $"%.3f{x}")
|
||||
|> String.concat ", "
|
||||
)
|
||||
|> String.concat "\n"
|
||||
|> setExtractedData
|
||||
|
||||
let extractedTextfield () =
|
||||
html
|
||||
$"""
|
||||
<textarea
|
||||
class="textarea grow"
|
||||
style="min-height: 500px; min-width: 1250px"
|
||||
readonly
|
||||
>{extractedData}</textarea>
|
||||
"""
|
||||
|
||||
let cancelButton () =
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
static="primary"
|
||||
@click={Ev(fun _ -> handleClearData ())}
|
||||
>
|
||||
Show heatmap
|
||||
</sp-action-button>
|
||||
"""
|
||||
|
||||
let extractButton () =
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
static="primary"
|
||||
@click={Ev(fun _ -> handleExtractData ())}
|
||||
>
|
||||
Show data
|
||||
</sp-action-button>
|
||||
"""
|
||||
|
||||
let downloadButton url =
|
||||
html
|
||||
$"""
|
||||
<a href="{url}">
|
||||
<sp-action-button
|
||||
size="m"
|
||||
style="width: 35px;"
|
||||
>
|
||||
<sp-icon-download slot="icon"></sp-icon-download>
|
||||
<sp-tooltip placement="top" self-managed>Download (zip)</sp-tooltip>
|
||||
</sp-action-button>
|
||||
</a>
|
||||
"""
|
||||
|
||||
Hook.useEffectOnce (fun () ->
|
||||
let keyup (ev: Types.Event) =
|
||||
@@ -2348,10 +2462,13 @@ let NetworkDialogButton (downloadUrl: string) (archiveName: string) color networ
|
||||
$"""
|
||||
<overlay-trigger type="modal">
|
||||
<sp-dialog-base slot="click-content" underlay responsive dismissable>
|
||||
<sp-dialog size="l" style="min-width: 1024px;" dismissable>
|
||||
<sp-dialog size="l" style="min-height: 800px; min-width: 1400px;" dismissable>
|
||||
<h2 slot="heading">Connection matrix for {archiveName}</h2>
|
||||
{Plotly.WaterContactHeatmap { color = color; network = network; particleFilter = particleFilter }}
|
||||
<a slot="footer" href="{downloadUrl}">Download (CSV)</a>
|
||||
{if showExtractedData then extractedTextfield() else Plotly.WaterContactHeatmap (true, hmi)}
|
||||
<div slot="footer">
|
||||
{downloadButton downloadUrl}
|
||||
{if showExtractedData then cancelButton () else extractButton ()}
|
||||
</div>
|
||||
</sp-dialog>
|
||||
</sp-dialog-base>
|
||||
<sp-action-button
|
||||
@@ -2576,6 +2693,7 @@ let fieldsControlsWC (dispatch: Msg -> unit) (model: Model) postdrift =
|
||||
Lit.nothing
|
||||
else
|
||||
let url = waterContactDownloadUrl model postdrift
|
||||
let hmi : Plotly.HeatmapInput = { color = color; network = model.infectionNetwork; particleFilter = model.particleFilter }
|
||||
html
|
||||
$"""
|
||||
<div>
|
||||
@@ -2584,10 +2702,10 @@ let fieldsControlsWC (dispatch: Msg -> unit) (model: Model) postdrift =
|
||||
<sp-icon-info></sp-icon-info>
|
||||
<sp-help-text size="s">Sources in rows and receiving sites in columns</sp-help-text>
|
||||
</div>
|
||||
{NetworkDialogButton url postdrift.Archive.name color model.infectionNetwork filter}
|
||||
{NetworkDialogButton url postdrift.Archive.name hmi}
|
||||
</div>
|
||||
|
||||
{Plotly.WaterContactHeatmap { color = color; particleFilter = filter; network = model.infectionNetwork }}
|
||||
{Plotly.WaterContactHeatmap (false, hmi)}
|
||||
</div>
|
||||
"""
|
||||
|
||||
@@ -2804,18 +2922,6 @@ let fieldsControlsSedV2 (dispatch: Msg -> unit) (model: Model) (layer: MapLayer)
|
||||
// toggleAllKinds
|
||||
// (dimap Some >> SelectNetworkSite >> dispatch)
|
||||
|
||||
let downloadMatrix model postdrift =
|
||||
"foo"
|
||||
// // let sorcererUrl = model.dataSvc
|
||||
// // let aid = string postdrift.Archive.archiveId
|
||||
// // let field = viewProp.FieldKind.ToString()
|
||||
// // let state = AnyState.ToString()
|
||||
// // let kind = filter.availableParticleTypes.Head.kind.ToInt()
|
||||
// // let frame = Drifters.calcDriftersFrame model.archive postdrift model.frame
|
||||
// // let zipName = $"{field}-matrix.zip"
|
||||
// // let urlStart = $"%s{sorcererUrl}/download/connection"
|
||||
// // urlStart + $"/%s{aid}/%i{frame}/%s{field}/%i{kind}/%s{state}/%s{zipName}"
|
||||
|
||||
let color =
|
||||
let prop = Colors.getProp Conc model.glLayers
|
||||
Map.tryFind prop.PropType model.propColors
|
||||
@@ -2835,21 +2941,16 @@ let fieldsControlsSedV2 (dispatch: Msg -> unit) (model: Model) (layer: MapLayer)
|
||||
</div>
|
||||
"""
|
||||
else
|
||||
let url = downloadMatrix model postdrift
|
||||
let hmi : Plotly.HeatmapInput = { color = color; particleFilter = filter; network = model.infectionNetwork }
|
||||
html
|
||||
$"""
|
||||
<div>
|
||||
<div class="flex-row">
|
||||
<div class="grow">
|
||||
{CageNetworkDialogButton postdrift.Archive.name hmi }
|
||||
<div class="flex-row grow" style="gap: 8px; align-items: center;">
|
||||
<sp-icon-info></sp-icon-info>
|
||||
<sp-help-text size="s">Contributions to cage centers</sp-help-text>
|
||||
<sp-help-text size="s">Contributions to the center point of each cage</sp-help-text>
|
||||
</div>
|
||||
{CageNetworkDialogButton url postdrift.Archive.name hmi }
|
||||
</div>
|
||||
|
||||
{Plotly.CageInteractionHeatmap hmi }
|
||||
</div>
|
||||
"""
|
||||
|
||||
let downloadAze =
|
||||
|
||||
@@ -34,6 +34,7 @@ let fetchDepthProfileData (archiveId: System.Guid) (period: Period) (metric: Sta
|
||||
| Temperature -> stats.FvStatsByIndex.GetTemperature archiveId period
|
||||
| Salinity -> stats.FvStatsByIndex.GetSalinity archiveId period
|
||||
| Speed -> stats.FvStatsByIndex.GetSpeed archiveId period
|
||||
| Density
|
||||
| U
|
||||
| V
|
||||
| WaterTransport
|
||||
|
||||
@@ -75,7 +75,7 @@ let getInteraction (map: OlMap) interactionId =
|
||||
:?> Draw
|
||||
|
||||
[<HookComponent>]
|
||||
let probeButton (model: Model) (disabled: bool) (selected: string option) (handleProbe: Event.MapBrowserEvent -> unit) =
|
||||
let probeButton (model: Model) (show: bool) (selected: string option) (handleProbe: Event.MapBrowserEvent -> unit) =
|
||||
let clickKey = Hook.useRef None
|
||||
|
||||
let cropMode = model.cropCoords.Length > 0
|
||||
@@ -90,7 +90,7 @@ let probeButton (model: Model) (disabled: bool) (selected: string option) (handl
|
||||
| _ -> false
|
||||
|
||||
let isSelected = selected = Some "probe"
|
||||
let isDisabled = disabled || otherSelected || cropMode || plotMode
|
||||
let isDisabled = otherSelected || cropMode || plotMode
|
||||
|
||||
// NOTE(simkir): If this button gets unmounted, the placing effect listener does not run its dispose function,
|
||||
// therefore we have to here save the key, and dispose it on unmount. Terrible, I know :(
|
||||
@@ -105,20 +105,23 @@ let probeButton (model: Model) (disabled: bool) (selected: string option) (handl
|
||||
// Remove eventlistener based on the probing state
|
||||
Hook.useEffectOnChange (selected, fun s -> Maps.crossHairSelect model.map clickKey handleProbe (s = Some "probe"))
|
||||
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
size="m"
|
||||
value="probe"
|
||||
?selected={isSelected}
|
||||
?disabled={isDisabled}
|
||||
>
|
||||
<sp-icon-sampler slot="icon"></sp-icon-sampler>
|
||||
</sp-action-button>
|
||||
"""
|
||||
if show then
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
size="m"
|
||||
value="probe"
|
||||
?selected={isSelected}
|
||||
?disabled={isDisabled}
|
||||
>
|
||||
<sp-icon-sampler slot="icon"></sp-icon-sampler>
|
||||
</sp-action-button>
|
||||
"""
|
||||
else
|
||||
Lit.nothing
|
||||
|
||||
[<HookComponent>]
|
||||
let measureButton (model: Model) dispatch (selected: string option) setSelected handlePlot handleMeasure =
|
||||
let measureButton (model: Model) dispatch (show: bool) (selected: string option) setSelected handlePlot handleMeasure =
|
||||
let measure = Hook.useRef<Draw> (getInteraction model.map "measureInteraction")
|
||||
let archiveId = Hook.useRef<System.Guid> model.archive.id
|
||||
let lineCoords = Hook.useRef<(float * float) array> Array.empty
|
||||
@@ -261,20 +264,23 @@ let measureButton (model: Model) dispatch (selected: string option) setSelected
|
||||
reset ()
|
||||
)
|
||||
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
size="m"
|
||||
value="measure"
|
||||
?disabled={isDisabled}
|
||||
?selected={isSelected}
|
||||
>
|
||||
<sp-icon-measure slot="icon"></sp-icon-measure>
|
||||
</sp-action-button>
|
||||
"""
|
||||
if show then
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
size="m"
|
||||
value="measure"
|
||||
?disabled={isDisabled}
|
||||
?selected={isSelected}
|
||||
>
|
||||
<sp-icon-measure slot="icon"></sp-icon-measure>
|
||||
</sp-action-button>
|
||||
"""
|
||||
else
|
||||
Lit.nothing
|
||||
|
||||
[<HookComponent>]
|
||||
let circleButton (model: Model) dispatch (selected: string option) handleCircle =
|
||||
let circleButton (model: Model) dispatch (show: bool) (selected: string option) handleCircle =
|
||||
let clickKey = Hook.useRef None
|
||||
let cropMode = model.cropCoords.Length > 0
|
||||
let plotMode =
|
||||
@@ -311,20 +317,76 @@ let circleButton (model: Model) dispatch (selected: string option) handleCircle
|
||||
None |> SetGridCircle |> dispatch
|
||||
)
|
||||
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
size="m"
|
||||
value="circle"
|
||||
?disabled={isDisabled}
|
||||
?selected={isSelected}
|
||||
>
|
||||
<sp-icon-circle slot="icon"></sp-icon-circle>
|
||||
</sp-action-button>
|
||||
"""
|
||||
if show then
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
size="m"
|
||||
value="circle"
|
||||
?disabled={isDisabled}
|
||||
?selected={isSelected}
|
||||
>
|
||||
<sp-icon-circle slot="icon"></sp-icon-circle>
|
||||
</sp-action-button>
|
||||
"""
|
||||
else
|
||||
Lit.nothing
|
||||
|
||||
[<HookComponent>]
|
||||
let cropButton (model: Model) dispatch (selected: string option) handleCrop =
|
||||
let locateButton (model: Model) (show: bool) (selected: string option) handleClear handleLocate =
|
||||
let clickKey = Hook.useRef None
|
||||
let cropMode = model.cropCoords.Length > 0
|
||||
let plotMode =
|
||||
model.probePoint.point.IsSome
|
||||
|| model.probePoint.idx.IsSome
|
||||
|| model.pickLine.Length > 0
|
||||
let otherSelected =
|
||||
match selected with
|
||||
| Some "locate" -> false
|
||||
| Some _ -> true
|
||||
| _ -> false
|
||||
|
||||
let isSelected = selected = Some "locate"
|
||||
let isDisabled = otherSelected || cropMode || plotMode
|
||||
|
||||
// NOTE(simkir): If this button gets unmounted, the placing effect listener does not run its dispose function,
|
||||
// therefore we have to here save the key, and dispose it on unmount. Terrible, I know :(
|
||||
Hook.useEffectOnce (fun () ->
|
||||
Hook.createDisposable (fun () ->
|
||||
let elem = model.map.getTargetElement ()
|
||||
do elem?style?cursor <- ""
|
||||
do clickKey.contents |> Option.iter Observable.unByKey
|
||||
)
|
||||
)
|
||||
|
||||
// Remove eventlistener based on the probing state
|
||||
Hook.useEffectOnChange (
|
||||
selected,
|
||||
fun s ->
|
||||
match s with
|
||||
| Some "locate" -> Maps.crossHairSelect model.map clickKey handleLocate true |> ignore
|
||||
| _ ->
|
||||
handleClear ()
|
||||
clickKey.contents |> Option.iter Observable.unByKey
|
||||
)
|
||||
|
||||
if show then
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
size="m"
|
||||
value="locate"
|
||||
?disabled={isDisabled}
|
||||
?selected={isSelected}
|
||||
>
|
||||
<sp-icon-location slot="icon"></sp-icon-location>
|
||||
</sp-action-button>
|
||||
"""
|
||||
else
|
||||
Lit.nothing
|
||||
|
||||
[<HookComponent>]
|
||||
let cropButton (model: Model) dispatch (show: bool) (selected: string option) handleCrop =
|
||||
let dragBoxEventKey = Hook.useRef<Event.EventsKey option> None
|
||||
|
||||
let cropMode = model.cropCoords.Length > 0
|
||||
@@ -390,18 +452,21 @@ let cropButton (model: Model) dispatch (selected: string option) handleCrop =
|
||||
)
|
||||
)
|
||||
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
id="crop-button"
|
||||
value="crop"
|
||||
size="m"
|
||||
?disabled={isDisabled}
|
||||
?selected={isSelected}
|
||||
>
|
||||
<sp-icon-crop slot="icon"></sp-icon-crop>
|
||||
</sp-action-button>
|
||||
"""
|
||||
if show then
|
||||
html
|
||||
$"""
|
||||
<sp-action-button
|
||||
id="crop-button"
|
||||
value="crop"
|
||||
size="m"
|
||||
?disabled={isDisabled}
|
||||
?selected={isSelected}
|
||||
>
|
||||
<sp-icon-crop slot="icon"></sp-icon-crop>
|
||||
</sp-action-button>
|
||||
"""
|
||||
else
|
||||
Lit.nothing
|
||||
|
||||
// TODO: Why is this is Probing?:w
|
||||
/// General purpose back button
|
||||
@@ -461,10 +526,13 @@ let backButton model dispatch =
|
||||
let measures (model: Model) dispatch =
|
||||
let selected, setSelected = Hook.useState<string option> None
|
||||
let radius = Hook.useRef<float> 10_000
|
||||
let lonlat, setLonlat = Hook.useState<string> ""
|
||||
|
||||
let showProbe = true
|
||||
let showMeasure = model.selectedPostdrift.IsNone || model.selectedDrifters.IsNone
|
||||
let showCircle = model.selectedPostdrift.IsNone || model.selectedDrifters.IsNone
|
||||
let collaps = match selected with | Some "circle" | Some "locate" -> true | _ -> false
|
||||
let showProbe = not collaps && model.selectedDrifters.IsNone
|
||||
let showMeasure = not collaps && model.selectedDrifters.IsNone
|
||||
let showCircle = (selected <> Some "locate") && model.selectedDrifters.IsNone
|
||||
let showLocate = (selected <> Some "circle") && model.selectedDrifters.IsNone
|
||||
let showCrop = false
|
||||
|
||||
let handleProbeMapClick (ev: Event.MapBrowserEvent) =
|
||||
@@ -494,6 +562,47 @@ let measures (model: Model) dispatch =
|
||||
|> SetGridCircle
|
||||
|> dispatch
|
||||
|
||||
let readCoord (line: string) : (float * float) option =
|
||||
line.Split([| '\t'; ' '; ','; ';' |])
|
||||
|> Array.filter ((<>) "")
|
||||
|> function
|
||||
| [| lat; lon |] -> tryParseFloat lat, tryParseFloat lon
|
||||
| _ -> None, None
|
||||
|> function
|
||||
| Some lon, Some lat ->
|
||||
(lon, lat)
|
||||
|> toEpsg3857'
|
||||
|> Some
|
||||
| _ -> None
|
||||
|
||||
let handleLocateAccept lonlat =
|
||||
lonlat
|
||||
|> readCoord
|
||||
|> Option.map (fun (x, y) ->
|
||||
[| { SampleData.empty with name = ""; radius = 25.0; coord = [| x; y |] } |]
|
||||
|> SetPointSamples
|
||||
|> dispatch
|
||||
|
||||
setLayerVisible model.map MapLayer.PointSamples true
|
||||
Maps.flyTo model.map 14 [| x; y |]
|
||||
)
|
||||
|> Option.defaultValue ()
|
||||
|
||||
let handleLocateMapClick (ev: Event.MapBrowserEvent) =
|
||||
let x, y = ev.coordinate[0], ev.coordinate[1]
|
||||
let ll = (x, y) |> toWgs84' |> fun (lon, lat) -> $"%.6f{lon}, %.6f{lat}"
|
||||
setLonlat ll
|
||||
handleLocateAccept ll
|
||||
|
||||
let handleLocateClear () =
|
||||
Array.empty |> SetPointSamples |> dispatch
|
||||
setLayerVisible model.map MapLayer.PointSamples false
|
||||
setLonlat ""
|
||||
|
||||
let setLonLat' (ll: string) =
|
||||
setLonlat ll
|
||||
handleLocateAccept ll
|
||||
|
||||
let setRadius (r: float) =
|
||||
radius.contents <- r
|
||||
model.gridCircle
|
||||
@@ -523,12 +632,33 @@ let measures (model: Model) dispatch =
|
||||
else
|
||||
setSelected (Some newSelected[0])
|
||||
|
||||
let locateField () =
|
||||
html
|
||||
$"""
|
||||
<div class="flex-row gap-8">
|
||||
<sp-textfield
|
||||
size="m"
|
||||
id="lonlat-field"
|
||||
style="flex-grow: 1; padding-left: 10px;"
|
||||
value="{lonlat}"
|
||||
placeholder="Lon, Lat (decimal degrees)"
|
||||
@change={EvVal(unbox >> setLonLat')}
|
||||
></sp-textfield>
|
||||
<sp-action-button
|
||||
style="flex-grow: 1; padding-right: 10px; "
|
||||
?disabled={lonlat.Length = 0}
|
||||
@click="{Ev (fun _ -> handleLocateAccept lonlat)}"
|
||||
>
|
||||
<sp-icon-search slot="icon"></sp-icon-search>
|
||||
</sp-action-button>
|
||||
</div>
|
||||
"""
|
||||
|
||||
let radiusField () =
|
||||
html
|
||||
$"""
|
||||
<sp-field-label style="flex-grow: 1;">
|
||||
Radius
|
||||
<sp-field-label style="flex-grow: 1; padding-left: 50px">
|
||||
Radius (m)
|
||||
</sp-field-label>
|
||||
<sp-number-field
|
||||
size="m"
|
||||
@@ -536,7 +666,7 @@ let measures (model: Model) dispatch =
|
||||
format-options="{formatDigits 0 0}"
|
||||
min={0.0} max={1_000_000.0}
|
||||
step="100"
|
||||
style="flex-grow: 1; padding-right: 15px"
|
||||
style="flex-grow: 1; padding-right: 20px"
|
||||
value="{radius.Value}"
|
||||
@mousedown={Ev(fun e -> e.stopPropagation ())}
|
||||
@change={EvVal(unbox >> setRadius)}
|
||||
@@ -554,17 +684,19 @@ let measures (model: Model) dispatch =
|
||||
.selected={currentSelected}
|
||||
@change={Ev handleMeasureChange}
|
||||
>
|
||||
{if showProbe then probeButton model false selected handleProbeMapClick else Lit.nothing}
|
||||
{if showMeasure then measureButton model dispatch selected setSelected handlePlotAccept handleMeasureAccept else Lit.nothing}
|
||||
{if showCircle then circleButton model dispatch selected handleCircleMapClick else Lit.nothing}
|
||||
{if showCrop then cropButton model dispatch selected handleCrop else Lit.nothing}
|
||||
{probeButton model showProbe selected handleProbeMapClick}
|
||||
{measureButton model dispatch showMeasure selected setSelected handlePlotAccept handleMeasureAccept}
|
||||
{circleButton model dispatch showCircle selected handleCircleMapClick}
|
||||
{locateButton model showLocate selected handleLocateClear handleLocateMapClick}
|
||||
{cropButton model dispatch showCrop selected handleCrop}
|
||||
</sp-action-group>
|
||||
|
||||
{match selected with | Some "circle" -> radiusField () | _ -> Lit.nothing }
|
||||
{match selected with
|
||||
| Some "circle" -> radiusField ()
|
||||
| Some "locate" -> locateField ()
|
||||
| _ -> Lit.nothing}
|
||||
|
||||
<sp-action-group class="measures-button-group">
|
||||
{Drifters.driftersInputModal model (CloneDriftersInput >> dispatch)}
|
||||
</sp-action-group>
|
||||
{if selected <> Some "locate" then Drifters.driftersInputModal model (CloneDriftersInput >> dispatch) else Lit.nothing}
|
||||
</div>
|
||||
|
||||
<sp-divider size="s"></sp-divider>
|
||||
|
||||
@@ -6,7 +6,6 @@ open Fable.Core.JsInterop
|
||||
open Lit
|
||||
|
||||
open Atlantis.Types
|
||||
open Remoting
|
||||
open Sorcerer.Types
|
||||
|
||||
|
||||
@@ -88,8 +87,6 @@ let private MetricsAccordion (showInstant: bool) (selectedMetrics: StatMetric ar
|
||||
</div>
|
||||
"""
|
||||
|
||||
let private testStats propType = Option.map (Array.contains propType >> not) >> Option.defaultValue false
|
||||
|
||||
[<HookComponent>]
|
||||
let PeriodSelection (period: Period) (onSelect: Period -> unit) =
|
||||
Hook.useHmr hmr
|
||||
@@ -120,9 +117,9 @@ let PeriodSelection (period: Period) (onSelect: Period -> unit) =
|
||||
let View (dispatch: Model.Msg -> unit) (model: Model.Model) =
|
||||
Hook.useHmr hmr
|
||||
|
||||
let noTemp = model.statsAvailable |> testStats StatProp.Temperature
|
||||
let noSalt = model.statsAvailable |> testStats StatProp.Salinity
|
||||
let noSpeed = model.statsAvailable |> testStats StatProp.Speed
|
||||
let noTemp = model.statsAvailable |> Array.contains StatProp.Temperature |> not
|
||||
let noSalt = model.statsAvailable |> Array.contains StatProp.Salinity |> not
|
||||
let noSpeed = model.statsAvailable |> Array.contains StatProp.Speed |> not
|
||||
let noStats = noTemp && noSalt && noSpeed
|
||||
|
||||
let statsHrefOpt =
|
||||
@@ -145,51 +142,41 @@ let View (dispatch: Model.Msg -> unit) (model: Model.Model) =
|
||||
let handlePeriodChange (newPeriod: Period) =
|
||||
Model.SetStatPeriod newPeriod |> dispatch
|
||||
|
||||
Hook.useEffectOnce (fun () ->
|
||||
let statsApi = StatsApi (getDataUrl ())
|
||||
if noStats then
|
||||
html
|
||||
$"""
|
||||
<div class="stats-nav-container">
|
||||
<sp-alert-banner ?open={noStats}>This archive has no available statistics</sp-alert-banner>
|
||||
</div>
|
||||
"""
|
||||
else
|
||||
html
|
||||
$"""
|
||||
<div class="stats-nav-container">
|
||||
<div>
|
||||
<div class="flex-row flex-align-center gap-16">
|
||||
<div class="grow">
|
||||
{PeriodSelection model.stats.period handlePeriodChange}
|
||||
</div>
|
||||
|
||||
console.info("[Stats] Mounting statsControl for archive %s", model.archive.name)
|
||||
<overlay-trigger triggered-by="hover">
|
||||
<sp-action-button
|
||||
slot="trigger"
|
||||
size="m"
|
||||
href={Lit.ifSome statsHrefOpt}
|
||||
target="_blank"
|
||||
?disabled={statsHrefOpt.IsNone}
|
||||
>
|
||||
<sp-icon-download slot="icon"></sp-icon-download>
|
||||
</sp-action-button>
|
||||
|
||||
match model.statsAvailable with
|
||||
| Some stats -> ()
|
||||
| None ->
|
||||
statsApi.FvStatsInfo.GetAvailableStats model.archive.id
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.iter (fun avail ->
|
||||
Model.SetStatsAvailable avail
|
||||
|> dispatch
|
||||
)
|
||||
)
|
||||
|
||||
html
|
||||
$"""
|
||||
<div class="stats-nav-container">
|
||||
<sp-alert-banner ?open={noStats}>This archive has no available statistics</sp-alert-banner>
|
||||
|
||||
<div>
|
||||
<div class="flex-row flex-align-center gap-16">
|
||||
<div class="grow">
|
||||
{PeriodSelection model.stats.period handlePeriodChange}
|
||||
<sp-tooltip slot="hover-content">
|
||||
Download a bundle of pre-processed statistics at the probing point.
|
||||
</sp-tooltip>
|
||||
</overlay-trigger>
|
||||
</div>
|
||||
|
||||
<overlay-trigger triggered-by="hover">
|
||||
<sp-action-button
|
||||
slot="trigger"
|
||||
size="m"
|
||||
href={Lit.ifSome statsHrefOpt}
|
||||
target="_blank"
|
||||
?disabled={statsHrefOpt.IsNone}
|
||||
>
|
||||
<sp-icon-download slot="icon"></sp-icon-download>
|
||||
</sp-action-button>
|
||||
|
||||
<sp-tooltip slot="hover-content">
|
||||
Download a bundle of pre-processed statistics at the probing point.
|
||||
</sp-tooltip>
|
||||
</overlay-trigger>
|
||||
{MetricsAccordion model.stats.showInstant model.stats.metrics handleMetricsChange}
|
||||
</div>
|
||||
|
||||
{MetricsAccordion model.stats.showInstant model.stats.metrics handleMetricsChange}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
"""
|
||||
@@ -23,6 +23,7 @@ let private fetchSeries (archiveId: System.Guid) (propType: StatProp) (metric: S
|
||||
| Temperature -> stats.FvStatsSeries.GetTemperature archiveId
|
||||
| Salinity -> stats.FvStatsSeries.GetSalinity archiveId
|
||||
| Speed -> stats.FvStatsSeries.GetSpeed archiveId
|
||||
| Density
|
||||
| U
|
||||
| V
|
||||
| WaterTransport
|
||||
@@ -52,6 +53,8 @@ let private fetchSeries (archiveId: System.Guid) (propType: StatProp) (metric: S
|
||||
)
|
||||
|> Array.rev
|
||||
|
||||
console.debug($"[SeriesPlot] fetched dephts {depths}")
|
||||
|
||||
let def: Plotly.PlotData = {
|
||||
Plotly.PlotData.empty with
|
||||
x = Array.init 12 float
|
||||
|
||||
@@ -25,6 +25,7 @@ let toProp (p: StatProp) : Prop =
|
||||
match p with // needed for colormap and scale
|
||||
| Temperature -> Prop.Temp
|
||||
| Salinity -> Prop.Salt
|
||||
| Density -> Prop.Dens
|
||||
| Speed -> Prop.Speed
|
||||
| Current
|
||||
| U
|
||||
@@ -37,6 +38,7 @@ let fromProp (p: Prop) : StatProp =
|
||||
| Prop.Temp -> Temperature
|
||||
| Prop.Salt -> Salinity
|
||||
| Prop.Speed -> Speed
|
||||
| Prop.Dens -> Density
|
||||
| _ -> Undefined
|
||||
|
||||
let fetchDepthMap (archiveId: System.Guid) (coordinate: (float * float)) : JS.Promise<Map<int, bool> option> =
|
||||
@@ -81,6 +83,7 @@ let fetchStatProps (archiveId: System.Guid) (stats: Model.Stats) : JS.Promise<si
|
||||
| Temperature -> api.FvStatsByLayer.GetTemperature
|
||||
| Salinity -> api.FvStatsByLayer.GetSalinity
|
||||
| Speed -> api.FvStatsByLayer.GetSpeed
|
||||
| Density
|
||||
| U
|
||||
| V
|
||||
| Current ->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Fable.Browser.IndexedDB": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.2.0, )",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<DefineConstants>FABLE_COMPILER</DefineConstants>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<Version>1.9.8</Version>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Fable.Core": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.4.0, )",
|
||||
|
||||
@@ -24,7 +24,38 @@
|
||||
min-height: 100px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<!-- .winterContainer { -->
|
||||
<!-- position:fixed; -->
|
||||
<!-- top:0; -->
|
||||
<!-- left:0; -->
|
||||
<!-- width:100%; -->
|
||||
<!-- height:100%; -->
|
||||
<!-- pointer-events:none; -->
|
||||
<!-- overflow:hidden; -->
|
||||
<!-- z-index:10; -->
|
||||
<!-- } -->
|
||||
<!-- @keyframes snowfall { -->
|
||||
<!-- 0% { -->
|
||||
<!-- transform: translateY(0) translateX(0) rotate(0deg); -->
|
||||
<!-- opacity: 0.9; -->
|
||||
<!-- } -->
|
||||
<!-- 100% { -->
|
||||
<!-- transform: translateY(100vh) translateX(var(--swirl)) rotate(var(--rot)); -->
|
||||
<!-- opacity: 0.9; -->
|
||||
<!-- } -->
|
||||
<!-- } -->
|
||||
<!-- .winterSnowflake { -->
|
||||
<!-- position: absolute; -->
|
||||
<!-- top: 0; -->
|
||||
<!-- left: 0; -->
|
||||
<!-- font-size: 1rem; -->
|
||||
<!-- color: white; -->
|
||||
<!-- opacity: 0; -->
|
||||
<!-- animation: snowfall linear infinite; -->
|
||||
<!-- will-change: transform; -->
|
||||
<!-- } -->
|
||||
<script>
|
||||
// NOTE: This should only be sent when we mount the script after confirming the id exists in sessionStorage
|
||||
function beforeSendHandler(type, payload) {
|
||||
@@ -34,6 +65,27 @@
|
||||
return payload;
|
||||
}
|
||||
</script>
|
||||
<!-- <div id="winterContainer" class="winterContainer" aria-hidden="true"></div> -->
|
||||
<!-- <script> -->
|
||||
<!-- // NOTE(mrtz): Add some snowflakes -->
|
||||
<!-- const container = document.getElementById('winterContainer'); -->
|
||||
<!-- const snowflakeCount = 150; -->
|
||||
<!-- const snowflakeSymbols = ['❄', '❅', '❆', '❇', '❈', '❉', '❊', '❋'] -->
|
||||
|
||||
<!-- for (let i = 0; i < snowflakeCount; i++) { -->
|
||||
<!-- const snowflake = document.createElement('div'); -->
|
||||
<!-- const randomSymbol = snowflakeSymbols[Math.floor(Math.random() * snowflakeSymbols.length)]; -->
|
||||
<!-- snowflake.className = 'winterSnowflake'; -->
|
||||
<!-- snowflake.style.left = `${Math.random() * 100}%`; -->
|
||||
<!-- snowflake.style.animationDuration = `${10 + Math.random() * 10}s`; -->
|
||||
<!-- snowflake.style.animationDelay = `${Math.random() * 10}s`; -->
|
||||
<!-- snowflake.style.setProperty('--swirl', `${Math.random() * 20 - 10}vw`); -->
|
||||
<!-- snowflake.style.setProperty('--rot', `${Math.random() * 720 + 360}deg`); -->
|
||||
<!-- snowflake.textContent = randomSymbol; -->
|
||||
<!-- container.appendChild(snowflake); -->
|
||||
<!-- } -->
|
||||
<!-- </script> -->
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"FSharp.Core": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.303, )",
|
||||
|
||||
@@ -569,6 +569,62 @@ module Handlers =
|
||||
getSimInfo = getPlumeSimInfo ctx
|
||||
}
|
||||
|
||||
let private runXtract
|
||||
(ctx: HttpContext)
|
||||
(xtractJob: XtractPayload)
|
||||
(partition: string option)
|
||||
=
|
||||
task {
|
||||
let user = getUserName ctx
|
||||
let group = getGroup ctx
|
||||
let groups = if String.IsNullOrEmpty group then None else Some [| group |]
|
||||
let id = ActorId user
|
||||
|
||||
try
|
||||
let proxy = ActorProxy.Create<IXtractActor> (id, "XtractActor")
|
||||
Log.Information ("runXtract: user {username}, archive={@archive}, name={@name}", user, xtractJob.archiveId, xtractJob.name)
|
||||
|
||||
match! proxy.SubmitXtract(xtractJob, groups, partition) with
|
||||
| Ok info ->
|
||||
Log.Debug $"job {info}"
|
||||
return Some info
|
||||
| Error e ->
|
||||
Log.Warning $"job submit failed: {e}"
|
||||
|
||||
match SignalRHub.getHub ctx with
|
||||
| Some h ->
|
||||
do! h.Clients.User(user).Send (Hub.Response.Note (Note.failure $"Job submission failed: {e}"))
|
||||
return None
|
||||
| None ->
|
||||
Log.Error "Could not get signalr hub context"
|
||||
return None
|
||||
with exn ->
|
||||
Log.Error $"runXtract: {exn.Message}"
|
||||
Log.Verbose $"runXtract: %A{exn}"
|
||||
return None
|
||||
}
|
||||
|> Async.AwaitTask
|
||||
|
||||
let private cancelXtractJob (ctx: HttpContext) (jobId: int) =
|
||||
task {
|
||||
let user = getUserName ctx
|
||||
let id = ActorId user
|
||||
|
||||
try
|
||||
let proxy = ActorProxy.Create<IJobActor> (id, "XtractActor")
|
||||
let! result = proxy.Cancel jobId
|
||||
return result
|
||||
with exn ->
|
||||
Log.Error $"[Xtract] cancelJob: {exn.Message}"
|
||||
return Error exn.Message
|
||||
}
|
||||
|> Async.AwaitTask
|
||||
|
||||
let xtractApi (ctx: HttpContext) : Api.Xtract = {
|
||||
startXtract = runXtract ctx
|
||||
cancelJob = cancelXtractJob ctx
|
||||
}
|
||||
|
||||
module Endpoints =
|
||||
let authEndpoints: HttpHandler =
|
||||
Remoting.createApi ()
|
||||
@@ -604,4 +660,10 @@ module Endpoints =
|
||||
Remoting.createApi ()
|
||||
|> Remoting.fromContext Handlers.plumeApi
|
||||
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|
||||
|> Remoting.buildHttpHandler
|
||||
|
||||
let xtractEndpoints: HttpHandler =
|
||||
Remoting.createApi ()
|
||||
|> Remoting.fromContext Handlers.xtractApi
|
||||
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|
||||
|> Remoting.buildHttpHandler
|
||||
@@ -4,6 +4,8 @@ open System
|
||||
open System.Data
|
||||
open System.Linq
|
||||
open System.Security.Claims
|
||||
open System.Threading.Tasks
|
||||
|
||||
open Archmaester.Actors
|
||||
open Archmaester.Dto
|
||||
open Dapper.FSharp.PostgreSQL
|
||||
@@ -145,46 +147,47 @@ module Handlers =
|
||||
}
|
||||
|> Async.AwaitTask
|
||||
|
||||
let private setNewArchivePermissions (p: Permission) =
|
||||
let id = ActorId p.uid
|
||||
let t = DateTime.UtcNow
|
||||
let term: Term = { start_time = t; end_time = t }
|
||||
|
||||
let ticket: Ticket = {
|
||||
task = JobType.Any
|
||||
quota = 100.0
|
||||
start_time = t
|
||||
end_time = t
|
||||
}
|
||||
|
||||
let private setNewArchivePermissions (p: Permission) : Async<Result<bool, exn>> =
|
||||
task {
|
||||
let proxy = ActorProxy.Create<IArchiveAccessActor> (id, nameof ArchiveAccessActor)
|
||||
let! (o: bool seq) = p.owners |> Array.map (fun x -> proxy.AddOwner (p.aid, x)) |> sequence
|
||||
let! uv = p.users |> Array.map (fun x -> proxy.AllowUserView (p.aid, x, term)) |> sequence
|
||||
let! ux =
|
||||
p.users
|
||||
|> Array.map (fun x -> proxy.AllowUserExec (p.aid, x, ticket))
|
||||
|> sequence
|
||||
let! gv =
|
||||
p.groups
|
||||
|> Array.map (fun x -> proxy.AllowGroupView (p.aid, x, term))
|
||||
|> sequence
|
||||
try
|
||||
let id = ActorId p.uid
|
||||
let t = DateTime.UtcNow
|
||||
let term: Term = { start_time = t; end_time = t }
|
||||
|
||||
let ticket: Ticket = {
|
||||
task = JobType.Any
|
||||
quota = 100.0
|
||||
start_time = t
|
||||
end_time = t
|
||||
}
|
||||
let proxy = ActorProxy.Create<IArchiveAccessActor> (id, nameof ArchiveAccessActor)
|
||||
let! (o: bool seq) = p.owners |> Array.map (fun x -> proxy.AddOwner (p.aid, x)) |> sequence
|
||||
let! uv = p.users |> Array.map (fun x -> proxy.AllowUserView (p.aid, x, term)) |> sequence
|
||||
let! ux =
|
||||
p.users
|
||||
|> Array.map (fun x -> proxy.AllowUserExec (p.aid, x, ticket))
|
||||
|> sequence
|
||||
let! gv =
|
||||
p.groups
|
||||
|> Array.map (fun x -> proxy.AllowGroupView (p.aid, x, term))
|
||||
|> sequence
|
||||
|
||||
if p.ref.IsSome then
|
||||
let proxy =
|
||||
ActorProxy.Create<IArchiveAccessActor> (ActorId p.uid, nameof ArchiveAccessActor)
|
||||
if p.ref.IsSome then
|
||||
let proxy =
|
||||
ActorProxy.Create<IArchiveAccessActor> (ActorId p.uid, nameof ArchiveAccessActor)
|
||||
|
||||
let! _ = proxy.AddParent (p.aid, p.ref.Value) |> Async.AwaitTask
|
||||
()
|
||||
let! _ = proxy.AddParent (p.aid, p.ref.Value) |> Async.AwaitTask
|
||||
()
|
||||
|
||||
let all = Seq.concat [ o; uv; ux; gv ]
|
||||
let res = all |> Seq.reduce (&&)
|
||||
let all = Seq.concat [ o; uv; ux; gv ]
|
||||
let res = all |> Seq.reduce (&&)
|
||||
|
||||
if not res then
|
||||
Log.Warning $"Archmaester.setArchivePermissions returned false: %A{all}"
|
||||
if not res then
|
||||
Log.Warning $"Archmaester.setArchivePermissions returned false: %A{all}"
|
||||
|
||||
return res
|
||||
return Ok res
|
||||
with ex ->
|
||||
return Error ex
|
||||
}
|
||||
|> Async.AwaitTask
|
||||
|
||||
@@ -219,10 +222,12 @@ module Handlers =
|
||||
Log.Information $"Adding archive: {item.props.name}"
|
||||
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
let archivist = Archives.Archivist (Db.getDataSource ())
|
||||
use db = archivist.startConnection ()
|
||||
let tr = archivist.startTransaction db
|
||||
|
||||
try
|
||||
let saveRes = db.tryAddArchive item
|
||||
let saveRes = archivist.tryAddArchive(db, item)
|
||||
Log.Debug $"saveRes %A{saveRes}"
|
||||
|
||||
match saveRes with
|
||||
@@ -241,7 +246,7 @@ module Handlers =
|
||||
|
||||
let aid = archive.ArchiveId
|
||||
|
||||
let! _ =
|
||||
let! res =
|
||||
setNewArchivePermissions {
|
||||
uid = uid
|
||||
gid = gid
|
||||
@@ -252,9 +257,22 @@ module Handlers =
|
||||
ref = item.props.reference
|
||||
}
|
||||
|
||||
ctx.SetStatusCode 201
|
||||
return Ok ()
|
||||
match res with
|
||||
| Ok ok ->
|
||||
if ok then
|
||||
tr.Commit ()
|
||||
ctx.SetStatusCode 201
|
||||
return Ok ()
|
||||
else
|
||||
tr.Rollback ()
|
||||
Log.Error("addArchive: error: one of the permissions failed")
|
||||
return Error "Error adding archive"
|
||||
| Error ex ->
|
||||
tr.Rollback ()
|
||||
Log.Error(ex, "addArchive: error")
|
||||
return Error "Error adding archive"
|
||||
with exn ->
|
||||
tr.Rollback ()
|
||||
Log.Error $"addArchive: error: {exn}"
|
||||
ctx.SetStatusCode 500
|
||||
return Error $"Could not add Archive: {exn.Message}"
|
||||
@@ -413,6 +431,21 @@ module Handlers =
|
||||
return Error $"Could not retrieve archive {aid}: {err}"
|
||||
}
|
||||
|
||||
let getBasePath (aid: ArchiveId) =
|
||||
Log.Information $"Getting archive basePath: {aid}"
|
||||
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
|
||||
match db.getBasePath aid with
|
||||
| Ok path ->
|
||||
Log.Debug $"getBasePath: {path}"
|
||||
return Ok path
|
||||
| Error err ->
|
||||
Log.Error $"getBasePath: archive with id {aid} not found"
|
||||
return Error $"Could not retrieve archive {aid}: {err}"
|
||||
}
|
||||
|
||||
let getFiles (aid: ArchiveId) =
|
||||
Log.Information $"Getting archive files: {aid}"
|
||||
|
||||
@@ -436,10 +469,10 @@ module Handlers =
|
||||
|
||||
match db.getAllArchiveFiles aid with
|
||||
| Ok files ->
|
||||
Log.Debug $"getFiles: {files.basePath} {files.series.Length}"
|
||||
Log.Debug $"getAllFiles: {files.basePath} {files.series.Length}"
|
||||
return Ok files
|
||||
| Error err ->
|
||||
Log.Error $"getFiles: archive with id {aid} not found"
|
||||
Log.Error $"getAllFiles: archive with id {aid} not found"
|
||||
return Error $"Could not retrieve archive {aid}: {err}"
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ open Dapr.Actors.Runtime
|
||||
open Oceanbox.ServerPack
|
||||
|
||||
open Archmaester.Actors
|
||||
open System.Threading.Tasks
|
||||
|
||||
type ArchivistActor(host: ActorHost, observatory: Observer.ObserverFactory) =
|
||||
inherit Actor(host)
|
||||
@@ -53,7 +54,14 @@ type ArchivistActor(host: ActorHost, observatory: Observer.ObserverFactory) =
|
||||
Api.Handlers.getArchivePropsById aid |> Result.map _.json |> Task.FromResult
|
||||
|
||||
member this.GetBasePath(aid) =
|
||||
let db = Oceanbox.DataAgent.Archives.Archivist (Db.getDataSource ())
|
||||
db.getBasePath aid |> Task.FromResult
|
||||
Api.Handlers.getBasePath aid |> Async.StartAsTask
|
||||
|
||||
member this.GetProjection(aid) =
|
||||
Api.Handlers.getArchivePropsById aid
|
||||
|> Result.map _.projection
|
||||
|> Task.FromResult
|
||||
|
||||
member this.GetArchiveFiles(aid) =
|
||||
Api.Handlers.getFiles aid |> Async.StartAsTask
|
||||
|
||||
override this.OnActivateAsync() = task { return () }
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>6.20.0</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Utils.fs" />
|
||||
|
||||
@@ -66,6 +66,7 @@ let handleSlurmEvents (next: HttpFunc) (ctx: HttpContext) =
|
||||
let proxy =
|
||||
match SlurmJobType.FromString job.jobType with
|
||||
| DriftersJob -> ActorProxy.Create<IJobActor>(id, "DriftersActor")
|
||||
| XtractJob -> ActorProxy.Create<IJobActor>(id, "XtractActor")
|
||||
| PlumeJob -> ActorProxy.Create<IJobActor>(id, "PlumeActor")
|
||||
| UnknownJob -> failwith "Unknown job"
|
||||
do! proxy.HandleJobEvent(job)
|
||||
|
||||
@@ -83,12 +83,13 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
|
||||
let grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
|
||||
let dep = None
|
||||
let part = job.partition |> Option.defaultValue "long"
|
||||
let cpt = 32
|
||||
|
||||
task {
|
||||
try
|
||||
let env: DriftersEnv = {
|
||||
JOB_INPUT = Encode.Auto.toString (4, job.input) |> base64e
|
||||
JOB_TYPE = job.model.ToString ()
|
||||
JOB_TYPE = job.model.ToString()
|
||||
JOB_ID = job.aid
|
||||
REF_ID = job.input.simulation.archiveId
|
||||
}
|
||||
@@ -101,7 +102,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
|
||||
Sites = job.input.release.groups |> Seq.sumBy _.sites.Length
|
||||
|}
|
||||
|
||||
let! r = slurm.Submit (job.name, this.myId, driftersScript, env, grp, part, dep, Some comment)
|
||||
let! r = slurm.Submit (job.name, this.myId, driftersScript, env, grp, part, cpt, dep, Some comment)
|
||||
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
|
||||
|
||||
match Decode.Auto.fromString<SlurmSubmissionResponse> s with
|
||||
@@ -163,6 +164,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
|
||||
let grp = if g.Length > 0 then g[0] else "" // TODO: multiple groups
|
||||
let dep = job.dependency |> Option.map (fun jobId -> $"afterok:{jobId}")
|
||||
let part = job.partition |> Option.defaultValue "long"
|
||||
let cpt = 32
|
||||
|
||||
task {
|
||||
try
|
||||
@@ -179,7 +181,7 @@ type DriftersActor(host: ActorHost, slurm: SlurmClient, settings: Common.Setting
|
||||
SimType = job.model.ToString()
|
||||
|}
|
||||
|
||||
let! r = slurm.Submit (job.name, this.myId, postdriftScript, env, grp, part, dep, Some comment)
|
||||
let! r = slurm.Submit (job.name, this.myId, postdriftScript, env, grp, part, cpt, dep, Some comment)
|
||||
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
|
||||
|
||||
match Decode.Auto.fromString<SlurmSubmissionResponse> s with
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>2.6.6</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Slurm.fs" />
|
||||
<Compile Include="JobActor.fs" />
|
||||
<Compile Include="DriftersActor.fs" />
|
||||
<Compile Include="PlumeActor.fs" />
|
||||
<Compile Include="XtractActor.fs" />
|
||||
<Compile Include="DriftersActor.fs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.Actors" />
|
||||
@@ -20,6 +21,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="ProjNet.FSharp" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="FsToolkit.ErrorHandling" />
|
||||
<PackageReference Include="Thoth.Json.Net" />
|
||||
<PackageReference Include="FSharp.Core" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -2,6 +2,8 @@ module Hipster.JobActor
|
||||
|
||||
open System
|
||||
open System.Collections.Generic
|
||||
open System.IO
|
||||
open System.IO.Compression
|
||||
open System.Text.Json
|
||||
open System.Threading.Tasks
|
||||
open Dapr.Client
|
||||
@@ -284,31 +286,35 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
|
||||
}
|
||||
|
||||
member this.getActiveJobs aid =
|
||||
Log.Debug $"get active jobs for: {this.myId}"
|
||||
task {
|
||||
Log.Debug $"get active jobs for: {this.myId}"
|
||||
|
||||
let active = [|
|
||||
for v in this.jobs do
|
||||
Log.Debug $"job: {aid} -> %A{v.Value}"
|
||||
let active = [|
|
||||
for v in this.jobs do
|
||||
Log.Debug $"job: {aid} -> %A{v.Value}"
|
||||
|
||||
if v.Value.refId = aid then
|
||||
match v.Value.status with
|
||||
| JobStatus.New
|
||||
| JobStatus.Waiting
|
||||
| JobStatus.Failed
|
||||
| JobStatus.Running -> v.Value
|
||||
| _ -> ()
|
||||
else
|
||||
()
|
||||
|]
|
||||
if v.Value.refId = aid then
|
||||
match v.Value.status with
|
||||
| JobStatus.New
|
||||
| JobStatus.Waiting
|
||||
| JobStatus.Failed
|
||||
| JobStatus.Running -> v.Value
|
||||
| _ -> ()
|
||||
else
|
||||
()
|
||||
|]
|
||||
|
||||
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
|
||||
task { return active }
|
||||
Log.Debug $"active jobs: {active.Length}/{this.jobs.Count}"
|
||||
return active
|
||||
}
|
||||
|
||||
member this.getFenceRadius() =
|
||||
Log.Information "fence?"
|
||||
let r = settings.file.fenceRadius
|
||||
Log.Information $"fence: {r}"
|
||||
Task.FromResult r
|
||||
task {
|
||||
Log.Information "fence?"
|
||||
let r = settings.file.fenceRadius
|
||||
Log.Information $"fence: {r}"
|
||||
return r
|
||||
}
|
||||
|
||||
member this.checkFence aid (pts: (float * float) list) =
|
||||
task { return this.validatePoints aid pts }
|
||||
@@ -321,7 +327,7 @@ type JobActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings) =
|
||||
let! msgIds = this.StateManager.GetOrAddStateAsync (this.msgKey, this.msgIds)
|
||||
Log.Debug $"JobActor activating: {this.myId}, jobs={jobs.Count} msgs={msgIds.Count}"
|
||||
|
||||
let doRemove x =
|
||||
let doRemove (x: JobInfo) =
|
||||
x.archiveId = Guid.Empty
|
||||
|| x.refId = Guid.Empty && x.simDays = 0.0
|
||||
|| (DateTime.UtcNow - x.submitTime).Seconds > stateTtl
|
||||
|
||||
@@ -38,7 +38,6 @@ type Arguments =
|
||||
|
||||
let private handleEvents (next: HttpFunc) (ctx: HttpContext) =
|
||||
Log.Debug "handleEvent()"
|
||||
|
||||
task {
|
||||
let! job = ctx.BindJsonAsync<SlurmJobStatusMsg> ()
|
||||
Log.Information $"received event: {job}"
|
||||
@@ -49,6 +48,7 @@ let private handleEvents (next: HttpFunc) (ctx: HttpContext) =
|
||||
match SlurmJobType.FromString job.jobType with
|
||||
| DriftersJob -> ActorProxy.Create<IJobActor> (id, "DriftersActor")
|
||||
| PlumeJob -> ActorProxy.Create<IJobActor> (id, "PlumeActor")
|
||||
| XtractJob -> ActorProxy.Create<IJobActor> (id, "XtractActor")
|
||||
| UnknownJob -> failwith "Unknown job"
|
||||
|
||||
do! proxy.HandleJobEvent (job)
|
||||
@@ -94,6 +94,7 @@ let configureActors (o: Runtime.ActorRuntimeOptions) =
|
||||
o.JsonSerializerOptions <- jopt
|
||||
o.Actors.RegisterActor<DriftersActor> ()
|
||||
o.Actors.RegisterActor<PlumeActor.PlumeActor> ()
|
||||
o.Actors.RegisterActor<XtractActor> ()
|
||||
|
||||
let jsonOptions =
|
||||
let conv = JsonFSharpConverter (JsonFSharpOptions.ThothLike ())
|
||||
|
||||
@@ -149,7 +149,7 @@ type SlurmClient(client: HttpClient, settings: ISlurmClientSettings) =
|
||||
Log.Information $"base: {client.BaseAddress}"
|
||||
client.GetStringAsync "diag"
|
||||
|
||||
member this.Submit(jobName, agentId, exec: string, env: IJobEnv, group, partition, dependency, comment) =
|
||||
member this.Submit(jobName, agentId, exec: string, env: IJobEnv, group, partition, cpu_per_task, dependency, comment) =
|
||||
let jobProps = {
|
||||
SlurmJobProps.empty with
|
||||
name = jobName
|
||||
@@ -162,6 +162,7 @@ type SlurmClient(client: HttpClient, settings: ISlurmClientSettings) =
|
||||
GROUP_ID = group
|
||||
env = env
|
||||
}
|
||||
cpus_per_task = cpu_per_task
|
||||
partition = partition
|
||||
account = Some group[1..] // stripping leading '/' in group name
|
||||
comment = comment
|
||||
|
||||
239
src/Atlantis/src/Server/Hipster/XtractActor.fs
Normal file
239
src/Atlantis/src/Server/Hipster/XtractActor.fs
Normal file
@@ -0,0 +1,239 @@
|
||||
module Hipster.XtractActor
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
open System.Threading.Tasks
|
||||
open Dapr.Client
|
||||
open Dapr.Actors
|
||||
open Dapr.Actors.Client
|
||||
open Dapr.Actors.Runtime
|
||||
open FsToolkit.ErrorHandling
|
||||
open Serilog
|
||||
open Thoth.Json.Net
|
||||
|
||||
open Archmaester.Actors
|
||||
open Hipster.Actors
|
||||
open Hipster.Job
|
||||
open Hipster.Slurm
|
||||
open Oceanbox.ServerPack
|
||||
|
||||
type Amqp = { auth: string; host: string }
|
||||
|
||||
type Slurm = {
|
||||
baseUrl: string
|
||||
slurmApi: string
|
||||
dbdApi: string
|
||||
user: string
|
||||
password: string
|
||||
}
|
||||
|
||||
type AppEnv =
|
||||
| Production
|
||||
| PreProd
|
||||
| Staging
|
||||
| Review
|
||||
static member FromString(s: string) =
|
||||
match s.ToLower () with
|
||||
| "prod" -> Production
|
||||
| "preprod" -> PreProd
|
||||
| "staging" -> Staging
|
||||
| _ -> Review
|
||||
|
||||
type XtractEnv = {
|
||||
JOB_INPUT: string
|
||||
JOB_TYPE: string
|
||||
JOB_ID: Guid
|
||||
REF_ID: Guid
|
||||
} with
|
||||
interface IJobEnv with
|
||||
member this.toJson() = [
|
||||
"JOB_INPUT", Encode.string this.JOB_INPUT
|
||||
"JOB_TYPE", Encode.string this.JOB_TYPE
|
||||
"JOB_ID", Encode.guid this.JOB_ID
|
||||
"REF_ID", Encode.guid this.REF_ID
|
||||
]
|
||||
|
||||
static member empty = {
|
||||
JOB_INPUT = ""
|
||||
JOB_TYPE = ""
|
||||
JOB_ID = Guid.Empty
|
||||
REF_ID = Guid.Empty
|
||||
}
|
||||
|
||||
type XtractActor(host: ActorHost, slurm: SlurmClient, settings: Common.Settings, _observatory: Observer.ObserverFactory)
|
||||
=
|
||||
inherit JobActor.JobActor(host, slurm, settings)
|
||||
|
||||
let xtractScript =
|
||||
let env = settings.appEnv |> AppEnv.FromString
|
||||
// TODO(mrtz): Add additional environments once excavator is migrated to atlas
|
||||
match env with
|
||||
| Production -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
|
||||
| PreProd -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
|
||||
| Staging -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
|
||||
| Review -> "#!/bin/sh\nexec /opt/bin/excavator-production.run"
|
||||
|
||||
interface IXtractActor with
|
||||
member this.SubmitXtract
|
||||
(job: XtractPayload, groups: string array option, partition: string option)
|
||||
: Task<Result<JobInfo, string>> =
|
||||
Log.Debug $"[XtractActor.SubmitXtract]: {this.myId}, job: {job.name}"
|
||||
|
||||
let submitJob () =
|
||||
let g = defaultArg groups [||]
|
||||
let grp = if g.Length > 0 then g[0] else ""
|
||||
let dep = None
|
||||
let part = partition |> Option.defaultValue "short"
|
||||
let cpt = 4
|
||||
|
||||
taskResult {
|
||||
let! archiveName =
|
||||
this.archivist.GetArchiveName job.archiveId
|
||||
|> TaskResult.mapError (fun e ->
|
||||
Log.Error $"[XtractActor.SubmitXtract] Failed to get archive name: %s{e}"
|
||||
$"Failed to get archive name: {e}"
|
||||
)
|
||||
|
||||
let! projection =
|
||||
this.archivist.GetProjection job.archiveId
|
||||
|> TaskResult.mapError (fun e ->
|
||||
Log.Error $"[XtractActor.SubmitXtract] Failed to get projection: %s{e}"
|
||||
$"Failed to get archive projection: {e}"
|
||||
)
|
||||
|
||||
let! basePath =
|
||||
this.archivist.GetBasePath job.archiveId
|
||||
|> TaskResult.mapError (fun e ->
|
||||
Log.Error $"[XtractActor.SubmitXtract] Failed to get basePath: %s{e}"
|
||||
$"Failed to get archive basePath: {e}"
|
||||
)
|
||||
|
||||
let! archiveFiles =
|
||||
this.archivist.GetArchiveFiles job.archiveId
|
||||
|> TaskResult.mapError (fun e ->
|
||||
Log.Error $"[XtractActor.SubmitXtract] Failed to get files: %s{e}"
|
||||
$"Failed to get archive files: {e}"
|
||||
)
|
||||
|
||||
Log.Debug
|
||||
$"[XtractActor.SubmitXtract] Got archive name: '{archiveName}', basePath: '{basePath}', projection: '{projection}'"
|
||||
|
||||
let uniqueOutputDirs =
|
||||
archiveFiles.series
|
||||
|> Array.map (fun f ->
|
||||
let dir = System.IO.Path.GetDirectoryName (f.name)
|
||||
System.IO.Path.Combine (basePath, dir)
|
||||
)
|
||||
|> Array.distinct
|
||||
|
||||
let sampleDirs =
|
||||
uniqueOutputDirs
|
||||
|> Array.take (min 3 uniqueOutputDirs.Length)
|
||||
|> String.concat ", "
|
||||
|
||||
let enrichedJob = {
|
||||
job with
|
||||
basePath = basePath
|
||||
caseName = archiveName
|
||||
projection = projection
|
||||
files = uniqueOutputDirs
|
||||
}
|
||||
|
||||
let jobJson =
|
||||
Encode.Auto.toString (4, enrichedJob, caseStrategy = CaseStrategy.CamelCase)
|
||||
|
||||
let env: XtractEnv = {
|
||||
JOB_INPUT = jobJson |> base64e
|
||||
JOB_TYPE = "xtract"
|
||||
JOB_ID = job.id
|
||||
REF_ID = job.archiveId
|
||||
}
|
||||
|
||||
let comment =
|
||||
Encode.Auto.toString {|
|
||||
User = this.myId
|
||||
JobType = "Xtract"
|
||||
Duration = (job.stop - job.start).TotalHours
|
||||
Positions = 1
|
||||
|}
|
||||
|
||||
let! r = slurm.Submit (job.name, this.myId, xtractScript, env, grp, part, cpt, dep, Some comment)
|
||||
let s = (new StreamReader (r.Content.ReadAsStream ())).ReadToEnd ()
|
||||
|
||||
let! slurmResponse =
|
||||
Decode.Auto.fromString<SlurmSubmissionResponse> s
|
||||
|> Result.mapError (fun e ->
|
||||
Log.Error $"[XtractActor.SubmitXtract] Failed to decode slurm response: %s{e}"
|
||||
e
|
||||
)
|
||||
|
||||
let jobInfo = {
|
||||
owner = this.myId
|
||||
groups = g
|
||||
jobId = slurmResponse.job_id
|
||||
archiveId = job.id
|
||||
refId = job.archiveId
|
||||
status = JobStatus.New
|
||||
name = job.name
|
||||
startTime = job.start
|
||||
simDays = (job.stop - job.start).TotalDays
|
||||
submitTime = DateTime.Now
|
||||
submitQueue = part
|
||||
}
|
||||
|
||||
Log.Debug
|
||||
$"[XtractActor.SubmitXtract]: jobKey: {this.jobsKey}, jobInfo: {jobInfo}, jobs: %A{this.jobs}"
|
||||
this.jobs.Add (slurmResponse.job_id, jobInfo)
|
||||
do! this.StateManager.SetStateAsync (this.jobsKey, this.jobs)
|
||||
return jobInfo
|
||||
}
|
||||
|> TaskResult.mapError (fun e ->
|
||||
Log.Error $"[XtractActor.SubmitXtract] Submit failed: %s{e}"
|
||||
e
|
||||
)
|
||||
|
||||
Log.Debug $"jobs: {this.jobs.Count}"
|
||||
Log.Debug $"submit: {this.myId}"
|
||||
|
||||
// let pts = job.positions |> Array.map (fun p -> (p.Long, p.Lat)) |> Array.toList
|
||||
let pts = [ job.positions |> (fun p -> (p.Long, p.Lat)) ]
|
||||
|
||||
task {
|
||||
let proxy = ActorProxy.Create<IArchiveAccessActor> (this.Id, "ArchiveAccessActor")
|
||||
let! allowed = proxy.CanRun (job.archiveId, JobType.Any)
|
||||
|
||||
if allowed then
|
||||
Log.Debug $"[XtractActor.SubmitXtract] user validated: {this.myId}"
|
||||
|
||||
if this.validatePoints job.archiveId pts then
|
||||
Log.Debug $"XtractActor.SubmitXtract extraction points validated: {this.myId}"
|
||||
return! submitJob ()
|
||||
else
|
||||
let msg = "One or more extraction points outside boundary."
|
||||
Log.Error msg
|
||||
return Error msg
|
||||
else
|
||||
let msg = "You don't have credentials to submit the job."
|
||||
Log.Error msg
|
||||
return Error msg
|
||||
}
|
||||
|
||||
interface IJobActor with
|
||||
member this.Cancel(jobId) = this.cancel jobId
|
||||
member this.HandleJobEvent(job) = this.handleJobEvent job
|
||||
member this.Remove(jobid) = this.remove jobid
|
||||
member this.RemoveById(aid: Guid) = this.removeById aid
|
||||
member this.Clear() = this.clear ()
|
||||
member this.GetJobState(jobid) = this.getJobState jobid
|
||||
member this.GetActiveJobs aid = this.getActiveJobs aid
|
||||
member this.GetFenceRadius() = this.getFenceRadius ()
|
||||
member this.CheckFence(aid, pts) = this.checkFence aid pts
|
||||
|
||||
interface IRemindable with
|
||||
member this.ReceiveReminderAsync(name: string, state: byte[], due: TimeSpan, term: TimeSpan) =
|
||||
Log.Debug $"XtractActor received reminder: {name} {state} {due} {term}"
|
||||
Task.CompletedTask
|
||||
|
||||
override this.OnActivateAsync() = base.OnActivateAsync ()
|
||||
|
||||
override this.OnDeactivateAsync() = base.SaveStateAsync ()
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>1.9.8</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
module Main
|
||||
|
||||
open System
|
||||
open System.Text.Json
|
||||
open System.Text.Json.Serialization
|
||||
open System.Net.Http
|
||||
|
||||
open Microsoft.AspNetCore.Authentication
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.Cors.Infrastructure
|
||||
open Microsoft.AspNetCore.Hosting
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.AspNetCore.SignalR
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Microsoft.Extensions.FileProviders
|
||||
open Microsoft.Extensions.Hosting
|
||||
open Microsoft.Extensions.Logging
|
||||
|
||||
open Argu
|
||||
open Dapr.Actors
|
||||
open Dapr.Actors.Client
|
||||
open Dapr.Client
|
||||
open FSharp.Data
|
||||
open FsToolkit.ErrorHandling
|
||||
open Fable.SignalR
|
||||
open Giraffe
|
||||
open Microsoft.AspNetCore.Authentication
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.Hosting
|
||||
open Microsoft.AspNetCore.Cors.Infrastructure
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.AspNetCore.SignalR
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Microsoft.Extensions.Logging
|
||||
open Microsoft.Extensions.FileProviders
|
||||
open Microsoft.Extensions.Hosting
|
||||
open System.Net.Http
|
||||
open Oceanbox.DataAgent
|
||||
open Polly
|
||||
open Prometheus
|
||||
open Saturn
|
||||
open Saturn.Dapr
|
||||
open Saturn.OpenTelemetry
|
||||
open Saturn.Observer
|
||||
open Saturn.OpenTelemetry
|
||||
open Sentry
|
||||
open Sentry.AspNetCore
|
||||
open Sentry.Extensibility
|
||||
@@ -37,6 +39,7 @@ open Serilog.Sinks.OpenTelemetry
|
||||
|
||||
open Atlantis
|
||||
open Atlantis.Shared
|
||||
open Oceanbox.DataAgent
|
||||
open Oceanbox.ServerPack.MultiAuth
|
||||
open Saturn.OpenFga
|
||||
open Settings
|
||||
@@ -82,7 +85,8 @@ let configureSerilog () =
|
||||
let configureLogging (builder: ILoggingBuilder) =
|
||||
builder
|
||||
.ClearProviders()
|
||||
.AddSerilog() |> ignore
|
||||
.AddSerilog()
|
||||
|> ignore
|
||||
|
||||
let corsPolicy (policy: CorsPolicyBuilder) =
|
||||
policy
|
||||
@@ -91,7 +95,7 @@ let corsPolicy (policy: CorsPolicyBuilder) =
|
||||
.AllowAnyMethod()
|
||||
.WithOrigins(appsettings.file.allowedOrigins)
|
||||
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
||||
|> ignore
|
||||
|> ignore
|
||||
|
||||
type UserIdProvider() =
|
||||
interface IUserIdProvider with
|
||||
@@ -194,6 +198,7 @@ let configureActors (options: Runtime.ActorRuntimeOptions) =
|
||||
options.Actors.RegisterActor<Archmaester.ArchivistActor.ArchivistActor>()
|
||||
options.Actors.RegisterActor<Hipster.DriftersActor.DriftersActor>()
|
||||
options.Actors.RegisterActor<Hipster.PlumeActor.PlumeActor>()
|
||||
options.Actors.RegisterActor<Hipster.XtractActor.XtractActor>()
|
||||
printfn "Configured actors."
|
||||
|
||||
// Options for sentry
|
||||
@@ -253,12 +258,12 @@ let stopImpersonating (next: HttpFunc) (ctx: HttpContext) =
|
||||
let getBarentsWatchToken (next: HttpFunc) (ctx: HttpContext) =
|
||||
task {
|
||||
let! tokenRes =
|
||||
tryGetEnv "BARENTSWATCH_CLIENT_ID"
|
||||
|> Option.bind (fun id ->
|
||||
tryGetEnv "BARENTSWATCH_SECRET"
|
||||
|> Option.map (BarentsWatch.getToken appsettings.redis id))
|
||||
|> Option.defaultValue (Error "Secret or client id missing" |> async.Return)
|
||||
|> Async.StartAsTask
|
||||
taskResult {
|
||||
let! id = tryGetEnv "BARENTSWATCH_CLIENT_ID" |> Result.requireSome "Missing barentswatch client id"
|
||||
let! secret = tryGetEnv "BARENTSWATCH_SECRET" |> Result.requireSome "Missing barentswatch secret"
|
||||
let! token = BarentsWatch.getToken appsettings.redis id secret
|
||||
return token
|
||||
}
|
||||
|
||||
match tokenRes with
|
||||
| Error err ->
|
||||
@@ -317,6 +322,7 @@ let webApp =
|
||||
routeStartsWith "/api/v1/-/" >=> requireUser >=> choose [
|
||||
Api.Endpoints.driftersEndpoints
|
||||
Api.Endpoints.plumeEndpoints
|
||||
Api.Endpoints.xtractEndpoints
|
||||
Api.Endpoints.inboxEndpoints
|
||||
Archmaester.Api.Endpoints.inventoryEndpoints
|
||||
]
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ParallelCompilation>true</ParallelCompilation>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<Version>2.102.0</Version>
|
||||
<RootNamespace>Server</RootNamespace>
|
||||
|
||||
@@ -81,7 +81,7 @@ let configureEnv () : Async<unit> =
|
||||
| Some x when x = "1" || x = "true" ->
|
||||
Log.Information "[Settings] > Waiting for Dapr..."
|
||||
Threading.Thread.Sleep 2000
|
||||
do! Async.Sleep (TimeSpan.FromMilliseconds 2000)
|
||||
do! Async.Sleep (TimeSpan.FromMilliseconds 2000.0)
|
||||
do! setupAzureEnv ()
|
||||
Log.Information $"[Settings] > Azure Keyvault credentials in {appsettings.keyVault}"
|
||||
do! setupDbEnv ()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"Argu": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.2.5, )",
|
||||
@@ -56,9 +56,7 @@
|
||||
"dependencies": {
|
||||
"Azure.Core": "1.44.1",
|
||||
"Microsoft.Identity.Client": "4.67.2",
|
||||
"Microsoft.Identity.Client.Extensions.Msal": "4.67.2",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
"Microsoft.Identity.Client.Extensions.Msal": "4.67.2"
|
||||
}
|
||||
},
|
||||
"Azure.Security.KeyVault.Secrets": {
|
||||
@@ -67,10 +65,7 @@
|
||||
"resolved": "4.7.0",
|
||||
"contentHash": "uOPCojkm41V4dKTORyGzl3/f/lriKpxSQ43fWDn4StRJBVmbF1F/DNWJhwm207kCnqgE/W9+tskJSimIKHCZkw==",
|
||||
"dependencies": {
|
||||
"Azure.Core": "1.44.1",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Text.Json": "6.0.10",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
"Azure.Core": "1.44.1"
|
||||
}
|
||||
},
|
||||
"Dapr.Actors": {
|
||||
@@ -251,8 +246,7 @@
|
||||
"resolved": "1.3.13",
|
||||
"contentHash": "znp8odpdkVGKVX0AvbhiXdmeMi0KJ+A4AyAQWSkfAEAe4Z4clRE+rVhrLnAGrFD1VEIUX2lsQ4o84ywpWZUSGw==",
|
||||
"dependencies": {
|
||||
"FSharp.Core": "4.7.0",
|
||||
"System.Text.Json": "6.0.0"
|
||||
"FSharp.Core": "4.7.0"
|
||||
}
|
||||
},
|
||||
"FSharpPlus": {
|
||||
@@ -273,8 +267,7 @@
|
||||
"FSharp.Core": "6.0.0",
|
||||
"FSharp.SystemTextJson": "1.3.13",
|
||||
"Giraffe.ViewEngine": "1.4.0",
|
||||
"Microsoft.IO.RecyclableMemoryStream": "3.0.1",
|
||||
"System.Text.Json": "8.0.5"
|
||||
"Microsoft.IO.RecyclableMemoryStream": "3.0.1"
|
||||
}
|
||||
},
|
||||
"IdentityModel.AspNetCore": {
|
||||
@@ -489,12 +482,7 @@
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
|
||||
"System.ClientModel": "1.1.0",
|
||||
"System.Diagnostics.DiagnosticSource": "6.0.1",
|
||||
"System.Memory.Data": "6.0.0",
|
||||
"System.Numerics.Vectors": "4.5.0",
|
||||
"System.Text.Encodings.Web": "6.0.0",
|
||||
"System.Text.Json": "6.0.10",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
"System.Memory.Data": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Azure.Security.KeyVault.Keys": {
|
||||
@@ -502,10 +490,7 @@
|
||||
"resolved": "4.6.0",
|
||||
"contentHash": "1KbCIkXmLaj+kDDNm1Va5rNlzgcJ/fVtnsoVmzZPKa38jz6DXhPyojdvGaOX8AdupGJceg0X1vrsGvZKN79Qzw==",
|
||||
"dependencies": {
|
||||
"Azure.Core": "1.37.0",
|
||||
"System.Memory": "4.5.4",
|
||||
"System.Text.Json": "4.7.2",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
"Azure.Core": "1.37.0"
|
||||
}
|
||||
},
|
||||
"Azure.Storage.Blobs": {
|
||||
@@ -513,8 +498,7 @@
|
||||
"resolved": "12.23.0",
|
||||
"contentHash": "wokJ5KX/iViQQ32xyCu69+Ter0aR4B9QQ+oR9NCpc/WPIanxnDErrmFfdmE7K8ZdccjHkvE/wEnqJxaF1+5wFg==",
|
||||
"dependencies": {
|
||||
"Azure.Storage.Common": "12.22.0",
|
||||
"System.Text.Json": "6.0.10"
|
||||
"Azure.Storage.Common": "12.22.0"
|
||||
}
|
||||
},
|
||||
"Azure.Storage.Common": {
|
||||
@@ -586,8 +570,7 @@
|
||||
"Fable.Remoting.MsgPack": "1.24.0",
|
||||
"Fable.SignalR.Shared": "2.1.0",
|
||||
"Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson": "9.0.0",
|
||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis": "9.0.0",
|
||||
"System.Text.Encodings.Web": "9.0.0"
|
||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Fable.SignalR.Shared": {
|
||||
@@ -683,8 +666,7 @@
|
||||
"resolved": "5.3.2",
|
||||
"contentHash": "LFtxXpQNor8az1ez3rN9oz2cqf/06i9yTrPyJ9R83qLEpFAU7Of0WL2hoSXzLHer4lh+6mO1NV4VQFiBzNRtjw==",
|
||||
"dependencies": {
|
||||
"FSharp.Core": "4.3.2",
|
||||
"System.Reflection.Emit.Lightweight": "4.3.0"
|
||||
"FSharp.Core": "4.3.2"
|
||||
}
|
||||
},
|
||||
"Giraffe.ViewEngine": {
|
||||
@@ -836,8 +818,7 @@
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Http.Features": "2.2.0",
|
||||
"System.Text.Encodings.Web": "4.5.0"
|
||||
"Microsoft.AspNetCore.Http.Features": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.Http.Features": {
|
||||
@@ -881,8 +862,7 @@
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "9ErxAAKaDzxXASB/b5uLEkLgUWv1QbeVxyJYEHQwMaxXOeFFVkQxiq8RyfVcifLU7NR0QY0p3acqx4ZpYfhHDg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Net.Http.Headers": "2.2.0",
|
||||
"System.Text.Encodings.Web": "4.5.0"
|
||||
"Microsoft.Net.Http.Headers": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
@@ -1089,8 +1069,7 @@
|
||||
"resolved": "4.67.2",
|
||||
"contentHash": "37t0TfekfG6XM8kue/xNaA66Qjtti5Qe1xA41CK+bEd8VD76/oXJc+meFJHGzygIC485dCpKoamG/pDfb9Qd7Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Abstractions": "6.35.0",
|
||||
"System.Diagnostics.DiagnosticSource": "6.0.1"
|
||||
"Microsoft.IdentityModel.Abstractions": "6.35.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Identity.Client.Extensions.Msal": {
|
||||
@@ -1158,8 +1137,7 @@
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "iZNkjYqlo8sIOI0bQfpsSoMTmB/kyvmV2h225ihyZT33aTp48ZpF6qYnXxzSXmHt8DpBAwBTX+1s1UFLbYfZKg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "2.2.0",
|
||||
"System.Buffers": "4.5.0"
|
||||
"Microsoft.Extensions.Primitives": "2.2.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.StringTools": {
|
||||
@@ -1188,10 +1166,7 @@
|
||||
"OpenTelemetry.Api": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.10.0",
|
||||
"contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w==",
|
||||
"dependencies": {
|
||||
"System.Diagnostics.DiagnosticSource": "9.0.0"
|
||||
}
|
||||
"contentHash": "HcmxppwGFna1oY8cLX6hZ/nU1dw07UutfOVCltrbVE3RNYwRD7qFdQRtQQAoKZnbXE9yW4QMdtohcLClNFOk8w=="
|
||||
},
|
||||
"OpenTelemetry.Api.ProviderBuilderExtensions": {
|
||||
"type": "Transitive",
|
||||
@@ -1272,17 +1247,13 @@
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Options": "9.0.0",
|
||||
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.10.0, 2.0.0)",
|
||||
"StackExchange.Redis": "[2.6.122, 3.0.0)",
|
||||
"System.Reflection.Emit.Lightweight": "4.7.0"
|
||||
"StackExchange.Redis": "[2.6.122, 3.0.0)"
|
||||
}
|
||||
},
|
||||
"Pipelines.Sockets.Unofficial": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.8",
|
||||
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==",
|
||||
"dependencies": {
|
||||
"System.IO.Pipelines": "5.0.1"
|
||||
}
|
||||
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ=="
|
||||
},
|
||||
"Polly": {
|
||||
"type": "Transitive",
|
||||
@@ -1300,11 +1271,7 @@
|
||||
"ProjNET": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.0",
|
||||
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.3",
|
||||
"System.Numerics.Vectors": "4.5.0"
|
||||
}
|
||||
"contentHash": "iMJG8qpGJ8SjFrB044O8wgo0raAWCdG1Bvly0mmVcjzsrexDHhC+dUct6Wb1YwQtupMBjSTWq7Fn00YeNErprA=="
|
||||
},
|
||||
"prometheus-net": {
|
||||
"type": "Transitive",
|
||||
@@ -1404,18 +1371,12 @@
|
||||
"Pipelines.Sockets.Unofficial": "2.2.8"
|
||||
}
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "pL2ChpaRRWI/p4LXyy4RgeWlYF2sgfj/pnVMvBqwNFr5cXg7CXNnWZWxrOONLg8VGdFB8oB+EG2Qw4MLgTOe+A=="
|
||||
},
|
||||
"System.ClientModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==",
|
||||
"dependencies": {
|
||||
"System.Memory.Data": "1.0.2",
|
||||
"System.Text.Json": "6.0.9"
|
||||
"System.Memory.Data": "1.0.2"
|
||||
}
|
||||
},
|
||||
"System.ComponentModel.Annotations": {
|
||||
@@ -1431,11 +1392,6 @@
|
||||
"System.Security.Cryptography.ProtectedData": "4.4.0"
|
||||
}
|
||||
},
|
||||
"System.Diagnostics.DiagnosticSource": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "8hy61dsFYYSDjT9iTAfygGMU3A0EAnG69x5FUXeKsCjMhBmtTBt4UMUEW3ipprFoorOW6Jw/7hDMjXtlrsOvVQ=="
|
||||
},
|
||||
"System.IdentityModel.Tokens.Jwt": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
@@ -1450,33 +1406,10 @@
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g=="
|
||||
},
|
||||
"System.IO.Pipelines": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.1",
|
||||
"contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg=="
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.5",
|
||||
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw=="
|
||||
},
|
||||
"System.Memory.Data": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ==",
|
||||
"dependencies": {
|
||||
"System.Text.Json": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
|
||||
},
|
||||
"System.Reflection.Emit.Lightweight": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.7.0",
|
||||
"contentHash": "a4OLB4IITxAXJeV74MDx49Oq2+PsF6Sml54XAFv+2RyWwtDBcabzoxiiJRhdhx+gaohLh4hEGCLQyBozXoQPqA=="
|
||||
"contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ=="
|
||||
},
|
||||
"System.Security.Cryptography.Pkcs": {
|
||||
"type": "Transitive",
|
||||
@@ -1496,16 +1429,6 @@
|
||||
"System.Security.Cryptography.Pkcs": "9.0.2"
|
||||
}
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.5",
|
||||
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg=="
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
|
||||
},
|
||||
"archmaester": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
@@ -1545,7 +1468,7 @@
|
||||
"Hipster.Api": "[1.0.1, )",
|
||||
"Oceanbox.DataAgent": "[7.3.0, )",
|
||||
"Oceanbox.DataAgent.Api": "[7.2.1, )",
|
||||
"Oceanbox.ServerPack": "[1.33.0, )",
|
||||
"Oceanbox.ServerPack": "[1.40.0, )",
|
||||
"Petimeter.Api": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
@@ -1568,6 +1491,7 @@
|
||||
"FSharp.Data": "[6.4.1, )",
|
||||
"FSharp.SystemTextJson": "[1.3.13, )",
|
||||
"FSharpPlus": "[1.7.0, )",
|
||||
"FsToolkit.ErrorHandling": "[5.0.1, )",
|
||||
"Newtonsoft.Json": "[13.0.3, )",
|
||||
"ProjNet.FSharp": "[5.2.0, )",
|
||||
"Serilog": "[4.2.0, )",
|
||||
@@ -1682,6 +1606,15 @@
|
||||
"FSharp.Core": "4.7.1"
|
||||
}
|
||||
},
|
||||
"FsToolkit.ErrorHandling": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[5.0.1, )",
|
||||
"resolved": "5.0.1",
|
||||
"contentHash": "93oG3WSogK05H4gkikAmx5pBf30TQJfO1Jky+o/N/nv+RTP3nfOfjlmCHzuyUjQCRFOQog/xQabcky+WBWceeQ==",
|
||||
"dependencies": {
|
||||
"FSharp.Core": "9.0.300"
|
||||
}
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[3.1.3, )",
|
||||
@@ -1721,10 +1654,7 @@
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.5.0, )",
|
||||
"resolved": "2.5.0",
|
||||
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.4"
|
||||
}
|
||||
"contentHash": "5/+2O2ADomEdUn09mlSigACdqvAf0m/pVPGtIPEPQWnyrVykYY0NlfXLIdkMgi41kvH9kNrPqYaFBTZtHYH7Xw=="
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "CentralTransitive",
|
||||
@@ -1789,8 +1719,7 @@
|
||||
"contentHash": "vlOKvmigJ3Sumoulp1HwCTFXgX4KuERVGIIw4ZqmhgUJnSiApDmY183ddzzHo2FIdIJ8vGwrMGx98v9cLAezFA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Http": "9.0.9",
|
||||
"System.ComponentModel.Annotations": "5.0.0",
|
||||
"System.Diagnostics.DiagnosticSource": "9.0.9"
|
||||
"System.ComponentModel.Annotations": "5.0.0"
|
||||
}
|
||||
},
|
||||
"ProjNet.FSharp": {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
},
|
||||
"fga": {
|
||||
"apiUrl": "http://prod-openfga.openfga.svc.cluster.local:8080",
|
||||
"apiUrl": "http://staging-openfga.staging-openfga.svc.cluster.local:8080",
|
||||
"apiKey": "",
|
||||
"storeId": "01JKTZXMP7ANN4GG2P5W8Y56M6",
|
||||
"modelId": "01JKTZYMCZZBVSBG66W27XMW0A"
|
||||
@@ -63,6 +63,10 @@
|
||||
"allowedOrigins": [
|
||||
"http://*.oceanbox.io",
|
||||
"https://*.oceanbox.io",
|
||||
"https://*.oceanbox.io:8080",
|
||||
"https://*.oceanbox.io:10380",
|
||||
"https://*.vtn.obx",
|
||||
"https://*.tox.obx",
|
||||
],
|
||||
"appName": "atlantis",
|
||||
"appEnv": "<x>",
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
architecture: standalone
|
||||
|
||||
# NOTE(mrtz): Hack for working with legacy registry
|
||||
global:
|
||||
security:
|
||||
allowInsecureImages: true
|
||||
image:
|
||||
repository: bitnamilegacy/redis
|
||||
|
||||
replica:
|
||||
replicaCount: 1
|
||||
|
||||
auth:
|
||||
enabled: true
|
||||
sentinel: true
|
||||
password: ""
|
||||
usePasswordFiles: false
|
||||
existingSecretPasswordKey: ""
|
||||
existingSecret: <x>-atlantis-redis
|
||||
|
||||
master:
|
||||
resources:
|
||||
limits:
|
||||
ephemeral-storage: 1024Mi
|
||||
memory: 192Mi
|
||||
requests:
|
||||
cpu: 150m
|
||||
ephemeral-storage: 50Mi
|
||||
memory: 128Mi
|
||||
@@ -1,103 +1,103 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
tag: latest
|
||||
tag: latest
|
||||
|
||||
podAnnotations:
|
||||
dapr.io/enabled: "true"
|
||||
dapr.io/app-id: "<x>-atlantis"
|
||||
dapr.io/app-port: "8085"
|
||||
dapr.io/api-token-secret: "dapr-api-token"
|
||||
dapr.io/config: "tracing"
|
||||
dapr.io/app-protocol: "http"
|
||||
dapr.io/log-as-json: "true"
|
||||
dapr.io/sidecar-cpu-request: "10m"
|
||||
dapr.io/sidecar-memory-request: "50Mi"
|
||||
# dapr.io/sidecar-cpu-limit: "300m"
|
||||
# dapr.io/sidecar-memory-limit: "1000Mi"
|
||||
dapr.io/enabled: "true"
|
||||
dapr.io/app-id: "<x>-atlantis"
|
||||
dapr.io/app-port: "8085"
|
||||
dapr.io/api-token-secret: "dapr-api-token"
|
||||
dapr.io/config: "tracing"
|
||||
dapr.io/app-protocol: "http"
|
||||
dapr.io/log-as-json: "true"
|
||||
dapr.io/sidecar-cpu-request: "10m"
|
||||
dapr.io/sidecar-memory-request: "50Mi"
|
||||
# dapr.io/sidecar-cpu-limit: "300m"
|
||||
# dapr.io/sidecar-memory-limit: "1000Mi"
|
||||
|
||||
env:
|
||||
- name: APP_NAMESPACE
|
||||
value: <x>-atlantis
|
||||
- name: APP_VERSION
|
||||
value: "<x>-tilt"
|
||||
- name: LOG_LEVEL
|
||||
value: "verbose"
|
||||
- name: REDIS_USER
|
||||
value: default
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-redis
|
||||
key: redis-password
|
||||
- name: DB_HOST
|
||||
value: <x>-atlantis-db-rw
|
||||
- name: DB_PORT
|
||||
value: "5432"
|
||||
- name: DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-db-app
|
||||
key: username
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-db-app
|
||||
key: password
|
||||
- name: DAPR_API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dapr-api-token
|
||||
key: token
|
||||
- name: ANALYTICS_WEB_ID
|
||||
value: 6f26c702-2c6d-46ea-8122-ffcedda5f762
|
||||
- name: APP_NAMESPACE
|
||||
value: <x>-atlantis
|
||||
- name: APP_VERSION
|
||||
value: "<x>-tilt"
|
||||
- name: LOG_LEVEL
|
||||
value: "verbose"
|
||||
- name: REDIS_USER
|
||||
value: default
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-redis
|
||||
key: redis-password
|
||||
- name: DB_HOST
|
||||
value: <x>-atlantis-db-rw
|
||||
- name: DB_PORT
|
||||
value: "5432"
|
||||
- name: DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-db-app
|
||||
key: username
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: <x>-atlantis-db-app
|
||||
key: password
|
||||
- name: DAPR_API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dapr-api-token
|
||||
key: token
|
||||
- name: ANALYTICS_WEB_ID
|
||||
value: 6f26c702-2c6d-46ea-8122-ffcedda5f762
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
|
||||
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
hosts:
|
||||
- host: <x>-atlantis.dev.oceanbox.io
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
internal:
|
||||
- path: /internal
|
||||
pathType: ImplementationSpecific
|
||||
- path: /dapr
|
||||
pathType: ImplementationSpecific
|
||||
- path: /actors
|
||||
pathType: ImplementationSpecific
|
||||
- path: /job
|
||||
pathType: ImplementationSpecific
|
||||
- path: /events
|
||||
pathType: ImplementationSpecific
|
||||
- path: /metrics
|
||||
pathType: ImplementationSpecific
|
||||
tls:
|
||||
- hosts:
|
||||
- <x>-atlantis.dev.oceanbox.io
|
||||
secretName: <x>-atlantis-tls
|
||||
enabled: true
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
|
||||
nginx.ingress.kubernetes.io/whitelist-source-range: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
hosts:
|
||||
- host: <x>-atlantis.dev.oceanbox.io
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
internal:
|
||||
- path: /internal
|
||||
pathType: ImplementationSpecific
|
||||
- path: /dapr
|
||||
pathType: ImplementationSpecific
|
||||
- path: /actors
|
||||
pathType: ImplementationSpecific
|
||||
- path: /job
|
||||
pathType: ImplementationSpecific
|
||||
- path: /events
|
||||
pathType: ImplementationSpecific
|
||||
- path: /metrics
|
||||
pathType: ImplementationSpecific
|
||||
tls:
|
||||
- hosts:
|
||||
- <x>-atlantis.dev.oceanbox.io
|
||||
secretName: <x>-atlantis-tls
|
||||
|
||||
storage:
|
||||
enabled: true
|
||||
size: 1G
|
||||
accessMode: ReadWriteOnce
|
||||
storageClass: ceph-rdb
|
||||
enabled: true
|
||||
size: 1G
|
||||
accessMode: ReadWriteOnce
|
||||
storageClass: ceph-rdb
|
||||
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
|
||||
cluster:
|
||||
backup:
|
||||
enabled: false
|
||||
backup:
|
||||
enabled: false
|
||||
|
||||
redis:
|
||||
enabled: true
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# yaml-language-server: $schema=https://gitlab.com/gitlab-org/gitlab/-/raw/master/app/assets/javascripts/editor/schema/ci.json
|
||||
variables:
|
||||
SKIP_TESTS: "true"
|
||||
SKIP_SINGULARITY: "true"
|
||||
SKIP_TESTS: "true"
|
||||
|
||||
include:
|
||||
- project: oceanbox/gitlab-ci
|
||||
ref: v4.2
|
||||
file: DotnetDeployment.gitlab-ci.yml
|
||||
inputs:
|
||||
project-name: codex
|
||||
project-dir: src/Codex
|
||||
- project: oceanbox/gitlab-ci
|
||||
ref: v4.5
|
||||
file: DotnetDeployment.gitlab-ci.yml
|
||||
inputs:
|
||||
project-name: codex
|
||||
project-dir: src/Codex
|
||||
|
||||
dockerize-codex:
|
||||
tags:
|
||||
- nix
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
WORKDIR /app
|
||||
|
||||
COPY dist /app
|
||||
|
||||
@@ -2,6 +2,7 @@ name='codex'
|
||||
cluster='oceanbox'
|
||||
|
||||
env=os.getenv('APP_ENV')
|
||||
username=os.getenv('USER')
|
||||
namespace=os.getenv('APP_NAMESPACE')
|
||||
|
||||
app = '{}-{}'.format(env, name)
|
||||
@@ -39,7 +40,9 @@ docker_build_with_restart(
|
||||
ignore = [ 'src/Client' ]
|
||||
)
|
||||
|
||||
k8s_yaml('tilt/k8s.yaml')
|
||||
local('cat tilt/k8s.yaml | sed "s/<x>/{}/" > tilt/_k8s.yaml'.format(username))
|
||||
|
||||
k8s_yaml('tilt/_k8s.yaml')
|
||||
k8s_resource(app, port_forwards='8085:8085')
|
||||
|
||||
# vim:ft=python
|
||||
@@ -5,7 +5,7 @@
|
||||
}:
|
||||
dockerTools.buildLayeredImage {
|
||||
name = "Codex";
|
||||
tag = "0.0.0-alpha.1";
|
||||
tag = "0.0.1";
|
||||
created = "now";
|
||||
|
||||
contents = [
|
||||
@@ -16,12 +16,12 @@ dockerTools.buildLayeredImage {
|
||||
];
|
||||
|
||||
extraCommands = ''
|
||||
mv -f lib/Codex app
|
||||
mkdir -p app
|
||||
cp -r ${server}/lib/Codex/* app
|
||||
'';
|
||||
|
||||
config = {
|
||||
cmd = [ "Codex.Server" ];
|
||||
workingDir = "/app";
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ open Fable.Core
|
||||
open Fable.Core.JsInterop
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
open FS.FluentUI
|
||||
|
||||
module Archive =
|
||||
let private fetchArchiveAcl (id: System.Guid) : JS.Promise<Result<Archmaester.Dto.ArchiveAcl, string>> =
|
||||
@@ -29,16 +30,6 @@ module Archive =
|
||||
return subs
|
||||
}
|
||||
|
||||
let private deleteArchive (id: System.Guid) =
|
||||
promise {
|
||||
try
|
||||
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
|
||||
return res
|
||||
with e ->
|
||||
console.error("Error deleting archive: %o", e)
|
||||
return Error "Error deleting archive"
|
||||
}
|
||||
|
||||
let private addArchiveGroup (archiveId: System.Guid) (group: string) =
|
||||
promise {
|
||||
try
|
||||
@@ -227,9 +218,6 @@ module Archive =
|
||||
let View (archiveId: System.Guid) =
|
||||
let loading, setLoading = React.useState true
|
||||
let error, setError = React.useState<string option> None
|
||||
let editing, setEditing = React.useState false
|
||||
let deleting, setDeleting = React.useState false
|
||||
let deleted, setDeleted = React.useState false
|
||||
let selectedGroup, setSelectedGroup = React.useState<string option> None
|
||||
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
|
||||
let aclOpt, setAcl = React.useState<Archmaester.Dto.ArchiveAcl option> None
|
||||
@@ -257,21 +245,6 @@ module Archive =
|
||||
| None ->
|
||||
console.warn("ACL has not been downloaded")
|
||||
|
||||
let handleDeleteArchive (id: System.Guid) =
|
||||
deleteArchive id
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok deleted ->
|
||||
if deleted then
|
||||
console.info("Archive deleted successfully")
|
||||
setDeleted true
|
||||
else
|
||||
setError (Some "Failed to delete archive")
|
||||
| Error err ->
|
||||
console.error("Error deleting archive: %s", err)
|
||||
setError (Some err)
|
||||
)
|
||||
|
||||
let handleSelectedGroupChange (groupOpt: string option) =
|
||||
console.debug("Selected group: %s", groupOpt)
|
||||
setSelectedGroup groupOpt
|
||||
@@ -319,94 +292,27 @@ module Archive =
|
||||
| None ->
|
||||
match archiveOpt with
|
||||
| Some archive ->
|
||||
Html.h1 (sprintf "Archive %s" archive.name)
|
||||
Fui.text.title1 (sprintf "Archive %s" archive.name)
|
||||
|
||||
if deleting then
|
||||
Html.h2 "Deleting archive ..."
|
||||
else
|
||||
Html.none
|
||||
|
||||
if deleted then
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.h2 "Archive successfully deleted"
|
||||
Html.a [
|
||||
prop.href (Router.format "archives")
|
||||
prop.text "Return to archives listing"
|
||||
]
|
||||
]
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "gap-8" ]
|
||||
prop.children [
|
||||
Archives.EditArchiveDialog archive (fun edited ->
|
||||
Some
|
||||
{archive with
|
||||
name = edited.Name
|
||||
startTime = edited.StartTime
|
||||
endTime = edited.StartTime.AddHours(edited.Frames)
|
||||
frames = edited.Frames
|
||||
isPublished = edited.Published
|
||||
isPublic = edited.Public
|
||||
}
|
||||
|> setArchive
|
||||
console.debug ("response: ", edited)
|
||||
)
|
||||
Archives.DeleteArchiveDialog archive
|
||||
]
|
||||
else
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "gap-8" ]
|
||||
prop.children [
|
||||
if deleting then
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setDeleting false
|
||||
handleDeleteArchive archive.archiveId
|
||||
)
|
||||
prop.text "Save"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setDeleting false
|
||||
)
|
||||
prop.text "Cancel"
|
||||
]
|
||||
elif editing then
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setEditing false
|
||||
)
|
||||
prop.text "Save"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setEditing false
|
||||
)
|
||||
prop.text "Cancel"
|
||||
]
|
||||
else
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setEditing true
|
||||
)
|
||||
prop.text "Edit"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick (fun ev ->
|
||||
setDeleting true
|
||||
)
|
||||
prop.text "Delete"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
if editing then
|
||||
Html.form [
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.label [
|
||||
prop.htmlFor "published-checkbox"
|
||||
prop.text "Published: "
|
||||
]
|
||||
|
||||
Html.input [
|
||||
prop.id "published-checkbox"
|
||||
prop.type' "checkbox"
|
||||
prop.custom ("checked", archive.isPublished)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
else
|
||||
Html.none
|
||||
]
|
||||
|
||||
Archives.InfoSection archive
|
||||
|
||||
@@ -744,4 +650,4 @@ module Archive =
|
||||
|
||||
| None ->
|
||||
Html.h1 "Archive not found"
|
||||
]
|
||||
]
|
||||
@@ -1,6 +1,8 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
open Fable.Core
|
||||
open Feliz
|
||||
open FS.FluentUI
|
||||
|
||||
type Archives =
|
||||
[<ReactComponent>]
|
||||
@@ -13,49 +15,131 @@ type Archives =
|
||||
Html.section [
|
||||
prop.classes [ "flex-row"; "flex-wrap"; "gap-16" ]
|
||||
prop.children [
|
||||
Html.ul [
|
||||
prop.children [
|
||||
Html.li [
|
||||
prop.text (sprintf "Description: %s" archive.description)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Archive type: %s" (string archive.archiveType))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Projection: %s" archive.projection)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Frequency: %d" archive.freq)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Frames: %d" archive.frames)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Created: %s" (archive.created.ToLongDateString()))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Start time: %s" (archive.startTime.ToLongDateString()))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "End time: %s" (archive.endTime.ToLongDateString()))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Length: %d days %d hours" archiveLength.Days archiveLength.Hours)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Owner: %s" archive.owner)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Expires: %s" (archive.expires |> Option.map string |> Option.defaultValue ""))
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Publised: %b" archive.isPublished)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Public: %b" archive.isPublic)
|
||||
]
|
||||
Html.li [
|
||||
prop.text (sprintf "Location: %s" "tos")
|
||||
Fui.table [
|
||||
table.classes [ "flex-basis-7"; "flex-grow" ]
|
||||
table.children [
|
||||
Fui.tableBody [
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.textDescriptionRegular [])
|
||||
tableCellLayout.children [ Fui.text "Description" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text archive.description ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.contactCardGenericRegular [])
|
||||
tableCellLayout.children [ Fui.text "Archive type" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.archiveType) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.globeSurfaceRegular [])
|
||||
tableCellLayout.children [ Fui.text "Projection" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text archive.projection ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.timerRegular [])
|
||||
tableCellLayout.children [ Fui.text "Frequency" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.freq) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.filmstripRegular [])
|
||||
tableCellLayout.children [ Fui.text "Frames" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.frames) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.calendarAddRegular [])
|
||||
tableCellLayout.children [ Fui.text "Time created" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (archive.created.ToLongDateString()) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.playRegular [])
|
||||
tableCellLayout.children [ Fui.text "Start time" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (archive.startTime.ToLongDateString()) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.stopRegular [])
|
||||
tableCellLayout.children [ Fui.text "End time" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (archive.endTime.ToLongDateString()) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.autoFitWidthRegular [])
|
||||
tableCellLayout.children [ Fui.text "Length" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (sprintf "%d days %d hours" archiveLength.Days archiveLength.Hours) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.eyeRegular [])
|
||||
tableCellLayout.children [ Fui.text "Published" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.isPublished) ]
|
||||
]
|
||||
]
|
||||
Fui.tableRow [
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.media (Fui.icon.peopleEyeRegular [])
|
||||
tableCellLayout.children [ Fui.text "Public" ]
|
||||
]
|
||||
]
|
||||
Fui.tableCell [ Fui.text (string archive.isPublic) ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -70,3 +154,342 @@ type Archives =
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
static member EditArchiveDialog(archive: Archmaester.Dto.ArchiveProps) onEdit =
|
||||
let initForm : Remoting.EditArchiveRequest =
|
||||
{
|
||||
Name = archive.name
|
||||
StartTime = archive.startTime
|
||||
Frames = archive.frames
|
||||
Published = archive.isPublished
|
||||
Public = archive.isPublic
|
||||
}
|
||||
let form, setForm = React.useState initForm
|
||||
let isOpen, setIsOpen = React.useState false
|
||||
let errMsg, setErrMsg = React.useState None
|
||||
let dataSet, setDataSet = React.useState None
|
||||
|
||||
React.useEffectOnce (fun _ ->
|
||||
Remoting.adminApi.getArchiveDataSet archive.archiveId
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok ds -> setDataSet (Some ds)
|
||||
| Error msg ->
|
||||
Browser.Dom.console.error("Error fetching dataset: %s", msg)
|
||||
)
|
||||
)
|
||||
|
||||
let handleEditArchive () =
|
||||
let utcTime = System.DateTime(form.StartTime.Year, form.StartTime.Month, form.StartTime.Day, 0, 0, 0, System.DateTimeKind.Utc)
|
||||
Remoting.adminApi.updateArchive archive.archiveId {form with StartTime = utcTime}
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok newArchive ->
|
||||
Browser.Dom.console.info("Added archive %s with id %s", newArchive.Name, string newArchive.Id)
|
||||
onEdit newArchive
|
||||
setIsOpen false
|
||||
| Error msg ->
|
||||
Browser.Dom.console.error("Error adding archive %s: %s", form.Name, msg)
|
||||
setErrMsg (Some msg)
|
||||
)
|
||||
|
||||
let framesExceedEnd =
|
||||
dataSet
|
||||
|> Option.map (fun ds ->
|
||||
form.StartTime.AddHours(form.Frames) > ds.EndTime
|
||||
)
|
||||
|> Option.defaultValue false
|
||||
|
||||
Fui.dialog [
|
||||
dialog.open' isOpen
|
||||
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
|
||||
setIsOpen d.``open``
|
||||
// NOTE: Reset form on open, so it's not noticeable in the UI
|
||||
if d.``open`` then
|
||||
setForm initForm
|
||||
)
|
||||
dialog.children [
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.icon (Fui.icon.editRegular [])
|
||||
button.text "Edit"
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.dialogSurface [
|
||||
dialogSurface.classes []
|
||||
dialogSurface.children [
|
||||
Fui.dialogBody [
|
||||
dialogBody.classes []
|
||||
dialogBody.children [
|
||||
Fui.dialogTitle [
|
||||
dialogTitle.text (sprintf "Edit archive %s" archive.name)
|
||||
dialogTitle.action (
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.action.close
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.appearance.transparent
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.dialogContent [
|
||||
dialogContent.classes ["flex-column"; "gap-8"]
|
||||
dialogContent.children [
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Name"
|
||||
]
|
||||
)
|
||||
field.children (
|
||||
Fui.input [
|
||||
input.value form.Name
|
||||
input.onChange (fun (v: ValueProp<string>) ->
|
||||
if v.value.Length <= 80 then
|
||||
setForm {form with Name = v.value}
|
||||
)
|
||||
]
|
||||
)
|
||||
field.hint $"{form.Name.Length}/80"
|
||||
]
|
||||
Html.div [
|
||||
prop.classes ["flex-row"; "gap-8"]
|
||||
prop.children [
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Start Date"
|
||||
]
|
||||
)
|
||||
|
||||
field.children (
|
||||
Fui.datePicker [
|
||||
datePicker.placeholder "Select a date..."
|
||||
datePicker.showWeekNumbers true
|
||||
datePicker.formatDate (fun d -> d.ToShortDateString())
|
||||
match dataSet with
|
||||
| None -> ()
|
||||
| Some ds ->
|
||||
datePicker.minDate ds.StartTime
|
||||
datePicker.maxDate ds.EndTime
|
||||
datePicker.value (Some form.StartTime)
|
||||
datePicker.onSelectDate (fun d ->
|
||||
d |> Option.iter (fun d' ->
|
||||
setForm {form with StartTime = d'}
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.field [
|
||||
field.label (
|
||||
Fui.label [
|
||||
label.required true
|
||||
label.text "Days"
|
||||
]
|
||||
)
|
||||
field.children (
|
||||
Fui.spinButton [
|
||||
spinButton.value (form.Frames / 24)
|
||||
spinButton.min 1
|
||||
spinButton.onChange (fun (d: SpinButtonOnChangeData) ->
|
||||
match d.value with
|
||||
| Some v ->
|
||||
setForm {form with Frames = v * 24}
|
||||
| None ->
|
||||
if d.displayValue.ToCharArray() |> Array.forall System.Char.IsDigit then
|
||||
setForm {form with Frames = int d.displayValue * 24}
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.text.caption1 [
|
||||
if framesExceedEnd then
|
||||
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||
|
||||
let endDate = form.StartTime.AddHours(form.Frames).ToShortDateString()
|
||||
text.text
|
||||
($"End Date: {endDate}, {form.Frames} frames"
|
||||
+
|
||||
if framesExceedEnd then
|
||||
" (Exceeds DataSet Bounds!)"
|
||||
else
|
||||
"")
|
||||
]
|
||||
Fui.checkbox [
|
||||
checkbox.label "Published"
|
||||
checkbox.checked' form.Published
|
||||
checkbox.onCheckedChange (fun c ->
|
||||
if not c then
|
||||
setForm {form with Published = c; Public = c}
|
||||
else
|
||||
setForm {form with Published = c}
|
||||
)
|
||||
]
|
||||
Fui.checkbox [
|
||||
checkbox.label "Public"
|
||||
checkbox.checked' form.Public
|
||||
checkbox.onCheckedChange (fun c -> setForm {form with Public = c})
|
||||
]
|
||||
match errMsg with
|
||||
| None -> Html.none
|
||||
| Some msg ->
|
||||
Fui.text [
|
||||
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||
text.text msg
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.dialogActions [
|
||||
dialogActions.position.end'
|
||||
dialogActions.children [
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.action.close
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
button.text "Cancel"
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.button [
|
||||
button.appearance.primary
|
||||
button.icon (Fui.icon.saveRegular [])
|
||||
button.text "Save changes"
|
||||
button.disabled <| (form = initForm)
|
||||
button.onClick (fun _ -> handleEditArchive ())
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
[<ReactComponent>]
|
||||
static member DeleteArchiveDialog(archive: Archmaester.Dto.ArchiveProps) =
|
||||
let isOpen, setIsOpen = React.useState false
|
||||
let userConfirmed, setUserConfirmed = React.useState false
|
||||
let errMsg, setErrMsg = React.useState None
|
||||
|
||||
let deleteArchive (id: System.Guid) =
|
||||
promise {
|
||||
try
|
||||
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
|
||||
return res
|
||||
with e ->
|
||||
Browser.Dom.console.error("Error deleting archive: %o", e)
|
||||
return Error "Error deleting archive"
|
||||
}
|
||||
|
||||
let handleDeleteArchive () =
|
||||
deleteArchive archive.archiveId
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok deleted ->
|
||||
if deleted then
|
||||
Browser.Dom.console.info("Archive deleted successfully")
|
||||
setIsOpen false
|
||||
Router.Router.navigateBack ()
|
||||
else
|
||||
setErrMsg (Some "Failed to delete archive")
|
||||
| Error err ->
|
||||
Browser.Dom.console.error("Error deleting archive: %s", err)
|
||||
setErrMsg (Some err)
|
||||
)
|
||||
|
||||
Fui.dialog [
|
||||
dialog.open' isOpen
|
||||
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
|
||||
setIsOpen d.``open``
|
||||
if d.``open`` then
|
||||
setUserConfirmed false
|
||||
setErrMsg None
|
||||
)
|
||||
dialog.children [
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.icon (Fui.icon.deleteRegular [])
|
||||
button.text "Delete"
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.dialogSurface [
|
||||
dialogSurface.classes []
|
||||
dialogSurface.children [
|
||||
Fui.dialogBody [
|
||||
dialogBody.classes []
|
||||
dialogBody.children [
|
||||
Fui.dialogTitle [
|
||||
dialogTitle.text (sprintf "Delete archive %s" archive.name)
|
||||
dialogTitle.action (
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.action.close
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.appearance.transparent
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.dialogContent [
|
||||
dialogContent.classes ["flex-column"; "gap-8"]
|
||||
dialogContent.children [
|
||||
Fui.text "Are you sure you want to delete the archive? This action cannot be reverted!"
|
||||
Fui.checkbox [
|
||||
checkbox.label "Yes, I am sure"
|
||||
checkbox.checked' userConfirmed
|
||||
checkbox.onCheckedChange setUserConfirmed
|
||||
]
|
||||
match errMsg with
|
||||
| None -> Html.none
|
||||
| Some msg ->
|
||||
Fui.text [
|
||||
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
|
||||
text.text msg
|
||||
]
|
||||
]
|
||||
]
|
||||
Fui.dialogActions [
|
||||
dialogActions.position.end'
|
||||
dialogActions.children [
|
||||
Fui.dialogTrigger [
|
||||
dialogTrigger.action.close
|
||||
dialogTrigger.children (
|
||||
Fui.button [
|
||||
button.icon (Fui.icon.dismissRegular [])
|
||||
button.text "Cancel"
|
||||
]
|
||||
)
|
||||
]
|
||||
Fui.button [
|
||||
button.appearance.primary
|
||||
button.icon (Fui.icon.deleteRegular [])
|
||||
button.text "Delete"
|
||||
button.disabled (not userConfirmed)
|
||||
button.onClick (fun _ -> handleDeleteArchive ())
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -97,7 +97,8 @@ type Archives =
|
||||
| Ok archives ->
|
||||
setArchives archives
|
||||
setError None
|
||||
| Error err -> setError (Some "Error fetching archives")
|
||||
| Error err ->
|
||||
setError (Some "Error fetching archives")
|
||||
|
||||
setLoading false
|
||||
)
|
||||
@@ -196,4 +197,4 @@ type Archives =
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user