Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Budgets #1708

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

Budgets #1708

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/ui/src/components/budgets/budgets.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.checkMetric {
display: inline;
}

.checkMetric::before {
content: ' (';
}

.checkMetric::after {
content: ') ';
}

.checkMetricDelta {
padding: 2px;
font-size: 8px;
vertical-align: super;
}

.checkThreshold {
display: inline;
font-weight: bold;
}
98 changes: 98 additions & 0 deletions packages/ui/src/components/budgets/budgets.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import { BudgetResult, BudgetStatus, MetricRunInfoDeltaType } from '@bundle-stats/utils';

import { Budgets } from '.';

export default {
title: 'Components/Budgets',
comonent: Budgets,
};

const BUDGETS: Array<BudgetResult> = [
{
config: {
condition: {
fact: 'webpack.duplicatePackagesCount.delta',
operator: 'greaterThan',
value: 0,
},
status: BudgetStatus.FAILURE,
},
value: 2,
data: {
value: 4,
displayValue: '4',
delta: 2,
displayDelta: '+2',
deltaPercentage: 50,
deltaType: MetricRunInfoDeltaType.HIGH_NEGATIVE,
displayDeltaPercentage: '+50%',
},
matched: true,
},
{
config: {
condition: {
fact: 'webpack.duplicatePackagesCount.value',
operator: 'greaterThan',
value: 0,
},
status: BudgetStatus.WARNING,
},
value: 2,
data: {
value: 2,
displayValue: '2',
delta: 0,
displayDelta: '0',
deltaType: MetricRunInfoDeltaType.NO_CHANGE,
deltaPercentage: 0,
displayDeltaPercentage: '0%',
},
matched: true,
},
{
config: {
condition: {
fact: 'webpack.packageCount.delta',
operator: 'greaterThan',
value: 0,
},
status: BudgetStatus.WARNING,
},
value: 10,
data: {
value: 10,
displayValue: '1',
delta: 1,
displayDelta: '+1',
deltaType: MetricRunInfoDeltaType.NEGATIVE,
deltaPercentage: 10,
displayDeltaPercentage: '+10%',
},
matched: true,
},
{
config: {
condition: {
fact: 'webpack.totalSizeByTypeALL.delta',
operator: 'greaterThan',
value: 5 * 1024,
},
status: BudgetStatus.WARNING,
},
value: 0,
data: {
value: 1048576,
displayValue: '1MiB',
delta: 10 * 1024,
displayDelta: '+10KiB',
deltaType: MetricRunInfoDeltaType.NEGATIVE,
deltaPercentage: 1,
displayDeltaPercentage: '+1%',
},
matched: true,
},
];

export const Default = () => <Budgets budgets={BUDGETS} />;
130 changes: 130 additions & 0 deletions packages/ui/src/components/budgets/budgets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { useMemo } from 'react';
import {
BudgetEvaluated,
BudgetResult,
BudgetStatus,
ConditionOperator,
MetricRunInfoDeltaType,
getGlobalMetricType,
} from '@bundle-stats/utils';
Comment on lines +2 to +9

Check notice

Code scanning / CodeQL

Unused variable, import, function or class

Unused import BudgetStatus.

import { Stack } from '../../layout/stack';
import { Alert } from '../../ui/alert';
import { Delta } from '../delta';
import { Metric } from '../metric';
// @ts-ignore
import css from './budgets.module.css';

const OPERATOR_MAP: Record<ConditionOperator, string> = {
smallerThanInclusive: 'equal',
smallerThan: 'bellow',
equal: 'equal',
notEqual: 'not equal',
greaterThan: 'above',
greaterThanInclusive: 'equal',
};

const ALERT_MAP: Record<string, string> = {
FAILURE: 'danger',
WARNING: 'warning',
SUCCESS: 'success',
};

interface BudgetProps extends React.HTMLAttributes<HTMLElement> {
budget: BudgetEvaluated;
}

