Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/fe/infrastructure details, resolves #1068 #1167

Merged
merged 26 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
93d96b9
Add infrastrucutre/details route
ajhollid Nov 19, 2024
0832320
Create stat boxes
ajhollid Nov 19, 2024
fbdd3c1
Add custom gauge component
ajhollid Nov 19, 2024
d2f42f4
Add styles for custom gauage
ajhollid Nov 19, 2024
ca9ca4f
Add gauges
ajhollid Nov 19, 2024
55e13d7
Add area chart file
ajhollid Nov 19, 2024
49d81cf
Add area chart
ajhollid Nov 19, 2024
92ba0d8
Add data for area chart
ajhollid Nov 19, 2024
924c799
add and update chartUtils
ajhollid Nov 19, 2024
6a0d61b
Update Area Chart docs and proptype
ajhollid Nov 19, 2024
a0c7346
Add area charts, fix dummy data
ajhollid Nov 19, 2024
e3c13d9
Fix label proptype to allow for dates and numbers, add comments
ajhollid Nov 19, 2024
9cf2be7
Add a randomly generated gradientID to so each chart has a unique gra…
ajhollid Nov 19, 2024
0c194a0
Change AreaChart responsive container height to 80%
ajhollid Nov 19, 2024
7f380ce
Add headers to charts, add multpilier for percentages, increase chart…
ajhollid Nov 19, 2024
5a2c6a9
Add jsdocs and proptypes to Infrastructure Details Page
ajhollid Nov 19, 2024
9e7e13b
truncate Gauge percentage to 2 decimal places
ajhollid Nov 19, 2024
217ae50
Fetch data from gist
ajhollid Nov 19, 2024
7223ca3
Add temporary dummy data route
ajhollid Nov 19, 2024
dbe174d
remove dummy from Area Chart
ajhollid Nov 20, 2024
8b73955
truncate decimals in tool top
ajhollid Nov 20, 2024
b1449d3
refactor details page to use reduce code duplication, caluclate area …
ajhollid Nov 20, 2024
9bbcaaf
memoize calculations in SVG gauge
ajhollid Nov 20, 2024
1f5b5dd
details -> Details
ajhollid Nov 20, 2024
8bbb001
Add monitor information at top of page
ajhollid Nov 20, 2024
11cc283
extract tooltip percentage formatting
ajhollid Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { getAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Infrastructure } from "./Pages/Infrastructure";
import InfrastructureDetails from "./Pages/Infrastructure/Details";
function App() {
const AdminCheckedRegister = withAdminCheck(Register);
const MonitorsWithAdminProp = withAdminProp(Monitors);
Expand All @@ -48,6 +49,7 @@ function App() {
const MaintenanceWithAdminProp = withAdminProp(Maintenance);
const SettingsWithAdminProp = withAdminProp(Settings);
const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings);
const InfrastructureDetailsWithAdminProp = withAdminProp(InfrastructureDetails);
const mode = useSelector((state) => state.ui.mode);
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
Expand Down Expand Up @@ -128,6 +130,11 @@ function App() {
element={<ProtectedRoute Component={Infrastructure} />}
/>

<Route
path="infrastructure/:monitorId"
element={<ProtectedRoute Component={InfrastructureDetailsWithAdminProp} />}
/>

<Route
path="incidents/:monitorId?"
element={<ProtectedRoute Component={Incidents} />}
Expand Down
157 changes: 157 additions & 0 deletions Client/src/Components/Charts/AreaChart/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { createGradient } from "../Utils/gradientUtils";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
import { useId } from "react";
/**
* CustomAreaChart component for rendering an area chart with optional gradient and custom ticks.
*
* @param {Object} props - The properties object.
* @param {Array} props.data - The data array for the chart.
* @param {string} props.xKey - The key for the x-axis data.
* @param {string} props.yKey - The key for the y-axis data.
* @param {Object} [props.xTick] - Custom tick component for the x-axis.
* @param {Object} [props.yTick] - Custom tick component for the y-axis.
* @param {string} [props.strokeColor] - The stroke color for the area.
* @param {string} [props.fillColor] - The fill color for the area.
* @param {boolean} [props.gradient=false] - Whether to apply a gradient fill.
* @param {string} [props.gradientDirection="vertical"] - The direction of the gradient.
* @param {string} [props.gradientStartColor] - The start color of the gradient.
* @param {string} [props.gradientEndColor] - The end color of the gradient.
* @param {Object} [props.customTooltip] - Custom tooltip component.
* @returns {JSX.Element} The rendered area chart component.
*
* @example
* // Example usage of CustomAreaChart
* import React from 'react';
* import CustomAreaChart from './CustomAreaChart';
* import { TzTick, PercentTick, InfrastructureTooltip } from './chartUtils';
*
* const data = [
* { createdAt: '2023-01-01T00:00:00Z', cpu: { usage_percent: 0.5 } },
* { createdAt: '2023-01-01T01:00:00Z', cpu: { usage_percent: 0.6 } },
* // more data points...
* ];
*
* const MyChartComponent = () => {
* return (
* <CustomAreaChart
* data={data}
* xKey="createdAt"
* yKey="cpu.usage_percent"
* xTick={<TzTick />}
* yTick={<PercentTick />}
* strokeColor="#8884d8"
* fillColor="#8884d8"
* gradient={true}
* gradientStartColor="#8884d8"
* gradientEndColor="#82ca9d"
* customTooltip={({ active, payload, label }) => (
* <InfrastructureTooltip
* label={label?.toString() ?? ""}
* yKey="cpu.usage_percent"
* yLabel="CPU Usage"
* active={active}
* payload={payload}
* />
* )}
* />
* );
* };
*
* export default MyChartComponent;
*/
const CustomAreaChart = ({
data,
dataKey,
xKey,
yKey,
xTick,
yTick,
strokeColor,
fillColor,
gradient = false,
gradientDirection = "vertical",
gradientStartColor,
gradientEndColor,
customTooltip,
height = "100%",
}) => {
const theme = useTheme();
const uniqueId = useId();
const gradientId = `gradient-${uniqueId}`;
return (
<ResponsiveContainer
width="100%"
height={height}
// FE team HELP! Why does this overflow if set to 100%?
>
<AreaChart data={data}>
<XAxis
dataKey={xKey}
{...(xTick && { tick: xTick })}
/>
<YAxis
dataKey={yKey}
{...(yTick && { tick: yTick })}
/>
{gradient === true &&
createGradient({
id: gradientId,
startColor: gradientStartColor,
endColor: gradientEndColor,
direction: gradientDirection,
})}
<CartesianGrid
stroke={theme.palette.border.light}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<Area
type="monotone"
dataKey={dataKey}
stroke={strokeColor}
fill={gradient === true ? `url(#${gradientId})` : fillColor}
/>
{customTooltip ? (
<Tooltip
cursor={{ stroke: theme.palette.border.light }}
content={customTooltip}
wrapperStyle={{ pointerEvents: "none" }}
/>
) : (
<Tooltip />
)}
</AreaChart>
</ResponsiveContainer>
);
};

CustomAreaChart.propTypes = {
data: PropTypes.array.isRequired,
dataKey: PropTypes.string.isRequired,
xTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
yTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
xKey: PropTypes.string.isRequired,
yKey: PropTypes.string.isRequired,
fillColor: PropTypes.string,
strokeColor: PropTypes.string,
gradient: PropTypes.bool,
gradientDirection: PropTypes.string,
gradientStartColor: PropTypes.string,
gradientEndColor: PropTypes.string,
customTooltip: PropTypes.func,
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

export default CustomAreaChart;
14 changes: 14 additions & 0 deletions Client/src/Components/Charts/CustomGauge/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.radial-chart {
position: relative;
display: inline-block;
}

.radial-chart-base {
opacity: 0.3;
}

.radial-chart-progress {
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset 1.5s ease-in-out;
}
113 changes: 113 additions & 0 deletions Client/src/Components/Charts/CustomGauge/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useTheme } from "@emotion/react";
import { useEffect, useState, useMemo } from "react";
import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import "./index.css";

/**
* A Performant SVG based circular gauge
*
* @component
* @param {Object} props - Component properties
* @param {number} [props.progress=0] - Progress percentage (0-100)
* @param {number} [props.radius=60] - Radius of the gauge circle
* @param {string} [props.color="#000000"] - Color of the progress stroke
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
*
* @example
* <CustomGauge
* progress={75}
* radius={50}
* color="#00ff00"
* strokeWidth={10}
* />
*
* @returns {React.ReactElement} Rendered CustomGauge component
*/
const CustomGauge = ({
progress = 0,
radius = 60,
color = "#000000",
strokeWidth = 15,
}) => {
// Calculate the length of the stroke for the circle
const { circumference, totalSize, strokeLength } = useMemo(
() => ({
circumference: 2 * Math.PI * radius,
totalSize: radius * 2 + strokeWidth * 2,
strokeLength: (progress / 100) * (2 * Math.PI * radius),
}),
[radius, strokeWidth, progress]
);
const [offset, setOffset] = useState(circumference);
const theme = useTheme();

// Handle initial animation
useEffect(() => {
setOffset(circumference);
const timer = setTimeout(() => {
setOffset(circumference - strokeLength);
}, 100);

return () => clearTimeout(timer);
}, [progress, circumference, strokeLength]);

return (
<Box
className="radial-chart"
width={radius}
height={radius}
>
<svg
viewBox={`0 0 ${totalSize} ${totalSize}`}
width={radius}
height={radius}
>
<circle
className="radial-chart-base"
stroke={theme.palette.background.fill}
strokeWidth={strokeWidth}
fill="none"
cx={totalSize / 2} // Center the circle
cy={totalSize / 2} // Center the circle
r={radius}
/>
<circle
className="radial-chart-progress"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={offset}
strokeLinecap="round"
fill="none"
cx={totalSize / 2}
cy={totalSize / 2}
r={radius}
/>
</svg>
Comment on lines +61 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Let's make this accessible, fam!

The SVG needs ARIA attributes and a title for screen readers.

 <svg
   viewBox={`0 0 ${totalSize} ${totalSize}`}
   width={radius}
   height={radius}
+  role="img"
+  aria-label={`Progress gauge showing ${progress.toFixed(2)}%`}
 >
+  <title>Progress Gauge</title>
   <circle
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<svg
viewBox={`0 0 ${totalSize} ${totalSize}`}
width={radius}
height={radius}
>
<circle
className="radial-chart-base"
stroke={theme.palette.background.fill}
strokeWidth={strokeWidth}
fill="none"
cx={totalSize / 2} // Center the circle
cy={totalSize / 2} // Center the circle
r={radius}
/>
<circle
className="radial-chart-progress"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={offset}
strokeLinecap="round"
fill="none"
cx={totalSize / 2}
cy={totalSize / 2}
r={radius}
/>
</svg>
<svg
viewBox={`0 0 ${totalSize} ${totalSize}`}
width={radius}
height={radius}
role="img"
aria-label={`Progress gauge showing ${progress.toFixed(2)}%`}
>
<title>Progress Gauge</title>
<circle
className="radial-chart-base"
stroke={theme.palette.background.fill}
strokeWidth={strokeWidth}
fill="none"
cx={totalSize / 2} // Center the circle
cy={totalSize / 2} // Center the circle
r={radius}
/>
<circle
className="radial-chart-progress"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={offset}
strokeLinecap="round"
fill="none"
cx={totalSize / 2}
cy={totalSize / 2}
r={radius}
/>
</svg>


<Typography
className="radial-chart-text"
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
...theme.typography.body2,
fill: theme.typography.body2.color,
}}
>
{`${progress.toFixed(2)}%`}
</Typography>
</Box>
);
};

export default CustomGauge;

CustomGauge.propTypes = {
progress: PropTypes.number,
radius: PropTypes.number,
color: PropTypes.string,
strokeWidth: PropTypes.number,
};
Comment on lines +108 to +113
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Yo, let's lock down these PropTypes!

Make these PropTypes more strict to catch bugs early in development.

 CustomGauge.propTypes = {
-  progress: PropTypes.number,
-  radius: PropTypes.number,
-  color: PropTypes.string,
-  strokeWidth: PropTypes.number,
+  progress: PropTypes.number.isRequired,
+  radius: PropTypes.number,
+  color: PropTypes.string,
+  strokeWidth: PropTypes.oneOf([PropTypes.number, PropTypes.string]),
 };

Committable suggestion skipped: line range outside the PR's diff.

Loading