Просмотр исходного кода

feat: implement initial version of nginx status code dashboard

This still needs a bit of work around tooltips due to the data is returned from the api from 5 different calls.
Jose Diaz-Gonzalez 2 лет назад
Родитель
Сommit
458115e87e

+ 10 - 0
dashboard/src/main/home/app-dashboard/expanded-app/MetricsSection.tsx

@@ -243,6 +243,16 @@ const MetricsSection: React.FunctionComponent<PropsType> = ({
         selectedRange={selectedRange}
         selectedRange={selectedRange}
         pods={pods}
         pods={pods}
       />
       />
+      <MetricsChart
+        currentChart={currentChart}
+        selectedController={selectedController}
+        selectedIngress={selectedIngress}
+        selectedMetric="nginx:status"
+        selectedMetricLabel="Nginx Status Codes"
+        selectedPod="All"
+        selectedRange={selectedRange}
+        pods={pods}
+      />
       {currentChart?.config?.autoscaling?.enabled && (
       {currentChart?.config?.autoscaling?.enabled && (
         <MetricsChart
         <MetricsChart
           currentChart={currentChart}
           currentChart={currentChart}

+ 1 - 0
dashboard/src/main/home/app-dashboard/expanded-app/metrics/AreaChart.tsx

@@ -234,6 +234,7 @@ const AreaChart: React.FunctionComponent<AreaProps> = ({
     if (width == 0 || height == 0 || width < 10) {
     if (width == 0 || height == 0 || width < 10) {
         return null;
         return null;
     }
     }
+
     const hpaGraphTooltipGlyphPosition =
     const hpaGraphTooltipGlyphPosition =
         (hpaEnabled &&
         (hpaEnabled &&
             tooltipData?.tooltipHpaData &&
             tooltipData?.tooltipHpaData &&

+ 21 - 13
dashboard/src/main/home/app-dashboard/expanded-app/metrics/MetricsChart.tsx

@@ -8,8 +8,9 @@ import { ChartTypeWithExtendedConfig } from "shared/types";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
 import AggregatedDataLegend from "../../../cluster-dashboard/expanded-chart/metrics/AggregatedDataLegend";
 import AggregatedDataLegend from "../../../cluster-dashboard/expanded-chart/metrics/AggregatedDataLegend";
-import StatusCodeDataLegend from "../../../cluster-dashboard/expanded-chart/metrics/StatusCodeDataLegend";
+import StatusCodeDataLegend from "./StatusCodeDataLegend";
 import AreaChart from "./AreaChart";
 import AreaChart from "./AreaChart";
+import StackedAreaChart from "./StackedAreaChart";
 import CheckboxRow from "components/form-components/CheckboxRow";
 import CheckboxRow from "components/form-components/CheckboxRow";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import { AvailableMetrics, GenericMetricResponse, NormalizedMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
 import { AvailableMetrics, GenericMetricResponse, NormalizedMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
@@ -289,8 +290,8 @@ const MetricsChart: React.FunctionComponent<PropsType> = ({
                             responses[i].data,
                             responses[i].data,
                             selectedMetric as AvailableMetrics
                             selectedMetric as AvailableMetrics
                         );
                         );
-                        let index = `${i+1}xx`
-                        aggregatedMetrics[index] = metrics.getParsedData()
+                        const metrixIndex = `${i+1}xx`;
+                        aggregatedMetrics[metrixIndex] = metrics.getParsedData()
                     }
                     }
 
 
                     setAreaData(aggregatedMetrics);
                     setAreaData(aggregatedMetrics);
@@ -377,21 +378,28 @@ const MetricsChart: React.FunctionComponent<PropsType> = ({
                             />
                             />
                         )}
                         )}
                     </ParentSize>
                     </ParentSize>
-                    {data.length > 0 && (
-                        <RowWrapper>
-                            <AggregatedDataLegend data={data} hideAvg={isAggregated} />
-                        </RowWrapper>
-                    )}
+                    <RowWrapper>
+                        <AggregatedDataLegend data={data} hideAvg={isAggregated} />
+                    </RowWrapper>
                 </>
                 </>
             )}
             )}
 
 
             {Object.keys(areaData).length > 0 && isLoading === 0 && (
             {Object.keys(areaData).length > 0 && isLoading === 0 && (
                 <>
                 <>
-                    {Object.keys(areaData).length > 0 && (
-                        <RowWrapper>
-                            <StatusCodeDataLegend />
-                        </RowWrapper>
-                    )}
+                    <ParentSize>
+                        {({ width, height }) => (
+                            <StackedAreaChart
+                                data={areaData}
+                                width={width}
+                                height={height - 10}
+                                resolution={selectedRange}
+                                margin={{ top: 40, right: -40, bottom: 0, left: 50 }}
+                            />
+                        )}
+                    </ParentSize>
+                    <RowWrapper>
+                        <StatusCodeDataLegend />
+                    </RowWrapper>
                 </>
                 </>
             )}
             )}
         </StyledMetricsChart>
         </StyledMetricsChart>

+ 254 - 0
dashboard/src/main/home/app-dashboard/expanded-app/metrics/StackedAreaChart.tsx

@@ -0,0 +1,254 @@
+import _ from "lodash";
+import React, { useMemo, useRef } from "react";
+import { AreaSeries, AreaStack, Tooltip, XYChart } from "@visx/xychart";
+import { AxisBottom, AxisLeft } from "@visx/axis"; 
+import { scaleLinear, scaleTime } from "@visx/scale";
+import { curveMonotoneX as visxCurve } from "@visx/curve";
+import { bisector, extent, max } from "d3-array";
+import { timeFormat } from "d3-time-format";
+import { GridColumns, GridRows } from "@visx/grid";
+
+import { default as areaTheme } from "./themes/area";
+import { NormalizedMetricsData } from "../../../cluster-dashboard/expanded-chart/metrics/types";
+
+export const background = "#3b697800";
+export const background2 = "#20405100";
+export const accentColor = "#949eff";
+export const accentColorDark = "#949eff";
+
+type StatusCode = "1xx" | "2xx" | "3xx" | "4xx" | "5xx";
+
+// util
+const formatDate = timeFormat("%H:%M:%S %b %d, '%y");
+
+const hourFormat = timeFormat("%H:%M");
+const dayFormat = timeFormat("%b %d");
+
+const dateScaleConfig = { type: 'point' } as const;
+const yScaleConfig = { type: 'linear' } as const;
+
+// map resolutions to formats
+const formats: { [range: string]: (date: Date) => string } = {
+    "1H": hourFormat,
+    "6H": hourFormat,
+    "1D": hourFormat,
+    "1M": dayFormat,
+};
+
+// accessors
+const getDate = (d: NormalizedMetricsData) => new Date(d.date * 1000);
+const getDateAsString = (d: NormalizedMetricsData) => formatDate(getDate(d));
+const getValue = (d: NormalizedMetricsData) =>
+    d?.value && Number(d.value?.toFixed(4));
+
+const bisectDate = bisector<NormalizedMetricsData, Date>(
+    (d) => new Date(d.date * 1000)
+).left;
+
+export type StackedAreaChartProps = {
+    data: Record<string, NormalizedMetricsData[]>;
+    resolution: string;
+    width: number;
+    height: number;
+    margin?: { top: number; right: number; bottom: number; left: number };
+};
+
+const StackedAreaChart: React.FunctionComponent<StackedAreaChartProps> = ({
+    data,
+    resolution,
+    width,
+    height,
+    margin = { top: 0, right: 0, bottom: 0, left: 0 },
+}) => {
+
+    const svgContainer = useRef();
+    // bounds
+    const innerWidth = width - margin.left - margin.right - 40;
+    const innerHeight = height - margin.top - margin.bottom - 20;
+
+    // scales
+    const dateScale = useMemo(
+        () =>
+            scaleTime({
+                range: [margin.left, innerWidth + margin.left],
+                domain: extent(
+                    data["1xx"],
+                    getDate
+                ) as [Date, Date],
+            }),
+        [margin.left, width, height, data]
+    );
+    const valueScale = useMemo(
+        () =>
+            scaleLinear({
+                range: [innerHeight + margin.top, margin.top],
+                domain: [
+                    0,
+                    1.25 *
+                    max(
+                        data["1xx"],
+                        getValue
+                    ),
+                ],
+                nice: true,
+            }),
+        [margin.top, width, height, data]
+    );
+
+    if (width == 0 || height == 0 || width < 10) {
+        return null;
+    }
+
+    return (
+        <div>
+            <svg width={width} height={height} ref={svgContainer}>
+                <rect
+                    x={0}
+                    y={0}
+                    width={width}
+                    height={height}
+                    fill="url(#area-background-gradient)"
+                    rx={14}
+                />
+
+                <XYChart
+                    theme={areaTheme}
+                    xScale={dateScaleConfig}
+                    yScale={yScaleConfig}
+                    height={height}
+                    width={width}
+                    margin={{ top: 50, right: 0, bottom: 50, left: 50 }}
+
+                >
+                    <GridRows
+                        left={margin.left}
+                        scale={valueScale}
+                        width={innerWidth}
+                        strokeDasharray="1,3"
+                        stroke="white"
+                        strokeOpacity={0.2}
+                        pointerEvents="none"
+                    />
+                    <GridColumns
+                        top={margin.top}
+                        scale={dateScale}
+                        height={innerHeight}
+                        strokeDasharray="1,3"
+                        stroke="white"
+                        strokeOpacity={0.2}
+                        pointerEvents="none"
+                    />
+                    <AreaStack curve={visxCurve}>
+                        <AreaSeries
+                            dataKey="1xx"
+                            data={data["1xx"]}
+                            xAccessor={getDate}
+                            yAccessor={getValue}
+                            fillOpacity={0.4}
+                        />
+                        <AreaSeries
+                            dataKey="2xx"
+                            data={data["2xx"]}
+                            xAccessor={getDate}
+                            yAccessor={getValue}
+                            fillOpacity={0.4}
+                        />
+                        <AreaSeries
+                            dataKey="3xx"
+                            data={data["3xx"]}
+                            xAccessor={getDate}
+                            yAccessor={getValue}
+                            fillOpacity={0.4}
+                        />
+                        <AreaSeries
+                            dataKey="4xx"
+                            data={data["4xx"]}
+                            xAccessor={getDate}
+                            yAccessor={getValue}
+                            fillOpacity={0.4}
+                        />
+                        <AreaSeries
+                            dataKey="5xx"
+                            data={data["5xx"]}
+                            xAccessor={getDate}
+                            yAccessor={getValue}
+                            fillOpacity={0.4}
+                        />
+                    </AreaStack>
+                    <AxisLeft
+                        left={10}
+                        scale={valueScale}
+                        hideAxisLine={true}
+                        hideTicks={true}
+                        tickLabelProps={() => ({
+                            fill: "white",
+                            fontSize: 11,
+                            textAnchor: "start",
+                            fillOpacity: 0.4,
+                            dy: 0,
+                        })}
+                    />
+                    <Tooltip<NormalizedMetricsData>
+                        showHorizontalCrosshair={true}
+                        showVerticalCrosshair={true}
+                        snapTooltipToDatumX={true}
+                        snapTooltipToDatumY={true}
+                        showDatumGlyph={true}
+                        renderTooltip={({ tooltipData, colorScale }) => (
+                            <>
+                                {/** date */}
+                                {(tooltipData?.nearestDatum?.datum &&
+                                    getDateAsString(tooltipData?.nearestDatum?.datum)) ||
+                                    "No date"}
+                                <br />
+                                <br />
+                                {/** temperatures */}
+                                {((Object.keys(tooltipData?.datumByKey ?? {})).filter((city) => city) as StatusCode[]).map((statusCode) => {
+                                    const temperature =
+                                        tooltipData?.nearestDatum?.datum &&
+                                        getValue(
+                                            tooltipData?.nearestDatum?.datum
+                                        );
+
+                                    return (
+                                        <div key={statusCode}>
+                                            <em
+                                                style={{
+                                                    color: colorScale?.(statusCode),
+                                                    textDecoration:
+                                                        tooltipData?.nearestDatum?.key === statusCode
+                                                            ? "underline"
+                                                            : undefined
+                                                }}
+                                            >
+                                                {statusCode}
+                                            </em>{" "}
+                                            {temperature == null || Number.isNaN(temperature)
+                                                ? "–"
+                                                : `${temperature} AVG RPS`}
+                                        </div>
+                                    );
+                                })}
+                            </>
+                        )}
+                    />
+                </XYChart>
+                <AxisBottom
+                    top={height - 20}
+                    scale={dateScale}
+                    tickFormat={formats[resolution]}
+                    hideAxisLine={true}
+                    hideTicks={true}
+                    tickLabelProps={() => ({
+                        fill: "white",
+                        fontSize: 11,
+                        textAnchor: "middle",
+                        fillOpacity: 0.4,
+                    })}
+                />
+            </svg>
+        </div>
+    )
+};
+
+export default StackedAreaChart;

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/StatusCodeDataLegend.tsx → dashboard/src/main/home/app-dashboard/expanded-app/metrics/StatusCodeDataLegend.tsx

@@ -1,8 +1,6 @@
 import React from "react";
 import React from "react";
-import * as stats from "simple-statistics";
 import styled from "styled-components";
 import styled from "styled-components";
 import chroma from "chroma-js";
 import chroma from "chroma-js";
-import { NormalizedMetricsData } from "./types";
 import { StatusCodeDataColors } from "./utils";
 import { StatusCodeDataColors } from "./utils";
 
 
 interface StatusCodeDataLegendProps {}
 interface StatusCodeDataLegendProps {}

+ 28 - 0
dashboard/src/main/home/app-dashboard/expanded-app/metrics/themes/area.tsx

@@ -0,0 +1,28 @@
+import { buildChartTheme, grayColors } from '@visx/xychart';
+
+let chart = buildChartTheme({
+    backgroundColor: '#222',
+    colors: [
+        "#4B4F7C", // gray (1xx)
+        "#FFFFFF", // white (2xx)
+        "#54B835", // green (3xx)
+        "#BBBB3C", // yellow (4xx)
+        "#9C20A5", // purple (5xx)
+    ],
+    tickLength: 4,
+    svgLabelSmall: {
+        fill: grayColors[2],
+    },
+    svgLabelBig: {
+        fill: grayColors[0],
+    },
+    gridColor: grayColors[4],
+    gridColorDark: grayColors[1],
+});
+
+chart.gridStyles.strokeDasharray = "1,3";
+chart.gridStyles.stroke = "white";
+chart.gridStyles.strokeOpacity = 0.2;
+
+
+export default chart;

+ 9 - 0
dashboard/src/main/home/app-dashboard/expanded-app/metrics/utils.tsx

@@ -1,3 +1,12 @@
+// these match log colors
+export const StatusCodeDataColors: Record<string, string> = {
+    "1xx": "#4B4F7C", // gray
+    "2xx": "#FFFFFF", // white
+    "3xx": "#54B835", // green
+    "4xx": "#BBBB3C", // yellow
+    "5xx": "#9C20A5", // purple
+};
+
 type RGB = {
 type RGB = {
     r: number;
     r: number;
     g: number;
     g: number;

+ 0 - 8
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/utils.ts

@@ -3,11 +3,3 @@ export const AggregatedDataColors: Record<string, string> = {
   avg: "#F2C94C",
   avg: "#F2C94C",
   max: "#B04649",
   max: "#B04649",
 };
 };
-
-export const StatusCodeDataColors: Record<string, string> = {
-  "1xx": "#D3AFD6", // purple
-  "2xx": "#C6E6AD", // green
-  "3xx": "#59DFEE", // blue
-  "4xx": "#FCE49D", // yellow
-  "5xx": "#FFB2A6", // red
-};