github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/components/AppSelector/index.tsx (about)

     1  import React, { useState, useEffect, useMemo } from 'react';
     2  import ModalWithToggle from '@webapp/ui/Modals/ModalWithToggle';
     3  import Input from '@webapp/ui/Input';
     4  import { App } from '@webapp/models/app';
     5  import SelectButton from '@webapp/components/AppSelector/SelectButton';
     6  import { Label, LabelString } from '@webapp/components/AppSelector/Label';
     7  import styles from './AppSelector.module.scss';
     8  
     9  interface AppSelectorProps {
    10    /** Triggered when an app is selected */
    11    onSelected: (name: string) => void;
    12  
    13    /** List of all applications */
    14    apps: Pick<App, 'name'>[];
    15  
    16    selectedAppName: string;
    17  }
    18  
    19  const AppSelector = ({
    20    onSelected,
    21    selectedAppName,
    22    apps,
    23  }: AppSelectorProps) => {
    24    const selectAppName = (name: string) => {
    25      onSelected(name);
    26    };
    27  
    28    const appNames = apps.map((a) => a.name);
    29  
    30    return (
    31      <div className={styles.container}>
    32        <Label />
    33        <SelectorModalWithToggler
    34          selectAppName={selectAppName}
    35          appNames={appNames}
    36          appName={selectedAppName}
    37        />
    38      </div>
    39    );
    40  };
    41  
    42  export default AppSelector;
    43  
    44  const DELIMITER = '.';
    45  const isGroupMember = (groupName: string, name: string) =>
    46    name.indexOf(groupName) === 0 &&
    47    (name[groupName.length] === DELIMITER || name.length === groupName.length);
    48  
    49  const getGroupMembers = (names: string[], name: string) =>
    50    names.filter((a) => isGroupMember(name, a));
    51  
    52  const getGroupNameFromAppName = (groups: string[], fullName: string) =>
    53    groups.filter((g) => isGroupMember(g, fullName))[0] || '';
    54  
    55  const getGroups = (filteredAppNames: string[]) => {
    56    const allGroups = filteredAppNames.map((i) => {
    57      const arr = i.split(DELIMITER);
    58      const cutProfileType = arr.length > 1 ? arr.slice(0, -1) : arr;
    59      return cutProfileType.join(DELIMITER);
    60    });
    61  
    62    const uniqGroups = Array.from(new Set(allGroups));
    63  
    64    const dedupedUniqGroups = uniqGroups.filter((x) => {
    65      return !uniqGroups.find((y) => x !== y && isGroupMember(y, x));
    66    });
    67  
    68    const groupOrApp = dedupedUniqGroups.map((u) => {
    69      const appNamesEntries = getGroupMembers(filteredAppNames, u);
    70  
    71      return appNamesEntries.length > 1 ? u : appNamesEntries?.[0];
    72    });
    73  
    74    return groupOrApp;
    75  };
    76  
    77  interface SelectorModalWithTogglerProps {
    78    appNames: string[];
    79    selectAppName: (name: string) => void;
    80    appName: string;
    81  }
    82  
    83  export const SelectorModalWithToggler = ({
    84    appNames,
    85    selectAppName,
    86    appName,
    87  }: SelectorModalWithTogglerProps) => {
    88    const [filter, setFilter] = useState('');
    89    const [isModalOpen, setModalOpenStatus] = useState(false);
    90  
    91    // selected is an array of strings
    92    //  0 corresponds to string of group / app name selected in the left pane
    93    //  1 corresponds to string of app name selected in the right pane
    94    const [selected, setSelected] = useState<string[]>([]);
    95    const filteredAppNames = useMemo(
    96      // filtered names by search input
    97      () =>
    98        appNames.filter((n: string) =>
    99          n.toLowerCase().includes(filter.trim().toLowerCase())
   100        ),
   101      [filter, appNames]
   102    );
   103  
   104    const groups = useMemo(() => getGroups(filteredAppNames), [filteredAppNames]);
   105  
   106    const profileTypes = useMemo(() => {
   107      if (!selected?.[0]) {
   108        return [];
   109      }
   110  
   111      const filtered = getGroupMembers(filteredAppNames, selected?.[0]);
   112  
   113      if (filtered.length > 1) {
   114        return filtered;
   115      }
   116  
   117      return [];
   118    }, [selected, groups, filteredAppNames]);
   119  
   120    const onSelect = ({ index, name }: { index: number; name: string }) => {
   121      const filtered = getGroupMembers(filteredAppNames, name);
   122  
   123      if (filtered.length === 1 || index === 1) {
   124        selectAppName(filtered?.[0]);
   125        setModalOpenStatus(false);
   126      }
   127  
   128      const arr = Array.from(selected);
   129  
   130      if (index === 0 && arr?.length > 1) {
   131        arr.pop();
   132      }
   133  
   134      arr[index] = name;
   135  
   136      setSelected(arr);
   137    };
   138  
   139    useEffect(() => {
   140      if (appName && !selected.length && groups.length) {
   141        if (groups.indexOf(appName) !== -1) {
   142          setSelected([appName]);
   143          setModalOpenStatus(false);
   144        } else {
   145          setSelected([getGroupNameFromAppName(groups, appName), appName]);
   146        }
   147      }
   148    }, [appName, selected, groups]);
   149  
   150    const listHeight = useMemo(() => {
   151      const height = (window?.innerHeight || 0) - 160;
   152  
   153      const listRequiredHeight =
   154        // 35 is list item height
   155        Math.max(groups?.length || 0, profileTypes?.length || 0) * 35;
   156  
   157      if (height && listRequiredHeight) {
   158        return height >= listRequiredHeight ? 'auto' : `${height}px`;
   159      }
   160  
   161      return 'auto';
   162    }, [groups, profileTypes]);
   163  
   164    return (
   165      <ModalWithToggle
   166        isModalOpen={isModalOpen}
   167        setModalOpenStatus={setModalOpenStatus}
   168        modalClassName={styles.appSelectorModal}
   169        modalHeight={listHeight}
   170        noDataEl={
   171          !filteredAppNames?.length ? (
   172            <div data-testid="app-selector-no-data" className={styles.noData}>
   173              No Data
   174            </div>
   175          ) : null
   176        }
   177        toggleText={appName || LabelString}
   178        headerEl={
   179          <>
   180            <div className={styles.headerTitle}>{LabelString}</div>
   181            <Input
   182              name="application seach"
   183              type="text"
   184              placeholder="Type an app"
   185              value={filter}
   186              onChange={(e) => setFilter(e.target.value)}
   187              className={styles.search}
   188              testId="application-search"
   189            />
   190          </>
   191        }
   192        leftSideEl={groups.map((name) => (
   193          <SelectButton
   194            name={name}
   195            onClick={() => onSelect({ index: 0, name })}
   196            fullList={appNames}
   197            isSelected={selected?.[0] === name}
   198            key={name}
   199          />
   200        ))}
   201        rightSideEl={profileTypes.map((name) => (
   202          <SelectButton
   203            name={name}
   204            onClick={() => onSelect({ index: 1, name })}
   205            fullList={appNames}
   206            isSelected={selected?.[1] === name}
   207            key={name}
   208          />
   209        ))}
   210      />
   211    );
   212  };