Skip to content

Commit 8b3ad1b

Browse files
authored
Merge pull request #446 from platformatic/create-components-for-new-autoscaler-ui
2 parents 7f87e04 + ab1bb92 commit 8b3ad1b

14 files changed

+1063
-15
lines changed

package-lock.json

Lines changed: 453 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"dependencies": {
2525
"@storybook/test": "^8.0.8",
2626
"autoprefixer": "^10.4.12",
27+
"d3": "^7.9.0",
2728
"postcss": "^8.4.17",
2829
"react": "^19.0.0",
2930
"react-dom": "^19.0.0",

src/components/ArcMetric.jsx

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import React from 'react'
2+
import styles from './ArcMetric.module.css'
3+
4+
/**
5+
* ArcMetric component
6+
* @param {Object} props
7+
* @param {number} props.value - The current value to display in the arc
8+
* @param {number} props.max - The maximum value for the arc (for progress calculation)
9+
* @param {string} props.unit - The unit of measure (e.g., 'MB')
10+
* @param {string} props.title - The title to display at the top
11+
* @param {React.ReactNode} props.helper - The helper text (can be ReactNode)
12+
*/
13+
export default function ArcMetric ({ value, max, unit, title, helper }) {
14+
// Arc settings
15+
const radius = 150
16+
const progress = Math.max(0, Math.min(1, value / max))
17+
18+
return (
19+
<div className={styles.container}>
20+
<div className={styles.headerRow}>
21+
<span className={styles.title}>{title}</span>
22+
<span className={styles.infoIcon}></span>
23+
</div>
24+
<div className={styles.arcRow}>
25+
<SemiCircleProgress
26+
percentage={progress * 100}
27+
strokeWidth={10}
28+
diameter={radius * 2}
29+
orientation='up'
30+
direction='right'
31+
showPercentValue
32+
value={value}
33+
unit={unit}
34+
/>
35+
<div className={styles.helperBox}>{helper}</div>
36+
</div>
37+
</div>
38+
)
39+
}
40+
41+
export function SemiCircleProgress ({
42+
stroke = '#fff',
43+
strokeWidth = 10,
44+
background = '#33373C',
45+
diameter = 200,
46+
orientation = 'up',
47+
direction = 'right',
48+
showPercentValue = false,
49+
percentage,
50+
value,
51+
unit
52+
}) {
53+
const coordinateForCircle = diameter / 2
54+
const radius = (diameter - 2 * strokeWidth) / 2
55+
const circumference = Math.PI * radius
56+
57+
let percentageValue
58+
if (percentage > 100) {
59+
percentageValue = 100
60+
} else if (percentage < 0) {
61+
percentageValue = 0
62+
} else {
63+
percentageValue = percentage
64+
}
65+
const semiCirclePercentage = percentageValue * (circumference / 100)
66+
67+
let rotation
68+
if (orientation === 'down') {
69+
if (direction === 'left') {
70+
rotation = 'rotate(180deg) rotateY(180deg)'
71+
} else {
72+
rotation = 'rotate(180deg)'
73+
}
74+
} else {
75+
if (direction === 'right') {
76+
rotation = 'rotateY(180deg)'
77+
}
78+
}
79+
80+
return (
81+
<div className='semicircle-container' style={{ position: 'relative' }}>
82+
<svg
83+
width={diameter}
84+
height={diameter / 2}
85+
style={{ transform: rotation, overflow: 'hidden' }}
86+
>
87+
<circle
88+
cx={coordinateForCircle}
89+
cy={coordinateForCircle}
90+
r={radius}
91+
fill='none'
92+
stroke={background}
93+
strokeWidth={strokeWidth}
94+
strokeDasharray={circumference}
95+
style={{
96+
strokeDashoffset: circumference
97+
}}
98+
/>
99+
<circle
100+
cx={coordinateForCircle}
101+
cy={coordinateForCircle}
102+
r={radius}
103+
fill='none'
104+
stroke={stroke}
105+
strokeWidth={strokeWidth}
106+
strokeDasharray={circumference}
107+
style={{
108+
strokeDashoffset: semiCirclePercentage,
109+
transition:
110+
'stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s'
111+
}}
112+
/>
113+
</svg>
114+
{showPercentValue && (
115+
<div className='flex items-center justify-center gap-2 absolute bottom-0 left-0 right-0 text-white text-[2.3rem] font-bold'>
116+
<span>{value}</span>
117+
<span className='text-white/70 text-[1.2rem] font-normal'>{unit}</span>
118+
</div>
119+
120+
)}
121+
</div>
122+
)
123+
}
124+
125+
// function percentageValidation (isRequired) {
126+
// return function (props, propName, componentName) {
127+
// const prop = props[propName]
128+
// if (prop == null) {
129+
// if (isRequired) {
130+
// throw new Error('Percentage is a required prop.')
131+
// }
132+
// } else {
133+
// if (typeof prop !== 'number') {
134+
// return new Error(
135+
// 'Invalid percentage. Must be a number between 0 and 100.'
136+
// )
137+
// }
138+
// if (props[propName] < 0 || props[propName] > 100) {
139+
// return new Error(
140+
// 'Invalid percentage. Must be a number between 0 and 100.'
141+
// )
142+
// }
143+
// }
144+
// }
145+
// }
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.container {
2+
@apply bg-black-russian rounded-lg p-4 box-border flex flex-col w-full gap-4;
3+
}
4+
5+
.headerRow {
6+
@apply flex justify-between items-center mb-4;
7+
}
8+
9+
.title {
10+
@apply text-[24px] font-semibold text-white;
11+
}
12+
13+
.infoIcon {
14+
@apply text-white/70 cursor-pointer text-[16px];
15+
}
16+
17+
.arcRow {
18+
@apply flex items-center justify-center relative;
19+
}
20+
21+
.helperBox {
22+
margin-left: 48px;
23+
display: flex;
24+
flex-direction: column;
25+
justify-content: center;
26+
color: #b0b6be;
27+
font-size: 1.2rem;
28+
font-weight: 400;
29+
min-width: 120px;
30+
text-align: left;
31+
}

src/components/MetricInfoBox.jsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, { useState } from 'react'
2+
import styles from './MetricInfoBox.module.css'
3+
import TrendDownIcon from './icons/TrendDownIcon'
4+
import TrendUpIcon from './icons/TrendUpIcon'
5+
import TrendLine from './TrendLine'
6+
import Tooltip from './Tooltip'
7+
import { DIRECTION_TOP, POSITION_CENTER, MEDIUM, WHITE } from './constants'
8+
import PlatformaticIcon from './PlatformaticIcon'
9+
10+
export default function MetricInfoBox ({
11+
title,
12+
value,
13+
unit,
14+
data = [],
15+
helper,
16+
tooltip,
17+
showGraph = false
18+
}) {
19+
// set trend variable up or down, depending on the first and last value of the data array
20+
const trend = data[0] > data[data.length - 1] ? 'up' : 'down'
21+
const [tooltipVisible, setTooltipVisible] = useState(false)
22+
23+
return (
24+
<div className={styles.container}>
25+
<div className={styles.headerRow}>
26+
<span className={styles.title}>{title}</span>
27+
{/* on hover show tooltip */}
28+
{tooltip && (
29+
<div
30+
className={styles.infoIcon}
31+
onMouseEnter={() => setTooltipVisible(true)}
32+
onMouseLeave={() => setTooltipVisible(false)}
33+
>
34+
<Tooltip
35+
content={<span>{tooltip}</span>}
36+
visible={tooltipVisible}
37+
direction={DIRECTION_TOP}
38+
position={POSITION_CENTER}
39+
offset={24}
40+
delay={100}
41+
activeDependsOnVisible
42+
>
43+
<PlatformaticIcon
44+
size={MEDIUM}
45+
iconName='InfoCircleIcon'
46+
color={WHITE}
47+
48+
/>
49+
</Tooltip>
50+
</div>
51+
)}
52+
</div>
53+
<div className={styles.content}>
54+
<div className={styles.contentText}>
55+
<div>
56+
<div className={styles.value}>
57+
<span className={styles.valueNumber}>{value}</span>
58+
<span className={styles.valueUnit}>{unit}</span>
59+
<div className={styles.trend}>
60+
{trend === 'up' ? <TrendUpIcon color='white' /> : <TrendDownIcon color='white' />}
61+
</div>
62+
</div>
63+
64+
</div>
65+
<div className={styles.helperBox}>
66+
{helper}
67+
</div>
68+
69+
</div>
70+
71+
{showGraph && (
72+
<div className={styles.contentGraph}>
73+
<TrendLine yValues={data} />
74+
</div>
75+
)}
76+
</div>
77+
78+
</div>
79+
)
80+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.container {
2+
@apply bg-black-russian rounded-lg p-4 box-border flex flex-col w-full gap-4;
3+
}
4+
5+
.headerRow {
6+
@apply flex justify-between items-center mb-4;
7+
}
8+
9+
.title {
10+
@apply text-[24px] font-semibold text-white;
11+
}
12+
13+
.infoIcon {
14+
@apply text-white/70 cursor-pointer text-[16px];
15+
}
16+
17+
.content {
18+
@apply flex items-center justify-center gap-4;
19+
}
20+
21+
.contentText {
22+
@apply flex flex-col gap-2 items-center justify-center;
23+
}
24+
.value {
25+
@apply flex gap-4 items-center justify-center;
26+
}
27+
28+
.valueNumber {
29+
@apply text-[24px] font-semibold text-white;
30+
}
31+
32+
.valueUnit {
33+
@apply text-[16px] font-normal text-white/70;
34+
}
35+
36+
.helperBox {
37+
@apply text-[14px] font-normal text-white/70;
38+
}
39+
40+
.contentGraph {
41+
@apply w-[210px] h-[60px];
42+
}

src/components/Tooltip.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function Tooltip ({
2323
componentClassName += ' ' + styles[`${position}`]
2424
}
2525
const fixedStyle = { top: '0px', left: '0px' }
26-
const wrapperRef = useRef()
26+
const wrapperRef = useRef(null)
2727

2828
useEffect(() => {
2929
if (activeDependsOnVisible) {

0 commit comments

Comments
 (0)