restart prometheus ui

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>
This commit is contained in:
Augustin Husson 2024-02-21 11:58:29 +01:00
parent aba0071480
commit 34d5c8dfaf
244 changed files with 5120 additions and 73102 deletions

18
web/ui/app/.eslintrc.cjs Normal file
View file

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
web/ui/app/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

30
web/ui/app/README.md Normal file
View file

@ -0,0 +1,30 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

13
web/ui/app/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2734
web/ui/app/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

39
web/ui/app/package.json Normal file
View file

@ -0,0 +1,39 @@
{
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@codemirror/autocomplete": "^6.11.1",
"@codemirror/commands": "^6.3.2",
"@codemirror/language": "^6.9.3",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.3.3",
"@codemirror/view": "^6.22.1",
"@lezer/common": "^1.2.1",
"@lezer/highlight": "^1.2.0",
"@mantine/code-highlight": "^7.5.3",
"@mantine/core": "^7.5.3",
"@mantine/dates": "^7.5.3",
"@mantine/hooks": "^7.5.3",
"@prometheus-io/codemirror-promql": "0.49.1",
"@uiw/react-codemirror": "^4.21.22",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5"
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
web/ui/app/src/App.css Normal file
View file

@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

20
web/ui/app/src/App.tsx Normal file
View file

@ -0,0 +1,20 @@
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { PromQLExtension } from "@prometheus-io/codemirror-promql";
const promqlExtension = new PromQLExtension();
function App() {
return (
<CodeMirror
basicSetup={false}
value="rate(foo)"
editable={false}
extensions={[
promqlExtension.asExtension(),
EditorView.lineWrapping,
]}
/>
)
}
export default App

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4 KiB

68
web/ui/app/src/index.css Normal file
View file

@ -0,0 +1,68 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

10
web/ui/app/src/main.tsx Normal file
View file

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

1
web/ui/app/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

25
web/ui/app/tsconfig.json Normal file
View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View file

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})

View file

@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { FetchFn } from '.';
import { FetchFn } from './index';
import { Matcher } from '../types';
import { labelMatchersToString } from '../parser';
import LRUCache from 'lru-cache';

View file

@ -32,7 +32,8 @@
"devDependencies": {
"@lezer/generator": "^1.5.1",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.3.14"
"@lezer/lr": "^1.3.14",
"@rollup/plugin-node-resolve": "^15.2.3"
},
"peerDependencies": {
"@lezer/highlight": "^1.1.2",

19737
web/ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,32 +1,22 @@
{
"name": "prometheus-io",
"description": "Monorepo for the Prometheus UI",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "GENERATE_SOURCEMAP=false bash build_ui.sh --all",
"build": "npm run build -w app",
"build:module": "bash build_ui.sh --build-module",
"start": "npm run start -w react-app",
"test": "npm run test --workspaces",
"lint": "npm run lint --workspaces"
"start": "npm run start -w app"
},
"workspaces": [
"react-app",
"module/*"
"app",
"codemirror-promql",
"lezer-promql"
],
"engines": {
"npm": ">=7.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/node": "^20.10.4",
"eslint-config-prettier": "^8.10.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^4.2.1",
"jest-canvas-mock": "^2.5.2",
"jest-fetch-mock": "^3.0.3",
"prettier": "^2.8.8",
"react-scripts": "^5.0.1",
"ts-jest": "^29.1.1",
"typescript": "^4.9.5"
},
"version": "0.49.1"
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"typescript": "^5.2.2",
"vite": "^5.1.0"
}
}

View file

@ -1,3 +0,0 @@
# This ensures that all links in the generated asset bundle will be relative,
# so that assets are loaded correctly even when a path prefix is used.
PUBLIC_URL=.

View file

@ -1,32 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"react-app",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"rules": {
"@typescript-eslint/explicit-function-return-type": ["off"],
"eol-last": [
"error",
"always"
],
"object-curly-spacing": [
"error",
"always"
],
"prefer-const": "warn",
"comma-dangle": [
"error",
{
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline"
}
]
},
"plugins": [
"prettier"
],
"ignorePatterns": ["src/vendor/**"]
}

View file

@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View file

