diff --git a/src/features/ModelView/ModelImageView/ModelImageView.styled.tsx b/src/features/ModelView/ModelImageView/ModelImageView.styled.tsx new file mode 100644 index 00000000..9c94d509 --- /dev/null +++ b/src/features/ModelView/ModelImageView/ModelImageView.styled.tsx @@ -0,0 +1,34 @@ +import styled from 'styled-components' +import { spacings } from '../../../tokens/spacings' + +export const ImageWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + border-style: solid; + border-width: 1px; + border-color: #cdcdcd; + + max-width: 100%; + height: 100%; + + > h5 { + font-weight: normal; + margin: 0; + padding: ${spacings.SMALL}; + } + + > .metadata-image { + max-width: 100%; + padding: ${spacings.SMALL}; + } + + @media (max-width: 1350px) { + max-width: 100%; + + > .metadata-image { + max-width: 100%; + } + } +` diff --git a/src/features/ModelView/ModelImageView/ModelImageView.tsx b/src/features/ModelView/ModelImageView/ModelImageView.tsx new file mode 100644 index 00000000..d3ccbee9 --- /dev/null +++ b/src/features/ModelView/ModelImageView/ModelImageView.tsx @@ -0,0 +1,13 @@ +import Img from '../image.png' +import * as Styled from './ModelImageView.styled' + +export const ModelImageView = () => { + // TODO + // Load image and text from results + return ( + + cat +
Image Text
+
+ ) +} diff --git a/src/features/ModelView/ModelMetadataView/ModelMetadataView.tsx b/src/features/ModelView/ModelMetadataView/ModelMetadataView.tsx new file mode 100644 index 00000000..f18248e9 --- /dev/null +++ b/src/features/ModelView/ModelMetadataView/ModelMetadataView.tsx @@ -0,0 +1,68 @@ +import { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import { useAnalogueModels } from '../../../hooks/useAnalogueModels' + +import { Button, Table, Typography } from '@equinor/eds-core-react' +import { ModelType } from '../../../pages/ModelPages/Model/Model' + +export const ModelMetadataView = () => { + const [model, setModel] = useState() + const { id } = useParams() + const { fetchModel } = useAnalogueModels() + + useEffect(() => { + async function getModel() { + return await fetchModel({ + params: { path: { id: id ?? '' } }, + }) + .then((response) => response?.data) + .then((model) => model && setModel(model as ModelType)) + } + if (!model) { + getModel() + } + }, [id, model, fetchModel]) + + if (!model) return

Loading ...

+ + // TODO + // Map rows to model data + + return ( +
+ Description and metadata + {model.description && ( +

+ {model.description} +
+

+ )} +
+ + + + Field + **Tor** + + + Fomation + **Tor** + + + Analouge + **Tor** + + + Zone + **Tor** + + +
+
+ + +
+ ) +} diff --git a/src/features/ModelView/ModelNameFrame/ModelNameFrame.styled.tsx b/src/features/ModelView/ModelNameFrame/ModelNameFrame.styled.tsx new file mode 100644 index 00000000..89f68936 --- /dev/null +++ b/src/features/ModelView/ModelNameFrame/ModelNameFrame.styled.tsx @@ -0,0 +1,14 @@ +import styled from 'styled-components' +import { spacings } from '../../../tokens/spacings' + +export const NameFrame = styled.div` + width: 100%; + padding: ${spacings.LARGE} 0; + background-color: rgb(247, 247, 247, 1); + + > h1 { + margin: 0; + padding: 0 ${spacings.X_LARGE}; + font-weight: normal; + } +` diff --git a/src/features/ModelView/ModelNameFrame/ModelNameFrame.tsx b/src/features/ModelView/ModelNameFrame/ModelNameFrame.tsx new file mode 100644 index 00000000..952e8669 --- /dev/null +++ b/src/features/ModelView/ModelNameFrame/ModelNameFrame.tsx @@ -0,0 +1,10 @@ +import { ModelType } from '../../../pages/ModelPages/Model/Model' +import * as Styled from './ModelNameFrame.styled' + +export const ModelNameFrame = ({ model }: { model: ModelType }) => { + return ( + +

{model.name}

+
+ ) +} diff --git a/src/features/ModelView/ModelNavigationBar/ModelNavigationBar.tsx b/src/features/ModelView/ModelNavigationBar/ModelNavigationBar.tsx new file mode 100644 index 00000000..53e332ee --- /dev/null +++ b/src/features/ModelView/ModelNavigationBar/ModelNavigationBar.tsx @@ -0,0 +1,48 @@ +import { SideBar, SidebarLinkProps } from '@equinor/eds-core-react' +import { + approve, + format_list_bulleted as formatListBullet, + settings, +} from '@equinor/eds-icons' +import { useLocation } from 'react-router-dom' + +export const ModelNavigationBar = () => { + const menuItems: SidebarLinkProps[] = [ + { + label: 'Details', + icon: formatListBullet, + href: 'details', + active: false, + }, + { + label: 'Compute', + icon: settings, + href: 'compute', + active: false, + }, + { + label: 'Results', + icon: approve, + href: 'results', + active: false, + }, + ] + + const location = useLocation() + const tab = location.pathname.split('/') + + return ( + + + + {menuItems.map((m) => ( + + ))} + + + ) +} diff --git a/src/features/ModelView/ModelSourceView/ModelSourceView.tsx b/src/features/ModelView/ModelSourceView/ModelSourceView.tsx new file mode 100644 index 00000000..5d856b84 --- /dev/null +++ b/src/features/ModelView/ModelSourceView/ModelSourceView.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import { useAnalogueModels } from '../../../hooks/useAnalogueModels' + +import { Table, Typography } from '@equinor/eds-core-react' +import { ModelType } from '../../../pages/ModelPages/Model/Model' + +export const ModelSourceView = () => { + const [model, setModel] = useState() + const { id } = useParams() + const { fetchModel } = useAnalogueModels() + + useEffect(() => { + async function getModel() { + return await fetchModel({ + params: { path: { id: id ?? '' } }, + }) + .then((response) => response?.data) + .then((model) => model && setModel(model as ModelType)) + } + if (!model) { + getModel() + } + }, [id, model, fetchModel]) + + if (!model) return

Loading ...

+ + // TODO + // Add uploaded by and upload time + + return ( +
+ Source +

Uploaded by ABCD@equinor.com on Sep 13, 2023

+ + + + + Model input files + + Size + + + + {model.fileUploads?.length === undefined || + model.fileUploads?.length > 0 ? ( + model.fileUploads?.map((file: any) => ( + + + {file.originalFileName} + + **Size** + + )) + ) : ( + + No files uploaded + -- + + )} + +
+
+ ) +} diff --git a/src/features/ModelView/ModelView.styled.tsx b/src/features/ModelView/ModelView.styled.tsx new file mode 100644 index 00000000..44156242 --- /dev/null +++ b/src/features/ModelView/ModelView.styled.tsx @@ -0,0 +1,73 @@ +import styled from 'styled-components' +import { spacings } from '../../tokens/spacings' + +export const MetadataWrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + height: 100%; + width: 100%; + padding: ${spacings.X_LARGE}; + row-gap: ${spacings.XXX_LARGE}; + column-gap: ${spacings.X_LARGE}; + overflow-y: scroll; + + @media (max-width: 1350px) { + flex-direction: column; + } +` +export const InnerMetadataWrapper = styled.div` + display: flex; + flex-direction: column; + row-gap: ${spacings.XXX_LARGE}; + + .edit-metadata-button { + margin-top: ${spacings.MEDIUM_SMALL}; + } + + .metadata-view { + width: 100%; + min-width: 256px; + + > div { + > table { + width: 480px; + + > tbody { + > tr { + > .table-row { + } + + > .table-first-col { + width: 80px; + padding-right: ${spacings.X_LARGE}; + } + } + } + } + } + } + + .source-view { + width: 100%; + min-width: 256px; + max-width: 640px; + + > table { + width: 480px; + + > thead, + tbody { + > tr { + > .table-row { + } + + > .table-first-col { + width: 376px; + padding-right: ${spacings.X_LARGE}; + } + } + } + } + } +` diff --git a/src/features/ModelView/ModelView.tsx b/src/features/ModelView/ModelView.tsx new file mode 100644 index 00000000..3647b40c --- /dev/null +++ b/src/features/ModelView/ModelView.tsx @@ -0,0 +1,16 @@ +import { ModelImageView } from '../../features/ModelView/ModelImageView/ModelImageView' +import { ModelMetadataView } from '../../features/ModelView/ModelMetadataView/ModelMetadataView' +import { ModelSourceView } from '../../features/ModelView/ModelSourceView/ModelSourceView' +import * as Styled from './ModelView.styled' + +export const ModelView = () => { + return ( + + + + + + + + ) +} diff --git a/src/features/ModelView/image.png b/src/features/ModelView/image.png new file mode 100644 index 00000000..d664bf8b Binary files /dev/null and b/src/features/ModelView/image.png differ diff --git a/src/hooks/useAnalogueModels.tsx b/src/hooks/useAnalogueModels.tsx index ce4047d1..1c748bff 100644 --- a/src/hooks/useAnalogueModels.tsx +++ b/src/hooks/useAnalogueModels.tsx @@ -16,7 +16,8 @@ type UseQueryOptions = ParamsOption & } const ANALOGUEMODELS_KEY = '/api/analogue-models' -const NC_FILE_KEY = '/api/analogue-models/{id}/netcdf-models' +const ANALOGUEMODEL_KEY = '/api/analogue-models/{id}' +const NC_FILE_KEY = '/api/analogue-models/{id}/input-models' export function useAnalogueModels() { const apiClient = useApiClient() @@ -30,6 +31,16 @@ export function useAnalogueModels() { return data } + async function fetchModel({ + params, + }: UseQueryOptions) { + const { data } = await apiClient.GET(ANALOGUEMODEL_KEY, { + params, + headers: new Headers({ Authorization: `Bearer ${token}` }), + }) + return data + } + async function createModel({ body, }: UseQueryOptions) { @@ -59,5 +70,5 @@ export function useAnalogueModels() { const models = useQuery([ANALOGUEMODELS_KEY, token], fetchModels) - return { fetchModels, createModel, models, uploadNCFile } + return { fetchModels, fetchModel, createModel, models, uploadNCFile } } diff --git a/src/pages/Browse/Browse.tsx b/src/pages/Browse/Browse.tsx index e2c69c75..1d734aa7 100644 --- a/src/pages/Browse/Browse.tsx +++ b/src/pages/Browse/Browse.tsx @@ -51,6 +51,11 @@ export const Browse = () => { return ( <> +
+ +
{ + return ( + <> +

Options to compute models will soon be possible here!

+ + ) +} diff --git a/src/pages/ModelPages/Model/Model.styled.tsx b/src/pages/ModelPages/Model/Model.styled.tsx new file mode 100644 index 00000000..8c6a0f48 --- /dev/null +++ b/src/pages/ModelPages/Model/Model.styled.tsx @@ -0,0 +1,14 @@ +import styled from 'styled-components' + +export const Wrapper = styled.div` + display: flex; + flex: auto; + flex-direction: row; + position: relative; + width: 100%; +` + +export const SidebarWrapper = styled.div` + heigth: 100%; + max-width: 256px; +` diff --git a/src/pages/ModelPages/Model/Model.tsx b/src/pages/ModelPages/Model/Model.tsx new file mode 100644 index 00000000..7b30b324 --- /dev/null +++ b/src/pages/ModelPages/Model/Model.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from 'react' +import { Outlet, useParams } from 'react-router-dom' +import { useAnalogueModels } from '../../../hooks/useAnalogueModels' +import { components } from '../../../models/schema' +import * as Styled from './Model.styled' + +import { ModelNameFrame } from '../../../features/ModelView/ModelNameFrame/ModelNameFrame' +import { ModelNavigationBar } from '../../../features/ModelView/ModelNavigationBar/ModelNavigationBar' + +export type ModelType = Partial< + components['schemas']['GetAnalogueModelQueryResponse']['data'] +> + +export const Model = () => { + const [model, setModel] = useState() + const { id } = useParams() + const { fetchModel } = useAnalogueModels() + + useEffect(() => { + async function getModel() { + return await fetchModel({ + params: { path: { id: id ?? '' } }, + }).then((response) => response?.data) + } + if (!model) { + getModel().then((model) => model && setModel(model as ModelType)) + } + }, [id, model, fetchModel]) + + return ( + <> + {model && } + + + + + + + + ) +} diff --git a/src/pages/ModelPages/Results/Results.tsx b/src/pages/ModelPages/Results/Results.tsx new file mode 100644 index 00000000..9e1a1a0a --- /dev/null +++ b/src/pages/ModelPages/Results/Results.tsx @@ -0,0 +1,7 @@ +export const Results = () => { + return ( + <> +

Results for models will soon be available here!

+ + ) +} diff --git a/src/router.tsx b/src/router.tsx index 76156f4f..b6681935 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,6 +1,10 @@ import { createBrowserRouter, NonIndexRouteObject } from 'react-router-dom' import { App } from './App' +import { ModelView } from './features/ModelView/ModelView' import { Browse } from './pages/Browse/Browse' +import { Compute } from './pages/ModelPages/Compute/Compute' +import { Model } from './pages/ModelPages/Model/Model' +import { Results } from './pages/ModelPages/Results/Results' interface Tab extends Required> { title: string @@ -17,12 +21,32 @@ const tabs: Tab[] = [ { title: 'API', path: 'api', element: }, { title: 'About', path: 'about', element: }, ] +const appRoutes = (tabs as NonIndexRouteObject[]).concat([ + { + path: 'model/:id/', + element: , + children: [ + { + path: 'details', + element: , + }, + { + path: 'compute', + element: , + }, + { + path: 'results', + element: , + }, + ], + }, +]) const router = createBrowserRouter([ { path: '/', element: , - children: tabs, + children: appRoutes, }, { path: '*',