mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-09 23:24:05 -08:00
Implement the /flags page in react (#6248)
* Implement the /flags page in react Signed-off-by: Chris Marchbanks <csmarchbanks@gmail.com> * Use custom react hook for calling api Signed-off-by: Chris Marchbanks <csmarchbanks@gmail.com>
This commit is contained in:
parent
74726367cf
commit
f17a0e17aa
|
@ -64,9 +64,12 @@
|
|||
"@types/flot": "0.0.31",
|
||||
"@types/moment-timezone": "^0.5.10",
|
||||
"@types/reactstrap": "^8.0.5",
|
||||
"@types/sinon": "^7.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "2.x",
|
||||
"@typescript-eslint/parser": "2.x",
|
||||
"babel-eslint": "10.x",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.15.1",
|
||||
"eslint": "6.x",
|
||||
"eslint-config-prettier": "^6.4.0",
|
||||
"eslint-config-react-app": "^5.0.2",
|
||||
|
@ -76,10 +79,8 @@
|
|||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-react": "7.x",
|
||||
"eslint-plugin-react-hooks": "1.x",
|
||||
"jest-fetch-mock": "^2.1.2",
|
||||
"prettier": "^1.18.2",
|
||||
"@types/sinon": "^7.5.0",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.15.1",
|
||||
"sinon": "^7.5.0"
|
||||
},
|
||||
"proxy": "http://localhost:9090"
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
import React, { FC, useEffect, useState } from 'react';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import { Alert, Button } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import { useFetch } from '../utils/useFetch';
|
||||
|
||||
import './Config.css';
|
||||
|
||||
const Config: FC<RouteComponentProps> = () => {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const { response, error } = useFetch('../api/v1/status/config');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('../api/v1/status/config')
|
||||
.then(res => res.json())
|
||||
.then(res => setConfig(res.data.yaml))
|
||||
.catch(error => setError(error.message));
|
||||
}, []);
|
||||
|
||||
const config = response && response.data.yaml;
|
||||
return (
|
||||
<>
|
||||
<h2>
|
||||
|
@ -38,7 +32,7 @@ const Config: FC<RouteComponentProps> = () => {
|
|||
|
||||
{error ? (
|
||||
<Alert color="danger">
|
||||
<strong>Error:</strong> Error fetching configuration: {error}
|
||||
<strong>Error:</strong> Error fetching configuration: {error.message}
|
||||
</Alert>
|
||||
) : config ? (
|
||||
<pre className="config-yaml">{config}</pre>
|
||||
|
|
111
web/ui/react-app/src/pages/Flags.test.tsx
Normal file
111
web/ui/react-app/src/pages/Flags.test.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import * as React from 'react';
|
||||
import { mount, shallow, ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import Flags, { FlagMap } from './Flags';
|
||||
import { Alert, Table } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const sampleFlagsResponse: {
|
||||
status: string;
|
||||
data: FlagMap;
|
||||
} = {
|
||||
status: 'success',
|
||||
data: {
|
||||
'alertmanager.notification-queue-capacity': '10000',
|
||||
'alertmanager.timeout': '10s',
|
||||
'config.file': './documentation/examples/prometheus.yml',
|
||||
'log.format': 'logfmt',
|
||||
'log.level': 'info',
|
||||
'query.lookback-delta': '5m',
|
||||
'query.max-concurrency': '20',
|
||||
'query.max-samples': '50000000',
|
||||
'query.timeout': '2m',
|
||||
'rules.alert.for-grace-period': '10m',
|
||||
'rules.alert.for-outage-tolerance': '1h',
|
||||
'rules.alert.resend-delay': '1m',
|
||||
'storage.remote.flush-deadline': '1m',
|
||||
'storage.remote.read-concurrent-limit': '10',
|
||||
'storage.remote.read-max-bytes-in-frame': '1048576',
|
||||
'storage.remote.read-sample-limit': '50000000',
|
||||
'storage.tsdb.allow-overlapping-blocks': 'false',
|
||||
'storage.tsdb.max-block-duration': '36h',
|
||||
'storage.tsdb.min-block-duration': '2h',
|
||||
'storage.tsdb.no-lockfile': 'false',
|
||||
'storage.tsdb.path': 'data/',
|
||||
'storage.tsdb.retention': '0s',
|
||||
'storage.tsdb.retention.size': '0B',
|
||||
'storage.tsdb.retention.time': '0s',
|
||||
'storage.tsdb.wal-compression': 'false',
|
||||
'storage.tsdb.wal-segment-size': '0B',
|
||||
'web.console.libraries': 'console_libraries',
|
||||
'web.console.templates': 'consoles',
|
||||
'web.cors.origin': '.*',
|
||||
'web.enable-admin-api': 'false',
|
||||
'web.enable-lifecycle': 'false',
|
||||
'web.external-url': '',
|
||||
'web.listen-address': '0.0.0.0:9090',
|
||||
'web.max-connections': '512',
|
||||
'web.page-title': 'Prometheus Time Series Collection and Processing Server',
|
||||
'web.read-timeout': '5m',
|
||||
'web.route-prefix': '/',
|
||||
'web.user-assets': '',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Flags', () => {
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
});
|
||||
|
||||
describe('before data is returned', () => {
|
||||
it('renders a spinner', () => {
|
||||
const flags = shallow(<Flags />);
|
||||
const icon = flags.find(FontAwesomeIcon);
|
||||
expect(icon.prop('icon')).toEqual(faSpinner);
|
||||
expect(icon.prop('spin')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is returned', () => {
|
||||
it('renders a table', async () => {
|
||||
const mock = fetch.mockResponse(JSON.stringify(sampleFlagsResponse));
|
||||
|
||||
let flags: ReactWrapper;
|
||||
await act(async () => {
|
||||
flags = mount(<Flags />);
|
||||
});
|
||||
flags.update();
|
||||
|
||||
expect(mock).toHaveBeenCalledWith('../api/v1/status/flags', undefined);
|
||||
const table = flags.find(Table);
|
||||
expect(table.prop('striped')).toBe(true);
|
||||
|
||||
const rows = flags.find('tr');
|
||||
const keys = Object.keys(sampleFlagsResponse.data);
|
||||
expect(rows.length).toBe(keys.length);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const row = rows.at(i);
|
||||
expect(row.find('th').text()).toBe(keys[i]);
|
||||
expect(row.find('td').text()).toBe(sampleFlagsResponse.data[keys[i]]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an error is returned', () => {
|
||||
it('displays an alert', async () => {
|
||||
const mock = fetch.mockReject(new Error('error loading flags'));
|
||||
|
||||
let flags: ReactWrapper;
|
||||
await act(async () => {
|
||||
flags = mount(<Flags />);
|
||||
});
|
||||
flags.update();
|
||||
|
||||
expect(mock).toHaveBeenCalledWith('../api/v1/status/flags', undefined);
|
||||
const alert = flags.find(Alert);
|
||||
expect(alert.prop('color')).toBe('danger');
|
||||
expect(alert.text()).toContain('error loading flags');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,50 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import { Alert, Table } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useFetch } from '../utils/useFetch';
|
||||
|
||||
const Flags: FC<RouteComponentProps> = () => <div>Flags page</div>;
|
||||
export interface FlagMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const Flags: FC<RouteComponentProps> = () => {
|
||||
const { response, error } = useFetch('../api/v1/status/flags');
|
||||
|
||||
const body = () => {
|
||||
const flags: FlagMap = response && response.data;
|
||||
if (error) {
|
||||
return (
|
||||
<Alert color="danger">
|
||||
<strong>Error:</strong> Error fetching flags: {error.message}
|
||||
</Alert>
|
||||
);
|
||||
} else if (flags) {
|
||||
return (
|
||||
<Table bordered={true} size="sm" striped={true}>
|
||||
<tbody>
|
||||
{Object.keys(flags).map(key => {
|
||||
return (
|
||||
<tr key={key}>
|
||||
<th>{key}</th>
|
||||
<td>{flags[key]}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
return <FontAwesomeIcon icon={faSpinner} spin />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Command-Line Flags</h2>
|
||||
{body()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Flags;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { configure } from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import { GlobalWithFetchMock } from 'jest-fetch-mock';
|
||||
import './globals';
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;
|
||||
customGlobal.fetch = require('jest-fetch-mock');
|
||||
customGlobal.fetchMock = customGlobal.fetch;
|
||||
|
|
27
web/ui/react-app/src/utils/useFetch.ts
Normal file
27
web/ui/react-app/src/utils/useFetch.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const useFetch = (url: string, options?: RequestInit) => {
|
||||
const [response, setResponse] = useState();
|
||||
const [error, setError] = useState();
|
||||
const [isLoading, setIsLoading] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
const json = await res.json();
|
||||
setResponse(json);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [url, options]);
|
||||
|
||||
return { response, error, isLoading };
|
||||
};
|
|
@ -3195,6 +3195,14 @@ create-react-context@^0.3.0:
|
|||
gud "^1.0.0"
|
||||
warning "^4.0.3"
|
||||
|
||||
cross-fetch@^2.2.2:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.3.tgz#e8a0b3c54598136e037f8650f8e823ccdfac198e"
|
||||
integrity sha512-PrWWNH3yL2NYIb/7WF/5vFG3DCQiXDOVf8k3ijatbrtnwNuhMWLC7YF7uqf53tbTFDzHIUD8oITw4Bxt8ST3Nw==
|
||||
dependencies:
|
||||
node-fetch "2.1.2"
|
||||
whatwg-fetch "2.0.4"
|
||||
|
||||
cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
|
@ -6024,6 +6032,14 @@ jest-environment-node@^24.9.0:
|
|||
jest-mock "^24.9.0"
|
||||
jest-util "^24.9.0"
|
||||
|
||||
jest-fetch-mock@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-2.1.2.tgz#1260b347918e3931c4ec743ceaf60433da661bd0"
|
||||
integrity sha512-tcSR4Lh2bWLe1+0w/IwvNxeDocMI/6yIA2bijZ0fyWxC4kQ18lckQ1n7Yd40NKuisGmcGBRFPandRXrW/ti/Bw==
|
||||
dependencies:
|
||||
cross-fetch "^2.2.2"
|
||||
promise-polyfill "^7.1.1"
|
||||
|
||||
jest-get-type@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
|
||||
|
@ -7230,6 +7246,11 @@ no-case@^2.2.0:
|
|||
dependencies:
|
||||
lower-case "^1.1.1"
|
||||
|
||||
node-fetch@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
|
||||
integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
|
||||
|
||||
node-fetch@^1.0.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
|
@ -8700,6 +8721,11 @@ promise-inflight@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
||||
|
||||
promise-polyfill@^7.1.1:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-7.1.2.tgz#ab05301d8c28536301622d69227632269a70ca3b"
|
||||
integrity sha512-FuEc12/eKqqoRYIGBrUptCBRhobL19PS2U31vMNTfyck1FxPyMfgsXyW4Mav85y/ZN1hop3hOwRlUDok23oYfQ==
|
||||
|
||||
promise@8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6"
|
||||
|
@ -11018,6 +11044,11 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
|
|||
dependencies:
|
||||
iconv-lite "0.4.24"
|
||||
|
||||
whatwg-fetch@2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
|
||||
integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==
|
||||
|
||||
whatwg-fetch@3.0.0, whatwg-fetch@>=0.10.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
|
||||
|
|
Loading…
Reference in a new issue