Compare commits
18 Commits
main
...
simkir/fra
| Author | SHA1 | Date | |
|---|---|---|---|
| 8db1592ca3 | |||
| b3de3d88c4 | |||
| 27bc379196 | |||
| 2ff9dafc13 | |||
| 6a24342322 | |||
| d05015decf | |||
| b66cc67802 | |||
| ac27a0544d | |||
| 3ba01c882b | |||
| 649b0af9d4 | |||
| 918041e063 | |||
| c3c6bb06df | |||
| 647302778f | |||
| e53de54c13 | |||
| 8e9d975cca | |||
| e8990fa1d4 | |||
| ff4f3aebab | |||
| 4ddbe133b0 |
@@ -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
|
||||
@@ -124,4 +124,4 @@ let runOrDefault args =
|
||||
0
|
||||
with e ->
|
||||
printfn "%A" e
|
||||
1
|
||||
1
|
||||
|
||||
@@ -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
3
.gitignore
vendored
@@ -29,4 +29,5 @@ NuGet.Config
|
||||
sync.list
|
||||
packages.lock.json
|
||||
package-lock.json
|
||||
*.nupkg
|
||||
*.nupkg
|
||||
*.jsx
|
||||
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" 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>
|
||||
@@ -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" />
|
||||
|
||||
15
package.json
15
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ pkgs.mkShellNoCC {
|
||||
buildInputs = [ dotnet-sdk ];
|
||||
|
||||
packages = [
|
||||
pkgs.fsautocomplete
|
||||
pkgs.fantomas
|
||||
|
||||
# JavaScript
|
||||
pkgs.bun
|
||||
pkgs.nodejs-slim
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 =
|
||||
|
||||
@@ -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>
|
||||
"""
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
dotnet fable watch -o build/client --run vite -c ../../vite.config.js
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
39
src/Atlantis/src/Client/catalog/Catalog.fsproj
Normal file
39
src/Atlantis/src/Client/catalog/Catalog.fsproj
Normal 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>
|
||||
24
src/Atlantis/src/Client/catalog/Index.fs
Normal file
24
src/Atlantis/src/Client/catalog/Index.fs
Normal 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
|
||||
27
src/Atlantis/src/Client/catalog/Map.fs
Normal file
27
src/Atlantis/src/Client/catalog/Map.fs
Normal 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)
|
||||
|
||||
()
|
||||
17
src/Atlantis/src/Client/catalog/Utils.fs
Normal file
17
src/Atlantis/src/Client/catalog/Utils.fs
Normal 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")
|
||||
43
src/Atlantis/src/Client/catalog/carbon/Carbon.fs
Normal file
43
src/Atlantis/src/Client/catalog/carbon/Carbon.fs
Normal 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>
|
||||
</>
|
||||
"""
|
||||
77
src/Atlantis/src/Client/catalog/carbon/Home.fs
Normal file
77
src/Atlantis/src/Client/catalog/carbon/Home.fs
Normal 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>
|
||||
"""
|
||||
24
src/Atlantis/src/Client/catalog/carbon/Index.fs
Normal file
24
src/Atlantis/src/Client/catalog/carbon/Index.fs
Normal 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
|
||||
)
|
||||
35
src/Atlantis/src/Client/catalog/carbon/Oceanography.fs
Normal file
35
src/Atlantis/src/Client/catalog/carbon/Oceanography.fs
Normal 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>
|
||||
"""
|
||||
118
src/Atlantis/src/Client/catalog/carbon/components/Header.fs
Normal file
118
src/Atlantis/src/Client/catalog/carbon/components/Header.fs
Normal 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>
|
||||
"""
|
||||
26
src/Atlantis/src/Client/catalog/carbon/index.html
Normal file
26
src/Atlantis/src/Client/catalog/carbon/index.html
Normal 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>
|
||||
35
src/Atlantis/src/Client/catalog/carbon/style.scss
Normal file
35
src/Atlantis/src/Client/catalog/carbon/style.scss
Normal 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%;
|
||||
}
|
||||
376
src/Atlantis/src/Client/catalog/fluentui/Archives.fs
Normal file
376
src/Atlantis/src/Client/catalog/fluentui/Archives.fs
Normal 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
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
50
src/Atlantis/src/Client/catalog/fluentui/Charts.fs
Normal file
50
src/Atlantis/src/Client/catalog/fluentui/Charts.fs
Normal 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>
|
||||
"""
|
||||
137
src/Atlantis/src/Client/catalog/fluentui/FluentUI.fs
Normal file
137
src/Atlantis/src/Client/catalog/fluentui/FluentUI.fs
Normal 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>
|
||||
"""
|
||||
21
src/Atlantis/src/Client/catalog/fluentui/Index.fs
Normal file
21
src/Atlantis/src/Client/catalog/fluentui/Index.fs
Normal 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
|
||||
)
|
||||
155
src/Atlantis/src/Client/catalog/fluentui/ModelAreaTree.fs
Normal file
155
src/Atlantis/src/Client/catalog/fluentui/ModelAreaTree.fs
Normal 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>
|
||||
"""
|
||||
60
src/Atlantis/src/Client/catalog/fluentui/Oceanography.fs
Normal file
60
src/Atlantis/src/Client/catalog/fluentui/Oceanography.fs
Normal 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}
|
||||
</>
|
||||
"""
|
||||
24
src/Atlantis/src/Client/catalog/fluentui/index.html
Normal file
24
src/Atlantis/src/Client/catalog/fluentui/index.html
Normal 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>
|
||||
26
src/Atlantis/src/Client/catalog/fluentui/style.scss
Normal file
26
src/Atlantis/src/Client/catalog/fluentui/style.scss
Normal 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%;
|
||||
}
|
||||
11
src/Atlantis/src/Client/catalog/index.css
Normal file
11
src/Atlantis/src/Client/catalog/index.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.navigation {
|
||||
width: 100%;
|
||||
|
||||
background-color: white;
|
||||
|
||||
border-bottom: solid gainsboro;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 8px;
|
||||
}
|
||||
27
src/Atlantis/src/Client/catalog/index.html
Normal file
27
src/Atlantis/src/Client/catalog/index.html
Normal 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>
|
||||
246
src/Atlantis/src/Client/catalog/mui/Archives.fs
Normal file
246
src/Atlantis/src/Client/catalog/mui/Archives.fs
Normal 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
|
||||
}
|
||||
</>
|
||||
"""
|
||||
25
src/Atlantis/src/Client/catalog/mui/Index.fs
Normal file
25
src/Atlantis/src/Client/catalog/mui/Index.fs
Normal 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
|
||||
)
|
||||
425
src/Atlantis/src/Client/catalog/mui/Mui.fs
Normal file
425
src/Atlantis/src/Client/catalog/mui/Mui.fs
Normal 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
|
||||
]
|
||||
]
|
||||
26
src/Atlantis/src/Client/catalog/mui/index.html
Normal file
26
src/Atlantis/src/Client/catalog/mui/index.html
Normal 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>
|
||||
16
src/Atlantis/src/Client/catalog/mui/style.scss
Normal file
16
src/Atlantis/src/Client/catalog/mui/style.scss
Normal 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;
|
||||
}
|
||||
BIN
src/Atlantis/src/Client/catalog/public/ob.png
Normal file
BIN
src/Atlantis/src/Client/catalog/public/ob.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
32
src/Atlantis/src/Client/catalog/public/ob.svg
Normal file
32
src/Atlantis/src/Client/catalog/public/ob.svg
Normal 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 |
9
src/Atlantis/src/Client/catalog/public/react.svg
Normal file
9
src/Atlantis/src/Client/catalog/public/react.svg
Normal 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 |
7
src/Atlantis/src/Client/catalog/public/robots.txt
Normal file
7
src/Atlantis/src/Client/catalog/public/robots.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
User-agent: Googlebot
|
||||
Disallow: /nogooglebot/
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://www.example.com/sitemap.xml
|
||||
3
src/Atlantis/src/Client/catalog/run
Executable file
3
src/Atlantis/src/Client/catalog/run
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
dotnet fable watch --verbose -e ".jsx" --run bunx --bun vite
|
||||
58
src/Atlantis/src/Client/catalog/vite.config.js
Normal file
58
src/Atlantis/src/Client/catalog/vite.config.js
Normal 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,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user