@spteck/fluentui-react-charts 0.1.8 → 1.0.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.
- package/dist/components/RenderLabel/RenderLabel.d.ts +8 -0
- package/dist/components/RenderLabel/index.d.ts +2 -0
- package/dist/components/RenderLabel/useRenderLabelStylesStyles.d.ts +1 -0
- package/dist/components/dashboard/DashBoard.d.ts +4 -0
- package/dist/components/dashboard/ExampleDashboardUsage.d.ts +6 -0
- package/dist/components/dashboard/ICardChartContainer.d.ts +16 -0
- package/dist/components/dashboard/IDashboardProps.d.ts +9 -0
- package/dist/components/dashboard/NoDashboards.d.ts +5 -0
- package/dist/components/dashboard/index.d.ts +3 -0
- package/dist/components/dashboard/selectZoom/SelectZoom.d.ts +15 -0
- package/dist/components/dashboard/useDashboardStyles.d.ts +7 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/svgImages/BusinessReportIcon.d.ts +9 -0
- package/dist/fluentui-react-charts.cjs.development.js +2 -0
- package/dist/fluentui-react-charts.cjs.development.js.map +1 -1
- package/dist/fluentui-react-charts.cjs.production.min.js +1 -1
- package/dist/fluentui-react-charts.cjs.production.min.js.map +1 -1
- package/dist/fluentui-react-charts.esm.js +2 -0
- package/dist/fluentui-react-charts.esm.js.map +1 -1
- package/dist/models/ChartDatum.d.ts +4 -0
- package/dist/models/index.d.ts +1 -0
- package/package.json +2 -3
- package/src/components/RenderLabel/RenderLabel.tsx +39 -0
- package/src/components/RenderLabel/index.ts +2 -0
- package/src/components/RenderLabel/useRenderLabelStylesStyles.ts +25 -0
- package/src/components/dashboard/DashBoard.tsx +220 -0
- package/src/components/dashboard/ExampleDashboardUsage.tsx +114 -0
- package/src/components/dashboard/ICardChartContainer.tsx +13 -0
- package/src/components/dashboard/IDashboardProps.tsx +13 -0
- package/src/components/dashboard/NoDashboards.tsx +26 -0
- package/src/components/dashboard/index.ts +4 -0
- package/src/components/dashboard/selectZoom/SelectZoom.tsx +189 -0
- package/src/components/dashboard/useDashboardStyles.ts +76 -0
- package/src/components/index.ts +3 -1
- package/src/components/svgImages/BusinessReportIcon.tsx +218 -0
- package/src/models/ChartDatum.ts +4 -0
- package/src/models/index.ts +1 -0
- package/dist/components/DashBoard.d.ts +0 -3
- package/src/components/DashBoard.tsx +0 -409
package/dist/models/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "1.0.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"typings": "dist/index.d.ts",
|
|
@@ -38,11 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"name": "@spteck/fluentui-react-charts",
|
|
40
40
|
"author": "João Mendes",
|
|
41
|
-
|
|
42
41
|
"module": "dist/fluentui-react-charts.esm.js",
|
|
43
42
|
"devDependencies": {
|
|
44
43
|
"@size-limit/preset-small-lib": "^11.2.0",
|
|
45
|
-
|
|
46
44
|
"@types/react": "^18.3.23",
|
|
47
45
|
"@types/react-dom": "^18.3.7",
|
|
48
46
|
"husky": "^9.1.7",
|
|
@@ -54,6 +52,7 @@
|
|
|
54
52
|
"dependencies": {
|
|
55
53
|
"@emotion/css": "^11.13.5",
|
|
56
54
|
"@fluentui/react-components": "^9.66.2",
|
|
55
|
+
"@iconify/react": "^6.0.0",
|
|
57
56
|
"@juggle/resize-observer": "^3.4.0",
|
|
58
57
|
"chart.js": "^4.5.0",
|
|
59
58
|
"chartjs-plugin-datalabels": "^2.2.0",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Caption1,
|
|
5
|
+
tokens,
|
|
6
|
+
} from '@fluentui/react-components';
|
|
7
|
+
import { Icon } from '@iconify/react';
|
|
8
|
+
|
|
9
|
+
import { useRenderLabelStyles } from './useRenderLabelStylesStyles';
|
|
10
|
+
|
|
11
|
+
export interface IRenderLabelProps {
|
|
12
|
+
label: string; icon?: string | JSX.Element; isRequired?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const RenderLabel: React.FunctionComponent<IRenderLabelProps> = (props: React.PropsWithChildren<IRenderLabelProps>) => {
|
|
16
|
+
const { label, icon, isRequired } = props;
|
|
17
|
+
const styles = useRenderLabelStyles();
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<div className={styles.labelContainer}>
|
|
21
|
+
{icon && React.isValidElement(icon) ? (
|
|
22
|
+
icon
|
|
23
|
+
) : (
|
|
24
|
+
<Icon
|
|
25
|
+
icon={icon as string}
|
|
26
|
+
className={styles.iconStyles}
|
|
27
|
+
width={"20px"}
|
|
28
|
+
height={"20px"}
|
|
29
|
+
color={tokens.colorBrandForeground1}
|
|
30
|
+
/>
|
|
31
|
+
)}
|
|
32
|
+
<Caption1 style={{ color: tokens.colorBrandForeground1 }}>{label}</Caption1>
|
|
33
|
+
<Caption1 style={{ color: tokens.colorPaletteRedForeground1 }}>{isRequired ? " *" : ""}</Caption1>
|
|
34
|
+
</div>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default RenderLabel;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import {
|
|
4
|
+
makeStyles,
|
|
5
|
+
shorthands,
|
|
6
|
+
} from '@fluentui/react-components';
|
|
7
|
+
|
|
8
|
+
export const useRenderLabelStyles = makeStyles({
|
|
9
|
+
|
|
10
|
+
labelContainer: {
|
|
11
|
+
display: "flex",
|
|
12
|
+
flexDirection: "row",
|
|
13
|
+
justifyContent: "flex-start",
|
|
14
|
+
alignItems: "center",
|
|
15
|
+
...shorthands.gap("6px"),
|
|
16
|
+
},
|
|
17
|
+
iconStyles: {
|
|
18
|
+
width: "26px",
|
|
19
|
+
},
|
|
20
|
+
item: {
|
|
21
|
+
paddingLeft: "15px",
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Card, CardHeader, Text, Theme } from '@fluentui/react-components';
|
|
4
|
+
import React, {
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react';
|
|
11
|
+
|
|
12
|
+
import { ICardChartContainer } from './ICardChartContainer';
|
|
13
|
+
import { IDashboardProps } from './IDashboardProps';
|
|
14
|
+
import { NoDashboards } from './NoDashboards';
|
|
15
|
+
import { SelectZoom } from './selectZoom/SelectZoom';
|
|
16
|
+
import { useDashboardStyles } from './useDashboardStyles';
|
|
17
|
+
import { useGraphUtils } from '../../hooks';
|
|
18
|
+
|
|
19
|
+
const MINIMUM_DASHBOARD_WIDTH = 600;
|
|
20
|
+
const MAX_ROWS = 4;
|
|
21
|
+
|
|
22
|
+
const Dashboard: React.FC<IDashboardProps> = ({
|
|
23
|
+
cardCharts,
|
|
24
|
+
theme,
|
|
25
|
+
containerWidth ,
|
|
26
|
+
containerHeight = '100%',
|
|
27
|
+
maxSpanRows = MAX_ROWS,
|
|
28
|
+
}) => {
|
|
29
|
+
const styles = useDashboardStyles();
|
|
30
|
+
|
|
31
|
+
const { getChartComponent } = useGraphUtils(theme as Theme);
|
|
32
|
+
|
|
33
|
+
const [CardChartContainer, setCardChartContainer] = useState<
|
|
34
|
+
ICardChartContainer[]
|
|
35
|
+
>([]);
|
|
36
|
+
|
|
37
|
+
const [sizes, setSizes] = useState<
|
|
38
|
+
Record<string, { spanCols: number; spanRows: number }>
|
|
39
|
+
>({});
|
|
40
|
+
const dragItem = useRef<number | null>(null);
|
|
41
|
+
const dragOverItem = useRef<number | null>(null);
|
|
42
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
setCardChartContainer(cardCharts);
|
|
46
|
+
setSizes(() => {
|
|
47
|
+
const initialSizes: Record<
|
|
48
|
+
string,
|
|
49
|
+
{ spanCols: number; spanRows: number }
|
|
50
|
+
> = {};
|
|
51
|
+
cardCharts.forEach(c => {
|
|
52
|
+
initialSizes[c.id] = {
|
|
53
|
+
spanCols: c.defaultSpan?.spanCols ?? 1,
|
|
54
|
+
spanRows: c.defaultSpan?.spanRows ?? 1,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
return initialSizes;
|
|
58
|
+
});
|
|
59
|
+
}, [cardCharts]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (containerWidth <= MINIMUM_DASHBOARD_WIDTH) {
|
|
63
|
+
setSizes(() => {
|
|
64
|
+
const reset: Record<
|
|
65
|
+
string,
|
|
66
|
+
{ spanCols: number; spanRows: number }
|
|
67
|
+
> = {};
|
|
68
|
+
CardChartContainer.forEach(c => {
|
|
69
|
+
reset[c.id] = { spanCols: 1, spanRows: 1 };
|
|
70
|
+
});
|
|
71
|
+
return reset;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}, [containerWidth, CardChartContainer]);
|
|
75
|
+
|
|
76
|
+
const showZoom = useMemo(() => containerWidth > MINIMUM_DASHBOARD_WIDTH, [
|
|
77
|
+
containerWidth,
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const { minCardWidth, gridGap, containerPadding } = useMemo(
|
|
81
|
+
() => ({
|
|
82
|
+
minCardWidth: 350,
|
|
83
|
+
gridGap: 16,
|
|
84
|
+
containerPadding: 20,
|
|
85
|
+
}),
|
|
86
|
+
[]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const innerWidth = containerWidth - containerPadding * 2;
|
|
90
|
+
const columns = useMemo(
|
|
91
|
+
() => Math.floor((innerWidth + gridGap) / (minCardWidth + gridGap)),
|
|
92
|
+
[innerWidth, gridGap, minCardWidth]
|
|
93
|
+
);
|
|
94
|
+
const maxZoom = Math.max(columns, 1);
|
|
95
|
+
|
|
96
|
+
const handleSort = useCallback(() => {
|
|
97
|
+
if (
|
|
98
|
+
dragItem.current !== null &&
|
|
99
|
+
dragOverItem.current !== null &&
|
|
100
|
+
dragItem.current !== dragOverItem.current
|
|
101
|
+
) {
|
|
102
|
+
const copy = [...CardChartContainer];
|
|
103
|
+
const [moved] = copy.splice(dragItem.current, 1);
|
|
104
|
+
copy.splice(dragOverItem.current, 0, moved);
|
|
105
|
+
setCardChartContainer(copy);
|
|
106
|
+
}
|
|
107
|
+
dragItem.current = null;
|
|
108
|
+
dragOverItem.current = null;
|
|
109
|
+
}, [CardChartContainer]);
|
|
110
|
+
|
|
111
|
+
const handleZoomSelect = useCallback(
|
|
112
|
+
(id: string, span: { spanCols: number; spanRows: number }) => {
|
|
113
|
+
setSizes(prev => ({
|
|
114
|
+
...prev,
|
|
115
|
+
[id]: {
|
|
116
|
+
spanCols: Math.min(Math.max(span.spanCols, 1), maxZoom),
|
|
117
|
+
spanRows: Math.min(Math.max(span.spanRows, 1), maxSpanRows),
|
|
118
|
+
},
|
|
119
|
+
}));
|
|
120
|
+
},
|
|
121
|
+
[maxZoom, maxSpanRows]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const defaultColsAndRowSpanBasedOnNumberColumns = useMemo(() => {
|
|
125
|
+
return CardChartContainer.reduce((acc, card) => {
|
|
126
|
+
acc[card.id] = {
|
|
127
|
+
spanCols: Math.min(card.defaultSpan?.spanCols ?? 1, columns),
|
|
128
|
+
spanRows: card.defaultSpan?.spanRows ?? 1,
|
|
129
|
+
};
|
|
130
|
+
return acc;
|
|
131
|
+
}, {} as Record<string, { spanCols: number; spanRows: number }>);
|
|
132
|
+
}, [CardChartContainer, columns]);
|
|
133
|
+
|
|
134
|
+
const renderCards = useMemo(() => {
|
|
135
|
+
return CardChartContainer.map((cardContainer, idx) => (
|
|
136
|
+
<div
|
|
137
|
+
className={styles.cardWrapper}
|
|
138
|
+
key={cardContainer.id}
|
|
139
|
+
draggable
|
|
140
|
+
onDragStart={() => (dragItem.current = idx)}
|
|
141
|
+
onDragEnter={() => (dragOverItem.current = idx)}
|
|
142
|
+
onDragOver={e => e.preventDefault()}
|
|
143
|
+
onDragEnd={handleSort}
|
|
144
|
+
style={{
|
|
145
|
+
gridColumnEnd: sizes[cardContainer.id]?.spanCols
|
|
146
|
+
? `span ${sizes[cardContainer.id].spanCols}`
|
|
147
|
+
: `span ${defaultColsAndRowSpanBasedOnNumberColumns[
|
|
148
|
+
cardContainer.id
|
|
149
|
+
]?.spanCols || 1}`,
|
|
150
|
+
gridRowEnd: sizes[cardContainer.id]?.spanRows
|
|
151
|
+
? `span ${sizes[cardContainer.id].spanRows}`
|
|
152
|
+
: `span ${defaultColsAndRowSpanBasedOnNumberColumns[
|
|
153
|
+
cardContainer.id
|
|
154
|
+
]?.spanRows || 1}`,
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<Card className={styles.cardBody}>
|
|
158
|
+
<CardHeader
|
|
159
|
+
header={
|
|
160
|
+
<Text weight="semibold" size={400}>
|
|
161
|
+
{cardContainer.cardTitle}
|
|
162
|
+
</Text>
|
|
163
|
+
}
|
|
164
|
+
action={
|
|
165
|
+
showZoom ? (
|
|
166
|
+
<SelectZoom
|
|
167
|
+
values={
|
|
168
|
+
sizes[cardContainer.id] ||
|
|
169
|
+
defaultColsAndRowSpanBasedOnNumberColumns[
|
|
170
|
+
cardContainer.id
|
|
171
|
+
] || { spanCols: 1, spanRows: 1 }
|
|
172
|
+
}
|
|
173
|
+
maxCols={maxZoom}
|
|
174
|
+
maxRows={maxSpanRows}
|
|
175
|
+
IsOpen={false}
|
|
176
|
+
onChange={v => handleZoomSelect(cardContainer.id, v)}
|
|
177
|
+
/>
|
|
178
|
+
) : (
|
|
179
|
+
undefined
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
/>
|
|
183
|
+
<div
|
|
184
|
+
className={styles.chartContainer}
|
|
185
|
+
style={{ height: containerHeight }}
|
|
186
|
+
>
|
|
187
|
+
{theme &&
|
|
188
|
+
theme.fontSizeBase100 &&
|
|
189
|
+
getChartComponent(cardContainer.chart, theme as Theme)}
|
|
190
|
+
</div>
|
|
191
|
+
</Card>
|
|
192
|
+
</div>
|
|
193
|
+
));
|
|
194
|
+
}, [
|
|
195
|
+
CardChartContainer,
|
|
196
|
+
handleSort,
|
|
197
|
+
sizes,
|
|
198
|
+
styles.cardBody,
|
|
199
|
+
styles.chartContainer,
|
|
200
|
+
showZoom,
|
|
201
|
+
maxZoom,
|
|
202
|
+
getChartComponent,
|
|
203
|
+
handleZoomSelect,
|
|
204
|
+
theme,
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
if (CardChartContainer.length === 0) {
|
|
208
|
+
return <NoDashboards />;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<>
|
|
213
|
+
<div ref={containerRef} className={styles.dashboardContainer}>
|
|
214
|
+
{renderCards}
|
|
215
|
+
</div>
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export default Dashboard;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Text, Theme, webLightTheme } from '@fluentui/react-components';
|
|
2
|
+
|
|
3
|
+
import DashBoard from './DashBoard';
|
|
4
|
+
import { ICardChartContainer } from './ICardChartContainer';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { Stack } from '../stack/Stack';
|
|
7
|
+
|
|
8
|
+
// Example usage of the reusable DashBoard component
|
|
9
|
+
const ExampleDashboardUsage: React.FC<{ theme: Theme }> = ({ theme }) => {
|
|
10
|
+
// Sample data for the charts
|
|
11
|
+
const sampleData = [
|
|
12
|
+
{ month: 'Jan', sales: 10000, expenses: 8000 },
|
|
13
|
+
{ month: 'Feb', sales: 15000, expenses: 9000 },
|
|
14
|
+
{ month: 'Mar', sales: 12000, expenses: 7500 },
|
|
15
|
+
{ month: 'Apr', sales: 18000, expenses: 10000 },
|
|
16
|
+
{ month: 'May', sales: 16000, expenses: 8500 },
|
|
17
|
+
{ month: 'Jun', sales: 22000, expenses: 11000 },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Define the chart containers with different chart types
|
|
21
|
+
const cardChartContainers: ICardChartContainer[] = [
|
|
22
|
+
{
|
|
23
|
+
id: '1',
|
|
24
|
+
cardTitle: 'Monthly Sales',
|
|
25
|
+
chart: {
|
|
26
|
+
id: '1',
|
|
27
|
+
title: 'Sales Performance',
|
|
28
|
+
type: 'line',
|
|
29
|
+
data: [{
|
|
30
|
+
label: 'Sales',
|
|
31
|
+
data: sampleData.map(d => ({ name: d.month, value: d.sales, x: d.month, y: d.sales }))
|
|
32
|
+
}],
|
|
33
|
+
},
|
|
34
|
+
defaultSpan: { spanCols: 2, spanRows: 1 },
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: '2',
|
|
38
|
+
cardTitle: 'Sales vs Expenses',
|
|
39
|
+
chart: {
|
|
40
|
+
id: '2',
|
|
41
|
+
title: 'Financial Overview',
|
|
42
|
+
type: 'bar',
|
|
43
|
+
data: [
|
|
44
|
+
{
|
|
45
|
+
label: 'Sales',
|
|
46
|
+
data: sampleData.map(d => ({ name: d.month, value: d.sales, x: d.month, y: d.sales }))
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: 'Expenses',
|
|
50
|
+
data: sampleData.map(d => ({ name: d.month, value: d.expenses, x: d.month, y: d.expenses }))
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
defaultSpan: { spanCols: 2, spanRows: 2 },
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: '3',
|
|
58
|
+
cardTitle: 'Revenue Distribution',
|
|
59
|
+
chart: {
|
|
60
|
+
id: '3',
|
|
61
|
+
title: 'Q2 Revenue',
|
|
62
|
+
type: 'pie',
|
|
63
|
+
data: [{
|
|
64
|
+
label: 'Revenue',
|
|
65
|
+
data: [
|
|
66
|
+
{ name: 'Product A', value: 35000 },
|
|
67
|
+
{ name: 'Product B', value: 25000 },
|
|
68
|
+
{ name: 'Product C', value: 18000 },
|
|
69
|
+
{ name: 'Services', value: 15000 },
|
|
70
|
+
]
|
|
71
|
+
}],
|
|
72
|
+
},
|
|
73
|
+
defaultSpan: { spanCols: 1, spanRows: 1 },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: '4',
|
|
77
|
+
cardTitle: 'Growth Trend',
|
|
78
|
+
chart: {
|
|
79
|
+
id: '4',
|
|
80
|
+
title: 'Monthly Growth',
|
|
81
|
+
type: 'area',
|
|
82
|
+
data: [{
|
|
83
|
+
label: 'Growth',
|
|
84
|
+
data: sampleData.map(d => {
|
|
85
|
+
const growthPercentage = ((d.sales - d.expenses) / d.expenses * 100);
|
|
86
|
+
return {
|
|
87
|
+
name: d.month,
|
|
88
|
+
value: Number(growthPercentage.toFixed(1)),
|
|
89
|
+
x: d.month,
|
|
90
|
+
y: Number(growthPercentage.toFixed(1))
|
|
91
|
+
};
|
|
92
|
+
})
|
|
93
|
+
}],
|
|
94
|
+
},
|
|
95
|
+
defaultSpan: { spanCols: 2, spanRows: 1 },
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Stack direction="vertical" style={{ width: '100%', height: '100vh' }}>
|
|
101
|
+
<Stack padding="l">
|
|
102
|
+
<Text as="h1" size={900} weight="bold">Sample Dashboard</Text>
|
|
103
|
+
</Stack>
|
|
104
|
+
<DashBoard
|
|
105
|
+
cardCharts={cardChartContainers}
|
|
106
|
+
theme={theme ?? webLightTheme}
|
|
107
|
+
containerWidth={window.innerWidth}
|
|
108
|
+
containerHeight={window.innerHeight - 100} // Adjust height for header
|
|
109
|
+
/>
|
|
110
|
+
</Stack>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default ExampleDashboardUsage;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChartDatum } from '../../models/ChartDatum';
|
|
4
|
+
import { IChart } from '../../models';
|
|
5
|
+
|
|
6
|
+
export interface ICardChartContainer {
|
|
7
|
+
chart: IChart;
|
|
8
|
+
cardTitle: string;
|
|
9
|
+
showZoom?: boolean;
|
|
10
|
+
id: string;
|
|
11
|
+
data?: { label: string; data: ChartDatum[]; }[];
|
|
12
|
+
defaultSpan?: { spanCols: number; spanRows: number; };
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ICardChartContainer } from './ICardChartContainer';
|
|
4
|
+
import { Theme } from '@fluentui/react-components';
|
|
5
|
+
|
|
6
|
+
export interface IDashboardProps {
|
|
7
|
+
cardCharts: ICardChartContainer[];
|
|
8
|
+
theme: Theme;
|
|
9
|
+
containerWidth: number;
|
|
10
|
+
containerHeight?: number;
|
|
11
|
+
|
|
12
|
+
maxSpanRows?: number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import BusinessReportIcon from '../svgImages/BusinessReportIcon';
|
|
4
|
+
import Stack from '../stack/Stack';
|
|
5
|
+
import { Text } from '@fluentui/react-components';
|
|
6
|
+
|
|
7
|
+
export interface INoDashboardsProps {
|
|
8
|
+
height?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const NoDashboards: React.FunctionComponent<INoDashboardsProps> = (props: React.PropsWithChildren<INoDashboardsProps>) => {
|
|
12
|
+
const { height } = props;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<Stack
|
|
17
|
+
style={{ height: height || "100%" }}
|
|
18
|
+
justifyContent="Center"
|
|
19
|
+
alignItems="Center"
|
|
20
|
+
>
|
|
21
|
+
<BusinessReportIcon width={200} height={200} />
|
|
22
|
+
<Text size={500} weight='semibold'>No Dashboards Available</Text>
|
|
23
|
+
</Stack>
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Caption1,
|
|
5
|
+
Menu,
|
|
6
|
+
MenuButton,
|
|
7
|
+
MenuPopover,
|
|
8
|
+
MenuProps,
|
|
9
|
+
MenuTrigger,
|
|
10
|
+
Tooltip,
|
|
11
|
+
tokens,
|
|
12
|
+
} from "@fluentui/react-components";
|
|
13
|
+
import {
|
|
14
|
+
Settings20Filled,
|
|
15
|
+
Settings20Regular,
|
|
16
|
+
bundleIcon,
|
|
17
|
+
} from "@fluentui/react-icons";
|
|
18
|
+
|
|
19
|
+
import { Icon } from "@iconify/react";
|
|
20
|
+
import { RenderLabel } from "../../RenderLabel";
|
|
21
|
+
import { css } from "@emotion/css";
|
|
22
|
+
|
|
23
|
+
export interface ISelectZoomProps {
|
|
24
|
+
IsOpen: boolean;
|
|
25
|
+
onChange?: (value: { spanCols: number; spanRows: number }) => void;
|
|
26
|
+
values: { spanCols: number; spanRows: number };
|
|
27
|
+
maxCols: number;
|
|
28
|
+
maxRows: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const useStyles = (): {
|
|
32
|
+
gridContainer: string;
|
|
33
|
+
cell: string;
|
|
34
|
+
hoveredCell: string;
|
|
35
|
+
selectedCell: string;
|
|
36
|
+
menuPopover: string;
|
|
37
|
+
bottomText: string;
|
|
38
|
+
} => ({
|
|
39
|
+
gridContainer: css`
|
|
40
|
+
display: grid;
|
|
41
|
+
gap: 4px;
|
|
42
|
+
padding: 8px;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
`,
|
|
45
|
+
cell: css`
|
|
46
|
+
width: 30px;
|
|
47
|
+
height: 30px;
|
|
48
|
+
border: 1px solid ${tokens.colorNeutralStroke1};
|
|
49
|
+
background-color: ${tokens.colorNeutralBackground1};
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
transition: background-color 150ms ease, transform 150ms ease;
|
|
52
|
+
will-change: background-color, transform;
|
|
53
|
+
`,
|
|
54
|
+
hoveredCell: css`
|
|
55
|
+
background-color: ${tokens.colorNeutralBackground1Hover};
|
|
56
|
+
transform: scale(1.05);
|
|
57
|
+
`,
|
|
58
|
+
selectedCell: css`
|
|
59
|
+
background-color: ${tokens.colorNeutralBackground1Selected};
|
|
60
|
+
`,
|
|
61
|
+
menuPopover: css`
|
|
62
|
+
min-width: fit-content;
|
|
63
|
+
`,
|
|
64
|
+
bottomText: css`
|
|
65
|
+
padding-left: 8px;
|
|
66
|
+
padding-right: 8px;
|
|
67
|
+
`,
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const SelectZoom: React.FunctionComponent<ISelectZoomProps> = (
|
|
73
|
+
props: React.PropsWithChildren<ISelectZoomProps>
|
|
74
|
+
) => {
|
|
75
|
+
const { onChange, values: defaultValues, maxCols, maxRows } = props;
|
|
76
|
+
const Settings = bundleIcon(Settings20Filled, Settings20Regular);
|
|
77
|
+
const styles = useStyles();
|
|
78
|
+
|
|
79
|
+
const [values, setValues] = React.useState(defaultValues);
|
|
80
|
+
const [hovered, setHovered] = React.useState<{
|
|
81
|
+
spanCols: number;
|
|
82
|
+
spanRows: number;
|
|
83
|
+
} | null>(null);
|
|
84
|
+
const [open, setOpen] = React.useState(false);
|
|
85
|
+
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
setValues(defaultValues);
|
|
88
|
+
}, [defaultValues]);
|
|
89
|
+
|
|
90
|
+
const onOpenChange: MenuProps["onOpenChange"] = (_, data) => {
|
|
91
|
+
setOpen(data.open);
|
|
92
|
+
setHovered(null);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleCellClick = (row: number, col: number):void => {
|
|
96
|
+
const newValues = { spanCols: col + 1, spanRows: row + 1 };
|
|
97
|
+
setValues(newValues);
|
|
98
|
+
onChange?.(newValues);
|
|
99
|
+
setOpen(false);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleCellHover = (row: number, col: number):void => {
|
|
103
|
+
setHovered({ spanCols: col + 1, spanRows: row + 1 });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleMouseLeave = ():void => {
|
|
107
|
+
setHovered(null);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const renderGridCells = (): React.ReactNode[] => {
|
|
111
|
+
const cells: React.ReactNode[] = [];
|
|
112
|
+
for (let row = 0; row < maxRows; row++) {
|
|
113
|
+
for (let col = 0; col < maxCols; col++) {
|
|
114
|
+
const isSelected = row < values.spanRows && col < values.spanCols;
|
|
115
|
+
const isHovered =
|
|
116
|
+
hovered && row < hovered.spanRows && col < hovered.spanCols;
|
|
117
|
+
cells.push(
|
|
118
|
+
<div
|
|
119
|
+
key={`${row}-${col}`}
|
|
120
|
+
className={`${styles.cell} ${isHovered ? styles.hoveredCell : ""} ${
|
|
121
|
+
isSelected ? styles.selectedCell : ""
|
|
122
|
+
}`}
|
|
123
|
+
onMouseEnter={() => handleCellHover(row, col)}
|
|
124
|
+
onClick={() => handleCellClick(row, col)}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return cells;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Compute popover width dynamically
|
|
133
|
+
const popoverWidth = React.useMemo(() => (30 * maxCols) + (4 * (maxCols - 1)) + 32, [maxCols]); // 30px for each cell, 4px gap, and 32px padding (16 left and right)
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Menu open={open} onOpenChange={onOpenChange}>
|
|
137
|
+
<MenuTrigger disableButtonEnhancement>
|
|
138
|
+
<Tooltip content="Configure" relationship="label">
|
|
139
|
+
<MenuButton
|
|
140
|
+
icon={<Settings />}
|
|
141
|
+
size="small"
|
|
142
|
+
appearance="transparent"
|
|
143
|
+
/>
|
|
144
|
+
</Tooltip>
|
|
145
|
+
</MenuTrigger>
|
|
146
|
+
|
|
147
|
+
<MenuPopover
|
|
148
|
+
style={{ width: `${popoverWidth}px`, minWidth: "120px", padding: 8 }}
|
|
149
|
+
>
|
|
150
|
+
<div
|
|
151
|
+
style={{
|
|
152
|
+
display: "flex",
|
|
153
|
+
flexDirection: "row",
|
|
154
|
+
alignItems: "center",
|
|
155
|
+
gap: "6px",
|
|
156
|
+
width: "100%",
|
|
157
|
+
boxSizing: "border-box",
|
|
158
|
+
padding: "8px",
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<RenderLabel
|
|
162
|
+
label={`Selected Span (${values.spanCols} × ${values.spanRows})`}
|
|
163
|
+
icon={
|
|
164
|
+
<Icon
|
|
165
|
+
icon="fluent:number-row-20-regular"
|
|
166
|
+
width="32"
|
|
167
|
+
height="32"
|
|
168
|
+
/>
|
|
169
|
+
}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Grid */}
|
|
174
|
+
<div
|
|
175
|
+
className={styles.gridContainer}
|
|
176
|
+
style={{
|
|
177
|
+
gridTemplateColumns: `repeat(${maxCols}, 30px)`,
|
|
178
|
+
gridTemplateRows: `repeat(${maxRows}, 30px)`,
|
|
179
|
+
}}
|
|
180
|
+
onMouseLeave={handleMouseLeave}
|
|
181
|
+
>
|
|
182
|
+
{renderGridCells()}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<Caption1 className={styles.bottomText}>Click to set span</Caption1>
|
|
186
|
+
</MenuPopover>
|
|
187
|
+
</Menu>
|
|
188
|
+
);
|
|
189
|
+
};
|