import React, { Component, ChangeEvent } from 'react'; import { RouteComponentProps } from '@reach/router'; import { Alert, Button, Col, Row } from 'reactstrap'; import Panel, { PanelOptions, PanelDefaultOptions } from '../Panel'; import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from '../utils/urlParams'; import Checkbox from '../Checkbox'; import PathPrefixProps from '../PathPrefixProps'; export type MetricGroup = { title: string; items: string[] }; interface PanelListState { panels: { key: string; options: PanelOptions; }[]; pastQueries: string[]; metricNames: string[]; fetchMetricsError: string | null; timeDriftError: string | null; } class PanelList extends Component { private key = 0; constructor(props: PathPrefixProps) { super(props); const urlPanels = decodePanelOptionsFromQueryString(window.location.search); this.state = { panels: urlPanels.length !== 0 ? urlPanels : [ { key: this.getKey(), options: PanelDefaultOptions, }, ], pastQueries: [], metricNames: [], fetchMetricsError: null, timeDriftError: null, }; } componentDidMount() { fetch(`${this.props.pathPrefix}/api/v1/label/__name__/values`, { cache: 'no-store' }) .then(resp => { if (resp.ok) { return resp.json(); } else { throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error } }) .then(json => { this.setState({ metricNames: json.data }); }) .catch(error => this.setState({ fetchMetricsError: error.message })); const browserTime = new Date().getTime() / 1000; fetch(`${this.props.pathPrefix}/api/v1/query?query=time()`, { cache: 'no-store' }) .then(resp => { if (resp.ok) { return resp.json(); } else { throw new Error('Unexpected response status when fetching metric names: ' + resp.statusText); // TODO extract error } }) .then(json => { const serverTime = json.data.result[0]; const delta = Math.abs(browserTime - serverTime); if (delta >= 30) { throw new Error( 'Detected ' + delta + ' seconds time difference between your browser and the server. Prometheus relies on accurate time and time drift might cause unexpected query results.' ); } }) .catch(error => this.setState({ timeDriftError: error.message })); window.onpopstate = () => { const panels = decodePanelOptionsFromQueryString(window.location.search); if (panels.length !== 0) { this.setState({ panels: panels }); } }; this.updatePastQueries(); } isHistoryEnabled = () => JSON.parse(localStorage.getItem('enable-query-history') || 'false') as boolean; getHistoryItems = () => JSON.parse(localStorage.getItem('history') || '[]') as string[]; toggleQueryHistory = (e: ChangeEvent) => { localStorage.setItem('enable-query-history', `${e.target.checked}`); this.updatePastQueries(); }; updatePastQueries = () => { this.setState({ pastQueries: this.isHistoryEnabled() ? this.getHistoryItems() : [], }); }; handleQueryHistory = (query: string) => { const isSimpleMetric = this.state.metricNames.indexOf(query) !== -1; if (isSimpleMetric || !query.length) { return; } const historyItems = this.getHistoryItems(); const extendedItems = historyItems.reduce( (acc, metric) => { return metric === query ? acc : [...acc, metric]; // Prevent adding query twice. }, [query] ); localStorage.setItem('history', JSON.stringify(extendedItems.slice(0, 50))); this.updatePastQueries(); }; getKey(): string { return (this.key++).toString(); } handleOptionsChanged(key: string, opts: PanelOptions): void { const newPanels = this.state.panels.map(p => { if (key === p.key) { return { key: key, options: opts, }; } return p; }); this.setState({ panels: newPanels }, this.updateURL); } updateURL(): void { const query = encodePanelOptionsToQueryString(this.state.panels); window.history.pushState({}, '', query); } addPanel = (): void => { const panels = this.state.panels.slice(); panels.push({ key: this.getKey(), options: PanelDefaultOptions, }); this.setState({ panels: panels }, this.updateURL); }; removePanel = (key: string): void => { const panels = this.state.panels.filter(panel => { return panel.key !== key; }); this.setState({ panels: panels }, this.updateURL); }; render() { const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state; const { pathPrefix } = this.props; return ( <> Enable query history {timeDriftError && ( Warning: Error fetching server time: {this.state.timeDriftError} )} {fetchMetricsError && ( Warning: Error fetching metrics list: {this.state.fetchMetricsError} )} {this.state.panels.map(p => ( this.handleOptionsChanged(p.key, opts)} removePanel={() => this.removePanel(p.key)} metricNames={metricNames} pastQueries={pastQueries} pathPrefix={pathPrefix} /> ))} ); } } export default PanelList;