import type { ComponentProps, ElementType } from 'react'; import React, { useCallback, useMemo, useState } from 'react'; import cx from 'classnames'; import { SECTIONS, COMPONENT, type Job, ReportMetricRow } from '@bundle-stats/utils'; import escapeRegExp from 'lodash/escapeRegExp'; import { WebpackChunk } from '@bundle-stats/utils'; import type { ReportMetricModuleRow, SortAction } from '../../types'; import config from '../../config.json'; import I18N from '../../i18n'; import { MetricsDisplayGroupBy, MetricsDisplayType, ModuleSizeMetric, ModuleSizeMetrics, } from '../../constants'; import { Box } from '../../layout/box'; import { FlexStack } from '../../layout/flex-stack'; import { Stack } from '../../layout/stack'; import { Button } from '../../ui/button'; import { ControlGroup } from '../../ui/control-group'; import { Dialog, useDialogState } from '../../ui/dialog'; import { EmptySet } from '../../ui/empty-set'; import { FileName } from '../../ui/file-name'; import { Filters } from '../../ui/filters'; import { InputSearch } from '../../ui/input-search'; import { Tag } from '../../ui/tag'; import { Table } from '../../ui/table'; import { Toolbar } from '../../ui/toolbar'; import { Tooltip } from '../../ui/tooltip'; import { ComponentLink } from '../component-link'; import { MetricsTable } from '../metrics-table'; import { MetricsTableExport } from '../metrics-table-export'; import { MetricsTableHeader } from '../metrics-table-header'; import { MetricsTreemap, getTreemapNodes, getTreemapNodesGroupedByPath } from '../metrics-treemap'; import { MetricsTableOptions } from '../metrics-table-options'; import { MetricsDisplaySelector } from '../metrics-display-selector'; import { MetricsTableTitle } from '../metrics-table-title'; import { ModuleInfo } from '../module-info'; import { generateFilterFieldsData } from './bundle-modules.utils'; import * as I18N_MODULES from './bundle-modules.i18n'; import css from './bundle-modules.module.css'; import { useMetricsDisplayType } from '../../hooks/metrics-display-type'; const DISPLAY_TYPE_GROUPS = { [MetricsDisplayType.TREEMAP]: [MetricsDisplayGroupBy.FOLDER], }; interface RowHeaderProps { row: ReportMetricRow; EntryComponentLink: ElementType; } const RowHeader = (props: RowHeaderProps) => { const { row, EntryComponentLink } = props; const moduleRow = row as ReportMetricModuleRow; return ( {moduleRow.duplicated && ( )} ); }; interface ViewMetricsTreemapProps { metricsTableTitle: React.ReactNode; jobs: Array; items: Array; displayType: { value: MetricsDisplayType; groupBy?: string }; emptyMessage: React.ReactNode; showEntryInfo: React.ComponentProps['onItemClick']; updateSearch: (newSerarch: string) => void; search: string; } const ViewMetricsTreemap = (props: ViewMetricsTreemapProps) => { const { metricsTableTitle, jobs, items, displayType, emptyMessage, showEntryInfo, updateSearch, search, } = props; // Get treenodes based on group const treeNodes = useMemo(() => { if (displayType.groupBy === 'folder') { return getTreemapNodesGroupedByPath(items); } return getTreemapNodes(items); }, [items, displayType]); // Search based on the group path on group title click const onGroupClick = useCallback( (groupPath: string) => { // Clear seach when groupPath is emty (root) if (groupPath === '') { updateSearch(''); return; } // Search by group path // 1. use `^` to match only the string beggining // 2. add `/` suffix to exactly match the directory const newSearch = `^${escapeRegExp(groupPath)}/`; // Reset search when toggling the same groupPath if (newSearch === search) { updateSearch(''); return; } updateSearch(newSearch); }, [updateSearch, search], ); return ( <>
); }; interface BundleModulesProps extends React.ComponentProps<'div'> { jobs?: Array; items?: Array; allItems?: Array; chunks?: Array; totalRowCount: number; updateFilters: () => void; resetFilters: () => void; resetAllFilters: () => void; filters: Record; sort: any; updateSort: ({ field, direction }: SortAction) => void; search: string; updateSearch: (newSerarch: string) => void; entryId?: string; hideEntryInfo: () => void; showEntryInfo: (entryId: string) => void; moduleMetric: ModuleSizeMetric; setModuleMetric: (newValue: ModuleSizeMetric) => void; customComponentLink: React.ElementType; } export const BundleModules = (props: BundleModulesProps) => { const { className = '', jobs = [], items = [], allItems = [], chunks = [], totalRowCount = 0, updateFilters, resetFilters, resetAllFilters, filters, sort, updateSort, search, updateSearch, entryId, hideEntryInfo, moduleMetric, setModuleMetric, showEntryInfo, customComponentLink: CustomComponentLink = ComponentLink, } = props; const rootClassName = cx(css.root, className); const jobLabels = jobs?.map((job) => job?.label); const [displayType, setDisplayType] = useMetricsDisplayType(DISPLAY_TYPE_GROUPS); const filterFieldsData = useMemo( () => generateFilterFieldsData({ filters, chunks, compareMode: jobs.length > 1 }), [jobs, filters, chunks], ); const EntryComponentLink = useCallback( ({ entryId: moduleEntryId, ...moduleNameRestProps }: { entryId: string } & ComponentProps) => ( ), [CustomComponentLink, filters, search, sort, moduleMetric], ); const metricsTableTitle = useMemo( () => ( ), [items, totalRowCount], ); const renderRowHeader = useCallback( (row: ReportMetricRow) => , [EntryComponentLink], ); const emptyMessage = useMemo( () => ( ), [totalRowCount, resetFilters, resetAllFilters], ); const entryItem = useMemo(() => { if (!entryId) { return null; } return allItems.find(({ key }) => key === entryId); }, [allItems, entryId]); const exportDialog = useDialogState(); const [exportSourceType, setExportSourceType] = useState(undefined); const handleExportClick = useCallback((sourceType: string) => { setExportSourceType(sourceType); exportDialog.toggle(); }, []); return ( <> ( )} > {displayType.value === MetricsDisplayType.TABLE && ( )} {displayType.value === MetricsDisplayType.TREEMAP && ( )} {entryItem && ( id)} labels={jobLabels} metricLabel={ModuleSizeMetrics[moduleMetric].label} customComponentLink={CustomComponentLink} onClose={hideEntryInfo} /> )} {exportDialog.open && ( )} ); };