Compare commits

...

18 Commits

Author SHA1 Message Date
8db1592ca3 Start on archive management in Mui
Table views of model areas and archives
2025-08-20 18:05:11 +02:00
b3de3d88c4 Experiment with FluentUI trees and cards
And async loading of archives
2025-08-19 17:21:55 +02:00
27bc379196 Download and present model areas under FluentUI 2025-08-19 13:20:40 +02:00
2ff9dafc13 Move catalog into Atlantis vite project 2025-08-18 16:45:22 +02:00
6a24342322 Add fsautocomplete and fantomas to shell pkgs 2025-08-18 09:43:23 +02:00
d05015decf Add map to FluentUI 2025-08-18 09:42:48 +02:00
b66cc67802 Start on FluentUI 2025-08-17 13:37:05 +02:00
ac27a0544d Add map and some navigation to Carbon 2025-08-17 13:36:59 +02:00
3ba01c882b Add OL map to Carbon 2025-08-17 13:36:27 +02:00
649b0af9d4 Start on Carbon 2025-08-17 13:36:27 +02:00
918041e063 Deploy Catalog to atlantis public
Static site only in bundledebug to test deploying the component
playground
2025-08-17 13:36:27 +02:00
c3c6bb06df Add back .build/Helpers.fs :) 2025-08-17 13:36:27 +02:00
647302778f Add InstallClient to Atlantis Fake build step 2025-08-17 13:36:27 +02:00
e53de54c13 Add *.jsx to .gitignore 2025-08-17 13:36:26 +02:00
8e9d975cca Make *.html files 2 indent size 2025-08-17 13:36:09 +02:00
e8990fa1d4 Remove outmost build project 2025-08-17 13:36:09 +02:00
ff4f3aebab Continue on Mui example
Trying to create a nice sidebar that can be collapsed
2025-08-17 13:36:08 +02:00
4ddbe133b0 Add Mui jsx stub in Catalog
Creating a playground for trying out different component libraries.
Focusing on React ones this time around.
2025-08-17 13:35:34 +02:00
55 changed files with 2944 additions and 673 deletions

View File

@@ -1,38 +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 bun "install" "."
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

View File

@@ -124,4 +124,4 @@ let runOrDefault args =
0
with e ->
printfn "%A" e
1
1

View File

@@ -7,6 +7,9 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false
[*.html]
indent_size = 2
[*.js]
indent_size = 2
max_line_length= 80

3
.gitignore vendored
View File

@@ -29,4 +29,5 @@ NuGet.Config
sync.list
packages.lock.json
package-lock.json
*.nupkg
*.nupkg
*.jsx

View File

@@ -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" Version="6.1.3" />
<PackageReference Include="Fake.DotNet.Cli" Version="6.1.3" />
<PackageReference Include="Fake.IO.FileSystem" Version="6.1.3" />
<PackageReference Include="Farmer" Version="1.9.6" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
</ItemGroup>
</Project>

View File

@@ -15,7 +15,7 @@
<Project Path="src/Atlantis/src/Client/Lib/Lib.fsproj" />
<Project Path="src/Atlantis/src/Client/Mapster/Mapster.fsproj" />
<Project Path="src/Atlantis/src/Client/Notary/Notary.fsproj" />
<Project Path="src\Atlantis\src\Client\Catalog\Catalog.fsproj" />
<Project Path="src\Atlantis\src\Client\catalog\Catalog.fsproj" />
</Folder>
<Folder Name="/Atlantis/Server/">
<Project Path="src/Atlantis/src/Server/Archmaester/Archmaester.fsproj" />

769
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -15,17 +15,29 @@
"@sentry/vite-plugin": "^3.5.0",
"@vitejs/plugin-react": "^5.0.0",
"rollup-plugin-scss": "^4.0.1",
"sass": "^1.89.2",
"semantic-release": "^24.2.5",
"semantic-release-dotnet": "^1.0.0",
"vite": "npm:rolldown-vite@latest",
"vite-plugin-mkcert": "^1.17.8"
},
"dependencies": {
"@carbon/icons-react": "^11.65.0",
"@carbon/react": "^1.89.0",
"@carbon/type": "^11.45.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fluentui/react-charts": "^9.2.2",
"@fluentui/react-components": "^9.68.3",
"@fluentui/react-icons": "^2.0.307",
"@fluentui/web-components": "^3.0.0-beta.115",
"@fontsource/roboto": "^5.2.6",
"@fortawesome/fontawesome-free": "^6.7.2",
"@ibm/plex-sans": "^1.1.0",
"@lit-labs/motion": "^1.0.8",
"@lit/context": "^1.1.5",
"@microsoft/signalr": "^8.0.7",
"@mui/icons-material": "^7.3.1",
"@mui/material": "^7.3.1",
"@sentry/browser": "^9.30.0",
"@shoelace-style/shoelace": "^2.20.1",
"@spectrum-web-components/accordion": "^1.7.0",
@@ -70,6 +82,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-plotly.js": "^2.6.0",
"sass": "^1.90.0",
"vis-timeline": "^7.7.4"
}
}

View File

@@ -15,6 +15,9 @@ pkgs.mkShellNoCC {
buildInputs = [ dotnet-sdk ];
packages = [
pkgs.fsautocomplete
pkgs.fantomas
# JavaScript
pkgs.bun
pkgs.nodejs-slim

View File

@@ -5,12 +5,12 @@ open Farmer.Builders
open Helpers
initializeContext()
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 testPath = Path.getFullName "test"
let libPath = Path.getFullName "src/Interfaces" |> Some
let distPath = Path.getFullName "dist"
let packPath = Path.getFullName "packages"
@@ -25,66 +25,63 @@ let fableWatch = $"fable watch -e .jsx -o build --run {vite}"
Target.create "Clean" (fun _ -> Shell.cleanDir distPath)
Target.create "InstallClient" (fun _ ->
run bun "install" "."
run dotnet "tool restore" ".")
Target.create "Bundle" (fun _ ->
[ "server", dotnet $"build -tl -c Release -o {distPath} -p:DefineConstants=" serverPath
"client", dotnet (fable "-m production") clientPath ]
|> runParallel
)
runParallel [
"server", dotnet $"build -tl -c Release -o {distPath} -p:DefineConstants=" serverPath
"client", dotnet (fable "-m production") clientPath
])
Target.create "BundleDebug" (fun _ ->
[ "server", dotnet $"build -tl -c Debug -o {distPath} -p:DefineConstants=" serverPath
"client", dotnet (fable "-m development --minify false --sourcemap true") clientPath ]
|> runParallel
Trace.log "--- Building Server ---"
run dotnet $"build -tl -c Debug -o {distPath} -p:DefineConstants=" serverPath
Trace.log "--- Building Frontend ---"
run dotnet (fable "-m development --minify false --sourcemap true") clientPath
)
Target.create "Pack" (fun _ ->
match libPath with
| Some p -> run dotnet $"pack -c Release -o \"{packPath}\"" p
| None -> ()
)
| None -> ())
Target.create "Run" (fun _ ->
[ "server", dotnet "watch run" serverPath
"client", dotnet fableWatch clientPath ]
|> runParallel
)
runParallel [
"server", dotnet "watch run" serverPath
"client", dotnet fableWatch clientPath
])
Target.create "Client" (fun _ ->
run dotnet fableWatch clientPath
)
Target.create "Client" (fun _ -> run dotnet fableWatch clientPath)
Target.create "Format" (fun _ ->
run dotnet "fantomas . -r" "src"
)
Target.create "Format" (fun _ -> run dotnet "fantomas . -r" "src")
Target.create "Test" (fun _ ->
if System.IO.Directory.Exists testPath then
[ "server", dotnet "run" (testPath + "/Server")
"client", dotnet $"fable -e .jsx -o build --run {vite}" (testPath + "/Client") ]
|> runParallel
else ()
)
runParallel [
"server", dotnet "run" (testPath + "/Server")
"client", dotnet $"fable -e .jsx -o build --run {vite}" (testPath + "/Client")
]
else
())
open Fake.Core.TargetOperators
let dependencies = [
"Clean"
==> "Bundle"
"Clean" ==> "InstallClient" ==> "Bundle"
"Clean"
==> "BundleDebug"
"Clean" ==> "InstallClient" ==> "BundleDebug"
"Clean"
==> "Test"
"Clean" ==> "InstallClient" ==> "Test"
"Clean"
==> "Run"
"Clean" ==> "InstallClient" ==> "Run"
"Clean"
==> "Pack"
"Clean" ==> "InstallClient" ==> "Pack"
"Client"
]
[<EntryPoint>]
let main args = runOrDefault args
let main args = runOrDefault args

View File

@@ -1,8 +1,10 @@
module App
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Lit
open Remoting
let initSentry = Array.contains window.location.hostname Sentry.hostTargets
@@ -11,20 +13,27 @@ if initSentry then
console.debug "Pushing to Sentry"
Sentry.init ()
let testAuthenticated () =
promise {
let! authenticated = authApi.IsAuthenticated() |> Async.StartAsPromise
if authenticated.IsSome && not (isNullOrUndefined sessionStorage["archive_id"]) then
do! Utils.initAtlantisSessionUrls () |> Async.StartAsPromise
else
do sessionStorage.removeItem "archive_id"
do console.info("Redirecting to /atlas.html")
do window.location.href <- "/atlas.html"
return ()
}
[<LitElement("init-app")>]
let InitApp () =
let _ = LitElement.init (fun cfg -> cfg.useShadowDom <- false)
Hook.useEffectOnce (fun () ->
async {
let! authenticated = authApi.IsAuthenticated()
if authenticated.IsSome && not (isNullOrUndefined sessionStorage["archive_id"]) then
do! Utils.initAtlantisSessionUrls ()
window.location.href <- "/map.html"
else
sessionStorage.removeItem "archive_id"
window.location.href <- "/atlas.html"
return ()
} |> Async.StartImmediate
testAuthenticated ()
|> Promise.start
)
Lit.nothing
Lit.nothing

View File

