@bundle-stats/ui 3.2.9 → 3.4.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/app/app.js +20 -5
  3. package/lib/app/app.stories.js +22 -1
  4. package/lib/assets/icons/alert-circle.svg +1 -0
  5. package/lib/assets/icons/check-circle.svg +1 -0
  6. package/lib/assets/icons.svg +8 -0
  7. package/lib/components/budget-info/budget-info.js +80 -0
  8. package/lib/components/budget-info/budget-info.module.css +40 -0
  9. package/lib/components/budget-info/budget-info.stories.js +44 -0
  10. package/lib/components/budget-info/index.js +18 -0
  11. package/lib/components/budget-insights/budget-insights.js +200 -0
  12. package/lib/components/budget-insights/budget-insights.module.css +53 -0
  13. package/lib/components/budget-insights/budget-insights.stories.js +104 -0
  14. package/lib/components/budget-insights/index.js +18 -0
  15. package/lib/components/bundle-assets-totals-table/bundle-assets-totals-table.js +43 -7
  16. package/lib/components/bundle-assets-totals-table/bundle-assets-totals-table.module.css +2 -2
  17. package/lib/components/bundle-assets-totals-table/bundle-assets-totals-table.stories.js +20 -1
  18. package/lib/components/index.js +8 -0
  19. package/lib/components/summary/summary.js +12 -7
  20. package/lib/components/summary/summary.module.css +1 -1
  21. package/lib/components/summary/summary.stories.js +29 -0
  22. package/lib/components/summary-item/summary-item.js +22 -5
  23. package/lib/components/summary-item/summary-item.module.css +11 -0
  24. package/lib/components/summary-item/summary-item.stories.js +51 -53
  25. package/lib/constants.js +4 -4
  26. package/lib/ui/alert/alert.js +13 -3
  27. package/lib/ui/icon/icon.js +2 -0
  28. package/lib-esm/app/app.js +19 -5
  29. package/lib-esm/app/app.stories.js +22 -1
  30. package/lib-esm/assets/icons/alert-circle.svg +1 -0
  31. package/lib-esm/assets/icons/check-circle.svg +1 -0
  32. package/lib-esm/assets/icons.svg +8 -0
  33. package/lib-esm/components/budget-info/budget-info.js +58 -0
  34. package/lib-esm/components/budget-info/budget-info.module.css +40 -0
  35. package/lib-esm/components/budget-info/budget-info.stories.js +30 -0
  36. package/lib-esm/components/budget-info/index.js +1 -0
  37. package/lib-esm/components/budget-insights/budget-insights.js +148 -0
  38. package/lib-esm/components/budget-insights/budget-insights.module.css +53 -0
  39. package/lib-esm/components/budget-insights/budget-insights.stories.js +84 -0
  40. package/lib-esm/components/budget-insights/index.js +1 -0
  41. package/lib-esm/components/bundle-assets-totals-table/bundle-assets-totals-table.js +24 -7
  42. package/lib-esm/components/bundle-assets-totals-table/bundle-assets-totals-table.module.css +2 -2
  43. package/lib-esm/components/bundle-assets-totals-table/bundle-assets-totals-table.stories.js +18 -0
  44. package/lib-esm/components/index.js +1 -0
  45. package/lib-esm/components/summary/summary.js +11 -7
  46. package/lib-esm/components/summary/summary.module.css +1 -1
  47. package/lib-esm/components/summary/summary.stories.js +27 -0
  48. package/lib-esm/components/summary-item/summary-item.js +20 -4
  49. package/lib-esm/components/summary-item/summary-item.module.css +11 -0
  50. package/lib-esm/components/summary-item/summary-item.stories.js +50 -38
  51. package/lib-esm/constants.js +4 -4
  52. package/lib-esm/ui/alert/alert.js +6 -3
  53. package/lib-esm/ui/icon/icon.js +2 -0
  54. package/package.json +3 -3
  55. package/src/app/app.jsx +12 -5
  56. package/src/assets/icons/alert-circle.svg +1 -0
  57. package/src/assets/icons/check-circle.svg +1 -0
  58. package/src/assets/icons.svg +8 -0
  59. package/src/components/budget-info/budget-info.jsx +65 -0
  60. package/src/components/budget-info/budget-info.module.css +40 -0
  61. package/src/components/budget-info/index.js +1 -0
  62. package/src/components/budget-insights/budget-insights.jsx +157 -0
  63. package/src/components/budget-insights/budget-insights.module.css +53 -0
  64. package/src/components/budget-insights/index.js +1 -0
  65. package/src/components/bundle-assets-totals-table/bundle-assets-totals-table.jsx +27 -13
  66. package/src/components/bundle-assets-totals-table/bundle-assets-totals-table.module.css +2 -2
  67. package/src/components/index.js +1 -0
  68. package/src/components/summary/summary.jsx +8 -7
  69. package/src/components/summary/summary.module.css +1 -1
  70. package/src/components/summary-item/summary-item.jsx +13 -2
  71. package/src/components/summary-item/summary-item.module.css +11 -0
  72. package/src/constants.js +7 -7
  73. package/src/ui/alert/alert.jsx +7 -2
  74. package/src/ui/icon/icon.jsx +2 -0
