mirror of
https://github.com/prometheus/prometheus.git
synced 2025-01-12 22:37:27 -08:00
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:
parent
a85e7aac0e
commit
8a9509b0a8
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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
|
||||
<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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
395
web/ui/react-app/src/pages/__snapshots__/Flags.test.tsx.snap
Normal file
395
web/ui/react-app/src/pages/__snapshots__/Flags.test.tsx.snap
Normal 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>
|
||||
`;
|
|
@ -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 };
|
||||
};
|
||||
|
|
44
web/ui/react-app/src/withStatusIndicator.tsx
Normal file
44
web/ui/react-app/src/withStatusIndicator.tsx
Normal 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)} />;
|
||||
};
|
Loading…
Reference in a new issue