@ -1,99 +0,0 @@
{
"name": "@prometheus-io/app",
"version": "0.49.1",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.11.1",
"@codemirror/commands": "^6.3.2",
"@codemirror/language": "^6.9.3",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.3.3",
"@codemirror/view": "^6.22.1",
"@forevolve/bootstrap-dark": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/react-fontawesome": "0.2.0",
"@lezer/common": "^1.1.1",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.3.14",
"@nexucis/fuzzy": "^0.4.1",
"@nexucis/kvsearch": "^0.8.1",
"@prometheus-io/codemirror-promql": "0.49.1",
"bootstrap": "^4.6.2",
"css.escape": "^1.5.1",
"downshift": "^7.6.2",
"http-proxy-middleware": "^2.0.6",
"jquery": "^3.7.1",
"jquery.flot.tooltip": "^0.9.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"popper.js": "^1.14.3",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2",
"react-infinite-scroll-component": "^6.1.0",
"react-resize-detector": "^7.1.2",
"react-router-dom": "^5.3.4",
"react-test-renderer": "^17.0.2",
"reactstrap": "^8.10.1",
"sanitize-html": "^2.11.0",
"sass": "1.69.5",
"tempusdominus-bootstrap-4": "^5.39.2",
"tempusdominus-core": "^5.19.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --runInBand --resetMocks=false",
"test:coverage": "react-scripts test --runInBand --resetMocks=false --no-watch --coverage",
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"eject": "react-scripts eject",
"lint:ci": "eslint --quiet \"src/**/*.{ts,tsx}\"",
"lint": "eslint --fix \"src/**/*.{ts,tsx}\"",
"snapshot": "react-scripts test --updateSnapshot"
},
"prettier": {
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 125
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"devDependencies": {
"@testing-library/react-hooks": "^7.0.2",
"@types/enzyme": "^3.10.18",
"@types/flot": "0.0.36",
"@types/jquery": "^3.5.29",
"@types/react": "^17.0.71",
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^17.0.25",
"@types/react-router-dom": "^5.3.3",
"@types/sanitize-html": "^2.9.5",
"@types/sinon": "^10.0.20",
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"mutationobserver-shim": "^0.3.7",
"sinon": "^14.0.2"
},
"jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"transformIgnorePatterns": [
"<rootDir>/../node_modules/(?!@prometheus-io/codemirror-promql)/",
"<rootDir>/../node_modules/(?!@prometheus-io/lezer-promql)/"
],
"moduleNameMapper": {
"lezer-promql": "<rootDir>/../node_modules/@prometheus-io/lezer-promql/dist/index.cjs"
}
},
"optionalDependencies": {
"fsevents": "^2.3.3"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,63 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<!--
Placeholders replaced by Prometheus during serving:
- GLOBAL_CONSOLES_LINK is replaced and set to the consoles link if it exists.
It will render a "Consoles" link in the navbar when it is non-empty.
- PROMETHEUS_AGENT_MODE is replaced by a boolean indicating if Prometheus is running in agent mode.
It true, it will disable querying capacities in the UI and generally adapt the UI to the agent mode.
It has to be represented as a string, because booleans can be mangled to !1 in production builds.
- PROMETHEUS_READY is replaced by a boolean indicating whether Prometheus was ready at the time the
web app was served. It has to be represented as a string, because booleans can be mangled to !1 in
production builds.
-->
<script>
const GLOBAL_CONSOLES_LINK='CONSOLES_LINK_PLACEHOLDER';
const GLOBAL_AGENT_MODE='AGENT_MODE_PLACEHOLDER';
const GLOBAL_READY='READY_PLACEHOLDER';
</script>
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" crossorigin="use-credentials"/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<!--
The TITLE_PLACEHOLDER magic value is replaced during serving by Prometheus.
We need it dynamic because it can be overridden by the command line flag `web.page-title`.
-->
<title>TITLE_PLACEHOLDER</title>
</head>
<body class="bootstrap">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View file

@ -1,15 +0,0 @@
{
"short_name": "Prometheus UI",
"name": "Prometheus Server Web Interface",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -1,45 +0,0 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import App from './App';
import Navigation from './Navbar';
import { Container } from 'reactstrap';
import { Route } from 'react-router-dom';
import {
AgentPage,
AlertsPage,
ConfigPage,
FlagsPage,
RulesPage,
ServiceDiscoveryPage,
StatusPage,
TargetsPage,
TSDBStatusPage,
PanelListPage,
} from './pages';
describe('App', () => {
const app = shallow(<App consolesLink={null} agentMode={false} ready={false} />);
it('navigates', () => {
expect(app.find(Navigation)).toHaveLength(1);
});
it('routes', () => {
[
AgentPage,
AlertsPage,
ConfigPage,
FlagsPage,
RulesPage,
ServiceDiscoveryPage,
StatusPage,
TargetsPage,
TSDBStatusPage,
PanelListPage,
].forEach((component) => {
const c = app.find(component);
expect(c).toHaveLength(1);
});
expect(app.find(Route)).toHaveLength(10);
expect(app.find(Container)).toHaveLength(1);
});
});

View file

@ -1,130 +0,0 @@
import { FC } from 'react';
import { Container } from 'reactstrap';
import Navigation from './Navbar';
import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-dom';
import { PathPrefixContext } from './contexts/PathPrefixContext';
import { ThemeContext, themeName, themeSetting } from './contexts/ThemeContext';
import { ReadyContext } from './contexts/ReadyContext';
import { AnimateLogoContext } from './contexts/AnimateLogoContext';
import { useLocalStorage } from './hooks/useLocalStorage';
import useMedia from './hooks/useMedia';
import {
AgentPage,
AlertsPage,
ConfigPage,
FlagsPage,
PanelListPage,
RulesPage,
ServiceDiscoveryPage,
StatusPage,
TargetsPage,
TSDBStatusPage,
} from './pages';
import { Theme, themeLocalStorageKey } from './Theme';
interface AppProps {
consolesLink: string | null;
agentMode: boolean;
ready: boolean;
}
const App: FC<AppProps> = ({ consolesLink, agentMode, ready }) => {
// This dynamically/generically determines the pathPrefix by stripping the first known
// endpoint suffix from the window location path. It works out of the box for both direct
// hosting and reverse proxy deployments with no additional configurations required.
let basePath = window.location.pathname;
const paths = [
'/agent',
'/graph',
'/alerts',
'/status',
'/tsdb-status',
'/flags',
'/config',
'/rules',
'/targets',
'/service-discovery',
];
if (basePath.endsWith('/')) {
basePath = basePath.slice(0, -1);
}
if (basePath.length > 1) {
for (let i = 0; i < paths.length; i++) {
if (basePath.endsWith(paths[i])) {
basePath = basePath.slice(0, basePath.length - paths[i].length);
break;
}
}
}
const [userTheme, setUserTheme] = useLocalStorage<themeSetting>(themeLocalStorageKey, 'auto');
const browserHasThemes = useMedia('(prefers-color-scheme)');
const browserWantsDarkTheme = useMedia('(prefers-color-scheme: dark)');
const [animateLogo, setAnimateLogo] = useLocalStorage<boolean>('animateLogo', false);
let theme: themeName;
if (userTheme !== 'auto') {
theme = userTheme;
} else {
theme = browserHasThemes ? (browserWantsDarkTheme ? 'dark' : 'light') : 'light';
}
return (
<ThemeContext.Provider
value={{ theme: theme, userPreference: userTheme, setTheme: (t: themeSetting) => setUserTheme(t) }}
>
<Theme />
<PathPrefixContext.Provider value={basePath}>
<ReadyContext.Provider value={ready}>
<Router basename={basePath}>
<AnimateLogoContext.Provider value={animateLogo}>
<Navigation consolesLink={consolesLink} agentMode={agentMode} animateLogo={animateLogo} />
<Container fluid style={{ paddingTop: 70 }}>
<Switch>
<Redirect exact from="/" to={agentMode ? '/agent' : '/graph'} />
{/*
NOTE: Any route added here needs to also be added to the list of
React-handled router paths ("reactRouterPaths") in /web/web.go.
*/}
<Route path="/agent">
<AgentPage />
</Route>
<Route path="/graph">
<PanelListPage />
</Route>
<Route path="/alerts">
<AlertsPage />
</Route>
<Route path="/config">
<ConfigPage />
</Route>
<Route path="/flags">
<FlagsPage />
</Route>
<Route path="/rules">
<RulesPage />
</Route>
<Route path="/service-discovery">
<ServiceDiscoveryPage />
</Route>
<Route path="/status">
<StatusPage agentMode={agentMode} setAnimateLogo={setAnimateLogo} />
</Route>
<Route path="/tsdb-status">
<TSDBStatusPage />
</Route>
<Route path="/targets">
<TargetsPage />
</Route>
</Switch>
</Container>
</AnimateLogoContext.Provider>
</Router>
</ReadyContext.Provider>
</PathPrefixContext.Provider>
</ThemeContext.Provider>
);
};
export default App;

View file

@ -1,30 +0,0 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import Navigation from './Navbar';
import { NavItem, NavLink } from 'reactstrap';
describe('Navbar should contain console Link', () => {
it('with non-empty consoleslink', () => {
const app = shallow(<Navigation consolesLink="/path/consoles" agentMode={false} />);
expect(
app.contains(
<NavItem>
<NavLink href="/path/consoles">Consoles</NavLink>
</NavItem>
)
).toBeTruthy();
});
});
describe('Navbar should not contain consoles link', () => {
it('with empty string in consolesLink', () => {
const app = shallow(<Navigation consolesLink={null} agentMode={false} />);
expect(
app.contains(
<NavItem>
<NavLink>Consoles</NavLink>
</NavItem>
)
).toBeFalsy();
});
});

View file

@ -1,97 +0,0 @@
import React, { FC, useState } from 'react';
import { Link } from 'react-router-dom';
import {
Collapse,
Navbar,
NavbarToggler,
Nav,
NavItem,
NavLink,
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
} from 'reactstrap';
import { ThemeToggle } from './Theme';
import { ReactComponent as PromLogo } from './images/prometheus_logo_grey.svg';
interface NavbarProps {
consolesLink: string | null;
agentMode: boolean;
animateLogo?: boolean | false;
}
const Navigation: FC<NavbarProps> = ({ consolesLink, agentMode, animateLogo }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
return (
<Navbar className="mb-3" dark color="dark" expand="md" fixed="top">
<NavbarToggler onClick={toggle} className="mr-2" />
<Link className="pt-0 navbar-brand" to={agentMode ? '/agent' : '/graph'}>
<PromLogo className={`d-inline-block align-top${animateLogo ? ' animate' : ''}`} title="Prometheus" />
Prometheus{agentMode && ' Agent'}
</Link>
<Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}>
<Nav className="ml-0" navbar>
{consolesLink !== null && (
<NavItem>
<NavLink href={consolesLink}>Consoles</NavLink>
</NavItem>
)}
{!agentMode && (
<>
<NavItem>
<NavLink tag={Link} to="/alerts">
Alerts
</NavLink>
</NavItem>
<NavItem>
<NavLink tag={Link} to="/graph">
Graph
</NavLink>
</NavItem>
</>
)}
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>
Status
</DropdownToggle>
<DropdownMenu>
<DropdownItem tag={Link} to="/status">
Runtime & Build Information
</DropdownItem>
{!agentMode && (
<DropdownItem tag={Link} to="/tsdb-status">
TSDB Status
</DropdownItem>
)}
<DropdownItem tag={Link} to="/flags">
Command-Line Flags
</DropdownItem>
<DropdownItem tag={Link} to="/config">
Configuration
</DropdownItem>
{!agentMode && (
<DropdownItem tag={Link} to="/rules">
Rules
</DropdownItem>
)}
<DropdownItem tag={Link} to="/targets">
Targets
</DropdownItem>
<DropdownItem tag={Link} to="/service-discovery">
Service Discovery
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
<NavItem>
<NavLink href="https://prometheus.io/docs/prometheus/latest/getting_started/">Help</NavLink>
</NavItem>
</Nav>
</Collapse>
<ThemeToggle />
</Navbar>
);
};
export default Navigation;

View file

@ -1,48 +0,0 @@
import React, { FC, useEffect } from 'react';
import { Form, Button, ButtonGroup } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faMoon, faSun, faAdjust } from '@fortawesome/free-solid-svg-icons';
import { useTheme } from './contexts/ThemeContext';
export const themeLocalStorageKey = 'user-prefers-color-scheme';
export const Theme: FC = () => {
const { theme } = useTheme();
useEffect(() => {
document.body.classList.toggle('bootstrap-dark', theme === 'dark');
document.body.classList.toggle('bootstrap', theme === 'light');
}, [theme]);
return null;
};
export const ThemeToggle: FC = () => {
const { userPreference, setTheme } = useTheme();
return (
<Form className="ml-auto" inline>
<ButtonGroup size="sm">
<Button
color="secondary"
title="Use light theme"
active={userPreference === 'light'}
onClick={() => setTheme('light')}
>
<FontAwesomeIcon icon={faSun} className={userPreference === 'light' ? 'text-white' : 'text-dark'} />
</Button>
<Button color="secondary" title="Use dark theme" active={userPreference === 'dark'} onClick={() => setTheme('dark')}>
<FontAwesomeIcon icon={faMoon} className={userPreference === 'dark' ? 'text-white' : 'text-dark'} />
</Button>
<Button
color="secondary"
title="Use browser-preferred theme"
active={userPreference === 'auto'}
onClick={() => setTheme('auto')}
>
<FontAwesomeIcon icon={faAdjust} className={userPreference === 'auto' ? 'text-white' : 'text-dark'} />
</Button>
</ButtonGroup>
</Form>
);
};