@@ -0,0 +1,157 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import cx from 'classnames';
4
+ import { METRIC_COMPONENT_LINKS } from '@bundle-stats/utils';
5
+ import * as budgetsInsightsTransformer from '@bundle-stats/utils/lib-esm/transformers/budgets-insights';
6
+
7
+ import { Box } from '../../layout/box';
8
+ import { Stack } from '../../layout/stack';
9
+ import { FlexStack } from '../../layout/flex-stack';
10
+ import { Alert } from '../../ui/alert';
11
+ import { Icon } from '../../ui/icon';
12
+ import { ComponentLink } from '../component-link';
13
+ import css from './budget-insights.module.css';
14
+
15
+ const Budget = (props) => {
16
+ const { className, metricId, budgetInsight, CustomLink } = props;
17
+ const componentLinkOptions = METRIC_COMPONENT_LINKS.get(metricId);
18
+ const budgetInsightInfo = budgetsInsightsTransformer.getInfo(metricId, budgetInsight);
19
+ const { data: messageData } = budgetInsightInfo.message;
20
+
21
+ return (
22
+ <CustomLink className={cx(css.budget, className)} {...componentLinkOptions?.link}>
23
+ <strong>{messageData.metricLabel}</strong>
24
+ {` value (`}
25
+ <strong>{messageData.currentValue}</strong>
26
+ {`) is ${messageData.diffLabel} `}
27
+ <strong>{messageData.budgetValue}</strong>
28
+ {` budget `}
29
+ </CustomLink>
30
+ );
31
+ };
32
+
33
+ Budget.propTypes = {
34
+ className: PropTypes.string,
35
+ metricId: PropTypes.string.isRequired,
36
+ budgetInsight: PropTypes.shape({
37
+ currentValue: PropTypes.number.isRequired,
38
+ budgetValue: PropTypes.number.isRequired,
39
+ failed: PropTypes.bool.isRequired,
40
+ }).isRequired,
41
+ CustomLink: PropTypes.elementType.isRequired,
42
+ };
43
+
44
+ Budget.defaultProps = {
45
+ className: '',
46
+ };
47
+
48
+ const BudgetsGroup = (props) => {
49
+ const { className, kind, source, budgets, CustomLink } = props;
50
+ const [showBudgets, setShowBudgets] = useState(false);
51
+
52
+ const isFailed = kind === 'danger';
53
+
54
+ return (
55
+ <Alert kind={kind} padding="none" className={cx(css.group, css[kind], className)}>
56
+ <Box
57
+ padding={['xsmall', 'small']}
58
+ className={css.groupHeader}
59
+ as="button"
60
+ type="button"
61
+ onClick={() => setShowBudgets(!showBudgets)}
62
+ >
63
+ <FlexStack space="xxsmall" className={css.groupTitle}>
64
+ <Icon
65
+ className={css.groupIcon}
66
+ glyph={isFailed ? Icon.ICONS.ALERT_CIRCLE : Icon.ICONS.CHECK_CIRCLE}
67
+ />
68
+ <span>
69
+ <strong>{budgets.length}</strong>
70
+ {` budget ${budgets.length > 1 ? 'checks' : 'check'} ${isFailed ? 'failed' : 'passed'}`}
71
+ </span>
72
+ <span className={css.groupTitleToggle}>{showBudgets ? 'hide' : 'show'} all</span>
73
+ </FlexStack>
74
+ </Box>
75
+
76
+ {showBudgets && (
77
+ <Box padding={['xsmall', 'small']} className={css.groupContent}>
78
+ <Stack space="none">
79
+ {budgets.map(([key, budget]) => (
80
+ <Budget
81
+ key={key}
82
+ metricId={`${source}.${key}`}
83
+ budgetInsight={budget}
84
+ CustomLink={CustomLink}
85
+ />
86
+ ))}
87
+ </Stack>
88
+ </Box>
89
+ )}
90
+ </Alert>
91
+ );
92
+ };
93
+
94
+ BudgetsGroup.propTypes = {
95
+ className: PropTypes.string,
96
+ kind: PropTypes.oneOf(['success', 'danger']).isRequired,
97
+ source: PropTypes.string.isRequired,
98
+ budgets: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
99
+ CustomLink: PropTypes.elementType.isRequired,
100
+ };
101
+
102
+ BudgetsGroup.defaultProps = {
103
+ className: '',
104
+ };
105
+
106
+ export const BudgetInsights = (props) => {
107
+ const { className, source, budgets, CustomLink } = props;
108
+
109
+ const [failedBudgets, passedBudgets] = useMemo(() => {
110
+ const passed = [];
111
+ const failed = [];
112
+
113
+ Object.entries(budgets).forEach(([key, budget]) => {
114
+ if (budget.failed) {
115
+ failed.push([key, budget]);
116
+ } else {
117
+ passed.push([key, budget]);
118
+ }
119
+ });
120
+
121
+ return [failed, passed];
122
+ }, [budgets]);
123
+
124
+ return (
125
+ <Stack className={className} space="xsmall">
126
+ {failedBudgets.length > 0 && (
127
+ <BudgetsGroup
128
+ kind="danger"
129
+ source={source}
130
+ budgets={failedBudgets}
131
+ CustomLink={CustomLink}
132
+ />
133
+ )}
134
+
135
+ {passedBudgets.length > 0 && (
136
+ <BudgetsGroup
137
+ kind="success"
138
+ source={source}
139
+ budgets={passedBudgets}
140
+ CustomLink={CustomLink}
141
+ />
142
+ )}
143
+ </Stack>
144
+ );
145
+ };
146
+
147
+ BudgetInsights.propTypes = {
148
+ className: PropTypes.string,
149
+ source: PropTypes.string.isRequired,
150
+ budgets: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
151
+ CustomLink: PropTypes.elementType,
152
+ };
153
+
154
+ BudgetInsights.defaultProps = {
155
+ className: '',
156
+ CustomLink: ComponentLink,
157
+ };
@@ -0,0 +1,53 @@
1
+ .groupHeader {
2
+ width: 100%;
3
+ border: none;
4
+ background: none;
5
+ cursor: pointer;
6
+ color: inherit;
7
+ }
8
+
9
+ .groupTitle {
10
+ width: 100%;
11
+ align-items: center;
12
+ }
13
+
14
+ .groupTitle:hover {
15
+ color: var(--color-text-dark);
16
+ text-decoration: underline;
17
+ }
18
+
19
+ .groupTitleToggle {
20
+ margin: 0 0 0 auto !important;
21
+ }
22
+
23
+ .danger .groupIcon {
24
+ color: var(--color-danger);
25
+ }
26
+
27
+ .success .groupIcon {
28
+ color: var(--color-success);
29
+ }
30
+
31
+ .groupContent {
32
+ padding-top: 0px;
33
+ background: transparent;
34
+ }
35
+
36
+ .budget {
37
+ display: block;
38
+ color: var(--color-text);
39
+ text-decoration: none;
40
+ }
41
+
42
+ .budgetLabel,
43
+ .budgetValue {
44
+ color: var(--color-text-dark);
45
+ font-weight: 500;
46
+ }
47
+
48
+ .budget:hover,
49
+ .budget:focus,
50
+ .budget:active {
51
+ color: var(--color-text);
52
+ text-decoration: underline;
53
+ }
@@ -0,0 +1 @@
1
+ export * from './budget-insights';
@@ -1,11 +1,15 @@
1
- import React from 'react';
1
+ import React, { useCallback, useMemo } from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import { get } from 'lodash';
3
4
  import { getBundleAssetsFileTypeComponentLink } from '@bundle-stats/utils';
