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

     1  import React, { useState, useMemo } from 'react';
     2  import { App } from '@webapp/models/app';
     3  import ModalWithToggle from '@webapp/ui/Modals/ModalWithToggle';
     4  import Input from '@webapp/ui/Input';
     5  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
     6  import { faSlidersH } from '@fortawesome/free-solid-svg-icons/faSlidersH';
     7  import { faUndo } from '@fortawesome/free-solid-svg-icons/faUndo';
     8  import { Tooltip } from '@webapp/ui/Tooltip';
     9  import { SpyNameFirstClassType } from '@pyroscope/models/src';
    10  import cl from 'classnames';
    11  import SelectButton from '../AppSelector/SelectButton';
    12  import useFilters from './useFilters';
    13  import { SPY_NAMES_TOOLTIPS, SPY_NAMES_ICONS } from './SpyNameIcons';
    14  import styles from './EnhancedAppSelector.module.scss';
    15  
    16  export interface EnhancedAppSelector {
    17    /** Triggered when an app is selected */
    18    onSelected: (name: string) => void;
    19  
    20    apps: App[];
    21  
    22    selectedAppName: string;
    23  }
    24  
    25  // TODO: this file has a lot of repetition with AppSelector
    26  // We should remove the old implementation (AppSelector)
    27  // When this one actually gets used
    28  function EnhancedAppSelector({
    29    onSelected,
    30    selectedAppName,
    31    apps,
    32  }: EnhancedAppSelector) {
    33    return (
    34      <div className={styles.container}>
    35        Application:&nbsp;
    36        <SelectorModalWithToggler
    37          selectAppName={onSelected}
    38          apps={apps}
    39          appName={selectedAppName}
    40        />
    41      </div>
    42    );
    43  }
    44  
    45  const DELIMITER = '.';
    46  const isGroupMember = (groupName: string, name: string) =>
    47    name.indexOf(groupName) === 0 &&
    48    (name[groupName.length] === DELIMITER || name.length === groupName.length);
    49  
    50  const getGroupMembers = (names: string[], name: string) =>
    51    names.filter((a) => isGroupMember(name, a));
    52  
    53  const getGroupNameFromAppName = (groups: string[], fullName: string) =>
    54    groups.filter((g) => isGroupMember(g, fullName))[0] || '';
    55  
    56  const getGroups = (filteredAppNames: string[]) => {
    57    const allGroups = filteredAppNames.map((i) => {
    58      const arr = i.split(DELIMITER);
    59      const cutProfileType = arr.length > 1 ? arr.slice(0, -1) : arr;
    60      return cutProfileType.join(DELIMITER);
    61    });
    62  
    63    const uniqGroups = Array.from(new Set(allGroups));
    64  
    65    const dedupedUniqGroups = uniqGroups.filter((x) => {
    66      return !uniqGroups.find((y) => x !== y && isGroupMember(y, x));
    67    });
    68  
    69    const groupOrApp = dedupedUniqGroups.map((u) => {
    70      const appNamesEntries = getGroupMembers(filteredAppNames, u);
    71  
    72      return appNamesEntries.length > 1 ? u : appNamesEntries?.[0];
    73    });
    74  
    75    return groupOrApp;
    76  };
    77  
    78  const getSelectedApp = (
    79    appName: string,
    80    groups: string[],
    81    selected: string[]
    82  ) => {
    83    const isFirstLevel = !!(groups.indexOf(appName) !== -1);
    84  
    85    if (selected.length !== 0) {
    86      return selected;
    87    }
    88  
    89    if (isFirstLevel) {
    90      return [appName];
    91    }
    92    return [getGroupNameFromAppName(groups, appName), appName];
    93  };
    94  
    95  interface SelectorModalWithTogglerProps {
    96    appName: string;
    97    apps: App[];
    98    selectAppName: (name: string) => void;
    99  }
   100  
   101  const SelectorModalWithToggler = ({
   102    appName,
   103    apps,
   104    selectAppName,
   105  }: SelectorModalWithTogglerProps) => {
   106    const [isModalOpen, setModalOpenStatus] = useState(false);
   107    const {
   108      filters,
   109      filteredAppNames,
   110      spyNameValues,
   111      profileTypeValues,
   112      handleFilterChange,
   113      resetClickableFilters,
   114    } = useFilters(apps);
   115  
   116    // selected is an array of strings
   117    //  0 corresponds to string of group / app name selected in the left pane
   118    //  1 corresponds to string of app name selected in the right pane
   119    const [selected, setSelected] = useState<string[]>([]);
   120  
   121    const groups = useMemo(() => getGroups(filteredAppNames), [filteredAppNames]);
   122    const selectedApp = getSelectedApp(appName, groups, selected);
   123  
   124    const profilesNames = useMemo(() => {
   125      if (!selectedApp?.[0]) {
   126        return [];
   127      }
   128  
   129      const filtered = getGroupMembers(filteredAppNames, selectedApp?.[0]);
   130  
   131      if (filtered.length > 1) {
   132        return filtered;
   133      }
   134  
   135      return [];
   136    }, [selectedApp, groups, filteredAppNames]);
   137  
   138    const onSelect = ({ index, name }: { index: number; name: string }) => {
   139      const filtered = getGroupMembers(filteredAppNames, name);
   140  
   141      if (filtered.length === 1 || index === 1) {
   142        selectAppName(filtered?.[0]);
   143        setModalOpenStatus(false);
   144      }
   145  
   146      const arr = Array.from(selectedApp);
   147  
   148      if (index === 0 && arr?.length > 1) {
   149        arr.pop();
   150      }
   151  
   152      arr[index] = name;
   153  
   154      setSelected(arr);
   155    };
   156  
   157    const listHeight = useMemo(() => {
   158      const height = (window?.innerHeight || 0) - 160;
   159  
   160      const listRequiredHeight =
   161        // 35 is list item height
   162        Math.max(groups?.length || 0, profilesNames?.length || 0) * 35;
   163  
   164      if (height && listRequiredHeight) {
   165        return height >= listRequiredHeight ? 'auto' : `${height}px`;
   166      }
   167  
   168      return 'auto';
   169    }, [groups, profilesNames]);
   170  
   171    return (
   172      <ModalWithToggle
   173        isModalOpen={isModalOpen}
   174        setModalOpenStatus={setModalOpenStatus}
   175        modalClassName={styles.appSelectorModal}
   176        modalHeight={listHeight}
   177        noDataEl={
   178          !filteredAppNames?.length ? (
   179            <div data-testid="app-selector-no-data" className={styles.noData}>
   180              No Data
   181            </div>
   182          ) : null
   183        }
   184        toggleText={appName || 'Select application'}
   185        headerEl={
   186          <div className={styles.header}>
   187            <div>
   188              <div className={styles.headerTitle}>SELECT APPLICATION</div>
   189              <Input
   190                name="application seach"
   191                type="text"
   192                placeholder="Type an app"
   193                value={filters.search.unwrapOr('')}
   194                onChange={(e) => handleFilterChange('search', e.target.value)}
   195                className={styles.searchInput}
   196                testId="application-search"
   197              />
   198            </div>
   199            <div>
   200              <div className={styles.headerTitle}>
   201                <FontAwesomeIcon icon={faSlidersH} /> FILTERS
   202                <button
   203                  className={styles.resetFilters}
   204                  disabled={
   205                    filters.profileTypes.isNothing && filters.spyNames.isNothing
   206                  }
   207                  onClick={resetClickableFilters}
   208                >
   209                  <FontAwesomeIcon icon={faUndo} />
   210                </button>
   211              </div>
   212              <div>
   213                <div className={styles.filter}>
   214                  <div className={styles.filterName}>Language</div>
   215                  <div className={styles.iconsContainer}>
   216                    {spyNameValues.map((v) => (
   217                      <Tooltip placement="top" title={SPY_NAMES_TOOLTIPS[v]}>
   218                        <button
   219                          type="button"
   220                          key={v}
   221                          data-testid={v}
   222                          className={cl(styles.icon, {
   223                            [styles.active]:
   224                              filters.spyNames
   225                                .unwrapOr(
   226                                  [] as (SpyNameFirstClassType | 'unknown')[]
   227                                )
   228                                .indexOf(v) !== -1,
   229                          })}
   230                          onClick={() => handleFilterChange('spyNames', v)}
   231                        >
   232                          {SPY_NAMES_ICONS[v]}
   233                        </button>
   234                      </Tooltip>
   235                    ))}
   236                  </div>
   237                </div>
   238                <div className={styles.filter}>
   239                  <div className={styles.filterName}>Profile type</div>
   240                  <div className={styles.profileTypesContainer}>
   241                    {profileTypeValues.map((v) => (
   242                      <button
   243                        type="button"
   244                        key={v}
   245                        className={cl(styles.profileType, {
   246                          [styles.active]:
   247                            filters.profileTypes
   248                              .unwrapOr([] as string[])
   249                              .indexOf(v) !== -1,
   250                        })}
   251                        onClick={() => handleFilterChange('profileTypes', v)}
   252                      >
   253                        {v}
   254                      </button>
   255                    ))}
   256                  </div>
   257                </div>
   258              </div>
   259            </div>
   260          </div>
   261        }
   262        leftSideEl={groups.map((name) => (
   263          <SelectButton
   264            name={name}
   265            onClick={() => onSelect({ index: 0, name })}
   266            fullList={filteredAppNames}
   267            isSelected={selectedApp?.[0] === name}
   268            key={name}
   269          />
   270        ))}
   271        rightSideEl={profilesNames.map((name) => (
   272          <SelectButton
   273            name={name}
   274            onClick={() => onSelect({ index: 1, name })}
   275            fullList={filteredAppNames}
   276            isSelected={selectedApp?.[1] === name}
   277            key={name}
   278          />
   279        ))}
   280      />
   281    );
   282  };
   283  
   284  export default EnhancedAppSelector;