Merge branch 'master' into anvil-tools-dev

main
Digimer 4 years ago committed by GitHub
commit 930b0b6729
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      Makefile.am
  2. 2
      anvil.spec.in
  3. 3
      cgi-bin/Makefile.am
  4. 0
      cgi-bin/cold_stop
  5. 9
      cgi-bin/get_anvils
  6. 9
      cgi-bin/get_cpu
  7. 11
      cgi-bin/get_memory
  8. 9
      cgi-bin/get_networks
  9. 9
      cgi-bin/get_replicated_storage
  10. 9
      cgi-bin/get_servers
  11. 9
      cgi-bin/get_shared_storage
  12. 9
      cgi-bin/get_status
  13. 9
      cgi-bin/set_membership
  14. 9
      cgi-bin/set_power
  15. 1
      configure.ac
  16. 1
      share/words.xml
  17. 5
      striker-ui/.eslintignore
  18. 63
      striker-ui/.eslintrc.json
  19. 34
      striker-ui/.gitignore
  20. 1
      striker-ui/.husky/.gitignore
  21. 6
      striker-ui/.husky/commit-msg
  22. 7
      striker-ui/.husky/pre-commit
  23. 4
      striker-ui/.lintstagedrc.json
  24. 6
      striker-ui/.prettierrc.json
  25. 136
      striker-ui/Makefile.am
  26. 34
      striker-ui/README.md
  27. 3
      striker-ui/commitlint.config.js
  28. 29
      striker-ui/components/AnvilContext.tsx
  29. 79
      striker-ui/components/AnvilDrawer.tsx
  30. 15
      striker-ui/components/Anvils/Anvil.tsx
  31. 83
      striker-ui/components/Anvils/AnvilList.tsx
  32. 83
      striker-ui/components/Anvils/SelectedAnvil.tsx
  33. 28
      striker-ui/components/Anvils/index.tsx
  34. 14
      striker-ui/components/Anvils/sortAnvils.ts
  35. 61
      striker-ui/components/Bars/AllocationBar.tsx
  36. 57
      striker-ui/components/Bars/ProgressBar.tsx
  37. 4
      striker-ui/components/Bars/index.tsx
  38. 39
      striker-ui/components/CPU.tsx
  39. 37
      striker-ui/components/Decorator.tsx
  40. 77
      striker-ui/components/FileSystem/FileSystems.tsx
  41. 80
      striker-ui/components/FileSystem/FileSystemsHost.tsx
  42. 3
      striker-ui/components/FileSystem/index.tsx
  43. 86
      striker-ui/components/Header.tsx
  44. 155
      striker-ui/components/Hosts/AnvilHost.tsx
  45. 41
      striker-ui/components/Hosts/index.tsx
  46. 69
      striker-ui/components/Memory.tsx
  47. 123
      striker-ui/components/Network/Network.tsx
  48. 3
      striker-ui/components/Network/index.tsx
  49. 47
      striker-ui/components/Network/processNetwork.ts
  50. 29
      striker-ui/components/Panels/InnerPanel.tsx
  51. 59
      striker-ui/components/Panels/Panel.tsx
  52. 40
      striker-ui/components/Panels/PanelHeader.tsx
  53. 5
      striker-ui/components/Panels/index.tsx
  54. 152
      striker-ui/components/Servers.tsx
  55. 64
      striker-ui/components/SharedStorage/SharedStorage.tsx
  56. 74
      striker-ui/components/SharedStorage/SharedStorageHost.tsx
  57. 3
      striker-ui/components/SharedStorage/index.tsx
  58. 29
      striker-ui/components/Spinner.tsx
  59. 50
      striker-ui/components/Storage.tsx
  60. 36
      striker-ui/components/Text/BodyText.tsx
  61. 15
      striker-ui/components/Text/HeaderText.tsx
  62. 4
      striker-ui/components/Text/index.tsx
  63. 2
      striker-ui/env.development
  64. 7
      striker-ui/lib/consts/ANVILS.ts
  65. 7
      striker-ui/lib/consts/API_BASE_URL.ts
  66. 16
      striker-ui/lib/consts/DEFAULT_THEME.ts
  67. 42
      striker-ui/lib/consts/ICONS.ts
  68. 3
      striker-ui/lib/consts/IS_DEV_ENV.ts
  69. 18
      striker-ui/lib/consts/NODES.ts
  70. 12
      striker-ui/lib/consts/SERVERS.ts
  71. 18
      striker-ui/lib/extended_date/ExtendedDate.ts
  72. 5
      striker-ui/lib/fetchers/fetchJSON.ts
  73. 18
      striker-ui/lib/fetchers/periodicFetch.ts
  74. 11
      striker-ui/lib/fetchers/putJSON.ts
  75. 7
      striker-ui/lib/sanitizers/hostsSanitizer.ts
  76. 2
      striker-ui/next-env.d.ts
  77. 5
      striker-ui/next.config.js
  78. 6445
      striker-ui/package-lock.json
  79. 46
      striker-ui/package.json
  80. 28
      striker-ui/pages/_app.tsx
  81. 37
      striker-ui/pages/_document.tsx
  82. 82
      striker-ui/pages/index.tsx
  83. BIN
      striker-ui/public/favicon.ico
  84. 1
      striker-ui/public/pngs
  85. 4
      striker-ui/public/vercel.svg
  86. 12
      striker-ui/styles/globals.css
  87. 59
      striker-ui/theme/index.ts
  88. 21
      striker-ui/tsconfig.json
  89. 5
      striker-ui/types/AnvilCPU.d.ts
  90. 16
      striker-ui/types/AnvilFileSystems.d.ts
  91. 8
      striker-ui/types/AnvilList.d.ts
  92. 5
      striker-ui/types/AnvilMemory.d.ts
  93. 45
      striker-ui/types/AnvilNetwork.d.ts
  94. 3
      striker-ui/types/AnvilNodeStatus.d.ts
  95. 34
      striker-ui/types/AnvilReplicatedStorage.d.ts
  96. 18
      striker-ui/types/AnvilServers.d.ts
  97. 4
      striker-ui/types/AnvilSet.d.ts
  98. 10
      striker-ui/types/AnvilSharedStorage.d.ts
  99. 13
      striker-ui/types/AnvilStatus.d.ts
  100. 1
      striker-ui/types/AnvilTypes.d.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -21,7 +21,8 @@ TARFILES = $(PACKAGE_NAME)-$(VERSION).tar.bz2 \
