@bundle-stats/ui 3.4.0-alpha.5 → 3.4.0-beta.0

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 (36) hide show
  1. package/lib/components/budget-info/budget-info.js +11 -17
  2. package/lib/components/budget-info/budget-info.stories.js +27 -12
  3. package/lib/components/budget-insights/budget-insights.js +37 -75
  4. package/lib/components/budget-insights/budget-insights.module.css +10 -2
  5. package/lib/components/budget-insights/budget-insights.stories.js +124 -63
  6. package/lib/components/bundle-assets-totals-table/bundle-assets-totals-table.js +3 -4
  7. package/lib/components/summary/summary.js +6 -12
  8. package/lib/components/summary/summary.module.css +1 -45
  9. package/lib/components/summary/summary.stories.js +41 -13
  10. package/lib/components/summary-item/summary-item.js +14 -22
  11. package/lib/components/summary-item/summary-item.module.css +25 -7
  12. package/lib/components/summary-item/summary-item.stories.js +20 -6
  13. package/lib/ui/hover-card/hover-card.js +7 -1
  14. package/lib-esm/components/budget-info/budget-info.js +11 -12
  15. package/lib-esm/components/budget-info/budget-info.stories.js +27 -12
  16. package/lib-esm/components/budget-insights/budget-insights.js +35 -71
  17. package/lib-esm/components/budget-insights/budget-insights.module.css +10 -2
  18. package/lib-esm/components/budget-insights/budget-insights.stories.js +115 -55
  19. package/lib-esm/components/bundle-assets-totals-table/bundle-assets-totals-table.js +3 -4
  20. package/lib-esm/components/summary/summary.js +6 -11
  21. package/lib-esm/components/summary/summary.module.css +1 -45
  22. package/lib-esm/components/summary/summary.stories.js +41 -13
  23. package/lib-esm/components/summary-item/summary-item.js +11 -20
  24. package/lib-esm/components/summary-item/summary-item.module.css +25 -7
  25. package/lib-esm/components/summary-item/summary-item.stories.js +20 -6
  26. package/lib-esm/ui/hover-card/hover-card.js +7 -1
  27. package/package.json +16 -16
  28. package/src/components/budget-info/budget-info.jsx +12 -22
  29. package/src/components/budget-insights/budget-insights.jsx +49 -82
  30. package/src/components/budget-insights/budget-insights.module.css +10 -2
  31. package/src/components/bundle-assets-totals-table/bundle-assets-totals-table.jsx +3 -3
  32. package/src/components/summary/summary.jsx +13 -17
  33. package/src/components/summary/summary.module.css +1 -45
  34. package/src/components/summary-item/summary-item.jsx +11 -17
  35. package/src/components/summary-item/summary-item.module.css +25 -7
  36. package/src/ui/hover-card/hover-card.jsx +5 -0
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import cx from 'classnames';
3
3
  import PropTypes from 'prop-types';
4
4
  import { InsightType } from '@bundle-stats/utils';
5
- import * as budgetsInsightsTransformer from '@bundle-stats/utils/lib-esm/transformers/budgets-insights';
6
5
 
7
6
  import { Icon } from '../../ui/icon';
8
7
  import { Tooltip } from '../../ui/tooltip';
@@ -24,38 +23,29 @@ const INSIGHT_TYPE_MAP = {
24
23
  },
25
24
  };
26
25
 