View file

@ -1,65 +0,0 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import Checkbox from './Checkbox';
import { FormGroup, Label, Input } from 'reactstrap';
const MockCmp: React.FC = () => <div className="mock" />;
describe('Checkbox', () => {
it('renders with subcomponents', () => {
const checkBox = shallow(<Checkbox />);
[FormGroup, Input, Label].forEach((component) => expect(checkBox.find(component)).toHaveLength(1));
});
it('passes down the correct FormGroup props', () => {
const checkBoxProps = { wrapperStyles: { color: 'orange' } };
const checkBox = shallow(<Checkbox {...checkBoxProps} />);
const formGroup = checkBox.find(FormGroup);
expect(Object.keys(formGroup.props())).toHaveLength(4);
expect(formGroup.prop('className')).toEqual('custom-control custom-checkbox');
expect(formGroup.prop('children')).toHaveLength(2);
expect(formGroup.prop('style')).toEqual({ color: 'orange' });
expect(formGroup.prop('tag')).toEqual('div');
});
it('passes down the correct FormGroup Input props', () => {
const results: string[] = [];
const checkBoxProps = {
onChange: (): void => {
results.push('clicked');
},
};
const checkBox = shallow(<Checkbox {...checkBoxProps} id="1" />);
const input = checkBox.find(Input);
expect(Object.keys(input.props())).toHaveLength(4);
expect(input.prop('className')).toEqual('custom-control-input');
expect(input.prop('id')).toMatch('1');
expect(input.prop('type')).toEqual('checkbox');
input.simulate('change');
expect(results).toHaveLength(1);
expect(results[0]).toEqual('clicked');
});
it('passes down the correct Label props', () => {
const checkBox = shallow(
<Checkbox id="1">
<MockCmp />
</Checkbox>
);
const label = checkBox.find(Label);
expect(Object.keys(label.props())).toHaveLength(6);
expect(label.prop('className')).toEqual('custom-control-label');
expect(label.find(MockCmp)).toHaveLength(1);
expect(label.prop('for')).toMatch('1');
expect(label.prop('style')).toEqual({ userSelect: 'none' });
expect(label.prop('tag')).toEqual('label');
});
it('shares checkbox `id` uuid with Input/Label subcomponents', () => {
const checkBox = shallow(<Checkbox id="2" />);
const input = checkBox.find(Input);
const label = checkBox.find(Label);
expect(label.prop('for')).toBeDefined();
expect(label.prop('for')).toEqual(input.prop('id'));
});
});

