@platformatic/ui-components 0.17.0 → 0.17.1

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.
@@ -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,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,39 @@
1
+ import React from 'react'
2
+ import MetricInfoBox from '../components/MetricInfoBox'
3
+
4
+ export default {
5
+ title: 'Platformatic/Metrics/MetricInfoBox',
6
+ component: MetricInfoBox
7
+ }
8
+
9
+ export const Default = () => (
10
+ <MetricInfoBox
11
+ title='Memory Allocation & Usage'
12
+ value={50}
13
+ unit='MB'
14
+ data={[100, 90, 80, 70, 60, 50, 40, 30, 20, 10]}
15
+ helper='Average Usage'
16
+ />
17
+ )
18
+ export const WithGraph = () => (
19
+ <MetricInfoBox
20
+ title='Memory Allocation & Usage'
21
+ value={50}
22
+ unit='MB'
23
+ data={[100, 90, 80, 70, 60, 50, 40, 30, 20, 10]}
24
+ helper='Average Usage'
25
+ showGraph
26
+ />
27
+ )
28
+
29
+ export const WithTooltip = () => (
30
+ <MetricInfoBox
31
+ title='Memory Allocation & Usage'
32
+ value={50}
33
+ unit='MB'
34
+ data={[100, 90, 80, 70, 60, 50, 40, 30, 20, 10]}
35
+ helper='Average Usage'
36
+ showGraph
37
+ tooltip='This is a tooltip'
38
+ />
39
+ )
@@ -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
+ }