From 13c7de0d4ae99cda9dcc412b29b1dcdf5d6240c3 Mon Sep 17 00:00:00 2001 From: Viorel Cojocaru Date: Sat, 16 Nov 2024 00:55:53 +0100 Subject: [PATCH] refactor(ui): bundle-assets - convert to ts --- .../bundle-assets/bundle-assets.jsx | 81 +----- ....stories.jsx => bundle-assets.stories.tsx} | 85 +++--- .../bundle-assets/bundle-assets.utils.js | 178 ------------ .../bundle-assets/bundle-assets.utils.ts | 260 ++++++++++++++++++ packages/ui/src/types.d.ts | 6 +- 5 files changed, 318 insertions(+), 292 deletions(-) rename packages/ui/src/components/bundle-assets/{bundle-assets.stories.jsx => bundle-assets.stories.tsx} (62%) delete mode 100644 packages/ui/src/components/bundle-assets/bundle-assets.utils.js create mode 100644 packages/ui/src/components/bundle-assets/bundle-assets.utils.ts diff --git a/packages/ui/src/components/bundle-assets/bundle-assets.jsx b/packages/ui/src/components/bundle-assets/bundle-assets.jsx index f24081bee0..ae8b65c393 100644 --- a/packages/ui/src/components/bundle-assets/bundle-assets.jsx +++ b/packages/ui/src/components/bundle-assets/bundle-assets.jsx @@ -1,19 +1,8 @@ import React, { useCallback, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; -import get from 'lodash/get'; -import isEmpty from 'lodash/isEmpty'; -import orderBy from 'lodash/orderBy'; import escapeRegExp from 'lodash/escapeRegExp'; -import { - ASSET_CHUNK, - ASSET_ENTRY_TYPE, - ASSET_FILE_TYPE, - ASSET_FILTERS, - COMPONENT, - FILE_TYPE_LABELS, - SECTIONS, -} from '@bundle-stats/utils'; +import { COMPONENT, SECTIONS } from '@bundle-stats/utils'; import config from '../../config.json'; import I18N from '../../i18n'; @@ -29,6 +18,7 @@ import { Filters } from '../../ui/filters'; import { EmptySet } from '../../ui/empty-set'; import { Toolbar } from '../../ui/toolbar'; import { AssetInfo } from '../asset-info'; +import { AssetName } from '../asset-name'; import { ComponentLink } from '../component-link'; import { MetricsTable } from '../metrics-table'; import { MetricsTableExport } from '../metrics-table-export'; @@ -38,78 +28,13 @@ import { MetricsDisplaySelector } from '../metrics-display-selector'; import { MetricsTableHeader } from '../metrics-table-header'; import { MetricsTreemap, getTreemapNodes, getTreemapNodesGroupedByPath } from '../metrics-treemap'; import { SEARCH_PLACEHOLDER } from './bundle-assets.i18n'; +import { getFilters } from './bundle-assets.utils'; import css from './bundle-assets.module.css'; -import { AssetName } from '../asset-name'; const DISPLAY_TYPE_GROUPS = { [MetricsDisplayType.TREEMAP]: ['folder'], }; -const getFileTypeFilters = (filters) => - Object.entries(FILE_TYPE_LABELS).map(([key, label]) => ({ - key, - label, - defaultValue: get(filters, `${ASSET_FILE_TYPE}.${key}`, true), -})); - -const getFilters = ({ compareMode, filters, chunks }) => { - const result = { - [ASSET_FILTERS.CHANGED]: { - label: 'Changed', - defaultValue: filters[ASSET_FILTERS.CHANGED], - disabled: !compareMode, - }, - }; - - if (!isEmpty(chunks)) { - const chunksFilter = { label: 'Chunks', children: [] }; - const chunksOrderedByName = orderBy(chunks, 'name'); - - chunksOrderedByName.forEach((chunk) => { - chunksFilter.children.push({ - key: chunk.id, - label: chunk.name, - defaultValue: filters[`${ASSET_CHUNK}.${chunk.id}`] ?? true, - }); - }); - - result[ASSET_CHUNK] = chunksFilter; - } - - result[ASSET_ENTRY_TYPE] = { - label: 'Type', - children: [ - { - key: ASSET_FILTERS.ENTRY, - label: 'Entry', - defaultValue: get(filters, `${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.ENTRY}`, true), - }, - { - key: ASSET_FILTERS.INITIAL, - label: 'Initial', - defaultValue: get(filters, `${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.INITIAL}`, true), - }, - { - key: ASSET_FILTERS.CHUNK, - label: 'Chunk', - defaultValue: get(filters, `${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.CHUNK}`, true), - }, - { - key: ASSET_FILTERS.OTHER, - label: 'Other', - defaultValue: get(filters, `${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.OTHER}`, true), - }, - ], - }; - - result[ASSET_FILE_TYPE] = { - label: 'File type', - children: getFileTypeFilters(filters), - }; - - return result; -}; - const ViewMetricsTreemap = (props) => { const { metricsTableTitle, diff --git a/packages/ui/src/components/bundle-assets/bundle-assets.stories.jsx b/packages/ui/src/components/bundle-assets/bundle-assets.stories.tsx similarity index 62% rename from packages/ui/src/components/bundle-assets/bundle-assets.stories.jsx rename to packages/ui/src/components/bundle-assets/bundle-assets.stories.tsx index 9ca6156131..7aa28c8232 100644 --- a/packages/ui/src/components/bundle-assets/bundle-assets.stories.jsx +++ b/packages/ui/src/components/bundle-assets/bundle-assets.stories.tsx @@ -8,6 +8,8 @@ import { } from '@bundle-stats/utils'; import merge from 'lodash/merge'; import set from 'lodash/set'; +import type { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; /* eslint-disable import/no-relative-packages */ import baselineStats from '../../../../../fixtures/webpack-stats.baseline.json'; @@ -20,52 +22,66 @@ const JOBS = createJobs([{ webpack: currentStats }, { webpack: baselineStats }]) const [currentJob, baselineJob] = JOBS; const setState = (params) => console.info(params); -export default { +const meta: Meta = { title: 'Components/BundleAssets', component: BundleAssets, decorators: [getWrapperDecorator()], + args: { + setState: action('STATE'), + }, }; -export const Default = () => ; +export default meta; -export const MultipleJobs = () => ; +type Story = StoryObj; -export const CustomFilters = () => ( - -); - -const JOBS_EMPTY_BASELINE = createJobs([{ webpack: currentStats }, {}]); + }, + }, +}; -export const EmptyBaseline = () => ; +export const EmptyBaseline: Story = { + args: { + jobs: createJobs([{ webpack: currentStats }, {}]), + }, +}; -export const NoAssets = () => ( - set(merge({}, job), 'metrics.webpack.assets', {}))} - setState={setState} - /> -); +export const NoAssets: Story = { + args: { + jobs: JOBS.map((job) => set(merge({}, job), 'metrics.webpack.assets', {})), + }, +}; -export const EmptyFilteredData = () => ( - -); + ], + search: 'vendors', + }, +}; -export const NotPredictive = () => ( - ( value: 2988, }, }), - ]} - setState={setState} - /> -); + ], + }, +}; diff --git a/packages/ui/src/components/bundle-assets/bundle-assets.utils.js b/packages/ui/src/components/bundle-assets/bundle-assets.utils.js deleted file mode 100644 index bee9b9fb6f..0000000000 --- a/packages/ui/src/components/bundle-assets/bundle-assets.utils.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * @type {import('@bundle-stats/utils').ReportMetricRow} ReportMetricRow - * @type {import('../../types').ReportMetricAssetRow} ReportMetricAssetRow - * @type {import('../../types').ReportMetricAssetRowMetaStatus} ReportMetricAssetRowFlagStatus - */ - -import intersection from 'lodash/intersection'; -import { - ASSET_CHUNK, - ASSET_ENTRY_TYPE, - ASSET_FILE_TYPE, - ASSET_FILTERS, - getFileType, -} from '@bundle-stats/utils'; - -/** - * Check if the asset cache is not predictive - * - * @param {ReportMetricRow} row - * @returns {boolean} - */ -export const getIsNotPredictive = (row) => { - const { key, runs } = row; - - return runs.reduce((agg, current, index) => { - if (agg) { - return agg; - } - - if (index + 1 === runs.length) { - return agg; - } - - if ( - current && - runs[index + 1] && - key !== current.name && - current.name === runs[index + 1].name && - current.value !== runs[index + 1].value - ) { - return true; - } - - return agg; - }, false); -}; - -/** - * @param {Array} - * @returns {ReportMetricAssetRowFlagStatus | boolean} - */ -export const getAssetMetaStatus = (values) => { - if (!values.includes(true)) { - return false; - } - - // filter empty runs - const metaValues = values.filter((value) => typeof value !== 'undefined'); - - const current = metaValues[0]; - const metaValuesLength = metaValues.length; - - if (metaValuesLength === 1) { - return Boolean(current); - } - - const baseline = metaValues[metaValuesLength - 1]; - - if (current && !baseline) { - return 'added'; - } - - if (!current && baseline) { - return 'removed'; - } - - return true; -}; - -/** - * Add asset row flags - * - * @param {ReportMetricRow} row - * @returns {ReportMetricAssetRow} - */ -export const addMetricReportAssetRowData = (row) => { - const { changed, runs } = row; - - // Collect meta for each run - const runsEntry = []; - const runsInitial = []; - const runsChunk = []; - - runs.forEach((run) => { - runsEntry.push(run?.isEntry); - runsInitial.push(run?.isInitial); - runsChunk.push(run?.isChunk); - }); - - const isEntry = getAssetMetaStatus(runsEntry); - const isInitial = getAssetMetaStatus(runsInitial); - const isChunk = getAssetMetaStatus(runsChunk); - const isAsset = !(isEntry || isInitial || isChunk); - const isNotPredictive = getIsNotPredictive(row); - const fileType = getFileType(row.key); - - // Flag asset as changed if name and value are identical, if one of the meta tags is changed - const assetChanged = - changed || - typeof isEntry !== 'boolean' || - typeof isInitial !== 'boolean' || - typeof isChunk !== 'boolean'; - - return { - ...row, - changed: assetChanged, - isEntry, - isInitial, - isChunk, - isAsset, - isNotPredictive, - fileType, - }; -}; - -/* eslint-disable prettier/prettier */ -export const generateGetRowFilter = - ({ chunkIds }) => (filters) => { - // List of chunkIds with filter value set to `true` - const checkedChunkIds = []; - - chunkIds.forEach((chunkId) => { - if (filters[`${ASSET_CHUNK}.${chunkId}`]) { - checkedChunkIds.push(chunkId); - } - }); - - const hasChunkFilters = - checkedChunkIds.length > 0 && checkedChunkIds.length !== chunkIds.length; - - return (item) => { - if (filters[ASSET_FILTERS.CHANGED] && !item.changed) { - return false; - } - - if ( - !( - (filters[`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.ENTRY}`] && item.isEntry) || - (filters[`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.INITIAL}`] && item.isInitial) || - (filters[`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.CHUNK}`] && item.isChunk) || - (filters[`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.OTHER}`] && item.isAsset) - ) - ) { - return false; - } - - if (!filters[`${ASSET_FILE_TYPE}.${item.fileType}`]) { - return false; - } - - // Filter if any of the chunkIds are checked - if (hasChunkFilters) { - const rowRunsChunkIds = item?.runs?.map((run) => run?.chunkId) || []; - return intersection(rowRunsChunkIds, checkedChunkIds).length > 0; - } - - return true; - }; - }; - -export const getCustomSort = (item) => [ - !item.isNotPredictive, - !item.changed, - !item.isInitial, - !item.isEntry, - !item.isChunk, - item.key, -]; diff --git a/packages/ui/src/components/bundle-assets/bundle-assets.utils.ts b/packages/ui/src/components/bundle-assets/bundle-assets.utils.ts new file mode 100644 index 0000000000..147ec4ec00 --- /dev/null +++ b/packages/ui/src/components/bundle-assets/bundle-assets.utils.ts @@ -0,0 +1,260 @@ +import get from 'lodash/get'; +import intersection from 'lodash/intersection'; +import isEmpty from 'lodash/isEmpty'; +import orderBy from 'lodash/orderBy'; +import type { ReportMetricRow, WebpackChunk } from '@bundle-stats/utils'; +import type { AssetMetricRun } from '@bundle-stats/utils/types/webpack'; +import { + ASSET_CHUNK, + ASSET_ENTRY_TYPE, + ASSET_FILE_TYPE, + ASSET_FILTERS, + FILE_TYPE_LABELS, + getFileType, +} from '@bundle-stats/utils'; +import type { + FilterFieldsData, + ReportMetricAssetRow, + ReportMetricAssetRowMetaStatus, + FilterGroupFieldData, +} from '../../types'; + +/** + * Check if the asset cache is not predictive + */ +export const getIsNotPredictive = (row: ReportMetricRow): boolean => { + const { key, runs } = row; + + return runs.reduce((agg, run, index) => { + if (agg) { + return agg; + } + + if (index + 1 === runs.length) { + return agg; + } + + const preRun = runs[index + 1]; + + if ( + run && + preRun && + key !== run.name && + run.name === preRun.name && + run.value !== preRun.value + ) { + return true; + } + + return agg; + }, false); +}; + +/** + * Return asset meta status + */ +export const getAssetMetaStatus = ( + values: Array, +): ReportMetricAssetRowMetaStatus | boolean => { + if (!values.includes(true)) { + return false; + } + + // filter empty runs + const metaValues = values.filter((value) => typeof value !== 'undefined'); + + const current = metaValues[0]; + const metaValuesLength = metaValues.length; + + if (metaValuesLength === 1) { + return Boolean(current); + } + + const baseline = metaValues[metaValuesLength - 1]; + + if (current && !baseline) { + return 'added'; + } + + if (!current && baseline) { + return 'removed'; + } + + return true; +}; + +/** + * Add asset row flags + */ +export const addMetricReportAssetRowData = (row: ReportMetricRow): ReportMetricAssetRow => { + const { changed, runs } = row; + + // Collect meta for each run + const runsEntry: Array = []; + const runsInitial: Array = []; + const runsChunk: Array = []; + + (runs as Array).forEach((run) => { + runsEntry.push(run?.isEntry || false); + runsInitial.push(run?.isInitial || false); + runsChunk.push(run?.isChunk || false); + }); + + const isEntry = getAssetMetaStatus(runsEntry); + const isInitial = getAssetMetaStatus(runsInitial); + const isChunk = getAssetMetaStatus(runsChunk); + const isAsset = !(isEntry || isInitial || isChunk); + const isNotPredictive = getIsNotPredictive(row); + const fileType = getFileType(row.key); + + // Flag asset as changed if name and value are identical, if one of the meta tags is changed + const assetChanged = + changed || + typeof isEntry !== 'boolean' || + typeof isInitial !== 'boolean' || + typeof isChunk !== 'boolean'; + + return { + ...row, + changed: assetChanged, + isEntry, + isInitial, + isChunk, + isAsset, + isNotPredictive, + fileType, + }; +}; + +type GenerateGetRowFilterOptions = { + chunkIds: Array; +}; + +export const getCustomSort = (item: ReportMetricAssetRow): Array => [ + !item.isNotPredictive, + !item.changed, + !item.isInitial, + !item.isEntry, + !item.isChunk, + item.key, +]; + +const getFileTypeFilters = (filters: Record): FilterGroupFieldData['children'] => + Object.entries(FILE_TYPE_LABELS).map(([key, label]) => ({ + key, + label, + defaultValue: get(filters, `${ASSET_FILE_TYPE}.${key}`, true) as boolean, +})); + +type GetFiltersOptions = { + compareMode: boolean; + filters: Record; + chunks: Array; +}; + +export const getFilters = ({ + compareMode, + filters, + chunks, +}: GetFiltersOptions): FilterFieldsData => { + const result: FilterFieldsData = { + [ASSET_FILTERS.CHANGED]: { + label: 'Changed', + defaultValue: filters[ASSET_FILTERS.CHANGED], + disabled: !compareMode, + }, + }; + + if (!isEmpty(chunks)) { + const chunksFilter: FilterGroupFieldData = { label: 'Chunks', children: [] }; + const chunksOrderedByName = orderBy(chunks, 'name'); + + chunksOrderedByName.forEach((chunk) => { + chunksFilter.children.push({ + key: chunk.id, + label: chunk.name, + defaultValue: filters[`${ASSET_CHUNK}.${chunk.id}`] ?? true, + }); + }); + + result[ASSET_CHUNK] = chunksFilter; + } + + result[ASSET_ENTRY_TYPE] = { + label: 'Type', + children: [ + { + key: ASSET_FILTERS.ENTRY, + label: 'Entry', + defaultValue: get(filters, `${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.ENTRY}`, true), + }, + { + key: ASSET_FILTERS.INITIAL, + label: 'Initial', + defaultValue: get(filters, `${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.INITIAL}`, true), + }, + { + key: ASSET_FILTERS.CHUNK, + label: 'Chunk', + defaultValue: get(filters, `${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.CHUNK}`, true), + }, + { + key: ASSET_FILTERS.OTHER, + label: 'Other', + defaultValue: get(filters, `${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.OTHER}`, true), + }, + ], + }; + + result[ASSET_FILE_TYPE] = { + label: 'File type', + children: getFileTypeFilters(filters), + }; + + return result; +}; + +/* eslint-disable prettier/prettier */ +export const generateGetRowFilter = + ({ chunkIds }: GenerateGetRowFilterOptions) => (filters: Record) => { + // List of chunkIds with filter value set to `true` + const checkedChunkIds: Array = []; + + chunkIds.forEach((chunkId) => { + if (filters[`${ASSET_CHUNK}.${chunkId}`]) { + checkedChunkIds.push(chunkId); + } + }); + + const hasChunkFilters = + checkedChunkIds.length > 0 && checkedChunkIds.length !== chunkIds.length; + + return (item: ReportMetricAssetRow) => { + if (filters[ASSET_FILTERS.CHANGED] && !item.changed) { + return false; + } + + if ( + !( + (filters[`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.ENTRY}`] && item.isEntry) || + (filters[`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.INITIAL}`] && item.isInitial) || + (filters[`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.CHUNK}`] && item.isChunk) || + (filters[`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.OTHER}`] && item.isAsset) + ) + ) { + return false; + } + + if (!filters[`${ASSET_FILE_TYPE}.${item.fileType}`]) { + return false; + } + + // Filter if any of the chunkIds are checked + if (hasChunkFilters) { + const rowRunsChunkIds = item?.runs?.map((run) => run?.chunkId) || []; + return intersection(rowRunsChunkIds, checkedChunkIds).length > 0; + } + + return true; + }; + }; diff --git a/packages/ui/src/types.d.ts b/packages/ui/src/types.d.ts index 2a1b3a644f..474db350ef 100644 --- a/packages/ui/src/types.d.ts +++ b/packages/ui/src/types.d.ts @@ -12,9 +12,13 @@ interface FilterFieldData { disabled?: boolean; } +interface ChildFilterFieldData extends FilterFieldData { + key: string; +} + type FilterGroupFieldData = { label: string; - children: Array<{ key: string } & FilterFieldData>; + children: Array; }; type FilterFieldsData = Record;