From f17a0e17aa1d40880ff6be7de2097602e7931183 Mon Sep 17 00:00:00 2001 From: Chris Marchbanks Date: Sat, 2 Nov 2019 03:27:36 -0600 Subject: [PATCH] Implement the /flags page in react (#6248) * Implement the /flags page in react Signed-off-by: Chris Marchbanks * Use custom react hook for calling api Signed-off-by: Chris Marchbanks --- web/ui/react-app/package.json | 7 +- web/ui/react-app/src/pages/Config.tsx | 16 +--- web/ui/react-app/src/pages/Flags.test.tsx | 111 ++++++++++++++++++++++ web/ui/react-app/src/pages/Flags.tsx | 46 ++++++++- web/ui/react-app/src/setupTests.ts | 4 + web/ui/react-app/src/utils/useFetch.ts | 27 ++++++ web/ui/react-app/yarn.lock | 31 ++++++ 7 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 web/ui/react-app/src/pages/Flags.test.tsx create mode 100644 web/ui/react-app/src/utils/useFetch.ts diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json index 770bdcd4f..66c12eda9 100644 --- a/web/ui/react-app/package.json +++ b/web/ui/react-app/package.json @@ -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" diff --git a/web/ui/react-app/src/pages/Config.tsx b/web/ui/react-app/src/pages/Config.tsx index a531a5fdb..f2e2e6e13 100644 --- a/web/ui/react-app/src/pages/Config.tsx +++ b/web/ui/react-app/src/pages/Config.tsx @@ -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 = () => { - 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 ( <>

@@ -38,7 +32,7 @@ const Config: FC = () => { {error ? ( - Error: Error fetching configuration: {error} + Error: Error fetching configuration: {error.message} ) : config ? (
{config}
diff --git a/web/ui/react-app/src/pages/Flags.test.tsx b/web/ui/react-app/src/pages/Flags.test.tsx new file mode 100644 index 000000000..3d721454b --- /dev/null +++ b/web/ui/react-app/src/pages/Flags.test.tsx @@ -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(); + 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.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.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'); + }); + }); +}); diff --git a/web/ui/react-app/src/pages/Flags.tsx b/web/ui/react-app/src/pages/Flags.tsx index dc5d8cc1b..d67e86c18 100644 --- a/web/ui/react-app/src/pages/Flags.tsx +++ b/web/ui/react-app/src/pages/Flags.tsx @@ -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 = () =>
Flags page
; +export interface FlagMap { + [key: string]: string; +} + +const Flags: FC = () => { + const { response, error } = useFetch('../api/v1/status/flags'); + + const body = () => { + const flags: FlagMap = response && response.data; + if (error) { + return ( + + Error: Error fetching flags: {error.message} + + ); + } else if (flags) { + return ( + + + {Object.keys(flags).map(key => { + return ( + + + + + ); + })} + +
{key}{flags[key]}
+ ); + } + return ; + }; + + return ( + <> +

Command-Line Flags

+ {body()} + + ); +}; export default Flags; diff --git a/web/ui/react-app/src/setupTests.ts b/web/ui/react-app/src/setupTests.ts index 08699661f..962ec1c82 100644 --- a/web/ui/react-app/src/setupTests.ts +++ b/web/ui/react-app/src/setupTests.ts @@ -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; diff --git a/web/ui/react-app/src/utils/useFetch.ts b/web/ui/react-app/src/utils/useFetch.ts new file mode 100644 index 000000000..b25266c68 --- /dev/null +++ b/web/ui/react-app/src/utils/useFetch.ts @@ -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 }; +}; diff --git a/web/ui/react-app/yarn.lock b/web/ui/react-app/yarn.lock index c8a025cd6..b2cab3bbc 100644 --- a/web/ui/react-app/yarn.lock +++ b/web/ui/react-app/yarn.lock @@ -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" -- GitLab