View file

@ -1,19 +0,0 @@
import React, { FC, memo, CSSProperties } from 'react';
import { FormGroup, Label, Input, InputProps } from 'reactstrap';
interface CheckboxProps extends InputProps {
wrapperStyles?: CSSProperties;
}
const Checkbox: FC<CheckboxProps> = ({ children, wrapperStyles, id, ...rest }) => {
return (
<FormGroup className="custom-control custom-checkbox" style={wrapperStyles}>
<Input {...rest} id={id} type="checkbox" className="custom-control-input" />
<Label style={{ userSelect: 'none' }} className="custom-control-label" for={id}>
{children}
</Label>
</FormGroup>
);
};
export default memo(Checkbox);

View file

@ -1,50 +0,0 @@
import { ComponentType, useEffect, useState } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
const initialNumberOfItemsDisplayed = 50;
export interface InfiniteScrollItemsProps<T> {
items: T[];
}
interface CustomInfiniteScrollProps<T> {
allItems: T[];
child: ComponentType<InfiniteScrollItemsProps<T>>;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
const CustomInfiniteScroll = <T,>({ allItems, child }: CustomInfiniteScrollProps<T>) => {
const [items, setItems] = useState<T[]>(allItems.slice(0, 50));
const [index, setIndex] = useState<number>(initialNumberOfItemsDisplayed);
const [hasMore, setHasMore] = useState<boolean>(allItems.length > initialNumberOfItemsDisplayed);
const Child = child;
useEffect(() => {
setItems(allItems.slice(0, initialNumberOfItemsDisplayed));
setHasMore(allItems.length > initialNumberOfItemsDisplayed);
}, [allItems]);
const fetchMoreData = () => {
if (items.length === allItems.length) {
setHasMore(false);
} else {
const newIndex = index + initialNumberOfItemsDisplayed;
setIndex(newIndex);
setItems(allItems.slice(0, newIndex));
}
};
return (
<InfiniteScroll
next={fetchMoreData}
hasMore={hasMore}
loader={<h4>loading...</h4>}
dataLength={items.length}
height={items.length > 25 ? '75vh' : ''}
>
<Child items={items} />
</InfiniteScroll>
);
};
export default CustomInfiniteScroll;

View file

@ -1,36 +0,0 @@
import React, { ChangeEvent, FC, useEffect } from 'react';
import { Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
export interface SearchBarProps {
handleChange: (e: string) => void;
placeholder: string;
defaultValue: string;
}
const SearchBar: FC<SearchBarProps> = ({ handleChange, placeholder, defaultValue }) => {
let filterTimeout: NodeJS.Timeout;
const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
clearTimeout(filterTimeout);
filterTimeout = setTimeout(() => {
handleChange(e.target.value);
}, 300);
};
useEffect(() => {
handleChange(defaultValue);
}, [defaultValue, handleChange]);
return (
<InputGroup>
<InputGroupAddon addonType="prepend">
<InputGroupText>{<FontAwesomeIcon icon={faSearch} />}</InputGroupText>
</InputGroupAddon>
<Input autoFocus onChange={handleSearchChange} placeholder={placeholder} defaultValue={defaultValue} />
</InputGroup>
);
};
export default SearchBar;

View file

@ -1,28 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Button } from 'reactstrap';
import { ToggleMoreLess } from './ToggleMoreLess';
describe('ToggleMoreLess', () => {
const showMoreValue = false;
const defaultProps = {
event: (): void => {
tggleBtn.setProps({ showMore: !showMoreValue });
},
showMore: showMoreValue,
};
const tggleBtn = shallow(<ToggleMoreLess {...defaultProps} />);
it('renders a show more btn at start', () => {
const btn = tggleBtn.find(Button);
expect(btn).toHaveLength(1);
expect(btn.prop('color')).toEqual('primary');
expect(btn.prop('size')).toEqual('xs');
expect(btn.render().text()).toEqual('show more');
});
it('renders a show less btn if clicked', () => {
tggleBtn.find(Button).simulate('click');
expect(tggleBtn.find(Button).render().text()).toEqual('show less');
});
});

