Merge pull request #109 from Tsu-ba-me/issues/4-web-ui-rpm
Add new web UI and its build resourcesmain
commit
57bf83f32d
94 changed files with 9174 additions and 3 deletions
@ -0,0 +1,5 @@ |
|||||||
|
# dependencies |
||||||
|
/node_modules |
||||||
|
|
||||||
|
# next.js |
||||||
|
/.next/ |
@ -0,0 +1,63 @@ |
|||||||
|
{ |
||||||
|
"env": { |
||||||
|
"browser": true, |
||||||
|
"commonjs": true, |
||||||
|
"es6": true, |
||||||
|
"node": true |
||||||
|
}, |
||||||
|
"extends": [ |
||||||
|
"airbnb", |
||||||
|
"eslint:recommended", |
||||||
|
"plugin:@typescript-eslint/recommended", |
||||||
|
"plugin:import/errors", |
||||||
|
"plugin:import/typescript", |
||||||
|
"plugin:import/warnings", |
||||||
|
"plugin:jsx-a11y/recommended", |
||||||
|
"plugin:prettier/recommended", |
||||||
|
"plugin:react/recommended", |
||||||
|
"plugin:react-hooks/recommended", |
||||||
|
"prettier/@typescript-eslint", |
||||||
|
"prettier/react" |
||||||
|
], |
||||||
|
"plugins": [ |
||||||
|
"@typescript-eslint", |
||||||
|
"import", |
||||||
|
"jsx-a11y", |
||||||
|
"prettier", |
||||||
|
"react", |
||||||
|
"react-hooks" |
||||||
|
], |
||||||
|
"parser": "@typescript-eslint/parser", |
||||||
|
"parserOptions": { |
||||||
|
"ecmaFeatures": { |
||||||
|
"jsx": true |
||||||
|
}, |
||||||
|
"ecmaVersion": 2020, |
||||||
|
"sourceType": "module" |
||||||
|
}, |
||||||
|
"rules": { |
||||||
|
"complexity": ["error", 5], |
||||||
|
"import/extensions": [ |
||||||
|
"error", |
||||||
|
"ignorePackages", |
||||||
|
{ |
||||||
|
"js": "never", |
||||||
|
"jsx": "never", |
||||||
|
"ts": "never", |
||||||
|
"tsx": "never" |
||||||
|
} |
||||||
|
], |
||||||
|
// Allow JSX in files with other extensions |
||||||
|
"react/jsx-filename-extension": [1, { "extensions": [".tsx"] }], |
||||||
|
// Use TypeScript's types for component props |
||||||
|
"react/prop-types": "off", |
||||||
|
// Importing React is not required in Next.js |
||||||
|
"react/react-in-jsx-scope": "off", |
||||||
|
|
||||||
|
"camelcase": "off", |
||||||
|
"@typescript-eslint/camelcase": "off" |
||||||
|
}, |
||||||
|
"settings": { |
||||||
|
"react": { "version": "detect" } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
||||||
|
|
||||||
|
# dependencies |
||||||
|
/node_modules |
||||||
|
/.pnp |
||||||
|
.pnp.js |
||||||
|
|
||||||
|
# testing |
||||||
|
/coverage |
||||||
|
|
||||||
|
# next.js |
||||||
|
/.next/ |
||||||
|
/out/ |
||||||
|
|
||||||
|
# production |
||||||
|
/build |
||||||
|
|
||||||
|
# misc |
||||||
|
.DS_Store |
||||||
|
*.pem |
||||||
|
|
||||||
|
# debug |
||||||
|
npm-debug.log* |
||||||
|
yarn-debug.log* |
||||||
|
yarn-error.log* |
||||||
|
|
||||||
|
# local env files |
||||||
|
.env.local |
||||||
|
.env.development.local |
||||||
|
.env.test.local |
||||||
|
.env.production.local |
||||||
|
|
||||||
|
# vercel |
||||||
|
.vercel |
@ -0,0 +1 @@ |
|||||||
|
_ |
@ -0,0 +1,6 @@ |
|||||||
|
#!/bin/sh |
||||||
|
. "$(dirname "$0")/_/husky.sh" |
||||||
|
|
||||||
|
cd striker-ui |
||||||
|
|
||||||
|
npx --no-install commitlint --edit "$1" |
@ -0,0 +1,7 @@ |
|||||||
|
#!/bin/sh |
||||||
|
. "$(dirname "$0")/_/husky.sh" |
||||||
|
|
||||||
|
# Must cd because pwd/cwd is the root of this repository. |
||||||
|
cd striker-ui |
||||||
|
|
||||||
|
npx --no-install lint-staged |
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"*.{js,jsx,ts,tsx}": "npm run lint -- --fix", |
||||||
|
"*.{json,md}": "prettier --write" |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
{ |
||||||
|
"endOfLine": "lf", |
||||||
|
"singleQuote": true, |
||||||
|
"tabWidth": 2, |
||||||
|
"trailingComma": "all" |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
MAINTAINERCLEANFILES = Makefile.in
|
||||||
|
|
||||||
|
# The files listed under EXTRA_DIST is only used for building, they should not
|
||||||
|
# be included in the installation process.
|
||||||
|
#
|
||||||
|
# It is recommended to use the following command to regenerate the file list
|
||||||
|
# as files get added/removed:
|
||||||
|
#
|
||||||
|
# find . \
|
||||||
|
# -not \( -name node_modules -prune \) \ # Ignore ./node_modules directory
|
||||||
|
# -not \( -name *git* -prune \) \ # Ignore git related files
|
||||||
|
# -not \( -name *husky* -prune \) \ # Ignore husky related files
|
||||||
|
# -not \( -name public -prune \) \ # Ignore ./public directory
|
||||||
|
# -not \( -name *[Mm]ake* -prune \) \ # Ignore make related files
|
||||||
|
# -not \( -name *.md -prune \) \ # Ignore all markdown files
|
||||||
|
# -type f \ # Only print file paths
|
||||||
|
# | sed -E 's@^./(.+)$@\1 \\@' # Remove leading "./" and append "\"
|
||||||
|
#
|
||||||
|
EXTRA_DIST = \
|
||||||
|
next.config.js \
|
||||||
|
next-env.d.ts \
|
||||||
|
tsconfig.json \
|
||||||
|
theme/index.ts \
|
||||||
|
styles/globals.css \
|
||||||
|
.lintstagedrc.json \
|
||||||
|
env.development \
|
||||||
|
package.json \
|
||||||
|
lib/extended_date/ExtendedDate.ts \
|
||||||
|
lib/fetchers/putJSON.ts \
|
||||||
|
lib/fetchers/fetchJSON.ts \
|
||||||
|
lib/fetchers/periodicFetch.ts \
|
||||||
|
lib/sanitizers/hostsSanitizer.ts \
|
||||||
|
lib/consts/ICONS.ts \
|
||||||
|
lib/consts/DEFAULT_THEME.ts \
|
||||||
|
lib/consts/SERVERS.ts \
|
||||||
|
lib/consts/ANVILS.ts \
|
||||||
|
lib/consts/API_BASE_URL.ts \
|
||||||
|
lib/consts/IS_DEV_ENV.ts \
|
||||||
|
lib/consts/NODES.ts \
|
||||||
|
.prettierrc.json \
|
||||||
|
types/AnvilServers.d.ts \
|
||||||
|
types/GetAllAnvilResponse.d.ts \
|
||||||
|
types/AnvilMemory.d.ts \
|
||||||
|
types/FetchResponse.d.ts \
|
||||||
|
types/AnvilSharedStorage.d.ts \
|
||||||
|
types/AnvilNetwork.d.ts \
|
||||||
|
types/AnvilTypes.d.ts \
|
||||||
|
types/GetResponse.d.ts \
|
||||||
|
types/AnvilNodeStatus.d.ts \
|
||||||
|
types/AnvilList.d.ts \
|
||||||
|
types/AnvilCPU.d.ts \
|
||||||
|
types/GetOneAnvilResponse.d.ts \
|
||||||
|
types/AnvilSet.d.ts \
|
||||||
|
types/AnvilStatus.d.ts \
|
||||||
|
types/AnvilReplicatedStorage.d.ts \
|
||||||
|
types/AnvilFileSystems.d.ts \
|
||||||
|
types/NodeSet.d.ts \
|
||||||
|
components/Memory.tsx \
|
||||||
|
components/Decorator.tsx \
|
||||||
|
components/Hosts/index.tsx \
|
||||||
|
components/Hosts/AnvilHost.tsx \
|
||||||
|
components/Spinner.tsx \
|
||||||
|
components/AnvilDrawer.tsx \
|
||||||
|
components/Text/HeaderText.tsx \
|
||||||
|
components/Text/index.tsx \
|
||||||
|
components/Text/BodyText.tsx \
|
||||||
|
components/FileSystem/index.tsx \
|
||||||
|
components/FileSystem/FileSystemsHost.tsx \
|
||||||
|
components/FileSystem/FileSystems.tsx \
|
||||||
|
components/Anvils/index.tsx \
|
||||||
|
components/Anvils/sortAnvils.ts \
|
||||||
|
components/Anvils/Anvil.tsx \
|
||||||
|
components/Anvils/AnvilList.tsx \
|
||||||
|
components/Anvils/SelectedAnvil.tsx \
|
||||||
|
components/Panels/index.tsx \
|
||||||
|
components/Panels/InnerPanel.tsx \
|
||||||
|
components/Panels/PanelHeader.tsx \
|
||||||
|
components/Panels/Panel.tsx \
|
||||||
|
components/Servers.tsx \
|
||||||
|
components/Storage.tsx \
|
||||||
|
components/AnvilContext.tsx \
|
||||||
|
components/SharedStorage/SharedStorage.tsx \
|
||||||
|
components/SharedStorage/index.tsx \
|
||||||
|
components/SharedStorage/SharedStorageHost.tsx \
|
||||||
|
components/Header.tsx \
|
||||||
|
components/Network/index.tsx \
|
||||||
|
components/Network/Network.tsx \
|
||||||
|
components/Network/processNetwork.ts \
|
||||||
|
components/CPU.tsx \
|
||||||
|
components/Bars/index.tsx \
|
||||||
|
components/Bars/ProgressBar.tsx \
|
||||||
|
components/Bars/AllocationBar.tsx \
|
||||||
|
pages/index.tsx \
|
||||||
|
pages/_app.tsx \
|
||||||
|
pages/_document.tsx \
|
||||||
|
.eslintrc.json \
|
||||||
|
commitlint.config.js \
|
||||||
|
.eslintignore \
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
htmldir = $(localstatedir)/www/html
|
||||||
|
outdir = out
|
||||||
|
|
||||||
|
# Trigger build target on make call without parameters.
|
||||||
|
all: out |
||||||
|
|
||||||
|
# Note: the input file to the generate endpoint prefix step must exist in
|
||||||
|
# EXTRA_DIST.
|
||||||
|
#
|
||||||
|
out: |
||||||
|
-@echo "Copying required build files to build (current) directory."
|
||||||
|
rsync -av --exclude "[Mm]ake*" $(srcdir)/ ./
|
||||||
|
-@echo "Allow tools to write to files in the build directory."
|
||||||
|
chmod -R +w .
|
||||||
|
-@echo "Install node modules (dependencies) prior to building."
|
||||||
|
npm install --no-package-lock --ignore-scripts
|
||||||
|
-@echo "Generate endpoint prefix."
|
||||||
|
sed 's@=.*@=/cgi-bin@' <env.development >.env.local
|
||||||
|
-@echo "Build front-end project."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
install-data-hook: |
||||||
|
-@echo "Place build output files."
|
||||||
|
cp -r $(outdir)/index.html $(outdir)/_next $(DESTDIR)/$(htmldir)/
|
||||||
|
-@echo "Create symlink to images to enable borrowing icon etc. without duplicating."
|
||||||
|
(cd $(DESTDIR)/$(htmldir); $(LN_S) skins/alteeve/images pngs)
|
||||||
|
|
||||||
|
uninstall-hook: |
||||||
|
-@echo "Remove all installed files of the current module."
|
||||||
|
(cd $(DESTDIR)/$(htmldir); rm -rf index.html _next pngs)
|
||||||
|
|
||||||
|
clean-local: |
||||||
|
-@echo "Clean up build output files."
|
||||||
|
test $(srcdir) == . && rm -rf $(outdir) .next || find . -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||||
|
|
||||||
|
distclean-local: clean-local |
@ -0,0 +1,34 @@ |
|||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). |
||||||
|
|
||||||
|
## Getting Started |
||||||
|
|
||||||
|
First, run the development server: |
||||||
|
|
||||||
|
```bash |
||||||
|
npm run dev |
||||||
|
# or |
||||||
|
yarn dev |
||||||
|
``` |
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. |
||||||
|
|
||||||
|
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. |
||||||
|
|
||||||
|
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. |
||||||
|
|
||||||
|
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. |
||||||
|
|
||||||
|
## Learn More |
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources: |
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. |
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. |
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! |
||||||
|
|
||||||
|
## Deploy on Vercel |
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. |
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. |
@ -0,0 +1,3 @@ |
|||||||
|
module.exports = { |
||||||
|
extends: ['@commitlint/config-conventional'], |
||||||
|
}; |
@ -0,0 +1,29 @@ |
|||||||
|
import { createContext, useState, ReactNode } from 'react'; |
||||||
|
|
||||||
|
interface AnvilContextType { |
||||||
|
uuid: string; |
||||||
|
setAnvilUuid: (uuid: string) => void; |
||||||
|
} |
||||||
|
|
||||||
|
const AnvilContextDefault: AnvilContextType = { |
||||||
|
uuid: '', |
||||||
|
setAnvilUuid: () => null, |
||||||
|
}; |
||||||
|
|
||||||
|
const AnvilContext = createContext<AnvilContextType>(AnvilContextDefault); |
||||||
|
|
||||||
|
const AnvilProvider = ({ children }: { children: ReactNode }): JSX.Element => { |
||||||
|
const [uuid, setUuid] = useState<string>(''); |
||||||
|
const setAnvilUuid = (anvilUuid: string): void => { |
||||||
|
setUuid(anvilUuid); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<AnvilContext.Provider value={{ uuid, setAnvilUuid }}> |
||||||
|
{children} |
||||||
|
</AnvilContext.Provider> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default AnvilProvider; |
||||||
|
export { AnvilContext }; |
@ -0,0 +1,79 @@ |
|||||||
|
import { Divider, Drawer, List, ListItem, Box } from '@material-ui/core'; |
||||||
|
import { makeStyles, createStyles } from '@material-ui/core/styles'; |
||||||
|
import { Dispatch, SetStateAction } from 'react'; |
||||||
|
import { BodyText, HeaderText } from './Text'; |
||||||
|
import { ICONS, ICON_SIZE } from '../lib/consts/ICONS'; |
||||||
|
import { DIVIDER } from '../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
interface DrawerProps { |
||||||
|
open: boolean; |
||||||
|
setOpen: Dispatch<SetStateAction<boolean>>; |
||||||
|
} |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => |
||||||
|
createStyles({ |
||||||
|
list: { |
||||||
|
width: '200px', |
||||||
|
}, |
||||||
|
divider: { |
||||||
|
background: DIVIDER, |
||||||
|
}, |
||||||
|
text: { |
||||||
|
paddingTop: '.5em', |
||||||
|
paddingLeft: '1.5em', |
||||||
|
}, |
||||||
|
}), |
||||||
|
); |
||||||
|
|
||||||
|
const AnvilDrawer = ({ open, setOpen }: DrawerProps): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Drawer |
||||||
|
BackdropProps={{ invisible: true }} |
||||||
|
anchor="left" |
||||||
|
open={open} |
||||||
|
onClose={() => setOpen(!open)} |
||||||
|
> |
||||||
|
<div role="presentation"> |
||||||
|
<List className={classes.list}> |
||||||
|
<ListItem button> |
||||||
|
<HeaderText text="Admin" /> |
||||||
|
</ListItem> |
||||||
|
<Divider className={classes.divider} /> |
||||||
|
{ICONS.map( |
||||||
|
(icon): JSX.Element => ( |
||||||
|
<ListItem |
||||||
|
button |
||||||
|
key={icon.image} |
||||||
|
component="a" |
||||||
|
href={ |
||||||
|
icon.uri.search(/^https?:/) !== -1 |
||||||
|
? icon.uri |
||||||
|
: `${process.env.NEXT_PUBLIC_API_URL}${icon.uri}` |
||||||
|
} |
||||||
|
> |
||||||
|
<Box display="flex" flexDirection="row" width="100%"> |
||||||
|
<Box> |
||||||
|
<img |
||||||
|
alt="" |
||||||
|
key="icon" |
||||||
|
src={icon.image} |
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...ICON_SIZE} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box flexGrow={1} className={classes.text}> |
||||||
|
<BodyText text={icon.text} /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</ListItem> |
||||||
|
), |
||||||
|
)} |
||||||
|
</List> |
||||||
|
</div> |
||||||
|
</Drawer> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default AnvilDrawer; |
@ -0,0 +1,15 @@ |
|||||||
|
import { BodyText } from '../Text'; |
||||||
|
import anvilState from '../../lib/consts/ANVILS'; |
||||||
|
|
||||||
|
const Anvil = ({ anvil }: { anvil: AnvilListItem }): JSX.Element => { |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<BodyText text={anvil.anvil_name} /> |
||||||
|
<BodyText |
||||||
|
text={anvilState.get(anvil.anvil_state) || 'State unavailable'} |
||||||
|
/> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Anvil; |
@ -0,0 +1,83 @@ |
|||||||
|
import { useContext, useEffect } from 'react'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { List, Box, Divider, ListItem } from '@material-ui/core'; |
||||||
|
import { HOVER, DIVIDER } from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
import Anvil from './Anvil'; |
||||||
|
import { AnvilContext } from '../AnvilContext'; |
||||||
|
import sortAnvils from './sortAnvils'; |
||||||
|
import Decorator, { Colours } from '../Decorator'; |
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({ |
||||||
|
root: { |
||||||
|
width: '100%', |
||||||
|
overflow: 'auto', |
||||||
|
height: '30vh', |
||||||
|
[theme.breakpoints.down('md')]: { |
||||||
|
height: '100%', |
||||||
|
overflow: 'hidden', |
||||||
|
}, |
||||||
|
}, |
||||||
|
divider: { |
||||||
|
background: DIVIDER, |
||||||
|
}, |
||||||
|
button: { |
||||||
|
'&:hover': { |
||||||
|
backgroundColor: HOVER, |
||||||
|
}, |
||||||
|
paddingLeft: 0, |
||||||
|
}, |
||||||
|
anvil: { |
||||||
|
paddingLeft: 0, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const selectDecorator = (state: string): Colours => { |
||||||
|
switch (state) { |
||||||
|
case 'optimal': |
||||||
|
return 'ok'; |
||||||
|
case 'not_ready': |
||||||
|
return 'warning'; |
||||||
|
case 'degraded': |
||||||
|
return 'error'; |
||||||
|
default: |
||||||
|
return 'off'; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const AnvilList = ({ list }: { list: AnvilListItem[] }): JSX.Element => { |
||||||
|
const { uuid, setAnvilUuid } = useContext(AnvilContext); |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (uuid === '') setAnvilUuid(sortAnvils(list)[0].anvil_uuid); |
||||||
|
}, [uuid, list, setAnvilUuid]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<List component="nav" className={classes.root} aria-label="mailbox folders"> |
||||||
|
{sortAnvils(list).map((anvil) => { |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Divider className={classes.divider} /> |
||||||
|
<ListItem |
||||||
|
button |
||||||
|
className={classes.button} |
||||||
|
key={anvil.anvil_uuid} |
||||||
|
onClick={() => setAnvilUuid(anvil.anvil_uuid)} |
||||||
|
> |
||||||
|
<Box display="flex" flexDirection="row" width="100%"> |
||||||
|
<Box p={1}> |
||||||
|
<Decorator colour={selectDecorator(anvil.anvil_state)} /> |
||||||
|
</Box> |
||||||
|
<Box p={1} flexGrow={1} className={classes.anvil}> |
||||||
|
<Anvil anvil={anvil} /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</ListItem> |
||||||
|
</> |
||||||
|
); |
||||||
|
})} |
||||||
|
</List> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default AnvilList; |
@ -0,0 +1,83 @@ |
|||||||
|
import { useContext } from 'react'; |
||||||
|
import { Switch, Box } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { HeaderText } from '../Text'; |
||||||
|
import { SELECTED_ANVIL } from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
import anvilState from '../../lib/consts/ANVILS'; |
||||||
|
import { AnvilContext } from '../AnvilContext'; |
||||||
|
import Decorator, { Colours } from '../Decorator'; |
||||||
|
import putJSON from '../../lib/fetchers/putJSON'; |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
root: { |
||||||
|
width: '100%', |
||||||
|
'&:hover $child': { |
||||||
|
backgroundColor: SELECTED_ANVIL, |
||||||
|
}, |
||||||
|
}, |
||||||
|
anvilName: { |
||||||
|
paddingLeft: 0, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const selectDecorator = (state: string): Colours => { |
||||||
|
switch (state) { |
||||||
|
case 'optimal': |
||||||
|
return 'ok'; |
||||||
|
case 'not_ready': |
||||||
|
return 'warning'; |
||||||
|
case 'degraded': |
||||||
|
return 'error'; |
||||||
|
default: |
||||||
|
return 'error'; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const isAnvilOn = (anvil: AnvilListItem): boolean => |
||||||
|
!( |
||||||
|
anvil.hosts.findIndex( |
||||||
|
({ state }: AnvilStatusHost) => state !== 'offline', |
||||||
|
) === -1 |
||||||
|
); |
||||||
|
|
||||||
|
const SelectedAnvil = ({ list }: { list: AnvilListItem[] }): JSX.Element => { |
||||||
|
const { uuid } = useContext(AnvilContext); |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
const index = list.findIndex( |
||||||
|
(anvil: AnvilListItem) => anvil.anvil_uuid === uuid, |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box display="flex" flexDirection="row" width="100%"> |
||||||
|
{uuid !== '' && ( |
||||||
|
<> |
||||||
|
<Box p={1}> |
||||||
|
<Decorator colour={selectDecorator(list[index].anvil_state)} /> |
||||||
|
</Box> |
||||||
|
<Box p={1} flexGrow={1} className={classes.anvilName}> |
||||||
|
<HeaderText text={list[index].anvil_name} /> |
||||||
|
<HeaderText |
||||||
|
text={ |
||||||
|
anvilState.get(list[index].anvil_state) || 'State unavailable' |
||||||
|
} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box p={1}> |
||||||
|
<Switch |
||||||
|
checked={isAnvilOn(list[index])} |
||||||
|
onChange={() => |
||||||
|
putJSON('/set_power', { |
||||||
|
anvil_uuid: list[index].anvil_uuid, |
||||||
|
is_on: !isAnvilOn(list[index]), |
||||||
|
}) |
||||||
|
} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default SelectedAnvil; |
@ -0,0 +1,28 @@ |
|||||||
|
import { Panel } from '../Panels'; |
||||||
|
import PeriodicFetch from '../../lib/fetchers/periodicFetch'; |
||||||
|
import SelectedAnvil from './SelectedAnvil'; |
||||||
|
import AnvilList from './AnvilList'; |
||||||
|
|
||||||
|
import sortAnvils from './sortAnvils'; |
||||||
|
|
||||||
|
const Anvils = ({ list }: { list: AnvilList | undefined }): JSX.Element => { |
||||||
|
const anvils: AnvilListItem[] = []; |
||||||
|
|
||||||
|
list?.anvils.forEach((anvil: AnvilListItem) => { |
||||||
|
const { data } = PeriodicFetch<AnvilStatus>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_status?anvil_uuid=${anvil.anvil_uuid}`, |
||||||
|
); |
||||||
|
anvils.push({ |
||||||
|
...anvil, |
||||||
|
...data, |
||||||
|
}); |
||||||
|
}); |
||||||
|
return ( |
||||||
|
<Panel> |
||||||
|
<SelectedAnvil list={anvils} /> |
||||||
|
<AnvilList list={sortAnvils(anvils)} /> |
||||||
|
</Panel> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Anvils; |
@ -0,0 +1,14 @@ |
|||||||
|
const sortAnvils = (unsortedList: AnvilListItem[]): AnvilListItem[] => { |
||||||
|
const optimal: AnvilListItem[] = []; |
||||||
|
const notReady: AnvilListItem[] = []; |
||||||
|
const degraded: AnvilListItem[] = []; |
||||||
|
|
||||||
|
unsortedList.forEach((anvil) => { |
||||||
|
if (anvil.anvil_state === 'optimal') optimal.push(anvil); |
||||||
|
else if (anvil.anvil_state === 'not_ready') notReady.push(anvil); |
||||||
|
else degraded.push(anvil); |
||||||
|
}); |
||||||
|
return [...degraded, ...notReady, ...optimal]; |
||||||
|
}; |
||||||
|
|
||||||
|
export default sortAnvils; |
@ -0,0 +1,61 @@ |
|||||||
|
import { makeStyles, withStyles } from '@material-ui/core/styles'; |
||||||
|
import { LinearProgress } from '@material-ui/core'; |
||||||
|
import { |
||||||
|
PURPLE, |
||||||
|
RED, |
||||||
|
BLUE, |
||||||
|
PANEL_BACKGROUND, |
||||||
|
BORDER_RADIUS, |
||||||
|
} from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
const breakpointWarning = 70; |
||||||
|
const breakpointAlert = 90; |
||||||
|
|
||||||
|
const BorderLinearProgress = withStyles({ |
||||||
|
root: { |
||||||
|
height: '1em', |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
}, |
||||||
|
colorPrimary: { |
||||||
|
backgroundColor: PANEL_BACKGROUND, |
||||||
|
}, |
||||||
|
bar: { |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
}, |
||||||
|
})(LinearProgress); |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
barOk: { |
||||||
|
backgroundColor: BLUE, |
||||||
|
}, |
||||||
|
barWarning: { |
||||||
|
backgroundColor: PURPLE, |
||||||
|
}, |
||||||
|
barAlert: { |
||||||
|
backgroundColor: RED, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const AllocationBar = ({ allocated }: { allocated: number }): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<BorderLinearProgress |
||||||
|
classes={{ |
||||||
|
bar: |
||||||
|
/* eslint-disable no-nested-ternary */ |
||||||
|
allocated > breakpointWarning |
||||||
|
? allocated > breakpointAlert |
||||||
|
? classes.barAlert |
||||||
|
: classes.barWarning |
||||||
|
: classes.barOk, |
||||||
|
}} |
||||||
|
variant="determinate" |
||||||
|
value={allocated} |
||||||
|
/> |
||||||
|
<LinearProgress variant="determinate" value={0} /> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default AllocationBar; |
@ -0,0 +1,57 @@ |
|||||||
|
import { makeStyles, withStyles } from '@material-ui/core/styles'; |
||||||
|
import { LinearProgress } from '@material-ui/core'; |
||||||
|
import { |
||||||
|
PURPLE, |
||||||
|
BLUE, |
||||||
|
PANEL_BACKGROUND, |
||||||
|
BORDER_RADIUS, |
||||||
|
} from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
const completed = 100; |
||||||
|
|
||||||
|
const BorderLinearProgress = withStyles({ |
||||||
|
root: { |
||||||
|
height: '1em', |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
}, |
||||||
|
colorPrimary: { |
||||||
|
backgroundColor: PANEL_BACKGROUND, |
||||||
|
}, |
||||||
|
bar: { |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
}, |
||||||
|
})(LinearProgress); |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
barOk: { |
||||||
|
backgroundColor: BLUE, |
||||||
|
}, |
||||||
|
barInProgress: { |
||||||
|
backgroundColor: PURPLE, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const ProgressBar = ({ |
||||||
|
progressPercentage, |
||||||
|
}: { |
||||||
|
progressPercentage: number; |
||||||
|
}): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<BorderLinearProgress |
||||||
|
classes={{ |
||||||
|
bar: |
||||||
|
progressPercentage < completed |
||||||
|
? classes.barInProgress |
||||||
|
: classes.barOk, |
||||||
|
}} |
||||||
|
variant="determinate" |
||||||
|
value={progressPercentage} |
||||||
|
/> |
||||||
|
<LinearProgress variant="determinate" value={0} /> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default ProgressBar; |
@ -0,0 +1,4 @@ |
|||||||
|
import AllocationBar from './AllocationBar'; |
||||||
|
import ProgressBar from './ProgressBar'; |
||||||
|
|
||||||
|
export { AllocationBar, ProgressBar }; |
@ -0,0 +1,39 @@ |
|||||||
|
import { useContext } from 'react'; |
||||||
|
import { Box } from '@material-ui/core'; |
||||||
|
import { Panel } from './Panels'; |
||||||
|
import { HeaderText, BodyText } from './Text'; |
||||||
|
import PeriodicFetch from '../lib/fetchers/periodicFetch'; |
||||||
|
import { AnvilContext } from './AnvilContext'; |
||||||
|
import Spinner from './Spinner'; |
||||||
|
|
||||||
|
const CPU = (): JSX.Element => { |
||||||
|
const { uuid } = useContext(AnvilContext); |
||||||
|
|
||||||
|
const { data, isLoading } = PeriodicFetch<AnvilCPU>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_cpu?anvil_uuid=${uuid}`, |
||||||
|
); |
||||||
|
|
||||||
|
const cpuData = |
||||||
|
isLoading || !data ? { allocated: 0, cores: 0, threads: 0 } : data; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Panel> |
||||||
|
<HeaderText text="CPU" /> |
||||||
|
{!isLoading ? ( |
||||||
|
<> |
||||||
|
<Box display="flex" width="100%"> |
||||||
|
<Box flexGrow={1} style={{ marginLeft: '1em', marginTop: '1em' }}> |
||||||
|
<BodyText text={`Total Cores: ${cpuData.cores}`} /> |
||||||
|
<BodyText text={`Total Threads: ${cpuData.threads}`} /> |
||||||
|
<BodyText text={`Allocated Cores: ${cpuData.allocated}`} /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<Spinner /> |
||||||
|
)} |
||||||
|
</Panel> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default CPU; |
@ -0,0 +1,37 @@ |
|||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { |
||||||
|
BLUE, |
||||||
|
GREY, |
||||||
|
PURPLE, |
||||||
|
RED, |
||||||
|
BORDER_RADIUS, |
||||||
|
} from '../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
export type Colours = 'ok' | 'off' | 'error' | 'warning'; |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
decorator: { |
||||||
|
width: '1.4em', |
||||||
|
height: '100%', |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
}, |
||||||
|
ok: { |
||||||
|
backgroundColor: BLUE, |
||||||
|
}, |
||||||
|
warning: { |
||||||
|
backgroundColor: PURPLE, |
||||||
|
}, |
||||||
|
error: { |
||||||
|
backgroundColor: RED, |
||||||
|
}, |
||||||
|
off: { |
||||||
|
backgroundColor: GREY, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const Decorator = ({ colour }: { colour: Colours }): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
return <div className={`${classes.decorator} ${classes[colour]}`} />; |
||||||
|
}; |
||||||
|
|
||||||
|
export default Decorator; |
@ -0,0 +1,77 @@ |
|||||||
|
import { useContext } from 'react'; |
||||||
|
|
||||||
|
import { Box } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { BodyText, HeaderText } from '../Text'; |
||||||
|
import { Panel, InnerPanel, PanelHeader } from '../Panels'; |
||||||
|
import SharedStorageHost from './FileSystemsHost'; |
||||||
|
import PeriodicFetch from '../../lib/fetchers/periodicFetch'; |
||||||
|
import { AnvilContext } from '../AnvilContext'; |
||||||
|
import Spinner from '../Spinner'; |
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({ |
||||||
|
header: { |
||||||
|
paddingTop: '.1em', |
||||||
|
paddingRight: '.7em', |
||||||
|
}, |
||||||
|
root: { |
||||||
|
overflow: 'auto', |
||||||
|
height: '78vh', |
||||||
|
paddingLeft: '.3em', |
||||||
|
[theme.breakpoints.down('md')]: { |
||||||
|
height: '100%', |
||||||
|
}, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const SharedStorage = ({ anvil }: { anvil: AnvilListItem[] }): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
const { uuid } = useContext(AnvilContext); |
||||||
|
const { data, isLoading } = PeriodicFetch<AnvilSharedStorage>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_shared_storage?anvil_uuid=${uuid}`, |
||||||
|
); |
||||||
|
return ( |
||||||
|
<Panel> |
||||||
|
<HeaderText text="Shared Storage" /> |
||||||
|
{!isLoading ? ( |
||||||
|
<Box className={classes.root}> |
||||||
|
{data?.file_systems && |
||||||
|
data.file_systems.map( |
||||||
|
(fs: AnvilFileSystem): JSX.Element => ( |
||||||
|
<InnerPanel key={fs.mount_point}> |
||||||
|
<PanelHeader> |
||||||
|
<Box display="flex" width="100%" className={classes.header}> |
||||||
|
<Box> |
||||||
|
<BodyText text={fs.mount_point} /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</PanelHeader> |
||||||
|
{fs?.hosts && |
||||||
|
fs.hosts.map( |
||||||
|
( |
||||||
|
host: AnvilFileSystemHost, |
||||||
|
index: number, |
||||||
|
): JSX.Element => ( |
||||||
|
<SharedStorageHost |
||||||
|
host={{ |
||||||
|
...host, |
||||||
|
...anvil[ |
||||||
|
anvil.findIndex((a) => a.anvil_uuid === uuid) |
||||||
|
].hosts[index], |
||||||
|
}} |
||||||
|
key={fs.hosts[index].free} |
||||||
|
/> |
||||||
|
), |
||||||
|
)} |
||||||
|
</InnerPanel> |
||||||
|
), |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
) : ( |
||||||
|
<Spinner /> |
||||||
|
)} |
||||||
|
</Panel> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default SharedStorage; |
@ -0,0 +1,80 @@ |
|||||||
|
import { Box } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import * as prettyBytes from 'pretty-bytes'; |
||||||
|
import { AllocationBar } from '../Bars'; |
||||||
|
import { BodyText } from '../Text'; |
||||||
|
import Decorator from '../Decorator'; |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
fs: { |
||||||
|
paddingLeft: '.7em', |
||||||
|
paddingRight: '.7em', |
||||||
|
paddingTop: '1.2em', |
||||||
|
}, |
||||||
|
bar: { |
||||||
|
paddingLeft: '.7em', |
||||||
|
paddingRight: '.7em', |
||||||
|
}, |
||||||
|
decoratorBox: { |
||||||
|
paddingRight: '.3em', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const SharedStorageHost = ({ |
||||||
|
host, |
||||||
|
}: { |
||||||
|
host: AnvilFileSystemHost; |
||||||
|
}): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Box display="flex" width="100%" className={classes.fs}> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<BodyText text={host.host_name || 'Not Available'} /> |
||||||
|
</Box> |
||||||
|
<Box className={classes.decoratorBox}> |
||||||
|
<Decorator colour={host.is_mounted ? 'ok' : 'error'} /> |
||||||
|
</Box> |
||||||
|
<Box> |
||||||
|
<BodyText text={host.is_mounted ? 'Mounted' : 'Not Mounted'} /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
{host.is_mounted && ( |
||||||
|
<> |
||||||
|
<Box display="flex" width="100%" className={classes.fs}> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<BodyText |
||||||
|
text={`Used: ${prettyBytes.default(host.total - host.free, { |
||||||
|
binary: true, |
||||||
|
})}`}
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box> |
||||||
|
<BodyText |
||||||
|
text={`Free: ${prettyBytes.default(host.free, { |
||||||
|
binary: true, |
||||||
|
})}`}
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<Box display="flex" width="100%" className={classes.bar}> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<AllocationBar |
||||||
|
allocated={((host.total - host.free) / host.total) * 100} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<Box display="flex" justifyContent="center" width="100%"> |
||||||
|
<BodyText |
||||||
|
text={`Total Storage: ${prettyBytes.default(host.total, { |
||||||
|
binary: true, |
||||||
|
})}`}
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default SharedStorageHost; |
@ -0,0 +1,3 @@ |
|||||||
|
import SharedStorage from './FileSystems'; |
||||||
|
|
||||||
|
export default SharedStorage; |
@ -0,0 +1,86 @@ |
|||||||
|
import { useState } from 'react'; |
||||||
|
import AppBar from '@material-ui/core/AppBar'; |
||||||
|
import { makeStyles, createStyles } from '@material-ui/core/styles'; |
||||||
|
import { Box, Button } from '@material-ui/core'; |
||||||
|
import { ICONS, ICON_SIZE } from '../lib/consts/ICONS'; |
||||||
|
import { BORDER_RADIUS, RED } from '../lib/consts/DEFAULT_THEME'; |
||||||
|
import AnvilDrawer from './AnvilDrawer'; |
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => |
||||||
|
createStyles({ |
||||||
|
appBar: { |
||||||
|
paddingTop: theme.spacing(0.5), |
||||||
|
paddingBottom: theme.spacing(0.5), |
||||||
|
paddingLeft: theme.spacing(3), |
||||||
|
paddingRight: theme.spacing(3), |
||||||
|
borderBottom: 'solid 1px', |
||||||
|
borderBottomColor: RED, |
||||||
|
}, |
||||||
|
input: { |
||||||
|
height: '2.8em', |
||||||
|
width: '30vw', |
||||||
|
backgroundColor: theme.palette.secondary.main, |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
}, |
||||||
|
barElement: { |
||||||
|
padding: 0, |
||||||
|
}, |
||||||
|
icons: { |
||||||
|
[theme.breakpoints.down('sm')]: { |
||||||
|
display: 'none', |
||||||
|
}, |
||||||
|
}, |
||||||
|
searchBar: { |
||||||
|
[theme.breakpoints.down('sm')]: { |
||||||
|
flexGrow: 1, |
||||||
|
paddingLeft: '15vw', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}), |
||||||
|
); |
||||||
|
|
||||||
|
const Header = (): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
const [open, setOpen] = useState(false); |
||||||
|
|
||||||
|
const toggleDrawer = (): void => setOpen(!open); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<AppBar position="static" className={classes.appBar}> |
||||||
|
<Box display="flex" justifyContent="space-between" flexDirection="row"> |
||||||
|
<Box className={classes.barElement}> |
||||||
|
<Button onClick={toggleDrawer}> |
||||||
|
<img alt="" src="/pngs/logo.png" width="160" height="40" /> |
||||||
|
</Button> |
||||||
|
</Box> |
||||||
|
<Box className={`${classes.barElement} ${classes.icons}`}> |
||||||
|
{ICONS.map( |
||||||
|
(icon): JSX.Element => ( |
||||||
|
<a |
||||||
|
key={icon.uri} |
||||||
|
href={ |
||||||
|
icon.uri.search(/^https?:/) !== -1 |
||||||
|
? icon.uri |
||||||
|
: `${process.env.NEXT_PUBLIC_API_URL}${icon.uri}` |
||||||
|
} |
||||||
|
> |
||||||
|
<img |
||||||
|
alt="" |
||||||
|
key="icon" |
||||||
|
src={icon.image} |
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...ICON_SIZE} |
||||||
|
/> |
||||||
|
</a> |
||||||
|
), |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</AppBar> |
||||||
|
<AnvilDrawer open={open} setOpen={setOpen} /> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Header; |
@ -0,0 +1,155 @@ |
|||||||
|
import { Box, Switch } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { InnerPanel, PanelHeader } from '../Panels'; |
||||||
|
import { ProgressBar } from '../Bars'; |
||||||
|
import { BodyText } from '../Text'; |
||||||
|
import Decorator, { Colours } from '../Decorator'; |
||||||
|
import HOST_STATUS from '../../lib/consts/NODES'; |
||||||
|
|
||||||
|
import putJSON from '../../lib/fetchers/putJSON'; |
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({ |
||||||
|
root: { |
||||||
|
overflow: 'auto', |
||||||
|
height: '28vh', |
||||||
|
paddingLeft: '.3em', |
||||||
|
[theme.breakpoints.down('md')]: { |
||||||
|
height: '100%', |
||||||
|
overflow: 'hidden', |
||||||
|
}, |
||||||
|
}, |
||||||
|
state: { |
||||||
|
paddingLeft: '.7em', |
||||||
|
paddingRight: '.7em', |
||||||
|
paddingTop: '1em', |
||||||
|
}, |
||||||
|
bar: { |
||||||
|
paddingLeft: '.7em', |
||||||
|
paddingRight: '.7em', |
||||||
|
}, |
||||||
|
header: { |
||||||
|
paddingTop: '.3em', |
||||||
|
paddingRight: '.7em', |
||||||
|
}, |
||||||
|
label: { |
||||||
|
paddingTop: '.3em', |
||||||
|
}, |
||||||
|
decoratorBox: { |
||||||
|
paddingRight: '.3em', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const selectStateMessage = (regex: RegExp, message: string): string => { |
||||||
|
const msg = regex.exec(message); |
||||||
|
|
||||||
|
if (msg) { |
||||||
|
return HOST_STATUS.get(msg[0]) || 'Error code not recognized'; |
||||||
|
} |
||||||
|
return 'Error code not found'; |
||||||
|
}; |
||||||
|
|
||||||
|
const selectDecorator = (state: string): Colours => { |
||||||
|
switch (state) { |
||||||
|
case 'online': |
||||||
|
return 'ok'; |
||||||
|
case 'offline': |
||||||
|
return 'off'; |
||||||
|
default: |
||||||
|
return 'warning'; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const AnvilHost = ({ |
||||||
|
hosts, |
||||||
|
}: { |
||||||
|
hosts: Array<AnvilStatusHost>; |
||||||
|
}): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
const stateRegex = /^[a-zA-Z]/; |
||||||
|
const messageRegex = /^(message_[0-9]+)/; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box className={classes.root}> |
||||||
|
{hosts && |
||||||
|
hosts.map( |
||||||
|
(host): JSX.Element => { |
||||||
|
return ( |
||||||
|
<InnerPanel key={host.host_uuid}> |
||||||
|
<PanelHeader> |
||||||
|
<Box display="flex" width="100%" className={classes.header}> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<BodyText text={host.host_name} /> |
||||||
|
</Box> |
||||||
|
<Box className={classes.decoratorBox}> |
||||||
|
<Decorator colour={selectDecorator(host.state)} /> |
||||||
|
</Box> |
||||||
|
<Box> |
||||||
|
<BodyText |
||||||
|
text={ |
||||||
|
host?.state?.replace(stateRegex, (c) => |
||||||
|
c.toUpperCase(), |
||||||
|
) || 'Not Available' |
||||||
|
} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</PanelHeader> |
||||||
|
<Box display="flex" className={classes.state}> |
||||||
|
<Box className={classes.label}> |
||||||
|
<BodyText text="Power: " /> |
||||||
|
</Box> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<Switch |
||||||
|
checked={host.state === 'online'} |
||||||
|
onChange={() => |
||||||
|
putJSON('/set_power', { |
||||||
|
host_uuid: host.host_uuid, |
||||||
|
is_on: !(host.state === 'online'), |
||||||
|
}) |
||||||
|
} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box className={classes.label}> |
||||||
|
<BodyText text="Member: " /> |
||||||
|
</Box> |
||||||
|
<Box> |
||||||
|
<Switch |
||||||
|
checked={host.state === 'online'} |
||||||
|
disabled={!(host.state === 'online')} |
||||||
|
onChange={() => |
||||||
|
putJSON('/set_membership', { |
||||||
|
host_uuid: host.host_uuid, |
||||||
|
is_member: !(host.state === 'online'), |
||||||
|
}) |
||||||
|
} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
{host.state !== 'online' && host.state !== 'offline' && ( |
||||||
|
<> |
||||||
|
<Box display="flex" width="100%" className={classes.state}> |
||||||
|
<Box> |
||||||
|
<BodyText |
||||||
|
text={selectStateMessage( |
||||||
|
messageRegex, |
||||||
|
host.state_message, |
||||||
|
)} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<Box display="flex" width="100%" className={classes.bar}> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<ProgressBar progressPercentage={host.state_percent} /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</InnerPanel> |
||||||
|
); |
||||||
|
}, |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default AnvilHost; |
@ -0,0 +1,41 @@ |
|||||||
|
import { useContext } from 'react'; |
||||||
|
import { Panel } from '../Panels'; |
||||||
|
import { HeaderText } from '../Text'; |
||||||
|
import AnvilHost from './AnvilHost'; |
||||||
|
import PeriodicFetch from '../../lib/fetchers/periodicFetch'; |
||||||
|
import { AnvilContext } from '../AnvilContext'; |
||||||
|
import Spinner from '../Spinner'; |
||||||
|
import hostsSanitizer from '../../lib/sanitizers/hostsSanitizer'; |
||||||
|
|
||||||
|
const Hosts = ({ anvil }: { anvil: AnvilListItem[] }): JSX.Element => { |
||||||
|
const { uuid } = useContext(AnvilContext); |
||||||
|
|
||||||
|
const { data, isLoading } = PeriodicFetch<AnvilStatus>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_status?anvil_uuid=${uuid}`, |
||||||
|
); |
||||||
|
|
||||||
|
const anvilIndex = anvil.findIndex((a) => a.anvil_uuid === uuid); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Panel> |
||||||
|
<HeaderText text="Nodes" /> |
||||||
|
{!isLoading ? ( |
||||||
|
<> |
||||||
|
{anvilIndex !== -1 && data && ( |
||||||
|
<AnvilHost |
||||||
|
hosts={hostsSanitizer(anvil[anvilIndex].hosts).map( |
||||||
|
(host, index) => { |
||||||
|
return data.hosts[index]; |
||||||
|
}, |
||||||
|
)} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<Spinner /> |
||||||
|
)} |
||||||
|
</Panel> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Hosts; |
@ -0,0 +1,69 @@ |
|||||||
|
import { useContext } from 'react'; |
||||||
|
import { Box } from '@material-ui/core'; |
||||||
|
import * as prettyBytes from 'pretty-bytes'; |
||||||
|
import { Panel } from './Panels'; |
||||||
|
import { AllocationBar } from './Bars'; |
||||||
|
import { HeaderText, BodyText } from './Text'; |
||||||
|
import PeriodicFetch from '../lib/fetchers/periodicFetch'; |
||||||
|
import { AnvilContext } from './AnvilContext'; |
||||||
|
import Spinner from './Spinner'; |
||||||
|
|
||||||
|
const Memory = (): JSX.Element => { |
||||||
|
const { uuid } = useContext(AnvilContext); |
||||||
|
const { data, isLoading } = PeriodicFetch<AnvilMemory>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_memory?anvil_uuid=${uuid}`, |
||||||
|
); |
||||||
|
|
||||||
|
const memoryData = |
||||||
|
isLoading || !data ? { total: 0, allocated: 0, reserved: 0 } : data; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Panel> |
||||||
|
<HeaderText text="Memory" /> |
||||||
|
{!isLoading ? ( |
||||||
|
<> |
||||||
|
{' '} |
||||||
|
<Box display="flex" width="100%"> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<BodyText |
||||||
|
text={`Allocated: ${prettyBytes.default(memoryData.allocated, { |
||||||
|
binary: true, |
||||||
|
})}`}
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box> |
||||||
|
<BodyText |
||||||
|
text={`Free: ${prettyBytes.default( |
||||||
|
memoryData.total - memoryData.allocated, |
||||||
|
{ |
||||||
|
binary: true, |
||||||
|
}, |
||||||
|
)}`}
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<Box display="flex" width="100%"> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<AllocationBar |
||||||
|
allocated={(memoryData.allocated / memoryData.total) * 100} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<Box display="flex" justifyContent="center" width="100%"> |
||||||
|
<BodyText |
||||||
|
text={`Total: ${prettyBytes.default(memoryData.total, { |
||||||
|
binary: true, |
||||||
|
})} | Reserved: ${prettyBytes.default(memoryData.reserved, { |
||||||
|
binary: true, |
||||||
|
})}`}
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<Spinner /> |
||||||
|
)} |
||||||
|
</Panel> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Memory; |
@ -0,0 +1,123 @@ |
|||||||
|
import { useContext } from 'react'; |
||||||
|
import { Box, Divider } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { Panel } from '../Panels'; |
||||||
|
import { HeaderText, BodyText } from '../Text'; |
||||||
|
import PeriodicFetch from '../../lib/fetchers/periodicFetch'; |
||||||
|
import { DIVIDER } from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
import processNetworkData from './processNetwork'; |
||||||
|
import { AnvilContext } from '../AnvilContext'; |
||||||
|
import Decorator, { Colours } from '../Decorator'; |
||||||
|
import Spinner from '../Spinner'; |
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({ |
||||||
|
container: { |
||||||
|
width: '100%', |
||||||
|
overflow: 'auto', |
||||||
|
height: '32vh', |
||||||
|
[theme.breakpoints.down('md')]: { |
||||||
|
height: '100%', |
||||||
|
overflow: 'hidden', |
||||||
|
}, |
||||||
|
}, |
||||||
|
root: { |
||||||
|
paddingTop: '.7em', |
||||||
|
paddingBottom: '.7em', |
||||||
|
}, |
||||||
|
noPaddingLeft: { |
||||||
|
paddingLeft: 0, |
||||||
|
}, |
||||||
|
divider: { |
||||||
|
background: DIVIDER, |
||||||
|
}, |
||||||
|
verticalDivider: { |
||||||
|
height: '3.5em', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const selectDecorator = (state: string): Colours => { |
||||||
|
switch (state) { |
||||||
|
case 'optimal': |
||||||
|
return 'ok'; |
||||||
|
case 'degraded': |
||||||
|
return 'warning'; |
||||||
|
case 'down': |
||||||
|
return 'error'; |
||||||
|
default: |
||||||
|
return 'warning'; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const Network = (): JSX.Element => { |
||||||
|
const { uuid } = useContext(AnvilContext); |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
const { data, isLoading } = PeriodicFetch<AnvilNetwork>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_networks?anvil_uuid=${uuid}`, |
||||||
|
); |
||||||
|
|
||||||
|
const processed = processNetworkData(data); |
||||||
|
return ( |
||||||
|
<Panel> |
||||||
|
<HeaderText text="Network" /> |
||||||
|
{!isLoading ? ( |
||||||
|
<Box className={classes.container}> |
||||||
|
{data && |
||||||
|
processed.bonds.map((bond: ProcessedBond) => { |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Box |
||||||
|
display="flex" |
||||||
|
flexDirection="row" |
||||||
|
width="100%" |
||||||
|
className={classes.root} |
||||||
|
> |
||||||
|
<Box p={1} className={classes.noPaddingLeft}> |
||||||
|
<Decorator colour={selectDecorator(bond.bond_state)} /> |
||||||
|
</Box> |
||||||
|
<Box p={1} flexGrow={1} className={classes.noPaddingLeft}> |
||||||
|
<BodyText text={bond.bond_name} /> |
||||||
|
<BodyText text={`${bond.bond_speed}Mbps`} /> |
||||||
|
</Box> |
||||||
|
<Box display="flex" style={{ paddingTop: '.5em' }}> |
||||||
|
{bond.hosts.map( |
||||||
|
(host, index: number): JSX.Element => ( |
||||||
|
<> |
||||||
|
<Box |
||||||
|
p={1} |
||||||
|
key={host.host_name} |
||||||
|
style={{ paddingTop: 0, paddingBottom: 0 }} |
||||||
|
> |
||||||
|
<Box> |
||||||
|
<BodyText |
||||||
|
text={host.host_name} |
||||||
|
selected={false} |
||||||
|
/> |
||||||
|
<BodyText text={host.link.link_name} /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
{index !== bond.hosts.length - 1 && ( |
||||||
|
<Divider |
||||||
|
className={`${classes.divider} ${classes.verticalDivider}`} |
||||||
|
orientation="vertical" |
||||||
|
flexItem |
||||||
|
/> |
||||||
|
)} |
||||||
|
</> |
||||||
|
), |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<Divider className={classes.divider} /> |
||||||
|
</> |
||||||
|
); |
||||||
|
})} |
||||||
|
</Box> |
||||||
|
) : ( |
||||||
|
<Spinner /> |
||||||
|
)} |
||||||
|
</Panel> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Network; |
@ -0,0 +1,3 @@ |
|||||||
|
import Network from './Network'; |
||||||
|
|
||||||
|
export default Network; |
@ -0,0 +1,47 @@ |
|||||||
|
const processNetworkData = (data: AnvilNetwork): ProcessedNetwork => { |
||||||
|
const processedBonds: string[] = []; |
||||||
|
const displayBonds: ProcessedNetwork = { bonds: [] }; |
||||||
|
|
||||||
|
data?.hosts.forEach((host) => { |
||||||
|
host.bonds.forEach((bond) => { |
||||||
|
const index = processedBonds.findIndex( |
||||||
|
(processed: string) => processed === bond.bond_name, |
||||||
|
); |
||||||
|
|
||||||
|
if (index === -1) { |
||||||
|
processedBonds.push(bond.bond_name); |
||||||
|
displayBonds.bonds.push({ |
||||||
|
bond_name: bond.bond_name, |
||||||
|
bond_uuid: bond.bond_uuid, |
||||||
|
bond_speed: 0, |
||||||
|
bond_state: 'degraded', |
||||||
|
hosts: [ |
||||||
|
{ |
||||||
|
host_name: host.host_name, |
||||||
|
host_uuid: host.host_uuid, |
||||||
|
link: bond.links[0].is_active ? bond.links[0] : bond.links[1], |
||||||
|
}, |
||||||
|
], |
||||||
|
}); |
||||||
|
} else { |
||||||
|
displayBonds.bonds[index].hosts.push({ |
||||||
|
host_name: host.host_name, |
||||||
|
host_uuid: host.host_uuid, |
||||||
|
link: bond.links[0].is_active ? bond.links[0] : bond.links[1], |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
/* eslint-disable no-param-reassign */ |
||||||
|
displayBonds.bonds.forEach((bond) => { |
||||||
|
const hostIndex = |
||||||
|
bond.hosts[0].link.link_speed > bond.hosts[1].link.link_speed ? 1 : 0; |
||||||
|
|
||||||
|
bond.bond_speed = bond.hosts[hostIndex].link.link_speed; |
||||||
|
bond.bond_state = bond.hosts[hostIndex].link.link_state; |
||||||
|
}); |
||||||
|
return displayBonds; |
||||||
|
}; |
||||||
|
|
||||||
|
export default processNetworkData; |
@ -0,0 +1,29 @@ |
|||||||
|
import { ReactNode } from 'react'; |
||||||
|
import { Box } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { BORDER_RADIUS, DIVIDER } from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
type Props = { |
||||||
|
children: ReactNode; |
||||||
|
}; |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
innerBody: { |
||||||
|
borderWidth: '1px', |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
borderStyle: 'solid', |
||||||
|
borderColor: DIVIDER, |
||||||
|
marginTop: '1.4em', |
||||||
|
marginBottom: '1.4em', |
||||||
|
paddingBottom: '.7em', |
||||||
|
position: 'relative', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const InnerPanel = ({ children }: Props): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
return <Box className={classes.innerBody}>{children}</Box>; |
||||||
|
}; |
||||||
|
|
||||||
|
export default InnerPanel; |
@ -0,0 +1,59 @@ |
|||||||
|
import { ReactNode } from 'react'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { |
||||||
|
BORDER_RADIUS, |
||||||
|
PANEL_BACKGROUND, |
||||||
|
TEXT, |
||||||
|
} from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
type Props = { |
||||||
|
children: ReactNode; |
||||||
|
}; |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
paper: { |
||||||
|
padding: '2.1em', |
||||||
|
backgroundColor: PANEL_BACKGROUND, |
||||||
|
opacity: 0.8, |
||||||
|
zIndex: 999, |
||||||
|
}, |
||||||
|
container: { |
||||||
|
margin: '1em', |
||||||
|
position: 'relative', |
||||||
|
}, |
||||||
|
square: { |
||||||
|
content: '""', |
||||||
|
position: 'absolute', |
||||||
|
width: '2.1em', |
||||||
|
height: '2.1em', |
||||||
|
border: '1px', |
||||||
|
borderColor: TEXT, |
||||||
|
borderWidth: '1px', |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
borderStyle: 'solid', |
||||||
|
padding: 0, |
||||||
|
margin: 0, |
||||||
|
}, |
||||||
|
topSquare: { |
||||||
|
top: '-.3em', |
||||||
|
left: '-.3em', |
||||||
|
}, |
||||||
|
bottomSquare: { |
||||||
|
bottom: '-.3em', |
||||||
|
right: '-.3em', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const Panel = ({ children }: Props): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={classes.container}> |
||||||
|
<div className={`${classes.square} ${classes.topSquare}`} /> |
||||||
|
<div className={`${classes.square} ${classes.bottomSquare}`} /> |
||||||
|
<div className={classes.paper}>{children}</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Panel; |
@ -0,0 +1,40 @@ |
|||||||
|
import { ReactNode } from 'react'; |
||||||
|
import { Box } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { BORDER_RADIUS, DIVIDER } from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
type Props = { |
||||||
|
children: ReactNode; |
||||||
|
}; |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
innerHeader: { |
||||||
|
position: 'relative', |
||||||
|
padding: '0 .7em', |
||||||
|
}, |
||||||
|
header: { |
||||||
|
top: '-.3em', |
||||||
|
left: '-.3em', |
||||||
|
padding: '1.4em 0', |
||||||
|
position: 'absolute', |
||||||
|
content: '""', |
||||||
|
borderColor: DIVIDER, |
||||||
|
borderWidth: '1px', |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
borderStyle: 'solid', |
||||||
|
width: '100%', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const PanelHeader = ({ children }: Props): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box className={classes.innerHeader} whiteSpace="pre-wrap"> |
||||||
|
<div className={classes.header} /> |
||||||
|
{children} |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default PanelHeader; |
@ -0,0 +1,5 @@ |
|||||||
|
import PanelHeader from './PanelHeader'; |
||||||
|
import InnerPanel from './InnerPanel'; |
||||||
|
import Panel from './Panel'; |
||||||
|
|
||||||
|
export { Panel, PanelHeader, InnerPanel }; |
@ -0,0 +1,152 @@ |
|||||||
|
import { useContext } from 'react'; |
||||||
|
import { List, ListItem, Divider, Box } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { Panel } from './Panels'; |
||||||
|
import PeriodicFetch from '../lib/fetchers/periodicFetch'; |
||||||
|
import { HeaderText, BodyText } from './Text'; |
||||||
|
import { HOVER, DIVIDER } from '../lib/consts/DEFAULT_THEME'; |
||||||
|
import { AnvilContext } from './AnvilContext'; |
||||||
|
import serverState from '../lib/consts/SERVERS'; |
||||||
|
import Decorator, { Colours } from './Decorator'; |
||||||
|
import Spinner from './Spinner'; |
||||||
|
import hostsSanitizer from '../lib/sanitizers/hostsSanitizer'; |
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({ |
||||||
|
root: { |
||||||
|
width: '100%', |
||||||
|
overflow: 'auto', |
||||||
|
height: '78vh', |
||||||
|
[theme.breakpoints.down('md')]: { |
||||||
|
height: '100%', |
||||||
|
}, |
||||||
|
}, |
||||||
|
divider: { |
||||||
|
background: DIVIDER, |
||||||
|
}, |
||||||
|
verticalDivider: { |
||||||
|
height: '75%', |
||||||
|
paddingTop: '1em', |
||||||
|
}, |
||||||
|
button: { |
||||||
|
'&:hover': { |
||||||
|
backgroundColor: HOVER, |
||||||
|
}, |
||||||
|
paddingLeft: 0, |
||||||
|
}, |
||||||
|
headerPadding: { |
||||||
|
paddingLeft: '.3em', |
||||||
|
}, |
||||||
|
hostsBox: { |
||||||
|
padding: '1em', |
||||||
|
paddingRight: 0, |
||||||
|
}, |
||||||
|
hostBox: { |
||||||
|
paddingTop: 0, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const selectDecorator = (state: string): Colours => { |
||||||
|
switch (state) { |
||||||
|
case 'running': |
||||||
|
return 'ok'; |
||||||
|
case 'shut off': |
||||||
|
return 'off'; |
||||||
|
case 'crashed': |
||||||
|
return 'error'; |
||||||
|
default: |
||||||
|
return 'warning'; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const Servers = ({ anvil }: { anvil: AnvilListItem[] }): JSX.Element => { |
||||||
|
const { uuid } = useContext(AnvilContext); |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
const { data, isLoading } = PeriodicFetch<AnvilServers>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_servers?anvil_uuid=${uuid}`, |
||||||
|
); |
||||||
|
|
||||||
|
const anvilIndex = anvil.findIndex((a) => a.anvil_uuid === uuid); |
||||||
|
|
||||||
|
const filteredHosts = hostsSanitizer(anvil[anvilIndex]?.hosts); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Panel> |
||||||
|
<div className={classes.headerPadding}> |
||||||
|
<HeaderText text="Servers" /> |
||||||
|
</div> |
||||||
|
{!isLoading ? ( |
||||||
|
<Box className={classes.root}> |
||||||
|
<List component="nav"> |
||||||
|
{data && |
||||||
|
data.servers.map((server: AnvilServer) => { |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<ListItem |
||||||
|
button |
||||||
|
className={classes.button} |
||||||
|
key={server.server_uuid} |
||||||
|
> |
||||||
|
<Box display="flex" flexDirection="row" width="100%"> |
||||||
|
<Box p={1}> |
||||||
|
<Decorator |
||||||
|
colour={selectDecorator(server.server_state)} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box p={1} flexGrow={1}> |
||||||
|
<BodyText text={server.server_name} /> |
||||||
|
<BodyText |
||||||
|
text={ |
||||||
|
serverState.get(server.server_state) || |
||||||
|
'Not Available' |
||||||
|
} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box display="flex" className={classes.hostsBox}> |
||||||
|
{server.server_state !== 'shut off' && |
||||||
|
server.server_state !== 'crashed' && |
||||||
|
filteredHosts.map( |
||||||
|
( |
||||||
|
host: AnvilStatusHost, |
||||||
|
index: number, |
||||||
|
): JSX.Element => ( |
||||||
|
<> |
||||||
|
<Box |
||||||
|
p={1} |
||||||
|
key={host.host_uuid} |
||||||
|
className={classes.hostBox} |
||||||
|
> |
||||||
|
<BodyText |
||||||
|
text={host.host_name} |
||||||
|
selected={ |
||||||
|
server.server_host_uuid === |
||||||
|
host.host_uuid |
||||||
|
} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
{index !== filteredHosts.length - 1 && ( |
||||||
|
<Divider |
||||||
|
className={`${classes.divider} ${classes.verticalDivider}`} |
||||||
|
orientation="vertical" |
||||||
|
/> |
||||||
|
)} |
||||||
|
</> |
||||||
|
), |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</ListItem> |
||||||
|
<Divider className={classes.divider} /> |
||||||
|
</> |
||||||
|
); |
||||||
|
})} |
||||||
|
</List> |
||||||
|
</Box> |
||||||
|
) : ( |
||||||
|
<Spinner /> |
||||||
|
)} |
||||||
|
</Panel> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Servers; |
@ -0,0 +1,64 @@ |
|||||||
|
import { useContext } from 'react'; |
||||||
|
|
||||||
|
import { Box } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { BodyText, HeaderText } from '../Text'; |
||||||
|
import { Panel, InnerPanel, PanelHeader } from '../Panels'; |
||||||
|
import SharedStorageHost from './SharedStorageHost'; |
||||||
|
import PeriodicFetch from '../../lib/fetchers/periodicFetch'; |
||||||
|
import { AnvilContext } from '../AnvilContext'; |
||||||
|
import Spinner from '../Spinner'; |
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({ |
||||||
|
header: { |
||||||
|
paddingTop: '.1em', |
||||||
|
paddingRight: '.7em', |
||||||
|
}, |
||||||
|
root: { |
||||||
|
overflow: 'auto', |
||||||
|
height: '78vh', |
||||||
|
paddingLeft: '.3em', |
||||||
|
[theme.breakpoints.down('md')]: { |
||||||
|
height: '100%', |
||||||
|
}, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const SharedStorage = (): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
const { uuid } = useContext(AnvilContext); |
||||||
|
const { data, isLoading } = PeriodicFetch<AnvilSharedStorage>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_shared_storage?anvil_uuid=${uuid}`, |
||||||
|
); |
||||||
|
return ( |
||||||
|
<Panel> |
||||||
|
<HeaderText text="Shared Storage" /> |
||||||
|
{!isLoading ? ( |
||||||
|
<Box className={classes.root}> |
||||||
|
{data?.storage_groups && |
||||||
|
data.storage_groups.map( |
||||||
|
(storageGroup: AnvilSharedStorageGroup): JSX.Element => ( |
||||||
|
<InnerPanel key={storageGroup.storage_group_uuid}> |
||||||
|
<PanelHeader> |
||||||
|
<Box display="flex" width="100%" className={classes.header}> |
||||||
|
<Box> |
||||||
|
<BodyText text={storageGroup.storage_group_name} /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</PanelHeader> |
||||||
|
<SharedStorageHost |
||||||
|
group={storageGroup} |
||||||
|
key={storageGroup.storage_group_uuid} |
||||||
|
/> |
||||||
|
</InnerPanel> |
||||||
|
), |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
) : ( |
||||||
|
<Spinner /> |
||||||
|
)} |
||||||
|
</Panel> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default SharedStorage; |
@ -0,0 +1,74 @@ |
|||||||
|
import { Box } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import * as prettyBytes from 'pretty-bytes'; |
||||||
|
import { AllocationBar } from '../Bars'; |
||||||
|
import { BodyText } from '../Text'; |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
fs: { |
||||||
|
paddingLeft: '.7em', |
||||||
|
paddingRight: '.7em', |
||||||
|
paddingTop: '1.2em', |
||||||
|
}, |
||||||
|
bar: { |
||||||
|
paddingLeft: '.7em', |
||||||
|
paddingRight: '.7em', |
||||||
|
}, |
||||||
|
decoratorBox: { |
||||||
|
paddingRight: '.3em', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const SharedStorageHost = ({ |
||||||
|
group, |
||||||
|
}: { |
||||||
|
group: AnvilSharedStorageGroup; |
||||||
|
}): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Box display="flex" width="100%" className={classes.fs}> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<BodyText |
||||||
|
text={`Used: ${prettyBytes.default( |
||||||
|
group.storage_group_total - group.storage_group_free, |
||||||
|
{ |
||||||
|
binary: true, |
||||||
|
}, |
||||||
|
)}`}
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box> |
||||||
|
<BodyText |
||||||
|
text={`Free: ${prettyBytes.default(group.storage_group_free, { |
||||||
|
binary: true, |
||||||
|
})}`}
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<Box display="flex" width="100%" className={classes.bar}> |
||||||
|
<Box flexGrow={1}> |
||||||
|
<AllocationBar |
||||||
|
allocated={ |
||||||
|
((group.storage_group_total - group.storage_group_free) / |
||||||
|
group.storage_group_total) * |
||||||
|
100 |
||||||
|
} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<Box display="flex" justifyContent="center" width="100%"> |
||||||
|
<BodyText |
||||||
|
text={`Total Storage: ${prettyBytes.default( |
||||||
|
group.storage_group_total, |
||||||
|
{ |
||||||
|
binary: true, |
||||||
|
}, |
||||||
|
)}`}
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default SharedStorageHost; |
@ -0,0 +1,3 @@ |
|||||||
|
import SharedStorage from './SharedStorage'; |
||||||
|
|
||||||
|
export default SharedStorage; |
@ -0,0 +1,29 @@ |
|||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress'; |
||||||
|
import { TEXT } from '../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
root: { |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'center', |
||||||
|
marginTop: '3em', |
||||||
|
}, |
||||||
|
spinner: { |
||||||
|
color: TEXT, |
||||||
|
variant: 'indeterminate', |
||||||
|
size: '50em', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const Spinner = (): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={classes.root}> |
||||||
|
<CircularProgress className={classes.spinner} /> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Spinner; |
@ -0,0 +1,50 @@ |
|||||||
|
import { Grid } from '@material-ui/core'; |
||||||
|
import * as prettyBytes from 'pretty-bytes'; |
||||||
|
import { Panel } from './Panels'; |
||||||
|
import { AllocationBar } from './Bars'; |
||||||
|
import { HeaderText, BodyText } from './Text'; |
||||||
|
import PeriodicFetch from '../lib/fetchers/periodicFetch'; |
||||||
|
|
||||||
|
const Storage = ({ uuid }: { uuid: string }): JSX.Element => { |
||||||
|
const { data, isLoading } = PeriodicFetch<AnvilMemory>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_memory?anvil_uuid=${uuid}`, |
||||||
|
); |
||||||
|
|
||||||
|
const memoryData = isLoading || !data ? { total: 0, free: 0 } : data; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Panel> |
||||||
|
<Grid container alignItems="center" justify="space-around"> |
||||||
|
<Grid item xs={12}> |
||||||
|
<HeaderText text="Storage Resync" /> |
||||||
|
</Grid> |
||||||
|
<Grid item xs={5}> |
||||||
|
<BodyText |
||||||
|
text={`Allocated: ${prettyBytes.default( |
||||||
|
memoryData.total - memoryData.free, |
||||||
|
{ |
||||||
|
binary: true, |
||||||
|
}, |
||||||
|
)}`}
|
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item xs={4}> |
||||||
|
<BodyText |
||||||
|
text={`Free: ${prettyBytes.default(memoryData.free, { |
||||||
|
binary: true, |
||||||
|
})}`}
|
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item xs={10}> |
||||||
|
<AllocationBar |
||||||
|
allocated={ |
||||||
|
((memoryData.total - memoryData.free) / memoryData.total) * 100 |
||||||
|
} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
</Grid> |
||||||
|
</Panel> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Storage; |
@ -0,0 +1,36 @@ |
|||||||
|
import { Typography } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
import { TEXT, UNSELECTED } from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
interface TextProps { |
||||||
|
text: string; |
||||||
|
selected?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({ |
||||||
|
selected: { |
||||||
|
color: TEXT, |
||||||
|
}, |
||||||
|
unselected: { |
||||||
|
color: UNSELECTED, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const BodyText = ({ text, selected }: TextProps): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Typography |
||||||
|
variant="subtitle1" |
||||||
|
className={selected ? classes.selected : classes.unselected} |
||||||
|
> |
||||||
|
{text} |
||||||
|
</Typography> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
BodyText.defaultProps = { |
||||||
|
selected: true, |
||||||
|
}; |
||||||
|
|
||||||
|
export default BodyText; |
@ -0,0 +1,15 @@ |
|||||||
|
import { Typography } from '@material-ui/core'; |
||||||
|
import { withStyles } from '@material-ui/core/styles'; |
||||||
|
import { TEXT } from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
const WhiteTypography = withStyles({ |
||||||
|
root: { |
||||||
|
color: TEXT, |
||||||
|
}, |
||||||
|
})(Typography); |
||||||
|
|
||||||
|
const HeaderText = ({ text }: { text: string }): JSX.Element => { |
||||||
|
return <WhiteTypography variant="h4">{text}</WhiteTypography>; |
||||||
|
}; |
||||||
|
|
||||||
|
export default HeaderText; |
@ -0,0 +1,4 @@ |
|||||||
|
import HeaderText from './HeaderText'; |
||||||
|
import BodyText from './BodyText'; |
||||||
|
|
||||||
|
export { HeaderText, BodyText }; |
@ -0,0 +1,2 @@ |
|||||||
|
# Use this to set a different origin |
||||||
|
NEXT_PUBLIC_API_URL= |
@ -0,0 +1,7 @@ |
|||||||
|
const anvilState: ReadonlyMap<string, string> = new Map([ |
||||||
|
['optimal', 'Optimal'], |
||||||
|
['not_ready', 'Not Ready'], |
||||||
|
['degraded', 'Degraded'], |
||||||
|
]); |
||||||
|
|
||||||
|
export default anvilState; |
@ -0,0 +1,7 @@ |
|||||||
|
import IS_DEV_ENV from './IS_DEV_ENV'; |
||||||
|
|
||||||
|
const API_BASE_URL = IS_DEV_ENV |
||||||
|
? process.env.DEVELOPMENT_API_BASE_URL |
||||||
|
: process.env.PRODUCTION_API_BASE_URL; |
||||||
|
|
||||||
|
export default API_BASE_URL; |
@ -0,0 +1,16 @@ |
|||||||
|
// Colour Palette
|
||||||
|
|
||||||
|
export const RED = '#D02724'; |
||||||
|
export const TEXT = '#F2F2F2'; |
||||||
|
export const PANEL_BACKGROUND = '#343434'; |
||||||
|
export const PANEL_BORDER = '#000000'; |
||||||
|
export const PURPLE = '#7353BA'; |
||||||
|
export const BLUE = '#4785FF'; |
||||||
|
export const GREY = '#E5E5E5'; |
||||||
|
export const HOVER = '#444'; |
||||||
|
export const UNSELECTED = '#666'; |
||||||
|
export const DIVIDER = '#888'; |
||||||
|
export const SELECTED_ANVIL = '#00ff00'; |
||||||
|
export const DISABLED = '#AAA'; |
||||||
|
|
||||||
|
export const BORDER_RADIUS = '3px'; |
@ -0,0 +1,42 @@ |
|||||||
|
export const ICONS = [ |
||||||
|
{ |
||||||
|
text: 'Files', |
||||||
|
image: '/pngs/files_on.png', |
||||||
|
uri: '/striker?files=true', |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: 'Tasks', |
||||||
|
image: '/pngs/tasks_no-jobs_icon.png', |
||||||
|
uri: '/striker?jobs=true', |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: 'Configure', |
||||||
|
image: '/pngs/configure_icon_on.png', |
||||||
|
uri: '/striker?configure=true', |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: 'Striker', |
||||||
|
image: '/pngs/striker_icon_on.png', |
||||||
|
uri: '/striker?striker=true', |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: 'Anvil', |
||||||
|
image: '/pngs/anvil_icon_on.png', |
||||||
|
uri: '/striker?anvil=true', |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: 'Email', |
||||||
|
image: '/pngs/email_on.png', |
||||||
|
uri: '/striker?email=true', |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: 'Help', |
||||||
|
image: '/pngs/help_icon_on.png', |
||||||
|
uri: 'https://alteeve.com/w/Support', |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
export const ICON_SIZE = { |
||||||
|
width: '40em', |
||||||
|
height: '40em', |
||||||
|
}; |
@ -0,0 +1,3 @@ |
|||||||
|
const IS_DEV_ENV: boolean = process.env.NODE_ENV !== 'production'; |
||||||
|
|
||||||
|
export default IS_DEV_ENV; |
@ -0,0 +1,18 @@ |
|||||||
|
const NODE_STATUS_MESSAGE_MAP: ReadonlyMap<string, string> = new Map([ |
||||||
|
['message_0222', 'The node is in an unknown state.'], |
||||||
|
['message_0223', 'The node is a full cluster member.'], |
||||||
|
[ |
||||||
|
'message_0224', |
||||||
|
'The node is coming online; the cluster resource manager is running (step 2/3).', |
||||||
|
], |
||||||
|
[ |
||||||
|
'message_0225', |
||||||
|
'The node is coming online; the node is a consensus cluster member (step 1/3).', |
||||||
|
], |
||||||
|
[ |
||||||
|
'message_0226', |
||||||
|
'The node has booted, but it is not (yet) joining the cluster.', |
||||||
|
], |
||||||
|
]); |
||||||
|
|
||||||
|
export default NODE_STATUS_MESSAGE_MAP; |
@ -0,0 +1,12 @@ |
|||||||
|
const serverState: ReadonlyMap<string, string> = new Map([ |
||||||
|
['running', 'Running'], |
||||||
|
['idle', 'Idle'], |
||||||
|
['paused', 'Paused'], |
||||||
|
['in shutdown', 'Shutting Down'], |
||||||
|
['shut off', 'Off'], |
||||||
|
['crashed', 'Crashed'], |
||||||
|
['pmsuspended', 'PM Suspended'], |
||||||
|
['migrating', 'Migrating'], |
||||||
|
]); |
||||||
|
|
||||||
|
export default serverState; |
@ -0,0 +1,18 @@ |
|||||||
|
class ExtendedDate extends Date { |
||||||
|
toLocaleISOString(): string { |
||||||
|
const localeDateParts: string[] = this.toLocaleDateString('en-US', { |
||||||
|
year: 'numeric', |
||||||
|
month: '2-digit', |
||||||
|
day: '2-digit', |
||||||
|
}).split('/', 3); |
||||||
|
const localDate = `${localeDateParts[2]}-${localeDateParts[0]}-${localeDateParts[1]}`; |
||||||
|
const localeTime: string = this.toLocaleTimeString('en-US', { |
||||||
|
hour12: false, |
||||||
|
}); |
||||||
|
const timezoneOffset: number = (this.getTimezoneOffset() / 60) * -1; |
||||||
|
|
||||||
|
return `${localDate}T${localeTime}${timezoneOffset}`; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default ExtendedDate; |
@ -0,0 +1,5 @@ |
|||||||
|
const fetchJSON = <T>(...args: [RequestInfo, RequestInit?]): Promise<T> => { |
||||||
|
return fetch(...args).then((response: Response) => response.json()); |
||||||
|
}; |
||||||
|
|
||||||
|
export default fetchJSON; |
@ -0,0 +1,18 @@ |
|||||||
|
import useSWR from 'swr'; |
||||||
|
import fetcher from './fetchJSON'; |
||||||
|
|
||||||
|
const PeriodicFetch = <T>( |
||||||
|
url: string, |
||||||
|
refreshInterval = 5000, |
||||||
|
): GetResponses => { |
||||||
|
const { data, error } = useSWR<T>(url, fetcher, { |
||||||
|
refreshInterval, |
||||||
|
}); |
||||||
|
return { |
||||||
|
data, |
||||||
|
isLoading: !error && !data, |
||||||
|
isError: error, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export default PeriodicFetch; |
@ -0,0 +1,11 @@ |
|||||||
|
const putJSON = <T>(uri: string, data: T): void => { |
||||||
|
fetch(`${process.env.NEXT_PUBLIC_API_URL}${uri}`, { |
||||||
|
method: 'PUT', |
||||||
|
headers: { |
||||||
|
'Content-Type': 'application/json', |
||||||
|
}, |
||||||
|
body: JSON.stringify(data), |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
export default putJSON; |
@ -0,0 +1,7 @@ |
|||||||
|
const hostsSanitizer = ( |
||||||
|
data: Array<AnvilStatusHost>, |
||||||
|
): Array<AnvilStatusHost> => { |
||||||
|
return data?.filter((host) => host.host_uuid); |
||||||
|
}; |
||||||
|
|
||||||
|
export default hostsSanitizer; |
@ -0,0 +1,2 @@ |
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/types/global" />
|
@ -0,0 +1,5 @@ |
|||||||
|
module.exports = { |
||||||
|
pageExtensions: ['ts', 'tsx'], |
||||||
|
poweredByHeader: false, |
||||||
|
reactStrictMode: true, |
||||||
|
}; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,46 @@ |
|||||||
|
{ |
||||||
|
"name": "striker-ui", |
||||||
|
"version": "0.1.0", |
||||||
|
"private": true, |
||||||
|
"scripts": { |
||||||
|
"build": "next build && next export", |
||||||
|
"dev": "next dev", |
||||||
|
"lint": "eslint --config .eslintrc.json --ext js,jsx,ts,tsx --max-warnings=0", |
||||||
|
"lint:dryrun": "npm run lint -- .", |
||||||
|
"lint:fix": "npm run lint -- --fix .", |
||||||
|
"postinstall": "cd .. && husky install striker-ui/.husky", |
||||||
|
"start": "next start" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@material-ui/core": "^4.11.3", |
||||||
|
"@material-ui/styles": "^4.11.3", |
||||||
|
"next": "10.0.6", |
||||||
|
"pretty-bytes": "^5.6.0", |
||||||
|
"react": "17.0.1", |
||||||
|
"react-dom": "17.0.1", |
||||||
|
"swr": "^0.4.2", |
||||||
|
"typeface-roboto": "^1.1.13", |
||||||
|
"typeface-roboto-condensed": "^1.1.13" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@commitlint/cli": "^11.0.0", |
||||||
|
"@commitlint/config-conventional": "^11.0.0", |
||||||
|
"@types/node": "^14.14.26", |
||||||
|
"@types/react": "^17.0.1", |
||||||
|
"@types/styled-components": "^5.1.7", |
||||||
|
"@typescript-eslint/eslint-plugin": "^4.15.0", |
||||||
|
"@typescript-eslint/parser": "^4.15.0", |
||||||
|
"eslint": "^7.19.0", |
||||||
|
"eslint-config-airbnb": "^18.2.1", |
||||||
|
"eslint-config-prettier": "^7.2.0", |
||||||
|
"eslint-plugin-import": "^2.22.1", |
||||||
|
"eslint-plugin-jsx-a11y": "^6.4.1", |
||||||
|
"eslint-plugin-prettier": "^3.3.1", |
||||||
|
"eslint-plugin-react": "^7.22.0", |
||||||
|
"eslint-plugin-react-hooks": "^4.2.0", |
||||||
|
"husky": "^5.0.9", |
||||||
|
"lint-staged": "^10.5.4", |
||||||
|
"prettier": "^2.2.1", |
||||||
|
"typescript": "^4.1.5" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
import { useEffect } from 'react'; |
||||||
|
import { AppProps } from 'next/app'; |
||||||
|
import { ThemeProvider } from '@material-ui/core/styles'; |
||||||
|
import theme from '../theme'; |
||||||
|
import '../styles/globals.css'; |
||||||
|
|
||||||
|
const App = ({ Component, pageProps }: AppProps): JSX.Element => { |
||||||
|
// This hook is for ensuring the styling is in sync between client and server
|
||||||
|
useEffect(() => { |
||||||
|
// Remove the server-side injected CSS.
|
||||||
|
const jssStyles = document.querySelector('#jss-server-side'); |
||||||
|
if (jssStyles) { |
||||||
|
jssStyles.parentElement?.removeChild(jssStyles); |
||||||
|
} |
||||||
|
}, []); |
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return ( |
||||||
|
<ThemeProvider theme={theme}> |
||||||
|
<Component |
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...pageProps} |
||||||
|
/> |
||||||
|
</ThemeProvider> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default App; |
@ -0,0 +1,37 @@ |
|||||||
|
import Document, { DocumentInitialProps, DocumentContext } from 'next/document'; |
||||||
|
import { ServerStyleSheets as MaterialUiServerStyleSheets } from '@material-ui/styles'; |
||||||
|
// import { ServerStyleSheet as StyledComponentSheets } from 'styled-components';
|
||||||
|
|
||||||
|
class MyDocument extends Document { |
||||||
|
static async getInitialProps( |
||||||
|
ctx: DocumentContext, |
||||||
|
): Promise<DocumentInitialProps> { |
||||||
|
const materialUiSheets = new MaterialUiServerStyleSheets(); |
||||||
|
const originalRenderPage = ctx.renderPage; |
||||||
|
|
||||||
|
ctx.renderPage = () => |
||||||
|
originalRenderPage({ |
||||||
|
enhanceApp: (App) => (props) => |
||||||
|
materialUiSheets.collect( |
||||||
|
<App |
||||||
|
/* eslint-disable react/jsx-props-no-spreading */ |
||||||
|
{...props} |
||||||
|
/>, |
||||||
|
), |
||||||
|
}); |
||||||
|
|
||||||
|
const initialProps = await Document.getInitialProps(ctx); |
||||||
|
|
||||||
|
return { |
||||||
|
...initialProps, |
||||||
|
styles: ( |
||||||
|
<> |
||||||
|
{initialProps.styles} |
||||||
|
{materialUiSheets.getStyleElement()} |
||||||
|
</> |
||||||
|
), |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default MyDocument; |
@ -0,0 +1,82 @@ |
|||||||
|
import { Box } from '@material-ui/core'; |
||||||
|
import { makeStyles } from '@material-ui/core/styles'; |
||||||
|
|
||||||
|
import Anvils from '../components/Anvils'; |
||||||
|
import Hosts from '../components/Hosts'; |
||||||
|
import CPU from '../components/CPU'; |
||||||
|
import SharedStorage from '../components/SharedStorage'; |
||||||
|
import Memory from '../components/Memory'; |
||||||
|
import Network from '../components/Network'; |
||||||
|
import PeriodicFetch from '../lib/fetchers/periodicFetch'; |
||||||
|
import Servers from '../components/Servers'; |
||||||
|
import Header from '../components/Header'; |
||||||
|
import AnvilProvider from '../components/AnvilContext'; |
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({ |
||||||
|
child: { |
||||||
|
width: '22%', |
||||||
|
height: '100%', |
||||||
|
[theme.breakpoints.down('lg')]: { |
||||||
|
width: '25%', |
||||||
|
}, |
||||||
|
[theme.breakpoints.down('md')]: { |
||||||
|
width: '100%', |
||||||
|
}, |
||||||
|
}, |
||||||
|
server: { |
||||||
|
width: '35%', |
||||||
|
height: '100%', |
||||||
|
[theme.breakpoints.down('lg')]: { |
||||||
|
width: '25%', |
||||||
|
}, |
||||||
|
[theme.breakpoints.down('md')]: { |
||||||
|
width: '100%', |
||||||
|
}, |
||||||
|
}, |
||||||
|
container: { |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'row', |
||||||
|
width: '100%', |
||||||
|
justifyContent: 'space-between', |
||||||
|
[theme.breakpoints.down('md')]: { |
||||||
|
display: 'block', |
||||||
|
}, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const Home = (): JSX.Element => { |
||||||
|
const classes = useStyles(); |
||||||
|
|
||||||
|
const { data } = PeriodicFetch<AnvilList>( |
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/get_anvils`, |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<AnvilProvider> |
||||||
|
<Header /> |
||||||
|
{data?.anvils && ( |
||||||
|
<Box className={classes.container}> |
||||||
|
<Box className={classes.child}> |
||||||
|
<Anvils list={data} /> |
||||||
|
<Hosts anvil={data.anvils} /> |
||||||
|
</Box> |
||||||
|
<Box className={classes.server}> |
||||||
|
<Servers anvil={data.anvils} /> |
||||||
|
</Box> |
||||||
|
<Box className={classes.child}> |
||||||
|
<SharedStorage /> |
||||||
|
</Box> |
||||||
|
<Box className={classes.child}> |
||||||
|
<Network /> |
||||||
|
<CPU /> |
||||||
|
<Memory /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
)} |
||||||
|
</AnvilProvider> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Home; |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1 @@ |
|||||||
|
../../html/skins/alteeve/images/ |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,12 @@ |
|||||||
|
html, |
||||||
|
body { |
||||||
|
background-image: url('/pngs/Texture.jpg'); |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, |
||||||
|
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; |
||||||
|
} |
||||||
|
|
||||||
|
* { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
import createMuiTheme, { Theme } from '@material-ui/core/styles/createMuiTheme'; |
||||||
|
import { |
||||||
|
PANEL_BACKGROUND, |
||||||
|
TEXT, |
||||||
|
PURPLE, |
||||||
|
BLUE, |
||||||
|
DISABLED, |
||||||
|
BORDER_RADIUS, |
||||||
|
} from '../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
|
const theme: Theme = createMuiTheme({ |
||||||
|
palette: { |
||||||
|
primary: { |
||||||
|
main: PANEL_BACKGROUND, |
||||||
|
}, |
||||||
|
secondary: { |
||||||
|
main: TEXT, |
||||||
|
}, |
||||||
|
background: { |
||||||
|
paper: PANEL_BACKGROUND, |
||||||
|
}, |
||||||
|
}, |
||||||
|
typography: { |
||||||
|
fontFamily: 'Roboto Condensed', |
||||||
|
fontWeightRegular: 200, |
||||||
|
fontSize: 14, |
||||||
|
}, |
||||||
|
overrides: { |
||||||
|
MuiSwitch: { |
||||||
|
switchBase: { |
||||||
|
// Controls default (unchecked) color for the thumb
|
||||||
|
color: TEXT, |
||||||
|
}, |
||||||
|
root: { |
||||||
|
padding: 8, |
||||||
|
}, |
||||||
|
track: { |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
border: 3, |
||||||
|
backgroundColor: PURPLE, |
||||||
|
opacity: 1, |
||||||
|
'$checked$checked + &': { |
||||||
|
// Controls checked color for the track
|
||||||
|
backgroundColor: BLUE, |
||||||
|
opacity: 1, |
||||||
|
}, |
||||||
|
'$disabled$disabled + &': { |
||||||
|
backgroundColor: DISABLED, |
||||||
|
}, |
||||||
|
}, |
||||||
|
thumb: { |
||||||
|
color: TEXT, |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
export default theme; |
@ -0,0 +1,21 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"target": "es5", |
||||||
|
"lib": ["dom", "dom.iterable", "esnext"], |
||||||
|
"allowJs": true, |
||||||
|
"skipLibCheck": true, |
||||||
|
"strict": true, |
||||||
|
"forceConsistentCasingInFileNames": true, |
||||||
|
"noEmit": true, |
||||||
|
"esModuleInterop": true, |
||||||
|
"module": "esnext", |
||||||
|
"moduleResolution": "node", |
||||||
|
"resolveJsonModule": true, |
||||||
|
"isolatedModules": true, |
||||||
|
"jsx": "preserve", |
||||||
|
"noImplicitAny": true, |
||||||
|
"typeRoots": ["types"] |
||||||
|
}, |
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], |
||||||
|
"exclude": ["node_modules"] |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
declare type AnvilCPU = { |
||||||
|
cores: number; |
||||||
|
threads: number; |
||||||
|
allocated: number; |
||||||
|
}; |
@ -0,0 +1,16 @@ |
|||||||
|
declare type AnvilFileSystemHost = { |
||||||
|
host_uuid: string; |
||||||
|
host_name: string; |
||||||
|
is_mounted: boolean; |
||||||
|
total: number; |
||||||
|
free: number; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilFileSystem = { |
||||||
|
mount_point: string; |
||||||
|
hosts: Array<AnvilFileSystemHost>; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilFileSystems = { |
||||||
|
file_systems: Array<AnvilFileSystem>; |
||||||
|
}; |
@ -0,0 +1,8 @@ |
|||||||
|
declare type AnvilListItem = { |
||||||
|
anvil_name: string; |
||||||
|
anvil_uuid: string; |
||||||
|
} & AnvilStatus; |
||||||
|
|
||||||
|
declare type AnvilList = { |
||||||
|
anvils: Array<AnvilListItem>; |
||||||
|
}; |
@ -0,0 +1,5 @@ |
|||||||
|
declare type AnvilMemory = { |
||||||
|
total: number; |
||||||
|
allocated: number; |
||||||
|
reserved: number; |
||||||
|
}; |
@ -0,0 +1,45 @@ |
|||||||
|
declare type AnvilNetworkBondLink = { |
||||||
|
link_name: string; |
||||||
|
link_uuid: string; |
||||||
|
link_speed: number; |
||||||
|
link_state: 'optimal' | 'degraded' | 'down'; |
||||||
|
is_active: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilNetworkHostBond = { |
||||||
|
bond_name: string; |
||||||
|
bond_uuid: string; |
||||||
|
links: Array<AnvilNetworkBondLink>; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilNetworkHosts = { |
||||||
|
host_name: string; |
||||||
|
host_uuid: string; |
||||||
|
bonds: Array<AnvilNetworkHostBond>; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilNetwork = { |
||||||
|
hosts: Array<AnvilNetworkHosts>; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type ProcessedBond = { |
||||||
|
bond_name: string; |
||||||
|
bond_uuid: string; |
||||||
|
bond_speed: number; |
||||||
|
bond_state: 'optimal' | 'degraded' | 'down'; |
||||||
|
hosts: Array<{ |
||||||
|
host_name: string; |
||||||
|
host_uuid: string; |
||||||
|
link: { |
||||||
|
link_name: string; |
||||||
|
link_uuid: string; |
||||||
|
link_speed: number; |
||||||
|
link_state: 'optimal' | 'degraded' | 'down'; |
||||||
|
is_active: boolean; |
||||||
|
}; |
||||||
|
}>; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type ProcessedNetwork = { |
||||||
|
bonds: Array<ProcessedBond>; |
||||||
|
}; |
@ -0,0 +1,3 @@ |
|||||||
|
declare type AnvilNodeStatus = { |
||||||
|
on: 0 | 1; |
||||||
|
}; |
@ -0,0 +1,34 @@ |
|||||||
|
declare type AnvilConnection = { |
||||||
|
protocol: 'async_a' | 'sync_c'; |
||||||
|
targets: Array<{ |
||||||
|
target_name: string; |
||||||
|
states: { |
||||||
|
connection: string; |
||||||
|
disk: string; |
||||||
|
}; |
||||||
|
role: string; |
||||||
|
logical_volume_path: string; |
||||||
|
}>; |
||||||
|
resync?: { |
||||||
|
rate: number; |
||||||
|
percent_complete: number; |
||||||
|
time_remain: number; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilVolume = { |
||||||
|
index: number; |
||||||
|
drbd_device_path: string; |
||||||
|
drbd_device_minor: number; |
||||||
|
size: number; |
||||||
|
connections: Array<AnvilConnection>; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilResource = { |
||||||
|
resource_name: string; |
||||||
|
volumes: Array<AnvilVolume>; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilReplicatedStorage = { |
||||||
|
resources: Array<AnvilResource>; |
||||||
|
}; |
@ -0,0 +1,18 @@ |
|||||||
|
declare type AnvilServer = { |
||||||
|
server_name: string; |
||||||
|
server_uuid: string; |
||||||
|
server_state: |
||||||
|
| 'running' |
||||||
|
| 'idle' |
||||||
|
| 'paused' |
||||||
|
| 'in shutdown' |
||||||
|
| 'shut off' |
||||||
|
| 'crashed' |
||||||
|
| 'pmsuspended' |
||||||
|
| 'migrating'; |
||||||
|
server_host_uuid: string; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilServers = { |
||||||
|
servers: Array<AnvilServer>; |
||||||
|
}; |
@ -0,0 +1,4 @@ |
|||||||
|
declare type AnvilSet = { |
||||||
|
anvil_uuid: string; |
||||||
|
is_on: boolean; |
||||||
|
}; |
@ -0,0 +1,10 @@ |
|||||||
|
declare type AnvilSharedStorageGroup = { |
||||||
|
storage_group_name: string; |
||||||
|
storage_group_uuid: string; |
||||||
|
storage_group_total: number; |
||||||
|
storage_group_free: number; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilSharedStorage = { |
||||||
|
storage_groups: Array<AnvilSharedStorageGroup>; |
||||||
|
}; |
@ -0,0 +1,13 @@ |
|||||||
|
declare type AnvilStatusHost = { |
||||||
|
state: 'offline' | 'booted' | 'crmd' | 'in_ccm' | 'online'; |
||||||
|
host_uuid: string; |
||||||
|
host_name: string; |
||||||
|
state_percent: number; |
||||||
|
state_message: string; |
||||||
|
removable: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type AnvilStatus = { |
||||||
|
anvil_state: 'optimal' | 'not_ready' | 'degraded'; |
||||||
|
hosts: Array<AnvilStatusHost>; |
||||||
|
}; |
@ -0,0 +1 @@ |
|||||||
|
declare type AnvilTypes = AnvilList | AnvilCPU | AnvilMemory | AnvilStatus; |
@ -0,0 +1,4 @@ |
|||||||
|
declare type FetchResponse = { |
||||||
|
error: Error | null; |
||||||
|
isLoading?: boolean; |
||||||
|
}; |
@ -0,0 +1,3 @@ |
|||||||
|
declare type GetAllAnvilResponse = FetchResponse & { |
||||||
|
anvilList: AnvilList; |
||||||
|
}; |
@ -0,0 +1,3 @@ |
|||||||
|
declare type GetOneAnvilResponse = FetchResponse & { |
||||||
|
anvilStatus: AnvilStatus; |
||||||
|
}; |
@ -0,0 +1,5 @@ |
|||||||
|
declare type GetResponses = { |
||||||
|
data: T; |
||||||
|
isLoading: boolean; |
||||||
|
isError: Error; |
||||||
|
}; |
@ -0,0 +1,11 @@ |
|||||||
|
declare type HostSet = { |
||||||
|
host_uuid: string; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type HostSetPower = HostSet & { |
||||||
|
is_on: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
declare type HostSetMembership = HostSet & { |
||||||
|
is_membership: boolean; |
||||||
|
}; |
Loading…
Reference in new issue