mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
* React UI: Support custom path prefixes The challenge was that the path prefix can be set dynamically as a flag on Prometheus, but the React app bundle is statically compiled in to expect a given path prefix. By adding a placeholder value to the React app's index.html and replacing it in Prometheus with the right path prefix during serving, this injects Prometheus's path prefix into the React app via a global const. Threading the path prefix into the different React components could have been done with React's Contexts (https://reactjs.org/docs/context.html), but I found the consumer side of context values to be a bit cumbersome (wrapping entire components in context consumers), so I ended up preferring direct threading of the path prefix values to components that needed them. Also, using contexts in tests is more verbose than just passing in path prefix values directly. Fixes https://github.com/prometheus/prometheus/issues/6163 Signed-off-by: Julius Volz <julius.volz@gmail.com> * Review feedback Signed-off-by: Julius Volz <julius.volz@gmail.com>
123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
import React, { FC, Fragment } 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 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 };
|
|
}
|
|
|
|
type StatusPageState = Array<{ [k: string]: string }>;
|
|
|
|
export const statusConfig: StatusConfig = {
|
|
startTime: { title: 'Start time', customizeValue: (v: string) => new Date(v).toUTCString() },
|
|
CWD: { title: 'Working directory' },
|
|
reloadConfigSuccess: {
|
|
title: 'Configuration reload',
|
|
customizeValue: (v: boolean) => (v ? 'Successful' : 'Unsuccessful'),
|
|
},
|
|
lastConfigTime: { title: 'Last successful configuration reload' },
|
|
chunkCount: { title: 'Head chunks' },
|
|
timeSeriesCount: { title: 'Head time series' },
|
|
corruptionCount: { title: 'WAL corruptions' },
|
|
goroutineCount: { title: 'Goroutines' },
|
|
storageRetention: { title: 'Storage retention' },
|
|
activeAlertmanagers: {
|
|
customRow: true,
|
|
customizeValue: (alertMgrs: { url: string }[]) => {
|
|
return (
|
|
<Fragment key="alert-managers">
|
|
<tr>
|
|
<th>Endpoint</th>
|
|
</tr>
|
|
{alertMgrs.map(({ url }) => {
|
|
const { origin, pathname } = new URL(url);
|
|
return (
|
|
<tr key={url}>
|
|
<td>
|
|
<a href={url}>{origin}</a>
|
|
{pathname}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
);
|
|
},
|
|
},
|
|
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 ? (
|
|
<>
|
|
{data.map((statuses, i) => {
|
|
return (
|
|
<Fragment key={i}>
|
|
<h2>{sectionTitles[i]}</h2>
|
|
<Table className="h-auto" size="sm" bordered striped>
|
|
<tbody>
|
|
{Object.entries(statuses).map(([k, v]) => {
|
|
const { title = k, customizeValue = (val: any) => val, customRow, skip } = statusConfig[k] || {};
|
|
if (skip) {
|
|
return null;
|
|
}
|
|
if (customRow) {
|
|
return customizeValue(v);
|
|
}
|
|
return (
|
|
<tr key={k}>
|
|
<th className="capitalize-title" style={{ width: '35%' }}>
|
|
{title}
|
|
</th>
|
|
<td className="text-break">{customizeValue(v)}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</Table>
|
|
</Fragment>
|
|
);
|
|
})}
|
|
</>
|
|
) : null;
|
|
};
|
|
|
|
export default Status;
|