View file

@ -1,28 +0,0 @@
import React, { FC } from 'react';
import { Button } from 'reactstrap';
interface ToggleMoreLessProps {
event(): void;
showMore: boolean;
}
export const ToggleMoreLess: FC<ToggleMoreLessProps> = ({ children, event, showMore }) => {
return (
<h3>
{children}
<Button
size="xs"
onClick={event}
style={{
padding: '0.3em 0.3em 0.25em 0.3em',
fontSize: '0.375em',
marginLeft: '1em',
verticalAlign: 'baseline',
}}
color="primary"
>
show {showMore ? 'less' : 'more'}
</Button>
</h3>
);
};

View file

@ -1,68 +0,0 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { WALReplayData } from '../types/types';
import { StartingContent } from './withStartingIndicator';
import { Alert, Progress } from 'reactstrap';
describe('Starting', () => {
describe('progress bar', () => {
it('does not show when replay not started', () => {
const status: WALReplayData = {
min: 0,
max: 0,
current: 0,
};
const starting = shallow(<StartingContent status={status} isUnexpected={false} />);
const progress = starting.find(Progress);
expect(progress).toHaveLength(0);
});
it('shows progress bar when max is not 0', () => {
const status: WALReplayData = {
min: 0,
max: 1,
current: 0,
};
const starting = shallow(<StartingContent status={status} isUnexpected={false} />);
const progress = starting.find(Progress);
expect(progress).toHaveLength(1);
});
it('renders progress correctly', () => {
const status: WALReplayData = {
min: 0,
max: 20,
current: 1,
};
const starting = shallow(<StartingContent status={status} isUnexpected={false} />);
const progress = starting.find(Progress);
expect(progress.prop('value')).toBe(2);
expect(progress.prop('min')).toBe(0);
expect(progress.prop('max')).toBe(21);
});
it('shows done when replay done', () => {
const status: WALReplayData = {
min: 0,
max: 20,
current: 20,
};
const starting = shallow(<StartingContent status={status} isUnexpected={false} />);
const progress = starting.find(Progress);
expect(progress.prop('value')).toBe(21);
expect(progress.prop('color')).toBe('success');
});
it('shows unexpected error', () => {
const status: WALReplayData = {
min: 0,
max: 20,
current: 0,
};
const starting = shallow(<StartingContent status={status} isUnexpected={true} />);
const alert = starting.find(Alert);
expect(alert.prop('color')).toBe('danger');
});
});
});

