Merge branch 'performance' into dev
This commit is contained in:
commit
e2ab4af493
24 files changed
+1028
-8
No files matched your search
@@ -8,7 +8,7 @@ import {
|
||||
User,
|
||||
UserPlus,
|
||||
} from '@/components/icons';
|
||||
import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
||||
import { Funnel, Gauge, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
||||
import { useMessages } from './useMessages';
|
||||
import { useNavigation } from './useNavigation';
|
||||
|
||||
@@ -53,6 +53,12 @@ export function useWebsiteNavItems(websiteId: string) {
|
||||
icon: <Clock />,
|
||||
path: renderPath('/realtime'),
|
||||
},
|
||||
{
|
||||
id: 'performance',
|
||||
label: t(labels.performance),
|
||||
icon: <Gauge />,
|
||||
path: renderPath('/performance'),
|
||||
},
|
||||
{
|
||||
id: 'compare',
|
||||
label: t(labels.compare),
|
||||
|
||||
@@ -345,6 +345,16 @@ export const labels: Record<string, string> = {
|
||||
support: 'label.support',
|
||||
documentation: 'label.documentation',
|
||||
switchAccount: 'label.switch-account',
|
||||
performance: 'label.performance',
|
||||
lcp: 'label.lcp',
|
||||
inp: 'label.inp',
|
||||
cls: 'label.cls',
|
||||
fcp: 'label.fcp',
|
||||
ttfb: 'label.ttfb',
|
||||
good: 'label.good',
|
||||
needsImprovement: 'label.needs-improvement',
|
||||
poor: 'label.poor',
|
||||
sampleSize: 'label.sample-size',
|
||||
};
|
||||
|
||||
export const messages: Record<string, string> = {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
.card {
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
box-shadow: 0 0 0 2px var(--base-color-primary);
|
||||
}
|
||||
|
||||
.good .rating {
|
||||
color: #0cce6b;
|
||||
}
|
||||
|
||||
.needs-improvement .rating {
|
||||
color: #ffa400;
|
||||
}
|
||||
|
||||
.poor .rating {
|
||||
color: #ff4e42;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useSpring } from '@react-spring/web';
|
||||
import { Column, Text } from '@umami/react-zen';
|
||||
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { WEB_VITALS_THRESHOLDS } from '@/lib/constants';
|
||||
import { formatNumber } from '@/lib/format';
|
||||
import styles from './PerformanceCard.module.css';
|
||||
|
||||
export interface PerformanceCardProps {
|
||||
metric: 'lcp' | 'inp' | 'cls' | 'fcp' | 'ttfb';
|
||||
value: number;
|
||||
label: string;
|
||||
formatValue?: (n: any) => string;
|
||||
onClick?: () => void;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
function getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
|
||||
const threshold = WEB_VITALS_THRESHOLDS[metric as keyof typeof WEB_VITALS_THRESHOLDS];
|
||||
if (!threshold || value <= 0) return 'good';
|
||||
if (value <= threshold.good) return 'good';
|
||||
if (value <= threshold.poor) return 'needs-improvement';
|
||||
return 'poor';
|
||||
}
|
||||
|
||||
export const PerformanceCard = ({
|
||||
metric,
|
||||
value = 0,
|
||||
label,
|
||||
formatValue = formatNumber,
|
||||
onClick,
|
||||
selected = false,
|
||||
}: PerformanceCardProps) => {
|
||||
const { t, labels } = useMessages();
|
||||
const rating = getRating(metric, value);
|
||||
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
|
||||
|
||||
return (
|
||||
<Column
|
||||
className={`${styles.card} ${styles[rating]} ${selected ? styles.selected : ''}`}
|
||||
justifyContent="center"
|
||||
paddingX="6"
|
||||
paddingY="4"
|
||||
borderRadius
|
||||
backgroundColor="surface-base"
|
||||
border
|
||||
gap="4"
|
||||
onClick={onClick}
|
||||
style={{ cursor: onClick ? 'pointer' : undefined }}
|
||||
>
|
||||
<Text weight="bold" wrap="nowrap">
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="4xl" weight="bold" wrap="nowrap">
|
||||
<AnimatedDiv title={value?.toString()}>{props?.x?.to(x => formatValue(x))}</AnimatedDiv>
|
||||
</Text>
|
||||
<Text size="sm" className={styles.rating}>
|
||||
{t(labels[rating === 'needs-improvement' ? 'needsImprovement' : rating])}
|
||||
</Text>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
const SvgGauge = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlSpace="preserve"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
{...props}
|
||||
>
|
||||
<path d="M256 48C141.1 48 48 141.1 48 256s93.1 208 208 208 208-93.1 208-208S370.9 48 256 48m0 384c-97.2 0-176-78.8-176-176S158.8 80 256 80s176 78.8 176 176-78.8 176-176 176" />
|
||||
<path d="M256 128c-8.8 0-16 7.2-16 16v16c0 8.8 7.2 16 16 16s16-7.2 16-16v-16c0-8.8-7.2-16-16-16m-108.7 44.7-11.3-11.3c-6.2-6.2-16.4-6.2-22.6 0s-6.2 16.4 0 22.6l11.3 11.3c6.2 6.2 16.4 6.2 22.6 0s6.2-16.4 0-22.6M144 240h-16c-8.8 0-16 7.2-16 16s7.2 16 16 16h16c8.8 0 16-7.2 16-16s-7.2-16-16-16m240 0h-16c-8.8 0-16 7.2-16 16s7.2 16 16 16h16c8.8 0 16-7.2 16-16s-7.2-16-16-16m-44.7-67.3c-6.2-6.2-16.4-6.2-22.6 0s-6.2 16.4 0 22.6l11.3 11.3c6.2 6.2 16.4 6.2 22.6 0s6.2-16.4 0-22.6zM256 208c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48m0 64c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16m0 64c-8.8 0-16 7.2-16 16v16c0 8.8 7.2 16 16 16s16-7.2 16-16v-16c0-8.8-7.2-16-16-16" />
|
||||
</svg>
|
||||
);
|
||||
export default SvgGauge;
|
||||
@@ -11,6 +11,7 @@ export { default as Expand } from './Expand';
|
||||
export { default as Export } from './Export';
|
||||
export { default as Flag } from './Flag';
|
||||
export { default as Funnel } from './Funnel';
|
||||
export { default as Gauge } from './Gauge';
|
||||
export { default as Gear } from './Gear';
|
||||
export { default as Lightbulb } from './Lightbulb';
|
||||
export { default as Lightning } from './Lightning';
|
||||
|
||||
Reference in new issue
Block a user