github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/pages/ServiceDiscovery.tsx (about)

     1  import React, { Children, useEffect, useState } from 'react';
     2  import { Target } from '@webapp/models/targets';
     3  import { useAppDispatch, useAppSelector } from '@webapp/redux/hooks';
     4  import {
     5    loadTargets,
     6    selectTargetsData,
     7  } from '@webapp/redux/reducers/serviceDiscovery';
     8  import { formatDistance, parseISO } from 'date-fns';
     9  import cx from 'classnames';
    10  import Button from '@webapp/ui/Button';
    11  import styles from './ServiceDiscovery.module.scss';
    12  
    13  enum Status {
    14    healthy = 'healthy',
    15    info = 'info',
    16    error = 'error',
    17  }
    18  
    19  const ServiceDiscoveryApp = () => {
    20    const data = targetsToMap(useAppSelector(selectTargetsData));
    21    const dispatch = useAppDispatch();
    22    const [unavailableFilter, setUnavailableFilter] = useState(false);
    23    const [expandAll, setExpandAll] = useState(true);
    24  
    25    useEffect(() => {
    26      async function run() {
    27        await dispatch(loadTargets());
    28      }
    29  
    30      run();
    31    }, []);
    32  
    33    function getUpCount(targets: Target[]) {
    34      return targets.filter((t) => t.health === 'up').length;
    35    }
    36  
    37    return (
    38      <div className={styles.serviceDiscoveryApp}>
    39        <h2 className={styles.header}>Targets</h2>
    40        <div className={styles.buttonGroup}>
    41          <Button
    42            kind="secondary"
    43            grouped
    44            onClick={() => setUnavailableFilter(!unavailableFilter)}
    45          >
    46            {unavailableFilter ? 'Show All' : 'Show Unhealthy Only'}
    47          </Button>
    48          <Button
    49            kind="secondary"
    50            grouped
    51            onClick={() => setExpandAll(!expandAll)}
    52          >
    53            {expandAll ? 'Collapse All' : 'Expand All'}
    54          </Button>
    55        </div>
    56  
    57        <div>
    58          {Object.keys(data).length === 0 ? (
    59            <div>
    60              {'No pull-mode targets configured. See '}
    61              <a
    62                className={styles.link}
    63                href="https://pyroscope.io/docs/golang-pull-mode/"
    64                target="_blank"
    65                rel="noreferrer"
    66              >
    67                documentation
    68              </a>
    69              {' for information on how to add targets.'}
    70            </div>
    71          ) : (
    72            Object.keys(data).map((job) => {
    73              const children = data[job].map((target) => {
    74                const targetElem = (
    75                  /* eslint-disable-next-line react/jsx-props-no-spreading */
    76                  <TargetComponent {...target} key={target.url} />
    77                );
    78                if (unavailableFilter) {
    79                  if (target.health !== 'up') {
    80                    return targetElem;
    81                  }
    82                  return null;
    83                }
    84                return targetElem;
    85              });
    86  
    87              return (
    88                <CollapsibleSection
    89                  title={`${data[job][0].job} (${getUpCount(data[job])}/${
    90                    data[job].length
    91                  }) up`}
    92                  key={job}
    93                  open={expandAll}
    94                >
    95                  {children}
    96                </CollapsibleSection>
    97              );
    98            })
    99          )}
   100        </div>
   101      </div>
   102    );
   103  };
   104  
   105  const CollapsibleSection = ({ children, title, open }: ShamefulAny) => {
   106    return Children.count(children.filter((c: ShamefulAny) => c)) > 0 ? (
   107      <details open={open}>
   108        <summary className={styles.collapsibleHeader}>{title}</summary>
   109        <div className={styles.collapsibleSection}>
   110          <table className={styles.target}>
   111            <thead>
   112              <tr>
   113                <th className={cx(styles.tableCell, styles.url)}>Scrape URL</th>
   114                <th className={cx(styles.tableCell, styles.health)}>Health</th>
   115                <th className={cx(styles.tableCell, styles.dicoveredLabels)}>
   116                  Discovered labels
   117                </th>
   118                <th className={cx(styles.tableCell, styles.labels)}>Labels</th>
   119                <th className={cx(styles.tableCell, styles.lastScrape)}>
   120                  Last scrape
   121                </th>
   122                <th className={cx(styles.tableCell, styles.scrapeDuration)}>
   123                  Scrape duration
   124                </th>
   125                <th className={cx(styles.tableCell, styles.error)}>Last error</th>
   126              </tr>
   127            </thead>
   128            <tbody>{children}</tbody>
   129          </table>
   130        </div>
   131      </details>
   132    ) : null;
   133  };
   134  
   135  function formatDuration(input: string): string {
   136    const a = input.match(/[a-zA-Z]+$/);
   137    const b = a ? a[0] : '';
   138    return `${parseFloat(input).toFixed(2)} ${b}`;
   139  }
   140  
   141  const TargetComponent = ({
   142    discoveredLabels,
   143    labels,
   144    url,
   145    lastError,
   146    lastScrape,
   147    lastScrapeDuration,
   148    health,
   149  }: Target) => {
   150    return (
   151      <tr>
   152        <td className={cx(styles.tableCell, styles.url)}>{url}</td>
   153        <td className={cx(styles.tableCell, styles.health)}>
   154          <Badge status={health === 'up' ? Status.healthy : Status.error}>
   155            {health}
   156          </Badge>
   157        </td>
   158        <td className={cx(styles.tableCell, styles.dicoveredLabels)}>
   159          {Object.keys(discoveredLabels).map((key) => (
   160            <Badge
   161              status={Status.info}
   162              key={key}
   163            >{`${key}=${discoveredLabels[key]}`}</Badge>
   164          ))}
   165        </td>
   166        <td className={cx(styles.tableCell, styles.labels)}>
   167          {Object.keys(labels).map((key) => (
   168            <Badge
   169              status={Status.info}
   170              key={key}
   171            >{`${key}=${labels[key]}`}</Badge>
   172          ))}
   173        </td>
   174        <td
   175          className={cx(styles.tableCell, styles.lastScrape)}
   176          title={lastScrape}
   177        >
   178          {formatDistance(parseISO(lastScrape), new Date())} ago
   179        </td>
   180        <td className={cx(styles.tableCell, styles.scrapeDuration)}>
   181          {formatDuration(lastScrapeDuration)}
   182        </td>
   183        <td className={cx(styles.tableCell, styles.error)}>{lastError || '-'}</td>
   184      </tr>
   185    );
   186  };
   187  
   188  const Badge = ({ children, status }: { children: string; status: Status }) => {
   189    function getStatusClass(status: ShamefulAny) {
   190      switch (status) {
   191        case Status.healthy:
   192          return styles.healthy;
   193        case Status.info:
   194          return styles.info;
   195        case Status.error:
   196          return styles.error;
   197        default:
   198          return styles.info;
   199      }
   200    }
   201    return (
   202      <span className={cx(styles.badge, getStatusClass(status))}>{children}</span>
   203    );
   204  };
   205  
   206  type TargetRecord = Record<string, Target[]>;
   207  const targetsToMap: (state: Target[]) => TargetRecord = (state) => {
   208    const acc = state.reduce((acc: TargetRecord, next: Target) => {
   209      if (!acc[next.job]) {
   210        acc[next.job] = [];
   211      }
   212      acc[next.job].push(next);
   213      return acc;
   214    }, {} as TargetRecord);
   215    return acc;
   216  };
   217  
   218  export default ServiceDiscoveryApp;