View file

@ -1,59 +0,0 @@
import React, { FC, ComponentType } from 'react';
import { Progress, Alert } from 'reactstrap';
import { useFetchReadyInterval } from '../hooks/useFetch';
import { WALReplayData } from '../types/types';
import { usePathPrefix } from '../contexts/PathPrefixContext';
import { useReady } from '../contexts/ReadyContext';
interface StartingContentProps {
isUnexpected: boolean;
status?: WALReplayData;
}
export const StartingContent: FC<StartingContentProps> = ({ status, isUnexpected }) => {
if (isUnexpected) {
return (
<Alert color="danger">
<strong>Error:</strong> Server is not responding
</Alert>
);
}
return (
<div className="text-center m-3">
<div className="m-4">
<h2>Starting up...</h2>
{status && status.max > 0 ? (
<div>
<p>
Replaying WAL ({status.current}/{status.max})
</p>
<Progress
animated
value={status.current - status.min + 1}
min={status.min}
max={status.max - status.min + 1}
color={status.max === status.current ? 'success' : undefined}
style={{ width: '10%', margin: 'auto' }}
/>
</div>
) : null}
</div>
</div>
);
};
export const withStartingIndicator =
<T extends Record<string, unknown>>(Page: ComponentType<T>): FC<T> =>
({ ...rest }) => {
const pathPrefix = usePathPrefix();
const { ready, walReplayStatus, isUnexpected } = useFetchReadyInterval(pathPrefix);
const staticReady = useReady();
if (staticReady || ready) {
return <Page {...(rest as T)} />;
}
return <StartingContent isUnexpected={isUnexpected} status={walReplayStatus.data} />;
};

