diff --git a/Common/Server/Infrastructure/Queue.ts b/Common/Server/Infrastructure/Queue.ts index 3c38f78354..4a68d36d78 100644 --- a/Common/Server/Infrastructure/Queue.ts +++ b/Common/Server/Infrastructure/Queue.ts @@ -65,11 +65,11 @@ export default class Queue { } public static getQueueInspectorRouter(): ExpressRouter { - const serverAdapter = new ExpressAdapter(); + const serverAdapter: ExpressAdapter = new ExpressAdapter(); createBullBoard({ queues: [ - ...Object.values(QueueName).map((queueName) => { + ...Object.values(QueueName).map((queueName: QueueName) => { return new BullMQAdapter(this.getQueue(queueName)); }), ], diff --git a/Common/Types/Date.ts b/Common/Types/Date.ts index ee6c32289c..9fc807b730 100644 --- a/Common/Types/Date.ts +++ b/Common/Types/Date.ts @@ -1138,8 +1138,6 @@ export default class OneUptimeDate { ); } - - public static getDateWithCustomTime(data: { hours: number; minutes: number; diff --git a/Common/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx b/Common/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx index 9f552f3e54..86f9032bb9 100644 --- a/Common/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +++ b/Common/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx @@ -1,10 +1,10 @@ // Tremor Raw LineChart [v0.3.1] /* eslint-disable @typescript-eslint/no-explicit-any */ -"use client" +"use client"; -import React from "react" -import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react" +import React from "react"; +import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react"; import { CartesianGrid, Dot, @@ -16,36 +16,41 @@ import { Tooltip, XAxis, YAxis, -} from "recharts" -import { AxisDomain } from "recharts/types/util/types" +} from "recharts"; +import { AxisDomain } from "recharts/types/util/types"; -import { useOnWindowResize } from "../Utils/UseWindowOnResize" +import { useOnWindowResize } from "../Utils/UseWindowOnResize"; import { AvailableChartColors, AvailableChartColorsKeys, constructCategoryColors, getColorClassName, -} from "../Utils/ChartColors" -import { cx } from "../Utils/Cx" -import { getYAxisDomain } from "../Utils/GetYAxisDomain" -import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey" +} from "../Utils/ChartColors"; +import { cx } from "../Utils/Cx"; +import { getYAxisDomain } from "../Utils/GetYAxisDomain"; +import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey"; //#region Legend interface LegendItemProps { - name: string - color: AvailableChartColorsKeys - onClick?: (name: string, color: AvailableChartColorsKeys) => void - activeLegend?: string + name: string; + color: AvailableChartColorsKeys; + onClick?: (name: string, color: AvailableChartColorsKeys) => void; + activeLegend?: string; } -const LegendItem = ({ +const LegendItem: ({ + name, + color, + onClick, + activeLegend, +}: LegendItemProps) => React.JSX.Element = ({ name, color, onClick, activeLegend, }: LegendItemProps) => { - const hasOnValueChange = !!onClick + const hasOnValueChange: boolean = Boolean(onClick); return (
  • { - e.stopPropagation() - onClick?.(name, color) + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + onClick?.(name, color); }} >
  • - ) -} + ); +}; interface ScrollButtonProps { - icon: React.ElementType - onClick?: () => void - disabled?: boolean + icon: React.ElementType; + onClick?: () => void; + disabled?: boolean; } -const ScrollButton = ({ icon, onClick, disabled }: ScrollButtonProps) => { - const Icon = icon - const [isPressed, setIsPressed] = React.useState(false) - const intervalRef = React.useRef(null) +const ScrollButton: ({ + icon, + onClick, + disabled, +}: ScrollButtonProps) => React.JSX.Element = ({ + icon, + onClick, + disabled, +}: ScrollButtonProps) => { + const Icon: React.ElementType = icon; + const [isPressed, setIsPressed] = React.useState(false); + const intervalRef: React.MutableRefObject = + React.useRef(null); React.useEffect(() => { if (isPressed) { intervalRef.current = setInterval(() => { - onClick?.() - }, 300) + onClick?.(); + }, 300); } else { - clearInterval(intervalRef.current as NodeJS.Timeout) + clearInterval(intervalRef.current as NodeJS.Timeout); } - return () => clearInterval(intervalRef.current as NodeJS.Timeout) - }, [isPressed, onClick]) + return () => { + return clearInterval(intervalRef.current as NodeJS.Timeout); + }; + }, [isPressed, onClick]); React.useEffect(() => { if (disabled) { - clearInterval(intervalRef.current as NodeJS.Timeout) - setIsPressed(false) + clearInterval(intervalRef.current as NodeJS.Timeout); + setIsPressed(false); } - }, [disabled]) + }, [disabled]); return ( - ) -} + ); +}; interface LegendProps extends React.OlHTMLAttributes { - categories: string[] - colors?: AvailableChartColorsKeys[] - onClickLegendItem?: (category: string, color: string) => void - activeLegend?: string - enableLegendSlider?: boolean + categories: string[]; + colors?: AvailableChartColorsKeys[]; + onClickLegendItem?: (category: string, color: string) => void; + activeLegend?: string; + enableLegendSlider?: boolean; } type HasScrollProps = { - left: boolean - right: boolean -} + left: boolean; + right: boolean; +}; -const Legend = React.forwardRef((props, ref) => { - const { - categories, - colors = AvailableChartColors, - className, - onClickLegendItem, - activeLegend, - enableLegendSlider = false, - ...other - } = props - const scrollableRef = React.useRef(null) - const scrollButtonsRef = React.useRef(null) - const [hasScroll, setHasScroll] = React.useState(null) - const [isKeyDowned, setIsKeyDowned] = React.useState(null) - const intervalRef = React.useRef(null) +const Legend: React.ForwardRefExoticComponent< + LegendProps & React.RefAttributes +> = React.forwardRef( + (props: LegendProps, ref: React.ForwardedRef) => { + const { + categories, + colors = AvailableChartColors, + className, + onClickLegendItem, + activeLegend, + enableLegendSlider = false, + ...other + } = props; - const checkScroll = React.useCallback(() => { - const scrollable = scrollableRef?.current - if (!scrollable) return + const scrollableRef: React.RefObject = + React.useRef(null); + const scrollButtonsRef: React.RefObject = + React.useRef(null); + const [hasScroll, setHasScroll] = React.useState( + null, + ); + const [isKeyDowned, setIsKeyDowned] = React.useState(null); + const intervalRef = React.useRef(null); - const hasLeftScroll = scrollable.scrollLeft > 0 - const hasRightScroll = - scrollable.scrollWidth - scrollable.clientWidth > scrollable.scrollLeft - - setHasScroll({ left: hasLeftScroll, right: hasRightScroll }) - }, [setHasScroll]) - - const scrollToTest = React.useCallback( - (direction: "left" | "right") => { - const element = scrollableRef?.current - const scrollButtons = scrollButtonsRef?.current - const scrollButtonsWith = scrollButtons?.clientWidth ?? 0 - const width = element?.clientWidth ?? 0 - - if (element && enableLegendSlider) { - element.scrollTo({ - left: - direction === "left" - ? element.scrollLeft - width + scrollButtonsWith - : element.scrollLeft + width - scrollButtonsWith, - behavior: "smooth", - }) - setTimeout(() => { - checkScroll() - }, 400) + const checkScroll = React.useCallback(() => { + const scrollable = scrollableRef?.current; + if (!scrollable) { + return; } - }, - [enableLegendSlider, checkScroll], - ) - React.useEffect(() => { - const keyDownHandler = (key: string) => { - if (key === "ArrowLeft") { - scrollToTest("left") - } else if (key === "ArrowRight") { - scrollToTest("right") + const hasLeftScroll = scrollable.scrollLeft > 0; + const hasRightScroll = + scrollable.scrollWidth - scrollable.clientWidth > scrollable.scrollLeft; + + setHasScroll({ left: hasLeftScroll, right: hasRightScroll }); + }, [setHasScroll]); + + const scrollToTest = React.useCallback( + (direction: "left" | "right") => { + const element = scrollableRef?.current; + const scrollButtons = scrollButtonsRef?.current; + const scrollButtonsWith = scrollButtons?.clientWidth ?? 0; + const width = element?.clientWidth ?? 0; + + if (element && enableLegendSlider) { + element.scrollTo({ + left: + direction === "left" + ? element.scrollLeft - width + scrollButtonsWith + : element.scrollLeft + width - scrollButtonsWith, + behavior: "smooth", + }); + setTimeout(() => { + checkScroll(); + }, 400); + } + }, + [enableLegendSlider, checkScroll], + ); + + React.useEffect(() => { + const keyDownHandler = (key: string) => { + if (key === "ArrowLeft") { + scrollToTest("left"); + } else if (key === "ArrowRight") { + scrollToTest("right"); + } + }; + if (isKeyDowned) { + keyDownHandler(isKeyDowned); + intervalRef.current = setInterval(() => { + keyDownHandler(isKeyDowned); + }, 300); + } else { + clearInterval(intervalRef.current!); } - } - if (isKeyDowned) { - keyDownHandler(isKeyDowned) - intervalRef.current = setInterval(() => { - keyDownHandler(isKeyDowned) - }, 300) - } else { - clearInterval(intervalRef.current as NodeJS.Timeout) - } - return () => clearInterval(intervalRef.current as NodeJS.Timeout) - }, [isKeyDowned, scrollToTest]) + return () => { + return clearInterval(intervalRef.current as NodeJS.Timeout); + }; + }, [isKeyDowned, scrollToTest]); - const keyDown = (e: KeyboardEvent) => { - e.stopPropagation() - if (e.key === "ArrowLeft" || e.key === "ArrowRight") { - e.preventDefault() - setIsKeyDowned(e.key) - } - } - const keyUp = (e: KeyboardEvent) => { - e.stopPropagation() - setIsKeyDowned(null) - } + const keyDown = (e: KeyboardEvent) => { + e.stopPropagation(); + if (e.key === "ArrowLeft" || e.key === "ArrowRight") { + e.preventDefault(); + setIsKeyDowned(e.key); + } + }; + const keyUp = (e: KeyboardEvent) => { + e.stopPropagation(); + setIsKeyDowned(null); + }; - React.useEffect(() => { - const scrollable = scrollableRef?.current - if (enableLegendSlider) { - checkScroll() - scrollable?.addEventListener("keydown", keyDown) - scrollable?.addEventListener("keyup", keyUp) - } + React.useEffect(() => { + const scrollable = scrollableRef?.current; + if (enableLegendSlider) { + checkScroll(); + scrollable?.addEventListener("keydown", keyDown); + scrollable?.addEventListener("keyup", keyUp); + } - return () => { - scrollable?.removeEventListener("keydown", keyDown) - scrollable?.removeEventListener("keyup", keyUp) - } - }, [checkScroll, enableLegendSlider]) + return () => { + scrollable?.removeEventListener("keydown", keyDown); + scrollable?.removeEventListener("keyup", keyUp); + }; + }, [checkScroll, enableLegendSlider]); - return ( -
      -
      - {categories.map((category, index) => ( - - ))} -
      - {enableLegendSlider && (hasScroll?.right || hasScroll?.left) ? ( - <> -
      - { - setIsKeyDowned(null) - scrollToTest("left") - }} - disabled={!hasScroll?.left} - /> - { - setIsKeyDowned(null) - scrollToTest("right") - }} - disabled={!hasScroll?.right} - /> -
      - - ) : null} -
    - ) -}) +
    + {categories.map((category, index) => { + return ( + + ); + })} +
    + {enableLegendSlider && (hasScroll?.right || hasScroll?.left) ? ( + <> +
    + { + setIsKeyDowned(null); + scrollToTest("left"); + }} + disabled={!hasScroll?.left} + /> + { + setIsKeyDowned(null); + scrollToTest("right"); + }} + disabled={!hasScroll?.right} + /> +
    + + ) : null} + + ); + }, +); -Legend.displayName = "Legend" +Legend.displayName = "Legend"; const ChartLegend = ( { payload }: any, @@ -324,18 +356,21 @@ const ChartLegend = ( legendPosition?: "left" | "center" | "right", yAxisWidth?: number, ) => { - const legendRef = React.useRef(null) + const legendRef = React.useRef(null); useOnWindowResize(() => { - const calculateHeight = (height: number | undefined) => - height ? Number(height) + 15 : 60 - setLegendHeight(calculateHeight(legendRef.current?.clientHeight)) - }) + const calculateHeight = (height: number | undefined) => { + return height ? Number(height) + 15 : 60; + }; + setLegendHeight(calculateHeight(legendRef.current?.clientHeight)); + }); - const legendPayload = payload.filter((item: any) => item.type !== "none") + const legendPayload = payload.filter((item: any) => { + return item.type !== "none"; + }); const paddingLeft = - legendPosition === "left" && yAxisWidth ? yAxisWidth - 8 : 0 + legendPosition === "left" && yAxisWidth ? yAxisWidth - 8 : 0; return (
    entry.value)} - colors={legendPayload.map((entry: any) => - categoryColors.get(entry.value), - )} + categories={legendPayload.map((entry: any) => { + return entry.value; + })} + colors={legendPayload.map((entry: any) => { + return categoryColors.get(entry.value); + })} onClickLegendItem={onClick!} activeLegend={activeLegend!} enableLegendSlider={enableLegendSlider!} />
    - ) -} + ); +}; //#region Tooltip -type TooltipProps = Pick +type TooltipProps = Pick; type PayloadItem = { - category: string - value: number - index: string - color: AvailableChartColorsKeys - type?: string - payload: any -} + category: string; + value: number; + index: string; + color: AvailableChartColorsKeys; + type?: string; + payload: any; +}; interface ChartTooltipProps { - active: boolean | undefined - payload: PayloadItem[] - label: string - valueFormatter: (value: number) => string + active: boolean | undefined; + payload: PayloadItem[]; + label: string; + valueFormatter: (value: number) => string; } const ChartTooltip = ({ @@ -388,7 +425,9 @@ const ChartTooltip = ({ valueFormatter, }: ChartTooltipProps) => { if (active && payload && payload.length) { - const legendPayload = payload.filter((item: any) => item.type !== "none") + const legendPayload = payload.filter((item: any) => { + return item.type !== "none"; + }); return (
    - {legendPayload.map(({ value, category, color }, index) => ( -
    -
    -
    - ))} + ); + })}
    - ) + ); } - return null -} + return null; +}; //#region LineChart interface ActiveDot { - index?: number - dataKey?: string + index?: number; + dataKey?: string; } type BaseEventProps = { - eventType: "dot" | "category" - categoryClicked: string - [key: string]: number | string -} + eventType: "dot" | "category"; + categoryClicked: string; + [key: string]: number | string; +}; -type LineChartEventProps = BaseEventProps | null | undefined +type LineChartEventProps = BaseEventProps | null | undefined; interface LineChartProps extends React.HTMLAttributes { - data: Record[] - index: string - categories: string[] - colors?: AvailableChartColorsKeys[] - valueFormatter?: (value: number) => string - startEndOnly?: boolean - showXAxis?: boolean - showYAxis?: boolean - showGridLines?: boolean - yAxisWidth?: number - intervalType?: "preserveStartEnd" | "equidistantPreserveStart" - curve?: "linear" | "monotone" - showTooltip?: boolean - showLegend?: boolean - autoMinValue?: boolean - minValue?: number - maxValue?: number - allowDecimals?: boolean - onValueChange?: (value: LineChartEventProps) => void - enableLegendSlider?: boolean - tickGap?: number - connectNulls?: boolean - xAxisLabel?: string - yAxisLabel?: string - legendPosition?: "left" | "center" | "right" - tooltipCallback?: (tooltipCallbackContent: TooltipProps) => void - customTooltip?: React.ComponentType + data: Record[]; + index: string; + categories: string[]; + colors?: AvailableChartColorsKeys[]; + valueFormatter?: (value: number) => string; + startEndOnly?: boolean; + showXAxis?: boolean; + showYAxis?: boolean; + showGridLines?: boolean; + yAxisWidth?: number; + intervalType?: "preserveStartEnd" | "equidistantPreserveStart"; + showTooltip?: boolean; + showLegend?: boolean; + autoMinValue?: boolean; + minValue?: number; + maxValue?: number; + allowDecimals?: boolean; + onValueChange?: (value: LineChartEventProps) => void; + enableLegendSlider?: boolean; + tickGap?: number; + connectNulls?: boolean; + xAxisLabel?: string; + yAxisLabel?: string; + legendPosition?: "left" | "center" | "right"; + tooltipCallback?: (tooltipCallbackContent: TooltipProps) => void; + customTooltip?: React.ComponentType; } const LineChart = React.forwardRef( @@ -508,7 +548,9 @@ const LineChart = React.forwardRef( categories = [], index, colors = AvailableChartColors, - valueFormatter = (value: number) => value.toString(), + valueFormatter = (value: number) => { + return value.toString(); + }, startEndOnly = false, showXAxis = true, showYAxis = true, @@ -532,28 +574,30 @@ const LineChart = React.forwardRef( tooltipCallback, customTooltip, ...other - } = props - const CustomTooltip = customTooltip + } = props; + const CustomTooltip = customTooltip; const paddingValue = - (!showXAxis && !showYAxis) || (startEndOnly && !showYAxis) ? 0 : 20 - const [legendHeight, setLegendHeight] = React.useState(60) + (!showXAxis && !showYAxis) || (startEndOnly && !showYAxis) ? 0 : 20; + const [legendHeight, setLegendHeight] = React.useState(60); const [activeDot, setActiveDot] = React.useState( undefined, - ) + ); const [activeLegend, setActiveLegend] = React.useState( undefined, - ) - const categoryColors = constructCategoryColors(categories, colors) + ); + const categoryColors = constructCategoryColors(categories, colors); - const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue) - const hasOnValueChange = !!onValueChange - const prevActiveRef = React.useRef(undefined) - const prevLabelRef = React.useRef(undefined) + const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); + const hasOnValueChange = Boolean(onValueChange); + const prevActiveRef = React.useRef(undefined); + const prevLabelRef = React.useRef(undefined); function onDotClick(itemData: any, event: React.MouseEvent) { - event.stopPropagation() + event.stopPropagation(); - if (!hasOnValueChange) return + if (!hasOnValueChange) { + return; + } if ( (itemData.index === activeDot?.index && itemData.dataKey === activeDot?.dataKey) || @@ -561,41 +605,43 @@ const LineChart = React.forwardRef( activeLegend && activeLegend === itemData.dataKey) ) { - setActiveLegend(undefined) - setActiveDot(undefined) - onValueChange?.(null) + setActiveLegend(undefined); + setActiveDot(undefined); + onValueChange?.(null); } else { - setActiveLegend(itemData.dataKey) + setActiveLegend(itemData.dataKey); setActiveDot({ index: itemData.index, dataKey: itemData.dataKey, - }) + }); onValueChange?.({ eventType: "dot", categoryClicked: itemData.dataKey, ...itemData.payload, - }) + }); } } function onCategoryClick(dataKey: string) { - if (!hasOnValueChange) return + if (!hasOnValueChange) { + return; + } if ( (dataKey === activeLegend && !activeDot) || (hasOnlyOneValueForKey(data, dataKey) && activeDot && activeDot.dataKey === dataKey) ) { - setActiveLegend(undefined) - onValueChange?.(null) + setActiveLegend(undefined); + onValueChange?.(null); } else { - setActiveLegend(dataKey) + setActiveLegend(dataKey); onValueChange?.({ eventType: "category", categoryClicked: dataKey, - }) + }); } - setActiveDot(undefined) + setActiveDot(undefined); } return ( @@ -606,9 +652,9 @@ const LineChart = React.forwardRef( onClick={ hasOnValueChange && (activeLegend || activeDot) ? () => { - setActiveDot(undefined) - setActiveLegend(undefined) - onValueChange?.(null) + setActiveDot(undefined); + setActiveLegend(undefined); + onValueChange?.(null); } : undefined } @@ -634,7 +680,10 @@ const LineChart = React.forwardRef( tick={{ transform: "translate(0, 6)" }} ticks={ startEndOnly - ? ([(data[0] as any)[index], (data[data.length - 1] as any)[index]] as any) + ? ([ + (data[0] as any)[index], + (data[data.length - 1] as any)[index], + ] as any) : undefined } fill="" @@ -699,26 +748,28 @@ const LineChart = React.forwardRef( position={{ y: 0 }} content={({ active, payload, label }: any) => { const cleanPayload: TooltipProps["payload"] = payload - ? payload.map((item: any) => ({ - category: item.dataKey, - value: item.value, - index: item.payload[index], - color: categoryColors.get( - item.dataKey, - ) as AvailableChartColorsKeys, - type: item.type, - payload: item.payload, - })) - : [] + ? payload.map((item: any) => { + return { + category: item.dataKey, + value: item.value, + index: item.payload[index], + color: categoryColors.get( + item.dataKey, + ) as AvailableChartColorsKeys, + type: item.type, + payload: item.payload, + }; + }) + : []; if ( tooltipCallback && (active !== prevActiveRef.current || label !== prevLabelRef.current) ) { - tooltipCallback({ active, payload: cleanPayload, label }) - prevActiveRef.current = active - prevLabelRef.current = label + tooltipCallback({ active, payload: cleanPayload, label }); + prevActiveRef.current = active; + prevLabelRef.current = label; } return showTooltip && active ? ( @@ -736,7 +787,7 @@ const LineChart = React.forwardRef( valueFormatter={valueFormatter} /> ) - ) : null + ) : null; }} /> @@ -744,102 +795,50 @@ const LineChart = React.forwardRef( - ChartLegend( + content={({ payload }: any) => { + return ChartLegend( { payload }, categoryColors, setLegendHeight, activeLegend, hasOnValueChange - ? (clickedLegendItem: string) => - onCategoryClick(clickedLegendItem) + ? (clickedLegendItem: string) => { + return onCategoryClick(clickedLegendItem); + } : undefined, enableLegendSlider, legendPosition, yAxisWidth, - ) - } + ); + }} /> ) : null} - {categories.map((category) => ( - { - const { - cx: cxCoord, - cy: cyCoord, - stroke, - strokeLinecap, - strokeLinejoin, - strokeWidth, - dataKey, - } = props - return ( - onDotClick(props, event)} - /> - ) - }} - dot={(props: any) => { - const { - stroke, - strokeLinecap, - strokeLinejoin, - strokeWidth, - cx: cxCoord, - cy: cyCoord, - dataKey, - index, - } = props - - if ( - (hasOnlyOneValueForKey(data, category) && - !( - activeDot || - (activeLegend && activeLegend !== category) - )) || - (activeDot?.index === index && - activeDot?.dataKey === category) - ) { + {categories.map((category) => { + return ( + { + const { + cx: cxCoord, + cy: cyCoord, + stroke, + strokeLinecap, + strokeLinejoin, + strokeWidth, + dataKey, + } = props; return ( ( "fill", ), )} + cx={cxCoord} + cy={cyCoord} + r={5} + fill="" + stroke={stroke} + strokeLinecap={strokeLinecap} + strokeLinejoin={strokeLinejoin} + strokeWidth={strokeWidth} + onClick={(_: any, event: any) => { + return onDotClick(props, event); + }} /> - ) - } - return - }} - key={category} - name={category} - type="linear" - dataKey={category} - stroke="" - strokeWidth={2} - strokeLinejoin="round" - strokeLinecap="round" - isAnimationActive={false} - connectNulls={connectNulls} - /> - ))} + ); + }} + dot={(props: any) => { + const { + stroke, + strokeLinecap, + strokeLinejoin, + strokeWidth, + cx: cxCoord, + cy: cyCoord, + dataKey, + index, + } = props; + + if ( + (hasOnlyOneValueForKey(data, category) && + !( + activeDot || + (activeLegend && activeLegend !== category) + )) || + (activeDot?.index === index && + activeDot?.dataKey === category) + ) { + return ( + + ); + } + return ; + }} + key={category} + name={category} + type="linear" + dataKey={category} + stroke="" + strokeWidth={2} + strokeLinejoin="round" + strokeLinecap="round" + isAnimationActive={false} + connectNulls={connectNulls} + /> + ); + })} {/* hidden lines to increase clickable target area */} {onValueChange - ? categories.map((category) => ( - { - event.stopPropagation() - const { name } = props - onCategoryClick(name) - }} - /> - )) + ? categories.map((category) => { + return ( + { + event.stopPropagation(); + const { name } = props; + onCategoryClick(name); + }} + /> + ); + }) : null} - ) + ); }, -) +); -LineChart.displayName = "LineChart" +LineChart.displayName = "LineChart"; -export { LineChart, type LineChartEventProps, type TooltipProps } \ No newline at end of file +export { LineChart, type LineChartEventProps, type TooltipProps }; diff --git a/Common/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts b/Common/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts index 737feb8fe4..7fbff12928 100644 --- a/Common/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +++ b/Common/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts @@ -1,3 +1,3 @@ -export default interface ChartDataPoint { - [x: string]: number | string; -} \ No newline at end of file +export default interface ChartDataPoint { + [x: string]: number | string; +} diff --git a/Common/UI/Components/Charts/ChartLibrary/Utils/ChartColors.ts b/Common/UI/Components/Charts/ChartLibrary/Utils/ChartColors.ts index 1b849b4138..dce18dce69 100644 --- a/Common/UI/Components/Charts/ChartLibrary/Utils/ChartColors.ts +++ b/Common/UI/Components/Charts/ChartLibrary/Utils/ChartColors.ts @@ -1,8 +1,12 @@ // Tremor Raw chartColors [v0.1.0] -export type ColorUtility = "bg" | "stroke" | "fill" | "text" +export type ColorUtility = "bg" | "stroke" | "fill" | "text"; -export const chartColors = { +export const chartColors: { + [color: string]: { + [key in ColorUtility]: string; + }; +} = { blue: { bg: "bg-blue-500", stroke: "stroke-blue-500", @@ -69,28 +73,24 @@ export const chartColors = { fill: "fill-rose-500", text: "text-rose-500", }, -} as const satisfies { - [color: string]: { - [key in ColorUtility]: string - } -} +}; -export type AvailableChartColorsKeys = keyof typeof chartColors +export type AvailableChartColorsKeys = keyof typeof chartColors; export const AvailableChartColors: AvailableChartColorsKeys[] = Object.keys( chartColors, -) as Array +) as Array; export const constructCategoryColors = ( categories: string[], colors: AvailableChartColorsKeys[], ): Map => { - const categoryColors = new Map() + const categoryColors = new Map(); categories.forEach((category, index) => { - categoryColors.set(category, colors[index % colors.length]!) - }) - return categoryColors -} + categoryColors.set(category, colors[index % colors.length]!); + }); + return categoryColors; +}; export const getColorClassName = ( color: AvailableChartColorsKeys, @@ -101,6 +101,6 @@ export const getColorClassName = ( stroke: "stroke-gray-500", fill: "fill-gray-500", text: "text-gray-500", - } - return chartColors[color]?.[type] ?? fallbackColor[type] -} \ No newline at end of file + }; + return chartColors[color]?.[type] ?? fallbackColor[type]; +}; diff --git a/Common/UI/Components/Charts/ChartLibrary/Utils/Cx.ts b/Common/UI/Components/Charts/ChartLibrary/Utils/Cx.ts index 4003f49828..d1bea6ed1c 100644 --- a/Common/UI/Components/Charts/ChartLibrary/Utils/Cx.ts +++ b/Common/UI/Components/Charts/ChartLibrary/Utils/Cx.ts @@ -1,8 +1,8 @@ // Tremor Raw cx [v0.0.0] -import clsx, { type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import clsx, { type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; -export function cx(...args: ClassValue[]) { - return twMerge(clsx(...args)) -} \ No newline at end of file +export function cx(...args: ClassValue[]): string { + return twMerge(clsx(...args)); +} diff --git a/Common/UI/Components/Charts/ChartLibrary/Utils/GetYAxisDomain.ts b/Common/UI/Components/Charts/ChartLibrary/Utils/GetYAxisDomain.ts index 29fa274a10..36bd188b37 100644 --- a/Common/UI/Components/Charts/ChartLibrary/Utils/GetYAxisDomain.ts +++ b/Common/UI/Components/Charts/ChartLibrary/Utils/GetYAxisDomain.ts @@ -1,11 +1,11 @@ // Tremor Raw getYAxisDomain [v0.0.0] -export const getYAxisDomain = ( - autoMinValue: boolean, - minValue: number | undefined, - maxValue: number | undefined, - ) => { - const minDomain = autoMinValue ? "auto" : (minValue ?? 0) - const maxDomain = maxValue ?? "auto" - return [minDomain, maxDomain] - } \ No newline at end of file +export const getYAxisDomain: (autoMinValue: boolean, minValue: number | undefined, maxValue: number | undefined) => (number | "auto")[] = ( + autoMinValue: boolean, + minValue: number | undefined, + maxValue: number | undefined, +): (number | "auto")[] => { + const minDomain: number | "auto" = autoMinValue ? "auto" : minValue ?? 0; + const maxDomain: number | "auto" = maxValue ?? "auto"; + return [minDomain, maxDomain]; +}; diff --git a/Common/UI/Components/Charts/ChartLibrary/Utils/HasOnlyOneValueForKey.ts b/Common/UI/Components/Charts/ChartLibrary/Utils/HasOnlyOneValueForKey.ts index 0397554f21..017a386899 100644 --- a/Common/UI/Components/Charts/ChartLibrary/Utils/HasOnlyOneValueForKey.ts +++ b/Common/UI/Components/Charts/ChartLibrary/Utils/HasOnlyOneValueForKey.ts @@ -1,19 +1,19 @@ // Tremor Raw hasOnlyOneValueForKey [v0.1.0] export function hasOnlyOneValueForKey( - array: any[], - keyToCheck: string, - ): boolean { - const val: any[] = [] - - for (const obj of array) { - if (Object.prototype.hasOwnProperty.call(obj, keyToCheck)) { - val.push(obj[keyToCheck]) - if (val.length > 1) { - return false - } + array: any[], + keyToCheck: string, +): boolean { + const val: any[] = []; + + for (const obj of array) { + if (Object.prototype.hasOwnProperty.call(obj, keyToCheck)) { + val.push(obj[keyToCheck]); + if (val.length > 1) { + return false; } } - - return true - } \ No newline at end of file + } + + return true; +} diff --git a/Common/UI/Components/Charts/ChartLibrary/Utils/UseWindowOnResize.ts b/Common/UI/Components/Charts/ChartLibrary/Utils/UseWindowOnResize.ts index 2d6fb5e9fd..90e4edbc33 100644 --- a/Common/UI/Components/Charts/ChartLibrary/Utils/UseWindowOnResize.ts +++ b/Common/UI/Components/Charts/ChartLibrary/Utils/UseWindowOnResize.ts @@ -1,15 +1,15 @@ -// Tremor Raw useOnWindowResize [v0.0.0] +import * as React from "react"; -import * as React from "react" - -export const useOnWindowResize = (handler: { (): void }) => { +export const useOnWindowResize: (handler: () => void) => void = (handler: () => void): void => { React.useEffect(() => { - const handleResize = () => { - handler() - } - handleResize() - window.addEventListener("resize", handleResize) + const handleResize: () => void = () => { + handler(); + }; + handleResize(); + window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize) - }, [handler]) -} \ No newline at end of file + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [handler]); +}; diff --git a/Common/UI/Components/Charts/Line/LineChart.tsx b/Common/UI/Components/Charts/Line/LineChart.tsx index 5065dee20b..e84fd1e5d9 100644 --- a/Common/UI/Components/Charts/Line/LineChart.tsx +++ b/Common/UI/Components/Charts/Line/LineChart.tsx @@ -7,7 +7,6 @@ import ChartCurve from "../Types/ChartCurve"; import ChartDataPoint from "../ChartLibrary/Types/ChartDataPoint"; import DataPointUtil from "../Utils/DataPoint"; - export interface ComponentProps { data: Array; xAxis: XAxis; @@ -19,6 +18,9 @@ export interface ComponentProps { const LineChartElement: FunctionComponent = ( props: ComponentProps, ): ReactElement => { + if (!props.sync) { + return <>; + } const [records, setRecords] = React.useState>([]); @@ -27,7 +29,6 @@ const LineChartElement: FunctionComponent = ( }); useEffect(() => { - if (!props.data || props.data.length === 0) { setRecords([]); } @@ -35,14 +36,12 @@ const LineChartElement: FunctionComponent = ( const records: Array = DataPointUtil.getChartDataPoints({ seriesPoints: props.data, xAxis: props.xAxis, + yAxis: props.yAxis, }); setRecords(records); - }, [props.data]); - - return ( = ( tickGap={1} index={"Time"} categories={categories} - colors={["indigo", "rose", "emerald", "amber", "cyan", "gray", "pink", "lime", "fuchsia"]} + colors={[ + "indigo", + "rose", + "emerald", + "amber", + "cyan", + "gray", + "pink", + "lime", + "fuchsia", + ]} valueFormatter={props.yAxis.options.formatter || undefined} showTooltip={true} connectNulls={true} yAxisWidth={60} - onValueChange={(v) => { - return console.log(v); - }} /> ); }; diff --git a/Common/UI/Components/Charts/Types/XAxis/XAxis.ts b/Common/UI/Components/Charts/Types/XAxis/XAxis.ts index a4be3397e9..8fb3cd9150 100644 --- a/Common/UI/Components/Charts/Types/XAxis/XAxis.ts +++ b/Common/UI/Components/Charts/Types/XAxis/XAxis.ts @@ -2,7 +2,7 @@ import XAxisMaxMin from "./XAxisMaxMin"; import XAxisType from "./XAxisType"; export enum XAxisAggregateType { - Average = "Average", + Average = "Average", Sum = "Sum", Max = "Max", Min = "Min", diff --git a/Common/UI/Components/Charts/Types/YAxis/YAxis.ts b/Common/UI/Components/Charts/Types/YAxis/YAxis.ts index ec8a8db873..594fe8fb76 100644 --- a/Common/UI/Components/Charts/Types/YAxis/YAxis.ts +++ b/Common/UI/Components/Charts/Types/YAxis/YAxis.ts @@ -13,7 +13,7 @@ export interface YAxisOptions { min: YAxisMaxMin; max: YAxisMaxMin; formatter: (value: number) => string; - precision: + precision: YAxisPrecision; } export default interface YAxis { diff --git a/Common/UI/Components/Charts/Utils/DataPoint.ts b/Common/UI/Components/Charts/Utils/DataPoint.ts index 02d9f20cf6..c5d161d989 100644 --- a/Common/UI/Components/Charts/Utils/DataPoint.ts +++ b/Common/UI/Components/Charts/Utils/DataPoint.ts @@ -1,6 +1,4 @@ - - -/// ChartDataPoint is in the format of: +/// ChartDataPoint is in the format of: // { // date: "Feb 22", // SolarPanels: 2756, @@ -15,109 +13,176 @@ import XAxisMaxMin from "../Types/XAxis/XAxisMaxMin"; import YAxis, { YAxisPrecision } from "../Types/YAxis/YAxis"; import XAxisUtil from "./XAxis"; - +interface SeriesData { + sum: number; + count: number; + max: number; + min: number; +} export default class DataPointUtil { - public static getChartDataPoints(data: { - seriesPoints: Array; - xAxis: XAxis; - yAxis: YAxis; - }): Array { + public static getChartDataPoints(data: { + seriesPoints: Array; + xAxis: XAxis; + yAxis: YAxis; + }): Array { + const { xAxisLegend, intervals, formatter } = this.initializeXAxisData( + data.xAxis, + ); + const arrayOfData: ChartDataPoint[] = this.initializeArrayOfData( + intervals, + xAxisLegend, + formatter, + ); + const seriesDataMap: { + [key: string]: SeriesData; + } = this.processSeriesData( + data.seriesPoints, + arrayOfData, + xAxisLegend, + formatter, + data.xAxis.options.aggregateType, + ); + this.formatSeriesData( + arrayOfData, + seriesDataMap, + data.yAxis.options.precision, + ); + return arrayOfData; + } - const xAxisMax: XAxisMaxMin = data.xAxis.options.max; - const xAxisMin: XAxisMaxMin = data.xAxis.options.min; - - const xAxisLegend: string = data.xAxis.legend; - - const intervals: Array = XAxisUtil.getPrecisionIntervals({ - xAxisMax: xAxisMax, - xAxisMin: xAxisMin - }); - - const formatter: (value: Date) => string = XAxisUtil.getFormatter({ - xAxisMax: xAxisMax, - xAxisMin: xAxisMin - }); - - const arrayOfData: Array = []; - - // format all the intervals. - for (const interval of intervals) { - const dataPoint: ChartDataPoint = {}; - dataPoint[xAxisLegend] = formatter(interval); - arrayOfData.push(dataPoint); - } - - interface SeriesData { - sum: number; - count: number; - max: number; - min: number; - } - - // Initialize a new data structure to store sum, count, max, and min for each series - const seriesDataMap: { [key: string]: SeriesData } = {}; - - // now we need to add the data points. - for (const series of data.seriesPoints) { - for (const dataPoint of series.data) { - const date: Date = dataPoint.x; - const value: number = dataPoint.y; - const formattedDate: string = formatter(date); - - for (const chartDataPoint of arrayOfData) { - if (chartDataPoint[xAxisLegend] === formattedDate) { - // Initialize series data if it doesn't exist - if (!seriesDataMap[series.seriesName]) { - seriesDataMap[series.seriesName] = { sum: 0, count: 0, max: Number.NEGATIVE_INFINITY, min: Number.POSITIVE_INFINITY }; - } - - // Update sum, count, max, and min - seriesDataMap[series.seriesName]!.sum += value; - seriesDataMap[series.seriesName]!.count += 1; - seriesDataMap[series.seriesName]!.max = Math.max(seriesDataMap[series.seriesName]!.max, value); - seriesDataMap[series.seriesName]!.min = Math.min(seriesDataMap[series.seriesName]!.min, value); - - // Calculate the average, sum, max, or min based on the aggregate type - if (data.xAxis.options.aggregateType === XAxisAggregateType.Average) { - chartDataPoint[series.seriesName] = seriesDataMap[series.seriesName]!.sum / seriesDataMap[series.seriesName]!.count; - } else if (data.xAxis.options.aggregateType === XAxisAggregateType.Sum) { - chartDataPoint[series.seriesName] = seriesDataMap[series.seriesName]!.sum; - } else if (data.xAxis.options.aggregateType === XAxisAggregateType.Max) { - chartDataPoint[series.seriesName] = seriesDataMap[series.seriesName]!.max; - } else if (data.xAxis.options.aggregateType === XAxisAggregateType.Min) { - chartDataPoint[series.seriesName] = seriesDataMap[series.seriesName]!.min; - } else { - throw new BadDataException("Aggregate type not supported."); - } - - if (chartDataPoint[series.seriesName] && typeof chartDataPoint[series.seriesName] === "number") { - - // Format the series data based on yAxis precision - const yAxisPrecision = data.yAxis.options.precision; - switch (yAxisPrecision) { - case YAxisPrecision.NoDecimals: - chartDataPoint[series.seriesName] = parseFloat((chartDataPoint[series.seriesName]! as number).toFixed(0)); - break; - case YAxisPrecision.OneDecimal: - chartDataPoint[series.seriesName] = parseFloat(((chartDataPoint[series.seriesName]! as number).toFixed(1))); - break; - case YAxisPrecision.TwoDecimals: - chartDataPoint[series.seriesName] = parseFloat(((chartDataPoint[series.seriesName]! as number).toFixed(2))); - break; - case YAxisPrecision.ThreeDecimals: - chartDataPoint[series.seriesName] = parseFloat(((chartDataPoint[series.seriesName]! as number).toFixed(3))) - break; - default: - throw new BadDataException("YAxis precision not supported."); - } - } - } - } - } - } - - return arrayOfData; + private static initializeXAxisData(xAxis: XAxis): { + xAxisMax: XAxisMaxMin; + xAxisMin: XAxisMaxMin; + xAxisLegend: string; + intervals: Array; + formatter: (value: Date) => string; + } { + const xAxisMax: XAxisMaxMin = xAxis.options.max; + const xAxisMin: XAxisMaxMin = xAxis.options.min; + const xAxisLegend: string = xAxis.legend; + const intervals: Array = XAxisUtil.getPrecisionIntervals({ + xAxisMax, + xAxisMin, + }); + const formatter: (value: Date) => string = XAxisUtil.getFormatter({ + xAxisMax, + xAxisMin, + }); + return { xAxisMax, xAxisMin, xAxisLegend, intervals, formatter }; + } + private static initializeArrayOfData( + intervals: Array, + xAxisLegend: string, + formatter: (value: Date) => string, + ): Array { + const arrayOfData: Array = []; + for (const interval of intervals) { + const dataPoint: ChartDataPoint = {}; + dataPoint[xAxisLegend] = formatter(interval); + arrayOfData.push(dataPoint); } -} \ No newline at end of file + return arrayOfData; + } + + private static processSeriesData( + seriesPoints: Array, + arrayOfData: Array, + xAxisLegend: string, + formatter: (value: Date) => string, + aggregateType: XAxisAggregateType, + ): { [key: string]: SeriesData } { + const seriesDataMap: { [key: string]: SeriesData } = {}; + for (const series of seriesPoints) { + for (const dataPoint of series.data) { + const date: Date = dataPoint.x; + const value: number = dataPoint.y; + const formattedDate: string = formatter(date); + for (const chartDataPoint of arrayOfData) { + if (chartDataPoint[xAxisLegend] === formattedDate) { + if (!seriesDataMap[series.seriesName]) { + seriesDataMap[series.seriesName] = { + sum: 0, + count: 0, + max: Number.NEGATIVE_INFINITY, + min: Number.POSITIVE_INFINITY, + }; + } + seriesDataMap[series.seriesName]!.sum += value; + seriesDataMap[series.seriesName]!.count += 1; + seriesDataMap[series.seriesName]!.max = Math.max( + seriesDataMap[series.seriesName]!.max, + value, + ); + seriesDataMap[series.seriesName]!.min = Math.min( + seriesDataMap[series.seriesName]!.min, + value, + ); + chartDataPoint[series.seriesName] = this.calculateAggregate( + seriesDataMap[series.seriesName]!, + aggregateType, + ); + } + } + } + } + return seriesDataMap; + } + + private static calculateAggregate( + seriesData: SeriesData, + aggregateType: XAxisAggregateType, + ): number { + switch (aggregateType) { + case XAxisAggregateType.Average: + return seriesData.sum / seriesData.count; + case XAxisAggregateType.Sum: + return seriesData.sum; + case XAxisAggregateType.Max: + return seriesData.max; + case XAxisAggregateType.Min: + return seriesData.min; + default: + throw new BadDataException("Aggregate type not supported."); + } + } + + private static formatSeriesData( + arrayOfData: Array, + seriesDataMap: { [key: string]: SeriesData }, + yAxisPrecision: YAxisPrecision, + ): void { + for (const chartDataPoint of arrayOfData) { + for (const seriesName in seriesDataMap) { + if ( + chartDataPoint[seriesName] && + typeof chartDataPoint[seriesName] === "number" + ) { + chartDataPoint[seriesName] = this.formatValue( + chartDataPoint[seriesName] as number, + yAxisPrecision, + ); + } + } + } + } + + private static formatValue( + value: number, + yAxisPrecision: YAxisPrecision, + ): number { + switch (yAxisPrecision) { + case YAxisPrecision.NoDecimals: + return parseFloat(value.toFixed(0)); + case YAxisPrecision.OneDecimal: + return parseFloat(value.toFixed(1)); + case YAxisPrecision.TwoDecimals: + return parseFloat(value.toFixed(2)); + case YAxisPrecision.ThreeDecimals: + return parseFloat(value.toFixed(3)); + default: + throw new BadDataException("YAxis precision not supported."); + } + } +} diff --git a/Common/UI/Components/Charts/Utils/XAxis.ts b/Common/UI/Components/Charts/Utils/XAxis.ts index 4cbb661230..98194c6135 100644 --- a/Common/UI/Components/Charts/Utils/XAxis.ts +++ b/Common/UI/Components/Charts/Utils/XAxis.ts @@ -3,207 +3,265 @@ import XAxisMaxMin from "../Types/XAxis/XAxisMaxMin"; import XAxisPrecision from "../Types/XAxis/XAxisPrecision"; export default class XAxisUtil { - public static getPrecision(data: { - xAxisMin: XAxisMaxMin, - xAxisMax: XAxisMaxMin, - }): XAxisPrecision { - - if (typeof data.xAxisMax === "number" || typeof data.xAxisMin === "number") { - // number not yet supported. - throw new NotImplementedException(); - } - - const startDate: Date = data.xAxisMin; - const endDate: Date = data.xAxisMax; - - const totalMilliseconds = endDate.getTime() - startDate.getTime(); - const totalSeconds = totalMilliseconds / 1000; - const totalMinutes = totalSeconds / 60; - const totalHours = totalMinutes / 60; - const totalDays = totalHours / 24; - const totalWeeks = totalDays / 7; - const totalMonths = totalDays / 30; - - - if (totalSeconds <= 50) return XAxisPrecision.EVERY_SECOND; - if (totalSeconds <= 250) return XAxisPrecision.EVERY_FIVE_SECONDS; - if (totalSeconds <= 500) return XAxisPrecision.EVERY_TEN_SECONDS; - if (totalSeconds <= 1500) return XAxisPrecision.EVERY_THIRTY_SECONDS; - if (totalMinutes <= 50) return XAxisPrecision.EVERY_MINUTE; - if (totalMinutes <= 250) return XAxisPrecision.EVERY_FIVE_MINUTES; - if (totalMinutes <= 500) return XAxisPrecision.EVERY_TEN_MINUTES; - if (totalMinutes <= 1500) return XAxisPrecision.EVERY_THIRTY_MINUTES; - if (totalHours <= 50) return XAxisPrecision.EVERY_HOUR; - if (totalHours <= 100) return XAxisPrecision.EVERY_TWO_HOURS; - if (totalHours <= 150) return XAxisPrecision.EVERY_THREE_HOURS; - if (totalHours <= 300) return XAxisPrecision.EVERY_SIX_HOURS; - if (totalHours <= 600) return XAxisPrecision.EVERY_TWELVE_HOURS; - if (totalDays <= 50) return XAxisPrecision.EVERY_DAY; - if (totalDays <= 100) return XAxisPrecision.EVERY_TWO_DAYS; - if (totalWeeks <= 50) return XAxisPrecision.EVERY_WEEK; - if (totalWeeks <= 100) return XAxisPrecision.EVERY_TWO_WEEKS; - if (totalMonths <= 50) return XAxisPrecision.EVERY_MONTH; - if (totalMonths <= 100) return XAxisPrecision.EVERY_TWO_MONTHS; - if (totalMonths <= 150) return XAxisPrecision.EVERY_THREE_MONTHS; - if (totalMonths <= 300) return XAxisPrecision.EVERY_SIX_MONTHS; - return XAxisPrecision.EVERY_YEAR; - + public static getPrecision(data: { + xAxisMin: XAxisMaxMin; + xAxisMax: XAxisMaxMin; + }): XAxisPrecision { + if ( + typeof data.xAxisMax === "number" || + typeof data.xAxisMin === "number" + ) { + // number not yet supported. + throw new NotImplementedException(); } - public static getPrecisionIntervals(data: { - xAxisMin: XAxisMaxMin, - xAxisMax: XAxisMaxMin, - }): Array { - const precision: XAxisPrecision = XAxisUtil.getPrecision(data); + const startDate: Date = data.xAxisMin as Date; + const endDate: Date = data.xAxisMax as Date; - if (typeof data.xAxisMax === "number" || typeof data.xAxisMin === "number") { - // number not yet supported. - throw new NotImplementedException(); - } + const totalMilliseconds: number = endDate.getTime() - startDate.getTime(); + const totalSeconds: number = totalMilliseconds / 1000; + const totalMinutes: number = totalSeconds / 60; + const totalHours: number = totalMinutes / 60; + const totalDays: number = totalHours / 24; + const totalWeeks: number = totalDays / 7; + const totalMonths: number = totalDays / 30; - const startDate: Date = new Date(data.xAxisMin); - const endDate: Date = new Date(data.xAxisMax); - const intervals: Array = []; + if (totalSeconds <= 100) { + return XAxisPrecision.EVERY_SECOND; + } + if (totalSeconds <= 500) { + return XAxisPrecision.EVERY_FIVE_SECONDS; + } + if (totalSeconds <= 1000) { + return XAxisPrecision.EVERY_TEN_SECONDS; + } + if (totalSeconds <= 3000) { + return XAxisPrecision.EVERY_THIRTY_SECONDS; + } + if (totalMinutes <= 100) { + return XAxisPrecision.EVERY_MINUTE; + } + if (totalMinutes <= 500) { + return XAxisPrecision.EVERY_FIVE_MINUTES; + } + if (totalMinutes <= 1000) { + return XAxisPrecision.EVERY_TEN_MINUTES; + } + if (totalMinutes <= 3000) { + return XAxisPrecision.EVERY_THIRTY_MINUTES; + } + if (totalHours <= 100) { + return XAxisPrecision.EVERY_HOUR; + } + if (totalHours <= 200) { + return XAxisPrecision.EVERY_TWO_HOURS; + } + if (totalHours <= 300) { + return XAxisPrecision.EVERY_THREE_HOURS; + } + if (totalHours <= 600) { + return XAxisPrecision.EVERY_SIX_HOURS; + } + if (totalHours <= 1200) { + return XAxisPrecision.EVERY_TWELVE_HOURS; + } + if (totalDays <= 100) { + return XAxisPrecision.EVERY_DAY; + } + if (totalDays <= 200) { + return XAxisPrecision.EVERY_TWO_DAYS; + } + if (totalWeeks <= 100) { + return XAxisPrecision.EVERY_WEEK; + } + if (totalWeeks <= 200) { + return XAxisPrecision.EVERY_TWO_WEEKS; + } + if (totalMonths <= 100) { + return XAxisPrecision.EVERY_MONTH; + } + if (totalMonths <= 200) { + return XAxisPrecision.EVERY_TWO_MONTHS; + } + if (totalMonths <= 300) { + return XAxisPrecision.EVERY_THREE_MONTHS; + } + if (totalMonths <= 600) { + return XAxisPrecision.EVERY_SIX_MONTHS; + } + return XAxisPrecision.EVERY_YEAR; + } - let currentDate = startDate; + public static getPrecisionIntervals(data: { + xAxisMin: XAxisMaxMin; + xAxisMax: XAxisMaxMin; + }): Array { + const precision: XAxisPrecision = XAxisUtil.getPrecision(data); - while (currentDate <= endDate) { - intervals.push(new Date(currentDate)); - - switch (precision) { - case XAxisPrecision.EVERY_SECOND: - currentDate.setSeconds(currentDate.getSeconds() + 1); - break; - case XAxisPrecision.EVERY_FIVE_SECONDS: - currentDate.setSeconds(currentDate.getSeconds() + 5); - break; - case XAxisPrecision.EVERY_TEN_SECONDS: - currentDate.setSeconds(currentDate.getSeconds() + 10); - break; - case XAxisPrecision.EVERY_THIRTY_SECONDS: - currentDate.setSeconds(currentDate.getSeconds() + 30); - break; - case XAxisPrecision.EVERY_MINUTE: - currentDate.setMinutes(currentDate.getMinutes() + 1); - break; - case XAxisPrecision.EVERY_FIVE_MINUTES: - currentDate.setMinutes(currentDate.getMinutes() + 5); - break; - case XAxisPrecision.EVERY_TEN_MINUTES: - currentDate.setMinutes(currentDate.getMinutes() + 10); - break; - case XAxisPrecision.EVERY_THIRTY_MINUTES: - currentDate.setMinutes(currentDate.getMinutes() + 30); - break; - case XAxisPrecision.EVERY_HOUR: - currentDate.setHours(currentDate.getHours() + 1); - break; - case XAxisPrecision.EVERY_TWO_HOURS: - currentDate.setHours(currentDate.getHours() + 2); - break; - case XAxisPrecision.EVERY_THREE_HOURS: - currentDate.setHours(currentDate.getHours() + 3); - break; - case XAxisPrecision.EVERY_SIX_HOURS: - currentDate.setHours(currentDate.getHours() + 6); - break; - case XAxisPrecision.EVERY_TWELVE_HOURS: - currentDate.setHours(currentDate.getHours() + 12); - break; - case XAxisPrecision.EVERY_DAY: - currentDate.setDate(currentDate.getDate() + 1); - break; - case XAxisPrecision.EVERY_TWO_DAYS: - currentDate.setDate(currentDate.getDate() + 2); - break; - case XAxisPrecision.EVERY_WEEK: - currentDate.setDate(currentDate.getDate() + 7); - break; - case XAxisPrecision.EVERY_TWO_WEEKS: - currentDate.setDate(currentDate.getDate() + 14); - break; - case XAxisPrecision.EVERY_MONTH: - currentDate.setMonth(currentDate.getMonth() + 1); - break; - case XAxisPrecision.EVERY_TWO_MONTHS: - currentDate.setMonth(currentDate.getMonth() + 2); - break; - case XAxisPrecision.EVERY_THREE_MONTHS: - currentDate.setMonth(currentDate.getMonth() + 3); - break; - case XAxisPrecision.EVERY_SIX_MONTHS: - currentDate.setMonth(currentDate.getMonth() + 6); - break; - case XAxisPrecision.EVERY_YEAR: - currentDate.setFullYear(currentDate.getFullYear() + 1); - break; - } - } - - return intervals; + if ( + typeof data.xAxisMax === "number" || + typeof data.xAxisMin === "number" + ) { + // number not yet supported. + throw new NotImplementedException(); } - public static getFormatter(data: { - xAxisMin: XAxisMaxMin, - xAxisMax: XAxisMaxMin, - }): (value: Date) => string { + const startDate: Date = new Date(data.xAxisMin as Date); + const endDate: Date = new Date(data.xAxisMax as Date); + const intervals: Array = []; - const precision: XAxisPrecision = XAxisUtil.getPrecision(data); + const currentDate: Date = new Date(startDate); - switch (precision) { - case XAxisPrecision.EVERY_SECOND: - case XAxisPrecision.EVERY_FIVE_SECONDS: - case XAxisPrecision.EVERY_TEN_SECONDS: - case XAxisPrecision.EVERY_THIRTY_SECONDS: - return (value: Date) => value.toISOString().substring(11, 19); // HH:mm:ss - case XAxisPrecision.EVERY_MINUTE: - case XAxisPrecision.EVERY_FIVE_MINUTES: - case XAxisPrecision.EVERY_TEN_MINUTES: - case XAxisPrecision.EVERY_THIRTY_MINUTES: - return (value: Date) => value.toISOString().substring(11, 16); // HH:mm - case XAxisPrecision.EVERY_HOUR: - case XAxisPrecision.EVERY_TWO_HOURS: - case XAxisPrecision.EVERY_THREE_HOURS: - case XAxisPrecision.EVERY_SIX_HOURS: - case XAxisPrecision.EVERY_TWELVE_HOURS: - return (value: Date) => { - const dateString = value.toISOString(); - const day = dateString.substring(8, 10); - const month = value.toLocaleString('default', { month: 'short' }); - const hour = dateString.substring(11, 13); - return `${day} ${month}, ${hour}:00`; - }; // DD MMM, HH:00 - case XAxisPrecision.EVERY_DAY: - case XAxisPrecision.EVERY_TWO_DAYS: - return (value: Date) => { - const dateString = value.toISOString(); - const day = dateString.substring(8, 10); - const month = value.toLocaleString('default', { month: 'short' }); - return `${day} ${month}`; - }; // DD MMM - case XAxisPrecision.EVERY_WEEK: - case XAxisPrecision.EVERY_TWO_WEEKS: - return (value: Date) => { - const dateString = value.toISOString(); - const day = dateString.substring(8, 10); - const month = value.toLocaleString('default', { month: 'short' }); - return `${day} ${month}`; - }; // DD MMM - case XAxisPrecision.EVERY_MONTH: - case XAxisPrecision.EVERY_TWO_MONTHS: - case XAxisPrecision.EVERY_THREE_MONTHS: - case XAxisPrecision.EVERY_SIX_MONTHS: - return (value: Date) => { - const dateString = value.toISOString(); - const day = dateString.substring(8, 10); - const year = dateString.substring(0, 4); - const month = value.toLocaleString('default', { month: 'short' }); - return `${day} ${month} ${year}`; - }; // DD MMM - case XAxisPrecision.EVERY_YEAR: - return (value: Date) => value.toISOString().substring(0, 4); // YYYY - default: - throw new Error("Unsupported precision"); - } + while (currentDate <= endDate) { + intervals.push(new Date(currentDate)); + + switch (precision) { + case XAxisPrecision.EVERY_SECOND: + currentDate.setSeconds(currentDate.getSeconds() + 1); + break; + case XAxisPrecision.EVERY_FIVE_SECONDS: + currentDate.setSeconds(currentDate.getSeconds() + 5); + break; + case XAxisPrecision.EVERY_TEN_SECONDS: + currentDate.setSeconds(currentDate.getSeconds() + 10); + break; + case XAxisPrecision.EVERY_THIRTY_SECONDS: + currentDate.setSeconds(currentDate.getSeconds() + 30); + break; + case XAxisPrecision.EVERY_MINUTE: + currentDate.setMinutes(currentDate.getMinutes() + 1); + break; + case XAxisPrecision.EVERY_FIVE_MINUTES: + currentDate.setMinutes(currentDate.getMinutes() + 5); + break; + case XAxisPrecision.EVERY_TEN_MINUTES: + currentDate.setMinutes(currentDate.getMinutes() + 10); + break; + case XAxisPrecision.EVERY_THIRTY_MINUTES: + currentDate.setMinutes(currentDate.getMinutes() + 30); + break; + case XAxisPrecision.EVERY_HOUR: + currentDate.setHours(currentDate.getHours() + 1); + break; + case XAxisPrecision.EVERY_TWO_HOURS: + currentDate.setHours(currentDate.getHours() + 2); + break; + case XAxisPrecision.EVERY_THREE_HOURS: + currentDate.setHours(currentDate.getHours() + 3); + break; + case XAxisPrecision.EVERY_SIX_HOURS: + currentDate.setHours(currentDate.getHours() + 6); + break; + case XAxisPrecision.EVERY_TWELVE_HOURS: + currentDate.setHours(currentDate.getHours() + 12); + break; + case XAxisPrecision.EVERY_DAY: + currentDate.setDate(currentDate.getDate() + 1); + break; + case XAxisPrecision.EVERY_TWO_DAYS: + currentDate.setDate(currentDate.getDate() + 2); + break; + case XAxisPrecision.EVERY_WEEK: + currentDate.setDate(currentDate.getDate() + 7); + break; + case XAxisPrecision.EVERY_TWO_WEEKS: + currentDate.setDate(currentDate.getDate() + 14); + break; + case XAxisPrecision.EVERY_MONTH: + currentDate.setMonth(currentDate.getMonth() + 1); + break; + case XAxisPrecision.EVERY_TWO_MONTHS: + currentDate.setMonth(currentDate.getMonth() + 2); + break; + case XAxisPrecision.EVERY_THREE_MONTHS: + currentDate.setMonth(currentDate.getMonth() + 3); + break; + case XAxisPrecision.EVERY_SIX_MONTHS: + currentDate.setMonth(currentDate.getMonth() + 6); + break; + case XAxisPrecision.EVERY_YEAR: + currentDate.setFullYear(currentDate.getFullYear() + 1); + break; + } } -} \ No newline at end of file + + return intervals; + } + + public static getFormatter(data: { + xAxisMin: XAxisMaxMin; + xAxisMax: XAxisMaxMin; + }): (value: Date) => string { + const precision: XAxisPrecision = XAxisUtil.getPrecision(data); + + switch (precision) { + case XAxisPrecision.EVERY_SECOND: + case XAxisPrecision.EVERY_FIVE_SECONDS: + case XAxisPrecision.EVERY_TEN_SECONDS: + case XAxisPrecision.EVERY_THIRTY_SECONDS: + return (value: Date) => { + return value.toISOString().substring(11, 19); + }; // HH:mm:ss + case XAxisPrecision.EVERY_MINUTE: + case XAxisPrecision.EVERY_FIVE_MINUTES: + case XAxisPrecision.EVERY_TEN_MINUTES: + case XAxisPrecision.EVERY_THIRTY_MINUTES: + return (value: Date) => { + return value.toISOString().substring(11, 16); + }; // HH:mm + case XAxisPrecision.EVERY_HOUR: + case XAxisPrecision.EVERY_TWO_HOURS: + case XAxisPrecision.EVERY_THREE_HOURS: + case XAxisPrecision.EVERY_SIX_HOURS: + case XAxisPrecision.EVERY_TWELVE_HOURS: + return (value: Date) => { + const dateString: string = value.toISOString(); + const day: string = dateString.substring(8, 10); + const month: string = value.toLocaleString("default", { + month: "short", + }); + const hour: string = dateString.substring(11, 13); + return `${day} ${month}, ${hour}:00`; + }; // DD MMM, HH:00 + case XAxisPrecision.EVERY_DAY: + case XAxisPrecision.EVERY_TWO_DAYS: + return (value: Date) => { + const dateString: string = value.toISOString(); + const day: string = dateString.substring(8, 10); + const month: string = value.toLocaleString("default", { + month: "short", + }); + return `${day} ${month}`; + }; // DD MMM + case XAxisPrecision.EVERY_WEEK: + case XAxisPrecision.EVERY_TWO_WEEKS: + return (value: Date) => { + const dateString: string = value.toISOString(); + const day: string = dateString.substring(8, 10); + const month: string = value.toLocaleString("default", { + month: "short", + }); + return `${day} ${month}`; + }; // DD MMM + case XAxisPrecision.EVERY_MONTH: + case XAxisPrecision.EVERY_TWO_MONTHS: + case XAxisPrecision.EVERY_THREE_MONTHS: + case XAxisPrecision.EVERY_SIX_MONTHS: + return (value: Date) => { + const dateString: string = value.toISOString(); + const day: string = dateString.substring(8, 10); + const year: string = dateString.substring(0, 4); + const month: string = value.toLocaleString("default", { + month: "short", + }); + return `${day} ${month} ${year}`; + }; // DD MMM + case XAxisPrecision.EVERY_YEAR: + return (value: Date) => { + return value.toISOString().substring(0, 4); + }; // YYYY + default: + throw new Error("Unsupported precision"); + } + } +} diff --git a/Dashboard/src/Components/Metrics/MetricView.tsx b/Dashboard/src/Components/Metrics/MetricView.tsx index 1d72241e7a..4022f82aec 100644 --- a/Dashboard/src/Components/Metrics/MetricView.tsx +++ b/Dashboard/src/Components/Metrics/MetricView.tsx @@ -29,7 +29,7 @@ import ModelAPI from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI"; import Metric from "Common/Models/AnalyticsModels/Metric"; import OneUptimeDate from "Common/Types/Date"; import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; -import ComponentLoader from "Common/UI/Components/Compon\entLoader/ComponentLoader"; +import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import ChartGroup, { Chart, @@ -66,7 +66,6 @@ export interface ComponentProps { const MetricView: FunctionComponent = ( props: ComponentProps, ): ReactElement => { - const [xAxisType, setXAxisType] = useState(XAxisType.Time); const [chartStartDate, setChartStartDate] = useState( @@ -135,7 +134,6 @@ const MetricView: FunctionComponent = ( return XAxisType.Date; }; - useEffect(() => { fetchAggregatedResults().catch((err: Error) => { setMetricResultsError(API.getFriendlyErrorMessage(err as Error)); @@ -158,29 +156,43 @@ const MetricView: FunctionComponent = ( continue; } - let xAxisAggregationType = XAxisAggregateType.Average; + let xAxisAggregationType: XAxisAggregateType = XAxisAggregateType.Average; - if(queryConfig.metricQueryData.filterData.aggegationType === MetricsAggregationType.Sum) { + if ( + queryConfig.metricQueryData.filterData.aggegationType === + MetricsAggregationType.Sum + ) { xAxisAggregationType = XAxisAggregateType.Sum; } - if(queryConfig.metricQueryData.filterData.aggegationType === MetricsAggregationType.Count) { + if ( + queryConfig.metricQueryData.filterData.aggegationType === + MetricsAggregationType.Count + ) { xAxisAggregationType = XAxisAggregateType.Sum; } - if(queryConfig.metricQueryData.filterData.aggegationType === MetricsAggregationType.Max) { + if ( + queryConfig.metricQueryData.filterData.aggegationType === + MetricsAggregationType.Max + ) { xAxisAggregationType = XAxisAggregateType.Max; } - if(queryConfig.metricQueryData.filterData.aggegationType === MetricsAggregationType.Min) { + if ( + queryConfig.metricQueryData.filterData.aggegationType === + MetricsAggregationType.Min + ) { xAxisAggregationType = XAxisAggregateType.Min; } - if(queryConfig.metricQueryData.filterData.aggegationType === MetricsAggregationType.Avg) { + if ( + queryConfig.metricQueryData.filterData.aggegationType === + MetricsAggregationType.Avg + ) { xAxisAggregationType = XAxisAggregateType.Average; } - const chart: Chart = { id: index.toString(), type: ChartType.LINE, diff --git a/Dashboard/src/Components/Monitor/MonitorCharts/MonitorChart.tsx b/Dashboard/src/Components/Monitor/MonitorCharts/MonitorChart.tsx index c4b1478b05..12ba5866b7 100644 --- a/Dashboard/src/Components/Monitor/MonitorCharts/MonitorChart.tsx +++ b/Dashboard/src/Components/Monitor/MonitorCharts/MonitorChart.tsx @@ -14,9 +14,14 @@ import Probe from "Common/Models/DatabaseModels/Probe"; import DataPoint from "Common/UI/Components/Charts/Types/DataPoint"; import SeriesPoints from "Common/UI/Components/Charts/Types/SeriesPoints"; import YAxisType from "Common/UI/Components/Charts/Types/YAxis/YAxisType"; -import YAxis, { YAxisPrecision } from "Common/UI/Components/Charts/Types/YAxis/YAxis"; +import YAxis, { + YAxisPrecision, +} from "Common/UI/Components/Charts/Types/YAxis/YAxis"; import XAxisType from "Common/UI/Components/Charts/Types/XAxis/XAxisType"; -import { XAxis, XAxisAggregateType } from "Common/UI/Components/Charts/Types/XAxis/XAxis"; +import { + XAxis, + XAxisAggregateType, +} from "Common/UI/Components/Charts/Types/XAxis/XAxis"; import ChartCurve from "Common/UI/Components/Charts/Types/ChartCurve"; export class MonitorCharts { @@ -259,31 +264,31 @@ export class MonitorCharts { monitorMetricsByMinute: Array; checkOn: CheckOn; }): XAxis { - const startTime: Date = - data.monitorMetricsByMinute[0]?.createdAt! || undefined; + const startTime: Date = + data.monitorMetricsByMinute[0]!.createdAt! || undefined; const endTime: Date = - data.monitorMetricsByMinute[data.monitorMetricsByMinute.length - 1] - ?.createdAt! || undefined; + data.monitorMetricsByMinute[data.monitorMetricsByMinute.length - 1]! + .createdAt! || undefined; let xAxisAggregationType: XAxisAggregateType = XAxisAggregateType.Average; - if(data.checkOn === CheckOn.ResponseStatusCode) { + if (data.checkOn === CheckOn.ResponseStatusCode) { xAxisAggregationType = XAxisAggregateType.Max; } - if(data.checkOn === CheckOn.IsOnline) { + if (data.checkOn === CheckOn.IsOnline) { xAxisAggregationType = XAxisAggregateType.Min; } - if(data.checkOn === CheckOn.ResponseTime) { + if (data.checkOn === CheckOn.ResponseTime) { xAxisAggregationType = XAxisAggregateType.Average; } - if( + if ( data.checkOn === CheckOn.DiskUsagePercent || data.checkOn === CheckOn.MemoryUsagePercent || data.checkOn === CheckOn.CPUUsagePercent - ){ + ) { xAxisAggregationType = XAxisAggregateType.Average; } @@ -309,7 +314,7 @@ export class MonitorCharts { precision: YAxisPrecision.NoDecimals, formatter: (value: number) => { return `${value} ms`; - } + }, }, }; } else if (data.checkOn === CheckOn.ResponseStatusCode) { @@ -322,7 +327,7 @@ export class MonitorCharts { precision: YAxisPrecision.NoDecimals, formatter: (value: number) => { return `${value}`; - } + }, }, }; } else if ( @@ -339,7 +344,7 @@ export class MonitorCharts { precision: YAxisPrecision.TwoDecimals, formatter: (value: number) => { return `${value}%`; - } + }, }, }; } @@ -353,7 +358,7 @@ export class MonitorCharts { precision: YAxisPrecision.NoDecimals, formatter: (value: number) => { return `${value}`; - } + }, }, }; } diff --git a/eslint.config.js b/eslint.config.js index b4ae69a974..5c5d22e190 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -204,6 +204,7 @@ export default tseslint.config( module: true, __dirname: true, exports: true, + "NodeJS": true }, parserOptions: { project: ["./tsconfig.json"], // Specify it only for TypeScript files