Start on archive management in Mui

Table views of model areas and archives
This commit is contained in:
2025-08-20 18:05:11 +02:00
parent b3de3d88c4
commit 8db1592ca3
11 changed files with 626 additions and 156 deletions

View File

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

View File

@@ -12,6 +12,7 @@
<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" />

View File

@@ -9,6 +9,7 @@ open Remoting
let private testAuthenticated () =
promise {
let! authOpt = authApi.IsAuthenticated () |> Async.StartAsPromise
do! Utils.initAtlantisSessionUrls () |> Async.StartAsPromise
match authOpt with
| Some auth ->

View File

@@ -14,4 +14,4 @@ let inline spreadStyles object : IStyleAttribute array = jsNative
let toLocaleString (date: System.DateTime) : string =
let jsDate = unbox<JS.Date> date
jsDate.toLocaleString()
jsDate?toLocaleString("en-GB")

View File

@@ -203,9 +203,8 @@ let View () =
do console.error("[Catalog] Archmaester url is empty!")
else
fetchModelAreas ()
|> Promise.iter (fun archives ->
setModelAreas archives
)
|> Promise.iter setModelAreas
)
Html.div [
@@ -270,7 +269,7 @@ let View () =
Html.div [
prop.style [
style.flexGrow 1
style.minWidth (length.px 256)
style.maxWidth (length.px 512)
]
prop.children (
ModelAreaTree.View handleArchiveClick modelAreas |> Utils.toReact
@@ -279,14 +278,57 @@ let View () =
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>}}
description={{<Caption1>Start time: {archive.startTime.ToShortDateString()}</Caption1>}}
/>
<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>
"""

View File

@@ -7,15 +7,30 @@ 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.Fvcom (Archmaester.Dto.FvcomVariant.Any, Archmaester.Dto.FvcomFormat.Any)
let archiveType = Archmaester.Dto.ArchiveType.Any
let! res = api.Archive.getModelAreaArchives(modelAreaId, archiveType) |> Async.StartAsPromise
@@ -67,14 +82,36 @@ let private ModelAreaLeaf key (area: Archmaester.Dto.ModelArea) (onArchiveClick:
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>{archive.name}</TreeItemLayout>
<TreeItemLayout
iconBefore={beforeIcon}
>
{archive.name}
</TreeItemLayout>
</TreeItem>
"""
)
@@ -104,13 +141,15 @@ let private ModelAreaLeaf key (area: Archmaester.Dto.ModelArea) (onArchiveClick:
[<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">
<Tree aria-label="Default" className={styles?tree}>
{items}
</Tree>
"""

View File

@@ -1,4 +1,4 @@
nav {
.navigation {
width: 100%;
background-color: white;

View File

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

View File

@@ -17,7 +17,7 @@ root.render (
$"""
<react.StrictMode>
<StyledEngineProvider injectFirst>
{App.View () |> Utils.toReact}
{App.View ()}
</StyledEngineProvider>
</react.StrictMode>
"""

View File

@@ -4,6 +4,8 @@ open Browser
open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Feliz.Router
open Feliz.UseMediaQuery
open Oceanbox.Catalog
@@ -17,7 +19,7 @@ importDefault "@mui/material/Box"
importDefault "@mui/material/Button"
importDefault "@mui/material/CssBaseline"
importDefault "@mui/material/Divider"
importDefault "@mui/material/Drawer"
let private MuiDrawer : obj = importDefault "@mui/material/Drawer"
importDefault "@mui/material/IconButton"
importDefault "@mui/material/List"
importDefault "@mui/material/ListItem"
@@ -28,11 +30,14 @@ importDefault "@mui/material/Toolbar"
importDefault "@mui/material/Typography"
// NOTE: Renaming the import without mangling the name
let private ChevronRightIcon: obj = importDefault "@mui/icons-material/ChevronRight"
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 QueryStatsIcon: obj = importDefault "@mui/icons-material/QueryStats"
let private ThemeProvider: obj = import "ThemeProvider" "@mui/material/styles"
let private createTheme: obj -> unit = import "createTheme" "@mui/material/styles"
@@ -52,6 +57,27 @@ 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" |})
@@ -74,7 +100,6 @@ let private Main =
"duration" ==> theme?transitions?duration?leavingScreen
])
)
style.marginLeft (length.px -drawerWidth)
style.custom (
"variants", [|
createObj [
@@ -93,37 +118,33 @@ let private Main =
)
])
let private AppBar =
let f (props: obj) =
let theme = props?theme
Utils.toStyle [
style.custom (
"transition", props?theme?transitions?create(("margin", "width"), createObj [
"easing" ==> theme?transitions?easing?sharp
"duration" ==> theme?transitions?duration?leavingScreen
])
)
style.custom (
"variants", [|
createObj [
"props" ==> fun props -> props?``open``
"style" ==> Utils.toStyle [
style.width (length.calc $"100%% - {drawerWidth}px")
style.marginLeft (length.px drawerWidth)
style.custom (
"transition", props?theme?transitions?create(("margin", "width"), createObj [
"easing" ==> theme?transitions?easing?easeOut
"duration" ==> theme?transitions?duration?enteringScreen
])
)
]
]
|]
)
]
Style.styled(MuiAppBar, {| shouldForwardProp = fun prop -> prop <> "open" |}) f
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) ->
@@ -136,13 +157,39 @@ let private DrawerHeader: unit =
style.padding (theme?spacing (0, 1) |> unbox<Styles.ICssUnit>)
|]
console.debug("[Mui] Styled DrawerHeader theme %o, toolbar: %o", theme, toolbar)
// 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))
let pages = [|
// 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"
@@ -150,18 +197,127 @@ let pages = [|
"../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 [
@@ -173,120 +329,97 @@ let View () =
</Button>
"""
JSX.html
$"""
<ThemeProvider theme={darkTheme}>
<Box sx={Utils.toStyle [ style.display.flex ]}>
<CssBaseline />
<AppBar position="fixed" open={isOpen}>
<Toolbar>
<IconButton
onClick={handleDrawerOpen}
sx={Utils.toStyle [
style.custom ("mr", 2)
if isOpen then style.display.none
]}
>
<MenuIcon />
</IconButton>
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"
<Typography
variant="h5"
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 [
style.custom ("mr", 2)
if isLargeScreen then
style.display.none
else
style.display.block
]
}
>
Oceanbox
</Typography>
<Box
{drawer}
</MuiDrawer>
<Drawer
variant={if isLargeScreen then "permanent" else "temporary"}
anchor="left"
open={isOpen}
sx={
Utils.toStyle [
style.flexGrow 1
if isLargeScreen then
style.display.block
else
style.display.none
]
}
>
{pages |> Array.map linkButton}
</Box>
</Toolbar>
</AppBar>
{drawer}
</Drawer>
<Drawer
sx={
Utils.toStyle [
style.width (length.px drawerWidth)
style.flexShrink 0
style.custom (
"& .MuiDrawer-paper",
Utils.toStyle [
style.width drawerWidth
style.boxSizing.borderBox
]
)
]
}
variant="persistent"
anchor="left"
open={isOpen}
>
<DrawerHeader>
<IconButton
onClick={handleDrawerClose}
>
<ChevronRightIcon />
</IconButton>
</DrawerHeader>
<Divider />
<List>
<ListItem key="oceanography" disablePadding>
<ListItemButton>
<ListItemIcon>
<WaterIcon />
</ListItemIcon>
<ListItemText primary="Oceanography" />
</ListItemButton>
</ListItem>
<ListItem key="compute" disablePadding>
<ListItemButton>
<ListItemIcon>
<StorageIcon />
</ListItemIcon>
<ListItemText primary="Compute" />
</ListItemButton>
</ListItem>
<ListItem key="statistics" disablePadding>
<ListItemButton>
<ListItemIcon>
<QueryStatsIcon />
</ListItemIcon>
<ListItemText primary="Statistics" />
</ListItemButton>
</ListItem>
</List>
</Drawer>
<Main open={isOpen}>
<DrawerHeader />
<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>
</Main>
</Box>
</ThemeProvider>
"""
<Main open={isOpen}>
<DrawerHeader />
{content}
</Main>
</Box>
</ThemeProvider>
"""
|> Utils.toReact
]
]

View File

@@ -5,4 +5,12 @@
.drawer-header {
display: flex;
justify-content: flex-end;
}
.archive-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 8px;
}