Sfoglia il codice sorgente

Compliance dashboard (#4118)

sdess09 2 anni fa
parent
commit
adbf21dd38

File diff suppressed because it is too large
+ 288 - 319
dashboard/package-lock.json


+ 5 - 3
dashboard/package.json

@@ -30,6 +30,7 @@
     "anser": "^2.0.1",
     "axios": "^0.21.2",
     "brace": "^0.11.1",
+    "chart.js": "^4.4.1",
     "chroma-js": "^2.4.2",
     "clipboard": "^2.0.8",
     "color": "^4.2.3",
@@ -56,6 +57,7 @@
     "react": "^18.0.0",
     "react-ace": "^8.1.0",
     "react-animate-height": "^3.2.2",
+    "react-chartjs-2": "^5.2.0",
     "react-color": "^2.19.3",
     "react-datepicker": "^4.8.0",
     "react-diff-viewer": "^3.1.1",
@@ -93,9 +95,9 @@
     "lint-staged": "lint-staged"
   },
   "devDependencies": {
-    "@babel/core": "^7.15.0",
+    "@babel/core": "^7.23.7",
     "@babel/plugin-syntax-dynamic-import": "^7.8.3",
-    "@babel/preset-env": "^7.15.0",
+    "@babel/preset-env": "^7.23.7",
     "@babel/preset-react": "^7.14.5",
     "@babel/preset-typescript": "^7.15.0",
     "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
@@ -136,7 +138,7 @@
     "@types/webpack-dev-server": "^3.11.5",
     "@typescript-eslint/eslint-plugin": "^6.8.0",
     "@typescript-eslint/parser": "^6.8.0",
-    "babel-loader": "^8.2.2",
+    "babel-loader": "^8.3.0",
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-styled-components": "^1.13.3",
     "cronstrue": "^2.28.0",

+ 157 - 137
dashboard/src/components/SOC2Checks.tsx

@@ -1,72 +1,76 @@
-import React, { useContext, useEffect, useMemo, useState } from "react";
+import React, { useEffect, useState } from "react";
+import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
-import { type RouteComponentProps, withRouter } from "react-router";
-import Spacer from "./porter/Spacer";
-import Step from "./porter/Step";
-import Link from "./porter/Link";
-import Text from "./porter/Text";
-import Error from "./porter/Error";
-import healthy from "assets/status-healthy.png";
-import failure from "assets/failure.svg";
-import Loading from "./Loading";
-import ToggleRow from "./porter/ToggleRow";
+
 import Container from "components/porter/Container";
+
 import external_link from "assets/external-link.svg";
+import failure from "assets/failure.svg";
 import pending from "assets/pending.svg";
-import Fieldset from "./porter/Fieldset";
-import { Context } from "shared/Context";
-//import DonutChart from "./DonutChart";
-// import DonutChart from "components/porter/DonutChart";
+import healthy from "assets/status-healthy.png";
+
+import Loading from "./Loading";
+import Link from "./porter/Link";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
+import ToggleRow from "./porter/ToggleRow";
+import { type Soc2Data, type Soc2Check } from "shared/types";
 
 type Props = RouteComponentProps & {
-  soc2Data: any
+  soc2Data: Soc2Check;
   error?: string;
   enableAll: boolean;
-  setSoc2Data: (x: any) => void;
+  setSoc2Data: (x: Soc2Check) => void;
   readOnly: boolean;
-
 };
 type ItemProps = RouteComponentProps & {
-  checkKey: string
-  checkLabel?: string
+  checkKey: string;
+  checkLabel?: string;
 };
 
-
-const SOC2Checks: React.FC<Props> = ({ soc2Data, enableAll, setSoc2Data, readOnly }) => {
-
+const SOC2Checks: React.FC<Props> = ({
+  soc2Data,
+  enableAll,
+  setSoc2Data,
+  readOnly,
+}) => {
   // const { soc2Data, setSoc2Data } = useContext(Context);
   const soc2Checks = soc2Data?.soc2_checks || {};
 
-  const combinedKeys = new Set([
-    ...Object.keys(soc2Checks)
-  ]);
+  const combinedKeys = new Set([...Object.keys(soc2Checks)]);
 
   useEffect(() => {
-
     if (enableAll) {
       const newSOC2Checks = Object.keys(soc2Checks).reduce((acc, key) => {
         acc[key] = {
           ...soc2Checks[key],
-          status: soc2Checks[key].enabled ? (soc2Checks[key].status === "PENDING" ? "PENDING" : "ENABLED") : "PENDING",
-        }
+          status: soc2Checks[key].enabled
+            ? soc2Checks[key].status === "PENDING_ENABLED"
+              ? "PENDING_ENABLED"
+              : "ENABLED"
+            : "PENDING_ENABLED",
+        };
         return acc;
       }, {});
-      setSoc2Data(prev => ({
+      setSoc2Data((prev: Soc2Data) => ({
         ...prev,
-        soc2_checks: newSOC2Checks
+        soc2_checks: newSOC2Checks,
       }));
-    }
-    else {
+    } else {
       const newSOC2Checks = Object.keys(soc2Checks).reduce((acc, key) => {
         acc[key] = {
           ...soc2Checks[key],
-          status: !soc2Checks[key].enabled ? "" : (soc2Checks[key].status === "PENDING" ? "PENDING" : "ENABLED"),
-        }
+          status: !soc2Checks[key].enabled
+            ? ""
+            : soc2Checks[key].status === "PENDING_ENABLED"
+              ? "PENDING_ENABLED"
+              : "ENABLED",
+        };
         return acc;
       }, {});
-      setSoc2Data(prev => ({
+      setSoc2Data((prev: Soc2Data) => ({
         ...prev,
-        soc2_checks: newSOC2Checks
+        soc2_checks: newSOC2Checks,
       }));
     }
   }, [enableAll]);
@@ -85,108 +89,123 @@ const SOC2Checks: React.FC<Props> = ({ soc2Data, enableAll, setSoc2Data, readOnl
       }
     };
 
+    const determineStatus = (currentStatus: string): string => {
+      if (currentStatus === "ENABLED") {
+        return "PENDING_DISABLED";
+      }
+      if (currentStatus === "PENDING_DISABLED") {
+        return "ENABLED";
+      }
+      if (currentStatus === "PENDING_ENABLED") {
+        return "";
+      }
+      if (currentStatus === "") {
+        return "PENDING_ENABLED";
+      }
+    };
+
     const handleEnable = (): void => {
-      setSoc2Data(prev => ({
+      setSoc2Data((prev) => ({
         ...prev,
         soc2_checks: {
           ...prev.soc2_checks,
           [checkKey]: {
             ...prev.soc2_checks[checkKey],
             enabled: !prev.soc2_checks[checkKey].enabled,
-            status: !prev.soc2_checks[checkKey].enabled ? "PENDING" : !prev.soc2_checks[checkKey].enabled ? "ENABLED" : "",
-          }
-        }
+            status: determineStatus(prev.soc2_checks[checkKey].status),
+          },
+        },
       }));
     };
 
     return (
-      <CheckItemContainer hasMessage={hasMessage} > {/* Pass isExpanded as a prop */}
-        < CheckItemTop onClick={handleToggle} >
-          {status === "LOADING" &&
-            <Loading
-              offset="0px"
-              width="20px"
-              height="20px" />
-          }
-          {status === "PENDING" &&
-            <StatusIcon src={pending} />
-          }
-          {status === "ENABLED" &&
-            <StatusIcon src={healthy} />
-          }
-          {status === "" &&
+      <CheckItemContainer hasMessage={hasMessage}>
+        {" "}
+        {/* Pass isExpanded as a prop */}
+        <CheckItemTop onClick={handleToggle}>
+          {status === "LOADING" && (
+            <Loading offset="0px" width="20px" height="20px" />
+          )}
+          {status === "PENDING_ENABLED" && <StatusIcon src={pending} />}
+          {status === "ENABLED" && <StatusIcon src={healthy} />}
+          {(status === "" || status === "PENDING_DISABLED") && (
             <StatusIcon height="10px" src={failure} />
-          }
+          )}
           <Spacer inline x={1} />
-          <Text style={{ marginLeft: '10px', flex: 1 }}>{checkLabel}</Text>
-          {
-            enabled && <ExpandIcon className="material-icons" isExpanded={isExpanded}>
+          <Text style={{ marginLeft: "10px", flex: 1 }}>{checkLabel}</Text>
+          {enabled && (
+            <ExpandIcon className="material-icons" isExpanded={isExpanded}>
               arrow_drop_down
             </ExpandIcon>
-          }
-        </CheckItemTop >
-        {
-          isExpanded && hasMessage && (
-            <div style={{ marginLeft: '10px' }}>
-              <Spacer y={.5} />
-              <Text>
-                {checkData?.message}
-              </Text>
-              <Spacer y={.5} />
-              {checkData?.metadata &&
-                Object.entries(checkData.metadata).map(([key, value]) => (
-                  <>
-                    <div key={key}>
-                      <ErrorMessageLabel>{key}:</ErrorMessageLabel>
-                      <ErrorMessageContent>{value}</ErrorMessageContent>
-                    </div>
-                  </>
-                ))}
-              <Spacer y={.5} />
-
-              {!checkData?.hideToggle &&
+          )}
+        </CheckItemTop>
+        {isExpanded && hasMessage && (
+          <div style={{ marginLeft: "10px" }}>
+            <Spacer y={0.5} />
+            <Text>{checkData?.message}</Text>
+            <Spacer y={0.5} />
+            {checkData?.metadata &&
+              Object.entries(checkData.metadata).map(([key, value]) => (
                 <>
-                  <Container row spaced style={{ marginRight: '10px' }}>
-                    <ToggleRow
-                      isToggled={enabled || enableAll}
-                      onToggle={() => {
-                        handleEnable();
-                      }}
-                      disabled={readOnly || enableAll || (enabled && checkData?.locked && status !== "PENDING")}
-                      disabledTooltip={readOnly ? ("Wait for provisioning to complete before editing this field.") : (enableAll ? "Global SOC 2 setting must be disabled to toggle this" : checkData?.disabledTooltip)}
-                    >
-                      <Container row>
-                        <Text>{checkData.enabledField}</Text>
-                        <Spacer inline x={1} />
-                        <Text color="helper">{checkData.info}</Text>
-                      </Container>
-                    </ToggleRow>
-
+                  <div key={key}>
+                    <ErrorMessageLabel>{key}:</ErrorMessageLabel>
+                    <ErrorMessageContent>{value}</ErrorMessageContent>
+                  </div>
+                </>
+              ))}
+            <Spacer y={0.5} />
 
-                    {
-                      checkData.link &&
-                      <Link
-                        onClick={() => {
-                          window.open(checkData.link, "_blank");
-                        }}
-                      >
-                        <TagIcon src={external_link} />
-                        More Info
-                      </Link>
+            {!checkData?.hideToggle && (
+              <>
+                <Container row spaced style={{ marginRight: "10px" }}>
+                  <ToggleRow
+                    isToggled={enabled || enableAll}
+                    onToggle={() => {
+                      handleEnable();
+                    }}
+                    disabled={
+                      readOnly ||
+                      enableAll ||
+                      (enabled &&
+                        checkData?.locked &&
+                        status !== "PENDING_ENABLED")
                     }
-                  </Container>
-                  <Spacer y={.5} />
-                </>
-              }
-            </div>
-          )
-        }
-      </CheckItemContainer >
+                    disabledTooltip={
+                      readOnly
+                        ? "Wait for provisioning to complete before editing this field."
+                        : enableAll
+                          ? "Global SOC 2 setting must be disabled to toggle this"
+                          : checkData?.disabledTooltip
+                    }
+                  >
+                    <Container row>
+                      <Text>{checkData.enabledField}</Text>
+                      <Spacer inline x={1} />
+                      <Text color="helper">{checkData.info}</Text>
+                    </Container>
+                  </ToggleRow>
+
+                  {checkData.link && (
+                    <Link
+                      onClick={() => {
+                        window.open(checkData.link, "_blank");
+                      }}
+                    >
+                      <TagIcon src={external_link} />
+                      More Info
+                    </Link>
+                  )}
+                </Container>
+                <Spacer y={0.5} />
+              </>
+            )}
+          </div>
+        )}
+      </CheckItemContainer>
     );
   };
   return (
-
-    <><Spacer y={1} />
+    <>
       <>
         {/* <Fieldset>
         <DonutChart soc2Data={soc2Data} />
@@ -197,25 +216,24 @@ const SOC2Checks: React.FC<Props> = ({ soc2Data, enableAll, setSoc2Data, readOnl
             <Soc2Item
               key={checkKey}
               checkKey={checkKey}
-              checkLabel={checkKey} />
+              checkLabel={checkKey}
+            />
           ))}
-        </AppearingDiv></></>
-
-  )
+        </AppearingDiv>
+      </>
+    </>
+  );
 };
 
-
-
 export default withRouter(SOC2Checks);
 
-
 const AppearingDiv = styled.div<{ color?: string }>`
   animation: floatIn 0.5s;
   animation-fill-mode: forwards;
   display: flex;
-  flex-direction: column; 
+  flex-direction: column;
   color: ${(props) => props.color || "#ffffff44"};
- 
+
   @keyframes floatIn {
     from {
       opacity: 0;
@@ -228,29 +246,31 @@ const AppearingDiv = styled.div<{ color?: string }>`
   }
 `;
 const StatusIcon = styled.img`
-height: ${props => props.height ? props.height : '14px'};
+  height: ${(props) => (props.height ? props.height : "14px")};
 `;
 
 const CheckItemContainer = styled.div`
   display: flex;
   flex-direction: column;
-  border: ${props => props.isExpanded ? '2px solid #3a48ca' : '1px solid ' + props.theme.border}; // Thicker and blue border if expanded
+  border: ${(props) =>
+    props.isExpanded
+      ? "2px solid #3a48ca"
+      : "1px solid " +
+      props.theme.border}; // Thicker and blue border if expanded
   border-radius: 5px;
   font-size: 13px;
   width: 100%;
   margin-bottom: 10px;
   padding-left: 10px;
-  cursor: ${props => (props.hasMessage ? 'pointer' : 'default')};
-  background: ${props => props.theme.clickable.bg};
+  cursor: ${(props) => (props.hasMessage ? "pointer" : "default")};
+  background: ${(props) => props.theme.clickable.bg};
 `;
 
-
-
 const CheckItemTop = styled.div`
   display: flex;
   align-items: center;
   padding: 10px;
-  background: ${props => props.theme.clickable.bg};
+  background: ${(props) => props.theme.clickable.bg};
 `;
 
 const ExpandIcon = styled.i<{ isExpanded: boolean }>`
@@ -259,19 +279,19 @@ const ExpandIcon = styled.i<{ isExpanded: boolean }>`
   font-size: 20px;
   cursor: pointer;
   border-radius: 20px;
-  transform: ${props => props.isExpanded ? "" : "rotate(-90deg)"};
+  transform: ${(props) => (props.isExpanded ? "" : "rotate(-90deg)")};
 `;
 const ErrorMessageLabel = styled.span`
   font-weight: bold;
   margin-left: 10px;
 `;
 const ErrorMessageContent = styled.div`
-  font-family: 'Courier New', Courier, monospace;
+  font-family: "Courier New", Courier, monospace;
   padding: 5px 10px;
   border-radius: 4px;
   margin-left: 10px;
   user-select: text;
-  cursor: text
+  cursor: text;
 `;
 const TagIcon = styled.img`
   height: 12px;

+ 23 - 15
dashboard/src/main/home/cluster-dashboard/dashboard/Compliance.tsx

@@ -9,6 +9,7 @@ import Loading from "components/Loading";
 import Button from "components/porter/Button";
 import Container from "components/porter/Container";
 import Error from "components/porter/Error";
+import Fieldset from "components/porter/Fieldset";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import ToggleRow from "components/porter/ToggleRow";
@@ -18,6 +19,9 @@ import api from "shared/api";
 import { Context } from "shared/Context";
 import sparkle from "assets/sparkle.svg";
 
+import DonutChart from "./DonutChart";
+import { Soc2Data } from "shared/types";
+
 type Props = {
   credentialId: string;
   provisionerError?: string;
@@ -36,7 +40,7 @@ type Props = {
 //   "disabledTooltip": "display if message is disabled",
 //  "hideToggle": true (if you want to hide the toggle
 // }
-const soc2DataDefault = {
+const soc2DataDefault: Soc2Data = {
   soc2_checks: {
     "Public SSH Access": {
       message:
@@ -257,7 +261,7 @@ const Compliance: React.FC<Props> = (props) => {
           project_id: currentProject ? currentProject.id : 0,
         }
       );
-    } catch (err) {}
+    } catch (err) { }
   };
 
   const isUserProvisioning = useMemo(() => {
@@ -267,7 +271,7 @@ const Compliance: React.FC<Props> = (props) => {
   const determineStatus = (enabled: boolean): string => {
     if (enabled) {
       if (currentCluster?.status === "UPDATING") {
-        return "PENDING";
+        return "PENDING_ENABLED";
       } else return "ENABLED";
     }
     return "";
@@ -306,7 +310,7 @@ const Compliance: React.FC<Props> = (props) => {
             },
             "Enhanced Image Vulnerability Scanning": {
               ...prevSoc2Data.soc2_checks[
-                "Enhanced Image Vulnerability Scanning"
+              "Enhanced Image Vulnerability Scanning"
               ],
               enabled: eksValues.enableEcrScanning,
               status: determineStatus(eksValues.enableEcrScanning),
@@ -317,8 +321,8 @@ const Compliance: React.FC<Props> = (props) => {
 
       setSoc2Enabled(
         cloudTrailEnabled &&
-          eksValues.enableKmsEncryption &&
-          eksValues.enableEcrScanning
+        eksValues.enableKmsEncryption &&
+        eksValues.enableEcrScanning
       );
     }
   }, [props.selectedClusterVersion]);
@@ -330,21 +334,25 @@ const Compliance: React.FC<Props> = (props) => {
 
     setIsReadOnly(
       currentCluster.status === "UPDATING" ||
-        currentCluster.status === "UPDATING_UNAVAILABLE"
+      currentCluster.status === "UPDATING_UNAVAILABLE"
     );
   }, []);
 
   return (
     <StyledCompliance>
       <Spacer y={1} />
-      <Container row>
-        <Text size={16}>SOC 2 compliance</Text>
-        <Spacer inline x={1} />
-        <NewBadge>
-          <img src={sparkle} />
-          New
-        </NewBadge>
-      </Container>
+      <Fieldset>
+        <Container row>
+          <Text size={16}>SOC 2 Compliance Dashboard</Text>
+          <Spacer inline x={1} />
+          <NewBadge>
+            <img src={sparkle} />
+            New
+          </NewBadge>
+        </Container>
+        <Spacer y={1} />
+        <DonutChart data={soc2Data} />
+      </Fieldset>
 
       <SOC2Checks
         enableAll={soc2Enabled}

+ 135 - 0
dashboard/src/main/home/cluster-dashboard/dashboard/DonutChart.tsx

@@ -0,0 +1,135 @@
+import React, { useEffect, useState } from "react";
+import { ArcElement, CategoryScale, Chart, Legend, Tooltip } from "chart.js";
+import { Doughnut } from "react-chartjs-2";
+
+import Container from "components/porter/Container";
+import Spacer from "components/porter/Spacer";
+import { type Soc2Check } from "shared/types";
+
+Chart.register(ArcElement, Tooltip, Legend, CategoryScale);
+
+type DonutChartProps = {
+    data: Soc2Check;
+};
+
+const DonutChart: React.FC<DonutChartProps> = ({ data }) => {
+    const [chartDataValues, setChartDataValues] = useState([0, 0, 0]);
+
+    useEffect(() => {
+        const counts = { ENABLED: 0, DISABLED: 0, PENDING: 0 };
+
+        Object.values(data.soc2_checks).forEach((check) => {
+            let status = check.status || "DISABLED";
+            if (status.includes("PENDING")) {
+                status = "PENDING";
+            }
+            counts[status.toUpperCase()]++;
+        });
+
+        setChartDataValues([counts.ENABLED, counts.DISABLED, counts.PENDING]);
+    }, [data]); // Dependency array ensures this runs only when `data` changes
+
+    const chartData = {
+        labels: ["Enabled", "Disabled", "Pending"],
+        datasets: [
+            {
+                data: chartDataValues,
+                backgroundColor: ["#5eaa7d", "#e34040", "rgb(255, 205, 86)"],
+                borderColor: "#171b21",
+                borderWidth: 2,
+                hoverBorderColor: "#171b21",
+                hoverBorderWidth: 3,
+                borderJoinStyle: "round",
+                hoverBorderJoinStyle: "bevel",
+            },
+        ],
+    };
+
+    const options = {
+        plugins: {
+            legend: false,
+            tooltip: {},
+        },
+        elements: {
+            arc: {
+                borderWidth: 3,
+                borderColor: "#fff",
+                borderAlign: "inner",
+                hoverOffset: 1,
+            },
+        },
+        responsive: true,
+        maintainAspectRatio: false,
+    };
+
+    const textCenter = {
+        id: "textCenter",
+        afterDatasetsDraw: (chart: unknown) => {
+            const { ctx, data } = chart;
+            ctx.save();
+            ctx.font = "15px sans-serif";
+            ctx.fillStyle = "#fff";
+            ctx.textAlign = "center";
+            ctx.textBaseline = "middle";
+
+            // Calculate the total
+            const total = data.datasets[0].data.reduce((a, b) => a + b, 0);
+
+            // Coordinates for the text
+            const x = chart.getDatasetMeta(0).data[0].x;
+            const y = chart.getDatasetMeta(0).data[0].y;
+
+            // Draw the first line of text
+            ctx.fillText(`${data.datasets[0].data[0]} / ${total}`, x, y - 10); // Adjust Y position as needed
+
+            // Draw the second line of text
+            ctx.fillText(`checks enabled`, x, y + 10); // Adjust Y position as needed
+
+            ctx.restore();
+        },
+    };
+
+    const CustomLegend = (): JSX.Element => (
+        <div
+            style={{
+                display: "flex",
+                flexDirection: "column",
+                justifyContent: "center",
+                alignItems: "start",
+            }}
+        >
+            {chartData.datasets[0].backgroundColor.map((color, index) => (
+                <div
+                    key={index}
+                    style={{ display: "flex", alignItems: "center", marginBottom: "4px" }}
+                >
+                    <span
+                        style={{
+                            backgroundColor: color,
+                            width: "12px",
+                            height: "12px",
+                            display: "inline-block",
+                            marginRight: "8px",
+                        }}
+                    ></span>
+                    {chartData.labels[index]}
+                </div>
+            ))}
+        </div>
+    );
+
+    return (
+        <>
+            <Spacer y={0.5} />
+            <Container row>
+                <div style={{ width: "300px", height: "300px" }}>
+                    <Doughnut data={chartData} options={options} plugins={[textCenter]} />
+                </div>
+                <Spacer inline x={1} />
+                <CustomLegend />
+            </Container>
+        </>
+    );
+};
+
+export default DonutChart;

+ 16 - 0
dashboard/src/shared/types.tsx

@@ -710,3 +710,19 @@ export type ClusterState = {
   gpuMinInstances: number;
   gpuMaxInstances: number;
 };
+
+export type Soc2Check = {
+  message: string;
+  enabled: boolean;
+  hideToggle?: boolean;
+  status: string;
+  disabledTooltip?: string;
+  link?: string;
+  locked?: boolean;
+  enabledField?: string;
+  info?: string;
+};
+
+export type Soc2Data = {
+  soc2_checks: Record<string, Soc2Check>;
+};

+ 1 - 1
dashboard/webpack.config.js

@@ -75,7 +75,7 @@ module.exports = () => {
       rules: [
         {
           test: /\.(ts|tsx|mjs|js|jsx)$/,
-          exclude: /node_modules/,
+          exclude: /node_modules\/(?!(chart.js|react-chartjs-2)\/).*/,
           use: [
             {
               loader: require.resolve("babel-loader"),

Some files were not shown because too many files changed in this diff