@@ -9,11 +9,11 @@ open Fable.OpenLayers.Event
open Feliz.prop
open Lit
open Lit.Elmish
open Remoting
open Archmaester.Dto
open Atlantis.Types
open Maps
open Remoting
importSideEffects "../public/style.scss"
importSideEffects "@spectrum-web-components/action-button/sp-action-button.js"
@@ -181,6 +181,7 @@ let fetchSubModelAreas (mid: ModelAreaId) =
let fetchModelAreaArchives (mid: ModelAreaId) : JS.Promise<ModelAreaId * ArchiveProps[]> =
let archiveUrl = sessionStorage["archmaester_url"]
let api = ArchivesApi archiveUrl
promise {
let! archives =
api.Archive.getModelAreaArchives (mid, ArchiveType.Fvcom (FvcomVariant.Any, FvcomFormat.Any))
@@ -207,13 +208,14 @@ let private update (cmd: Msg) (model: Model) =
m, Elmish.Cmd.OfPromise.perform fetchSubModelAreas helloWorld.modelAreaId SetModelAreas
| SetModelAreas models ->
let m = { model with modelAreas = Map.ofArray models }
drawMapBBox m
do drawMapBBox m
m, Elmish.Cmd.none
| SetSelectedModelArea selected ->
let mid =
selected
|> Option.bind (fun areaId -> Map.tryFind areaId model.modelAreas)
let m = { model with selectedModelArea = mid }
if model.validUser && mid.IsSome then
m, Elmish.Cmd.OfPromise.perform fetchModelAreaArchives mid.Value.modelAreaId AddArchives
else
@@ -349,6 +351,7 @@ let private topNav (dispatch: Msg -> unit) (model: Model) =
[<HookComponent>]
let archiveSelectModal (modelAreaOpt: ModelArea option) (archives: ArchiveProps array) onClose =
let modelAreaName = modelAreaOpt |> Option.map (fun model -> $"{model.name} archives") |> Option.defaultValue "N/A"
let archiveRow (archive: ArchiveProps) =
let selectArchive (archive: ArchiveProps) =
sessionStorage["archive_id"] <- string archive.archiveId
@@ -357,43 +360,40 @@ let archiveSelectModal (modelAreaOpt: ModelArea option) (archives: ArchiveProps
let startTimeStr = archive.startTime.ToString "dd/MM/yyyy"
let daysLong = archive.endTime - archive.startTime
html
$"""
html $"""
<sp-table-row @click={Ev (fun _ -> selectArchive archive)}>
<sp-table-cell>{archive.name}</sp-table-cell>
<sp-table-cell>{startTimeStr}</sp-table-cell>
<sp-table-cell>{daysLong.Days}</sp-table-cell>
</sp-table-row>
"""
let archiveRows =
match modelAreaOpt with
| None -> html $"""No model area selected"""
| Some modelArea -> archives |> Lit.mapUnique (fun a -> a.archiveId.ToString ()) archiveRow
| Some modelArea ->
archives
|> Array.sortByDescending _.startTime
|> Lit.mapUnique (fun a -> a.archiveId.ToString ()) archiveRow
html
$"""
<sp-underlay ?open={modelAreaOpt.IsSome} @click={fun _ -> onClose ()}></sp-underlay>
<sp-dialog size="l" dismissable @close={Ev (fun _ -> onClose ())}>
<h2 slot="heading"> {modelAreaOpt
|> Option.map _.name
|> Option.defaultValue "N/A"}</h2>
<h4>Archives</h4>
<sp-table
size="s"
scroller="true"
>
<sp-table-head>
<sp-table-head-cell>Name</sp-table-head-cell>
<sp-table-head-cell>Start</sp-table-head-cell>
<sp-table-head-cell>Days</sp-table-head-cell>
</sp-table-head>
<sp-table-body>
{archiveRows}
</sp-table-body>
</sp-table>
</sp-dialog>
html $"""
<sp-underlay ?open={modelAreaOpt.IsSome} @click={fun _ -> onClose ()}></sp-underlay>
<sp-dialog size="l" dismissable @close={Ev (fun _ -> onClose ())}>
<h2 slot="heading">{modelAreaName}</h2>
<sp-table
size="s"
scroller="true"
style="width: 100%%;"
>
<sp-table-head>
<sp-table-head-cell>Name</sp-table-head-cell>
<sp-table-head-cell>Start</sp-table-head-cell>
<sp-table-head-cell>Days</sp-table-head-cell>
</sp-table-head>
<sp-table-body>
{archiveRows}
</sp-table-body>
</sp-table>
</sp-dialog>
"""
let selectStyle name =

View File

@@ -1,165 +0,0 @@
module App
open Archmaester
open Archmaester.Dto
open Browser
open Fable.Remoting.Client
open Lit
let archmaester =
Remoting.createApi ()
|> Remoting.withRouteBuilder Api.apiRouteBuilder
|> Remoting.buildProxy<Api.Inventory>
let fetchArchives modelAreaId callback =
async {
let! res = archmaester.getArchive modelAreaId
match res with
| Error err -> console.error $"Fetch archives error: {err}"
| Ok archives -> Some [| archives |] |> callback
}
|> Async.StartImmediate
let fetchModelAreas callback =
async {
let! res = archmaester.getModelAreaArchives (HelloWorld, ArchiveType.FromString "*:*:*")
match res with
| Error err -> console.error $"Fetch model areas error: {err}"
| Ok modelAreas -> Some modelAreas |> callback
}
[<LitElement("archive-listing")>]
let ArchiveListing () =
let _, _ = LitElement.init (fun init -> init.useShadowDom <- false)
let (modelAreas: ArchiveProps[] option), setModelAreas = Hook.useState None
Hook.useEffectOnce (fun _ -> fetchModelAreas setModelAreas |> Async.StartImmediate)
let archiveId (a: Archmaester.Dto.ArchiveProps) = a.archiveId.ToString ()
let archiveFiles files =
let fileEntry (fileName, _) = html $"""<li>{fileName}</li>"""
files |> Array.sortBy fst |> Lit.mapUnique fst fileEntry
let archiveOwners (acl: ArchiveAcl option) =
let owners =
match acl with
| Some x -> x.users
| None -> [||]
let userEntry name = html $"""<li>{name}</li>"""
let users' = owners |> Array.map userEntry |> Lit.ofArray
if Array.isEmpty owners then
Lit.nothing
else
html
$"""
<span>Owners:</span>
<div class="overflow-y-scroll">
<ul class="list-disc list-inside">
{users'}
</ul>
</div>
"""
let archiveUsers (acl: ArchiveAcl option) =
let users =
match acl with
| Some x -> x.users
| None -> [||]
let userEntry name = html $"""<li>{name}</li>"""
let users' = users |> Array.map userEntry |> Lit.ofArray
if Array.isEmpty users then
Lit.nothing
else
html
$"""
<span>Users:</span>
<div class="overflow-y-scroll">
<ul class="list-disc list-inside">
{users'}
</ul>
</div>
"""
let archiveGroups (acl: ArchiveAcl option) =
let groups =
match acl with
| Some x -> x.users
| None -> [||]
let groupEntry name = html $"""<li>{name}</li>"""
let groups' = groups |> Array.map groupEntry |> Lit.ofArray
if Array.isEmpty groups then
Lit.nothing
else
html
$"""
<span>Groups:</span>
<div class="overflow-y-scroll max-h-96">
<ul class="list-disc list-inside">
{groups'}
</ul>
</div>
"""
let archiveItem (item: ArchiveProps) =
html
$"""
<div class="card w-full shadow-xl">
<div class="card-body">
<h2 class="card-title">
{item.name}
<div class="badge badge-secondary">{item.archiveType}</div>
</h2>
<h3>Start date: {item.startTime.ToShortDateString ()} {item.startTime.ToShortTimeString ()}</h3>
<span>Projection: {item.projection}</span>
{{archiveOwners item.acl}}
{{archiveUsers item.acl}}
{{archiveGroups item.acl}}
<h4>Start date: {item.startTime}</h4>
<h4>End date: {item.endTime}</h4>
</div>
</div>
"""
let archiveName archiveName =
html
$"""
<p>{archiveName}</p>
"""
let modelAreaItem (item: ArchiveProps) =
// let archiveList =
// Array.map archiveName item.archiveType
html
$"""
<div class="card w-full shadow-xl">
<div class="card-body">
<h2 class="card-title">
{item.name}
</h2>
{item}
</div>
</div>
"""
let modelAreaList =
match modelAreas with
| None -> html $"""<progress class="progress w-56"></progress>"""
| Some [||] -> html $"""<h1>No ModelAreas</h1>"""
| Some models ->
html
$"""
{models |> Lit.mapUnique (fun m -> m.name) modelAreaItem}
"""
html
$"""
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{modelAreaList}
</div>
"""

View File

@@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.20.0</Version>
<RootNamespace>Archivist</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Content Include="input.css" />
<Content Include="index.html" />
<Compile Include="App.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Lit" Version="1.4.2" />
<PackageReference Include="Fable.Remoting.Client" Version="7.32.0" />
<PackageReference Update="FSharp.Core" Version="9.0.201" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\Interfaces\Archmaester\Archmaester.Api.fsproj" />
</ItemGroup>
</Project>

View File

@@ -1,42 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Fable</title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="fable.ico" />
<link href="./build/client/style.css" rel="stylesheet">
</head>
<body>
<!-- navbar -->
<div class="navbar bg-base-300">
<div class="lg:hidden">
<label for="drawer-toggle" class="btn btn-square btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-6 h-6 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</label>
</div>
<div class="px-2 mx-2">
Archmeister
</div>
</div>
<div class="drawer drawer-mobile">
<input id="drawer-toggle" type="checkbox" class="drawer-toggle">
<div class="drawer-content">
<!-- content -->
<div class="flex justify-center">
<archive-listing></archive-listing>
</div>
</div>
<div class="drawer-side">
<label for="drawer-toggle" class="drawer-overlay"></label>
<ul class="menu p-4 w-80 bg-base-100 text-base-content">
<li><a>Models</a></li>
<li><a>Archives</a></li>
<li><a>Groups</a></li>
<li><a>Users</a></li>
</ul>
</div>
</div>
<script type="module" src="./build/App.jsx"></script>
</body>
</html>

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env bash
dotnet fable watch -o build/client --run vite -c ../../vite.config.js

View File

@@ -21,7 +21,8 @@
<PackageReference Update="FSharp.Core" Version="9.0.201"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="catalog\Catalog.fsproj"/>
<ProjectReference Include="Atlas\Atlas.fsproj"/>
<ProjectReference Include="Mapster\Mapster.fsproj"/>
</ItemGroup>
</Project>
</Project>

View File

@@ -5,6 +5,8 @@ open Fable.Remoting.Client
open Atlantis
open Sorcerer
let getArchiveUrl () = Browser.WebStorage.sessionStorage["archmaester_url"]
let authApi =
Remoting.createApi ()
|> Remoting.withCredentials true
@@ -124,8 +126,6 @@ let driftersJobApi () =
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|> Remoting.buildProxy<Api.Drifters>
let archiveUrl = Browser.WebStorage.sessionStorage["archmaester_url"]
let archiveApi () =
Remoting.createApi ()
|> Remoting.withCredentials true
@@ -167,4 +167,4 @@ let plumeApi () =
Remoting.createApi ()
|> Remoting.withCredentials true
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|> Remoting.buildProxy<Api.Plume>
|> Remoting.buildProxy<Api.Plume>

View File

@@ -52,9 +52,12 @@ let fromBase64String (s: string) : string = jsNative
[<Emit("btoa($0)")>]
let toBase64String (s: string) : string = jsNative
let strNull = String.IsNullOrWhiteSpace
let strNotNull = String.IsNullOrWhiteSpace >> not
/// Helper function for testing whether a string is null or only whitespace
let tryStr str =
if String.IsNullOrWhiteSpace str then
if strNull str then
None
else
Some str
@@ -237,4 +240,4 @@ let initAtlantisSessionUrls () =
let! sUrl = Remoting.servicesApi.GetFileService()
console.log $"Data service: {sUrl}"
sessionStorage["sorcerer_url"] <- sUrl
}
}

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.20.0</Version>
<RootNamespace>Archivist</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="Utils.fs" />
<Compile Include="Map.fs" />
<Compile Include="carbon/components/Header.fs" />
<Compile Include="carbon/Oceanography.fs" />
<Compile Include="carbon/Home.fs" />
<Compile Include="carbon/Carbon.fs" />
<Compile Include="carbon/Index.fs" />
<Compile Include="mui/Archives.fs" />
<Compile Include="mui/Mui.fs" />
<Compile Include="mui/Index.fs" />
<Compile Include="fluentui/ModelAreaTree.fs" />
<Compile Include="fluentui/Charts.fs" />
<Compile Include="fluentui/Archives.fs" />
<Compile Include="fluentui/Oceanography.fs" />
<Compile Include="fluentui/FluentUI.fs" />
<Compile Include="fluentui/Index.fs" />
<Compile Include="Index.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Browser.Url" Version="1.4.0" />
<PackageReference Include="Fable.Core" Version="4.5.0" />
<PackageReference Include="Fable.OpenLayers" Version="2.18.0" />
<PackageReference Include="Fable.Remoting.Client" Version="7.32.0" />
<PackageReference Update="FSharp.Core" Version="9.0.201" />
<PackageReference Include="Feliz" Version="2.9.0" />
<PackageReference Include="Feliz.Router" Version="4.0.0" />
<PackageReference Include="Feliz.UseMediaQuery" Version="1.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lib\Lib.fsproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,24 @@
module Oceanbox.Catalog.Index
open Browser
open Fable.Core
open Remoting
let private testAuthenticated () =
promise {
let! authOpt = authApi.IsAuthenticated () |> Async.StartAsPromise
do! Utils.initAtlantisSessionUrls () |> Async.StartAsPromise
match authOpt with
| Some auth ->
console.info("[Catalog] Auth info: %o", auth)
| None ->
console.error("[Catalog] No auth info!")
do window.location.href <- "/signin"
}
console.info("[Catalog] Welcome to the Catalog! But first, we have to test if you're authenticated!")
testAuthenticated () |> Promise.start