ACLOCAL_AMFLAGS = -I m4 ACLOCAL_AMFLAGS = -I m4
SUBDIRS = Anvil cgi-bin html journald.conf.d man ocf \ SUBDIRS = Anvil cgi-bin html journald.conf.d man ocf \
pxe scancore-agents scripts share tools units pxe scancore-agents scripts share striker-ui \
tools units
anvilconfdir = ${sysconfdir}/anvil anvilconfdir = ${sysconfdir}/anvil

@ -23,6 +23,8 @@ BuildRequires: systemd autoconf automake
BuildRequires: fence-agents-common BuildRequires: fence-agents-common
# OCFROOT # OCFROOT
BuildRequires: resource-agents BuildRequires: resource-agents
# Require the "npm" tool for building the web UI.
BuildRequires: npm >= 6
%description %description
This package generates the anvil-core, anvil-striker, anvil-node and anvil-dr This package generates the anvil-core, anvil-striker, anvil-node and anvil-dr

@ -2,11 +2,10 @@ MAINTAINERCLEANFILES = Makefile.in
cgibindir = $(localstatedir)/www/cgi-bin cgibindir = $(localstatedir)/www/cgi-bin
dist_cgibin_SCRIPTS = \ dist_cgibin_SCRIPTS = \
cold_stop \
get_anvil_status \
get_anvils \ get_anvils \
get_cpu \ get_cpu \
get_memory \ get_memory \
get_networks \
get_replicated_storage \ get_replicated_storage \
get_servers \ get_servers \
get_shared_storage \ get_shared_storage \

@ -31,6 +31,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();

@ -31,6 +31,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();

@ -31,6 +31,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();
@ -40,7 +49,7 @@ $anvil->Database->get_anvils();
print $anvil->Template->get({file => "shared.html", name => "json_headers", show_name => 0})."\n"; print $anvil->Template->get({file => "shared.html", name => "json_headers", show_name => 0})."\n";
my $target = $anvil->Get->short_host_name(); my $target = $anvil->Get->short_host_name();
my $hash = { reserved => 68719476736 }; my $hash = { reserved => 8589934592 };
my $anvil_uuid = ""; my $anvil_uuid = "";
if ($anvil->data->{cgi}{anvil_uuid}{value}) if ($anvil->data->{cgi}{anvil_uuid}{value})
{ {

@ -202,6 +202,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();

@ -31,6 +31,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();

@ -31,6 +31,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();

@ -113,6 +113,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();

@ -31,6 +31,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();

@ -166,6 +166,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();

@ -125,6 +125,15 @@ if (not $anvil->data->{sys}{database}{connections})
$anvil->nice_exit({exit_code => 1}); $anvil->nice_exit({exit_code => 1});
} }
my $cookie_problem = $anvil->Account->read_cookies();
# Don't do anything data-related if the user is not logged in.
if ($cookie_problem)
{
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" });
$anvil->nice_exit({ exit_code => 1 });
}
# Read in any CGI variables, if needed. # Read in any CGI variables, if needed.
$anvil->Get->cgi(); $anvil->Get->cgi();

@ -165,6 +165,7 @@ AC_CONFIG_FILES([Makefile
scancore-agents/Makefile scancore-agents/Makefile
scripts/Makefile scripts/Makefile
share/Makefile share/Makefile
striker-ui/Makefile
tools/Makefile tools/Makefile
units/Makefile]) units/Makefile])

@ -418,6 +418,7 @@ The attempt to start the servers appears to have failed. The return code '0' was
<key name="error_0304">Failed to parse the request body: [#!variable!request_body_string!#]. Reason: [#!variable!json_decode_error!#]</key> <key name="error_0304">Failed to parse the request body: [#!variable!request_body_string!#]. Reason: [#!variable!json_decode_error!#]</key>
<key name="error_0305">Unable to connect to the database, unable to manage a server at this time.</key> <key name="error_0305">Unable to connect to the database, unable to manage a server at this time.</key>
<key name="error_0306">Unable to connect to the database, unable to provision a server at this time.</key> <key name="error_0306">Unable to connect to the database, unable to provision a server at this time.</key>
<key name="error_0307">Failed to perform requested task(s) because the requester is not authenticated.</key>
<!-- Files templates --> <!-- Files templates -->
<!-- NOTE: Translating these files requires an understanding of which lines are translatable --> <!-- NOTE: Translating these files requires an understanding of which lines are translatable -->

@ -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,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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1 @@
../../html/skins/alteeve/images/

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

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…
Cancel
Save