4
5
  import * as webpack from '@bundle-stats/utils/lib-esm/webpack';
5
6
 
6
7
  import { ASSETS_SIZES_FILE_TYPE_MAP } from '../../constants';
8
+ import { FlexStack } from '../../layout/flex-stack';
7
9
  import { MetricsTable } from '../metrics-table';
8
10
  import { ComponentLink } from '../component-link';
11
+ import { BudgetInfo } from '../budget-info';
12
+ import css from './bundle-assets-totals-table.module.css';
9
13
 
10
14
  export const BundleAssetsTotalsTable = ({
11
15
  className,
@@ -13,18 +17,28 @@ export const BundleAssetsTotalsTable = ({
13
17
  customComponentLink: CustomComponentLink,
14
18
  ...restProps
15
19
  }) => {
16
- const items = webpack.compareBySection.sizes(jobs);
17
-
18
- const renderRowHeader = (item) => {
19
- const fileType = ASSETS_SIZES_FILE_TYPE_MAP[item.key];
20
- const { section, title, params } = getBundleAssetsFileTypeComponentLink(fileType, item.label);
21
-
22
- return (
23
- <CustomComponentLink section={section} title={title} params={params}>
24
- {item.label}
25
- </CustomComponentLink>
26
- );
27
- };
20
+ const items = useMemo(() => webpack.compareBySection.sizes(jobs), [jobs]);
21
+ const budgets = jobs[0].insights?.webpack?.budgets;
22
+
23
+ const renderRowHeader = useCallback(
24
+ (item) => {
25
+ const fileType = ASSETS_SIZES_FILE_TYPE_MAP[item.key];
26
+ const { section, title, params } = getBundleAssetsFileTypeComponentLink(fileType, item.label);
27
+ const [_, ...metricSlugs] = item.key.split('.');
28
+ const metricKey = metricSlugs.join('.');
29
+ const budget = get(budgets, metricKey);
30
+
31
+ return (
32
+ <CustomComponentLink section={section} title={title} params={params}>
33
+ <FlexStack space="xxsmall" className={css.itemTitleText}>
34
+ <span>{item.label}</span>
35
+ {budget && <BudgetInfo metricId={item.key} budgetInsight={budget} />}
36
+ </FlexStack>
37
+ </CustomComponentLink>
38
+ );
39
+ },
40
+ [CustomComponentLink],
41
+ );
28
42
 
29
43
  return (
30
44
  <MetricsTable
@@ -1,3 +1,3 @@
1
- .title {
2
- color: var(--color-text-light);
1
+ .itemTitleText {
2
+ align-items: center;
3
3
  }
@@ -4,6 +4,7 @@ export { BundleAssetsTotalsTable } from './bundle-assets-totals-table';
4
4
  export { BundleAssetsTotalsChartBars } from './bundle-assets-totals-chart-bars';
5
5
  export { BundlePackages } from './bundle-packages';
6
6
  export { BundleModules } from './bundle-modules';
7
+ export { BudgetInsights } from './budget-insights';
7
8
  export { ComponentLink } from './component-link';
8
9
  export { DuplicatePackagesWarning } from './duplicate-packages-warning';
9
10
  export { Delta } from './delta';
@@ -12,11 +12,14 @@ import { ComponentLink } from '../component-link';
12
12
  import { SummaryItem } from '../summary-item';
13
13
  import css from './summary.module.css';
14
14
 
15
+ const getSourceMetricId = (fullMetricId) => fullMetricId.split('.').slice(1).join('.');
16
+
15
17
  export const Summary = ({
16
18
  className,
17
19
  size,
18
20
  keys,
19
21
  data,
22
+ budgets,
20
23
  loading,
21
24
  showSummaryItemDelta,
22
25
  summaryItemLink: SummaryItemCustomLink,
@@ -24,13 +27,14 @@ export const Summary = ({
24
27
  <Box outline className={cx(css.root, className)}>
25
28
  <FlexStack className={css.items}>
26
29
  {Array.from(METRIC_COMPONENT_LINKS)
27
- .filter(([metricId]) => keys.includes(metricId))
30
+ .filter(([metricId]) => keys.includes(getSourceMetricId(metricId)))
28
31
  .map(([metricId, metricOptions]) => (
29
32
  <div key={metricId} className={css.item}>
30
33
  <SummaryItem
31
34
  size={size}
32
35
  id={metricId}
33
36
  data={get(data, metricId)}
37
+ budget={get(budgets, getSourceMetricId(metricId))}
34
38
  loading={loading}
35
39
  showMetricDescription
36
40
  showDelta={showSummaryItemDelta && metricOptions.showDelta !== false}
@@ -49,6 +53,7 @@ export const Summary = ({
49
53
  Summary.defaultProps = {
50
54
  className: '',
51
55
  data: null,
56
+ budgets: null,
52
57
  loading: false,
53
58
  size: '',
54
59
  keys: METRICS_WEBPACK_GENERAL,
@@ -60,12 +65,8 @@ Summary.propTypes = {
60
65
  className: PropTypes.string,
61
66
  size: PropTypes.string,
62
67
  keys: PropTypes.arrayOf(PropTypes.string),
63
- data: PropTypes.shape({
64
- [PropTypes.string]: PropTypes.shape({
65
- baseline: PropTypes.number,
66
- current: PropTypes.number,
67
- }),
68
- }),
68
+ data: PropTypes.object, // eslint-disable-line react/forbid-prop-types
69
+ budgets: PropTypes.object, // eslint-disable-line react/forbid-prop-types
69
70
  loading: PropTypes.bool,
70
71
  showSummaryItemDelta: PropTypes.bool,
71
72
  summaryItemLink: PropTypes.elementType,
@@ -4,7 +4,7 @@
4
4
  }
5
5
 
6
6
  .item {
7
- padding: var(--space-small) var(--space-xxlarge) var(--space-small) var(--space-small);
7
+ padding: var(--space-small);
8
8
  flex: 1 0 25%; /* force equal sizes */
9
9
  position: relative;
10
10
  z-index: 1;
@@ -10,6 +10,7 @@ import { Stack } from '../../layout/stack';
10
10
  import { FlexStack } from '../../layout/flex-stack';
11
11
  import { Metric } from '../metric';
12
12
  import { Delta } from '../delta';
13
+ import { BudgetInfo } from '../budget-info';
13
14
  import css from './summary-item.module.css';
14
15
 
15
16
  const MetricInfo = ({ description, url }) => (
@@ -43,6 +44,7 @@ export const SummaryItem = ({
43
44
  loading,
44
45
  showDelta,
45
46
  showMetricDescription,
47
+ budget,
46
48
  ...props
47
49
  }) => {
48
50
  const { baseline, current } = data || { baseline: 0, current: 0 };
@@ -50,19 +52,20 @@ export const SummaryItem = ({
50
52
  const metric = getGlobalMetricType(id);
51
53
  const runInfo = getMetricRunInfo(metric, current, baseline);
52
54
  const showMetricDescriptionTooltip = showMetricDescription && metric?.description;
53
-
54
55
  const rootClassName = cx(css.root, className, css[size], showDelta && css.showDelta);
55
56
 
56
57
  return (
57
58
  <Stack space="xxxsmall" as={Component} className={rootClassName} {...props}>
58
59
  <FlexStack as="h3" space="xxxsmall" className={css.title}>
59
- <span>{metric.label}</span>
60
+ <span className={css.titleLabel}>{metric.label}</span>
60
61
 
61
62
  {showMetricDescriptionTooltip && (
62
63
  <HoverCard className={css.icon} label={<Icon glyph="help" />}>
63
64
  <MetricInfo {...metric} />
64
65
  </HoverCard>
65
66
  )}
67
+
68
+ {budget && <BudgetInfo className={css.budgetInfo} metricId={id} budgetInsight={budget} />}
66
69
  </FlexStack>
67
70
 
68
71
  {!loading ? (
@@ -102,6 +105,7 @@ SummaryItem.defaultProps = {
102
105
  loading: false,
103
106
  showMetricDescription: false,
104
107
  showDelta: true,
108
+ budget: undefined,
105
109
  };
106
110
 
107
111
  SummaryItem.propTypes = {
@@ -128,4 +132,11 @@ SummaryItem.propTypes = {
128
132
 
129
133
  /** Show delta */
130
134
  showDelta: PropTypes.bool,
135
+
136
+ /** Budget insight data */
137
+ budget: PropTypes.shape({
138
+ currentBalue: PropTypes.number,
139
+ budgetValue: PropTypes.number,
140
+ failed: PropTypes.bool,
141
+ }),
131
142
  };
@@ -1,3 +1,8 @@
1
+ .root {
2
+ position: relative;
3
+ padding-right: var(--space-xlarge);
4
+ }
5
+
1
6
  .title {
2
7
  align-items: center;
3
8
  color: var(--color-text-ultra-light);
@@ -8,6 +13,12 @@
8
13
  white-space: nowrap;
9
14
  }
10
15
 
16
+ .budgetInfo {
17
+ position: absolute;
18
+ right: 0;
19
+ top: 0;
20
+ }
21
+
11
22
  .currentMetric {
12
23
  display: block;
13
24
  color: var(--color-heading);
package/src/constants.js CHANGED
@@ -47,11 +47,11 @@ export const ASSETS_SIZES_FILE_TYPE_MAP = FILE_TYPES.reduce(
47
47
  );
48
48
 
49
49
  export const METRICS_WEBPACK_GENERAL = [
50
- 'webpack.totalSizeByTypeALL',
51
- 'webpack.totalInitialSizeJS',
52
- 'webpack.totalInitialSizeCSS',
53
- 'webpack.cacheInvalidation',
50
+ 'totalSizeByTypeALL',
51
+ 'totalInitialSizeJS',
52
+ 'totalInitialSizeCSS',
53
+ 'cacheInvalidation',
54
54
  ];
55
- export const METRICS_WEBPACK_ASSETS = ['webpack.assetCount', 'webpack.chunkCount'];
56
- export const METRICS_WEBPACK_MODULES = ['webpack.moduleCount'];
57
- export const METRICS_WEBPACK_PACKAGES = ['webpack.packageCount', 'webpack.duplicatePackagesCount'];
55
+ export const METRICS_WEBPACK_ASSETS = ['assetCount', 'chunkCount'];
56
+ export const METRICS_WEBPACK_MODULES = ['moduleCount'];
57
+ export const METRICS_WEBPACK_PACKAGES = ['packageCount', 'duplicatePackagesCount'];
@@ -6,10 +6,15 @@ import { Box } from '../../layout/box';
6
6
  import css from './alert.module.css';
7
7
 
8
8
  export const Alert = (props) => {
9
- const { className, kind, children } = props;
9
+ const { className, kind, children, ...restProps } = props;
10
10
 
11
11
  return (
12
- <Box padding={['xsmall', 'small']} outline className={cx(css.root, className, css[kind])}>
12
+ <Box
13
+ padding={['xsmall', 'small']}
14
+ outline
15
+ className={cx(css.root, className, css[kind])}
16
+ {...restProps}
17
+ >
13
18
  {children}
14
19
  </Box>
15
20
  );
@@ -5,11 +5,13 @@ import cx from 'classnames';
5
5
  import css from './icon.module.css';
6
6
 
7
7
  const ICONS = {
8
+ ALERT_CIRCLE: 'alert-circle',
8
9
  ARROW: 'arrow',
9
10
  ARROW_RIGHT_CIRLCE: 'arrow-right-circle',
10
11
  CANCEL: 'cancel',
11
12
  CLOSE: 'close',
12
13
  CLOCK: 'clock',
14
+ CHECK_CIRCLE: 'check-circle',
13
15
  COMMIT: 'commit',
14
16
  FILTER: 'filter',
15
17
  HELP: 'help',