View File

@@ -0,0 +1,27 @@
module Oceanbox.Catalog.Map
open Browser
open Fable.OpenLayers
let create () : unit =
let map =
let osmLayer : Layer =
Layer.tileLayer [
layer.source (Source.osm [])
]
let view =
View.view [
view.center [| 0; 0 |]
view.zoom 2
]
OlMap.map [
map.layers [|
osmLayer
|]
map.view view
]
do map.setTarget "map"
do console.debug("Create map %o", map)
()

View File

@@ -0,0 +1,17 @@
module Oceanbox.Catalog.Utils
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
let inline toReact (elem: JSX.Element) : ReactElement = unbox elem
let inline toStyle (styles: IStyleAttribute seq) : obj = createObj (unbox styles)
[<Emit("Object.entries($0)")>]
let inline spreadStyles object : IStyleAttribute array = jsNative
let toLocaleString (date: System.DateTime) : string =
let jsDate = unbox<JS.Date> date
jsDate?toLocaleString("en-GB")

View File

@@ -0,0 +1,43 @@
module Oceanbox.Catalog.Carbon.App
open System
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Fable.OpenLayers
open Feliz
let private Content: obj = import "Content" "@carbon/react"
let private BuoyIcon: obj = import "Buoy" "@carbon/icons-react"
type Page =
| Home
| Oceanography
[<JSX.Component>]
let View () =
let isOpen, setOpen = React.useState false
let currentPage, setPage = React.useState Home
let page =
match currentPage with
| Home -> Home.View ()
| Oceanography -> Oceanography.View ()
let handleNavigate (str: string) ev =
console.info("Navigate from header: %s, %o", str, ev)
match str with
| "home" -> setPage Home
| "oceanography" -> setPage Oceanography
| _ -> console.error("Invalid page from navigation callback: %s", str)
JSX.html $"""
<>
{Header.MainHeader handleNavigate}
<Content>
{page}
</Content>
</>
"""

View File

@@ -0,0 +1,77 @@
module Oceanbox.Catalog.Carbon.Home
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Fable.OpenLayers
open Feliz
open Oceanbox
let private Button: obj = import "Button" "@carbon/react"
let private Column: obj = import "Column" "@carbon/react"
let private Grid: obj = import "Grid" "@carbon/react"
let private AddIcon: obj = import "Add" "@carbon/react/icons"
[<JSX.Component>]
let View () =
React.useEffectOnce (fun () ->
console.debug("Mounting Home")
do Catalog.Map.create ()
()
)
JSX.html $"""
<Grid className="home-grid" fullWidth>
<Column
sm={4}
md={8}
lg={ {| span = 16; offset = 4; |} }
xlg={ {| span = 16; offset = 3; |} }
max={ {| span = 16; offset = 2; |} }
>
<h2>Carbon</h2>
</Column>
<Column
sm={4}
md={8}
lg={ {| span = 16; offset = 4; |} }
xlg={ {| span = 16; offset = 3; |} }
max={ {| span = 16; offset = 2; |} }
>
<Grid>
<Column sm={4} md={8} lg={16} max={4}>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non
enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus
imperdiet. Semper risus in hendrerit gravida rutrum quisque non tellus.
Convallis convallis tellus id interdum velit laoreet id donec ultrices.
Odio morbi quis commodo odio aenean sed adipiscing. Amet nisl suscipit
adipiscing bibendum est ultricies integer quis. Cursus euismod quis viverra
nibh cras. Metus vulputate eu scelerisque felis imperdiet proin fermentum
leo. Mauris commodo quis imperdiet massa tincidunt. Cras tincidunt lobortis
feugiat vivamus at augue. At augue eget arcu dictum varius duis at
consectetur lorem. Velit sed ullamcorper morbi tincidunt. Lorem donec massa
sapien faucibus et molestie ac.
</p>
</Column>
<Column sm={4} md={8} lg={16} max={12}>
<div id="map"></div>
</Column>
</Grid>
</Column>
<Column
sm={4}
lg={ {| span = 6; offset = 4; |} }
xlg={ {| span = 16; offset = 3; |} }
max={ {| span = 16; offset = 2; |} }
>
<Button size="sm" kind="secondary">Cancel</Button>
<Button size="sm" renderIcon={AddIcon}>Hello, Carbon!</Button>
</Column>
</Grid>
"""

View File

@@ -0,0 +1,24 @@
module Oceanbox.Catalog.Carbon.Index
open System
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Oceanbox.Catalog
let React : obj = importAll "react"
let root = ReactDOM.createRoot (document.getElementById "root")
root.render (
JSX.html
$"""
<React.StrictMode>
{App.View () |> Utils.toReact}
</React.StrictMode>
"""
|> Utils.toReact
)

View File

@@ -0,0 +1,35 @@
module Oceanbox.Catalog.Carbon.Oceanography
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Fable.OpenLayers
open Feliz
open Oceanbox
let private Column: obj = import "Column" "@carbon/react"
let private Grid: obj = import "Grid" "@carbon/react"
[<JSX.Component>]
let View () =
React.useEffectOnce (fun () ->
console.debug("Mounting Oceanography")
do Catalog.Map.create ()
()
)
JSX.html $"""
<Grid className="oceanography-grid" narrow>
<Column
sm={4}
md={8}
lg={ {| span = 16; offset = 4; |} }
xlg={ {| span = 16; offset = 3; |} }
max={ {| span = 16; offset = 2; |} }
>
<div id="map"></div>
</Column>
</Grid>
"""

View File

@@ -0,0 +1,118 @@
module Oceanbox.Catalog.Carbon.Header
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
let private CarbonHeader: obj = import "Header" "@carbon/react"
let private HeaderContainer: obj = import "HeaderContainer" "@carbon/react"
let private HeaderGlobalAction: obj = import "HeaderGlobalAction" "@carbon/react"
let private HeaderGlobalBar: obj = import "HeaderGlobalBar" "@carbon/react"
let private HeaderMenuButton: obj = import "HeaderMenuButton" "@carbon/react"
let private HeaderMenuItem: obj = import "HeaderMenuItem" "@carbon/react"
let private HeaderName: obj = import "HeaderName" "@carbon/react"
let private HeaderNavigation: obj = import "HeaderNavigation" "@carbon/react"
let private HeaderSideNavItems: obj = import "HeaderSideNavItems" "@carbon/react"
let private SideNav: obj = import "SideNav" "@carbon/react"
let private SideNavItems: obj = import "SideNavItems" "@carbon/react"
let private SideNavLink: obj = import "SideNavLink" "@carbon/react"
let private SideNavDivider: obj = import "SideNavDivider" "@carbon/react"
let private SideNavMenu: obj = import "SideNavMenu" "@carbon/react"
let private SideNavMenuItem: obj = import "SideNavMenuItem" "@carbon/react"
let private SkipToContent: obj = import "SkipToContent" "@carbon/react"
let private BuoyIcon: obj = import "Buoy" "@carbon/icons-react"
let private HomeIcon: obj = import "Home" "@carbon/icons-react"
let private IbmCloudHpcIcon: obj = import "IbmCloudHpc" "@carbon/icons-react"
let private NotificationIcon: obj = import "Notification" "@carbon/icons-react"
let private SwitcherIcon: obj = import "Switcher" "@carbon/icons-react"
let private urlHashtag (str: string) : string =
let url = URL.Create(str)
console.debug("URI: %s, hash: %s", url, url.hash)
url.hash
[<JSX.Component>]
let MainHeader (navigate: string -> obj -> unit) =
let currentPage = urlHashtag document.URL
let inner props =
let isSideNavExpanded = props?isSideNavExpanded
let onClickSideNavExpand = props?onClickSideNavExpand
JSX.html $"""
<Header aria-label="Main header">
<SkipToContent />
<HeaderMenuButton
aria-label="Open menu"
isActive={isSideNavExpanded}
onClick={onClickSideNavExpand}
/>
<HeaderName href="/catalog" prefix="IO">
Oceanbox
</HeaderName>
<HeaderNavigation aria-label="Navigation">
<HeaderMenuItem href="/">Atlantis</HeaderMenuItem>
<HeaderMenuItem href="/catalog">Home</HeaderMenuItem>
<HeaderMenuItem href="../mui/index.html">Mui</HeaderMenuItem>
<HeaderMenuItem href="index.html">Carbon</HeaderMenuItem>
<HeaderMenuItem href="../fluentui/index.html">FluentUI</HeaderMenuItem>
</HeaderNavigation>
<HeaderGlobalBar>
<HeaderGlobalAction
aria-label="Notifications"
>
<NotificationIcon size={20} />
</HeaderGlobalAction>
<HeaderGlobalAction
aria-label="App switcher"
>
<SwitcherIcon size={20} />
</HeaderGlobalAction>
</HeaderGlobalBar>
<SideNav
aria-label="Side navigation"
isPersistent={true}
expanded={isSideNavExpanded}
>
<SideNavItems>
<HeaderSideNavItems>
<HeaderMenuItem href="/">Atlantis</HeaderMenuItem>
<HeaderMenuItem href="/catalog">Home</HeaderMenuItem>
<HeaderMenuItem href="../mui/index.html">Mui</HeaderMenuItem>
<HeaderMenuItem href="index.html">Carbon</HeaderMenuItem>
<HeaderMenuItem href="../fluentui/index.html">FluentUI</HeaderMenuItem>
</HeaderSideNavItems>
<SideNavLink
href="#"
renderIcon={HomeIcon}
isActive={currentPage = ""}
onClick={navigate "home"}
>
Home
</SideNavLink>
<SideNavLink
href="#oceanography"
renderIcon={BuoyIcon}
isActive={currentPage = "#oceanography"}
onClick={navigate "oceanography"}
>
Oceanography
</SideNavLink>
<SideNavLink href="#compute" renderIcon={IbmCloudHpcIcon}>
Compute
</SideNavLink>
<SideNavDivider />
</SideNavItems>
</SideNav>
</Header>
"""
JSX.html $"""
<HeaderContainer render={inner}>
</HeaderContainer>
"""

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Fable - Carbon</title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="../react.svg" />
<link href="../index.css" rel="stylesheet">
<link href="style.scss" rel="stylesheet">
</head>
<body>
<header>
<nav class="navigation">
<a href="/">Atlantis</a>
<a href="/catalog">Home</a>
<a href="../mui/index.html">Mui</a>
<a href="index.html">Carbon</a>
<a href="../fluentui/index.html">FluentUI</a>
</nav>
</header>
<div id="root"></div>
<script type="module" src="../../build/catalog/carbon/Index.jsx"></script>
</body>
</html>

