@platformatic/ui-components 0.17.0 → 0.17.2
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/assets/{index-CqZKPFqq.js → index-BeInQDZN.js} +1 -1
- package/dist/assets/{index-BgaMsXXo.css → index-DE70C3KG.css} +1 -1
- package/dist/index.html +2 -2
- package/dist/main.css +60 -0
- package/package.json +2 -1
- package/src/components/ArcMetric.jsx +145 -0
- package/src/components/ArcMetric.module.css +31 -0
- package/src/components/MetricInfoBox.jsx +50 -0
- package/src/components/MetricInfoBox.module.css +20 -0
- package/src/components/Tooltip.jsx +1 -1
- package/src/components/TrendLine.jsx +146 -0
- package/src/components/TrendMetric.jsx +37 -0
- package/src/components/TrendMetric.module.css +25 -0
- package/src/components/icons/TrendDownIcon.jsx +41 -0
- package/src/components/icons/TrendUpIcon.jsx +43 -0
- package/src/components/icons/index.js +4 -0
- package/src/stories/ArcMetric.stories.jsx +17 -0
- package/src/stories/MetricInfoBox.stories.jsx +49 -0
- package/src/stories/TrendLine.stories.jsx +20 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useState } from 'react'
|
|
2
|
+
import * as d3 from 'd3'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TrendLine component
|
|
6
|
+
* @param {Object} props
|
|
7
|
+
* @param {number[]} props.yValues - Array of y-values (equally spaced on x-axis)
|
|
8
|
+
* @param {number} [props.width=400] - Width of the SVG
|
|
9
|
+
* @param {number} [props.height=120] - Height of the SVG
|
|
10
|
+
*/
|
|
11
|
+
function TrendLine ({ yValues }) {
|
|
12
|
+
const ref = useRef(null)
|
|
13
|
+
const containerRef = useRef(null)
|
|
14
|
+
/** @type {React.RefObject<HTMLDivElement>} */
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
const typedContainerRef = containerRef
|
|
17
|
+
const [dimensions, setDimensions] = useState({ width: 400, height: 120 })
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!typedContainerRef.current) return
|
|
21
|
+
// Initial set
|
|
22
|
+
const bounding = typedContainerRef.current.getBoundingClientRect()
|
|
23
|
+
if (bounding) {
|
|
24
|
+
setDimensions({ width: bounding.width, height: bounding.height })
|
|
25
|
+
}
|
|
26
|
+
// Resize observer
|
|
27
|
+
const resizeObserver = new window.ResizeObserver(entries => {
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.target === typedContainerRef.current) {
|
|
30
|
+
const { width, height } = entry.contentRect
|
|
31
|
+
setDimensions({ width, height })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
resizeObserver.observe(typedContainerRef.current)
|
|
36
|
+
return () => resizeObserver.disconnect()
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const { width, height } = dimensions
|
|
41
|
+
if (!yValues || yValues.length === 0) return
|
|
42
|
+
// Limit to max 5 points by downsampling if needed
|
|
43
|
+
let limitedYValues = yValues
|
|
44
|
+
if (yValues.length > 5) {
|
|
45
|
+
const step = (yValues.length - 1) / 4
|
|
46
|
+
limitedYValues = Array.from({ length: 5 }, (_, i) => yValues[Math.round(i * step)])
|
|
47
|
+
}
|
|
48
|
+
const svg = d3.select(ref.current)
|
|
49
|
+
svg.selectAll('*').remove() // Clear previous
|
|
50
|
+
|
|
51
|
+
// Margins for padding
|
|
52
|
+
// const margin = { top: 10, right: 10, bottom: 10, left: 10 }
|
|
53
|
+
const margin = { top: 0, right: 0, bottom: 0, left: 0 }
|
|
54
|
+
const w = width - margin.left - margin.right
|
|
55
|
+
const h = height - margin.top - margin.bottom
|
|
56
|
+
|
|
57
|
+
// Ensure yValues is a number[] and filter out undefined/null
|
|
58
|
+
const cleanYValues = limitedYValues.filter(v => typeof v === 'number' && !isNaN(v))
|
|
59
|
+
if (cleanYValues.length === 0) return
|
|
60
|
+
|
|
61
|
+
// Helper to ensure [number, number][]
|
|
62
|
+
function toXYPairs (arr) {
|
|
63
|
+
const out = []
|
|
64
|
+
for (let i = 0; i < arr.length; i++) {
|
|
65
|
+
const x = i
|
|
66
|
+
const y = arr[i]
|
|
67
|
+
if (typeof x === 'number' && typeof y === 'number' && Number.isFinite(x) && Number.isFinite(y)) {
|
|
68
|
+
out.push([x, y])
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
const points = toXYPairs(cleanYValues)
|
|
74
|
+
if (points.length < 2) return
|
|
75
|
+
|
|
76
|
+
// X and Y scales
|
|
77
|
+
const x = d3.scaleLinear()
|
|
78
|
+
.domain([0, points.length - 1])
|
|
79
|
+
.range([0, w])
|
|
80
|
+
const y = d3.scaleLinear()
|
|
81
|
+
.domain([
|
|
82
|
+
Number(d3.max(cleanYValues) ?? 1),
|
|
83
|
+
Number(d3.min(cleanYValues) ?? 0)
|
|
84
|
+
]) // Invert so higher values are up
|
|
85
|
+
.range([margin.top, h + margin.top])
|
|
86
|
+
|
|
87
|
+
// Line generator for [x, y] pairs
|
|
88
|
+
const line = d3.line()
|
|
89
|
+
.x(d => x(d[0]) + margin.left)
|
|
90
|
+
.y(d => y(d[1]))
|
|
91
|
+
.curve(d3.curveCatmullRom.alpha(0.5))
|
|
92
|
+
|
|
93
|
+
// Area generator for [x, y] pairs
|
|
94
|
+
const area = d3.area()
|
|
95
|
+
.x(d => x(d[0]) + margin.left)
|
|
96
|
+
.y0(h + margin.top)
|
|
97
|
+
.y1(d => y(d[1]))
|
|
98
|
+
.curve(d3.curveCatmullRom.alpha(0.5))
|
|
99
|
+
|
|
100
|
+
// Draw area (shadow/fill)
|
|
101
|
+
svg.append('path')
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
.attr('d', area(points))
|
|
104
|
+
.attr('fill', 'url(#trend-gradient)')
|
|
105
|
+
.attr('opacity', 1)
|
|
106
|
+
|
|
107
|
+
// Draw line
|
|
108
|
+
svg.append('path')
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
.attr('d', line(points))
|
|
111
|
+
.attr('fill', 'none')
|
|
112
|
+
.attr('stroke', '#BFC3C7')
|
|
113
|
+
.attr('stroke-width', 2)
|
|
114
|
+
.attr('stroke-linecap', 'round')
|
|
115
|
+
|
|
116
|
+
// Gradient for area
|
|
117
|
+
const defs = svg.append('defs')
|
|
118
|
+
const gradient = defs.append('linearGradient')
|
|
119
|
+
.attr('id', 'trend-gradient')
|
|
120
|
+
.attr('x1', '0%')
|
|
121
|
+
.attr('y1', '0%')
|
|
122
|
+
.attr('x2', '0%')
|
|
123
|
+
.attr('y2', '100%')
|
|
124
|
+
gradient.append('stop')
|
|
125
|
+
.attr('offset', '0%')
|
|
126
|
+
.attr('stop-color', '#BFC3C7')
|
|
127
|
+
.attr('stop-opacity', 0.12)
|
|
128
|
+
gradient.append('stop')
|
|
129
|
+
.attr('offset', '100%')
|
|
130
|
+
.attr('stop-color', '#BFC3C7')
|
|
131
|
+
.attr('stop-opacity', 0)
|
|
132
|
+
}, [yValues, dimensions])
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className='w-full h-full' ref={typedContainerRef} style={{ position: 'relative' }}>
|
|
136
|
+
<svg
|
|
137
|
+
ref={ref}
|
|
138
|
+
width='100%'
|
|
139
|
+
height='100%'
|
|
140
|
+
style={{ display: 'block', background: '#0B1016', borderRadius: 6 }}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default TrendLine
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import styles from './TrendMetric.module.css'
|
|
3
|
+
import TrendUpIcon from './icons/TrendUpIcon'
|
|
4
|
+
import TrendDownIcon from './icons/TrendDownIcon'
|
|
5
|
+
import TrendLine from './TrendLine'
|
|
6
|
+
|
|
7
|
+
export default function TrendMetric ({ value, unit, helper, showGraph = false, data }) {
|
|
8
|
+
// set trend variable up or down, depending on the first and last value of the data array
|
|
9
|
+
const trend = data[0] > data[data.length - 1] ? 'up' : 'down'
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className={styles.container}>
|
|
13
|
+
<div className={styles.contentText}>
|
|
14
|
+
<div>
|
|
15
|
+
<div className={styles.value}>
|
|
16
|
+
<span className={styles.valueNumber}>{value}</span>
|
|
17
|
+
<span className={styles.valueUnit}>{unit}</span>
|
|
18
|
+
<div className={styles.trend}>
|
|
19
|
+
{trend === 'up' ? <TrendUpIcon color='white' /> : <TrendDownIcon color='white' />}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
</div>
|
|
24
|
+
<div className={styles.helperBox}>
|
|
25
|
+
{helper}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
{showGraph && (
|
|
31
|
+
<div className={styles.contentGraph}>
|
|
32
|
+
<TrendLine yValues={data} />
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.contentText {
|
|
2
|
+
@apply flex flex-col gap-2 items-center justify-center;
|
|
3
|
+
}
|
|
4
|
+
.value {
|
|
5
|
+
@apply flex gap-4 items-center justify-center;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.valueNumber {
|
|
9
|
+
@apply text-[24px] font-semibold text-white;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.valueUnit {
|
|
13
|
+
@apply text-[16px] font-normal text-white/70;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.helperBox {
|
|
17
|
+
@apply text-[14px] font-normal text-white/70;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.contentGraph {
|
|
21
|
+
@apply w-[210px] h-[60px];
|
|
22
|
+
}
|
|
23
|
+
.container {
|
|
24
|
+
@apply flex items-center justify-center gap-4;
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import styles from './Icons.module.css'
|
|
3
|
+
import { SMALL, MAIN_DARK_BLUE } from '../constants'
|
|
4
|
+
|
|
5
|
+
const TrendDownIcon = ({
|
|
6
|
+
color = MAIN_DARK_BLUE,
|
|
7
|
+
size = SMALL,
|
|
8
|
+
disabled = false,
|
|
9
|
+
inactive = false
|
|
10
|
+
}) => {
|
|
11
|
+
let className = `${styles.svgClassName} ` + styles[`${color}`]
|
|
12
|
+
if (disabled) {
|
|
13
|
+
className += ` ${styles.iconDisabled}`
|
|
14
|
+
}
|
|
15
|
+
if (inactive) {
|
|
16
|
+
className += ` ${styles.iconInactive}`
|
|
17
|
+
}
|
|
18
|
+
let icon = <></>
|
|
19
|
+
|
|
20
|
+
switch (size) {
|
|
21
|
+
case SMALL:
|
|
22
|
+
icon = (
|
|
23
|
+
<svg
|
|
24
|
+
width={16}
|
|
25
|
+
height={16}
|
|
26
|
+
viewBox='0 0 16 16'
|
|
27
|
+
fill='none'
|
|
28
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
29
|
+
className={className}
|
|
30
|
+
>
|
|
31
|
+
<path d='M2.66602 2.25L5.83487 8.1916C6.20065 8.87744 7.1748 8.90203 7.57471 8.2355L8.77521 6.23467C9.1712 5.5747 10.1331 5.59072 10.5069 6.26352L14.666 13.75M14.666 13.75L11.166 12.75M14.666 13.75V9.75' stroke='white' strokeLinecap='round' strokeLinejoin='round' />
|
|
32
|
+
</svg>
|
|
33
|
+
)
|
|
34
|
+
break
|
|
35
|
+
default:
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
return icon
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default TrendDownIcon
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import styles from './Icons.module.css'
|
|
3
|
+
import { SMALL, MAIN_DARK_BLUE } from '../constants'
|
|
4
|
+
|
|
5
|
+
const TrendUpIcon = ({
|
|
6
|
+
color = MAIN_DARK_BLUE,
|
|
7
|
+
size = SMALL,
|
|
8
|
+
disabled = false,
|
|
9
|
+
inactive = false
|
|
10
|
+
}) => {
|
|
11
|
+
let className = `${styles.svgClassName} ` + styles[`${color}`]
|
|
12
|
+
if (disabled) {
|
|
13
|
+
className += ` ${styles.iconDisabled}`
|
|
14
|
+
}
|
|
15
|
+
if (inactive) {
|
|
16
|
+
className += ` ${styles.iconInactive}`
|
|
17
|
+
}
|
|
18
|
+
let icon = <></>
|
|
19
|
+
|
|
20
|
+
switch (size) {
|
|
21
|
+
case SMALL:
|
|
22
|
+
icon = (
|
|
23
|
+
<svg
|
|
24
|
+
width={16}
|
|
25
|
+
height={16}
|
|
26
|
+
viewBox='0 0 16 16'
|
|
27
|
+
fill='none'
|
|
28
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
29
|
+
className={className}
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
<path d='M0.833984 12.25L4.00284 6.3084C4.36862 5.62256 5.34277 5.59797 5.74268 6.2645L6.94318 8.26533C7.33917 8.9253 8.30105 8.90928 8.67483 8.23648L12.834 0.75M12.834 0.75L9.33398 1.75M12.834 0.75V4.75' stroke='white' strokeLinecap='round' strokeLinejoin='round' />
|
|
33
|
+
|
|
34
|
+
</svg>
|
|
35
|
+
)
|
|
36
|
+
break
|
|
37
|
+
default:
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
return icon
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default TrendUpIcon
|
|
@@ -209,6 +209,8 @@ import TaxonomyIcon from './TaxonomyIcon'
|
|
|
209
209
|
import TeamsIcon from './TeamsIcon'
|
|
210
210
|
import TerminalIcon from './TerminalIcon'
|
|
211
211
|
import TrashIcon from './TrashIcon'
|
|
212
|
+
import TrendDownIcon from './TrendDownIcon'
|
|
213
|
+
import TrendUpIcon from './TrendUpIcon'
|
|
212
214
|
import TwoUsersIcon from './TwoUsersIcon'
|
|
213
215
|
import UpgradeIcon from './UpgradeIcon'
|
|
214
216
|
import UploadFileIcon from './UploadFileIcon'
|
|
@@ -438,6 +440,8 @@ export default {
|
|
|
438
440
|
TeamsIcon,
|
|
439
441
|
TerminalIcon,
|
|
440
442
|
TrashIcon,
|
|
443
|
+
TrendDownIcon,
|
|
444
|
+
TrendUpIcon,
|
|
441
445
|
TwoUsersIcon,
|
|
442
446
|
UpgradeIcon,
|
|
443
447
|
UploadFileIcon,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ArcMetric from '../components/ArcMetric'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Platformatic/Metrics/ArcMetric',
|
|
6
|
+
component: ArcMetric
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Default = () => (
|
|
10
|
+
<ArcMetric
|
|
11
|
+
value={50}
|
|
12
|
+
max={100}
|
|
13
|
+
unit='MB'
|
|
14
|
+
title='Memory Allocation & Usage'
|
|
15
|
+
helper={<><span>100 MB</span><br /><span>Allocated</span></>}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import MetricInfoBox from '../components/MetricInfoBox'
|
|
3
|
+
import TrendMetric from '../components/TrendMetric'
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Platformatic/Metrics/MetricInfoBox',
|
|
7
|
+
component: MetricInfoBox
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Default = () => (
|
|
11
|
+
<MetricInfoBox
|
|
12
|
+
title='Memory Allocation & Usage'
|
|
13
|
+
>
|
|
14
|
+
<TrendMetric
|
|
15
|
+
value={50}
|
|
16
|
+
unit='MB'
|
|
17
|
+
data={[100, 90, 80, 70, 60, 50, 40, 30, 20, 10]}
|
|
18
|
+
helper='Average Usage'
|
|
19
|
+
/>
|
|
20
|
+
</MetricInfoBox>
|
|
21
|
+
)
|
|
22
|
+
export const WithGraph = () => (
|
|
23
|
+
<MetricInfoBox
|
|
24
|
+
title='Memory Allocation & Usage'
|
|
25
|
+
>
|
|
26
|
+
<TrendMetric
|
|
27
|
+
value={50}
|
|
28
|
+
unit='MB'
|
|
29
|
+
data={[100, 90, 80, 70, 60, 50, 40, 30, 20, 10]}
|
|
30
|
+
helper='Average Usage'
|
|
31
|
+
showGraph
|
|
32
|
+
/>
|
|
33
|
+
</MetricInfoBox>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export const WithTooltip = () => (
|
|
37
|
+
<MetricInfoBox
|
|
38
|
+
title='Memory Allocation & Usage'
|
|
39
|
+
tooltip='This is a tooltip'
|
|
40
|
+
>
|
|
41
|
+
<TrendMetric
|
|
42
|
+
value={50}
|
|
43
|
+
unit='MB'
|
|
44
|
+
data={[100, 90, 80, 70, 60, 50, 40, 30, 20, 10]}
|
|
45
|
+
helper='Average Usage'
|
|
46
|
+
showGraph
|
|
47
|
+
/>
|
|
48
|
+
</MetricInfoBox>
|
|
49
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import TrendLine from '../components/TrendLine'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Components/TrendLine',
|
|
6
|
+
component: TrendLine
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Template = (args) => (
|
|
10
|
+
<div style={{ background: '#0B1016', padding: 24, minHeight: 180 }}>
|
|
11
|
+
<TrendLine {...args} />
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export const Default = Template.bind({})
|
|
16
|
+
Default.args = {
|
|
17
|
+
yValues: [20, 40, 30, 50, 35, 60, 55, 70, 65, 80, 60, 75, 70, 90, 85],
|
|
18
|
+
width: 400,
|
|
19
|
+
height: 120
|
|
20
|
+
}
|