Replace fetching hooks with class render prop component (#6267)

* replace fetching hooks with class render prop component

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* rename Fetcher

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* status page markup separated from fetcher component

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* fetch api reusability

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* extract Config and Flags pages as 'dumb' components

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* more components splitting

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* implement fetchWithstatus HOC

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* refactor changed files tests

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* switching back to hooks.

Signed-off-by: blalov <boyko.lalov@tick42.com>
Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* fetch response bug fix

Signed-off-by: Boyko Lalov <boyskila@gmail.com>

* make wrapped by withstatusIndicator components names consistent

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
This commit is contained in:
Boyko 2019-11-12 15:35:47 +02:00 committed by Julius Volz
parent a85e7aac0e
commit 8a9509b0a8
11 changed files with 606 additions and 325 deletions

View file

@ -98,7 +98,7 @@ class ExpressionInput extends Component<ExpressionInputProps, ExpressionInputSta
},
});
return (
<SanitizeHTML tag="li" {...itemProps} allowedTags={['strong']}>
<SanitizeHTML key={title} tag="li" {...itemProps} allowedTags={['strong']}>
{string}
</SanitizeHTML>
);

View file

@ -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();
}
});
});

View file

@ -1,36 +0,0 @@
import { useState, useEffect } from 'react';
const useFetches = <R extends any>(urls: string[], options?: RequestInit) => {
if (!urls.length) {
throw new Error("Doesn't have url to fetch.");
}
const [response, setResponse] = useState<R[]>();
const [error, setError] = useState<Error>();
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;

View file

@ -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<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
const { response, error } = useFetch(`${pathPrefix}/api/v1/status/config`);
type YamlConfig = { yaml?: string };
interface ConfigContentProps {
error?: Error;
data?: YamlConfig;
}
const YamlContent = ({ yaml }: YamlConfig) => <pre className="config-yaml">{yaml}</pre>;
YamlContent.displayName = 'Config';
const ConfigWithStatusIndicator = withStatusIndicator(YamlContent);
export const ConfigContent: FC<ConfigContentProps> = ({ error, data }) => {
const [copied, setCopied] = useState(false);
const config = response && response.data.yaml;
const config = data && data.yaml;
return (
<>
<h2>
Configuration&nbsp;
<CopyToClipboard
text={config ? config! : ''}
onCopy={(text, result) => {
text={config!}
onCopy={(_, result) => {
setCopied(result);
setTimeout(setCopied, 1500);
}}
@ -30,18 +39,14 @@ const Config: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
</Button>
</CopyToClipboard>
</h2>
{error ? (
<Alert color="danger">
<strong>Error:</strong> Error fetching configuration: {error.message}
</Alert>
) : config ? (
<pre className="config-yaml">{config}</pre>
) : (
<FontAwesomeIcon icon={faSpinner} spin />
)}
<ConfigWithStatusIndicator error={error} isLoading={!config} yaml={config} />
</>
);
};
const Config: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
const { response, error } = useFetch<YamlConfig>(`${pathPrefix}/api/v1/status/config`);
return <ConfigContent error={error} data={response.data} />;
};
export default Config;

View file

@ -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(<Flags />);
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(<FlagsContent data={sampleFlagsResponse} />);
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 pathPrefix="/path/prefix" />);
});
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(<FlagsContent />)).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 pathPrefix="/path/prefix" />);
});
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(<FlagsContent data={sampleFlagsResponse} />);
expect(toJson(w)).toMatchSnapshot();
});
});

View file

