Skip to content

Commit

Permalink
Melon Head: Spike dashboard page (#95)
Browse files Browse the repository at this point in the history
Implements basic dashboard (as homepage) and 404 page.
  • Loading branch information
turboMaCk authored and ICTGuerrilla committed Sep 17, 2023
1 parent 24cfd8f commit 3721eb3
Show file tree
Hide file tree
Showing 20 changed files with 314 additions and 47 deletions.
23 changes: 16 additions & 7 deletions melon-head/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ Web UI client for [Orca](../orca).

## Developing

First make sure configuration file is present in the project.
You can for instance symlink example config or copy it if you need to make some changes
to make it work in your setup:

```
$ ln -s config.example.json config.json
```

We're using npm and npm scripts while working on project.

| command | function |
|------------------|----------------------------|
| `npm install` | Install dependencies |
| `npm build` | Build production assets |
| `npm run format` | Autoformat source code |
| `npm run start` | Run development server |
| `npm run watch` | Run compiler in watch mode |
| command | function |
| ------------------ | ---------------------------- |
| `npm install` | Install dependencies |
| `npm run clean` | Clean parcel cache |
| `npm run build` | Build production assets |
| `npm run format` | Autoformat source code |
| `npm start` | Run development server |
| `npm run watch` | Run compiler in watch mode |


## Nix Build
Expand Down
1 change: 1 addition & 0 deletions melon-head/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "Oeca web clinet",
"scripts": {
"build": "rescript && parcel build src/index.html",
"clean": "rm -rf .parcel-cache",
"start": "parcel serve src/index.html",
"watch": "rescript build -w",
"format": "rescript format -all",
Expand Down
8 changes: 3 additions & 5 deletions melon-head/src/Api.res
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ let mapDecodingError = (res: result<'a, string>): result<'a, error> => {
}
}

type api = {
type t = {
host: string,
keycloak: Keycloak.t,
}

let make = (~config: Config.t, ~keycloak: Keycloak.t): api => {
let make = (~config: Config.t, ~keycloak: Keycloak.t): t => {
{
host: config.apiUrl,
keycloak,
Expand All @@ -62,9 +62,7 @@ let fromRequest = (future, decoder) => {

type webData<'a> = RemoteData.t<'a, error>

let getJson = (api: api, ~path: string, ~decoder: Json.Decode.t<'a>): Future.t<
result<'a, error>,
> => {
let getJson = (api: t, ~path: string, ~decoder: Json.Decode.t<'a>): Future.t<result<'a, error>> => {
Request.make(
~url=api.host ++ path,
~responseType=Json,
Expand Down
10 changes: 8 additions & 2 deletions melon-head/src/App.res
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ module ConfiguredApp = {

let (sessionState: Api.webData<Session.t>, setSessionState) = React.useState(RemoteData.init)

let (isNavOpen, setIsNavOpen) = React.useState(_ => true)
let (isNavOpen, setIsNavOpen) = React.useState(_ => false)
let (isProfileOpen, setIsProfileOpen) = React.useState(_ => false)

let url = RescriptReactRouter.useUrl()

React.useEffect0(() => {
let req = api->Api.getJson(~path="/session/current", ~decoder=Session.Decode.session)
setSessionState(RemoteData.setLoading)
Expand All @@ -28,11 +30,15 @@ module ConfiguredApp = {
}}
<div className={styles["main-container"]}>
<AppHeader
session={sessionState}
toggleNav={_ => setIsNavOpen(v => !v)}
isNavOpen={isNavOpen}
openProfile={_ => setIsProfileOpen(_ => true)}
/>
/* Routing to pages */
{switch url.path {
| list{} => <Dashboard session=sessionState api />
| _ => <PageNotFound />
}}
</div>
{if isProfileOpen {
<Profile
Expand Down
4 changes: 2 additions & 2 deletions melon-head/src/App/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
}

.main-container {
display: grid;
flex: 100% 1 1;
grid-area: main;
grid-template-areas: 'header' 'body' 'footer';
display: flex;
flex-direction: column;
}
1 change: 0 additions & 1 deletion melon-head/src/AppHeader.res
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

@react.component
let make = (
~session: Api.webData<Session.t>,
~toggleNav: JsxEvent.Mouse.t => unit,
~openProfile: JsxEvent.Mouse.t => unit,
~isNavOpen: bool,
Expand Down
9 changes: 8 additions & 1 deletion melon-head/src/AppHeader/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ $divider: 1px solid rgba(white, 0.2);
}

.root {
grid-area: 'header';
background: #211D22;
color: white;

Expand All @@ -38,6 +37,14 @@ $divider: 1px solid rgba(white, 0.2);
font-size: 24px;
font-weight: 600;
margin: 0;
white-space: nowrap;
overflow: hidden;

@media screen and (max-width: 620px) {
& {
font-size: 16px;
}
}
}

.profile-btn {
Expand Down
83 changes: 83 additions & 0 deletions melon-head/src/Dashboard.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
@module external styles: {..} = "./Dashboard/styles.module.scss"

open Belt

module ViewBool = {
@react.component
let make = (~value: bool) => {value ? React.string("yes") : React.string("no")}
}

module RowBasedTable = {
@react.component
let make = (~rows: array<(string, 'a => React.element)>, ~data: Api.webData<'a>, ~title=None) => {
let viewRow = (index, (name, getter)) =>
<tr key={Int.toString(index)}>
<td> {React.string(name)} </td>
<td> {data->RemoteData.unwrap(getter, ~default=React.null)} </td>
</tr>

let viewRows = rows->Array.mapWithIndex(viewRow)->React.array

<div className={styles["row-table-wrapper"]}>
{switch title {
| Some(str) => <h2 className={styles["row-table-title"]}> {React.string(str)} </h2>
| None => React.null
}}
<table className={styles["row-table-table"]}>
<tbody> viewRows </tbody>
</table>
{switch data {
| Loading =>
<div className={styles["row-table-loading"]}>
<Icons.Loading variant=Icons.Dark />
</div>
| Failure(err) =>
<div className={styles["row-table-error"]}>
<h4> {React.string("Error loading data")} </h4>
<pre> {React.string(Api.showError(err))} </pre>
</div>
| _ => React.null
}}
</div>
}
}

@react.component
let make = (~session: Api.webData<Session.t>, ~api: Api.t) => {
open Stats

let (basicStats, setBasicStats) = React.useState(RemoteData.init)

React.useEffect0(() => {
let req = api->Api.getJson(~path="/stats/basic", ~decoder=Stats.Decode.basic)
setBasicStats(RemoteData.setLoading)

req->Future.get(res => {
setBasicStats(_ => RemoteData.fromResult(res))
})

Some(() => Future.cancel(req))
})

let applicationsRows = [
("Waiting for email verification", ({unverified}) => React.string(unverified->Int.toString)),
("In processing", ({processing}) => React.string(processing->Int.toString)),
("Accpted", ({accepted}) => React.string(accepted->Int.toString)),
("Rejected", ({rejected}) => React.string(rejected->Int.toString)),
]

let permissionsRows = [
(
"List all applications in the system",
session => {<ViewBool value={Session.hasRole(session, ~role=Session.ListApplications)} />},
),
]

<Page>
<Page.Title> {React.string("Dashboard")} </Page.Title>
<div className={styles["stats-grid"]}>
<RowBasedTable rows=applicationsRows data=basicStats title=Some("Current Applications") />
<RowBasedTable rows=permissionsRows data=session title=Some("Your Permissions/Roles") />
</div>
</Page>
}
65 changes: 65 additions & 0 deletions melon-head/src/Dashboard/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.stats-grid {
display: grid;
grid-template-columns: 25% 25% 25% 25%;
gap: 12px;
}

.row-table-wrapper {
position: relative;
border-radius: 4px;
border: 4px solid rgba(#211D22, .4);
border-radius: 5px;
overflow: hidden;
}

.row-table-title {
background:rgba(#211D22, .4);
font-size: 16px;
line-height: 1.5em;
font-weight: 600;
padding: 0 12px 4px;
color: white;
}

.row-table-table {
width: 100%;

tr:first-child td {
border-top: none;
}

td {
padding: 12px;
border-top: 1px solid rgba(#211D22, .4);

&:first-child {
font-weight: 600;
}

&:last-child {
text-align: right;
}
}
}

@mixin overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}

.row-table-loading {
@include overlay;
background: rgba(white, 0.6);
display: flex;
justify-content: center;
align-items: center;
}

.row-table-error {
@include overlay;
background: #EF562E;
padding: 12px;
}
16 changes: 10 additions & 6 deletions melon-head/src/Icons.res
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
@module external melonSvg: string = "/static/svg/melon.svg"
@module external profileImg: string = "/static/png/profile.png"

type variant =
| Dark
| Light

module Hamburger = {
@react.component
let make = (~isOpen: bool) => {
Expand All @@ -14,10 +18,6 @@ module Hamburger = {
}

module Profile = {
type variant =
| Dark
| Light

@react.component
let make = (~variant: variant=Dark) => {
let cssClass = switch variant {
Expand All @@ -40,8 +40,12 @@ module Close = {

module Loading = {
@react.component
let make = () => {
<div className={styles["loading"]}>
let make = (~variant: variant=Light) => {
let className = switch variant {
| Light => styles["loading-light"]
| Dark => styles["loading-dark"]
}
<div className>
<div />
<div />
<div />
Expand Down
48 changes: 30 additions & 18 deletions melon-head/src/Icons/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
}
}

.loading {
@mixin loading($color) {
display: inline-block;
position: relative;
width: 80px;
Expand All @@ -102,24 +102,28 @@
width: 13px;
height: 13px;
border-radius: 50%;
background: #fff;
background: $color;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
div:nth-child(1) {
left: 8px;
animation: lds-ellipsis1 0.6s infinite;
}
div:nth-child(2) {
left: 8px;
animation: lds-ellipsis2 0.6s infinite;
}
div:nth-child(3) {
left: 32px;
animation: lds-ellipsis2 0.6s infinite;
}
div:nth-child(4) {
left: 56px;
animation: lds-ellipsis3 0.6s infinite;

&:nth-child(1) {
left: 8px;
animation: lds-ellipsis1 0.6s infinite;
}

&:nth-child(2) {
left: 8px;
animation: lds-ellipsis2 0.6s infinite;
}

&:nth-child(3) {
left: 32px;
animation: lds-ellipsis2 0.6s infinite;
}

&:nth-child(4) {
left: 56px;
animation: lds-ellipsis3 0.6s infinite;
}
}

@keyframes lds-ellipsis1 {
Expand Down Expand Up @@ -150,3 +154,11 @@
}
}
}

.loading-light {
@include loading(white);
}

.loading-dark {
@include loading(#211D22);
}
Loading

0 comments on commit 3721eb3

Please sign in to comment.