View File

@@ -0,0 +1,35 @@
/* TODO(simkir): Import only what I use */
@use "@carbon/react" with (
$font-path: "@ibm/plex"
);
@use "ol/ol";
html,
body,
#root,
main {
height: 100%;
}
p {
text-align: justify;
}
#map {
width: 100%;
min-height: 320px;
height: 100%;
}
.navigation {
display: none;
}
.home-grid {
row-gap: 16px;
}
.oceanography-grid {
height: 100%;
}

View File

@@ -0,0 +1,376 @@
module Oceanbox.Catalog.FluentUI.Archives
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Oceanbox.Catalog
open Remoting
let private createTableColumn: obj -> unit = import "createTableColumn" "@fluentui/react-components"
let private makeStyles: obj -> obj = import "makeStyles" "@fluentui/react-components"
let private Button: obj = import "Button" "@fluentui/react-components"
let private Card: obj = import "Card" "@fluentui/react-components"
let private CardHeader: obj = import "CardHeader" "@fluentui/react-components"
let private Caption1: obj = import "Caption1" "@fluentui/react-components"
let private DataGrid: obj = import "DataGrid" "@fluentui/react-components"
let private DataGridBody: obj = import "DataGridBody" "@fluentui/react-components"
let private DataGridCell: obj = import "DataGridCell" "@fluentui/react-components"
let private DataGridHeader: obj = import "DataGridHeader" "@fluentui/react-components"
let private DataGridHeaderCell: obj = import "DataGridHeaderCell" "@fluentui/react-components"
let private DataGridRow: obj = import "DataGridRow" "@fluentui/react-components"
let private Dialog: obj = import "Dialog" "@fluentui/react-components"
let private DialogActions: obj = import "DialogActions" "@fluentui/react-components"
let private DialogBody: obj = import "DialogBody" "@fluentui/react-components"
let private DialogContent: obj = import "DialogContent" "@fluentui/react-components"
let private DialogSurface: obj = import "DialogSurface" "@fluentui/react-components"
let private DialogTitle: obj = import "DialogTitle" "@fluentui/react-components"
let private DialogTrigger: obj = import "DialogTrigger" "@fluentui/react-components"
let private TableCellLayout: obj = import "TableCellLayout" "@fluentui/react-components"
let private FuiText: obj = import "Text" "@fluentui/react-components"
let private ToggleButton: obj = import "ToggleButton" "@fluentui/react-components"
let private BookmarkRegularIcon : obj = import "Bookmark20Regular" "@fluentui/react-icons"
let private BranchRegularIcon: obj = import "BranchRegular" "@fluentui/react-icons"
let private GridKanbanRegularIcon: obj = import "GridKanbanRegular" "@fluentui/react-icons"
let private TableRegularIcon: obj = import "TableRegular" "@fluentui/react-icons"
type private ArchiveView =
| Table
| Tree
| Grid
override this.ToString () =
match this with
| Table -> "table"
| Tree -> "tree"
| Grid -> "grid"
let private fetchHelloWorldArchive (api: ArchivesApi) =
promise {
let! helloWorld = api.ModelArea.getModelArea Archmaester.Dto.HelloWorld |> Async.StartAsPromise
return helloWorld
}
let private fetchModelAreas () =
promise {
let api = getArchiveUrl () |> ArchivesApi
do! Utils.initAtlantisSessionUrls () |> Async.StartAsPromise
let! helloWorldOpt = fetchHelloWorldArchive api
match helloWorldOpt with
| Some helloWorld ->
console.info("[Catalog] Fetched hello world: %o", helloWorld)
let! subArchives = api.ModelArea.getSubModelAreas helloWorld.modelAreaId |> Async.StartAsPromise
return subArchives
| None ->
console.error "[Catalog] No hello world! No archives!"
return [||]
}
let private useStyles: obj = makeStyles {|
card = {|
minWidth = "256px"
|}
cardContainer = {|
flexGrow = 2
|}
|}
let private columns: obj array = [|
createTableColumn {|
columnId = "Name"
renderHeaderCell = fun () -> "Name"
renderCell = fun (item: Archmaester.Dto.ModelArea) -> item.name
|}
createTableColumn {|
columnId = "Archives"
renderHeaderCell = fun () -> "Archives"
renderCell = fun (item: Archmaester.Dto.ModelArea) -> item.archives
|}
createTableColumn {|
columnId = "Models"
renderHeaderCell = fun () -> "Models"
renderCell = fun (item: Archmaester.Dto.ModelArea) -> item.models
|}
createTableColumn {|
columnId = "FocalPoint"
renderHeaderCell = fun () -> "Focal point (x, y)"
renderCell = fun (item: Archmaester.Dto.ModelArea) ->
let x, y = item.focalPoint
sprintf "%0.2f, %0.2f" x y
|}
createTableColumn {|
columnId = "Desc"
renderHeaderCell = fun () -> "Description"
renderCell = fun (item: Archmaester.Dto.ModelArea) -> item.description
|}
|]
[<JSX.Component>]
let private ModelAreaTable (archives: Archmaester.Dto.ModelArea array) =
let renderCell (item: obj) (props: obj) =
JSX.html $"""
<DataGridCell>{props?renderCell item}</DataGridCell>
"""
let renderHeader props =
let renderHeaderCell: unit -> obj = props?renderHeaderCell
JSX.html $"""
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
"""
let renderRow row =
let item = row?item
let rowId: int = row?rowId
JSX.html $"""
<DataGridRow
key={rowId}
>
{renderCell item}
</DataGridRow>
"""
JSX.html $"""
<DataGrid
items={archives}
columns={columns}
getRowId={fun (modelArea: Archmaester.Dto.ModelArea) -> modelArea.modelAreaId}
style={{{{ flexGrow: 1, minWidth: "550px" }}}}
>
<DataGridHeader>
<DataGridRow>
{renderHeader}
</DataGridRow>
</DataGridHeader>
<DataGridBody>
{renderRow}
</DataGridBody>
</DataGrid>
"""
[<JSX.Component>]
let private DialogTest () =
JSX.html """
<Dialog>
<DialogTrigger disableButtonEnchancement>
<Button appearance="primary">Dialog</Button>
</DialogTrigger>
<DialogSurface>
<DialogBody>
<DialogTitle>Dialog Title</DialogTitle>
<DialogContent>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus imperdiet. Semper risus in hendrerit gravida rutrum quisque non tellus. Convallis convallis tellus id interdum velit laoreet id donec ultrices. Odio morbi quis commodo odio aenean sed adipiscing. Amet nisl suscipit adipiscing bibendum est ultricies integer quis. Cursus euismod quis viverra nibh cras. Metus vulputate eu scelerisque felis imperdiet proin fermentum leo. Mauris commodo quis imperdiet massa tincidunt. Cras tincidunt lobortis feugiat vivamus at augue. At augue eget arcu dictum varius duis at consectetur lorem. Velit sed ullamcorper morbi tincidunt. Lorem donec massa sapien faucibus et molestie ac.
</DialogContent>
<DialogActions>
<Button appearance="primary">Do something</Button>
<DialogTrigger disableButtonEnchancement>
<Button appearance="secondary">Close</Button>
</DialogTrigger>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
"""
[<ReactComponent>]
let View () =
let styles: obj = emitJsExpr () "useStyles()"
let archiveView, setArchiveView = React.useState<ArchiveView> Table
let selectedArchive, setSelectedArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
let modelAreas, setModelAreas = React.useState<Archmaester.Dto.ModelArea array> [||]
let handleArchiveClick (archive: Archmaester.Dto.ArchiveProps) =
setSelectedArchive (Some archive)
React.useEffectOnce (fun () ->
console.debug("Current archive view", archiveView)
if Utils.strNull (getArchiveUrl ()) then
do console.error("[Catalog] Archmaester url is empty!")
else
fetchModelAreas ()
|> Promise.iter setModelAreas
)
Html.div [
prop.style [
style.padding (length.px 16)
]
prop.children [
Html.h1 "Archives"
Html.div [
prop.style [
style.display.flex
style.gap (length.px 16)
style.paddingBottom (length.px 16)
]
prop.children [
DialogTest () |> Utils.toReact
JSX.html $"""
<div>
<ToggleButton
icon={{<TableRegularIcon/>}}
checked={archiveView = Table}
onClick={fun () -> setArchiveView Table}
>
</ToggleButton>
<ToggleButton
icon={{<BranchRegularIcon/>}}
checked={archiveView = Tree}
onClick={fun () -> setArchiveView Tree}
>
</ToggleButton>
<ToggleButton
icon={{<GridKanbanRegularIcon />}}
checked={archiveView = Grid}
onClick={fun () -> setArchiveView Grid}
>
</ToggleButton>
</div>
"""
|> Utils.toReact
]
]
Html.div [
prop.style [
style.display.flex
style.flexDirection.row
style.alignItems.flexStart
style.justifyContent.flexStart
style.flexWrap.wrap
style.gap (length.px 16)
]
prop.children [
match archiveView with
| Table -> ModelAreaTable modelAreas |> Utils.toReact
| Tree ->
Html.div [
prop.style [
style.flexGrow 1
style.maxWidth (length.px 512)
]
prop.children (
ModelAreaTree.View handleArchiveClick modelAreas |> Utils.toReact
)
]
match selectedArchive with
| Some archive ->
let duration = archive.endTime - archive.startTime
JSX.html $"""
<div className={styles?cardContainer}>
<Card key={archive.archiveId} className={styles?card} selected={false}>
<CardHeader
image={{<BookmarkRegularIcon />}}
header={{<FuiText weight="semibold">{archive.name}</FuiText>}}
/>
<div>
<div>
<span>Type: {string archive.archiveType}</span>
</div>
<div>
<span>Projection: {archive.projection}</span>
</div>
<div>
<span>Default zoom: {archive.defaultZoom}</span>
</div>
<div>
<span>Frequency: {archive.freq}</span>
</div>
<div>
<span>Frames: {archive.frames}</span>
</div>
<div>
<span>Created: {Utils.toLocaleString archive.created}</span>
</div>
<div>
<span>Start time: {Utils.toLocaleString archive.startTime}</span>
</div>
<div>
<span>End time: {Utils.toLocaleString archive.endTime}</span>
</div>
<div>
<span>Duration: {duration.Days} days</span>
</div>
<div>
<span>Owner: {archive.owner}</span>
</div>
<div>
<span>Expires: {archive.expires}</span>
</div>
<div>
<span>Publised: {if archive.isPublished then "True" else "False"}</span>
</div>
<div>
<span>Public: {if archive.isPublic then "True" else "False"}</span>
</div>
</div>
</Card>
</div>
"""
|> Utils.toReact
| None -> Html.none
| Grid ->
Html.div [
prop.style [
style.display.flex
style.flexWrap.wrap
style.gap (length.px 16)
style.maxWidth (length.px 1024)
]
prop.children (
modelAreas
|> Array.sortBy _.name
|> Array.map (fun area ->
JSX.html $"""
<Card key={area.modelAreaId} className={styles?card} selected={false}>
<CardHeader
image={{<BookmarkRegularIcon />}}
header={{<FuiText weight="semibold">{area.name}</FuiText>}}
description={{<Caption1>{area.description}</Caption1>}}
/>
</Card>
"""
|> Utils.toReact
)
)
]
Html.div [
prop.style [
style.flexGrow 2
style.flexShrink 1
]
prop.children (
Charts.View modelAreas |> Utils.toReact
)
]
]
]
]
]