const Budget = (props: BudgetProps) => {
const { budget, ...restProps } = props;

const metricSegments = budget.config.condition.fact.split('.');
const metricKey = metricSegments.slice(0, -1).join('.');

const field = metricSegments[metricSegments.length - 1];

let displayField = '';
let deltaDisplayField: string;

switch (field) {
case 'deltaPercentage':
displayField = 'relative difference';
deltaDisplayField = budget.data.displayDeltaPercentage as string;
break;
case 'delta':
displayField = 'absolute difference';
deltaDisplayField = budget.data.displayDelta as string;
break;
default:
displayField = 'value';
deltaDisplayField = budget.data.displayDeltaPercentage as string;
}

const metric = getGlobalMetricType(metricKey);

return (
<Alert kind={ALERT_MAP[budget.config.status]} {...restProps}>
<p>
<strong>{metric.label}</strong>
{` ${displayField} `}
<Metric inline value={budget.data.value} formatter={metric.formatter} className={css.checkMetric}>
<Delta
displayValue={deltaDisplayField}
deltaType={budget.data.deltaType as MetricRunInfoDeltaType}
inverted
className={css.checkMetricDelta}
/>
</Metric>
{` is ${OPERATOR_MAP[budget.config.condition.operator]} `}
<Metric
value={budget.config.condition.value}
formatter={metric.formatter}
inline
className={css.checkThreshold}
/>
</p>
</Alert>
);
};

export interface BudgetsProps extends React.HTMLAttributes<HTMLElement> {
budgets: Array<BudgetResult>;
}

export const Budgets = (props: BudgetsProps) => {
const { budgets, ...restProps } = props;

const [matchedBudgets] = useMemo(() => {
const matched: Array<BudgetEvaluated> = [];
const unmatched: Array<BudgetEvaluated> = [];

budgets.forEach((budget) => {
// @ts-expect-error
if (typeof budget.data === 'undefined') {
return;
}

// @ts-expect-error
if (typeof budget.matched === 'undefined') {
return;
}

// @ts-expect-error
if (budget.matched === true) {
matched.push(budget as BudgetEvaluated);
// @ts-expect-error
} else if (budget.matched === false) {
unmatched.push(budget as BudgetEvaluated);
}
});

return [matched, unmatched];
}, [budgets]);

return (
<Stack space="xxsmall" {...restProps}>
{matchedBudgets.map((budget) => (
<Budget budget={budget} />
))}
</Stack>
);
};
1 change: 1 addition & 0 deletions packages/ui/src/components/budgets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './budgets';
102 changes: 102 additions & 0 deletions packages/utils/src/__tests__/budgets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { BudgetStatus } from '../constants';

import * as budgets from '../budgets';

describe('Budgets', () => {
test('should return skipped budget when data is missing', () => {
expect(
budgets.evaluate(
{
condition: {
fact: 'webpack.totalSizeByTypeALL.delta',
operator: 'greaterThan',
value: 0,
},
status: BudgetStatus.FAILURE,
},
{},
),
).toEqual({
config: {
condition: {
fact: 'webpack.totalSizeByTypeALL.delta',
operator: 'greaterThan',
value: 0,
},
status: BudgetStatus.FAILURE,
},
});
});

test('should return skipped budget when the condition does not match', () => {
expect(
budgets.evaluate(
{
condition: {
fact: 'webpack.totalSizeByTypeALL.delta',
operator: 'greaterThan',
value: 0,
},
status: BudgetStatus.FAILURE,
},
{
webpack: {
totalSizeByTypeALL: {
delta: 0,
},
},
},
),
).toEqual({
config: {
condition: {
fact: 'webpack.totalSizeByTypeALL.delta',
operator: 'greaterThan',
value: 0,
},
status: BudgetStatus.FAILURE,
},
value: 0,
data: {
delta: 0,
},
matched: false,
});
});

test('should return matched budget', () => {
expect(
budgets.evaluate(
{
condition: {
fact: 'webpack.totalSizeByTypeALL.delta',
operator: 'greaterThan',
value: 0,
},
status: BudgetStatus.FAILURE,
},
{
webpack: {
totalSizeByTypeALL: {
delta: 1,
},
},
},
),
).toEqual({
config: {
condition: {
fact: 'webpack.totalSizeByTypeALL.delta',
operator: 'greaterThan',
value: 0,
},
status: BudgetStatus.FAILURE,
},
value: 1,
data: {
delta: 1,
},
matched: true,
});
});
});
Loading