Skip to content

Commit

Permalink
feat(ui): Budgets component
Browse files Browse the repository at this point in the history
  • Loading branch information
vio committed Jan 4, 2023
1 parent ab87bdf commit 9cd27c2
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 0 deletions.
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;
}
90 changes: 90 additions & 0 deletions packages/ui/src/components/budgets/budgets.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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> = [
{
condition: {
fact: 'webpack.duplicatePackagesCount.delta',
operator: 'greaterThan',
value: 0,
},
value: 2,
data: {
value: 4,
displayValue: '4',
delta: 2,
displayDelta: '+2',
deltaPercentage: 50,
deltaType: MetricRunInfoDeltaType.HIGH_NEGATIVE,
displayDeltaPercentage: '+50%',
},
matched: true,
status: BudgetStatus.FAILURE,
},
{
condition: {
fact: 'webpack.duplicatePackagesCount.value',
operator: 'greaterThan',
value: 0,
},
value: 2,
data: {
value: 2,
displayValue: '2',
delta: 0,
displayDelta: '0',
deltaType: MetricRunInfoDeltaType.NO_CHANGE,
deltaPercentage: 0,
displayDeltaPercentage: '0%',
},
matched: true,
status: BudgetStatus.WARNING,
},
{
condition: {
fact: 'webpack.packageCount.delta',
operator: 'greaterThan',
value: 0,
},
value: 10,
data: {
value: 10,
displayValue: '1',
delta: 1,
displayDelta: '+1',
deltaType: MetricRunInfoDeltaType.NEGATIVE,
deltaPercentage: 10,
displayDeltaPercentage: '+10%',
},
matched: true,
status: BudgetStatus.WARNING,
},
{
condition: {
fact: 'webpack.totalSizeByTypeALL.delta',
operator: 'greaterThan',
value: 5 * 1024,
},
value: 0,
data: {
value: 1048576,
displayValue: '1MiB',
delta: 10 * 1024,
displayDelta: '+10KiB',
deltaType: MetricRunInfoDeltaType.NEGATIVE,
deltaPercentage: 1,
displayDeltaPercentage: '+1%',
},
matched: true,
status: BudgetStatus.WARNING,
},
];

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

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 VALUE_MAP: Record<string, string> = {
value: 'to',
delta: 'by',
deltaPercentage: 'by',
};

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

interface BudgetMatched extends Omit<BudgetEvaluated, 'status'> {
status: BudgetStatus;
}

interface BudgetProps extends React.HTMLAttributes<HTMLElement> {
check: BudgetMatched;
}

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

const metricSegments = check.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 = check.data.displayDeltaPercentage as string;
break;
case 'delta':
displayField = 'absolute difference';
deltaDisplayField = check.data.displayDelta as string;
break;
default:
displayField = 'value';
deltaDisplayField = check.data.displayDeltaPercentage as string;
}

const metric = getGlobalMetricType(metricKey);

return (
<Alert kind={ALERT_MAP[check.status]} {...restProps}>
<p>
<strong>{metric.label}</strong>
{` ${displayField} `}
<Metric inline value={check.data.value} formatter={metric.formatter} className={css.checkMetric}>
<Delta
displayValue={deltaDisplayField}
deltaType={check.data.deltaType as MetricRunInfoDeltaType}
inverted
className={css.checkMetricDelta}
/>
</Metric>
{` is ${OPERATOR_MAP[check.condition.operator]} `}
<Metric
value={check.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<BudgetMatched> = [];
const unmatched: Array<BudgetEvaluated> = [];

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

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

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

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

return (
<Stack space="xxsmall" {...restProps}>
{matchedBudgets.map((check) => (
<Budget check={check} />
))}
</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';

0 comments on commit 9cd27c2

Please sign in to comment.