27
- export const BudgetInfo = ({ className = '', metricId, budgetInsight }) => {
28
- const budgetInsightInfo = budgetsInsightsTransformer.getInfo(metricId, budgetInsight);
29
- const { className: insightTypeClassName, glyph } = INSIGHT_TYPE_MAP[budgetInsightInfo.type];
26
+ export const BudgetInfo = ({ className = '', budgetInsight }) => {
27
+ const { className: insightTypeClassName, glyph } = INSIGHT_TYPE_MAP[budgetInsight.type];
30
28
 
31
29
  const rootClassName = cx(css.root, insightTypeClassName, className);
32
- const { data: messageData } = budgetInsightInfo.message;
33
30
 
34
31
  return (
35
- <Tooltip
36
- className={rootClassName}
37
- title={
38
- <>
39
- <strong>{messageData.metricLabel}</strong>
40
- {` value (`}
41
- <strong>{messageData.currentValue}</strong>
42
- {`) is ${messageData.diffLabel} `}
43
- <strong>{messageData.budgetValue}</strong>
44
- {` budget `}
45
- </>
46
- }
47
- >
32
+ <Tooltip className={rootClassName} title={budgetInsight.message.text}>
48
33
  <Icon className={css.icon} glyph={glyph} size="medium" />
49
34
  </Tooltip>
50
35
  );
51
36
  };
52
37
 
53
38
  BudgetInfo.propTypes = {
54
- metricId: PropTypes.string.isRequired,
55
39
  budgetInsight: PropTypes.shape({
56
- currentValue: PropTypes.number,
57
- budgetValue: PropTypes.number,
58
- failed: PropTypes.bool,
40
+ type: PropTypes.string,
41
+ message: PropTypes.shape({
42
+ text: PropTypes.string,
43
+ }),
44
+ data: PropTypes.shape({
45
+ currentValue: PropTypes.number,
46
+ budgetValue: PropTypes.number,
47
+ failed: PropTypes.bool,
48
+ }),
59
49
  }).isRequired,
60
50
  className: PropTypes.string,
61
51
  };
@@ -2,7 +2,6 @@ import React, { useMemo, useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import cx from 'classnames';
4
4
  import { METRIC_COMPONENT_LINKS } from '@bundle-stats/utils';
5
- import * as budgetsInsightsTransformer from '@bundle-stats/utils/lib-esm/transformers/budgets-insights';
6
5
 
7
6
  import { Box } from '../../layout/box';
8
7
  import { Stack } from '../../layout/stack';
@@ -13,19 +12,15 @@ import { ComponentLink } from '../component-link';
13
12
  import css from './budget-insights.module.css';
14
13
 
15
14
  const Budget = (props) => {
16
- const { className, metricId, budgetInsight, CustomLink } = props;
15
+ const { className, metricId, budget, CustomLink } = props;
17
16
  const componentLinkOptions = METRIC_COMPONENT_LINKS.get(metricId);
18
- const budgetInsightInfo = budgetsInsightsTransformer.getInfo(metricId, budgetInsight);
19
- const { data: messageData } = budgetInsightInfo.message;
20
17
 
21
18
  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 `}
19
+ <CustomLink
20
+ className={cx(css.budget, className, budget.type === 'SUCCESS' && css.budgetSuccess)}
21
+ {...componentLinkOptions?.link}
22
+ >
23
+ {budget.message.text}
29
24
  </CustomLink>
30
25
  );
31
26
  };
@@ -33,10 +28,16 @@ const Budget = (props) => {
33
28
  Budget.propTypes = {
34
29
  className: PropTypes.string,
35
30
  metricId: PropTypes.string.isRequired,
36
- budgetInsight: PropTypes.shape({
37
- currentValue: PropTypes.number.isRequired,
38
- budgetValue: PropTypes.number.isRequired,
39
- failed: PropTypes.bool.isRequired,
31
+ budget: PropTypes.shape({
32
+ type: PropTypes.string,
33
+ message: PropTypes.shape({
34
+ text: PropTypes.string,
35
+ }),
36
+ data: PropTypes.shape({
37
+ currentValue: PropTypes.number.isRequired,
38
+ budgetValue: PropTypes.number.isRequired,
39
+ failed: PropTypes.bool.isRequired,
40
+ }),
40
41
  }).isRequired,
41
42
  CustomLink: PropTypes.elementType.isRequired,
42
43
  };
@@ -45,14 +46,39 @@ Budget.defaultProps = {
45
46
  className: '',
46
47
  };
47
48
 
48
- const BudgetsGroup = (props) => {
49
- const { className, kind, source, budgets, CustomLink } = props;
49
+ const BUDGET_ALERT_KIND = new Map([
50
+ ['ERROR', 'danger'],
51
+ ['WARNING', 'warning'],
52
+ ['SUCCESS', 'success'],
53
+ ]);
54
+
55
+ const BUDGET_ICON = new Map([
56
+ ['ERROR', Icon.ICONS.ALERT_CIRCLE],
57
+ ['WARNING', Icon.ICONS.WARNING],
58
+ ['SUCCESS', Icon.ICONS.CHECK_CIRCLE],
59
+ ]);
60
+
61
+ export const BudgetInsights = (props) => {
62
+ const { className, source, budgets, CustomLink } = props;
50
63
  const [showBudgets, setShowBudgets] = useState(false);
51
64
 
52
- const isFailed = kind === 'danger';
65
+ const iconGlyph = BUDGET_ICON.get(budgets.type);
66
+ const alertKind = BUDGET_ALERT_KIND.get(budgets.type);
67
+ const rootClassName = cx(css.group, className, css[`group-${alertKind}`]);
68
+
69
+ const budgetItems = useMemo(() => {
70
+ const items = Object.entries(budgets.data);
71
+
72
+ // When there are errors/warnings, sort budget items first
73
+ if (['ERROR', 'WARNING'].includes(budgets.type)) {
74
+ items.sort((a) => (a[1].type === budgets.type ? -1 : 1));
75
+ }
76
+
77
+ return items;
78
+ }, [budgets]);
53
79
 
54
80
  return (
55
- <Alert kind={kind} padding="none" className={cx(css.group, css[kind], className)}>
81
+ <Alert kind={alertKind} padding="none" className={rootClassName}>
56
82
  <Box
57
83
  padding={['xsmall', 'small']}
58
84
  className={css.groupHeader}
@@ -61,14 +87,8 @@ const BudgetsGroup = (props) => {
61
87
  onClick={() => setShowBudgets(!showBudgets)}
62
88
  >
63
89
  <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>
90
+ {iconGlyph && <Icon className={css.groupIcon} glyph={iconGlyph} />}
91
+ <span>{budgets.message.text}</span>
72
92
  <span className={css.groupTitleToggle}>{showBudgets ? 'hide' : 'show'} all</span>
73
93
  </FlexStack>
74
94
  </Box>
@@ -76,11 +96,11 @@ const BudgetsGroup = (props) => {
76
96
  {showBudgets && (
77
97
  <Box padding={['xsmall', 'small']} className={css.groupContent}>
78
98
  <Stack space="none">
79
- {budgets.map(([key, budget]) => (
99
+ {budgetItems.map(([key, budget]) => (
80
100
  <Budget
81
101
  key={key}
82
102
  metricId={`${source}.${key}`}
83
- budgetInsight={budget}
103
+ budget={budget}
84
104
  CustomLink={CustomLink}
85
105
  />
86
106
  ))}
@@ -91,59 +111,6 @@ const BudgetsGroup = (props) => {
91
111
  );
92
112
  };
93
113
 
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
114
  BudgetInsights.propTypes = {
148
115
  className: PropTypes.string,
149
116
  source: PropTypes.string.isRequired,
@@ -20,11 +20,15 @@
20
20
  margin: 0 0 0 auto !important;
21
21
  }
22
22
 
23
- .danger .groupIcon {
23
+ .group-danger .groupIcon {
24
24
  color: var(--color-danger);
25
25
  }
26
26
 
27
- .success .groupIcon {
27
+ .group-warning .groupIcon {
28
+ color: var(--color-warning);
29
+ }
30
+
31
+ .group-success .groupIcon {
28
32
  color: var(--color-success);
29
33
  }
30
34
 
@@ -39,6 +43,10 @@
39
43
  text-decoration: none;
40
44
  }
41
45
 
46
+ .budgetSuccess {
47
+ color: var(--color-text-light);
48
+ }
49
+
42
50
  .budgetLabel,
43
51
  .budgetValue {
44
52
  color: var(--color-text-dark);
@@ -18,7 +18,7 @@ export const BundleAssetsTotalsTable = ({
18
18
  ...restProps
19
19
  }) => {
20
20
  const items = useMemo(() => webpack.compareBySection.sizes(jobs), [jobs]);
21
- const budgets = jobs[0].insights?.webpack?.budgets;
21
+ const budgets = jobs[0].insights?.webpack?.budgets?.data;
22
22
 
23
23
  const renderRowHeader = useCallback(
24
24
  (item) => {
@@ -30,9 +30,9 @@ export const BundleAssetsTotalsTable = ({
30
30
 
31
31
  return (
32
32
  <CustomComponentLink section={section} title={title} params={params}>
33
- <FlexStack space="xxsmall" className={css.itemTitleText}>
33
+ <FlexStack space="xxxsmall" className={css.itemTitleText}>
34
34
  <span>{item.label}</span>
35
- {budget && <BudgetInfo metricId={item.key} budgetInsight={budget} />}
35
+ {budget && <BudgetInfo budgetInsight={budget} />}
36
36
  </FlexStack>
37
37
  </CustomComponentLink>
38
38
  );
@@ -5,7 +5,6 @@ import { get } from 'lodash';
5
5
  import { METRIC_COMPONENT_LINKS } from '@bundle-stats/utils';
6
6
 
7
7
  import { METRICS_WEBPACK_GENERAL } from '../../constants';
8
- import { Icon } from '../../ui/icon';
9
8
  import { Box } from '../../layout/box';
10
9
  import { FlexStack } from '../../layout/flex-stack';
11
10
  import { ComponentLink } from '../component-link';
@@ -29,22 +28,19 @@ export const Summary = ({
29
28
  {Array.from(METRIC_COMPONENT_LINKS)
30
29
  .filter(([metricId]) => keys.includes(getSourceMetricId(metricId)))
31
30
  .map(([metricId, metricOptions]) => (
32
- <div key={metricId} className={css.item}>
33
- <SummaryItem
34
- size={size}
35
- id={metricId}
36
- data={get(data, metricId)}
37
- budget={get(budgets, getSourceMetricId(metricId))}
38
- loading={loading}
39
- showMetricDescription
40
- showDelta={showSummaryItemDelta && metricOptions.showDelta !== false}
41
- />
42
- {!loading && (
43
- <SummaryItemCustomLink {...metricOptions.link} className={css.readMore}>
44
- <Icon className={css.readMoreIcon} glyph={Icon.ICONS.ARROW_RIGHT_CIRLCE} />
45
- </SummaryItemCustomLink>
46
- )}
47
- </div>
31
+ <SummaryItem
32
+ key={metricId}
33
+ className={css.item}
34
+ as={SummaryItemCustomLink}
35
+ {...metricOptions.link}
36
+ size={size}
37
+ id={metricId}
38
+ data={get(data, metricId)}
39
+ budget={get(budgets, ['data', getSourceMetricId(metricId)])}
40
+ loading={loading}
41
+ showMetricDescription
42
+ showDelta={showSummaryItemDelta && metricOptions.showDelta !== false}
43
+ />
48
44
  ))}
49
45
  </FlexStack>
50
46
  </Box>
@@ -6,49 +6,5 @@
6
6
  .item {
7
7
  padding: var(--space-small);
8
8
  flex: 1 0 25%; /* force equal sizes */
9
- position: relative;
10
- z-index: 1;
11
- }
12
-
13
- .item::before {
14
- content: '';
15
- display: block;
16
- width: 1px;
17
- position: absolute;
18
- left: -1px;
19
- top: var(--space-small);
20
- bottom: var(--space-small);
21
- background: var(--color-outline);
22
- }
23
-
24
- .readMore {
25
- display: block;
26
- position: absolute;
27
- right: 0;
28
- bottom: 0;
29
- padding: var(--space-small);
30
- color: var(--color-text-ultra-light);
31
- transition: var(--transition-out);
32
- }
33
-
34
- .readMore:hover,
35
- .readMore:focus {
36
- color: var(--color-primary);
37
- transition: var(--transition-in);
38
- }
39
-
40
- .readMoreIcon {
41
- display: block;
42
- }
43
-
44
- /** hover state */
45
- .readMore {
46
- opacity: 0;
47
- }
48
-
49
- .item:hover .readMore,
50
- .item:active .readMore,
51
- .item:focus .readMore {
52
- opacity: 1;
53
- transition: var(--transition-in);
9
+ text-decoration: none;
54
10
  }
@@ -4,10 +4,9 @@ import cx from 'classnames';
4
4
  import { getGlobalMetricType, getMetricRunInfo } from '@bundle-stats/utils';
5
5
 
6
6
  import { HoverCard } from '../../ui/hover-card';
7
- import { Icon } from '../../ui/icon';
8
7
  import { Skeleton } from '../../ui/skeleton';
9
- import { Stack } from '../../layout/stack';
10
8
  import { FlexStack } from '../../layout/flex-stack';
9
+ import { Stack } from '../../layout/stack';
11
10
  import { Metric } from '../metric';
12
11
  import { Delta } from '../delta';
13
12
  import { BudgetInfo } from '../budget-info';
@@ -18,9 +17,9 @@ const MetricInfo = ({ description, url }) => (
18
17
  <p>{description}</p>
19
18
  {url && (
20
19
  <p>
21
- <a href={url} className={css.readMoreLink}>
20
+ <button type="button" onClick={() => window.open(url)} className={css.readMoreLink}>
22
21
  Read more
23
- </a>
22
+ </button>
24
23
  </p>
25
24
  )}
26
25
  </Stack>
@@ -55,17 +54,16 @@ export const SummaryItem = ({
55
54
  const rootClassName = cx(css.root, className, css[size], showDelta && css.showDelta);
56
55
 
57
56
  return (
58
- <Stack space="xxxsmall" as={Component} className={rootClassName} {...props}>
59
- <FlexStack as="h3" space="xxxsmall" className={css.title}>
60
- <span className={css.titleLabel}>{metric.label}</span>
61
-
62
- {showMetricDescriptionTooltip && (
63
- <HoverCard className={css.icon} label={<Icon glyph="help" />}>
57
+ <Stack space="xxsmall" as={Component} className={rootClassName} {...props}>
58
+ <FlexStack space="xxxsmall" as="h3" className={css.title}>
59
+ {showMetricDescriptionTooltip ? (
60
+ <HoverCard label={metric.label}>
64
61
  <MetricInfo {...metric} />
65
62
  </HoverCard>
63
+ ) : (
64
+ metric.label
66
65
  )}
67
-
68
- {budget && <BudgetInfo className={css.budgetInfo} metricId={id} budgetInsight={budget} />}
66
+ {budget && <BudgetInfo className={css.budgetInfo} budgetInsight={budget} />}
69
67
  </FlexStack>
70
68
 
71
69
  {!loading ? (
@@ -134,9 +132,5 @@ SummaryItem.propTypes = {
134
132
  showDelta: PropTypes.bool,
135
133
 
136
134
  /** Budget insight data */
137
- budget: PropTypes.shape({
138
- currentBalue: PropTypes.number,
139
- budgetValue: PropTypes.number,
140
- failed: PropTypes.bool,
141
- }),
135
+ budget: PropTypes.object, // eslint-disable-line react/forbid-prop-types
142
136
  };
@@ -8,17 +8,11 @@
8
8
  color: var(--color-text-ultra-light);
9
9
  font-weight: bold;
10
10
  font-size: var(--size-xsmall);
11
- line-height: var(--space-small);
11
+ line-height: 1;
12
12
  text-transform: uppercase;
13
13
  white-space: nowrap;
14
14
  }
15
15
 
16
- .budgetInfo {
17
- position: absolute;
18
- right: 0;
19
- top: 0;
20
- }
21
-
22
16
  .currentMetric {
23
17
  display: block;
24
18
  color: var(--color-heading);
@@ -39,6 +33,30 @@
39
33
  line-height: var(--space-small);
40
34
  }
41
35
 
36
+ .readMoreLink {
37
+ appearance: none;
38
+ background: none;
39
+ margin: 0;
40
+ border: 0;
41
+ padding: 0;
42
+ color: var(--color-primary);
43
+ text-decoration: underline;
44
+ cursor: pointer;
45
+ }
46
+
47
+ .readMoreLink:hover,
48
+ .readMoreLink:active,
49
+ .readMoreLink:focus {
50
+ color: var(--color-primary-dark);
51
+ text-decoration: none;
52
+ }
53
+
54
+ /* Hover state */
55
+ .root:hover .title {
56
+ color: var(--color-text-light);
57
+ }
58
+
59
+
42
60
  /* Loading state */
43
61
  .loading.currentMetric {
44
62
  width: 8em;
@@ -31,6 +31,10 @@ export const HoverCard = (props) => {
31
31
  // Fallback to span if no href
32
32
  const Component = resolveComponent(as, href);
33
33
 
34
+ const handleOnClick = (event) => {
35
+ event.preventDefault();
36
+ };
37
+
34
38
  return (
35
39
  <div className={cx(css.root, className)}>
36
40
  <HovercardAnchor state={state} href={href} className={css.anchor} as={Component}>
@@ -41,6 +45,7 @@ export const HoverCard = (props) => {
41
45
  state={state}
42
46
  className={cx(css.hoverCard, hoverCardClassName)}
43
47
  style={{ zIndex: 10000 }}
48
+ onClick={handleOnClick}
44
49
  >
45
50
  <HovercardArrow size={24} />
46
51
  {typeof children === 'function' ? children({ close: state.hide }) : children}