View File

@@ -0,0 +1,50 @@
module Oceanbox.Catalog.FluentUI.Charts
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Oceanbox.Catalog
open Remoting
let private VerticalBarChart: obj = import "VerticalBarChart" "@fluentui/react-charts"
type private DataPoint = {
x: string
y: int
legend: string
color: string
xAxisCalloutData: string
yAxisCalloutData: string
}
[<JSX.Component>]
let View (modelAreas: Archmaester.Dto.ModelArea array) =
let width, setWidth = React.useState 400
let points =
modelAreas
|> Array.map (fun modelArea -> {
x = modelArea.name
y = modelArea.archives
legend = "Model area archives"
color = "dodgerblue"
xAxisCalloutData = ""
yAxisCalloutData = ""
})
let rootStyle =
Utils.toStyle [
style.width (length.px width)
]
JSX.html $"""
<div style={rootStyle}>
<VerticalBarChart
chartTitle="Vertical bar chart basic example "
data={points}
width={width}
/>
</div>
"""

View File

@@ -0,0 +1,137 @@
module Oceanbox.Catalog.FluentUI.App
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Feliz.UseMediaQuery
open Feliz.Router
open Oceanbox.Catalog
let private webLightTheme: obj = import "webLightTheme" "@fluentui/react-components"
let private makeStyles: obj -> obj = import "makeStyles" "@fluentui/react-components"
let private bundleIcon: obj -> obj = import "bundleIcon" "@fluentui/react-icons"
let private AppItem: obj = import "AppItem" "@fluentui/react-components"
let private Button: obj = import "Button" "@fluentui/react-components"
let private FluentProvider: obj = import "FluentProvider" "@fluentui/react-components"
let private Hamburger: obj = import "Hamburger" "@fluentui/react-components"
let private NavDrawer: obj = import "NavDrawer" "@fluentui/react-components"
let private NavDrawerBody: obj = import "NavDrawerBody" "@fluentui/react-components"
let private NavDrawerHeader: obj = import "NavDrawerHeader" "@fluentui/react-components"
let private NavDivider: obj = import "NavDivider" "@fluentui/react-components"
let private NavItem: obj = import "NavItem" "@fluentui/react-components"
let private Tooltip: obj = import "Tooltip" "@fluentui/react-components"
let private WaterRegularIcon: obj = import "Water20Regular" "@fluentui/react-icons"
let private ArchiveRegularIcon: obj = import "Archive20Regular" "@fluentui/react-icons"
let private ChartMultipleRegularIcon: obj = import "ChartMultiple20Regular" "@fluentui/react-icons"
let private useStyles: obj = makeStyles {|
root = {|
height = "100%"
flexGrow = "1"
|}
appIcon = {|
width = "32px"
height = "32px"
|}
navDrawer = {|
borderRight = "2px solid gainsboro"
|}
portalContainer = {|
position = "fixed"
top = "0.5em"
left = "2.5em"
|}
|}
[<JSX.Component>]
let View () =
let styles: obj = emitJsExpr () "useStyles()"
let responsive = React.useResponsive ()
let currentUrl, updateUrl = React.useState(Router.currentUrl())
// TODO(simkir): Close nav when smaller screen
let isOpen, setOpen = React.useState true
let screenLarge = responsive = ScreenSize.WideScreen
let router =
React.router [
router.onUrlChanged updateUrl
router.children [
match currentUrl with
| [ "oceanography" ] -> Oceanography.View isOpen (fun () -> setOpen true) |> Utils.toReact
| [ "archives" ] -> Archives.View()
| otherwise -> Html.h1 "404 Not found"
]
]
let pageId =
match currentUrl with
| [ "oceanography" ] -> "1"
| [ "archives" ] -> "2"
| [ "charts" ] -> "3"
| otherwise -> "1"
JSX.html $"""
<FluentProvider id="fluent-provider" className={styles?provider} theme={webLightTheme}>
<NavDrawer
className={styles?navDrawer}
open={screenLarge && isOpen}
type="inline"
position="start"
selectedValue={pageId}
>
<NavDrawerHeader>
<Tooltip relationship="label" content="Close navigation">
<Hamburger onClick={fun () -> setOpen (not isOpen)} />
</Tooltip>
</NavDrawerHeader>
<NavDrawerBody>
<AppItem as="a" href="/">
<img className={styles?appIcon} src="../ob.svg"></img>
Oceanbox
</AppItem>
<AppItem as="a" href="/catalog">
Home
</AppItem>
<AppItem as="a" href="../mui/index.html">
Mui
</AppItem>
<AppItem as="a" href="../carbon/index.html">
Carbon
</AppItem>
<AppItem as="a" href="index.html">
FluentUI
</AppItem>
<NavDivider />
<NavItem
value="1"
icon={{<WaterRegularIcon />}}
onClick={fun () -> Router.navigate "oceanography"}
>
Oceanography
</NavItem>
<NavItem
value="2"
icon={{<ArchiveRegularIcon />}}
onClick={fun () -> Router.navigate "archives"}
>
Archives
</NavItem>
</NavDrawerBody>
</NavDrawer>
<main className={styles?root}>
{router}
</main>
</FluentProvider>
"""

View File

@@ -0,0 +1,21 @@
module Oceanbox.Catalog.FluentUI.Index
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Oceanbox.Catalog
let React : obj = importAll "react"
let root = ReactDOM.createRoot (document.getElementById "root")
root.render (
JSX.html
$"""
<React.StrictMode>
{App.View () |> Utils.toReact}
</React.StrictMode>
"""
|> Utils.toReact
)

View File

@@ -0,0 +1,155 @@
module Oceanbox.Catalog.FluentUI.ModelAreaTree
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Remoting
let private makeStyles: obj -> obj = import "makeStyles" "@fluentui/react-components"
let private Tree: obj = import "Tree" "@fluentui/react-components"
let private TreeItem: obj = import "TreeItem" "@fluentui/react-components"
let private TreeItemLayout: obj = import "TreeItemLayout" "@fluentui/react-components"
let private Tooltip: obj = import "Tooltip" "@fluentui/react-components"
let private Spinner: obj = import "Spinner" "@fluentui/react-components"
let private CheckmarkStarburstRegularIcon: obj = import "CheckmarkStarburstRegular" "@fluentui/react-icons"
let private GlobeRegularIcon: obj = import "GlobeRegular" "@fluentui/react-icons"
let private LockClosedRegularIcon: obj = import "LockClosedRegular" "@fluentui/react-icons"
let private useStyles: obj = makeStyles {|
tree = {|
overflowY = "scroll"
maxHeight = "1000px"
|}
|}
let private fetchArchives (modelAreaId: Archmaester.Dto.ModelAreaId) =
promise {
let api = getArchiveUrl () |> ArchivesApi
let archiveType = Archmaester.Dto.ArchiveType.Any
let! res = api.Archive.getModelAreaArchives(modelAreaId, archiveType) |> Async.StartAsPromise
return res
}
[<JSX.Component>]
let private ModelAreaLeaf key (area: Archmaester.Dto.ModelArea) (onArchiveClick: Archmaester.Dto.ArchiveProps -> unit) =
let isOpen, setOpen = React.useState false
let loading, setLoading = React.useState false
let archives, setArchives = React.useState<Archmaester.Dto.ArchiveProps array> [||]
let handleOpenChange (ev: Types.Event) =
console.debug("Model area %s tree open changed", area.name)
setOpen (not isOpen)
let handleArchiveClick (archive: Archmaester.Dto.ArchiveProps) () =
console.debug("Clicked archive %o", archive)
onArchiveClick archive
React.useEffect (
(fun () ->
console.debug("Mounting model area leaf")
if isOpen then
setLoading true
fetchArchives area.modelAreaId
|> Promise.iter (fun res ->
match res with
| Ok archives ->
console.debug("Fetched archives: %o", archives)
setArchives archives
setLoading false
| Error err ->
console.error("Error fetching archives: %s", err)
)
),
[| box isOpen |]
)
let icon =
if loading then
JSX.html """
<Spinner size="tiny" />
"""
else
JS.undefined
let subTree =
if isOpen then
let leafs =
archives
|> Array.sortBy _.name
|> Array.map (fun archive ->
let beforeIcon =
if archive.isPublished then
if archive.isPublic then
JSX.html """
<Tooltip content="Public">
<GlobeRegularIcon />
</Tooltip>
"""
else
JSX.html """
<Tooltip content="Published">
<CheckmarkStarburstRegularIcon />
</Tooltip>
"""
else
JSX.html """<LockClosedRegularIcon />"""
JSX.html $"""
<TreeItem
key={archive.archiveId}
itemType="leaf"
onClick={handleArchiveClick archive}
>
<TreeItemLayout
iconBefore={beforeIcon}
>
{archive.name}
</TreeItemLayout>
</TreeItem>
"""
)
JSX.html $"""
<Tree>
{leafs}
</Tree>
"""
else
JS.undefined
JSX.html $"""
<TreeItem
itemType="branch"
open={isOpen}
onOpenChange={handleOpenChange}
>
<TreeItemLayout
expandIcon={icon}
>
{area.name}
</TreeItemLayout>
{subTree}
</TreeItem>
"""
[<JSX.Component>]
let View (onArchiveClick: Archmaester.Dto.ArchiveProps -> unit) (modelAreas: Archmaester.Dto.ModelArea array) =
let styles: obj = emitJsExpr () "useStyles()"
let items =
modelAreas
|> Array.sortBy _.name
|> Array.map (fun area -> ModelAreaLeaf area.modelAreaId area onArchiveClick)
JSX.html $"""
<Tree aria-label="Default" className={styles?tree}>
{items}
</Tree>
"""

