Compare commits
236 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 | |||
|
|
c83beadd45 | ||
| 46d6b1277b | |||
|
|
10a8c42319
|
||
| 9ed4165ebc | |||
|
|
8cc840adc5 | ||
|
|
79c6e2abd0 | ||
|
|
3093757454 | ||
|
028945bfca
|
|||
|
|
09556bc5df | ||
|
|
94b7f25852 | ||
|
|
2831c8a5cb | ||
|
|
1fed7adf80 | ||
|
|
a9145f6f79 | ||
|
|
ccbe07619f | ||
| 95e6096fbb | |||
|
|
65c29879ab | ||
| b794fc3b68 | |||
| df7be8d894 | |||
|
a474e7cbd4
|
|||
| 5aa83c4bf2 | |||
| 210a04c24d | |||
| 6a35b374c2 | |||
|
785d0d57ae
|
|||
| 7e376f3609 | |||
| ae233cb764 | |||
| 0b9751a97b | |||
| c6c5659b2c | |||
| e1d67df304 | |||
| 7b00f80ac9 | |||
| 92be7a0201 | |||
| 7700924d0e | |||
| 6620c44202 | |||
|
ca5c6791d3
|
|||
| d4dd7945cb | |||
| 4626333c74 | |||
| d5341acd28 | |||
| 3c2da99235 | |||
| 013b5fea91 | |||
| 7ea657b582 | |||
| d1e416c850 | |||
| 232c095954 | |||
| 48dcee7d7f | |||
| 02b6b36f95 | |||
| 8f38f19dd0 | |||
| 04bde9e221 | |||
| 3d948d3ba9 | |||
| 67c23b8707 | |||
| fbe0e59175 | |||
| 1f86f950d6 | |||
| ffce84bb37 | |||
| 2308d50310 | |||
| 1f5dd53673 | |||
| 2f4a458964 | |||
| 679ab2f945 | |||
| 13b257f7ff | |||
| bfc25a2894 | |||
| 9f112aedd8 | |||
| b030e8eeb7 | |||
| 6c8bb8b95a | |||
| 99c0279a1f | |||
| ba3906da71 | |||
| 570a2237f1 | |||
| 50b5524c13 | |||
| b2c475101c | |||
| cde779ea00 | |||
| 8106bb9380 | |||
| df56303949 | |||
| 79238c2a8f | |||
| ac82e2c7ba | |||
| cf8ec277cb | |||
|
|
7054ade55d | ||
|
|
0a54ae3dcc | ||
| 6016cda9ef | |||
|
|
4257ec5598 |
@@ -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
|
||||
|
||||
|
||||
44
.envrc
44
.envrc
@@ -1,46 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export NPINS_DIRECTORY="nix"
|
||||
export APP_ENV=$USER
|
||||
|
||||
# the shebang is ignored, but nice for editors
|
||||
watch_file lon.lock
|
||||
watch_file nix/sources.json
|
||||
|
||||
# Load .env file if it exists
|
||||
dotenv_if_exists
|
||||
|
||||
# Activate development shell
|
||||
if type -P lorri &>/dev/null; then
|
||||
eval "$(lorri direnv)"
|
||||
else
|
||||
echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]'
|
||||
use nix
|
||||
fi
|
||||
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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,7 +30,6 @@ _*.yaml
|
||||
tilt/base/_manifest.yaml
|
||||
NuGet.Config
|
||||
sync.list
|
||||
package-lock.json
|
||||
*.nupkg
|
||||
*.fable-temp*
|
||||
.env
|
||||
.env
|
||||
@@ -1,43 +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'
|
||||
- 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>
|
||||
@@ -26,11 +26,10 @@
|
||||
<!-- Restore with Lockfiles -->
|
||||
<!-- https://www.gresearch.co.uk/blog/article/improve-nuget-restores-with-static-graph-evaluation/ -->
|
||||
<RestoreUseStaticGraphEvaluation>true</RestoreUseStaticGraphEvaluation>
|
||||
<RestoreLockedMode Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</RestoreLockedMode>
|
||||
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
|
||||
|
||||
<!-- Performance -->
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<OtherFlags>$(OtherFlags) --test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen</OtherFlags>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -34,10 +34,12 @@
|
||||
<PackageVersion Include="Fable.SimpleHttp" Version="3.6.0" />
|
||||
<PackageVersion Include="Feliz.Router" Version="4.0.0"/>
|
||||
<PackageVersion Include="Feliz" Version="2.9.0" />
|
||||
<PackageVersion Include="Feliz.UseElmish" Version="2.5.0" />
|
||||
<PackageVersion Include="Feliz.CompilerPlugins" Version="2.2.0" />
|
||||
<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"/>
|
||||
@@ -80,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"/>
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
<Project Path="src/Atlantis/src/Server/Petimeter/Petimeter.fsproj" />
|
||||
<Project Path="src/Atlantis/src/Server/Server.fsproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Codex/">
|
||||
<Project Path="src\Codex\src\Client\Codex.Client.fsproj" />
|
||||
<Project Path="src\Codex\src\Server\Codex.Server.fsproj" />
|
||||
</Folder>
|
||||
<Folder Name="/DataAgent/">
|
||||
<Project Path="src/DataAgent/src/Entity/Entity.csproj" />
|
||||
</Folder>
|
||||
|
||||
49
README.md
49
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`:
|
||||
|
||||
@@ -167,6 +165,9 @@ You should now be able to access the Atlantis client (with HMR) on <atlantis.loc
|
||||
|
||||
### Trust Root Certificate
|
||||
|
||||
> [!note]
|
||||
> 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:
|
||||
|
||||
1. In firefox, navigate to settings and search for _"Certificates"._
|
||||
@@ -176,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).
|
||||
287
RELEASE_NOTES.md
287
RELEASE_NOTES.md
@@ -1,5 +1,292 @@
|
||||
# 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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **js:** Remove unused packages ([10a8c42](https://gitlab.com/oceanbox/Poseidon/commit/10a8c42319bbceebbf413d330fc72071f428e2dd))
|
||||
|
||||
## [1.33.3](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.2...v1.33.3) (2025-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nix:** Pre-commit with prek ([3093757](https://gitlab.com/oceanbox/Poseidon/commit/309375745443add8347ad6fca7b2dfe86ed1b7a7))
|
||||
* **nix:** Watch correct lock file ([79c6e2a](https://gitlab.com/oceanbox/Poseidon/commit/79c6e2abd0e26d220c10e532dffe53511f5dea0a))
|
||||
|
||||
## [1.33.2](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.1...v1.33.2) (2025-11-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* create input display modal on demand ([1fed7ad](https://gitlab.com/oceanbox/Poseidon/commit/1fed7adf8009282184cacb4ddabf91274ee22b60))
|
||||
* **drifters:** Add datepickers for drifters sims ([785d0d5](https://gitlab.com/oceanbox/Poseidon/commit/785d0d57ae6cc53b59ad1d8708e5f513aca8dd77))
|
||||
* sim duration with restrictions ([a9145f6](https://gitlab.com/oceanbox/Poseidon/commit/a9145f6f79aa30cbe529d7a797b38dd2abe0a6cd))
|
||||
* **timeline:** display use local time in timeline ([ccbe076](https://gitlab.com/oceanbox/Poseidon/commit/ccbe07619f31c1b7511c8a8f4ca11fe3af8cbd53))
|
||||
* **timeline:** Marker uses UTC instead of CET + DST ([a474e7c](https://gitlab.com/oceanbox/Poseidon/commit/a474e7cbd4a894b9c1f5f20420dc728f93bc2e07))
|
||||
|
||||
## [1.33.1](https://gitlab.com/oceanbox/Poseidon/compare/v1.33.0...v1.33.1) (2025-11-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add missing * in Drifters ArchiveType FromString ([7054ade](https://gitlab.com/oceanbox/Poseidon/commit/7054ade55dffd058f0d622447da08587633b815c))
|
||||
* **Atlantis:** Add wildcards in allow origin ([cde779e](https://gitlab.com/oceanbox/Poseidon/commit/cde779ea00f0666e6b548c7d711873b64cb294d9))
|
||||
|
||||
# [1.33.0](https://gitlab.com/oceanbox/Poseidon/compare/v1.32.0...v1.33.0) (2025-11-24)
|
||||
|
||||
|
||||
|
||||
31
default.nix
31
default.nix
@@ -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`
|
||||
@@ -28,25 +28,16 @@ let
|
||||
|
||||
packages = import ./nix/packages {
|
||||
inherit
|
||||
env
|
||||
deps
|
||||
pkgs
|
||||
version
|
||||
dotnet-sdk
|
||||
dotnet-runtime
|
||||
env
|
||||
deps
|
||||
;
|
||||
inherit netrcConfig;
|
||||
};
|
||||
in
|
||||
rec {
|
||||
inherit packages;
|
||||
|
||||
inherit scripts;
|
||||
|
||||
# Expose atlantis as default packages
|
||||
default = packages.atlantis;
|
||||
|
||||
# Docker and Singurlarity images
|
||||
containers = pkgs.callPackage ./nix/containers.nix {
|
||||
inherit (packages)
|
||||
atlantis
|
||||
@@ -58,9 +49,21 @@ rec {
|
||||
version
|
||||
env
|
||||
;
|
||||
codex = packages.codex;
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit packages;
|
||||
|
||||
inherit scripts;
|
||||
|
||||
# Expose atlantis as default packages
|
||||
default = packages.atlantis;
|
||||
|
||||
# Docker and Singurlarity images
|
||||
containers = containers;
|
||||
|
||||
checks = {
|
||||
pre-commit = import ./nix/pre-commit.nix;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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/
|
||||
@@ -6,6 +6,7 @@
|
||||
atlantis-client,
|
||||
sorcerer,
|
||||
archivist,
|
||||
codex,
|
||||
}:
|
||||
let
|
||||
# Entrypoints
|
||||
@@ -35,6 +36,7 @@ in
|
||||
cp -r ${atlantis}/lib/Atlantis/* ./app/
|
||||
cp -r ${atlantis-client}/public ./app/
|
||||
'';
|
||||
|
||||
config = {
|
||||
cmd = [ "Server" ];
|
||||
workingDir = "/app";
|
||||
@@ -65,6 +67,7 @@ in
|
||||
workingDir = "/app";
|
||||
};
|
||||
};
|
||||
|
||||
archivist = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "archivist";
|
||||
tag = archivist.version;
|
||||
@@ -95,4 +98,5 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
codex = pkgs.callPackage ../src/Codex/container.nix { server = codex; };
|
||||
}
|
||||
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
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
deps,
|
||||
fable,
|
||||
version,
|
||||
dotnet-sdk,
|
||||
netrcConfig,
|
||||
stdenvNoCC,
|
||||
nodeModules,
|
||||
nix-gitignore,
|
||||
packageSources,
|
||||
dotnet-sdk,
|
||||
dotnet-runtime,
|
||||
buildDotnetModule,
|
||||
writableTmpDirAsHomeHook,
|
||||
}:
|
||||
let
|
||||
root = ../../.;
|
||||
@@ -22,124 +21,69 @@ let
|
||||
|
||||
pname = "Atlantis";
|
||||
|
||||
nodeDeps = stdenvNoCC.mkDerivation {
|
||||
inherit version;
|
||||
pname = "${pname}-node-deps";
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
writableTmpDirAsHomeHook
|
||||
];
|
||||
|
||||
src = lib.fileset.toSource {
|
||||
inherit root;
|
||||
fileset = lib.fileset.unions [
|
||||
../../package.json
|
||||
../../bun.lock
|
||||
];
|
||||
};
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
# Only install dependencies, don't build
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||
|
||||
# Disable post-install scripts to avoid shebang issues
|
||||
bun install \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--backend copyfile \
|
||||
--no-progress \
|
||||
--force
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out
|
||||
cp -r node_modules $out/
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
# Required else we get errors that our fixed-output derivation references store paths
|
||||
dontFixup = true;
|
||||
|
||||
outputHashMode = "recursive";
|
||||
outputHashAlgo = "sha256";
|
||||
# NOTE: Empty this when a new dependency is added
|
||||
outputHash = "sha256-T9X1EFeoNV3yKdVUIMOvaYtja6XR0fne6CDkKHD5rhE=";
|
||||
};
|
||||
|
||||
atlantis-client = buildDotnetModule {
|
||||
inherit dotnet-sdk dotnet-runtime;
|
||||
inherit src version;
|
||||
pname = "${pname}-Client";
|
||||
|
||||
projectFile = "src/Atlantis/src/Client/Client.fsproj";
|
||||
dotnetRestoreFlags = "--force-evaluate";
|
||||
# nugetDeps = ./atlantis-client.json; # nix-build -A packages.atlantis-client.fetch-deps && ./result src/Atlantis/nix/atlantis-client.json
|
||||
nugetDeps = deps {
|
||||
inherit
|
||||
pkgs
|
||||
netrcConfig
|
||||
packageSources
|
||||
;
|
||||
name = "${pname}-Client";
|
||||
lockfiles = [
|
||||
../../src/Atlantis/src/Client/packages.lock.json
|
||||
];
|
||||
};
|
||||
|
||||
# Skip the default dotnet build since we're using Fable
|
||||
dontDotnetBuild = true;
|
||||
|
||||
buildInputs = [
|
||||
fable
|
||||
bun
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
export HOME=$TMPDIR
|
||||
|
||||
cp -r ${nodeDeps}/node_modules ./.
|
||||
chmod -R u+rw node_modules
|
||||
|
||||
chmod -R u+x node_modules/.bin
|
||||
patchShebangs node_modules/.bin
|
||||
export PATH="./node_modules/.bin:$PATH"
|
||||
|
||||
cd src/Atlantis/src/Client
|
||||
|
||||
# NOTE(mrtz): Uses fable from nixpkgs instead of dotnet (Could be out of sync). --MSBuildCracker
|
||||
${lib.getExe fable} -e .jsx -o build
|
||||
|
||||
# Run vite from the Atlantis directory with proper config, always bundle for prod
|
||||
${lib.getExe bun} ../../../../node_modules/.bin/vite build -c ../../vite.config.js --outDir dist/public --mode Production
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
# Copy output (*.js, *.css and *.html) to `/public`.
|
||||
mkdir -p $out/public
|
||||
cp -r dist/public/* $out/public/
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
dontPatchELF = true;
|
||||
dontStrip = true;
|
||||
};
|
||||
in
|
||||
atlantis-client
|
||||
buildDotnetModule {
|
||||
inherit dotnet-sdk dotnet-runtime;
|
||||
inherit src version;
|
||||
pname = "${pname}-Client";
|
||||
|
||||
projectFile = "src/Atlantis/src/Client/Client.fsproj";
|
||||
dotnetRestoreFlags = "--force-evaluate";
|
||||
# nugetDeps = ./atlantis-client.json; # nix-build -A packages.atlantis-client.fetch-deps && ./result src/Atlantis/nix/atlantis-client.json
|
||||
nugetDeps = deps {
|
||||
inherit
|
||||
pkgs
|
||||
netrcConfig
|
||||
packageSources
|
||||
;
|
||||
name = "${pname}-Client";
|
||||
lockfiles = [
|
||||
../../src/Atlantis/src/Client/packages.lock.json
|
||||
];
|
||||
};
|
||||
|
||||
# Skip the default dotnet build since we're using Fable
|
||||
dontDotnetBuild = true;
|
||||
|
||||
buildInputs = [
|
||||
fable
|
||||
bun
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
export HOME=$TMPDIR
|
||||
|
||||
cp -r ${nodeModules}/node_modules ./.
|
||||
chmod -R u+rw node_modules
|
||||
|
||||
chmod -R u+x node_modules/.bin
|
||||
patchShebangs node_modules/.bin
|
||||
export PATH="./node_modules/.bin:$PATH"
|
||||
|
||||
cd src/Atlantis/src/Client
|
||||
|
||||
# NOTE(mrtz): Uses fable from nixpkgs instead of dotnet (Could be out of sync). --MSBuildCracker
|
||||
${lib.getExe fable} -e .jsx -o build
|
||||
|
||||
# Run vite from the Atlantis directory with proper config, always bundle for prod
|
||||
${lib.getExe bun} ../../../../node_modules/.bin/vite build -c ../../vite.config.js --outDir dist/public --mode Production
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
# Copy output (*.js, *.css and *.html) to `/public`.
|
||||
mkdir -p $out/public
|
||||
cp -r dist/public/* $out/public/
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
dontPatchELF = true;
|
||||
dontStrip = true;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ buildDotnetModule rec {
|
||||
pname = "Atlantis";
|
||||
# NOTE(mrtz): Ensures reproducibility and reduces closure size,
|
||||
# by filtering out irrelevant files and `.git` which changes between commits.
|
||||
src = nix-gitignore.gitignoreSource [ ] ../../.;
|
||||
src = nix-gitignore.gitignoreSource [ ] ../..;
|
||||
projectFile = "src/Atlantis/src/Server/Server.fsproj";
|
||||
dotnetRestoreFlags = "--force-evaluate";
|
||||
nugetDeps = deps {
|
||||
@@ -38,6 +38,7 @@ buildDotnetModule rec {
|
||||
runtimeDeps = [
|
||||
pkgs.netcdf
|
||||
];
|
||||
# NOTE: Add back when we have tests
|
||||
doCheck = false;
|
||||
buildType = env;
|
||||
# Copy `appsettings` for local build
|
||||
|
||||
@@ -9,23 +9,18 @@
|
||||
}:
|
||||
let
|
||||
# NOTE(mrtz): Gitlab Nuget Registry does not support groupwide fetches :/
|
||||
packageSources = {
|
||||
"Oceanbox.FvcomKit" = "https://gitlab.com/api/v4/projects/35569541/packages/nuget/download";
|
||||
"ProjNet.FSharp" = "https://gitlab.com/api/v4/projects/35009572/packages/nuget/download";
|
||||
"SDSLite.Oceanbox" = "https://gitlab.com/api/v4/projects/34025102/packages/nuget/download";
|
||||
"Oceanbox.ServerPack" = "https://gitlab.com/api/v4/projects/67427353/packages/nuget/download";
|
||||
"Oceanbox.DataAgent" = "https://gitlab.com/api/v4/projects/37541600/packages/nuget/download";
|
||||
"Drifters.Api" = "https://gitlab.com/api/v4/projects/37086336/packages/nuget/download";
|
||||
"Fable.SignalR.AspNetCore" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Fable.SignalR.Saturn" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Fable.SignalR.Shared" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Fable.SignalR" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Fable.SignalR.Elmish" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Fable.Lit" = "https://gitlab.com/api/v4/projects/61744837/packages/nuget/download";
|
||||
"Fable.Lit.Elmish" = "https://gitlab.com/api/v4/projects/61744837/packages/nuget/download";
|
||||
"Fable.Lit.React" = "https://gitlab.com/api/v4/projects/61744837/packages/nuget/download";
|
||||
"Fable.OpenLayers" = "https://gitlab.com/api/v4/projects/36202053/packages/nuget/download";
|
||||
"Matplotlib.ColorMaps" = "https://gitlab.com/api/v4/projects/36675671/packages/nuget/download";
|
||||
packageSources = import ./sources.nix;
|
||||
|
||||
nodeModules = pkgs.callPackage ./node-modules.nix {};
|
||||
|
||||
codex-client = pkgs.callPackage ../../src/Codex/src/Client {
|
||||
inherit
|
||||
deps
|
||||
dotnet-sdk
|
||||
netrcConfig
|
||||
nodeModules
|
||||
packageSources
|
||||
;
|
||||
};
|
||||
in
|
||||
{
|
||||
@@ -38,6 +33,7 @@ in
|
||||
packageSources
|
||||
;
|
||||
};
|
||||
|
||||
# NOTE(mrtz): It's acutally Oceanbox.DataAgent
|
||||
archmaester = pkgs.callPackage ./dataagent.nix {
|
||||
inherit
|
||||
@@ -48,6 +44,7 @@ in
|
||||
packageSources
|
||||
;
|
||||
};
|
||||
|
||||
# NOTE(mrtz): It's acutally Poseidon.Api
|
||||
interfaces = pkgs.callPackage ./api.nix {
|
||||
inherit
|
||||
@@ -59,6 +56,7 @@ in
|
||||
packageSources
|
||||
;
|
||||
};
|
||||
|
||||
atlantis = pkgs.callPackage ./atlantis.nix {
|
||||
inherit
|
||||
env
|
||||
@@ -70,6 +68,7 @@ in
|
||||
packageSources
|
||||
;
|
||||
};
|
||||
|
||||
sorcerer = pkgs.callPackage ./sorcerer.nix {
|
||||
inherit
|
||||
env
|
||||
@@ -80,7 +79,6 @@ in
|
||||
packageSources
|
||||
;
|
||||
};
|
||||
|
||||
archivist = pkgs.callPackage ./archivist.nix {
|
||||
inherit
|
||||
env
|
||||
@@ -90,15 +88,26 @@ in
|
||||
packageSources
|
||||
;
|
||||
};
|
||||
|
||||
atlantis-client = pkgs.callPackage ./atlantis-client.nix {
|
||||
inherit
|
||||
deps
|
||||
netrcConfig
|
||||
version
|
||||
dotnet-sdk
|
||||
netrcConfig
|
||||
nodeModules
|
||||
dotnet-runtime
|
||||
packageSources
|
||||
;
|
||||
};
|
||||
}
|
||||
|
||||
codex = pkgs.callPackage ../../src/Codex/src/Server {
|
||||
inherit
|
||||
deps
|
||||
dotnet-sdk
|
||||
netrcConfig
|
||||
dotnet-runtime
|
||||
packageSources
|
||||
;
|
||||
client = codex-client;
|
||||
};
|
||||
}
|
||||
52
nix/packages/node-modules.nix
Normal file
52
nix/packages/node-modules.nix
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
bun,
|
||||
stdenvNoCC,
|
||||
nix-gitignore,
|
||||
writableTmpDirAsHomeHook,
|
||||
}:
|
||||
stdenvNoCC.mkDerivation {
|
||||
name = "node-modules";
|
||||
|
||||
nativeBuildInputs = [
|
||||
bun
|
||||
writableTmpDirAsHomeHook
|
||||
];
|
||||
|
||||
src = nix-gitignore.gitignoreSource [ ] ../../.;
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
# Required else we get errors that our fixed-output derivation references store paths
|
||||
dontFixup = true;
|
||||
|
||||
# Only install dependencies, don't build
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||
|
||||
# Disable post-install scripts to avoid shebang issues
|
||||
bun install \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--backend copyfile \
|
||||
--no-progress \
|
||||
--force
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out
|
||||
cp -r node_modules $out/
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
outputHashMode = "recursive";
|
||||
outputHashAlgo = "sha256";
|
||||
# NOTE: Empty this when a new dependency is added
|
||||
outputHash = "sha256-bbCaGoZRE7vRuAS3eRyP8yHANYXBJVaHmuL99BAovjY=";
|
||||
}
|
||||
@@ -44,6 +44,7 @@ buildDotnetModule rec {
|
||||
runtimeDeps = [
|
||||
pkgs.netcdf
|
||||
];
|
||||
buildType = env;
|
||||
# TODO: Add back when we have tests
|
||||
doCheck = false;
|
||||
buildType = env;
|
||||
}
|
||||
|
||||
19
nix/packages/sources.nix
Normal file
19
nix/packages/sources.nix
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"Drifters.Api" = "https://gitlab.com/api/v4/projects/37086336/packages/nuget/download";
|
||||
"Fable.Lit" = "https://gitlab.com/api/v4/projects/61744837/packages/nuget/download";
|
||||
"Fable.Lit.Elmish" = "https://gitlab.com/api/v4/projects/61744837/packages/nuget/download";
|
||||
"Fable.Lit.React" = "https://gitlab.com/api/v4/projects/61744837/packages/nuget/download";
|
||||
"Fable.OpenLayers" = "https://gitlab.com/api/v4/projects/36202053/packages/nuget/download";
|
||||
"Fable.SignalR" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Fable.SignalR.AspNetCore" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Fable.SignalR.Elmish" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Fable.SignalR.Saturn" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Fable.SignalR.Shared" = "https://gitlab.com/api/v4/projects/40255650/packages/nuget/download";
|
||||
"Matplotlib.ColorMaps" = "https://gitlab.com/api/v4/projects/36675671/packages/nuget/download";
|
||||
"Oceanbox.DataAgent" = "https://gitlab.com/api/v4/projects/37541600/packages/nuget/download";
|
||||
"Oceanbox.FvcomKit" = "https://gitlab.com/api/v4/projects/35569541/packages/nuget/download";
|
||||
"Oceanbox.ServerPack" = "https://gitlab.com/api/v4/projects/67427353/packages/nuget/download";
|
||||
"ProjNet.FSharp" = "https://gitlab.com/api/v4/projects/35009572/packages/nuget/download";
|
||||
"Oceanbox.SDSLite" = "https://gitlab.com/api/v4/projects/34025102/packages/nuget/download";
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ let
|
||||
in
|
||||
pre-commit.run {
|
||||
src = ./.;
|
||||
# TODO: Do not run at pre-commit time
|
||||
# default_stages = [
|
||||
# "pre-push"
|
||||
# ];
|
||||
# NOTE: Do not run at pre-commit time
|
||||
default_stages = [
|
||||
"pre-push"
|
||||
];
|
||||
package = pkgs.prek;
|
||||
hooks = {
|
||||
nixfmt-rfc-style = {
|
||||
enable = true;
|
||||
@@ -22,7 +23,7 @@ pre-commit.run {
|
||||
# statix = {
|
||||
# enable = true;
|
||||
# package = pkgs.statix;
|
||||
# settings.ignore = [ "nix/default.nix" ];
|
||||
# settings.ignore = [ "../nix/default.nix" ];
|
||||
# };
|
||||
# TODO(mrtz): Format manually for now
|
||||
# fantomas = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -18,7 +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",
|
||||
@@ -68,6 +74,6 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
"vis-timeline": "^7.7.4"
|
||||
"vis-timeline": "^8.4.0"
|
||||
}
|
||||
}
|
||||
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"
|
||||
|
||||
@@ -74,7 +74,11 @@ let addUsers (args: PrincipalArgs) =
|
||||
|
||||
async {
|
||||
try
|
||||
match! aclApi.addUsers users with
|
||||
let req : Archmaester.AddUsersRequest = {
|
||||
group = ""
|
||||
users = users
|
||||
}
|
||||
match! aclApi.addUsers req with
|
||||
| Ok _ ->
|
||||
Log.Information $"Added users %A{users}"
|
||||
return Ok ()
|
||||
|
||||
@@ -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}}
|
||||
@@ -29,4 +29,4 @@ pkgs.mkShellNoCC {
|
||||
export APP_NAME=atlantis
|
||||
export APP_NAMESPACE=$USER-atlantis
|
||||
'';
|
||||
}
|
||||
}
|
||||
@@ -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,10 +1,9 @@
|
||||
<?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>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<Version>2.102.0</Version>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<RootNamespace>Main</RootNamespace>
|
||||
@@ -25,4 +24,4 @@
|
||||
<ProjectReference Include="Atlas\Atlas.fsproj" />
|
||||
<ProjectReference Include="Mapster\Mapster.fsproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -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"
|
||||
|
||||
@@ -17,5 +17,25 @@ let private uk : obj =
|
||||
|
||||
dateTimeFormat "en-GB" opts
|
||||
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
let private ukShort : obj =
|
||||
let opts = {|
|
||||
dateStyle = "short"
|
||||
|}
|
||||
|
||||
dateTimeFormat "en-GB" opts
|
||||
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
let private ukDateTimeShort : obj =
|
||||
let opts = {|
|
||||
dateStyle = "short"
|
||||
timeStyle = "short"
|
||||
|}
|
||||
|
||||
dateTimeFormat "en-GB" opts
|
||||
|
||||
/// Returns date string formatted as e.g.: "Wednesday 11 June 2025 at 06:00"
|
||||
let format (date: System.DateTime) : string = uk?format date
|
||||
let format (date: System.DateTime) : string = uk?format date
|
||||
|
||||
let shortDate (date: System.DateTime) : string = ukShort?format date
|
||||
let shortDateTime (date: System.DateTime) : string = ukDateTimeShort?format date
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -4,4 +4,4 @@ module React =
|
||||
open Fable.Core
|
||||
open Feliz
|
||||
|
||||
let inline fromJsx (el: JSX.Element) : ReactElement = unbox el
|
||||
let inline fromJsx (el: JSX.Element) : ReactElement = unbox el
|
||||
|
||||
@@ -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"
|
||||
@@ -646,7 +646,7 @@ type ArchiveInfo = {
|
||||
startTime: DateTime
|
||||
defaultZoom: float
|
||||
focalPoint: float * float
|
||||
polygon: (float * float)[] option
|
||||
polygon: (float * float) array option
|
||||
frames: int
|
||||
} with
|
||||
static member empty = {
|
||||
@@ -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 =
|
||||
|
||||
@@ -82,9 +82,10 @@ let fromBase64String (s: string) : string = jsNative
|
||||
[<Emit("btoa($0)")>]
|
||||
let toBase64String (s: string) : string = jsNative
|
||||
|
||||
let strNull = String.IsNullOrWhiteSpace
|
||||
let strNotNull = strNull >> not
|
||||
/// Helper function for testing whether a string is null or only whitespace
|
||||
let tryStr str =
|
||||
if String.IsNullOrWhiteSpace str then None else Some str
|
||||
let tryStr str = if strNotNull str then Some str else None
|
||||
|
||||
/// Uses js getElementById on the HTML id. Tests elem for isNullOrUndefined
|
||||
let tryElem (id: string) : Types.Element option =
|
||||
@@ -130,6 +131,12 @@ let onEnterOrEscape onEnter onEscape (ev: Types.Event) =
|
||||
| "Escape" -> onEscape ev
|
||||
| _ -> ()
|
||||
|
||||
let debounce delay (f: 'T -> unit) : 'T -> unit =
|
||||
let mutable timer = unbox null
|
||||
fun args ->
|
||||
do JS.clearTimeout timer
|
||||
do timer <- JS.setTimeout (fun () -> f args) delay
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the ISO week number from a date. Taken from https://weeknumber.com/how-to/javascript since we cannot
|
||||
/// use https://learn.microsoft.com/en-us/dotnet/api/system.globalization.calendar.getweekofyear?view=net-8.0
|
||||
@@ -164,7 +171,7 @@ let getWeek (date: DateTime) : int =
|
||||
let diff = thursdayThisWeek - thursdayWeekOne
|
||||
let week = 1 + int (diff.TotalDays / 7.0)
|
||||
|
||||
// TODO: Test when there are 53 weeks ...
|
||||
// TODO: Test when there are 53 weeks...
|
||||
Int32.Clamp (week, 1, 52)
|
||||
|
||||
module Archives =
|
||||
@@ -173,7 +180,7 @@ module Archives =
|
||||
/// Calculates the time for the given frame
|
||||
let findFrameTime (archive: Atlantis.Types.ArchiveInfo) (frame: int) : DateTime =
|
||||
let duration = System.TimeSpan.FromSeconds (float frame * float archive.saveFreq)
|
||||
archive.startTime + duration
|
||||
archive.startTime.ToUniversalTime() + duration
|
||||
|
||||
/// Calculates the time for the given frame. Must be within range of the archives' total frames.
|
||||
let tryFindFrameTime (archive: Atlantis.Types.ArchiveInfo) (frame: int) : DateTime option =
|
||||
@@ -185,7 +192,7 @@ module Archives =
|
||||
/// Calculates when the archive ends based on the total number of frames, the save frequency and the start time.
|
||||
let findEndTime (archive: Atlantis.Types.ArchiveInfo) : DateTime =
|
||||
let duration = System.TimeSpan.FromSeconds (float archive.frames * float archive.saveFreq)
|
||||
archive.startTime + duration
|
||||
archive.startTime.ToUniversalTime() + duration
|
||||
|
||||
let getMaxTimeStep (timeUnit: Atlantis.Types.TimeUnit) (archive: Atlantis.Types.ArchiveInfo) frame : int option =
|
||||
let availableSeconds = (archive.frames - frame) * archive.saveFreq |> float
|
||||
@@ -226,4 +233,4 @@ module Array =
|
||||
|
||||
init |> Array.foldBack folder array
|
||||
|
||||
let sequenceResults x = traverseResult id x
|
||||
let sequenceResults x = traverseResult id x
|
||||
|
||||
@@ -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}
|
||||
"""
|
||||
@@ -110,8 +110,8 @@ let calcDriftersFrame (archive: ArchiveInfo) (drifter: SimArchive) (frame: int)
|
||||
frame * archive.saveFreq
|
||||
|> float
|
||||
|> TimeSpan.FromSeconds
|
||||
let currentTime = archive.startTime + duration
|
||||
let drifterStart = drifter.Archive.startTime
|
||||
let currentTime = archive.startTime.ToUniversalTime() + duration
|
||||
let drifterStart = drifter.Archive.startTime.ToUniversalTime()
|
||||
let drifterDuration = currentTime - drifterStart
|
||||
|
||||
drifterDuration.TotalSeconds
|
||||
@@ -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
|
||||
@@ -277,9 +277,10 @@ let fetchParticles (model: Model) =
|
||||
let startTime = drifter.Archive.startTime
|
||||
let endTime = startTime + drifter.Duration
|
||||
let frame = calcDriftersFrame model.archive drifter model.frame
|
||||
if ct >= startTime && ct <= endTime && frame >= 0 then
|
||||
let aid = drifter.Archive.archiveId
|
||||
if ct >= startTime && ct <= endTime && frame >= 0 && aid <> Guid.Empty then
|
||||
promise {
|
||||
let! p = api.Particles.GetFrame drifter.Archive.archiveId frame |> Async.StartAsPromise
|
||||
let! p = api.Particles.GetFrame aid frame |> Async.StartAsPromise
|
||||
console.log $"fetchParticles {frame} : {p.Length}"
|
||||
let p' =
|
||||
p
|
||||
@@ -1862,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
|
||||
@@ -2271,7 +2272,7 @@ module private Deposition =
|
||||
</div>
|
||||
"""
|
||||
|
||||
let sitesRow idx site =
|
||||
let sitesRow idx (site: ReleaseSite) =
|
||||
let lon, lat = site.position |> toWgs84'
|
||||
html
|
||||
$"""
|
||||
@@ -2720,24 +2721,54 @@ module private WaterContact =
|
||||
"""
|
||||
|
||||
[<HookComponent>]
|
||||
let private setupMetaParams disable maxDurationH (simulation: SimulationModel) (setSimModel: SimulationModel -> unit) (tmpDrifter: SimArchive option) (setTmpDrifter: SimArchive option -> unit)=
|
||||
let private setupMetaParams disable (maxDurationHr: float) (simulation: SimulationModel) (setSimModel: SimulationModel -> unit) (tmpDrifter: SimArchive option) (setTmpDrifter: SimArchive option -> unit) (archiveStart: DateTime) (archiveEnd: DateTime) =
|
||||
let setName (s: string) =
|
||||
let maxLength = 75
|
||||
let s' = if s.Length < maxLength then s else s.Substring(0, maxLength)
|
||||
setSimModel { simulation with name = s' }
|
||||
{ simulation with name = s' } |> setSimModel
|
||||
|
||||
let setStartT (dt: DateTime) =
|
||||
// Set time to midday (12:00)
|
||||
let startDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
|
||||
console.debug ("[Drifters] setStartDateTime:", startDate)
|
||||
let updatedTmpDrifter =
|
||||
tmpDrifter
|
||||
|> Option.map (fun x ->
|
||||
let archive = { x.Archive with startTime = startDate }
|
||||
{ x with Archive = archive })
|
||||
|
||||
updatedTmpDrifter |> setTmpDrifter
|
||||
{ simulation with startTime = startDate } |> setSimModel
|
||||
|
||||
// TODO: Changing this should update the top-level release, as we want to place the simulation on the timeline
|
||||
let setEndT (e: Browser.Types.Event) =
|
||||
let nDays: float = !!e.target.Value
|
||||
let duration = nDays |> TimeSpan.FromDays
|
||||
console.debug($"Update simulation days: dur = {duration} ({nDays})")
|
||||
let d =
|
||||
tmpDrifter
|
||||
|> Option.map (fun x -> { x with Duration = duration })
|
||||
let setEndT (dt: DateTime) =
|
||||
let endDate = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
|
||||
|
||||
setTmpDrifter d
|
||||
let startDate = tmpDrifter |> Option.map _.Archive.startTime |> Option.defaultValue (endDate.AddDays -1)
|
||||
if startDate >= endDate then
|
||||
simulation |> setSimModel
|
||||
else
|
||||
let duration = endDate - startDate
|
||||
let nDays = duration.Days
|
||||
|
||||
setSimModel { simulation with simDays = nDays }
|
||||
if nDays |> float <> simulation.simDays then
|
||||
console.debug $"Update simulation days: dur = {duration} ({nDays})"
|
||||
let updatedTmpDrifter =
|
||||
tmpDrifter
|
||||
|> Option.map (fun x -> { x with Duration = duration })
|
||||
|
||||
updatedTmpDrifter |> setTmpDrifter
|
||||
{ simulation with simDays = nDays } |> setSimModel
|
||||
|
||||
let setDuration (v: float) =
|
||||
let duration = v |> TimeSpan.FromDays
|
||||
console.debug($"Update simulation days: dur = {duration} ({v})")
|
||||
|
||||
tmpDrifter
|
||||
|> Option.map (fun x -> { x with Duration = duration })
|
||||
|> setTmpDrifter
|
||||
|
||||
setSimModel { simulation with simDays = v }
|
||||
|
||||
let allowReverse =
|
||||
match simulation.kind with
|
||||
@@ -2748,13 +2779,13 @@ let private setupMetaParams disable maxDurationH (simulation: SimulationModel) (
|
||||
|
||||
let toggleReverse _ =
|
||||
let r' = not simulation.reverse
|
||||
tmpDrifter
|
||||
|> Option.map (fun d -> { d with Reverse = r' })
|
||||
|> setTmpDrifter
|
||||
|
||||
{ simulation with reverse = r' }
|
||||
|> setSimModel
|
||||
{ simulation with reverse = r' } |> setSimModel
|
||||
|
||||
// Calculate min/max dates with buffer
|
||||
let minStartDate = archiveStart
|
||||
let maxStartDate = archiveEnd - TimeSpan.FromDays(simulation.simDays)
|
||||
let minEndDate = simulation.startTime + TimeSpan.FromHours(24.0)
|
||||
let maxEndDate = simulation.startTime + TimeSpan.FromHours(maxDurationHr)
|
||||
|
||||
html
|
||||
$"""
|
||||
@@ -2777,42 +2808,61 @@ let private setupMetaParams disable maxDurationH (simulation: SimulationModel) (
|
||||
</div>
|
||||
|
||||
<div class="grow m-8">
|
||||
<div class="flex-row gap-16" style="padding-bottom: 5px padding-top: 20px">
|
||||
<div style="padding-bottom: 5px padding-top: 20px">
|
||||
<sp-field-group style="flex-grow: 1;" vertical>
|
||||
<sp-field-label>
|
||||
Duration (days)
|
||||
<sp-field-label size="s" for="vertical">
|
||||
Start date
|
||||
</sp-field-label>
|
||||
<div class="flex-row">
|
||||
<sp-number-field
|
||||
id="sim-duration"
|
||||
label="SimDuration"
|
||||
?disabled={disable}
|
||||
value="{simulation.simDays}"
|
||||
min={0.0}
|
||||
max={maxDurationH / 24.0}
|
||||
@change={Ev(setEndT)}
|
||||
style="flex-grow: 1"
|
||||
format-options="{formatDigits 0 0}"
|
||||
></sp-number-field>
|
||||
</div>
|
||||
<sp-field-group horizontal style="padding-bottom: 10px;">
|
||||
{FluentUI.Lit.DatePicker (disable, simulation.startTime, setStartT, minStartDate, maxStartDate)}
|
||||
</sp-field-group>
|
||||
</sp-field-group>
|
||||
|
||||
<sp-field-group style="flex-grow: 1;" vertical>
|
||||
<sp-field-label size="s" for="vertical">
|
||||
Time direction
|
||||
End date
|
||||
</sp-field-label>
|
||||
<sp-switch
|
||||
size="m"
|
||||
style="flex-grow: 1; padding-top: 5px"
|
||||
?disabled={not allowReverse || disable}
|
||||
?checked={simulation.reverse}
|
||||
@click={Ev(toggleReverse)}
|
||||
>
|
||||
Reverse
|
||||
</sp-switch>
|
||||
<sp-field-group horizontal style="padding-bottom: 10px;">
|
||||
{FluentUI.Lit.DatePicker (disable, simulation.startTime.AddDays(simulation.simDays), setEndT, minEndDate, maxEndDate)}
|
||||
</sp-field-group>
|
||||
</sp-field-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-row m-8 gap-8">
|
||||
<div class="grow">
|
||||
<sp-field-label>
|
||||
Duration (days)
|
||||
</sp-field-label>
|
||||
|
||||
<div class="flex-row gap-8">
|
||||
<sp-number-field
|
||||
?disabled="{disable}"
|
||||
value="{simulation.simDays}"
|
||||
min={0}
|
||||
max={maxDurationHr / 24.0 |> int}
|
||||
style="width: 80%%"
|
||||
@change={EvVal(unbox >> setDuration)}
|
||||
>
|
||||
</sp-action-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grow">
|
||||
<sp-field-label size="s" for="vertical">
|
||||
Time direction
|
||||
</sp-field-label>
|
||||
<sp-switch
|
||||
size="m"
|
||||
style="width: 20%%; flex-grow: 1; padding-top: 5px; padding-right: 40px"
|
||||
?disabled={not allowReverse || disable}
|
||||
?checked={simulation.reverse}
|
||||
@click={Ev(toggleReverse)}
|
||||
>
|
||||
Reverse
|
||||
</sp-switch>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
[<HookComponent>]
|
||||
@@ -2822,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
|
||||
@@ -3231,6 +3281,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
quiet
|
||||
static="white"
|
||||
>
|
||||
{lon |> sprintf "%.6f"}
|
||||
</sp-action-button>
|
||||
@@ -3239,6 +3290,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
quiet
|
||||
static="white"
|
||||
>
|
||||
{lat |> sprintf "%.6f"}
|
||||
</sp-action-button>
|
||||
@@ -3247,6 +3299,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
quiet
|
||||
static="white"
|
||||
>
|
||||
{site.radius |> abs |> sprintf "%.1f"}
|
||||
</sp-action-button>
|
||||
@@ -3255,6 +3308,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
quiet
|
||||
static="white"
|
||||
>
|
||||
{site.depth |> abs |> sprintf "%.1f"}
|
||||
</sp-action-button>
|
||||
@@ -3263,6 +3317,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
quiet
|
||||
static="white"
|
||||
>
|
||||
{site.ddepth |> abs |> sprintf "%.1f"}
|
||||
</sp-action-button>
|
||||
@@ -3276,6 +3331,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
size="s"
|
||||
quiet
|
||||
style="width: 50px"
|
||||
static="white"
|
||||
>
|
||||
{idx + 1}
|
||||
</sp-action-button>
|
||||
@@ -3284,6 +3340,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
size="s"
|
||||
quiet
|
||||
style="width: 200px"
|
||||
static="white"
|
||||
>
|
||||
{group.name}
|
||||
</sp-action-button>
|
||||
@@ -3299,6 +3356,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
<sp-action-button
|
||||
size="s"
|
||||
style="width: 50px"
|
||||
static="white"
|
||||
>
|
||||
Nr
|
||||
</sp-action-button>
|
||||
@@ -3306,6 +3364,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
<sp-action-button
|
||||
size="s"
|
||||
style="width: 200px"
|
||||
static="white"
|
||||
>
|
||||
Name
|
||||
</sp-action-button>
|
||||
@@ -3313,6 +3372,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
<sp-action-button
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
static="white"
|
||||
>
|
||||
Longitude
|
||||
</sp-action-button>
|
||||
@@ -3320,6 +3380,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
<sp-action-button
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
static="white"
|
||||
>
|
||||
Latitude
|
||||
</sp-action-button>
|
||||
@@ -3327,6 +3388,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
<sp-action-button
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
static="white"
|
||||
>
|
||||
Radius (m)
|
||||
</sp-action-button>
|
||||
@@ -3334,6 +3396,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
<sp-action-button
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
static="white"
|
||||
>
|
||||
Depth (m)
|
||||
</sp-action-button>
|
||||
@@ -3341,6 +3404,7 @@ let private displayReleaseGroups (groups: Map<GroupIdx, GroupModel>) =
|
||||
<sp-action-button
|
||||
size="s"
|
||||
style="width: 100px"
|
||||
static="white"
|
||||
>
|
||||
Variance (+/-)
|
||||
</sp-action-button>
|
||||
@@ -3401,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">
|
||||
@@ -3422,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">
|
||||
@@ -3664,33 +3728,20 @@ let simulationControls (simType: SimType) (dispatch: Msg -> unit) (model: Model)
|
||||
// META DATA //
|
||||
///////////////
|
||||
|
||||
// let setStartDate (str: string) =
|
||||
// let startDate = DateTime.Parse(str)
|
||||
// let d =
|
||||
// xmodel'.tmpDrifter
|
||||
// |> Option.map (fun x ->
|
||||
// let archive = { x.Archive with startTime = startDate }
|
||||
// { x with Archive = archive })
|
||||
//
|
||||
// SetTmpDrifter d
|
||||
// |> dispatch'
|
||||
//
|
||||
// SetSimulationModel { xmodel'.simulation with startTime = startDate }
|
||||
// |> dispatch'
|
||||
// Absolute max duration
|
||||
let maxDurationDays = 10_000.
|
||||
// match xmodel'.simulation.kind with
|
||||
// | DepositionSim -> 731.
|
||||
// | WaterContactSim -> 366.
|
||||
// | DownwellingSim -> 366.
|
||||
// | LiceSim -> 366.
|
||||
// | VirusSim -> 366.
|
||||
// | TransportSim -> 30.
|
||||
|
||||
// absolute max duration
|
||||
let maxDurationDays =
|
||||
match xmodel'.simulation.kind with
|
||||
| DepositionSim -> 731.
|
||||
| WaterContactSim -> 366.
|
||||
| DownwellingSim -> 366.
|
||||
| LiceSim -> 366.
|
||||
| VirusSim -> 366.
|
||||
| TransportSim -> 30.
|
||||
|
||||
let maxDurationH =
|
||||
let maxDurationHr =
|
||||
let buffer =
|
||||
match simType with
|
||||
| DepositionSim -> 168. // 1 week
|
||||
| WaterContactSim ->
|
||||
// Need to subtract release span and cohort tracking time (traits.transition)
|
||||
// to guarantee that all cohorts are fully propagated by the end of the archive
|
||||
@@ -3699,10 +3750,7 @@ let simulationControls (simType: SimType) (dispatch: Msg -> unit) (model: Model)
|
||||
|> Seq.max
|
||||
|> (+) (xmodel'.release.span.ToFloat())
|
||||
| _ -> 1.0
|
||||
if xmodel'.simulation.reverse then
|
||||
(simStartT - archiveStartUTC).TotalHours - buffer
|
||||
else
|
||||
(archiveEndT - simStartT).TotalHours - buffer
|
||||
(archiveEndT - simStartT).TotalHours - buffer
|
||||
|> min (24.0 * maxDurationDays)
|
||||
|> max 0.0
|
||||
|
||||
@@ -3747,8 +3795,19 @@ let simulationControls (simType: SimType) (dispatch: Msg -> unit) (model: Model)
|
||||
openBoundary = None
|
||||
}
|
||||
|
||||
let simulation' =
|
||||
xmodel'.simulation.ToSimulationInput()
|
||||
|> fun sim ->
|
||||
if xmodel'.simulation.reverse then
|
||||
let startUtc = xmodel'.simulation.startTime.ToUniversalTime()
|
||||
let endUtc = startUtc + TimeSpan.FromDays(xmodel'.simulation.simDays)
|
||||
{ sim with startTime = Some endUtc }
|
||||
else
|
||||
let startUtc = xmodel'.simulation.startTime.ToUniversalTime()
|
||||
{ sim with startTime = Some startUtc }
|
||||
|
||||
let input = {
|
||||
simulation = xmodel'.simulation.ToSimulationInput()
|
||||
simulation = simulation'
|
||||
advection = advection' |> Some
|
||||
release = release'
|
||||
particles = particles' |> Some
|
||||
@@ -3810,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
|
||||
@@ -4074,11 +4134,24 @@ let simulationControls (simType: SimType) (dispatch: Msg -> unit) (model: Model)
|
||||
|
||||
match xmodel'.tmpDrifter with
|
||||
| Some d when d.Status = JobStatus.New ->
|
||||
let d' = Some { d with Archive = { d.Archive with startTime = simStartT }; Reverse = xmodel'.simulation.reverse }
|
||||
SetTmpDrifter d' |> dispatch'
|
||||
{ d with Archive.startTime = simStartT; }
|
||||
|> Some
|
||||
|> SetTmpDrifter
|
||||
|> dispatch'
|
||||
| _ -> ()
|
||||
)
|
||||
|
||||
Hook.useEffectOnChange (
|
||||
xmodel'.simulation.startTime,
|
||||
fun t ->
|
||||
// NOTE(simkir): Only update the sim item when its new, when it has been started, do not update
|
||||
let frame = (int (t - archiveStartUTC).TotalSeconds) / archive.saveFreq
|
||||
|
||||
frame
|
||||
|> SetFrame
|
||||
|> dispatch
|
||||
)
|
||||
|
||||
let measuresHeight =
|
||||
Utils.tryGetElemRect "measures-controls"
|
||||
|> Option.map (fun rect -> rect.height)
|
||||
@@ -4110,7 +4183,7 @@ let simulationControls (simType: SimType) (dispatch: Msg -> unit) (model: Model)
|
||||
size="s"
|
||||
density="spacious"
|
||||
>
|
||||
{setupMetaParams false maxDurationH xmodel'.simulation (SetSimulationModel >> dispatch') xmodel'.tmpDrifter (SetTmpDrifter >> dispatch')}
|
||||
{setupMetaParams false maxDurationHr xmodel'.simulation (SetSimulationModel >> dispatch') xmodel'.tmpDrifter (SetTmpDrifter >> dispatch') archiveStartUTC archiveEndT}
|
||||
{match xmodel'.simulation.kind with
|
||||
| TransportSim -> transportControls dispatch' xmodel'
|
||||
| LiceSim -> liceControls dispatch' xmodel'
|
||||
@@ -4228,7 +4301,7 @@ let driftersInputModal
|
||||
onClone archive.SimType
|
||||
setIsOpen false
|
||||
|
||||
let displayJson =
|
||||
let displayJson () =
|
||||
match inputOpt with
|
||||
| None ->
|
||||
html $"<span>This simulation does not have input information...</span>"
|
||||
@@ -4236,14 +4309,18 @@ let driftersInputModal
|
||||
let inputJsonStr = JS.JSON.stringify(value = input, space = 4)
|
||||
html $"<pre>{inputJsonStr}</pre>"
|
||||
|
||||
let displayInput =
|
||||
let displayInput () =
|
||||
let noop = fun _ -> ()
|
||||
// For the modal view, use the base archive time bounds
|
||||
let archiveStartUTC = model.archive.startTime.ToUniversalTime()
|
||||
let archiveEndT = archiveStartUTC.AddSeconds(model.archive.frames * model.archive.saveFreq |> float)
|
||||
|
||||
match simType with
|
||||
| TransportSim ->
|
||||
html
|
||||
$"""
|
||||
<sp-accordion size="s" allow-multiple>
|
||||
{setupMetaParams true 1e6 drifterModel.simulation noop drifterModel.tmpDrifter noop}
|
||||
{setupMetaParams true 10_000 drifterModel.simulation noop drifterModel.tmpDrifter noop archiveStartUTC archiveEndT}
|
||||
<sp-accordion-item ?open={true} label="Release sites">
|
||||
{displayReleaseGroups drifterModel.release.groups}
|
||||
</sp-accordion-item>
|
||||
@@ -4256,7 +4333,7 @@ let driftersInputModal
|
||||
html
|
||||
$"""
|
||||
<sp-accordion size="s" allow-multiple>
|
||||
{setupMetaParams true 1e6 drifterModel.simulation noop drifterModel.tmpDrifter noop}
|
||||
{setupMetaParams true 10_000 drifterModel.simulation noop drifterModel.tmpDrifter noop archiveStartUTC archiveEndT}
|
||||
<sp-accordion-item ?open={true} label="Release sites">
|
||||
{displayReleaseGroups drifterModel.release.groups}
|
||||
</sp-accordion-item>
|
||||
@@ -4272,7 +4349,7 @@ let driftersInputModal
|
||||
html
|
||||
$"""
|
||||
<sp-accordion size="s" allow-multiple>
|
||||
{setupMetaParams true 1e6 drifterModel.simulation noop drifterModel.tmpDrifter noop}
|
||||
{setupMetaParams true 10_000 drifterModel.simulation noop drifterModel.tmpDrifter noop archiveStartUTC archiveEndT}
|
||||
<sp-accordion-item ?open={true} label="Release sites">
|
||||
{displayReleaseGroups drifterModel.release.groups}
|
||||
</sp-accordion-item>
|
||||
@@ -4288,7 +4365,7 @@ let driftersInputModal
|
||||
html
|
||||
$"""
|
||||
<sp-accordion size="s" allow-multiple>
|
||||
{setupMetaParams true 1e6 drifterModel.simulation noop drifterModel.tmpDrifter noop}
|
||||
{setupMetaParams true 10_000 drifterModel.simulation noop drifterModel.tmpDrifter noop archiveStartUTC archiveEndT}
|
||||
<sp-accordion-item ?open={true} label="Release sites">
|
||||
{displayReleaseGroups drifterModel.release.groups}
|
||||
</sp-accordion-item>
|
||||
@@ -4302,7 +4379,7 @@ let driftersInputModal
|
||||
html
|
||||
$"""
|
||||
<sp-accordion size="s" allow-multiple>
|
||||
{setupMetaParams true 1e6 drifterModel.simulation noop drifterModel.tmpDrifter noop}
|
||||
{setupMetaParams true 10_000 drifterModel.simulation noop drifterModel.tmpDrifter noop archiveStartUTC archiveEndT}
|
||||
<sp-accordion-item ?open={true} label="Release groups">
|
||||
{Deposition.displayReleaseGroups drifterModel.traits drifterModel.release.groups}
|
||||
</sp-accordion-item>
|
||||
@@ -4325,11 +4402,13 @@ let driftersInputModal
|
||||
slot="click-content"
|
||||
underlay
|
||||
dismissable
|
||||
@close={Ev (fun _ -> setIsOpen false)}
|
||||
>
|
||||
<sp-dialog class="flex-grow" style="width: 900px; max-height: 800px">
|
||||
<sp-dialog
|
||||
class="flex-grow" style="width: 900px; max-height: 800px">
|
||||
<h2 slot="heading">Simulation type: {headline}</h2>
|
||||
<div style="overflow-y: scroll;">
|
||||
{if showJson then displayJson else displayInput}
|
||||
{if not isOpen then Lit.nothing elif showJson then displayJson () else displayInput ()}
|
||||
</div>
|
||||
<sp-button
|
||||
slot="button"
|
||||
@@ -4349,6 +4428,7 @@ let driftersInputModal
|
||||
<sp-action-button
|
||||
id="drifter-info-button"
|
||||
slot="trigger"
|
||||
@click={Ev (fun _ -> setIsOpen true)}
|
||||
?disabled="{false}"
|
||||
>
|
||||
<sp-icon-info slot="icon"></sp-icon-info>
|
||||
|
||||
100
src/Atlantis/src/Client/Mapster/FluentUI/DatePicker.fs
Normal file
100
src/Atlantis/src/Client/Mapster/FluentUI/DatePicker.fs
Normal file
@@ -0,0 +1,100 @@
|
||||
namespace FluentUI
|
||||
|
||||
module private ReactLib =
|
||||
open Browser
|
||||
open Fable.Core
|
||||
open Fable.Core.JsInterop
|
||||
open Feliz
|
||||
open System
|
||||
|
||||
import "DatePicker" "@fluentui/react-datepicker-compat"
|
||||
|
||||
import "FluentProvider" "@fluentui/react-components"
|
||||
import "Field" "@fluentui/react-components"
|
||||
|
||||
[<JSX.Component>]
|
||||
let DatePicker
|
||||
(
|
||||
disabled: bool,
|
||||
value: DateTime,
|
||||
onSelectDate: DateTime -> unit,
|
||||
minDate: DateTime,
|
||||
maxDate: DateTime
|
||||
) =
|
||||
let onSelectDateRef = React.useRef onSelectDate
|
||||
let internalValue, setInternalValue = React.useState value
|
||||
let isUserInteractionRef = React.useRef false
|
||||
|
||||
// Update ref on every render to always have the latest callback
|
||||
React.useEffect ((fun () -> onSelectDateRef.current <- onSelectDate), [| box onSelectDate |])
|
||||
|
||||
// Only update internal value when external value changes AND it's not from user interaction
|
||||
React.useEffect (
|
||||
(fun () ->
|
||||
if not isUserInteractionRef.current && internalValue <> value then
|
||||
setInternalValue value
|
||||
isUserInteractionRef.current <- false
|
||||
),
|
||||
[| box value |]
|
||||
)
|
||||
|
||||
let handleSelectDate =
|
||||
React.useCallback (
|
||||
(fun (data: obj) ->
|
||||
let date = data?value
|
||||
if not (isNull date) then
|
||||
let selectedDate: DateTime = unbox date
|
||||
// Validate against min/max constraints
|
||||
let isValid =
|
||||
if selectedDate >= minDate && selectedDate <= maxDate then
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
if isValid then
|
||||
isUserInteractionRef.current <- true
|
||||
setInternalValue selectedDate
|
||||
onSelectDateRef.current selectedDate
|
||||
),
|
||||
[| box minDate; box maxDate |]
|
||||
)
|
||||
|
||||
let onSelectDateHandler =
|
||||
React.useCallback (
|
||||
emitJsExpr
|
||||
handleSelectDate
|
||||
"""
|
||||
(ev, data) => {
|
||||
if (data != null) {
|
||||
$0(data);
|
||||
} else {
|
||||
$0({ value: ev });
|
||||
}
|
||||
}
|
||||
""",
|
||||
[| box handleSelectDate |]
|
||||
)
|
||||
|
||||
JSX.html
|
||||
$"""
|
||||
<FluentProvider theme={Lib.webLightTheme}>
|
||||
<Field>
|
||||
<DatePicker
|
||||
inlinePopup
|
||||
firstDayOfWeek={1}
|
||||
showGoToToday={false}
|
||||
showWeekNumbers={true}
|
||||
disabled={disabled}
|
||||
value={internalValue}
|
||||
onSelectDate={onSelectDateHandler}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
/>
|
||||
</Field>
|
||||
</FluentProvider>
|
||||
"""
|
||||
|
||||
module Lit =
|
||||
open Lit
|
||||
|
||||
let DatePicker = React.toLit (ReactLib.DatePicker >> Lib.React.fromJsx)
|
||||
@@ -72,4 +72,4 @@ module private ReactLib =
|
||||
module Lit =
|
||||
open Lit
|
||||
|
||||
let PeriodCalendar<'T> = React.toLit (ReactLib.PeriodCalendar >> Lib.React.fromJsx)
|
||||
let PeriodCalendar<'T> = React.toLit (ReactLib.PeriodCalendar >> 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
|
||||
}
|
||||
}
|
||||
@@ -905,13 +909,14 @@ let update cmd model =
|
||||
if model.frame + t >= 0 && t < model.archive.frames then
|
||||
// NOTE: The bounds checking above lets us just get the Option
|
||||
let currentTime = Utils.Archives.findFrameTime model.archive t
|
||||
console.debug ("Mapster.SetFrame currentTime:", currentTime)
|
||||
let currentTimeUTC = currentTime.ToUniversalTime()
|
||||
console.debug ("[Mapster] SetFrame currentTime:", currentTimeUTC)
|
||||
// NOTE: When changing frame, update the simulations start time for the one you are creating
|
||||
let updatedDrifterOpt: SimArchive option =
|
||||
model.selectedPostdrift
|
||||
|> Option.map (fun d ->
|
||||
if d.Status = JobStatus.New then
|
||||
let archive' = { d.Archive with startTime = currentTime }
|
||||
let archive' = { d.Archive with startTime = currentTimeUTC }
|
||||
{ d with Archive = archive' }
|
||||
else
|
||||
d
|
||||
@@ -1534,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
|
||||
},
|
||||
@@ -1571,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
|
||||
@@ -1794,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
|
||||
|
||||
@@ -1872,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>
|
||||
@@ -14,6 +14,7 @@
|
||||
<Compile Include="SignalRHub.fs" />
|
||||
<Compile Include="FluentUI/Lib.fs" />
|
||||
<Compile Include="FluentUI/Select.fs" />
|
||||
<Compile Include="FluentUI/DatePicker.fs" />
|
||||
<Compile Include="FluentUI/ColormapSelect.fs" />
|
||||
<Compile Include="FluentUI/PeriodCalendar.fs" />
|
||||
<Compile Include="Colors.fs" />
|
||||
@@ -36,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 =
|
||||
@@ -1152,8 +1212,8 @@ let sideNav (dispatch: Msg -> unit) (model: Model) =
|
||||
<span>Step:</span>
|
||||
</div>
|
||||
<div class="times">
|
||||
<span>{currentTime.ToShortDateString ()}</span>
|
||||
<span>{currentTime.ToString "HH:mm"}</span>
|
||||
<span>{currentTime.ToLocalTime().ToShortDateString ()}</span>
|
||||
<span>{currentTime.ToLocalTime().ToString "HH:mm"}</span>
|
||||
<span>{stepStr}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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
|
||||
|
||||
@@ -59,28 +59,27 @@ let simStatusMessage (job: JobInfo) =
|
||||
/// </summary>
|
||||
/// <param name="siteOpt"></param>
|
||||
/// <param name="map"></param>
|
||||
let updateSelectedReleaseSite (siteOpt: (int*PlumeReleaseSite) option, map) =
|
||||
let updateSelectedReleaseSite (siteOpt: (int * PlumeReleaseSite) option, map) =
|
||||
map
|
||||
|> updateBaseLayer MapLayer.SelectedReleaseGroup (fun baseLayer ->
|
||||
let layer = baseLayer :?> VectorLayer
|
||||
let source = layer.getSource () :?> VectorSource
|
||||
source.clear()
|
||||
|> updateBaseLayer
|
||||
MapLayer.SelectedReleaseGroup
|
||||
(fun baseLayer ->
|
||||
let layer = baseLayer :?> VectorLayer
|
||||
let source = layer.getSource () :?> VectorSource
|
||||
source.clear ()
|
||||
|
||||
match siteOpt with
|
||||
| Some (sIdx, site) ->
|
||||
let lat = site.position |> toWgs84' |> snd
|
||||
let r' = site.radius * mercatorScaleFactor lat
|
||||
let p' = site.position |> posToCoord
|
||||
let circle = Geometry.circle p' r' GeometryLayout.XY
|
||||
let feature =
|
||||
Feature.feature [
|
||||
feature.geometryOrProperties circle
|
||||
]
|
||||
feature.setId(sIdx)
|
||||
match siteOpt with
|
||||
| Some (sIdx, site) ->
|
||||
let lat = site.position |> toWgs84' |> snd
|
||||
let r' = site.radius * mercatorScaleFactor lat
|
||||
let p' = site.position |> posToCoord
|
||||
let circle = Geometry.circle p' r' GeometryLayout.XY
|
||||
let feature = Feature.feature [ feature.geometryOrProperties circle ]
|
||||
feature.setId (sIdx)
|
||||
|
||||
source.addFeature(feature)
|
||||
| _ -> ()
|
||||
)
|
||||
source.addFeature (feature)
|
||||
| _ -> ()
|
||||
)
|
||||
|
||||
/// <summary>
|
||||
/// Clears all release features, and add all sites as features to the map
|
||||
@@ -88,31 +87,30 @@ let updateSelectedReleaseSite (siteOpt: (int*PlumeReleaseSite) option, map) =
|
||||
/// <param name="selected"></param>
|
||||
/// <param name="sites"></param>
|
||||
/// <param name="map"></param>
|
||||
let updateUnselectedReleaseSites (selected: (int*PlumeReleaseSite) option, sites: PlumeReleaseSite list, map) =
|
||||
let updateUnselectedReleaseSites (selected: (int * PlumeReleaseSite) option, sites: PlumeReleaseSite list, map) =
|
||||
map
|
||||
|> updateBaseLayer MapLayer.UnselectedReleaseGroups (fun baseLayer ->
|
||||
let layer = baseLayer :?> VectorLayer
|
||||
let source = layer.getSource () :?> VectorSource
|
||||
source.clear()
|
||||
|> updateBaseLayer
|
||||
MapLayer.UnselectedReleaseGroups
|
||||
(fun baseLayer ->
|
||||
let layer = baseLayer :?> VectorLayer
|
||||
let source = layer.getSource () :?> VectorSource
|
||||
source.clear ()
|
||||
|
||||
sites
|
||||
|> List.iteri (fun idx site ->
|
||||
match selected with
|
||||
| Some (i, _) when i = idx -> () // Site will be shown in selected layer
|
||||
| _ ->
|
||||
let lat = site.position |> toWgs84' |> snd
|
||||
let r' = site.radius * mercatorScaleFactor lat
|
||||
let p' = site.position |> posToCoord
|
||||
let circle = Geometry.circle p' r' GeometryLayout.XY
|
||||
let feature =
|
||||
Feature.feature [
|
||||
feature.geometryOrProperties circle
|
||||
]
|
||||
feature.setId(idx)
|
||||
sites
|
||||
|> List.iteri (fun idx site ->
|
||||
match selected with
|
||||
| Some (i, _) when i = idx -> () // Site will be shown in selected layer
|
||||
| _ ->
|
||||
let lat = site.position |> toWgs84' |> snd
|
||||
let r' = site.radius * mercatorScaleFactor lat
|
||||
let p' = site.position |> posToCoord
|
||||
let circle = Geometry.circle p' r' GeometryLayout.XY
|
||||
let feature = Feature.feature [ feature.geometryOrProperties circle ]
|
||||
feature.setId (idx)
|
||||
|
||||
source.addFeature(feature)
|
||||
source.addFeature (feature)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
let private update (msg: PlumeMsg) (model: PlumeModel) =
|
||||
match msg with
|
||||
@@ -148,11 +146,7 @@ let private update (msg: PlumeMsg) (model: PlumeModel) =
|
||||
// NOTE: We can deselect a site, so the id must be an option
|
||||
let siteOpt =
|
||||
sIdxOpt
|
||||
|> Option.bind (fun siteId ->
|
||||
model.sites
|
||||
|> List.tryItem siteId
|
||||
|> Option.map (fun site -> siteId, site)
|
||||
)
|
||||
|> Option.bind (fun siteId -> model.sites |> List.tryItem siteId |> Option.map (fun site -> siteId, site))
|
||||
|
||||
console.debug ($"[Plume] SelectSite msg not opt :%A{siteOpt}")
|
||||
siteOpt
|
||||
@@ -160,21 +154,27 @@ let private update (msg: PlumeMsg) (model: PlumeModel) =
|
||||
|> Option.iter (fun site ->
|
||||
let zoom = if site.radius > 25.0 then 14.0 else 22.0
|
||||
let coord = site.position |> posToCoord
|
||||
flyTo model.openLayersMap zoom coord)
|
||||
flyTo model.openLayersMap zoom coord
|
||||
)
|
||||
|
||||
let m = { model with selectedSite = siteOpt }
|
||||
m,
|
||||
Elmish.Cmd.batch [
|
||||
Elmish.Cmd.OfFunc.perform updateSelectedReleaseSite (m.selectedSite, m.openLayersMap) PlumeMsg.Noop
|
||||
Elmish.Cmd.OfFunc.perform updateUnselectedReleaseSites (m.selectedSite, m.sites, m.openLayersMap) PlumeMsg.Noop
|
||||
Elmish.Cmd.OfFunc.perform
|
||||
updateUnselectedReleaseSites
|
||||
(m.selectedSite, m.sites, m.openLayersMap)
|
||||
PlumeMsg.Noop
|
||||
]
|
||||
| SetTraits t ->
|
||||
console.debug ("[Plume] SetTraits msg:", t)
|
||||
{ model with traits = t }, Elmish.Cmd.none
|
||||
| SetSimModel s ->
|
||||
console.debug ("[Plume] SetSimModel msg:", s)
|
||||
{ model with simulation = s }, Elmish.Cmd.none
|
||||
| PlumeMsg.SetSimStarted (started, jobIdOpt) -> {model with simStarted = (started, jobIdOpt)}, Elmish.Cmd.none
|
||||
// Timeline changes should not override plume simulation dates
|
||||
let sim' = { model.simulation with name = s.name; fvcom = s.fvcom; timeIdx = s.timeIdx }
|
||||
{ model with simulation = sim' }, Elmish.Cmd.none
|
||||
| PlumeMsg.SetSimStarted (started, jobIdOpt) -> { model with simStarted = (started, jobIdOpt) }, Elmish.Cmd.none
|
||||
| PlumeMsg.ResetModel m -> { m with simulation.name = model.simulation.name }, Elmish.Cmd.none
|
||||
| PlumeMsg.Noop () -> model, Elmish.Cmd.none
|
||||
|
||||
@@ -197,7 +197,9 @@ let placingToggleButton (disabled: bool) (map: OlMap) (onPlace: Coordinate -> un
|
||||
Hook.createDisposable (fun () ->
|
||||
let elem = map.getTargetElement ()
|
||||
elem?style?cursor <- ""
|
||||
mapClickKey.contents |> Option.iter Observable.unByKey))
|
||||
mapClickKey.contents |> Option.iter Observable.unByKey
|
||||
)
|
||||
)
|
||||
|
||||
Hook.useEffectOnChange (placing, crossHairSelect map mapClickKey releaseClickHandler)
|
||||
|
||||
@@ -217,7 +219,7 @@ let placingToggleButton (disabled: bool) (map: OlMap) (onPlace: Coordinate -> un
|
||||
[<HookComponent>]
|
||||
let private releaseSitesControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeModel) =
|
||||
let defaultSite, setDefaultSite = Hook.useState PlumeReleaseSite.empty
|
||||
console.debug("[Plume.ReleaseControls] defaultSite :", defaultSite)
|
||||
console.debug ("[Plume.ReleaseControls] defaultSite :", defaultSite)
|
||||
|
||||
let tryFence (site: PlumeReleaseSite) : PlumeReleaseSite option =
|
||||
match xmodel'.fence with
|
||||
@@ -230,18 +232,18 @@ let private releaseSitesControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeMo
|
||||
if circle.intersectsCoordinate (site.position |> posToCoord) then
|
||||
Some site
|
||||
else
|
||||
console.error("[Plume.ReleaseControls] Trying to place release site outside of fencing radius")
|
||||
console.error ("[Plume.ReleaseControls] Trying to place release site outside of fencing radius")
|
||||
None
|
||||
|
||||
let handleMapPlaceRelease (coords: Coordinate) =
|
||||
console.debug($"[Plume.ReleaseControls] Click add site : %s{coords.ToString ()}")
|
||||
console.debug ($"[Plume.ReleaseControls] Click add site : %s{coords.ToString ()}")
|
||||
{ defaultSite with position = coordToPos coords }
|
||||
|> tryFence
|
||||
|> Option.map (fun site -> [ site ] |> PlumeMsg.AddReleaseSites |> dispatch')
|
||||
|> Option.defaultValue ()
|
||||
|
||||
let handleAddCoordRelease _ =
|
||||
console.debug($"[Plume.ReleaseControls] Add site by coord : %s{defaultSite.position.ToString ()}")
|
||||
console.debug ($"[Plume.ReleaseControls] Add site by coord : %s{defaultSite.position.ToString ()}")
|
||||
defaultSite
|
||||
|> tryFence
|
||||
|> Option.map (fun site -> [ site ] |> PlumeMsg.AddReleaseSites |> dispatch')
|
||||
@@ -259,13 +261,9 @@ let private releaseSitesControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeMo
|
||||
|> toWgs84'
|
||||
|
||||
let deleteSite idx (_: Browser.Types.Event) =
|
||||
idx
|
||||
|> PlumeMsg.RemoveReleaseSite
|
||||
|> dispatch'
|
||||
idx |> PlumeMsg.RemoveReleaseSite |> dispatch'
|
||||
|
||||
None
|
||||
|> PlumeMsg.SelectSite
|
||||
|> dispatch'
|
||||
None |> PlumeMsg.SelectSite |> dispatch'
|
||||
|
||||
// De-select if already selected
|
||||
let selectSite sIdx (_: Browser.Types.Event) =
|
||||
@@ -280,13 +278,19 @@ let private releaseSitesControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeMo
|
||||
|> Option.map (fst >> (=) sIdx)
|
||||
|> Option.defaultValue false
|
||||
|
||||
let setRadius (v: float) = updateSelectedSite (fun site -> { site with radius = v })
|
||||
let setDepth (v: int) = updateSelectedSite (fun site -> { site with depth = -v })
|
||||
let setTheta (v: int) = updateSelectedSite (fun site -> { site with theta = v })
|
||||
let setRadius (v: float) =
|
||||
updateSelectedSite (fun site -> { site with radius = v })
|
||||
let setDepth (v: int) =
|
||||
updateSelectedSite (fun site -> { site with depth = -v })
|
||||
let setTheta (v: int) =
|
||||
updateSelectedSite (fun site -> { site with theta = v })
|
||||
|
||||
let setTemperature (v: float) = { xmodel'.traits with temp = v } |> SetTraits |> dispatch'
|
||||
let setSalinity (v: float) = { xmodel'.traits with salt = v } |> SetTraits |> dispatch'
|
||||
let setTransport (v: float) = { xmodel'.traits with transport = v } |> SetTraits |> dispatch'
|
||||
let setTemperature (v: float) =
|
||||
{ xmodel'.traits with temp = v } |> SetTraits |> dispatch'
|
||||
let setSalinity (v: float) =
|
||||
{ xmodel'.traits with salt = v } |> SetTraits |> dispatch'
|
||||
let setTransport (v: float) =
|
||||
{ xmodel'.traits with transport = v } |> SetTraits |> dispatch'
|
||||
|
||||
let diameterBox =
|
||||
// Display in mm diameter, store in m radius
|
||||
@@ -521,11 +525,11 @@ let private releaseSitesControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeMo
|
||||
value={latitude}
|
||||
?disabled="{false}"
|
||||
@change={EvVal (
|
||||
unbox<float>
|
||||
>> fun v ->
|
||||
let newPos = toEpsg3857' (fst selectedSitePos, v)
|
||||
updateSelectedSite (fun site -> { site with position = newPos })
|
||||
)}
|
||||
unbox<float>
|
||||
>> fun v ->
|
||||
let newPos = toEpsg3857' (fst selectedSitePos, v)
|
||||
updateSelectedSite (fun site -> { site with position = newPos })
|
||||
)}
|
||||
>
|
||||
</sp-number-field>
|
||||
</sp-field-group>
|
||||
@@ -558,11 +562,11 @@ let private releaseSitesControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeMo
|
||||
value={longitude}
|
||||
?disabled="{false}"
|
||||
@change={EvVal (
|
||||
unbox<float>
|
||||
>> fun v ->
|
||||
let newPos = toEpsg3857' (v, snd selectedSitePos)
|
||||
updateSelectedSite (fun site -> { site with position = newPos })
|
||||
)}
|
||||
unbox<float>
|
||||
>> fun v ->
|
||||
let newPos = toEpsg3857' (v, snd selectedSitePos)
|
||||
updateSelectedSite (fun site -> { site with position = newPos })
|
||||
)}
|
||||
></sp-number-field>
|
||||
</sp-field-group>
|
||||
"""
|
||||
@@ -595,14 +599,14 @@ let private releaseSitesControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeMo
|
||||
label="Edit"
|
||||
style="width: 35px"
|
||||
?selected="{isSelected sIdx}"
|
||||
@click={Ev(selectSite sIdx)}
|
||||
@click={Ev (selectSite sIdx)}
|
||||
>
|
||||
<sp-icon-edit slot="icon"></sp-icon-edit>
|
||||
</sp-action-button>
|
||||
|
||||
<sp-action-button
|
||||
style="width: 35px"
|
||||
@click={Ev(deleteSite sIdx)}
|
||||
@click={Ev (deleteSite sIdx)}
|
||||
>
|
||||
<sp-icon-delete slot="icon"></sp-icon-delete>
|
||||
<sp-tooltip placement="right" self-managed>Remove site</sp-tooltip>
|
||||
@@ -635,7 +639,7 @@ let private releaseSitesControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeMo
|
||||
<sp-action-button
|
||||
style="width: 300px"
|
||||
?disabled={not xmodel'.sites.IsEmpty}
|
||||
@click={Ev(handleAddCoordRelease)}
|
||||
@click={Ev (handleAddCoordRelease)}
|
||||
>
|
||||
<sp-icon-add slot="icon"></sp-icon-add>
|
||||
Add by coordinates
|
||||
@@ -668,141 +672,177 @@ let private plumeControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeModel) =
|
||||
[<HookComponent>]
|
||||
let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Model) =
|
||||
let archive = model.archive
|
||||
let plumeModelOpt = model.plumeModelOpt
|
||||
let isLoading = model.isLoading
|
||||
let map = model.map
|
||||
let currentFrame = model.frame
|
||||
let archiveStartUTC = archive.startTime.ToUniversalTime ()
|
||||
let archiveEndT =
|
||||
archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float)
|
||||
let simStartT = archiveStartUTC.AddSeconds (model.frame * archive.saveFreq |> float)
|
||||
let simStartT =
|
||||
archiveStartUTC.AddSeconds (currentFrame * archive.saveFreq |> float)
|
||||
let submitted, setSubmitted = Hook.useState false
|
||||
let simDays, setSimDays = Hook.useState 7.0
|
||||
|
||||
let createNewModel () : PlumeModel =
|
||||
let simulation: PlumeSimModel = {
|
||||
PlumeSimModel.empty with
|
||||
fvcom = archive.id
|
||||
start = simStartT
|
||||
stop = simStartT.AddDays(1)
|
||||
// TODO: save frequency e.g 24 (daily)
|
||||
}
|
||||
// Preserve existing dates when updating other fields (coordinates, release pipes)
|
||||
let existingSimulation = plumeModelOpt |> Option.map (fun m -> m.simulation)
|
||||
|
||||
let simulation: PlumeSimModel =
|
||||
match existingSimulation with
|
||||
| Some existing -> { existing with fvcom = archive.id }
|
||||
| None ->
|
||||
// NOTE: Start plume at mid day
|
||||
let simStartMidday =
|
||||
DateTime (simStartT.Year, simStartT.Month, simStartT.Day, 12, 0, 0)
|
||||
{
|
||||
PlumeSimModel.empty with
|
||||
fvcom = archive.id
|
||||
start = simStartMidday
|
||||
stop = simStartMidday.AddDays 1
|
||||
}
|
||||
|
||||
let traits = PlumeTraits.empty
|
||||
|
||||
console.debug ("[Plume.SimControls] initialModel release:", traits, "sim:", simulation)
|
||||
// NOTE: If the top-level map has saved a model, use it instead
|
||||
{
|
||||
fence = model.archive.polygon
|
||||
fence = archive.polygon
|
||||
kind = DefaultPlume
|
||||
simStarted = false, None
|
||||
sites = []
|
||||
simulation = simulation
|
||||
traits = traits
|
||||
selectedSite = None
|
||||
openLayersMap = model.map
|
||||
openLayersMap = map
|
||||
}
|
||||
|
||||
let initialModel () =
|
||||
let xmodel' =
|
||||
model.plumeModelOpt |> Option.defaultWith (fun () -> createNewModel ())
|
||||
xmodel', Elmish.Cmd.none
|
||||
|
||||
let xmodel', dispatch' = Hook.useElmish (initialModel, update)
|
||||
let xmodel', dispatch' =
|
||||
Hook.useElmish (
|
||||
(fun () ->
|
||||
let xmodel' =
|
||||
match plumeModelOpt with
|
||||
| Some existingModel ->
|
||||
console.debug (
|
||||
"[Plume.SimControls] Preserving existing plume model with dates:",
|
||||
existingModel.simulation.start,
|
||||
existingModel.simulation.stop
|
||||
)
|
||||
existingModel
|
||||
| None ->
|
||||
console.debug ("[Plume.SimControls] Creating new plume model")
|
||||
createNewModel ()
|
||||
xmodel', Elmish.Cmd.none
|
||||
),
|
||||
update
|
||||
)
|
||||
let modelRef = Hook.useRef (Some xmodel')
|
||||
|
||||
modelRef.contents |> SetPlumeModel |> dispatch
|
||||
// Only update parent model when xmodel' actually changes
|
||||
Hook.useEffectOnChange (
|
||||
xmodel',
|
||||
fun newModel ->
|
||||
modelRef.contents <- Some newModel
|
||||
SetPlumeModel (Some newModel) |> dispatch
|
||||
)
|
||||
|
||||
///////////////
|
||||
// META DATA //
|
||||
///////////////
|
||||
|
||||
// let setStartDate (str: string) =
|
||||
// let startDate = DateTime.Parse (str)
|
||||
// let startDate' = simStartT
|
||||
//
|
||||
// SetSimModel { xmodel'.simulation with start = startDate } |> dispatch'
|
||||
let setStartDateTime (dt: DateTime) =
|
||||
// Set time to midday (12:00)
|
||||
let dtMidday = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
|
||||
console.debug ("[Plume] setStartDateTime:", dtMidday)
|
||||
|
||||
// let setStopDate (str: string) =
|
||||
// let stopDate = DateTime.Parse(str)
|
||||
// SetSimModel { xmodel'.simulation with stop = stopDate }
|
||||
// |> dispatch'
|
||||
// Use SetRelease to update dates directly without going through SetSimModel
|
||||
// This avoids triggering unnecessary elmish updates that cause datepicker rerenders
|
||||
SetRelease (dtMidday, xmodel'.simulation.stop) |> dispatch'
|
||||
|
||||
// let Picker(title: string, callback: unit -> unit) =
|
||||
// JSX.jsx"""
|
||||
// import {DatePicker,defaultTheme, Provider} from '@adobe/react-spectrum'
|
||||
// <Provider theme={defaultTheme} >
|
||||
// <DatePicker label={title} />
|
||||
// </Provider>
|
||||
// """
|
||||
// |> toReact
|
||||
//
|
||||
// let litPicker f: TemplateResult =
|
||||
// React.toLit Picker f
|
||||
// NOTE: Plume dates should update timeline, but timeline should never update plume dates
|
||||
// Calculate frame that corresponds to this date and update timeline
|
||||
let duration = dtMidday - archiveStartUTC
|
||||
let newFrame = int (duration.TotalSeconds / float archive.saveFreq)
|
||||
if newFrame >= 0 && newFrame < archive.frames then
|
||||
console.debug ("[Plume] Updating timeline to frame:", newFrame)
|
||||
SetFrame newFrame |> dispatch
|
||||
|
||||
let setStopDateTime (dt: DateTime) =
|
||||
// Set time to midday (12:00)
|
||||
let dtMidday = DateTime (dt.Year, dt.Month, dt.Day, 12, 0, 0)
|
||||
console.debug ("[Plume] setStopDateTime:", dtMidday)
|
||||
|
||||
// Use SetRelease to update dates directly without going through SetSimModel
|
||||
SetRelease (xmodel'.simulation.start, dtMidday) |> dispatch'
|
||||
|
||||
// NOTE: Only start date updates timeline position, stop date just sets duration
|
||||
let setName (s: string) =
|
||||
SetSimModel { xmodel'.simulation with name = s } |> dispatch'
|
||||
let currentModel = modelRef.contents |> Option.defaultValue xmodel'
|
||||
SetSimModel { currentModel.simulation with name = s } |> dispatch'
|
||||
|
||||
let maxDurationH =
|
||||
let buffer = 1.0
|
||||
(archiveEndT - simStartT).TotalHours - buffer
|
||||
|
||||
// Update stop time when start or duration changes
|
||||
Hook.useEffectOnChange(
|
||||
(xmodel'.simulation.start, simDays),
|
||||
fun (tStart, duration) ->
|
||||
let tStop =
|
||||
duration
|
||||
|> TimeSpan.FromDays
|
||||
|> tStart.Add
|
||||
{ xmodel'.simulation with stop = tStop }
|
||||
|> SetSimModel
|
||||
|> dispatch'
|
||||
console.debug (
|
||||
"[Plume] Rendering metaControls with start:",
|
||||
xmodel'.simulation.start,
|
||||
"stop:",
|
||||
xmodel'.simulation.stop
|
||||
)
|
||||
|
||||
let startDateKey = "start-date"
|
||||
let endDateKey = "end-date"
|
||||
|
||||
// Calculate min/max dates with buffer
|
||||
// let bufferTimeSpan = TimeSpan.FromHours(bufferHours)
|
||||
let minStartDate = archiveStartUTC
|
||||
let maxStartDate = archiveEndT
|
||||
let minEndDate = archiveStartUTC
|
||||
let maxEndDate = archiveEndT
|
||||
|
||||
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="sim-name"
|
||||
label="Name"
|
||||
placeholder="simulation-name"
|
||||
required="true"
|
||||
value="{xmodel'.simulation.name}"
|
||||
@change={EvVal(setName)}
|
||||
style="width: 300px"
|
||||
></sp-textfield>
|
||||
</sp-field-group>
|
||||
</div>
|
||||
$"""
|
||||
<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="sim-name"
|
||||
label="Name"
|
||||
placeholder="simulation-name"
|
||||
required="true"
|
||||
value="{xmodel'.simulation.name}"
|
||||
@change={EvVal (setName)}
|
||||
style="width: 300px"
|
||||
></sp-textfield>
|
||||
</sp-field-group>
|
||||
</div>
|
||||
|
||||
<sp-field-group horizontal id="vw" style="padding-bottom: 5px">
|
||||
<div
|
||||
style="
|
||||
margin: 10px;
|
||||
padding-bottom: 5px;
|
||||
"
|
||||
>
|
||||
<sp-field-label size="s" for="sim-duration">
|
||||
Duration (days)
|
||||
</sp-field-label>
|
||||
<sp-number-field
|
||||
id="sim-duration"
|
||||
label="SimDuration"
|
||||
value="{simDays}"
|
||||
min={1.0}
|
||||
max={maxDurationH / 24.0}
|
||||
@change={EvVal(unbox >> setSimDays)}
|
||||
style="width: 140px"
|
||||
format-options="{formatDigits 0 0}"
|
||||
></sp-number-field>
|
||||
</div>
|
||||
</sp-field-group>
|
||||
<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'.simulation.start, setStartDateTime, minStartDate, maxStartDate)}
|
||||
</sp-field-label>
|
||||
</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'.simulation.stop, setStopDateTime, minEndDate, maxEndDate)}
|
||||
</sp-field-group>
|
||||
</sp-field-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
@@ -818,7 +858,7 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
|> List.tryHead
|
||||
|> Option.defaultWith (fun () -> PlumeReleaseSite.empty)
|
||||
let sim' = xmodel'.simulation
|
||||
let id = Guid.NewGuid()
|
||||
let id = Guid.NewGuid ()
|
||||
|
||||
let long, lat = site'.position |> toWgs84'
|
||||
let latLong = { Lat = lat; Long = long }
|
||||
@@ -849,25 +889,24 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
|
||||
let! job = api.startPlume input
|
||||
setSubmitted true
|
||||
do Lib.Umami.track(
|
||||
"mapster-submit-plume",
|
||||
{|
|
||||
archiveId = string archive.id
|
||||
archiveName = string archive.name
|
||||
status = if job.IsSome then "success" else "failed"
|
||||
|}
|
||||
)
|
||||
do
|
||||
Lib.Umami.track (
|
||||
"mapster-submit-plume",
|
||||
{|
|
||||
archiveId = string archive.id
|
||||
archiveName = string archive.name
|
||||
status = if job.IsSome then "success" else "failed"
|
||||
|}
|
||||
)
|
||||
|
||||
match job with
|
||||
| None ->
|
||||
Note.failure "[Plume] Job submission failed"
|
||||
|> SetNotification
|
||||
|> dispatch
|
||||
Note.failure "[Plume] Job submission failed" |> SetNotification |> dispatch
|
||||
|
||||
PlumeMsg.SetSimStarted (true, Some 0) |> dispatch'
|
||||
None |> SetLoading |> dispatch
|
||||
|
||||
let msg : Petimeter.Inbox.InboxItem = {
|
||||
let msg: Petimeter.Inbox.InboxItem = {
|
||||
id = Guid.Empty
|
||||
content = $"Job '{sim'.name}' : Failed"
|
||||
unread = true
|
||||
@@ -896,16 +935,16 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
|
||||
let reset (_: Browser.Types.Event) =
|
||||
console.debug ("[Plume.SimControls] Reset sim")
|
||||
clearFeatures model.map MapLayer.SelectedReleaseGroup
|
||||
clearFeatures model.map MapLayer.UnselectedReleaseGroups
|
||||
clearFeatures map MapLayer.SelectedReleaseGroup
|
||||
clearFeatures map MapLayer.UnselectedReleaseGroups
|
||||
None |> SetLoading |> dispatch
|
||||
|
||||
createNewModel () |> PlumeMsg.ResetModel |> dispatch'
|
||||
|
||||
let cancel (_: Browser.Types.Event) =
|
||||
console.debug ("[Plume.SimControls] Cancel sim")
|
||||
clearFeatures model.map MapLayer.SelectedReleaseGroup
|
||||
clearFeatures model.map MapLayer.UnselectedReleaseGroups
|
||||
clearFeatures map MapLayer.SelectedReleaseGroup
|
||||
clearFeatures map MapLayer.UnselectedReleaseGroups
|
||||
None |> SetLoading |> dispatch
|
||||
|
||||
modelRef.contents <- None
|
||||
@@ -913,7 +952,8 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
SetMode Mode.Moot |> dispatch
|
||||
|
||||
let loadingSpinner =
|
||||
html $"""
|
||||
html
|
||||
$"""
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
@@ -951,7 +991,7 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
static="primary"
|
||||
style="width: 100px;"
|
||||
?disabled={noName || noSites || submitted || loading}
|
||||
@click={Ev(submit)}
|
||||
@click={Ev (submit)}
|
||||
>
|
||||
Submit
|
||||
</sp-action-button>
|
||||
@@ -959,14 +999,14 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
static="primary"
|
||||
style="width: 100px;"
|
||||
?disabled={loading}
|
||||
@click={Ev(reset)}
|
||||
@click={Ev (reset)}
|
||||
>
|
||||
Reset
|
||||
</sp-action-button>
|
||||
<sp-action-button
|
||||
static="primary"
|
||||
style="width: 100px;"
|
||||
@click={Ev(cancel)}
|
||||
@click={Ev (cancel)}
|
||||
>
|
||||
Cancel
|
||||
</sp-action-button>
|
||||
@@ -1003,9 +1043,9 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
xmodel'.simStarted,
|
||||
fun (updatedStarted, _) ->
|
||||
if updatedStarted then
|
||||
console.log("[Plume.SimControls] Plume started: resetting")
|
||||
do clearFeatures model.map MapLayer.SelectedReleaseGroup
|
||||
do clearFeatures model.map MapLayer.UnselectedReleaseGroups
|
||||
console.log ("[Plume.SimControls] Plume started: resetting")
|
||||
do clearFeatures map MapLayer.SelectedReleaseGroup
|
||||
do clearFeatures map MapLayer.UnselectedReleaseGroups
|
||||
Msg.SetSideNavMode OceanControls |> dispatch
|
||||
modelRef.contents <- None
|
||||
)
|
||||
@@ -1013,18 +1053,13 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
//
|
||||
// Top-level model changes
|
||||
//
|
||||
Hook.useEffectOnChange (
|
||||
model.frame,
|
||||
fun _ ->
|
||||
// NOTE(simkir): Only update the sim item when it is new, when it has been started, do not update
|
||||
{ xmodel'.simulation with start = simStartT } |> SetSimModel |> dispatch'
|
||||
)
|
||||
|
||||
let measuresHeight =
|
||||
tryGetElemRect "measures-controls"
|
||||
|> Option.map _.height
|
||||
|> Option.defaultValue 80
|
||||
|
||||
// NOTE: Dates are source of truth - they update timeline but are never updated by timeline
|
||||
html
|
||||
$"""
|
||||
<div
|
||||
@@ -1053,10 +1088,11 @@ let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Mo
|
||||
>
|
||||
{metaControls}
|
||||
{if xmodel'.kind = DefaultPlume then
|
||||
plumeControls dispatch' xmodel'
|
||||
else Lit.nothing}
|
||||
{if model.isLoading.IsSome then loadingSpinner else Lit.nothing}
|
||||
plumeControls dispatch' xmodel'
|
||||
else
|
||||
Lit.nothing}
|
||||
{if isLoading.IsSome then loadingSpinner else Lit.nothing}
|
||||
</div>
|
||||
</div>
|
||||
{submitButtons model.isLoading.IsSome}
|
||||
{submitButtons isLoading.IsSome}
|
||||
"""
|
||||
@@ -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 ->
|
||||
|
||||
@@ -149,15 +149,17 @@ module Timeline =
|
||||
|> Math.Round
|
||||
|> int
|
||||
|
||||
let private handleTimeChange dispatch (archive: ArchiveInfo) onFrameCallback newTime =
|
||||
let time: DateTime = newTime
|
||||
let ct: TimeSpan = time - archive.startTime
|
||||
let private handleTimeChange dispatch (archive: ArchiveInfo) onFrameCallback (newTime: DateTime) =
|
||||
let timeUtc = if newTime.Kind = DateTimeKind.Local then newTime.ToUniversalTime() else newTime
|
||||
let ct: TimeSpan = newTime - archive.startTime
|
||||
let frame =
|
||||
let f = getClosestFrame ct archive.saveFreq
|
||||
if f < 0 then 0
|
||||
else if f > archive.frames then archive.frames
|
||||
else f
|
||||
// console.log $"new time: {newTime} -> ({ct.TotalSeconds}s) on frame {frame}/{archive.frames}"
|
||||
|
||||
console.debug($"[Timeline] new time: %A{newTime} -> (%f{ct.TotalSeconds}s) on frame %i{frame}/%i{archive.frames}")
|
||||
|
||||
onFrameCallback frame
|
||||
SetPlaying false |> dispatch
|
||||
|
||||
@@ -167,7 +169,8 @@ module Timeline =
|
||||
archive.saveFreq * archive.frames
|
||||
|> float
|
||||
|> TimeSpan.FromSeconds
|
||||
// console.log $"{archive.name} has lasted for: {archive.saveFreq}s * {archive.frames} frames = {duration.TotalDays} day(s) ({duration.TotalHours}h)"
|
||||
|
||||
console.debug($"[Timeline] {archive.name} has lasted for: {archive.saveFreq}s * {archive.frames} frames = {duration.TotalDays} day(s) ({duration.TotalHours}h)")
|
||||
|
||||
let aid = string archive.id
|
||||
timelineItem [
|
||||
@@ -245,20 +248,21 @@ module Timeline =
|
||||
console.debug("[Timeline] ==== Init ==== ")
|
||||
let container = document.getElementById "timeline"
|
||||
let archive = props.archive
|
||||
let endTime =
|
||||
archive.startTime.AddSeconds(archive.saveFreq * archive.frames |> float)
|
||||
let startTime = archive.startTime.ToUniversalTime()
|
||||
let endTime = startTime.AddSeconds(archive.saveFreq * archive.frames |> float)
|
||||
let options =
|
||||
timelineOptions [
|
||||
// options.timeAxis !!{| scale = "hour"; step = 1 |}
|
||||
options.align.center
|
||||
options.verticalScroll false
|
||||
options.cluster {| maxItems = 2; showStipes = true |}
|
||||
options.min archive.startTime
|
||||
options.min startTime
|
||||
options.max endTime
|
||||
]
|
||||
// this is a mess. any state in handleRemove ends up in a closure, so we must
|
||||
// dispatch to remove items and catch the change in an effect to do the update
|
||||
// options?onRemove <- fun a -> handleRemove a // NOTE: must be lambda to work!
|
||||
// options?onRemove <- fun a -> handleRemove a
|
||||
// NOTE: must be lambda to work!
|
||||
|
||||
// console.log $"timeline options %A{options}"
|
||||
let t =
|
||||
@@ -268,7 +272,7 @@ module Timeline =
|
||||
groups = model.groupDataSet,
|
||||
options = options
|
||||
)
|
||||
t.setCurrentTime DateTime.Now
|
||||
t.setCurrentTime DateTime.UtcNow
|
||||
t.onSelect (handleSelect dispatch props.onSelect)
|
||||
t.onDoubleClick (fun ev -> handleTimeChange dispatch archive props.onFrameChange ev.time)
|
||||
t.onTimeChanged (fun ev -> handleTimeChange dispatch archive props.onFrameChange ev.time)
|
||||
@@ -281,10 +285,10 @@ module Timeline =
|
||||
props.archive,
|
||||
fun archive ->
|
||||
let t = model.tl.Value
|
||||
let endTime =
|
||||
archive.startTime.AddSeconds(archive.saveFreq * archive.frames |> float)
|
||||
t.addCustomTime(archive.startTime) |> ignore
|
||||
t.setOptions {| min = archive.startTime; max = endTime |}
|
||||
let startTime = archive.startTime.ToUniversalTime()
|
||||
let endTime = startTime.AddSeconds(archive.saveFreq * archive.frames |> float)
|
||||
t.addCustomTime(startTime) |> ignore
|
||||
t.setOptions {| min = startTime; max = endTime |}
|
||||
let groups = [|
|
||||
timelineGroup [
|
||||
group.id 1
|
||||
@@ -381,7 +385,7 @@ module Timeline =
|
||||
model.tl
|
||||
|> Option.iter (fun timeline ->
|
||||
let archive = props.archive
|
||||
let startTime = archive.startTime
|
||||
let startTime = archive.startTime.ToUniversalTime()
|
||||
let totalSeconds = n * archive.saveFreq |> float |> TimeSpan.FromSeconds
|
||||
let ct = startTime + totalSeconds
|
||||
|
||||
@@ -389,7 +393,7 @@ module Timeline =
|
||||
props.selected
|
||||
|> Option.iter (fun d ->
|
||||
// NOTE(stigrj): For some reason playing continues for one extra step, so stop one frame ahead of time
|
||||
let endTime = d.Archive.startTime + d.Duration - TimeSpan.FromSeconds(float d.Archive.freq)
|
||||
let endTime = d.Archive.startTime.ToUniversalTime() + d.Duration - TimeSpan.FromSeconds(float d.Archive.freq)
|
||||
if ct >= endTime then SetPlaying false |> dispatch)
|
||||
|
||||
do timeline.setCustomTime(ct)
|
||||
|
||||
@@ -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, )",
|
||||
@@ -715,7 +715,6 @@
|
||||
"Fable.Core": "4.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"net9.0/linux-x64": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -21,7 +23,7 @@ open Archmaester
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module Handlers =
|
||||
let notImplemented = fun _ -> async.Return (Error "not implemented")
|
||||
let notImplemented = fun _ -> failwith "Not implemented"
|
||||
|
||||
type private Permission = {
|
||||
uid: string
|
||||
@@ -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}"
|
||||
@@ -334,10 +352,12 @@ module Handlers =
|
||||
|> Array.ofSeq
|
||||
|
||||
let filter = {
|
||||
id = None
|
||||
archiveType = Some aType
|
||||
owner = Some user
|
||||
user = Some user
|
||||
groups = Some groups
|
||||
searchTerm = None
|
||||
}
|
||||
|
||||
async {
|
||||
@@ -411,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}"
|
||||
|
||||
@@ -434,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}"
|
||||
}
|
||||
|
||||
@@ -607,19 +642,32 @@ module Handlers =
|
||||
return models
|
||||
}
|
||||
|
||||
let getArchives (ctx: HttpContext) (mid: ModelAreaId, filter: ArchiveFilter) =
|
||||
let getArchives (ctx: HttpContext) (page: int) (rowCount: int) (filter: ArchiveFilter) =
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
|
||||
match db.getModelAreaArchives (mid, filter) with
|
||||
| Error err ->
|
||||
Log.Error $"getArchives: error: {err}"
|
||||
ctx.SetStatusCode 500
|
||||
return Error "Could not retrieve model area archives"
|
||||
match db.getArchives(page, rowCount, filter) with
|
||||
| Ok archives ->
|
||||
Log.Debug $"getArchives: %A{archives}"
|
||||
Log.Debug("[Archmaester] getArchives: length for filter {Filter}: {ArchiveCount}", filter, archives.Length)
|
||||
ctx.SetStatusCode 200
|
||||
return Ok archives
|
||||
| Error ex ->
|
||||
Log.Error(ex, "[Archmaester] getArchives error with filter {Filter}", filter)
|
||||
ctx.SetStatusCode 500
|
||||
return Error "Could not retrieve model area archives"
|
||||
}
|
||||
|
||||
let getArchiveCount (ctx: HttpContext) (filter: ArchiveFilter) =
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
match db.getArchiveCount filter with
|
||||
| Ok archiveCount ->
|
||||
Log.Debug("[Archmaester] getArchiveCount {Count} with filter {Filter}: {ArchiveCount}", archiveCount, filter)
|
||||
ctx.SetStatusCode 200
|
||||
return Ok archiveCount
|
||||
| Error ex ->
|
||||
Log.Error(ex, "[Archmaester] getArchiveCount error with filter {Filter}", filter)
|
||||
ctx.SetStatusCode 500
|
||||
return Error "Could not retrieve model area archives"
|
||||
}
|
||||
|
||||
let getArchiveOrModelPolygon (ctx: HttpContext) (aid: ArchiveId) =
|
||||
@@ -703,9 +751,8 @@ module Handlers =
|
||||
}
|
||||
|
||||
let getAcl (ctx: HttpContext) (aid: ArchiveId) =
|
||||
Log.Information $"Getting archive acl: {aid}"
|
||||
|
||||
async {
|
||||
Log.Information $"Getting archive acl: {aid}"
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
|
||||
match db.getArchiveAcl aid with
|
||||
@@ -719,6 +766,42 @@ module Handlers =
|
||||
return Error $"Could not retrieve acl: {err}"
|
||||
}
|
||||
|
||||
let getGroups (ctx: HttpContext) =
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
let res = db.getGroups ()
|
||||
match res with
|
||||
| Ok groups ->
|
||||
let dtos = groups |> Array.map _.Name
|
||||
return dtos
|
||||
| Error err ->
|
||||
return [||]
|
||||
}
|
||||
|
||||
let getGroupArchives (ctx: HttpContext) (group: string) =
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
let res = db.getGroupArchives group
|
||||
match res with
|
||||
| Ok archives ->
|
||||
let dtos = archives
|
||||
return dtos
|
||||
| Error err ->
|
||||
return [||]
|
||||
}
|
||||
|
||||
let getGroupUsers (ctx: HttpContext) (group: string) =
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
let res = db.getGroupUsers group
|
||||
match res with
|
||||
| Ok users ->
|
||||
let dtos = users |> Array.map _.Name
|
||||
return dtos
|
||||
| Error err ->
|
||||
return [||]
|
||||
}
|
||||
|
||||
let addToArchiveAcl (ctx: HttpContext) (aclType: AclType) (aid: ArchiveId, names: string[]) =
|
||||
Log.Information $"Adding acl to archive: {aid}"
|
||||
|
||||
@@ -748,13 +831,13 @@ module Handlers =
|
||||
return Error $"Could not add acl: {err}"
|
||||
}
|
||||
|
||||
let addToAcl (ctx: HttpContext) (aclType: AclType) (names: string[]) =
|
||||
Log.Information $"Adding acl {aclType}: %A{names}"
|
||||
let addToAcl (ctx: HttpContext) (aclType: AclType) (request: AddUsersRequest) =
|
||||
Log.Information $"Adding acl {aclType}: %A{request.users}"
|
||||
|
||||
async {
|
||||
let db = Archives.Archivist (Db.getDataSource ())
|
||||
|
||||
match db.tryAddToAcl (aclType, names) with
|
||||
match db.tryAddToAcl (aclType, request.users) with
|
||||
| Ok _ ->
|
||||
ctx.SetStatusCode 201
|
||||
return Ok ()
|
||||
@@ -928,14 +1011,14 @@ module Handlers =
|
||||
let user = ctx.User.Identity.Name
|
||||
|
||||
{
|
||||
getModelAreaArchives = getModelAreaArchives ctx
|
||||
addSubArchive = fun sub -> requireEditor user sub.reference (fun () -> createSubArchive ctx sub)
|
||||
getArchive = fun aid -> requireViewer user aid (fun () -> getArchiveProps ctx aid)
|
||||
getRefArchives = fun (aid, _ as args) -> requireViewer user aid (fun () -> getRefArchives ctx args)
|
||||
getArchivePolygon = fun aid -> requireViewer user aid (fun () -> getArchivePolygon aid)
|
||||
getModelAreaArchives = getModelAreaArchives ctx
|
||||
getRefArchives = fun (aid, _ as args) -> requireViewer user aid (fun () -> getRefArchives ctx args)
|
||||
resizeArchive = fun (aid, _, _ as args) -> requireEditor user aid (fun () -> resizeArchive args)
|
||||
retireArchive = fun aid -> requireEditor user aid (fun () -> deleteArchive ctx aid)
|
||||
updateArchive = fun (aid, _ as args) -> requireEditor user aid (fun () -> updateArchive ctx args)
|
||||
resizeArchive = fun (aid, _, _ as args) -> requireEditor user aid (fun () -> resizeArchive args)
|
||||
}
|
||||
|
||||
let modelAreaHandlers (ctx: HttpContext) : Api.ModelArea = {
|
||||
@@ -946,28 +1029,28 @@ module Handlers =
|
||||
}
|
||||
|
||||
let adminHandlers (ctx: HttpContext) : Api.Admin = {
|
||||
newArchive = addArchive ctx
|
||||
getArchiveDto = getArchiveDto
|
||||
augmentFiles = augmentFiles
|
||||
renameFiles = notImplemented
|
||||
getFiles = getFiles
|
||||
getAllFiles = getAllFiles
|
||||
queryModelAreaId = queryModelAreaId ctx
|
||||
addModelArea = addModelArea ctx
|
||||
updateModelArea = updateModelArea ctx
|
||||
setModelAreaPolygon = setModelAreaPolygon ctx
|
||||
setArchivePolygon = setArchivePolygon ctx
|
||||
updateArchiveAttribs = updateArchiveAttribs ctx
|
||||
deleteModelArea = Db.rmModelAreaFromDb
|
||||
removeRetiredAttribs = removeRetiredAttribs
|
||||
addUsers = addToAcl ctx AclType.User
|
||||
addGroups = addToAcl ctx AclType.Group
|
||||
removeUsers = removeFromAcl ctx AclType.User
|
||||
removeGroups = removeFromAcl ctx AclType.Group
|
||||
addType = addArchiveType
|
||||
removeType = removeArchiveType
|
||||
addAssociation = addAssociatedAttribs ctx
|
||||
addGroups = notImplemented
|
||||
addModelArea = addModelArea ctx
|
||||
addType = addArchiveType
|
||||
addUsers = addToAcl ctx AclType.User
|
||||
augmentFiles = augmentFiles
|
||||
deleteModelArea = Db.rmModelAreaFromDb
|
||||
getAllFiles = getAllFiles
|
||||
getArchiveDto = getArchiveDto
|
||||
getFiles = getFiles
|
||||
newArchive = addArchive ctx
|
||||
queryModelAreaId = queryModelAreaId ctx
|
||||
removeAssociation = removeAssociatedAttribs ctx
|
||||
removeGroups = removeFromAcl ctx AclType.Group
|
||||
removeRetiredAttribs = removeRetiredAttribs
|
||||
removeType = removeArchiveType
|
||||
removeUsers = removeFromAcl ctx AclType.User
|
||||
renameFiles = notImplemented
|
||||
setArchivePolygon = setArchivePolygon ctx
|
||||
setModelAreaPolygon = setModelAreaPolygon ctx
|
||||
updateArchiveAttribs = updateArchiveAttribs ctx
|
||||
updateModelArea = updateModelArea ctx
|
||||
}
|
||||
|
||||
module Endpoints =
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace Common
|
||||
|
||||
module Utils =
|
||||
open System
|
||||
open System.Net.Http
|
||||
open System
|
||||
|
||||
module Utils =
|
||||
open Azure.Identity
|
||||
open Azure.Security.KeyVault.Secrets
|
||||
open Serilog
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user