mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -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 (
|
return (
|
||||||
<SanitizeHTML tag="li" {...itemProps} allowedTags={['strong']}>
|
<SanitizeHTML key={title} tag="li" {...itemProps} allowedTags={['strong']}>
|
||||||
{string}
|
{string}
|
||||||
</SanitizeHTML>
|
</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 { RouteComponentProps } from '@reach/router';
|
||||||
import { Alert, Button } from 'reactstrap';
|
import { 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 CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
import { useFetch } from '../utils/useFetch';
|
|
||||||
import PathPrefixProps from '../PathPrefixProps';
|
import PathPrefixProps from '../PathPrefixProps';
|
||||||
|
|
||||||
import './Config.css';
|
import './Config.css';
|
||||||
|
import { withStatusIndicator } from '../withStatusIndicator';
|
||||||
|
import { useFetch } from '../utils/useFetch';
|
||||||
|
|
||||||
const Config: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
type YamlConfig = { yaml?: string };
|
||||||
const { response, error } = useFetch(`${pathPrefix}/api/v1/status/config`);
|
|
||||||
|
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 [copied, setCopied] = useState(false);
|
||||||
|
const config = data && data.yaml;
|
||||||
const config = response && response.data.yaml;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>
|
<h2>
|
||||||
Configuration
|
Configuration
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={config ? config! : ''}
|
text={config!}
|
||||||
onCopy={(text, result) => {
|
onCopy={(_, result) => {
|
||||||
setCopied(result);
|
setCopied(result);
|
||||||
setTimeout(setCopied, 1500);
|
setTimeout(setCopied, 1500);
|
||||||
}}
|
}}
|
||||||
|
@ -30,18 +39,14 @@ const Config: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
||||||
</Button>
|
</Button>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</h2>
|
</h2>
|
||||||
|
<ConfigWithStatusIndicator error={error} isLoading={!config} yaml={config} />
|
||||||
{error ? (
|
|
||||||
<Alert color="danger">
|
|
||||||
<strong>Error:</strong> Error fetching configuration: {error.message}
|
|
||||||
</Alert>
|
|
||||||
) : config ? (
|
|
||||||
<pre className="config-yaml">{config}</pre>
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon icon={faSpinner} spin />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
export default Config;
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { mount, shallow, ReactWrapper } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { FlagsContent } from './Flags';
|
||||||
import Flags, { FlagMap } from './Flags';
|
import { Table } from 'reactstrap';
|
||||||
import { Alert, Table } from 'reactstrap';
|
import toJson from 'enzyme-to-json';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
const sampleFlagsResponse: {
|
const sampleFlagsResponse = {
|
||||||
status: string;
|
|
||||||
data: FlagMap;
|
|
||||||
} = {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
'alertmanager.notification-queue-capacity': '10000',
|
'alertmanager.notification-queue-capacity': '10000',
|
||||||
'alertmanager.timeout': '10s',
|
'alertmanager.timeout': '10s',
|
||||||
'config.file': './documentation/examples/prometheus.yml',
|
'config.file': './documentation/examples/prometheus.yml',
|
||||||
|
@ -50,62 +43,23 @@ const sampleFlagsResponse: {
|
||||||
'web.read-timeout': '5m',
|
'web.read-timeout': '5m',
|
||||||
'web.route-prefix': '/',
|
'web.route-prefix': '/',
|
||||||
'web.user-assets': '',
|
'web.user-assets': '',
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Flags', () => {
|
describe('Flags', () => {
|
||||||
beforeEach(() => {
|
it('renders a table with properly configured props', () => {
|
||||||
fetch.resetMocks();
|
const w = shallow(<FlagsContent data={sampleFlagsResponse} />);
|
||||||
});
|
const table = w.find(Table);
|
||||||
|
expect(table.props()).toMatchObject({
|
||||||
describe('before data is returned', () => {
|
bordered: true,
|
||||||
it('renders a spinner', () => {
|
size: 'sm',
|
||||||
const flags = shallow(<Flags />);
|
striped: true,
|
||||||
const icon = flags.find(FontAwesomeIcon);
|
|
||||||
expect(icon.prop('icon')).toEqual(faSpinner);
|
|
||||||
expect(icon.prop('spin')).toEqual(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should not fail if data is missing', () => {
|
||||||
describe('when data is returned', () => {
|
expect(shallow(<FlagsContent />)).toHaveLength(1);
|
||||||
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]]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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 React, { FC } from 'react';
|
||||||
import { RouteComponentProps } from '@reach/router';
|
import { RouteComponentProps } from '@reach/router';
|
||||||
import { Alert, Table } from 'reactstrap';
|
import { Table } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { withStatusIndicator } from '../withStatusIndicator';
|
||||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { useFetch } from '../utils/useFetch';
|
import { useFetch } from '../utils/useFetch';
|
||||||
import PathPrefixProps from '../PathPrefixProps';
|
import PathPrefixProps from '../PathPrefixProps';
|
||||||
|
|
||||||
export interface FlagMap {
|
interface FlagMap {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Flags: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
interface FlagsProps {
|
||||||
const { response, error } = useFetch(`${pathPrefix}/api/v1/status/flags`);
|
data?: FlagMap;
|
||||||
|
}
|
||||||
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 />;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export const FlagsContent: FC<FlagsProps> = ({ data = {} }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>Command-Line Flags</h2>
|
<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;
|
export default Flags;
|
||||||
|
|
|
@ -1,33 +1,15 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
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 toJson from 'enzyme-to-json';
|
||||||
|
import { StatusContent } from './Status';
|
||||||
|
|
||||||
describe('Status', () => {
|
describe('Status', () => {
|
||||||
afterEach(() => jest.restoreAllMocks());
|
it('should not fail with undefined data', () => {
|
||||||
it('should render spinner while waiting data', () => {
|
const wrapper = shallow(<StatusContent data={[]} />);
|
||||||
const wrapper = shallow(<Status />);
|
expect(wrapper).toHaveLength(1);
|
||||||
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',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
describe('Snapshot testing', () => {
|
describe('Snapshot testing', () => {
|
||||||
const response = [
|
const response: any = [
|
||||||
{
|
{
|
||||||
startTime: '2019-10-30T22:03:23.247913868+02:00',
|
startTime: '2019-10-30T22:03:23.247913868+02:00',
|
||||||
CWD: '/home/boyskila/Desktop/prometheus',
|
CWD: '/home/boyskila/Desktop/prometheus',
|
||||||
|
@ -63,8 +45,7 @@ describe('Status', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
it('should match table snapshot', () => {
|
it('should match table snapshot', () => {
|
||||||
(useFetch as any).default = jest.fn().mockImplementation(() => ({ response }));
|
const wrapper = shallow(<StatusContent data={response} />);
|
||||||
const wrapper = shallow(<Status />);
|
|
||||||
expect(toJson(wrapper)).toMatchSnapshot();
|
expect(toJson(wrapper)).toMatchSnapshot();
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import React, { FC, Fragment } from 'react';
|
import React, { Fragment, FC } from 'react';
|
||||||
import { RouteComponentProps } from '@reach/router';
|
import { RouteComponentProps } from '@reach/router';
|
||||||
import { Table, Alert } from 'reactstrap';
|
import { Table } from 'reactstrap';
|
||||||
import useFetches from '../hooks/useFetches';
|
import { withStatusIndicator } from '../withStatusIndicator';
|
||||||
|
import { useFetch } from '../utils/useFetch';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import PathPrefixProps from '../PathPrefixProps';
|
import PathPrefixProps from '../PathPrefixProps';
|
||||||
|
|
||||||
const ENDPOINTS = ['/api/v1/status/runtimeinfo', '/api/v1/status/buildinfo', '/api/v1/alertmanagers'];
|
|
||||||
const sectionTitles = ['Runtime Information', 'Build Information', 'Alertmanagers'];
|
const sectionTitles = ['Runtime Information', 'Build Information', 'Alertmanagers'];
|
||||||
|
|
||||||
interface StatusConfig {
|
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 = {
|
export const statusConfig: StatusConfig = {
|
||||||
startTime: { title: 'Start time', customizeValue: (v: string) => new Date(v).toUTCString() },
|
startTime: { title: 'Start time', customizeValue: (v: string) => new Date(v).toUTCString() },
|
||||||
|
@ -31,9 +32,9 @@ export const statusConfig: StatusConfig = {
|
||||||
storageRetention: { title: 'Storage retention' },
|
storageRetention: { title: 'Storage retention' },
|
||||||
activeAlertmanagers: {
|
activeAlertmanagers: {
|
||||||
customRow: true,
|
customRow: true,
|
||||||
customizeValue: (alertMgrs: { url: string }[]) => {
|
customizeValue: (alertMgrs: { url: string }[], key) => {
|
||||||
return (
|
return (
|
||||||
<Fragment key="alert-managers">
|
<Fragment key={key}>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Endpoint</th>
|
<th>Endpoint</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -55,37 +56,8 @@ export const statusConfig: StatusConfig = {
|
||||||
droppedAlertmanagers: { skip: true },
|
droppedAlertmanagers: { skip: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpointsMemo: { [prefix: string]: string[] } = {};
|
export const StatusContent: FC<StatusPageProps> = ({ data = [] }) => {
|
||||||
|
|
||||||
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 (
|
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 ? (
|
|
||||||
<>
|
<>
|
||||||
{data.map((statuses, i) => {
|
{data.map((statuses, i) => {
|
||||||
return (
|
return (
|
||||||
|
@ -93,20 +65,20 @@ const Status: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' })
|
||||||
<h2>{sectionTitles[i]}</h2>
|
<h2>{sectionTitles[i]}</h2>
|
||||||
<Table className="h-auto" size="sm" bordered striped>
|
<Table className="h-auto" size="sm" bordered striped>
|
||||||
<tbody>
|
<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] || {};
|
const { title = k, customizeValue = (val: any) => val, customRow, skip } = statusConfig[k] || {};
|
||||||
if (skip) {
|
if (skip) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (customRow) {
|
if (customRow) {
|
||||||
return customizeValue(v);
|
return customizeValue(v, k);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<tr key={k}>
|
<tr key={k}>
|
||||||
<th className="capitalize-title" style={{ width: '35%' }}>
|
<th className="capitalize-title" style={{ width: '35%' }}>
|
||||||
{title}
|
{title}
|
||||||
</th>
|
</th>
|
||||||
<td className="text-break">{customizeValue(v)}</td>
|
<td className="text-break">{customizeValue(v, title)}</td>
|
||||||
</tr>
|
</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;
|
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';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
export const useFetch = (url: string, options?: RequestInit) => {
|
export type APIResponse<T> = { status: string; data?: T };
|
||||||
const [response, setResponse] = useState();
|
|
||||||
const [error, setError] = useState();
|
export interface FetchState<T> {
|
||||||
const [isLoading, setIsLoading] = useState();
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
@ -13,7 +21,7 @@ export const useFetch = (url: string, options?: RequestInit) => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(res.statusText);
|
throw new Error(res.statusText);
|
||||||
}
|
}
|
||||||
const json = await res.json();
|
const json = (await res.json()) as APIResponse<T>;
|
||||||
setResponse(json);
|
setResponse(json);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -22,6 +30,5 @@ export const useFetch = (url: string, options?: RequestInit) => {
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [url, options]);
|
}, [url, options]);
|
||||||
|
|
||||||
return { response, error, isLoading };
|
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