View file

@ -1,44 +0,0 @@
import React, { FC, ComponentType } from 'react';
import { Alert } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
interface StatusIndicatorProps {
error?: Error;
isLoading?: boolean;
customErrorMsg?: JSX.Element;
componentTitle?: string;
}
export const withStatusIndicator =
<T extends Record<string, any>>( // eslint-disable-line @typescript-eslint/no-explicit-any
Component: ComponentType<T>
): FC<StatusIndicatorProps & T> =>
({ error, isLoading, customErrorMsg, componentTitle, ...rest }) => {
if (error) {
return (
<Alert color="danger">
{customErrorMsg ? (
customErrorMsg
) : (
<>
<strong>Error:</strong> Error fetching {componentTitle || Component.displayName}: {error.message}
</>
)}
</Alert>
);
}
if (isLoading) {
return (
<FontAwesomeIcon
size="3x"
icon={faSpinner}
spin
className="position-absolute"
style={{ transform: 'translate(-50%, -50%)', top: '50%', left: '50%' }}
/>
);
}
return <Component {...(rest as T)} />;
};

View file

@ -1 +0,0 @@
export const API_PATH = 'api/v1';

View file

@ -1,3 +0,0 @@
import { createContext } from 'react';
export const AnimateLogoContext = createContext<boolean>(false);

View file

@ -1,9 +0,0 @@
import React from 'react';
const PathPrefixContext = React.createContext('');
function usePathPrefix(): string {
return React.useContext(PathPrefixContext);
}
export { usePathPrefix, PathPrefixContext };

View file

@ -1,9 +0,0 @@
import React from 'react';
const ReadyContext = React.createContext(false);
function useReady(): boolean {
return React.useContext(ReadyContext);
}
export { useReady, ReadyContext };

View file

@ -1,22 +0,0 @@
import React from 'react';
export type themeName = 'light' | 'dark';
export type themeSetting = themeName | 'auto';
export interface ThemeCtx {
theme: themeName;
userPreference: themeSetting;
setTheme: (t: themeSetting) => void;
}
// defaults, will be overridden in App.tsx
export const ThemeContext = React.createContext<ThemeCtx>({
theme: 'light',
userPreference: 'auto',
// eslint-disable-next-line @typescript-eslint/no-empty-function
setTheme: (s: themeSetting) => {},
});
export const useTheme = (): ThemeCtx => {
return React.useContext(ThemeContext);
};

View file

@ -1,11 +0,0 @@
import React from 'react';
const ToastContext = React.createContext((msg: string) => {
return;
});
function useToastContext() {
return React.useContext(ToastContext);
}
export { useToastContext, ToastContext };

View file

@ -1,6 +0,0 @@
import jquery from 'jquery';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).jQuery = jquery;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).moment = require('moment');

