commit
930b0b6729
105 changed files with 9266 additions and 4 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; |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue