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}
/>
)}
>
);
};