github.com/thanos-io/thanos@v0.32.5/pkg/ui/react-app/src/pages/graph/PanelList.tsx (about)

     1  import React, { FC, useState, useEffect } from 'react';
     2  import { RouteComponentProps } from '@reach/router';
     3  import { UncontrolledAlert, Button } from 'reactstrap';
     4  
     5  import Panel, { PanelOptions, PanelDefaultOptions } from './Panel';
     6  import Checkbox from '../../components/Checkbox';
     7  import PathPrefixProps from '../../types/PathPrefixProps';
     8  import { StoreListProps } from '../../thanos/pages/stores/Stores';
     9  import { Store } from '../../thanos/pages/stores/store';
    10  import { FlagMap } from '../flags/Flags';
    11  import { generateID, decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString, callAll } from '../../utils';
    12  import { useFetch } from '../../hooks/useFetch';
    13  import { useLocalStorage } from '../../hooks/useLocalStorage';
    14  import { withStatusIndicator } from '../../components/withStatusIndicator';
    15  
    16  export type PanelMeta = { key: string; options: PanelOptions; id: string };
    17  
    18  export const updateURL = (nextPanels: PanelMeta[]) => {
    19    const query = encodePanelOptionsToQueryString(nextPanels);
    20    window.history.pushState({}, '', query);
    21  };
    22  
    23  interface PanelListProps extends PathPrefixProps, RouteComponentProps {
    24    panels: PanelMeta[];
    25    metrics: string[];
    26    useLocalTime: boolean;
    27    queryHistoryEnabled: boolean;
    28    stores: StoreListProps;
    29    enableAutocomplete: boolean;
    30    enableHighlighting: boolean;
    31    enableLinter: boolean;
    32    defaultStep: string;
    33    defaultEngine: string;
    34  }
    35  
    36  export const PanelListContent: FC<PanelListProps> = ({
    37    metrics = [],
    38    useLocalTime,
    39    pathPrefix,
    40    queryHistoryEnabled,
    41    stores = {},
    42    enableAutocomplete,
    43    enableHighlighting,
    44    enableLinter,
    45    defaultStep,
    46    defaultEngine,
    47    ...rest
    48  }) => {
    49    const [panels, setPanels] = useState(rest.panels);
    50    const [historyItems, setLocalStorageHistoryItems] = useLocalStorage<string[]>('history', []);
    51    const [storeData, setStoreData] = useState([] as Store[]);
    52  
    53    useEffect(() => {
    54      // Convert stores data to a unified stores array.
    55      const storeList: Store[] = [];
    56      for (const type in stores) {
    57        storeList.push(...stores[type]);
    58      }
    59      setStoreData(storeList);
    60      !panels.length && addPanel();
    61      window.onpopstate = () => {
    62        const panels = decodePanelOptionsFromQueryString(window.location.search);
    63        if (panels.length > 0) {
    64          setPanels(panels);
    65        }
    66      };
    67      // We want useEffect to act only as componentDidMount, but react still complains about the empty dependencies list.
    68      // eslint-disable-next-line react-hooks/exhaustive-deps
    69    }, [stores]);
    70  
    71    const handleExecuteQuery = (query: string) => {
    72      const isSimpleMetric = metrics.indexOf(query) !== -1;
    73      if (isSimpleMetric || !query.length) {
    74        return;
    75      }
    76      const extendedItems = historyItems.reduce(
    77        (acc, metric) => {
    78          return metric === query ? acc : [...acc, metric]; // Prevent adding query twice.
    79        },
    80        [query]
    81      );
    82      setLocalStorageHistoryItems(extendedItems.slice(0, 50));
    83    };
    84  
    85    const addPanel = () => {
    86      callAll(
    87        setPanels,
    88        updateURL
    89      )([
    90        ...panels,
    91        {
    92          id: generateID(),
    93          key: `${panels.length}`,
    94          options: PanelDefaultOptions,
    95        },
    96      ]);
    97    };
    98  
    99    return (
   100      <>
   101        {panels.map(({ id, options }) => (
   102          <Panel
   103            onExecuteQuery={handleExecuteQuery}
   104            key={id}
   105            options={options}
   106            id={id}
   107            onOptionsChanged={(opts) =>
   108              callAll(setPanels, updateURL)(panels.map((p) => (id === p.id ? { ...p, options: opts } : p)))
   109            }
   110            removePanel={() =>
   111              callAll(
   112                setPanels,
   113                updateURL
   114              )(
   115                panels.reduce<PanelMeta[]>(
   116                  (acc, panel) => (panel.id !== id ? [...acc, { ...panel, key: `${acc.length}` }] : acc),
   117                  []
   118                )
   119              )
   120            }
   121            useLocalTime={useLocalTime}
   122            metricNames={metrics}
   123            pastQueries={queryHistoryEnabled ? historyItems : []}
   124            pathPrefix={pathPrefix}
   125            stores={storeData}
   126            enableAutocomplete={enableAutocomplete}
   127            enableHighlighting={enableHighlighting}
   128            defaultEngine={defaultEngine}
   129            enableLinter={enableLinter}
   130            defaultStep={defaultStep}
   131          />
   132        ))}
   133        <Button className="d-block mb-3" color="primary" onClick={addPanel}>
   134          Add Panel
   135        </Button>
   136      </>
   137    );
   138  };
   139  
   140  const PanelListContentWithIndicator = withStatusIndicator(PanelListContent);
   141  
   142  const PanelList: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => {
   143    const [delta, setDelta] = useState(0);
   144    const [useLocalTime, setUseLocalTime] = useLocalStorage('use-local-time', false);
   145    const [enableQueryHistory, setEnableQueryHistory] = useLocalStorage('enable-query-history', false);
   146    const [enableStoreFiltering, setEnableStoreFiltering] = useLocalStorage('enable-store-filtering', false);
   147    const [enableAutocomplete, setEnableAutocomplete] = useLocalStorage('enable-autocomplete', true);
   148    const [enableHighlighting, setEnableHighlighting] = useLocalStorage('enable-syntax-highlighting', true);
   149    const [enableLinter, setEnableLinter] = useLocalStorage('enable-linter', true);
   150  
   151    const { response: metricsRes, error: metricsErr } = useFetch<string[]>(`${pathPrefix}/api/v1/label/__name__/values`);
   152    const {
   153      response: storesRes,
   154      error: storesErr,
   155      isLoading: storesLoading,
   156    } = useFetch<StoreListProps>(`${pathPrefix}/api/v1/stores`);
   157    const {
   158      response: flagsRes,
   159      error: flagsErr,
   160      isLoading: flagsLoading,
   161    } = useFetch<FlagMap>(`${pathPrefix}/api/v1/status/flags`);
   162    const defaultStep = flagsRes?.data?.['query.default-step'] || '1s';
   163    const defaultEngine = flagsRes?.data?.['query.promql-engine'];
   164  
   165    const browserTime = new Date().getTime() / 1000;
   166    const { response: timeRes, error: timeErr } = useFetch<{ result: number[] }>(`${pathPrefix}/api/v1/query?query=time()`);
   167  
   168    useEffect(() => {
   169      if (timeRes.data) {
   170        const serverTime = timeRes.data.result[0];
   171        setDelta(Math.abs(browserTime - serverTime));
   172      }
   173      /**
   174       * React wants to include browserTime to useEffect dependencies list which will cause a delta change on every re-render
   175       * Basically it's not recommended to disable this rule, but this is the only way to take control over the useEffect
   176       * dependencies and to not include the browserTime variable.
   177       **/
   178      // eslint-disable-next-line react-hooks/exhaustive-deps
   179    }, [timeRes.data]);
   180  
   181    return (
   182      <>
   183        <div className="clearfix">
   184          <div className="float-left">
   185            <Checkbox
   186              wrapperStyles={{ display: 'inline-block' }}
   187              id="use-local-time-checkbox"
   188              onChange={({ target }) => setUseLocalTime(target.checked)}
   189              defaultChecked={useLocalTime}
   190            >
   191              Use local time
   192            </Checkbox>
   193            <Checkbox
   194              wrapperStyles={{ marginLeft: 20, display: 'inline-block' }}
   195              id="query-history-checkbox"
   196              onChange={({ target }) => setEnableQueryHistory(target.checked)}
   197              defaultChecked={enableQueryHistory}
   198            >
   199              Enable query history
   200            </Checkbox>
   201            <Checkbox
   202              wrapperStyles={{ marginLeft: 20, display: 'inline-block' }}
   203              id="store-filtering-checkbox"
   204              defaultChecked={enableStoreFiltering}
   205              onChange={({ target }) => setEnableStoreFiltering(target.checked)}
   206            >
   207              Enable Store Filtering
   208            </Checkbox>
   209          </div>
   210          <div className="float-right">
   211            <Checkbox
   212              wrapperStyles={{ marginLeft: 20, display: 'inline-block' }}
   213              id="autocomplete-checkbox"
   214              onChange={({ target }) => setEnableAutocomplete(target.checked)}
   215              defaultChecked={enableAutocomplete}
   216            >
   217              Enable autocomplete
   218            </Checkbox>
   219            <Checkbox
   220              wrapperStyles={{ marginLeft: 20, display: 'inline-block' }}
   221              id="highlighting-checkbox"
   222              onChange={({ target }) => setEnableHighlighting(target.checked)}
   223              defaultChecked={enableHighlighting}
   224            >
   225              Enable highlighting
   226            </Checkbox>
   227            <Checkbox
   228              wrapperStyles={{ marginLeft: 20, display: 'inline-block' }}
   229              id="linter-checkbox"
   230              onChange={({ target }) => setEnableLinter(target.checked)}
   231              defaultChecked={enableLinter}
   232            >
   233              Enable linter
   234            </Checkbox>
   235          </div>
   236        </div>
   237        {(delta > 30 || timeErr) && (
   238          <UncontrolledAlert color="danger">
   239            <strong>Warning: </strong>
   240            {timeErr && `Unexpected response status when fetching server time: ${timeErr.message}`}
   241            {delta >= 30 &&
   242              `Error fetching server time: Detected ${delta} seconds time difference between your browser and the server. Thanos relies on accurate time and time drift might cause unexpected query results.`}
   243          </UncontrolledAlert>
   244        )}
   245        {metricsErr && (
   246          <UncontrolledAlert color="danger">
   247            <strong>Warning: </strong>
   248            Error fetching metrics list: Unexpected response status when fetching metric names: {metricsErr.message}
   249          </UncontrolledAlert>
   250        )}
   251        {storesErr && (
   252          <UncontrolledAlert color="danger">
   253            <strong>Warning: </strong>
   254            Error fetching stores list: Unexpected response status when fetching stores: {storesErr.message}
   255          </UncontrolledAlert>
   256        )}
   257        {flagsErr && (
   258          <UncontrolledAlert color="danger">
   259            <strong>Warning: </strong>
   260            Error fetching flags list: Unexpected response status when fetching flags: {flagsErr.message}
   261          </UncontrolledAlert>
   262        )}
   263        <PanelListContentWithIndicator
   264          panels={decodePanelOptionsFromQueryString(window.location.search)}
   265          pathPrefix={pathPrefix}
   266          useLocalTime={useLocalTime}
   267          metrics={metricsRes.data}
   268          stores={enableStoreFiltering ? storesRes.data : {}}
   269          enableAutocomplete={enableAutocomplete}
   270          enableHighlighting={enableHighlighting}
   271          enableLinter={enableLinter}
   272          defaultStep={defaultStep}
   273          defaultEngine={defaultEngine}
   274          queryHistoryEnabled={enableQueryHistory}
   275          isLoading={storesLoading || flagsLoading}
   276        />
   277      </>
   278    );
   279  };
   280  
   281  export default PanelList;