@ -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<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
const { response, error } = useFetch(`${pathPrefix}/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 />;
};
interface FlagsProps {
data?: FlagMap;
}
export const FlagsContent: FC<FlagsProps> = ({ data = {} }) => {
return (
<>
<h2>Command-Line Flags</h2>
{body()}
<Table bordered size="sm" striped>
<tbody>
{Object.keys(data).map(key => (
<tr key={key}>
<th>{key}</th>
<td>{data[key]}</td>
</tr>
))}
</tbody>
</Table>
</>
);
};
const FlagsWithStatusIndicator = withStatusIndicator(FlagsContent);
FlagsContent.displayName = 'Flags';
const Flags: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => {
const { response, error, isLoading } = useFetch<FlagMap>(`${pathPrefix}/api/v1/status/flags`);
return <FlagsWithStatusIndicator data={response.data} error={error} isLoading={isLoading} />;
};
export default Flags;

View file

@ -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(<Status />);
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(<Status />);
expect(wrapper.find(Alert)).toHaveLength(1);
});
it('should fetch proper API endpoints', () => {
const useFetchSpy = jest.spyOn(useFetch, 'default');
shallow(<Status pathPrefix="/path/prefix" />);
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(<StatusContent data={[]} />);
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(<Status />);
const wrapper = shallow(<StatusContent data={response} />);
expect(toJson(wrapper)).toMatchSnapshot();
jest.restoreAllMocks();
});

View file

@ -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 (
<Fragment key="alert-managers">
<Fragment key={key}>
<tr>
<th>Endpoint</th>
</tr>
@ -55,37 +56,8 @@ export const statusConfig: StatusConfig = {
droppedAlertmanagers: { skip: true },
};
const endpointsMemo: { [prefix: string]: string[] } = {};
const Status: FC<RouteComponentProps & PathPrefixProps> = ({ 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<StatusPageState[]>(endpointsMemo[pathPrefix]);
if (error) {
return (
<Alert color="danger">
<strong>Error:</strong> Error fetching status: {error.message}
</Alert>
);
} else if (isLoading) {
return (
<FontAwesomeIcon
size="3x"
icon={faSpinner}
spin
className="position-absolute"
style={{ transform: 'translate(-50%, -50%)', top: '50%', left: '50%' }}
/>
);
}
return data ? (
export const StatusContent: FC<StatusPageProps> = ({ data = [] }) => {
return (
<>
{data.map((statuses, i) => {
return (
@ -93,20 +65,20 @@ const Status: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' })
<h2>{sectionTitles[i]}</h2>
<Table className="h-auto" size="sm" bordered striped>
<tbody>
{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 (
<tr key={k}>
<th className="capitalize-title" style={{ width: '35%' }}>
{title}
</th>
<td className="text-break">{customizeValue(v)}</td>
<td className="text-break">{customizeValue(v, title)}</td>
</tr>
);
})}
@ -116,7 +88,30 @@ const Status: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' })
);
})}
</>
) : null;
);
};
const StatusWithStatusIndicator = withStatusIndicator(StatusContent);
StatusContent.displayName = 'Status';
const Status: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => {
const path = `${pathPrefix}/api/v1`;
const status = useFetch<StatusPageState>(`${path}/status/runtimeinfo`);
const runtime = useFetch<StatusPageState>(`${path}/status/buildinfo`);
const build = useFetch<StatusPageState>(`${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 (
<StatusWithStatusIndicator
data={data}
isLoading={status.isLoading || runtime.isLoading || build.isLoading}
error={status.error || runtime.error || build.error}
/>
);
};
export default Status;

View file

@ -0,0 +1,395 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Flags should match snapshot 1`] = `
<Fragment>
<h2>
Command-Line Flags
</h2>
<Table
bordered={true}
responsiveTag="div"
size="sm"
striped={true}
tag="table"
>
<tbody>
<tr
key="alertmanager.notification-queue-capacity"
>
<th>
alertmanager.notification-queue-capacity
</th>
<td>
10000
</td>
</tr>
<tr
key="alertmanager.timeout"
>
<th>
alertmanager.timeout
</th>
<td>
10s
</td>
</tr>
<tr
key="config.file"
>
<th>
config.file
</th>
<td>
./documentation/examples/prometheus.yml
</td>
</tr>
<tr
key="log.format"
>
<th>
log.format
</th>
<td>
logfmt
</td>
</tr>
<tr
key="log.level"
>
<th>
log.level
</th>
<td>
info
</td>
</tr>
<tr
key="query.lookback-delta"
>
<th>
query.lookback-delta
</th>
<td>
5m
</td>
</tr>
<tr
key="query.max-concurrency"
>
<th>
query.max-concurrency
</th>
<td>
20
</td>
</tr>
<tr
key="query.max-samples"
>
<th>
query.max-samples
</th>
<td>
50000000
</td>
</tr>
<tr
key="query.timeout"
>
<th>
query.timeout
</th>
<td>
2m
</td>
</tr>
<tr
key="rules.alert.for-grace-period"
>
<th>
rules.alert.for-grace-period
</th>
<td>
10m
</td>
</tr>
<tr
key="rules.alert.for-outage-tolerance"
>
<th>
rules.alert.for-outage-tolerance
</th>
<td>
1h
</td>
</tr>
<tr
key="rules.alert.resend-delay"
>
<th>
rules.alert.resend-delay
</th>
<td>
1m
</td>
</tr>
<tr
key="storage.remote.flush-deadline"
>
<th>
storage.remote.flush-deadline
</th>
<td>
1m
</td>
</tr>
<tr
key="storage.remote.read-concurrent-limit"
>
<th>
storage.remote.read-concurrent-limit
</th>
<td>
10
</td>
</tr>
<tr
key="storage.remote.read-max-bytes-in-frame"
>
<th>
storage.remote.read-max-bytes-in-frame
</th>
<td>
1048576
</td>
</tr>
<tr
key="storage.remote.read-sample-limit"
>
<th>
storage.remote.read-sample-limit
</th>
<td>
50000000
</td>
</tr>
<tr
key="storage.tsdb.allow-overlapping-blocks"
>
<th>
storage.tsdb.allow-overlapping-blocks
</th>
<td>
false
</td>
</tr>
<tr
key="storage.tsdb.max-block-duration"
>
<th>
storage.tsdb.max-block-duration
</th>
<td>
36h
</td>
</tr>
<tr
key="storage.tsdb.min-block-duration"
>
<th>
storage.tsdb.min-block-duration
</th>
<td>
2h
</td>
</tr>
<tr
key="storage.tsdb.no-lockfile"
>
<th>
storage.tsdb.no-lockfile
</th>
<td>
false
</td>
</tr>
<tr
key="storage.tsdb.path"
>
<th>
storage.tsdb.path
</th>
<td>
data/
</td>
</tr>
<tr
key="storage.tsdb.retention"
>
<th>
storage.tsdb.retention
</th>
<td>
0s
</td>
</tr>
<tr
key="storage.tsdb.retention.size"
>
<th>
storage.tsdb.retention.size
</th>
<td>
0B
</td>
</tr>
<tr
key="storage.tsdb.retention.time"
>
<th>
storage.tsdb.retention.time
</th>
<td>
0s
</td>
</tr>
<tr
key="storage.tsdb.wal-compression"
>
<th>
storage.tsdb.wal-compression
</th>
<td>
false
</td>
</tr>
<tr
key="storage.tsdb.wal-segment-size"
>
<th>
storage.tsdb.wal-segment-size
</th>
<td>
0B
</td>
</tr>
<tr
key="web.console.libraries"
>
<th>
web.console.libraries
</th>
<td>
console_libraries
</td>
</tr>
<tr
key="web.console.templates"
>
<th>
web.console.templates
</th>
<td>
consoles
</td>
</tr>
<tr
key="web.cors.origin"
>
<th>
web.cors.origin
</th>
<td>
.*
</td>
</tr>
<tr
key="web.enable-admin-api"
>
<th>
web.enable-admin-api
</th>
<td>
false
</td>
</tr>
<tr
key="web.enable-lifecycle"
>
<th>
web.enable-lifecycle
</th>
<td>
false
</td>
</tr>
<tr
key="web.external-url"
>
<th>
web.external-url
</th>
<td />
</tr>
<tr
key="web.listen-address"
>
<th>
web.listen-address
</th>
<td>
0.0.0.0:9090
</td>
</tr>
<tr
key="web.max-connections"
>
<th>
web.max-connections
</th>
<td>
512
</td>
</tr>
<tr
key="web.page-title"
>
<th>
web.page-title
</th>
<td>
Prometheus Time Series Collection and Processing Server
</td>
</tr>
<tr
key="web.read-timeout"
>
<th>
web.read-timeout
</th>
<td>
5m
</td>
</tr>
<tr
key="web.route-prefix"
>
<th>
web.route-prefix
</th>
<td>
/
</td>
</tr>
<tr
key="web.user-assets"
>
<th>
web.user-assets
</th>
<td />
</tr>
</tbody>
</Table>
</Fragment>
`;

View file

@ -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<T> = { status: string; data?: T };
export interface FetchState<T> {
response: APIResponse<T>;
error?: Error;
isLoading: boolean;
}
export const useFetch = <T extends {}>(url: string, options?: RequestInit): FetchState<T> => {
const [response, setResponse] = useState<APIResponse<T>>({ status: 'start fetching' });
const [error, setError] = useState<Error>();
const [isLoading, setIsLoading] = useState<boolean>(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<T>;
setResponse(json);
setIsLoading(false);
} catch (error) {
@ -22,6 +30,5 @@ export const useFetch = (url: string, options?: RequestInit) => {
};
fetchData();
}, [url, options]);
return { response, error, isLoading };
};

View file

@ -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 = <T extends {}>(Component: ComponentType<T>): FC<StatusIndicatorProps & T> => ({
error,
isLoading,
customErrorMsg,
...rest
}) => {
if (error) {
return (
<Alert color="danger">
{customErrorMsg ? (
customErrorMsg
) : (
<>
<strong>Error:</strong> Error fetching {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)} />;
};