View file

@ -1,114 +0,0 @@
import { useState, useEffect } from 'react';
import { API_PATH } from '../constants/constants';
import { WALReplayStatus } from '../types/types';
export type APIResponse<T> = { status: string; data: T };
export interface FetchState<T> {
response: APIResponse<T>;
error?: Error;
isLoading: boolean;
}
export interface FetchStateReadyInterval {
ready: boolean;
isUnexpected: boolean;
walReplayStatus: WALReplayStatus;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useFetch = <T extends Record<string, any>>(url: string, options?: RequestInit): FetchState<T> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [response, setResponse] = useState<APIResponse<T>>({ status: 'start fetching' } as any);
const [error, setError] = useState<Error>();
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const res = await fetch(url, { cache: 'no-store', credentials: 'same-origin', ...options });
if (!res.ok) {
throw new Error(res.statusText);
}
const json = (await res.json()) as APIResponse<T>;
setResponse(json);
setIsLoading(false);
} catch (err: unknown) {
const error = err as Error;
setError(error);
}
};
fetchData();
}, [url, options]);
return { response, error, isLoading };
};
let wasReady = false;
// This is used on the starting page to periodically check if the server is ready yet,
// and check the status of the WAL replay.
export const useFetchReadyInterval = (pathPrefix: string, options?: RequestInit): FetchStateReadyInterval => {
const [ready, setReady] = useState<boolean>(false);
const [isUnexpected, setIsUnexpected] = useState<boolean>(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [walReplayStatus, setWALReplayStatus] = useState<WALReplayStatus>({} as any);
useEffect(() => {
if (wasReady) {
setReady(true);
} else {
// This helps avoid a memory leak.
let mounted = true;
const fetchStatus = async () => {
try {
let res = await fetch(`${pathPrefix}/-/ready`, { cache: 'no-store', credentials: 'same-origin', ...options });
if (res.status === 200) {
if (mounted) {
setReady(true);
}
wasReady = true;
clearInterval(interval);
} else if (res.status !== 503) {
if (mounted) {
setIsUnexpected(true);
}
clearInterval(interval);
return;
} else {
if (mounted) {
setIsUnexpected(false);
}
res = await fetch(`${pathPrefix}/${API_PATH}/status/walreplay`, {
cache: 'no-store',
credentials: 'same-origin',
});
if (res.ok) {
const data = (await res.json()) as WALReplayStatus;
if (mounted) {
setWALReplayStatus(data);
}
}
}
} catch (error) {
if (mounted) {
setIsUnexpected(true);
}
clearInterval(interval);
return;
}
};
fetchStatus();
const interval = setInterval(fetchStatus, 1000);
return () => {
clearInterval(interval);
mounted = false;
};
}
}, [pathPrefix, options]);
return { ready, isUnexpected, walReplayStatus };
};

View file

@ -1,41 +0,0 @@
import { useLocalStorage } from './useLocalStorage';
import { renderHook, act } from '@testing-library/react-hooks';
describe('useLocalStorage', () => {
it('returns the initialState', () => {
const initialState = { a: 1, b: 2 };
const { result } = renderHook(() => useLocalStorage('mystorage', initialState));
expect(result.current[0]).toEqual(initialState);
});
it('stores the initialState as serialized json in localstorage', () => {
const key = 'mystorage';
const initialState = { a: 1, b: 2 };
renderHook(() => useLocalStorage(key, initialState));
expect(localStorage.getItem(key)).toEqual(JSON.stringify(initialState));
});
it('returns a setValue function that can reset local storage', () => {
const key = 'mystorage';
const initialState = { a: 1, b: 2 };
const { result } = renderHook(() => useLocalStorage(key, initialState));
const newValue = { a: 2, b: 5 };
act(() => {
result.current[1](newValue);
});
expect(result.current[0]).toEqual(newValue);
expect(localStorage.getItem(key)).toEqual(JSON.stringify(newValue));
});
it('localStorage.getItem calls once', () => {
// do not prepare the initial state on every render except the first
const spyStorage = jest.spyOn(Storage.prototype, 'getItem') as jest.Mock;
const key = 'mystorage';
const initialState = { a: 1, b: 2 };
const { result } = renderHook(() => useLocalStorage(key, initialState));
const newValue = { a: 2, b: 5 };
act(() => {
result.current[1](newValue);
});
expect(spyStorage).toHaveBeenCalledTimes(1);
spyStorage.mockReset();
});
});

Some files were not shown because too many files have changed in this diff Show more