diff --git a/web/ui/react-app/src/ExpressionInput.tsx b/web/ui/react-app/src/ExpressionInput.tsx index b6fbe39a41..fb514884e0 100644 --- a/web/ui/react-app/src/ExpressionInput.tsx +++ b/web/ui/react-app/src/ExpressionInput.tsx @@ -98,7 +98,7 @@ class ExpressionInput extends Component + {string} ); diff --git a/web/ui/react-app/src/hooks/useFetches.test.tsx b/web/ui/react-app/src/hooks/useFetches.test.tsx deleted file mode 100644 index f874d67ef3..0000000000 --- a/web/ui/react-app/src/hooks/useFetches.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import useFetches from './useFetches'; -import { renderHook } from '@testing-library/react-hooks'; - -describe('useFetches', () => { - beforeEach(() => { - fetchMock.resetMocks(); - }); - it('should can handle multiple requests', async done => { - fetchMock.mockResponse(JSON.stringify({ satus: 'success', data: { id: 1 } })); - const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: ['/foo/bar', '/foo/bar', '/foo/bar'] }); - await waitForNextUpdate(); - expect(result.current.response).toHaveLength(3); - done(); - }); - it('should can handle success flow -> isLoading=true, response=[data, data], isLoading=false', async done => { - fetchMock.mockResponse(JSON.stringify({ satus: 'success', data: { id: 1 } })); - const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: ['/foo/bar'] }); - expect(result.current.isLoading).toEqual(true); - await waitForNextUpdate(); - expect(result.current.response).toHaveLength(1); - expect(result.current.isLoading).toEqual(false); - done(); - }); - it('should isLoading remains true on empty response', async done => { - fetchMock.mockResponse(jest.fn()); - const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: ['/foo/bar'] }); - expect(result.current.isLoading).toEqual(true); - await waitForNextUpdate(); - setTimeout(() => { - expect(result.current.isLoading).toEqual(true); - done(); - }, 1000); - }); - it('should set error message when response fail', async done => { - fetchMock.mockReject(new Error('errr')); - const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: ['/foo/bar'] }); - expect(result.current.isLoading).toEqual(true); - await waitForNextUpdate(); - expect(result.current.error!.message).toEqual('errr'); - expect(result.current.isLoading).toEqual(true); - done(); - }); - it('should throw an error if array is empty', async done => { - try { - useFetches([]); - const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: [] }); - await waitForNextUpdate().then(done); - expect(result.error.message).toEqual("Doesn't have url to fetch."); - done(); - } catch (e) { - } finally { - done(); - } - }); -}); diff --git a/web/ui/react-app/src/hooks/useFetches.tsx b/web/ui/react-app/src/hooks/useFetches.tsx deleted file mode 100644 index 24b2c08fd3..0000000000 --- a/web/ui/react-app/src/hooks/useFetches.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useState, useEffect } from 'react'; - -const useFetches = (urls: string[], options?: RequestInit) => { - if (!urls.length) { - throw new Error("Doesn't have url to fetch."); - } - const [response, setResponse] = useState(); - const [error, setError] = useState(); - - useEffect(() => { - const fetchData = async () => { - try { - const responses: R[] = await Promise.all( - urls - .map(async url => { - const res = await fetch(url, options); - if (!res.ok) { - throw new Error(res.statusText); - } - const result = await res.json(); - return result.data; - }) - .filter(Boolean) // Remove falsy values - ); - setResponse(responses); - } catch (error) { - setError(error); - } - }; - fetchData(); - }, [urls, options]); - - return { response, error, isLoading: !response || !response.length }; -}; - -export default useFetches; diff --git a/web/ui/react-app/src/pages/Config.tsx b/web/ui/react-app/src/pages/Config.tsx index 81804cbd86..55efda9770 100644 --- a/web/ui/react-app/src/pages/Config.tsx +++ b/web/ui/react-app/src/pages/Config.tsx @@ -1,26 +1,35 @@ -import React, { FC, useState } from 'react'; +import React, { useState, FC } 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 { Button } from 'reactstrap'; import CopyToClipboard from 'react-copy-to-clipboard'; -import { useFetch } from '../utils/useFetch'; import PathPrefixProps from '../PathPrefixProps'; import './Config.css'; +import { withStatusIndicator } from '../withStatusIndicator'; +import { useFetch } from '../utils/useFetch'; -const Config: FC = ({ pathPrefix }) => { - const { response, error } = useFetch(`${pathPrefix}/api/v1/status/config`); +type YamlConfig = { yaml?: string }; + +interface ConfigContentProps { + error?: Error; + data?: YamlConfig; +} + +const YamlContent = ({ yaml }: YamlConfig) =>
{yaml}
; +YamlContent.displayName = 'Config'; + +const ConfigWithStatusIndicator = withStatusIndicator(YamlContent); + +export const ConfigContent: FC = ({ error, data }) => { const [copied, setCopied] = useState(false); - - const config = response && response.data.yaml; + const config = data && data.yaml; return ( <>

Configuration  { + text={config!} + onCopy={(_, result) => { setCopied(result); setTimeout(setCopied, 1500); }} @@ -30,18 +39,14 @@ const Config: FC = ({ pathPrefix }) => {

- - {error ? ( - - Error: Error fetching configuration: {error.message} - - ) : config ? ( -
{config}
- ) : ( - - )} + ); }; +const Config: FC = ({ pathPrefix }) => { + const { response, error } = useFetch(`${pathPrefix}/api/v1/status/config`); + return ; +}; + export default Config; diff --git a/web/ui/react-app/src/pages/Flags.test.tsx b/web/ui/react-app/src/pages/Flags.test.tsx index f71ddaee3e..6c8c254c76 100644 --- a/web/ui/react-app/src/pages/Flags.test.tsx +++ b/web/ui/react-app/src/pages/Flags.test.tsx @@ -1,111 +1,65 @@ 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'; +import { shallow } from 'enzyme'; +import { FlagsContent } from './Flags'; +import { Table } from 'reactstrap'; +import toJson from 'enzyme-to-json'; -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': '', - }, +const sampleFlagsResponse = { + '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); + it('renders a table with properly configured props', () => { + const w = shallow(); + const table = w.find(Table); + expect(table.props()).toMatchObject({ + bordered: true, + size: 'sm', + striped: 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('/path/prefix/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]]); - } - }); + it('should not fail if data is missing', () => { + expect(shallow()).toHaveLength(1); }); - - 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('/path/prefix/api/v1/status/flags', undefined); - const alert = flags.find(Alert); - expect(alert.prop('color')).toBe('danger'); - expect(alert.text()).toContain('error loading flags'); - }); + it('should match snapshot', () => { + const w = shallow(); + expect(toJson(w)).toMatchSnapshot(); }); }); diff --git a/web/ui/react-app/src/pages/Flags.tsx b/web/ui/react-app/src/pages/Flags.tsx index cf62a6126b..c6c3d397c6 100644 --- a/web/ui/react-app/src/pages/Flags.tsx +++ b/web/ui/react-app/src/pages/Flags.tsx @@ -1,51 +1,42 @@ 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 { Table } from 'reactstrap'; +import { withStatusIndicator } from '../withStatusIndicator'; import { useFetch } from '../utils/useFetch'; import PathPrefixProps from '../PathPrefixProps'; -export interface FlagMap { +interface FlagMap { [key: string]: string; } -const Flags: FC = ({ pathPrefix }) => { - const { response, error } = useFetch(`${pathPrefix}/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 ; - }; +interface FlagsProps { + data?: FlagMap; +} +export const FlagsContent: FC = ({ data = {} }) => { return ( <>

Command-Line Flags

- {body()} + + + {Object.keys(data).map(key => ( + + + + + ))} + +
{key}{data[key]}
); }; +const FlagsWithStatusIndicator = withStatusIndicator(FlagsContent); + +FlagsContent.displayName = 'Flags'; + +const Flags: FC = ({ pathPrefix = '' }) => { + const { response, error, isLoading } = useFetch(`${pathPrefix}/api/v1/status/flags`); + return ; +}; export default Flags; diff --git a/web/ui/react-app/src/pages/Status.test.tsx b/web/ui/react-app/src/pages/Status.test.tsx index 89f81f2f30..537ff14b10 100644 --- a/web/ui/react-app/src/pages/Status.test.tsx +++ b/web/ui/react-app/src/pages/Status.test.tsx @@ -1,33 +1,15 @@ import * as React from 'react'; import { shallow } from 'enzyme'; -import { Status } from '.'; -import { Alert } from 'reactstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import * as useFetch from '../hooks/useFetches'; import toJson from 'enzyme-to-json'; +import { StatusContent } from './Status'; describe('Status', () => { - afterEach(() => jest.restoreAllMocks()); - it('should render spinner while waiting data', () => { - const wrapper = shallow(); - expect(wrapper.find(FontAwesomeIcon)).toHaveLength(1); - }); - it('should render Alert on error', () => { - (useFetch as any).default = jest.fn().mockImplementation(() => ({ error: new Error('foo') })); - const wrapper = shallow(); - expect(wrapper.find(Alert)).toHaveLength(1); - }); - it('should fetch proper API endpoints', () => { - const useFetchSpy = jest.spyOn(useFetch, 'default'); - shallow(); - expect(useFetchSpy).toHaveBeenCalledWith([ - '/path/prefix/api/v1/status/runtimeinfo', - '/path/prefix/api/v1/status/buildinfo', - '/path/prefix/api/v1/alertmanagers', - ]); + it('should not fail with undefined data', () => { + const wrapper = shallow(); + expect(wrapper).toHaveLength(1); }); describe('Snapshot testing', () => { - const response = [ + const response: any = [ { startTime: '2019-10-30T22:03:23.247913868+02:00', CWD: '/home/boyskila/Desktop/prometheus', @@ -63,8 +45,7 @@ describe('Status', () => { }, ]; it('should match table snapshot', () => { - (useFetch as any).default = jest.fn().mockImplementation(() => ({ response })); - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); jest.restoreAllMocks(); }); diff --git a/web/ui/react-app/src/pages/Status.tsx b/web/ui/react-app/src/pages/Status.tsx index 27f1dfef89..fb5c969734 100644 --- a/web/ui/react-app/src/pages/Status.tsx +++ b/web/ui/react-app/src/pages/Status.tsx @@ -1,20 +1,21 @@ -import React, { FC, Fragment } from 'react'; +import React, { Fragment, FC } from 'react'; import { RouteComponentProps } from '@reach/router'; -import { Table, Alert } from 'reactstrap'; -import useFetches from '../hooks/useFetches'; - -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { Table } from 'reactstrap'; +import { withStatusIndicator } from '../withStatusIndicator'; +import { useFetch } from '../utils/useFetch'; import PathPrefixProps from '../PathPrefixProps'; -const ENDPOINTS = ['/api/v1/status/runtimeinfo', '/api/v1/status/buildinfo', '/api/v1/alertmanagers']; const sectionTitles = ['Runtime Information', 'Build Information', 'Alertmanagers']; interface StatusConfig { - [k: string]: { title?: string; customizeValue?: (v: any) => any; customRow?: boolean; skip?: boolean }; + [k: string]: { title?: string; customizeValue?: (v: any, key: string) => any; customRow?: boolean; skip?: boolean }; } -type StatusPageState = Array<{ [k: string]: string }>; +type StatusPageState = { [k: string]: string }; + +interface StatusPageProps { + data?: StatusPageState[]; +} export const statusConfig: StatusConfig = { startTime: { title: 'Start time', customizeValue: (v: string) => new Date(v).toUTCString() }, @@ -31,9 +32,9 @@ export const statusConfig: StatusConfig = { storageRetention: { title: 'Storage retention' }, activeAlertmanagers: { customRow: true, - customizeValue: (alertMgrs: { url: string }[]) => { + customizeValue: (alertMgrs: { url: string }[], key) => { return ( - + Endpoint @@ -55,37 +56,8 @@ export const statusConfig: StatusConfig = { droppedAlertmanagers: { skip: true }, }; -const endpointsMemo: { [prefix: string]: string[] } = {}; - -const Status: FC = ({ pathPrefix = '' }) => { - if (!endpointsMemo[pathPrefix]) { - // TODO: Come up with a nicer solution for this? - // - // The problem is that there's an infinite reload loop if the endpoints array is - // reconstructed on every render, as the dependency checking in useFetches() - // then thinks that something has changed... the whole useFetches() should - // probably removed and solved differently (within the component?) somehow. - endpointsMemo[pathPrefix] = ENDPOINTS.map(ep => `${pathPrefix}${ep}`); - } - const { response: data, error, isLoading } = useFetches(endpointsMemo[pathPrefix]); - if (error) { - return ( - - Error: Error fetching status: {error.message} - - ); - } else if (isLoading) { - return ( - - ); - } - return data ? ( +export const StatusContent: FC = ({ data = [] }) => { + return ( <> {data.map((statuses, i) => { return ( @@ -93,20 +65,20 @@ const Status: FC = ({ pathPrefix = '' })

{sectionTitles[i]}

- {Object.entries(statuses).map(([k, v]) => { + {Object.entries(statuses).map(([k, v], i) => { const { title = k, customizeValue = (val: any) => val, customRow, skip } = statusConfig[k] || {}; if (skip) { return null; } if (customRow) { - return customizeValue(v); + return customizeValue(v, k); } return ( - + ); })} @@ -116,7 +88,30 @@ const Status: FC = ({ pathPrefix = '' }) ); })} - ) : null; + ); +}; +const StatusWithStatusIndicator = withStatusIndicator(StatusContent); + +StatusContent.displayName = 'Status'; + +const Status: FC = ({ pathPrefix = '' }) => { + const path = `${pathPrefix}/api/v1`; + const status = useFetch(`${path}/status/runtimeinfo`); + const runtime = useFetch(`${path}/status/buildinfo`); + const build = useFetch(`${path}/alertmanagers`); + + let data; + if (status.response.data && runtime.response.data && build.response.data) { + data = [status.response.data, runtime.response.data, build.response.data]; + } + + return ( + + ); }; export default Status; diff --git a/web/ui/react-app/src/pages/__snapshots__/Flags.test.tsx.snap b/web/ui/react-app/src/pages/__snapshots__/Flags.test.tsx.snap new file mode 100644 index 0000000000..ae38259866 --- /dev/null +++ b/web/ui/react-app/src/pages/__snapshots__/Flags.test.tsx.snap @@ -0,0 +1,395 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Flags should match snapshot 1`] = ` + +

+ Command-Line Flags +

+
{title} {customizeValue(v)}{customizeValue(v, title)}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 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 + +
+
+`; diff --git a/web/ui/react-app/src/utils/useFetch.ts b/web/ui/react-app/src/utils/useFetch.ts index b25266c683..7272e87ef9 100644 --- a/web/ui/react-app/src/utils/useFetch.ts +++ b/web/ui/react-app/src/utils/useFetch.ts @@ -1,9 +1,17 @@ import { useState, useEffect } from 'react'; -export const useFetch = (url: string, options?: RequestInit) => { - const [response, setResponse] = useState(); - const [error, setError] = useState(); - const [isLoading, setIsLoading] = useState(); +export type APIResponse = { status: string; data?: T }; + +export interface FetchState { + response: APIResponse; + error?: Error; + isLoading: boolean; +} + +export const useFetch = (url: string, options?: RequestInit): FetchState => { + const [response, setResponse] = useState>({ status: 'start fetching' }); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchData = async () => { @@ -13,7 +21,7 @@ export const useFetch = (url: string, options?: RequestInit) => { if (!res.ok) { throw new Error(res.statusText); } - const json = await res.json(); + const json = (await res.json()) as APIResponse; setResponse(json); setIsLoading(false); } catch (error) { @@ -22,6 +30,5 @@ export const useFetch = (url: string, options?: RequestInit) => { }; fetchData(); }, [url, options]); - return { response, error, isLoading }; }; diff --git a/web/ui/react-app/src/withStatusIndicator.tsx b/web/ui/react-app/src/withStatusIndicator.tsx new file mode 100644 index 0000000000..356a671dd3 --- /dev/null +++ b/web/ui/react-app/src/withStatusIndicator.tsx @@ -0,0 +1,44 @@ +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; +} + +export const withStatusIndicator = (Component: ComponentType): FC => ({ + error, + isLoading, + customErrorMsg, + ...rest +}) => { + if (error) { + return ( + + {customErrorMsg ? ( + customErrorMsg + ) : ( + <> + Error: Error fetching {Component.displayName}: {error.message} + + )} + + ); + } + + if (isLoading) { + return ( + + ); + } + return ; +};