Skip to content

Commit 3a127f7

Browse files
committed
Improve KPIControl added compact card
1 parent 2e89a54 commit 3a127f7

5 files changed

Lines changed: 358 additions & 56 deletions

File tree

454 KB
Loading

docs/documentation/docs/controls/KPIControl.md

Lines changed: 155 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# KPIControl
22

3-
A React component for displaying Key Performance Indicator (KPI) cards in a responsive grid layout. Each card visualizes progress toward a goal with visual indicators, progress bars, and status badges.
3+
A React component for displaying Key Performance Indicator (KPI) cards in a responsive grid layout. Each card visualizes progress toward a goal with visual indicators, progress bars, and status badges. Two card variants are available: a full-featured card and a compact version.
44

55
## Table of Contents
66

77
- [Installation](#installation)
88
- [Basic Usage](#basic-usage)
9+
- [Card Variants](#card-variants)
910
- [Properties](#properties)
1011
- [Data Structure](#data-structure)
1112
- [Goal Metrics](#goal-metrics)
@@ -20,7 +21,7 @@ npm install @pnp/spfx-controls-react
2021

2122
## Example of KPIControl
2223

23-
![KPIControl Example](../assets/KPIControl3.png)
24+
![KPIControl Example](../assets/KPIControl.png)
2425

2526
## Basic Usage
2627

@@ -35,7 +36,13 @@ const MyKPIComponent: React.FC = () => {
3536
};
3637
```
3738

38-
## Using Individual KPI Cards
39+
## Card Variants
40+
41+
The KPIControl offers two card variants to suit different use cases:
42+
43+
### KPICard (Full Version)
44+
45+
The full KPICard displays comprehensive information including footer metrics (Goal, Total Items, % of Total) and a status badge.
3946

4047
```tsx
4148
import * as React from 'react';
@@ -59,15 +66,71 @@ const MyKPICard: React.FC = () => {
5966
};
6067
```
6168

69+
### KPICardCompact (Compact Version)
70+
71+
The compact version displays only essential information: title, goal metric indicator, current value, goal, and progress bar. Ideal for dashboards with limited space or when displaying many KPIs.
72+
73+
```tsx
74+
import * as React from 'react';
75+
import { KPICardCompact } from '@pnp/spfx-controls-react/lib/KPIControl';
76+
import { IKpiCardData, EGoalMetric } from '@pnp/spfx-controls-react/lib/KPIControl';
77+
78+
const MyCompactKPICard: React.FC = () => {
79+
const kpiData: IKpiCardData = {
80+
identifier: 'sales-kpi',
81+
title: 'Sales Revenue',
82+
currentValue: 125000,
83+
goal: 150000,
84+
totalItems: 200000,
85+
description: 'Total sales revenue for Q1',
86+
goalMetric: EGoalMetric.HIGHER_IS_BETTER,
87+
};
88+
89+
return (
90+
<KPICardCompact dataCard={kpiData} />
91+
);
92+
};
93+
```
94+
95+
### Comparison
96+
97+
| Feature | KPICard | KPICardCompact |
98+
|---------|---------|----------------|
99+
| Title |||
100+
| Goal Metric Indicator |||
101+
| Current Value / Goal |||
102+
| Progress Bar |||
103+
| Progress Percentage |||
104+
| Footer Metrics (Goal, Total Items, % of Total) |||
105+
| Status Badge (On Track / Off Track) |||
106+
| Glow Effect |||
107+
| Info Tooltip |||
108+
62109
## Properties
63110

64111
### Kpis Component Properties
65112

66113
| Property | Type | Required | Default | Description |
67114
|----------|------|----------|---------|-------------|
68115
| `skeletonCount` | `number` | No | `3` | Number of skeleton cards to display while loading |
116+
| `compact` | `boolean` | No | `false` | When `true`, renders compact KPI cards instead of full cards |
117+
118+
#### Using the compact prop
119+
120+
```tsx
121+
import * as React from 'react';
122+
import { Kpis } from '@pnp/spfx-controls-react/lib/KPIControl';
123+
124+
// Full cards (default)
125+
const FullKpis: React.FC = () => <Kpis />;
126+
127+
// Compact cards
128+
const CompactKpis: React.FC = () => <Kpis compact />;
129+
```
130+
131+
### KPICard & KPICardCompact Component Properties
69132

70-
### KPICard Component Properties
133+
Both card variants share the same props interface:
71134

72135
| Property | Type | Required | Default | Description |
73136
|----------|------|----------|---------|-------------|
@@ -235,3 +298,91 @@ const salesKpi: IKpiCardData = {
235298
goalMetric: EGoalMetric.HIGHER_IS_BETTER,
236299
};
237300
```
301+
302+
### Using Compact Cards in a Dashboard
303+
304+
```tsx
305+
import * as React from 'react';
306+
import { KPICardCompact } from '@pnp/spfx-controls-react/lib/KPIControl';
307+
import { IKpiCardData, EGoalMetric } from '@pnp/spfx-controls-react/lib/KPIControl';
308+
309+
const kpis: IKpiCardData[] = [
310+
{
311+
identifier: 'kpi-1',
312+
title: 'Sales Revenue',
313+
currentValue: 125000,
314+
goal: 150000,
315+
totalItems: 200000,
316+
description: 'Total sales revenue for Q1',
317+
goalMetric: EGoalMetric.HIGHER_IS_BETTER,
318+
},
319+
{
320+
identifier: 'kpi-2',
321+
title: 'Customer Satisfaction',
322+
currentValue: 87,
323+
goal: 90,
324+
totalItems: 100,
325+
description: 'Customer satisfaction score',
326+
goalMetric: EGoalMetric.HIGHER_IS_BETTER,
327+
},
328+
{
329+
identifier: 'kpi-3',
330+
title: 'Response Time',
331+
currentValue: 1.5,
332+
goal: 2,
333+
totalItems: 5,
334+
description: 'Average response time in hours',
335+
goalMetric: EGoalMetric.LOWER_IS_BETTER,
336+
},
337+
];
338+
339+
const MyCompactKPIs: React.FC = () => {
340+
return (
341+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '16px' }}>
342+
{kpis.map((kpi) => (
343+
<KPICardCompact key={kpi.identifier} dataCard={kpi} />
344+
))}
345+
</div>
346+
);
347+
};
348+
```
349+
350+
### Mixing Full and Compact Cards
351+
352+
You can use both card types in the same dashboard for different purposes:
353+
354+
```tsx
355+
import * as React from 'react';
356+
import { KPICard, KPICardCompact } from '@pnp/spfx-controls-react/lib/KPIControl';
357+
import { IKpiCardData, EGoalMetric } from '@pnp/spfx-controls-react/lib/KPIControl';
358+
359+
const primaryKpi: IKpiCardData = {
360+
identifier: 'primary-kpi',
361+
title: 'Revenue',
362+
currentValue: 125000,
363+
goal: 150000,
364+
totalItems: 200000,
365+
description: 'Primary KPI with full details',
366+
goalMetric: EGoalMetric.HIGHER_IS_BETTER,
367+
};
368+
369+
const secondaryKpis: IKpiCardData[] = [
370+
// ... secondary KPIs
371+
];
372+
373+
const MixedDashboard: React.FC = () => {
374+
return (
375+
<div>
376+
{/* Primary KPI with full details */}
377+
<KPICard dataCard={primaryKpi} />
378+
379+
{/* Secondary KPIs in compact format */}
380+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px', marginTop: '16px' }}>
381+
{secondaryKpis.map((kpi) => (
382+
<KPICardCompact key={kpi.identifier} dataCard={kpi} />
383+
))}
384+
</div>
385+
</div>
386+
);
387+
};
388+
```
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
Card,
3+
CardHeader,
4+
ProgressBar,
5+
tokens,
6+
Tooltip,
7+
Text,
8+
Badge,
9+
InfoLabel,
10+
} from '@fluentui/react-components';
11+
import * as React from 'react';
12+
import Stack from './stack/Stack';
13+
14+
import { useKpiStyles } from './useKpiStyles';
15+
import {
16+
AlertFilled,
17+
CheckmarkCircleRegular,
18+
} from '@fluentui/react-icons';
19+
import { IKpiCardProps } from './IKpiCardProps';
20+
import { EGoalMetric } from './IKpiCardData';
21+
import strings from 'ControlStrings';
22+
23+
export const KPICardCompact: React.FunctionComponent<IKpiCardProps> = (
24+
props: React.PropsWithChildren<IKpiCardProps>,
25+
) => {
26+
const { dataCard } = props;
27+
const styles = useKpiStyles();
28+
29+
// Determine if KPI is on track based on goal metric type
30+
const isOnTrack = React.useMemo(
31+
() =>
32+
dataCard.goalMetric === EGoalMetric.LOWER_IS_BETTER
33+
? dataCard.currentValue <= dataCard.goal // Lower is better: on track when current <= goal
34+
: dataCard.currentValue >= dataCard.goal, // Higher is better: on track when current >= goal
35+
[dataCard.currentValue, dataCard.goal, dataCard.goalMetric],
36+
);
37+
38+
const progressColor = React.useMemo(
39+
() => (isOnTrack ? 'success' : 'error'),
40+
[isOnTrack],
41+
);
42+
43+
// Success / Danger foregrounds (icon + badge text + bar fill)
44+
const accentFg = React.useMemo(
45+
() =>
46+
isOnTrack
47+
? tokens.colorPaletteLightGreenForeground2
48+
: tokens.colorPaletteRedForeground2,
49+
[isOnTrack],
50+
);
51+
52+
// Success / Danger backgrounds (badge pill bg)
53+
54+
// Success / Danger borders (badge pill border)
55+
56+
57+
return (
58+
<>
59+
<Card className={styles.card} style={{ height: 'fit-content' }}>
60+
{/* Glow blob effect - green for on track, red for exceeds goal */}
61+
<div
62+
className={isOnTrack ? styles.glowBlobSuccess : styles.glowBlobError}
63+
/>
64+
<Stack gap="0px" padding="m">
65+
<CardHeader
66+
header={
67+
<Stack direction="vertical" gap="2px">
68+
<Stack direction="horizontal" alignItems="center" gap="8px">
69+
<InfoLabel
70+
info={
71+
<>
72+
<Text size={300} color="neutralSecondary">
73+
{dataCard.description || strings.KPINoDescription}
74+
</Text>
75+
</>
76+
}
77+
>
78+
<Text weight="bold" size={300}>
79+
{dataCard.title?.toUpperCase() || strings.KPIDEfaultTitle}
80+
</Text>
81+
</InfoLabel>
82+
</Stack>
83+
<Text
84+
size={200}
85+
style={{
86+
color: tokens.colorNeutralForeground3,
87+
fontStyle: 'italic',
88+
}}
89+
>
90+
{dataCard.goalMetric === EGoalMetric.LOWER_IS_BETTER
91+
? strings.KPILowerIsBetter
92+
: strings.KPIHigherIsBetter}
93+
</Text>
94+
</Stack>
95+
}
96+
action={
97+
<Tooltip
98+
content={
99+
isOnTrack
100+
? `✓ ${strings.KPIWithinGoalThreshold}`
101+
: `⚠ ${strings.KPIExceedsGoalTreshhold}`
102+
}
103+
relationship="inaccessible"
104+
>
105+
<Badge
106+
className={styles.headerActionBadge}
107+
appearance="ghost"
108+
size="small"
109+
icon={
110+
isOnTrack ? (
111+
<CheckmarkCircleRegular
112+
style={{ color: accentFg, fontSize: '22px' }}
113+
/>
114+
) : (
115+
<AlertFilled
116+
style={{ color: accentFg, fontSize: '22px' }}
117+
/>
118+
)
119+
}
120+
/>
121+
</Tooltip>
122+
}
123+
/>
124+
<Stack gap="s" direction="horizontal" alignItems="baseline">
125+
<Text weight="bold" size={900}>
126+
{dataCard.currentValue} <br />
127+
</Text>
128+
<Text weight="semibold" size={300} color="neutralSecondary">
129+
/ {dataCard.goal} {strings.KPIGoal}
130+
</Text>
131+
</Stack>
132+
<Stack gap="s">
133+
<Stack direction="horizontal" justifyContent="space-between">
134+
<Text size={300} color="neutralSecondary">
135+
{strings.KPIProgressGoal}
136+
</Text>
137+
<Text size={300} color="neutralSecondary" weight="bold">
138+
{((dataCard.currentValue / dataCard.goal) * 100).toFixed(2)}%
139+
</Text>
140+
</Stack>
141+
<ProgressBar
142+
value={dataCard.currentValue / dataCard.goal}
143+
color={progressColor}
144+
style={{ height: '3px' }}
145+
shape="rounded"
146+
/>
147+
</Stack>
148+
</Stack>
149+
</Card>
150+
</>
151+
);
152+
};

0 commit comments

Comments
 (0)