View File

@@ -0,0 +1,60 @@
module Oceanbox.Catalog.FluentUI.Oceanography
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Fable.OpenLayers
open Feliz
open Oceanbox
let private makeStyles: obj -> obj = import "makeStyles" "@fluentui/react-components"
let private Button: obj = import "Button" "@fluentui/react-components"
let private Portal: obj = import "Portal" "@fluentui/react-components"
let private WindowColumnOneFourthLeftFocusLeftFilledIcon: obj = import "WindowColumnOneFourthLeftFocusLeftFilled" "@fluentui/react-icons"
let private useStyles: obj = makeStyles {|
portalContainer = {|
position = "fixed"
top = "0.5em"
left = "2.5em"
|}
|}
[<JSX.Component>]
let View sideNavOpen onClick =
let styles: obj = emitJsExpr () "useStyles()"
let mountNode, setMountNode = React.useState<Types.HTMLElement option> None
let mapBackButton =
if sideNavOpen then
JSX.nothing
else
JSX.html $"""
<Portal mountNode={mountNode}>
<div className={styles?portalContainer}>
<Button
icon={{<WindowColumnOneFourthLeftFocusLeftFilled />}}
onClick={onClick}
>
</Button>
</div>
</Portal>
"""
React.useEffectOnce (fun () ->
console.debug("Mounting Oceanography")
do Catalog.Map.create ()
()
)
JSX.html $"""
<>
<div id="map" ref={setMountNode}></div>
{mapBackButton}
</>
"""

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Fable - FluentUI</title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="../react.svg" />
<link href="../index.css" rel="stylesheet">
<link href="style.scss" rel="stylesheet">
</head>
<body>
<nav id="catalog-nav">
<a href="/">Atlantis</a>
<a href="/catalog">Home</a>
<a href="../mui/index.html">Mui</a>
<a href="../carbon/index.html">Carbon</a>
<a href="index.html">FluentUI</a>
</nav>
<div id="root"></div>
<script type="module" src="../../build/catalog/fluentui/Index.jsx"></script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
@use "ol/ol";
html, body, #root, #fluent-provider {
height: 100%;
}
body {
margin: 0;
}
main {
padding: 0;
}
#catalog-nav {
display: none;
}
div #fluent-provider {
display: flex;
}
#map {
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,11 @@
.navigation {
width: 100%;
background-color: white;
border-bottom: solid gainsboro;
}
main {
padding: 8px;
}

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Catalog</title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="react.svg" />
<link href="index.css" rel="stylesheet">
</head>
<body>
<header>
<nav class="navigation">
<a href="/">Atlantis</a>
<a href="/catalog">Home</a>
<a href="/catalog/mui/index.html">Mui</a>
<a href="/catalog/carbon/index.html">Carbon</a>
<a href="/catalog/fluentui/index.html">FluentUI</a>
</nav>
</header>
<main>
<p>Welcome to this oceanbox playground. Click on one of the component libraries to test their feel and look</p>
</main>
<script type="module" src="../build/catalog/Index.jsx"></script>
</body>
</html>

View File

@@ -0,0 +1,246 @@
module Oceanbox.Catalog.Mui.Archives
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Oceanbox.Catalog
open Remoting
importDefault "@mui/material/Breadcrumbs"
importDefault "@mui/material/Button"
importDefault "@mui/material/Link"
importDefault "@mui/material/Paper"
importDefault "@mui/material/Table"
importDefault "@mui/material/TableBody"
importDefault "@mui/material/TableCell"
importDefault "@mui/material/TableContainer"
importDefault "@mui/material/TableHead"
importDefault "@mui/material/TableRow"
importDefault "@mui/material/Typography"
let private fetchHelloWorldArchive (api: ArchivesApi) =
promise {
let! helloWorld = api.ModelArea.getModelArea Archmaester.Dto.HelloWorld |> Async.StartAsPromise
return helloWorld
}
let private fetchModelAreas () =
promise {
let api = getArchiveUrl () |> ArchivesApi
let! helloWorldOpt = fetchHelloWorldArchive api
match helloWorldOpt with
| Some helloWorld ->
console.info("[Catalog] Fetched hello world: %o", helloWorld)
let! subArchives = api.ModelArea.getSubModelAreas helloWorld.modelAreaId |> Async.StartAsPromise
return subArchives
| None ->
console.error "[Catalog] No hello world! No archives!"
return [||]
}
let private fetchModelAreaArchives (modelArea: Archmaester.Dto.ModelArea) =
promise {
let api = getArchiveUrl () |> ArchivesApi
let! subArchivesRes =
api.Archive.getModelAreaArchives(modelArea.modelAreaId, Archmaester.Dto.ArchiveType.Any)
|> Async.StartAsPromise
return subArchivesRes
}
[<JSX.Component>]
let ModelAreaArchiveTable (modelArea: Archmaester.Dto.ModelArea) (onClick: Archmaester.Dto.ArchiveProps -> unit) =
let archives, setArchives = React.useState<Archmaester.Dto.ArchiveProps array> [||]
React.useEffectOnce (fun () ->
fetchModelAreaArchives modelArea
|> Promise.iter (function
| Ok archives -> setArchives archives
| Error err -> console.error("[Catalog] Error fetching model area %s archives %s", modelArea.name, err)
)
)
let rows =
archives
|> Array.sortBy _.name
|> Array.map (fun archive ->
let expiresStr =
archive.expires
|> Option.map Utils.toLocaleString
|> Option.defaultValue "No expiry"
JSX.html $"""
<TableRow
key={archive.archiveId}
hover={true}
onClick={fun () -> onClick archive}
>
<TableCell>{archive.name}</TableCell>
<TableCell>{string archive.archiveType}</TableCell>
<TableCell>{archive.frames}</TableCell>
<TableCell>{archive.freq}</TableCell>
<TableCell>{archive.projection}</TableCell>
<TableCell>{Utils.toLocaleString archive.startTime}</TableCell>
<TableCell>{Utils.toLocaleString archive.endTime}</TableCell>
<TableCell>{Utils.toLocaleString archive.created}</TableCell>
<TableCell>{expiresStr}</TableCell>
</TableRow>
"""
)
JSX.html $"""
<TableContainer component={{Paper}}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Type</TableCell>
<TableCell>Frames</TableCell>
<TableCell>Frequency</TableCell>
<TableCell>Projection</TableCell>
<TableCell>Start</TableCell>
<TableCell>End</TableCell>
<TableCell>Created</TableCell>
<TableCell>Expires</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows}
</TableBody>
</Table>
</TableContainer>
"""
[<JSX.Component>]
let ModelAreaView (modelArea: Archmaester.Dto.ModelArea) (onBack: unit -> unit) =
let selectedArchive, setSelectedArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
let handleClickBack () =
onBack ()
let handleArchiveClick (archive: Archmaester.Dto.ArchiveProps) =
setSelectedArchive (Some archive)
let archiveBreadcrumb =
match selectedArchive with
| Some archive ->
JSX.html $"""
<Link
underline="hover"
color="inherit"
>
{archive.name}
</Link>
"""
| None ->
JSX.nothing
JSX.html $"""
<>
<div className="archive-header">
<Button
variant="contained"
onClick={fun () -> handleClickBack ()}
>
Back
</Button>
<Breadcrumbs>
<Link
underline="hover"
color="inherit"
onClick={fun () -> setSelectedArchive None}
>
{modelArea.name}
</Link>
{archiveBreadcrumb}
</Breadcrumbs>
</div>
{
match selectedArchive with
| Some archive -> JSX.nothing
| None -> ModelAreaArchiveTable modelArea handleArchiveClick
}
</>
"""
[<JSX.Component>]
let View () =
let selectedModelArea, setSelectedModelArea = React.useState<Archmaester.Dto.ModelArea option> None
let modelAreas, setModelAreas = React.useState<Archmaester.Dto.ModelArea array> [||]
React.useEffectOnce (fun () ->
if Utils.strNull (getArchiveUrl ()) then
do console.error("[Catalog] Archmaester url is empty!")
else
// TODO: Cache these somehow?
fetchModelAreas ()
|> Promise.iter setModelAreas
)
let rows =
modelAreas
|> Array.sortBy _.name
|> Array.map (fun modelArea ->
JSX.html $"""
<TableRow
key={modelArea.modelAreaId}
hover={true}
onClick={fun () -> setSelectedModelArea (Some modelArea)}
>
<TableCell>{modelArea.name}</TableCell>
<TableCell>{modelArea.archives}</TableCell>
<TableCell>{modelArea.models}</TableCell>
<TableCell>{modelArea.description}</TableCell>
</TableRow>
"""
)
let modelAreaTable =
JSX.html $"""
<TableContainer component={{Paper}}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Archives</TableCell>
<TableCell>Models</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows}
</TableBody>
</Table>
</TableContainer>
"""
JSX.html $"""
<>
<Typography
variant="h2"
sx={{{{
paddingBottom: "16px",
}}}}
>
Archives
</Typography>
{
match selectedModelArea with
| Some modelArea ->
ModelAreaView modelArea (fun () -> setSelectedModelArea None)
| None ->
modelAreaTable
}
</>
"""

View File

@@ -0,0 +1,25 @@
module Oceanbox.Catalog.Mui.Index
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Oceanbox.Catalog
importAll "react"
import "StyledEngineProvider" "@mui/material/styles"
let root = ReactDOM.createRoot (document.getElementById "root")
root.render (
JSX.html
$"""
<react.StrictMode>
<StyledEngineProvider injectFirst>
{App.View ()}
</StyledEngineProvider>
</react.StrictMode>
"""
|> Utils.toReact
)

View File

@@ -0,0 +1,425 @@
module Oceanbox.Catalog.Mui.App
open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Feliz.Router
open Feliz.UseMediaQuery
open Oceanbox.Catalog
importSideEffects "@fontsource/roboto/300.css"
importSideEffects "@fontsource/roboto/400.css"
importSideEffects "@fontsource/roboto/500.css"
importSideEffects "@fontsource/roboto/700.css"
let private MuiAppBar: obj = importDefault "@mui/material/AppBar"
importDefault "@mui/material/Box"
importDefault "@mui/material/Button"
importDefault "@mui/material/CssBaseline"
importDefault "@mui/material/Divider"
let private MuiDrawer : obj = importDefault "@mui/material/Drawer"
importDefault "@mui/material/IconButton"
importDefault "@mui/material/List"
importDefault "@mui/material/ListItem"
importDefault "@mui/material/ListItemButton"
importDefault "@mui/material/ListItemIcon"
importDefault "@mui/material/ListItemText"
importDefault "@mui/material/Toolbar"
importDefault "@mui/material/Typography"
// NOTE: Renaming the import without mangling the name
let private ChevronLeftIcon: obj = importDefault "@mui/icons-material/ChevronLeft"
let private HomeIcon: obj = importDefault "@mui/icons-material/Home"
let private InventoryIcon: obj = importDefault "@mui/icons-material/Inventory"
let private MapIcon: obj = importDefault "@mui/icons-material/Map"
let private MenuIcon: obj = importDefault "@mui/icons-material/Menu"
let private QueryStatsIcon: obj = importDefault "@mui/icons-material/QueryStats"
let private StorageIcon: obj = importDefault "@mui/icons-material/Storage"
let private WaterIcon: obj = importDefault "@mui/icons-material/Water"
let private ThemeProvider: obj = import "ThemeProvider" "@mui/material/styles"
let private createTheme: obj -> unit = import "createTheme" "@mui/material/styles"
let private useTheme: unit -> unit = import "useTheme" "@mui/material/styles"
let private styled (comp: string) : obj -> unit = import "styled" "@mui/material/styles"
let private styledWithOptions (comp: string) (options: obj) : obj -> unit = import "styled" "@mui/material/styles"
type Style =
[<Import("styled", "@mui/material/styles")>]
static member inline styled(comp: string, ?options: obj) : obj -> obj = jsNative
[<Import("styled", "@mui/material/styles")>]
static member inline styled(comp: obj, ?options: obj) : obj -> obj = jsNative
let private drawerWidth = 260
let private darkTheme =
createTheme (createObj [ "palette" ==> createObj [ "mode" ==> "light" ] ])
let private openedMixin theme : obj = emitJsExpr theme """({
width: drawerWidth,
transition: $0.transitions.create('width', {
easing: $0.transitions.easing.sharp,
duration: $0.transitions.duration.enteringScreen,
}),
overflowX: 'hidden',
})"""
let private closedMixin theme = emitJsExpr theme """({
transition: $0.transitions.create('width', {
easing: $0.transitions.easing.sharp,
duration: $0.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: `calc(${$0.spacing(7)} + 1px)`,
[$0.breakpoints.up('sm')]: {
width: `calc(${$0.spacing(8)} + 1px)`,
},
})"""
let private MyComponent =
Style.styled
("div", {| shouldForwardProp = fun prop -> prop <> "open" |})
(Utils.toStyle [
style.color "darkslategray"
style.backgroundColor "aliceblue"
style.padding 8
style.borderRadius 4
])
let private Main =
styledWithOptions "main" {| shouldForwardProp = fun prop -> prop <> "open" |} (fun (props: obj) ->
let theme = props?theme
Utils.toStyle [
style.flexGrow 1
style.padding (props?theme?spacing 3 |> unbox<Styles.ICssUnit>)
style.custom (
"transition", props?theme?transitions?create("margin", createObj [
"easing" ==> theme?transitions?easing?sharp
"duration" ==> theme?transitions?duration?leavingScreen
])
)
style.custom (
"variants", [|
createObj [
"props" ==> fun props -> props?``open``
"style" ==> Utils.toStyle [
style.custom (
"transition", props?theme?transitions?create("margin", createObj [
"easing" ==> theme?transitions?easing?easeOut
"duration" ==> theme?transitions?duration?enteringScreen
])
)
style.marginLeft 0
]
]
|]
)
])
let private AppBar : obj = emitJsExpr drawerWidth """styled(MuiAppBar, {
shouldForwardProp: (prop) => prop !== 'open',
})(({ theme }) => ({
[theme.breakpoints.up('md')]: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
variants: [
{
props: ({ open }) => open,
style: {
[theme.breakpoints.up('md')]: {
marginLeft: $0,
width: `calc(100% - ${$0}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
}
}
],
}))
"""
let private DrawerHeader: unit =
styled "div" (fun (props: obj) ->
let theme: obj = props?theme
let toolbar = Utils.spreadStyles theme?mixins?toolbar
let style = [|
style.display.flex
style.alignItems.center
style.justifyContent.flexEnd
style.padding (theme?spacing (0, 1) |> unbox<Styles.ICssUnit>)
|]
// NOTE(simkir): Not sure how to do spreading in a nice way. If you do `yield!` Fable turns the expression lazy,
// kinda.
Utils.toStyle (Array.append style toolbar))
// NOTE: Taken from https://mui.com/material-ui/react-drawer/#mini-variant-drawer
let private Drawer : obj = emitJsExpr () """styled(MuiDrawer, {
shouldForwardProp: (prop) => prop !== 'open',
})(({ theme }) => ({
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
boxSizing: 'border-box',
variants: [
{
props: ({ open }) => open,
style: {
...openedMixin(theme),
'& .MuiDrawer-paper': openedMixin(theme),
},
},
{
props: ({ open }) => !open,
style: {
...closedMixin(theme),
'& .MuiDrawer-paper': closedMixin(theme),
},
},
],
}),
)
"""
let private pages = [|
"/", "Atlantis"
"/catalog/index.html", "Home"
"index.html", "Mui"
"../carbon/index.html", "Carbon"
"../fluentui/index.html", "FluentUI"
|]
[<JSX.Component>]
let private DummyMain () =
JSX.html """
<>
<Typography variant="h2">Home</Typography>
<Typography >
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non
enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus
imperdiet. Semper risus in hendrerit gravida rutrum quisque non tellus.
Convallis convallis tellus id interdum velit laoreet id donec ultrices.
Odio morbi quis commodo odio aenean sed adipiscing. Amet nisl suscipit
adipiscing bibendum est ultricies integer quis. Cursus euismod quis viverra
nibh cras. Metus vulputate eu scelerisque felis imperdiet proin fermentum
leo. Mauris commodo quis imperdiet massa tincidunt. Cras tincidunt lobortis
feugiat vivamus at augue. At augue eget arcu dictum varius duis at
consectetur lorem. Velit sed ullamcorper morbi tincidunt. Lorem donec massa
sapien faucibus et molestie ac.
</Typography>
<Button variant="contained">Hello, Mui</Button>
<MyComponent>Styled div</MyComponent>
</>
"""
[<JSX.Component>]
let View () =
let theme = useTheme ()
let responsive = React.useResponsive()
let currentUrl, updateUrl = React.useState(Router.currentUrl())
let isOpen, setOpen = React.useState false
let handleDrawerOpen () = setOpen true
let handleDrawerClose () = setOpen false
let isLargeScreen =
responsive = ScreenSize.WideScreen
|| responsive = ScreenSize.Desktop
let url = Router.currentUrl ()
let drawer =
JSX.html $"""
<>
<DrawerHeader>
<IconButton
onClick={handleDrawerClose}
>
<ChevronLeftIcon />
</IconButton>
</DrawerHeader>
<Divider />
<List>
<ListItem key="home" disablePadding>
<ListItemButton
selected={url = []}
onClick={fun () -> Router.navigate ""}
>
<ListItemIcon>
<HomeIcon />
</ListItemIcon>
<ListItemText primary="Home" sx={Utils.toStyle [ style.opacity (if isOpen then 1 else 0)]} />
</ListItemButton>
</ListItem>
<ListItem key="oceanography" disablePadding>
<ListItemButton
selected={List.tryHead url = Some "oceanography"}
onClick={fun () -> Router.navigate "oceanography"}
>
<ListItemIcon>
<MapIcon />
</ListItemIcon>
<ListItemText primary="Oceanography" sx={Utils.toStyle [ style.opacity (if isOpen then 1 else 0)]} />
</ListItemButton>
</ListItem>
<ListItem key="compute" disablePadding>
<ListItemButton
selected={List.tryHead url = Some "compute"}
onClick={fun () -> Router.navigate "compute"}
>
<ListItemIcon>
<StorageIcon />
</ListItemIcon>
<ListItemText primary="Compute" sx={Utils.toStyle [ style.opacity (if isOpen then 1 else 0)]} />
</ListItemButton>
</ListItem>
<ListItem key="statistics" disablePadding>
<ListItemButton
selected={List.tryHead url = Some "statistics"}
onClick={fun () -> Router.navigate "statistics"}
>
<ListItemIcon>
<QueryStatsIcon />
</ListItemIcon>
<ListItemText primary="Statistics" sx={Utils.toStyle [ style.opacity (if isOpen then 1 else 0)]} />
</ListItemButton>
</ListItem>
<ListItem key="archives" disablePadding>
<ListItemButton
selected={List.tryHead url = Some "archives"}
onClick={fun () -> Router.navigate "archives"}
>
<ListItemIcon>
<InventoryIcon />
</ListItemIcon>
<ListItemText primary="Archives" sx={Utils.toStyle [ style.opacity (if isOpen then 1 else 0)]} />
</ListItemButton>
</ListItem>
</List>
</>
"""
let linkButton (url: string, str: string) =
JSX.html $"""
<Button
key={str}
href={url}
sx={
Utils.toStyle [
style.color "white"
]
}
>
{str}
</Button>
"""
let content =
match currentUrl with
| [] -> DummyMain() |> Utils.toReact
| ["oceanography"] -> JSX.html """<Typography variant="h2">Oceanography</Typography>""" |> Utils.toReact
| ["compute"] -> JSX.html """<Typography variant="h2">Compute</Typography>""" |> Utils.toReact
| ["statistics"] -> JSX.html """<Typography variant="h2">Statistics</Typography>""" |> Utils.toReact
| ["archives"] -> Archives.View () |> Utils.toReact
| otherwise -> Html.h1 "404 Not found"
React.router [
router.onUrlChanged updateUrl
router.children [
JSX.html $"""
<ThemeProvider theme={darkTheme}>
<Box sx={Utils.toStyle [ style.display.flex ]}>
<CssBaseline />
<AppBar position="fixed" open={isOpen}>
<Toolbar>
<IconButton
color="inherit"
edge="start"
onClick={handleDrawerOpen}
sx={Utils.toStyle [
style.custom ("mr", 2)
if isOpen then style.display.none
]}
>
<MenuIcon />
</IconButton>
<Typography
variant="h5"
sx={
Utils.toStyle [
style.custom ("mr", 2)
]
}
>
Oceanbox
</Typography>
<Box
sx={
Utils.toStyle [
style.flexGrow 1
if not isLargeScreen then
style.display.none
]
}
>
{pages |> Array.map linkButton}
</Box>
</Toolbar>
</AppBar>
<MuiDrawer
open={isOpen}
sx={
Utils.toStyle [
if isLargeScreen then
style.display.none
else
style.display.block
]
}
>
{drawer}
</MuiDrawer>
<Drawer
variant={if isLargeScreen then "permanent" else "temporary"}
anchor="left"
open={isOpen}
sx={
Utils.toStyle [
if isLargeScreen then
style.display.block
else
style.display.none
]
}
>
{drawer}
</Drawer>
<Main open={isOpen}>
<DrawerHeader />
{content}
</Main>
</Box>
</ThemeProvider>
"""
|> Utils.toReact
]
]

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Playground - Mui</title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="../react.svg" />
<link href="../index.css" rel="stylesheet">
<link href="style.scss" rel="stylesheet">
</head>
<body>
<header>
<nav class="navigation">
<a href="/">Atlantis</a>
<a href="/catalog/index.html">Home</a>
<a href="index.html">Mui</a>
<a href="../carbon/index.html">Carbon</a>
<a href="../fluentui/index.html">FluentUI</a>
</nav>
</header>
<div id="root"></div>
<script type="module" src="../../build/catalog/mui/Index.jsx"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
.navigation {
display: none;
}
.drawer-header {
display: flex;
justify-content: flex-end;
}
.archive-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 8px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,32 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850.39 566.93">
<defs>
<style>
.cls-1 {
fill: #003f70;
}
.cls-2 {
fill: #7296b7;
}
</style>
</defs>
<g>
<g>
<circle class="cls-1" cx="296.72" cy="235.29" r="33.2"/>
<circle class="cls-1" cx="338.77" cy="190.14" r="21.65"/>
<g>
<path class="cls-1" d="M505.8,293.18A66.28,66.28,0,0,0,439.6,227v-48.3a114.5,114.5,0,0,0,0,229v-48.3A66.27,66.27,0,0,0,505.8,293.18Zm-66.2,63a63,63,0,1,1,63-63A63.09,63.09,0,0,1,439.6,356.2Z"/>
<g>
<path class="cls-2" d="M439.6,185.56v4c57,0,103.38,46.48,103.38,103.61S496.6,396.78,439.6,396.78v4c59.21,0,107.38-48.27,107.38-107.61S498.81,185.56,439.6,185.56Z"/>
<path class="cls-2" d="M439.6,192.44v3.86c53.3,0,96.66,43.46,96.66,96.88s-43.36,96.87-96.66,96.87v3.87c55.42,0,100.51-45.19,100.51-100.74S495,192.44,439.6,192.44Z"/>
<path class="cls-2" d="M439.6,199.32V203c49.6,0,89.95,40.44,89.95,90.15s-40.35,90.14-89.95,90.14V387c51.64,0,93.65-42.11,93.65-93.86S491.24,199.32,439.6,199.32Z"/>
<path class="cls-2" d="M439.6,206.2v3.56a83.42,83.42,0,0,1,0,166.83v3.57a87,87,0,0,0,0-174Z"/>
<path class="cls-2" d="M439.6,213.07v3.42a76.69,76.69,0,0,1,0,153.37v3.42a80.11,80.11,0,0,0,0-160.21Z"/>
<path class="cls-2" d="M439.6,220v3.27a70,70,0,0,1,0,139.91v3.27a73.23,73.23,0,0,0,0-146.45Z"/>
<path class="cls-2" d="M439.6,226.83V230a63.23,63.23,0,0,1,0,126.46v3.12a66.35,66.35,0,0,0,0-132.7Z"/>
<path class="cls-2" d="M439.6,178.68h0v4.16h0c60.7,0,110.09,49.5,110.09,110.34S500.3,403.51,439.6,403.51h0v4.16h0c63,0,114.24-51.36,114.24-114.49S502.59,178.68,439.6,178.68Z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
<title>React Logo</title>
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"/>
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1,7 @@
User-agent: Googlebot
Disallow: /nogooglebot/
User-agent: *
Allow: /
Sitemap: https://www.example.com/sitemap.xml

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
dotnet fable watch --verbose -e ".jsx" --run bunx --bun vite

View File

@@ -0,0 +1,58 @@
import mkcert from "vite-plugin-mkcert"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
const certDir = `${process.env.HOME}/.vite-plugin-mkcert`;
const proxy = {
target: `http://127.0.0.1:8085/`,
changeOrigin: false,
secure: false,
ws: true
};
export default defineConfig({
appType: "mpa",
clearScreen: false,
plugins: [
react(),
mkcert({
hosts: [
"localhost",
"*.local.oceanbox.io"
],
savePath: `${certDir}/certs`,
mkcertPath: `${certDir}/mkcert`
}),
],
build: {
rollupOptions: {
input: {
index: "index.html",
carbon: "carbon/index.html",
mui: "mui/index.html",
fluentui: "fluentui/index.html",
},
},
},
server: {
port: 8081,
host: "0.0.0.0",
https: true,
cors: true,
proxy: {
'/socket': proxy,
"/api": proxy,
'/isAuthenticated': proxy,
'/signin-oidc': proxy,
'/signin': proxy,
'/signout': proxy,
'/token': proxy,
'/claims': proxy,
'/impersonate': proxy,
'/unimpersonate': proxy,
'/hub': proxy,
'/barentswatch-token': proxy,
},
},
})

View File

@@ -21,19 +21,6 @@
</head>
<body>
<!-- <script> -->
<!-- if (window.location.pathname == "/map") { -->
<!-- var app = document.createElement("map-app"); -->
<!-- } else if (window.location.pathname == "/atlas") { -->
<!-- var app = document.createElement("atlas-app"); -->
<!-- } else if (window.location.pathname == "/login") { -->
<!-- var app = document.createElement("login-app"); -->
<!-- } else { -->
<!-- var app = document.createElement("init-app"); -->
<!-- }; -->
<!-- app.setAttribute("style", "height: 100%;"); -->
<!-- document.body.appendChild(app); -->
<!-- </script> -->
<init-app id="app" style="height: 100%;"></init-app>
<script type="module" src="./build/App.jsx"></script>
</body>

View File

@@ -18,78 +18,83 @@ var proxy = {
};
const plugins = [
react({jsxRuntime: "automatic"}),
mkcert({
hosts: [
"localhost",
"*.local.oceanbox.io"
],
savePath: `${certDir}/certs`,
mkcertPath: `${certDir}/mkcert`
}),
!process.env.CREATE_SENTRY
? null
: sentryVitePlugin({
org: "oceanbox",
project: "atlantis-client",
authToken: "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",
filesToDeleteAfterUpload: "*",
sourcemaps: {
ignore: ["../../node_modules/**", "./vite.config.js"]
},
telemetry: false,
}),
react({jsxRuntime: "automatic"}),
mkcert({
hosts: [
"localhost",
"*.local.oceanbox.io"
],
savePath: `${certDir}/certs`,
mkcertPath: `${certDir}/mkcert`
}),
!process.env.CREATE_SENTRY
? null
: sentryVitePlugin({
org: "oceanbox",
project: "atlantis-client",
authToken: "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",
filesToDeleteAfterUpload: "*",
sourcemaps: {
ignore: ["../../node_modules/**", "./vite.config.js"]
},
telemetry: false,
}),
];
export default defineConfig({
plugins: plugins,
resolve: {
alias: {
// We're only using a subset from plotly
// Add alias to enable typing regardless
'plotly.js': resolve(__dirname, './plotly-bundle'),
// The bundler file still needs access to the actual plotly module
'plotly-dist': resolve(__dirname, '../../node_modules/plotly.js')
appType: "mpa",
plugins: plugins,
resolve: {
alias: {
// We're only using a subset from plotly
// Add alias to enable typing regardless
'plotly.js': resolve(__dirname, './plotly-bundle'),
// The bundler file still needs access to the actual plotly module
'plotly-dist': resolve(__dirname, '../../node_modules/plotly.js')
},
},
define: {
global: {},
'process.env': {},
},
build: {
rollupOptions: {
input: {
main: resolve(__dirname, './src/Client/index.html'),
atlas: resolve(__dirname, './src/Client/atlas.html'),
mapster: resolve(__dirname, './src/Client/map.html'),
catalog: resolve(__dirname, "./src/Client/catalog/index.html"),
catalogMui: "catalog/mui/index.html",
catalogCarbon: "catalog/carbon/index.html",
catalogFluentUI: "catalog/fluentui/index.html",
},
},
define: {
global: {},
'process.env': {},
minify: "oxc",
},
// config options
server: {
port: clientPort,
host: '0.0.0.0',
https: true,
cors: true,
proxy: {
'/api': proxy,
'/isAuthenticated': proxy,
'/signin-oidc': proxy,
'/signin': proxy,
'/signout': proxy,
'/token': proxy,
'/claims': proxy,
'/impersonate': proxy,
'/unimpersonate': proxy,
'/hub': proxy,
'/barentswatch-token': proxy,
'/socket': proxy,
},
build: {
rollupOptions: {
input: {
main: resolve(__dirname, './src/Client/index.html'),
atlas: resolve(__dirname, './src/Client/atlas.html'),
mapster: resolve(__dirname, './src/Client/map.html'),
},
},
minify: "oxc",
},
// config options
server: {
port: clientPort,
host: '0.0.0.0',
https: true,
cors: true,
proxy: {
'/api': proxy,
'/isAuthenticated': proxy,
'/signin-oidc': proxy,
'/signin': proxy,
'/signout': proxy,
'/token': proxy,
'/claims': proxy,
'/impersonate': proxy,
'/unimpersonate': proxy,
'/hub': proxy,
'/barentswatch-token': proxy,
'/socket': proxy,
},
watch: {
ignored: [
"**/*.fs" // Don't watch F# files
],
}
watch: {
ignored: [
"**/*.fs" // Don't watch F# files
],
}
})
}
})