2
0
Эх сурвалжийг харах

SOC 2 FE (#4160)

Co-authored-by: Ian Edwards <ianedwards559@gmail.com>
jusrhee 2 жил өмнө
parent
commit
ff0b43b0b8

BIN
dashboard/src/assets/compliance.png


+ 3 - 0
dashboard/src/assets/compliance.svg

@@ -0,0 +1,3 @@
+<svg width="33" height="38" viewBox="0 0 33 38" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5895 17.5287L16.8105 20.7883L23.2526 14.2691M5 7.74978L15.0032 2.68826C16.8168 1.77058 18.9516 1.77058 20.7652 2.68826L30.7684 7.74978C30.7684 7.74978 30.7684 17.2679 30.7684 22.2878C30.7684 27.3076 26.1831 30.6962 17.8842 36C9.58535 30.6962 5 26.2211 5 22.2878V7.74978Z" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 1 - 0
dashboard/src/assets/framework.svg


+ 3 - 0
dashboard/src/assets/green-check.svg

@@ -0,0 +1,3 @@
+<svg width="12" height="9" viewBox="0 0 12 9" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.6 1L3.44048 8.2L1 5.74572" stroke="#11A84D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/link-external.svg

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.25 1H3.25C2.00736 1 1 2.00735 1 3.24999V10.75C1 11.9926 2.00736 13 3.25 13H10.75C11.9926 13 13 11.9926 13 10.75V7.74996M9.24963 1.00018L13 1M13 1V4.37507M13 1L6.62445 7.37478" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/link.svg

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.10557 4.73176L1.81633 6.02099C1.33483 6.50249 1.05794 7.15764 1.063 7.84618C1.06806 8.53472 1.33889 9.19386 1.84323 9.68263C2.33197 10.187 2.99126 10.4578 3.67968 10.4629C4.38382 10.468 5.02346 10.2067 5.50498 9.72524L6.79422 8.436M8.44098 6.82074L9.73022 5.5315C10.2117 5.05001 10.4886 4.39486 10.4835 3.70632C10.4785 3.01778 10.2077 2.35864 9.70331 1.86987C9.21468 1.38125 8.55551 1.1104 7.86697 1.10534C7.17843 1.10028 6.5232 1.36145 6.04168 1.84296L4.75244 3.1322M3.81822 7.69682L7.68594 3.82911" stroke="#494B4F" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/not-applicable.svg

@@ -0,0 +1,3 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.0625 9.9375L9.9375 5.0625M7.5 14C3.91015 14 1 11.0899 1 7.5C1 3.91015 3.91015 1 7.5 1C11.0899 1 14 3.91015 14 7.5C14 11.0899 11.0899 14 7.5 14Z" stroke="#494B4F" stroke-width="1.2" stroke-linecap="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/provider.svg

@@ -0,0 +1,3 @@
+<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.09386 4.09362H4.15066M8.31301 10L5.17577 6.90735C4.56867 7.14262 3.84332 7.05023 3.16948 6.85311C1.56469 6.38365 0.649812 4.72063 1.12604 3.13865C1.60226 1.55667 3.28926 0.654788 4.89404 1.12425C6.49883 1.59371 7.41371 3.25673 6.93748 4.83871L6.86277 5.24433L10 8.33698V10H8.31301Z" stroke="white" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/shield.svg

@@ -0,0 +1,3 @@
+<svg width="30" height="36" viewBox="0 0 30 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.5895 16.5287L14.8105 19.7883L21.2526 13.2691M3 6.74978L13.0032 1.68826C14.8168 0.770581 16.9516 0.77058 18.7652 1.68826L28.7684 6.74978C28.7684 6.74978 28.7684 16.2679 28.7684 21.2878C28.7684 26.3076 24.1831 29.6962 15.8842 35C7.58535 29.6962 3 25.2211 3 21.2878V6.74978Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/type.svg

@@ -0,0 +1,3 @@
+<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4 1H2C1.44772 1 1 1.44772 1 2V4M4 10H2C1.44772 10 1 9.55228 1 9V7M7 1H9C9.55228 1 10 1.44772 10 2V4M10 7V9C10 9.55228 9.55228 10 9 10H7M1 5.50014H10" stroke="white" stroke-width="0.8" stroke-linecap="round"/>
+</svg>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 7 - 0
dashboard/src/assets/vanta.svg


+ 3 - 0
dashboard/src/assets/warning.svg

@@ -0,0 +1,3 @@
+<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 7.11877V4.03495M7 9.40453V9.43164M10.78 12H3.22005C2.1874 12 1.31595 11.2962 1.04177 10.3333C0.924728 9.92226 1.06854 9.49413 1.28851 9.12961L5.06846 2.1007C5.95407 0.633097 8.04594 0.633099 8.93154 2.10071L12.7115 9.12961C12.9315 9.49413 13.0753 9.92226 12.9582 10.3333C12.6841 11.2962 11.8126 12 10.78 12Z" stroke="#E3992B" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 1 - 6
dashboard/src/components/AWSCostConsent.tsx

@@ -46,7 +46,7 @@ const AWSCostConsent: React.FC<Props> = ({
           noWrapper
           expandText="[+] Show details"
           collapseText="[-] Hide details"
-          Header={<Cost>$224.58 / mo</Cost>}
+          Header={<Text size={20} weight={600}>$224.58 / mo</Text>}
           ExpandedSection={
             <>
               <Spacer height="15px" />
@@ -123,11 +123,6 @@ const AWSCostConsent: React.FC<Props> = ({
 
 export default AWSCostConsent;
 
-const Cost = styled.div`
-  font-weight: 600;
-  font-size: 20px;
-`;
-
 const Tab = styled.span`
   margin-left: 20px;
   height: 1px;

+ 1 - 1
dashboard/src/components/porter/Banner.tsx

@@ -36,7 +36,7 @@ const Banner: React.FC<Props> = ({
 
   return (
     <StyledBanner
-      color={type === "error" ? "#ff385d" : type === "warning" && "#f5cb42"}
+      color={type === "error" ? "#ff385d" : type === "warning" && "#F7A324"}
       noMargin={noMargin}
     >
       <>

+ 40 - 0
dashboard/src/components/porter/Image.tsx

@@ -0,0 +1,40 @@
+import React from "react";
+import styled from "styled-components";
+
+type Props = {
+  src: string | undefined;
+  size?: number;
+  opacity?: number;
+  additionalStyles?: string;
+  style?: React.CSSProperties;
+};
+
+const Icon: React.FC<Props> = ({
+  src,
+  size,
+  opacity,
+  additionalStyles,
+  style,
+}) => {
+  return (
+    <StyledIcon
+      src={src}
+      size={size}
+      opacity={opacity}
+      additionalStyles={additionalStyles}
+      style={style}
+    />
+  );
+};
+
+export default Icon;
+
+const StyledIcon = styled.img<{
+  size?: number;
+  opacity?: number;
+  additionalStyles?: string;
+}>`
+  height: ${props => props.size || 20}px;
+  opacity: ${props => props.opacity || 1};
+  ${props => props.additionalStyles ? props.additionalStyles : ""}
+`;

+ 78 - 16
dashboard/src/components/porter/Select.tsx

@@ -1,9 +1,16 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
 import styled from "styled-components";
+import arrow from "assets/arrow-down.svg";
+import Container from "./Container";
 
 type Props = {
   width?: string;
-  options: { label: string; value: string }[];
+  options: Array<{ 
+    label: string;
+    value: string;
+    icon?: HTMLImageElement | string;
+    disabled?: boolean;
+  }>;
   label?: string | React.ReactNode;
   labelColor?: string;
   height?: string;
@@ -12,6 +19,7 @@ type Props = {
   disabled?: boolean;
   value?: string;
   setValue?: (value: string) => void;
+  prefix?: React.ReactNode;
 };
 
 const Select: React.FC<Props> = ({
@@ -25,12 +33,30 @@ const Select: React.FC<Props> = ({
   disabled,
   value,
   setValue,
+  prefix,
 }) => {
+  const prefixRef = useRef<HTMLDivElement>(null);
+
   return (
     <Block width={width}>
       {label && <Label color={labelColor}>{label}</Label>}
       <SelectWrapper>
-        <i className="material-icons">arrow_drop_down</i>
+        <AbsoluteWrapper>
+        <Prefix>{prefix}</Prefix>
+        <Bar />
+        {options.map((option) => {
+          if (option.value === value) {
+            return (
+              <Container key={1} row>
+                {option.icon && <Img src={option?.icon} />}
+                {option.label}
+              </Container>
+            )
+          }
+          return null;
+        })}
+        <img src={arrow} />
+        </AbsoluteWrapper>
         <StyledSelect
           onChange={(e) => {
             setValue(e.target.value);
@@ -43,7 +69,7 @@ const Select: React.FC<Props> = ({
         >
           {options.map((option, i) => {
             return (
-              <option value={option.value} key={i}>
+              <option value={option.value} key={i} disabled={option.disabled}>
                 {option.label}
               </option>
             );
@@ -63,6 +89,43 @@ const Select: React.FC<Props> = ({
 
 export default Select;
 
+const Img = styled.img`
+  height: 16px;
+  margin-right: 10px;
+`;
+
+const AbsoluteWrapper = styled.div`
+  position: absolute;
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  > img {
+    width: 8px;
+    position: absolute;
+    right: 10px;
+    top: calc(50% - 3px);
+    z-index: -1;
+  }
+`;
+
+const Bar = styled.div`
+  width: 1px;
+  height: 15px;
+  background: #494b4f;
+  margin-left: 9px;
+  margin-right: 11px;
+`;
+
+const Prefix = styled.div`
+  margin-left: 10px;
+  font-size: 13px;
+  color: #aaaabb;
+  display: flex;
+  align-items: center;
+  z-index: -1;
+`;
+
 const Block = styled.div<{
   width: string;
 }>`
@@ -92,26 +155,28 @@ const Error = styled.div`
 
 const SelectWrapper = styled.div`
   position: relative;
-  background: #26292e;
+  background: ${(props) => props.theme.fg};
+  border: 1px solid ${(props) => (props.hasError ? "#ff3b62" : "#494b4f")};
+  :hover {
+    border: 1px solid ${(props) => (props.hasError ? "#ff3b62" : "#7a7b80")};
+  }
   z-index: 0;
+  display: flex;
+  align-items: center;
   border-radius: 5px;
+  font-size: 13px;
   overflow: hidden;
-  > i {
-    font-size: 18px;
-    position: absolute;
-    right: 7px;
-    top: calc(50% - 9px);
-    z-index: -1;
-  }
 `;
 
 const StyledSelect = styled.select<{
   width: string;
   height: string;
   hasError: boolean;
+  paddingLeft: number;
 }>`
   height: ${(props) => props.height || "35px"};
   padding: 5px 10px;
+  padding-left: ${(props) => props.paddingLeft}px;
   width: ${(props) => props.width || "200px"};
   color: #ffffff;
   font-size: 13px;
@@ -121,9 +186,6 @@ const StyledSelect = styled.select<{
   background: none;
   appearance: none;
   overflow: hidden;
+  opacity: 0;
   z-index: 1;
-  border: 1px solid ${(props) => (props.hasError ? "#ff3b62" : "#494b4f")};
-  :hover {
-    border: 1px solid ${(props) => (props.hasError ? "#ff3b62" : "#7a7b80")};
-  }
 `;

+ 8 - 2
dashboard/src/components/porter/Text.tsx

@@ -5,9 +5,11 @@ type Props = {
   size?: number;
   color?: string;
   weight?: number;
-  children: any;
+  children: React.ReactNode;
   additionalStyles?: string;
   truncate?: boolean;
+  onClick?: () => void;
+  style?: React.CSSProperties;
 };
 
 const Text: React.FC<Props> = ({
@@ -16,7 +18,9 @@ const Text: React.FC<Props> = ({
   color,
   children,
   additionalStyles,
-  truncate // added this
+  truncate, // added this
+  onClick,
+  style,
 }) => {
   const getColor = () => {
     switch (color) {
@@ -36,6 +40,8 @@ const Text: React.FC<Props> = ({
       weight={weight}
       additionalStyles={additionalStyles}
       truncate={truncate}
+      onClick={onClick}
+      style={style}
     >
       {children}
     </StyledText>

+ 84 - 62
dashboard/src/main/home/Home.tsx

@@ -1,55 +1,63 @@
 import React, { useContext, useEffect, useRef, useState } from "react";
 import { createPortal } from "react-dom";
-import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
+import {
+  Route,
+  Switch,
+  withRouter,
+  type RouteComponentProps,
+} from "react-router";
 import styled, { ThemeProvider } from "styled-components";
 
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { PorterUrl, pushFiltered, pushQueryParams } from "shared/routing";
-import midnight from "shared/themes/midnight";
-import standard from "shared/themes/standard";
-import { ClusterType, ProjectListType, ProjectType } from "shared/types";
-
 import ConfirmOverlay from "components/ConfirmOverlay";
 import Loading from "components/Loading";
-import DashboardRouter from "./cluster-dashboard/DashboardRouter";
-import Dashboard from "./dashboard/Dashboard";
-import Integrations from "./integrations/Integrations";
-import LaunchWrapper from "./launch/LaunchWrapper";
-
-import AddOnDashboard from "./add-on-dashboard/AddOnDashboard";
-import AppDashboard from "./app-dashboard/AppDashboard";
-import CreateDatabase from "./database-dashboard/CreateDatabase";
-import DatabaseDashboard from "./database-dashboard/DatabaseDashboard";
-import Navbar from "./navbar/Navbar";
-import ProjectSettings from "./project-settings/ProjectSettings";
-import Sidebar from "./sidebar/Sidebar";
-
 import NoClusterPlaceHolder from "components/NoClusterPlaceHolder";
 import Button from "components/porter/Button";
 import Modal from "components/porter/Modal";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
-import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
+
+import api from "shared/api";
+import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { fakeGuardedRoute } from "shared/auth/RouteGuard";
 import ClusterResourcesProvider from "shared/ClusterResourcesContext";
+import { Context } from "shared/Context";
 import DeploymentTargetProvider from "shared/DeploymentTargetContext";
+import { pushFiltered, pushQueryParams, type PorterUrl } from "shared/routing";
+import midnight from "shared/themes/midnight";
+import standard from "shared/themes/standard";
+import {
+  type ClusterType,
+  type ProjectListType,
+  type ProjectType,
+} from "shared/types";
 import { overrideInfraTabEnabled } from "utils/infrastructure";
+
 import discordLogo from "../../assets/discord.svg";
+import AddOnDashboard from "./add-on-dashboard/AddOnDashboard";
 import NewAddOnFlow from "./add-on-dashboard/NewAddOnFlow";
 import AppView from "./app-dashboard/app-view/AppView";
+import AppDashboard from "./app-dashboard/AppDashboard";
 import Apps from "./app-dashboard/apps/Apps";
 import CreateApp from "./app-dashboard/create-app/CreateApp";
 import ExpandedApp from "./app-dashboard/expanded-app/ExpandedApp";
 import NewAppFlow from "./app-dashboard/new-app-flow/NewAppFlow";
+import DashboardRouter from "./cluster-dashboard/DashboardRouter";
 import PreviewEnvs from "./cluster-dashboard/preview-environments/v2/PreviewEnvs";
 import SetupApp from "./cluster-dashboard/preview-environments/v2/setup-app/SetupApp";
+import ComplianceDashboard from "./compliance-dashboard/ComplianceDashboard";
+import Dashboard from "./dashboard/Dashboard";
+import CreateDatabase from "./database-dashboard/CreateDatabase";
+import DatabaseDashboard from "./database-dashboard/DatabaseDashboard";
 import DatabaseView from "./database-dashboard/DatabaseView";
 import InfrastructureRouter from "./infrastructure/InfrastructureRouter";
+import Integrations from "./integrations/Integrations";
+import LaunchWrapper from "./launch/LaunchWrapper";
 import ModalHandler from "./ModalHandler";
+import Navbar from "./navbar/Navbar";
 import { NewProjectFC } from "./new-project/NewProject";
 import Onboarding from "./onboarding/Onboarding";
-
+import ProjectSettings from "./project-settings/ProjectSettings";
+import Sidebar from "./sidebar/Sidebar";
 
 // Guarded components
 const GuardedProjectSettings = fakeGuardedRoute("settings", "", [
@@ -125,10 +133,10 @@ const Home: React.FC<Props> = (props) => {
   };
 
   const getProjects = async (id?: number) => {
-    let { currentProject } = props;
-    let queryString = window.location.search;
-    let urlParams = new URLSearchParams(queryString);
-    let projectId = urlParams.get("project_id");
+    const { currentProject } = props;
+    const queryString = window.location.search;
+    const urlParams = new URLSearchParams(queryString);
+    const projectId = urlParams.get("project_id");
     if (!projectId && currentProject?.id) {
       pushQueryParams(props, { project_id: currentProject.id.toString() });
     }
@@ -156,7 +164,7 @@ const Home: React.FC<Props> = (props) => {
         }
 
         const project = await api
-          .getProject("<token>", {}, { id: id })
+          .getProject("<token>", {}, { id })
           .then((res) => res.data as ProjectType);
 
         setCurrentProject(project);
@@ -200,24 +208,24 @@ const Home: React.FC<Props> = (props) => {
       } else {
         setHasFinishedOnboarding(true);
       }
-    } catch (error) { }
+    } catch (error) {}
   };
 
   useEffect(() => {
     checkOnboarding();
     checkIfCanCreateProject();
-    let { match } = props;
+    const { match } = props;
 
     // Handle redirect from DO
-    let queryString = window.location.search;
-    let urlParams = new URLSearchParams(queryString);
+    const queryString = window.location.search;
+    const urlParams = new URLSearchParams(queryString);
 
-    let err = urlParams.get("error");
+    const err = urlParams.get("error");
     if (err) {
       setCurrentError(err);
     }
 
-    let defaultProjectId = parseInt(urlParams.get("project_id"));
+    const defaultProjectId = parseInt(urlParams.get("project_id"));
 
     setGhRedirect(urlParams.get("gh_oauth") !== null);
     urlParams.delete("gh_oauth");
@@ -290,9 +298,9 @@ const Home: React.FC<Props> = (props) => {
   }, [props.currentProject?.id]);
 
   useEffect(() => {
-    let queryString = window.location.search;
-    let urlParams = new URLSearchParams(queryString);
-    let err = urlParams.get("error");
+    const queryString = window.location.search;
+    const urlParams = new URLSearchParams(queryString);
+    const err = urlParams.get("error");
     if (
       !hasFinishedOnboarding &&
       props.history.location.pathname &&
@@ -328,7 +336,9 @@ const Home: React.FC<Props> = (props) => {
 
       setProjects(projectList);
       if (!projectList.length) {
-        setCurrentProject(null, () => redirectToNewProject());
+        setCurrentProject(null, () => {
+          redirectToNewProject();
+        });
       } else {
         const project = await api
           .getProject("<token>", {}, { id: projectList[0].id })
@@ -362,18 +372,18 @@ const Home: React.FC<Props> = (props) => {
 
     try {
       const res = await api.getClusters<
-        {
+        Array<{
           infra_id?: number;
           name: string;
-        }[]
+        }>
       >("<token>", {}, { id: currentProject?.id });
 
-      const destroyInfraPromises = res.data.map((cluster) => {
+      const destroyInfraPromises = res.data.map(async (cluster) => {
         if (!cluster.infra_id) {
           return undefined;
         }
 
-        return api.destroyInfra(
+        return await api.destroyInfra(
           "<token>",
           {},
           { project_id: currentProject.id, infra_id: cluster.infra_id }
@@ -418,7 +428,10 @@ const Home: React.FC<Props> = (props) => {
                 setRefreshClusters={setForceRefreshClusters}
               />
             ) : (
-              <DiscordButton href="https://discord.gg/34n7NN7FJ7" target="_blank">
+              <DiscordButton
+                href="https://discord.gg/34n7NN7FJ7"
+                target="_blank"
+              >
                 <Icon src={discordLogo} />
                 Join Our Discord
               </DiscordButton>
@@ -474,6 +487,11 @@ const Home: React.FC<Props> = (props) => {
                 <Route path="/databases">
                   <DatabaseDashboard />
                 </Route>
+
+                <Route path="/compliance">
+                  <ComplianceDashboard />
+                </Route>
+
                 <Route path="/addons/new">
                   <NewAddOnFlow />
                 </Route>
@@ -496,17 +514,17 @@ const Home: React.FC<Props> = (props) => {
                   overrideInfraTabEnabled({
                     projectID: currentProject?.id,
                   })) && (
-                    <Route
-                      path="/infrastructure"
-                      render={() => {
-                        return (
-                          <DashboardWrapper>
-                            <InfrastructureRouter />
-                          </DashboardWrapper>
-                        );
-                      }}
-                    />
-                  )}
+                  <Route
+                    path="/infrastructure"
+                    render={() => {
+                      return (
+                        <DashboardWrapper>
+                          <InfrastructureRouter />
+                        </DashboardWrapper>
+                      );
+                    }}
+                  />
+                )}
                 <Route
                   path="/dashboard"
                   render={() => {
@@ -535,7 +553,7 @@ const Home: React.FC<Props> = (props) => {
                   render={() => {
                     if (currentCluster?.id === -1) {
                       return <Loading />;
-                    } else if (!currentCluster || !currentCluster.name) {
+                    } else if (!currentCluster?.name) {
                       return (
                         <DashboardWrapper>
                           <NoClusterPlaceHolder></NoClusterPlaceHolder>
@@ -563,7 +581,7 @@ const Home: React.FC<Props> = (props) => {
                   render={() => <GuardedProjectSettings />}
                 />
                 {currentProject?.validate_apply_v2 &&
-                  currentProject.preview_envs_enabled ? (
+                currentProject.preview_envs_enabled ? (
                   <>
                     <Route exact path="/preview-environments/configure">
                       <SetupApp />
@@ -597,7 +615,9 @@ const Home: React.FC<Props> = (props) => {
                     : ""
                 }
                 onYes={handleDelete}
-                onNo={() => setCurrentModal(null, null)}
+                onNo={() => {
+                  setCurrentModal(null, null);
+                }}
               />,
               document.body
             )}
@@ -608,14 +628,14 @@ const Home: React.FC<Props> = (props) => {
                 </Text>
                 <Spacer y={1} />
                 <Text color="helper">
-                  Your account email does not match the email associated with this
-                  project invite. Please log out and sign up again with the
+                  Your account email does not match the email associated with
+                  this project invite. Please log out and sign up again with the
                   correct email using the invite link.
                 </Text>
                 <Spacer y={1} />
                 <Text color="helper">
-                  You should reach out to the person who sent you the invite link
-                  to get the correct email.
+                  You should reach out to the person who sent you the invite
+                  link to get the correct email.
                 </Text>
                 <Spacer y={1} />
                 <Button onClick={props.logOut}>Log out</Button>
@@ -684,7 +704,9 @@ const DiscordButton = styled.a`
   border-radius: 3px;
   color: #ffffff44;
   height: 40px;
-  font-family: Work Sans, sans-serif;
+  font-family:
+    Work Sans,
+    sans-serif;
   font-size: 14px;
   font-weight: bold;
   cursor: pointer;

+ 533 - 0
dashboard/src/main/home/compliance-dashboard/ComplianceDashboard.tsx

@@ -0,0 +1,533 @@
+import React, { useState } from "react";
+import _, { set } from "lodash";
+import styled from "styled-components";
+
+import Spacer from "components/porter/Spacer";
+import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader";
+
+import compliance from "assets/compliance.svg";
+import Container from "components/porter/Container";
+
+import Text from "components/porter/Text";
+import Select from "components/porter/Select";
+import Image from "components/porter/Image";
+import Banner from "components/porter/Banner";
+import Modal from "components/porter/Modal";
+import ExpandableSection from "components/porter/ExpandableSection";
+import Fieldset from "components/porter/Fieldset";
+import Link from "components/porter/Link";
+import Input from "components/porter/Input";
+import Button from "components/porter/Button";
+
+import framework from "assets/framework.svg";
+import typeSvg from "assets/type.svg";
+import provider from "assets/provider.svg";
+import aws from "assets/aws.png";
+import vanta from "assets/vanta.svg";
+import linkExternal from "assets/link-external.svg";
+import greenCheck from "assets/green-check.svg";
+import warning from "assets/warning.svg";
+import notApplicable from "assets/not-applicable.svg";
+import loading from "assets/loading.gif";
+import refresh from "assets/refresh.png";
+
+type Props = {
+  projectId: number;
+};
+
+const dummyChecks = [
+  {
+    status: "not-applicable",
+    name: "Application changes reviewed",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "not-applicable",
+    name: "AWS accounts deprovisioned when employees leave",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "not-applicable",
+    name: "AWS accounts reviewed",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "passing",
+    name: "CloudTrail enabled",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "not-applicable",
+    name: "Company has a version control system",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "not-applicable",
+    name: "Critical vulnerabilities identified in packages are addressed (AWS Container)",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "not-applicable",
+    name: "Critical vulnerabilities identified in packages are addressed (AWS Inspector)",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "action-required",
+    name: "Database IO monitored (AWS)",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "passing",
+    name: "CloudTrail enabled",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "not-applicable",
+    name: "Company has a version control system",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "not-applicable",
+    name: "Critical vulnerabilities identified in packages are addressed (AWS Container)",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "not-applicable",
+    name: "Critical vulnerabilities identified in packages are addressed (AWS Inspector)",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+  {
+    status: "action-required",
+    name: "Database IO monitored (AWS)",
+    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+  },
+];
+
+const ComplianceDashboard: React.FC<Props> = () => {
+  const [actionRequired, setActionRequired] = useState(true); // TODO: replace with actual data
+  const [provisioningError, setProvisioningError] = useState(""); // TODO: replace with actual data
+  const [provisioningStatus, setProvisioningStatus] = useState("");
+  const [statusFilter, setStatusFilter] = useState("all");
+  const [confirmCost, setConfirmCost] = useState("");
+  const [showCostConsentModal, setShowCostConsentModal] = useState(false);
+  const [showExpandedErrorModal, setShowExpandedErrorModal] = useState(false);
+  const [expandedCheck, setExpandedCheck] = useState<{ 
+    status: string, name: string, link: string 
+  } | null>(null);
+
+  // TODO: implement
+  const updateInfrastructure = (): void => {
+    setProvisioningError("");
+    setProvisioningStatus("pending");
+
+    setTimeout(() => {
+      setProvisioningStatus("failed");
+      setProvisioningError("Error: Some step failed");
+    }, 2000);
+  };
+
+  return (
+    <StyledComplianceDashboard>
+      <DashboardHeader
+        image={compliance}
+        title="Compliance"
+        description="Configure your Porter infrastructure for various compliance frameworks."
+        disableLineBreak
+      />
+      <Container row>
+        <Select
+          options={[
+            { value: "soc-2", label: "SOC 2" },
+            { value: "hipaa", label: "HIPAA (request access)", disabled: true },
+          ]}
+          width="200px"
+          value={"soc-2"}
+          setValue={() => {
+          }
+          }
+          prefix={
+            <Container row>
+              <Image src={framework} size={15} opacity={0.6} />
+              <Spacer inline x={0.5} />
+              Framework
+            </Container>
+          }
+        />
+        <Spacer inline x={1} />
+        <Select
+          options={[
+            { value: "aws", label: "AWS", icon: aws },
+            { value: "gcp", label: "Google Cloud (coming soon)", disabled: true },
+            { value: "azure", label: "Azure (coming soon)", disabled: true },
+          ]}
+          width="180px"
+          value={"aws"}
+          setValue={() => {
+          }
+          }
+          prefix={
+            <Container row>
+              <Image src={typeSvg} size={15} opacity={0.6} />
+              <Spacer inline x={0.5} />
+              Type
+            </Container>
+          }
+        />
+        <Spacer inline x={1} />
+        <Select
+          options={[
+            { value: "vanta", label: "Vanta", icon: vanta },
+            { value: "drata", label: "Drata (coming soon)", disabled: true },
+            { value: "oneleet", label: "Oneleet (coming soon)", disabled: true },
+          ]}
+          width="200px"
+          value={"vanta"}
+          setValue={() => {
+          }
+          }
+          prefix={
+            <Container row>
+              <Image src={provider} size={15} opacity={.6} />
+              <Spacer inline x={.5} />
+              Provider
+            </Container>
+          }
+        />
+      </Container>
+
+      <Spacer y={1} />
+
+      <Container row>
+        <Image src={vanta} size={25} />
+        <Spacer inline x={1} />
+        <Text 
+          size={21}
+          additionalStyles=":hover { text-decoration: underline } cursor: pointer;"
+          onClick={() => {
+            window.open("https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST", "_blank")
+          }}
+        >
+          AWS SOC 2 Controls (Vanta)
+          <Spacer inline x={.5} />
+          <Image src={linkExternal} size={16} additionalStyles="margin-bottom: -2px"/>
+        </Text>
+      </Container>
+
+      <Spacer y={1} />
+
+      {
+        actionRequired &&
+        provisioningStatus !== "pending" &&
+        provisioningStatus !== "failed" && (
+          <>
+            <Banner type="warning">
+              Action is required to pass additional controls.
+              <Spacer inline x={.5} />
+              <Text
+                style={{
+                  textDecoration: "underline",
+                  cursor: "pointer"
+                }}
+                onClick={() => {
+                  setShowCostConsentModal(true);
+                }}
+              >
+                Enable SOC 2 infrastructure controls
+              </Text>
+            </Banner>
+            <Spacer y={1} />
+          </>
+        )
+      }
+      {provisioningStatus === "pending" && (
+        <>
+          <Banner icon={<Image src={loading} style={{ height: "16px", width: "16px" }} />}>
+            SOC 2 infrastructure controls are being enabled. Note: This may take up to 30 minutes.
+          </Banner>
+          <Spacer y={1} />
+        </>
+      )}
+      {provisioningError && (
+        <>
+          <Banner type="error">
+            {provisioningError}
+            <Spacer inline x={1} />
+            <Text
+              style={{
+                textDecoration: "underline",
+                cursor: "pointer"
+              }}
+              onClick={() => {
+                setShowExpandedErrorModal(true);
+              }}
+            >
+              Learn more
+            </Text>
+            <Spacer inline x={1} />
+            <Text
+              style={{
+                textDecoration: "underline",
+                cursor: "pointer"
+              }}
+              onClick={updateInfrastructure}
+            >
+              <Image src={refresh} size={12} style={{ marginBottom: "-2px" }} />
+              <Spacer inline x={.5} />
+              Retry update
+            </Text>
+          </Banner>
+          <Spacer y={1} />
+        </>
+      )}
+
+      <Container row>
+        <PanelFilter
+          isActive={statusFilter === "all"}
+          onClick={() => {
+            setStatusFilter("all");
+          }}
+        >
+          <Text color="helper">All</Text>
+          <Spacer y={.2} />
+          <Text size={18}>45</Text>
+        </PanelFilter>
+        <Spacer inline x={1.5} />
+        <PanelFilter
+          isActive={statusFilter === "passing"}
+          onClick={() => {
+            setStatusFilter("passing");
+          }}
+        >
+          <Container row>
+            <Image src={greenCheck} size={10} />
+            <Spacer inline x={.5} />
+            <Text color="helper">Passing</Text>
+          </Container>
+          <Spacer y={.2} />
+          <Text size={18}>3</Text>
+        </PanelFilter>
+        <Spacer inline x={1.5} />
+        <PanelFilter
+          isActive={statusFilter === "action-required"}
+          onClick={() => {
+            setStatusFilter("action-required");
+          }}
+        >
+          <Container row>
+            <Image src={warning} size={12} />
+            <Spacer inline x={.5} />
+            <Text color="helper">Action required</Text>
+          </Container>
+          <Spacer y={.2} />
+          <Text size={18}>17</Text>
+        </PanelFilter>
+        <Spacer inline x={1.5} />
+        <PanelFilter
+          isActive={statusFilter === "not-applicable"}
+          onClick={() => {
+            setStatusFilter("not-applicable");
+          }}
+        >
+          <Container row>
+            <Image src={notApplicable} size={12} />
+            <Spacer inline x={.5} />
+            <Text color="helper">Not applicable</Text>
+          </Container>
+          <Spacer y={.2} />
+          <Text size={18}>25</Text>
+        </PanelFilter>
+      </Container>
+
+      <Spacer y={1.5} />
+
+      {dummyChecks.map((check, i) => {
+        return (
+          <>
+            <Container row key={i}>
+              <Container style={{ width: "200px" }} row>
+                {check.status === "passing" && <Image src={greenCheck} size={10} />}
+                {check.status === "action-required" && (
+                  <Image src={warning} size={14} />
+                )}
+                {check.status === "not-applicable" && (
+                  <Image src={notApplicable} size={14} />
+                )}
+                <Spacer inline x={.7} />
+                {check.status === "passing" && <Text color="helper">Passing</Text>}
+                {check.status === "action-required" && (
+                  <ActionRequired>
+                    <Text color="helper">Action required</Text>
+                    <Spacer inline x={.5} />
+                    <i 
+                      className="material-icons-outlined"
+                      onClick={() => { setExpandedCheck(check) }}
+                    >
+                      help_outline
+                    </i>
+                  </ActionRequired>
+                )}
+                {check.status === "not-applicable" && <Text color="#494B4F">Not applicable</Text>}
+              </Container>
+              <Text
+                color={check.status === "not-applicable" ? "#494B4F" : ""}
+                style={{ 
+                  marginBottom: "-1px",
+                  cursor: "pointer",
+                }}
+                additionalStyles=":hover { text-decoration: underline }"
+                onClick={() => {
+                  window.open(check.link, "_blank");
+                }}
+              >
+                {check.name}
+                <Spacer inline x={.5} />
+                <Image 
+                  src={linkExternal}
+                  opacity={check.status === "not-applicable" ? 0.25 : 1}
+                  size={12}
+                  additionalStyles="margin-bottom: -2px"
+                />
+              </Text>
+            </Container>
+            <Spacer y={1} />
+          </>
+        );
+      })}
+
+      <Spacer y={2} />
+
+      {showExpandedErrorModal && (
+        <Modal closeModal={() => { setShowExpandedErrorModal(false) }}>
+          <Container row>
+            <Text size={16}>
+              Error enabling AWS SOC 2 controls
+            </Text>
+          </Container>
+          <Spacer y={.7} />
+          <Text color="helper">
+            {provisioningError}
+          </Text>
+        </Modal>
+      )}
+      {expandedCheck && (
+        <Modal closeModal={() => { setExpandedCheck(null) }}>
+          <Container row>
+            <Image src={warning} size={16} />
+            <Spacer inline x={.7} />
+            <Text size={16}>
+              Action required for "{expandedCheck.name}"
+            </Text>
+          </Container>
+          <Spacer y={.7} />
+          <Text color="helper">
+            Porter is unable to automatically resolve this control. Please follow xyz instructions in order to xyz.
+          </Text>
+        </Modal>
+      )}
+      {showCostConsentModal && (
+        <Modal closeModal={() => { setShowCostConsentModal(false) }}>
+          <Text size={16}>SOC 2 cost consent (TODO)</Text>
+          <Spacer height="15px" />
+          <Text color="helper">
+            Porter will create the underlying infrastructure in your own AWS
+            account. You will be separately charged by AWS for this
+            infrastructure. The cost for this base infrastructure is as follows:
+          </Text>
+          <Spacer y={1} />
+          <ExpandableSection
+            noWrapper
+            expandText="[+] Show details"
+            collapseText="[-] Hide details"
+            Header={<Text size={20} weight={600}>$224.58 / mo</Text>}
+            ExpandedSection={
+              <>
+                <Spacer height="15px" />
+                <Fieldset background="#1b1d2688">
+                  • Amazon Elastic Kubernetes Service (EKS) = $73/mo
+                  <Spacer height="15px" />
+                  • Amazon EC2:
+                  <Spacer height="15px" />
+                  <Tab />+ System workloads: t3.medium instance (2) = $60.74/mo
+                  <Spacer height="15px" />
+                  <Tab />+ Monitoring workloads: t3.large instance (1) = $60.74/mo
+                  <Spacer height="15px" />
+                  <Tab />+ Application workloads: t3.medium instance (1) =
+                  $30.1/mo
+                </Fieldset>
+              </>
+            }
+          />
+          <Spacer y={1} />
+          <Text color="helper">
+            The base AWS infrastructure covers up to 2 vCPU and 4GB of RAM.
+            Separate from the AWS cost, Porter charges based on your resource
+            usage.
+          </Text>
+          <Spacer inline width="5px" />
+          <Spacer y={0.5} />
+          <Link hasunderline to="https://porter.run/pricing" target="_blank">
+            Learn more about our pricing.
+          </Link>
+          <Spacer y={1} />
+          <Input
+            placeholder="224.58"
+            value={confirmCost}
+            setValue={setConfirmCost}
+            width="100%"
+            height="40px"
+          />
+          <Spacer y={1} />
+          <Button
+            disabled={confirmCost !== "224.58"}
+            onClick={() => {
+              setConfirmCost("");
+              updateInfrastructure();
+              setShowCostConsentModal(false);
+            }}
+          >
+            Enable SOC 2 infra controls
+          </Button>
+        </Modal>
+      )}
+    </StyledComplianceDashboard>
+  );
+};
+
+export default ComplianceDashboard;
+
+const ActionRequired = styled.div`
+  > i {
+    font-size: 15px;
+    color: #aaaabb;
+    cursor: pointer;
+    :hover {
+      color: #ffffff;
+    }
+  }
+  display: flex;
+  align-items: center;
+`;
+
+const Tab = styled.span`
+  margin-left: 20px;
+  height: 1px;
+`;
+
+const PanelFilter = styled.div<{ isActive: boolean }>`
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  padding: 10px 15px;
+  cursor: pointer;
+  border-radius: 5px;
+  background: ${(props) => props.theme.clickable.bg};
+  border: 1px solid ${(props) => props.isActive ? "#fefefe" : "#494b4f"};
+  :hover {
+    ${(props) => !props.isActive && "border: 1px solid #7a7b80;"}
+  }
+`;
+
+const StyledComplianceDashboard = styled.div`
+  width: 100%;
+  height: 100%;
+`;

+ 12 - 1
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -12,6 +12,7 @@ import addOns from "assets/add-ons.svg";
 import database from "assets/database.svg";
 import collapseSidebar from "assets/collapse-sidebar.svg";
 import pr_icon from "assets/pull_request_icon.svg";
+import compliance from "assets/compliance.svg";
 
 import { Context } from "shared/Context";
 
@@ -140,7 +141,8 @@ class Sidebar extends Component<PropsType, StateType> {
                 <i className="material-icons">build_circle</i>
                 Infrastructure
               </NavButton>
-            )}
+            )
+          }
           {this.props.isAuthorized("integrations", "", [
             "get",
             "create",
@@ -330,6 +332,15 @@ class Sidebar extends Component<PropsType, StateType> {
               </NavButton>
             )}
 
+            {currentProject?.soc2_controls_enabled && (
+              <NavButton 
+                path="/compliance"
+              >
+                <Img src={compliance} />
+                Compliance
+              </NavButton>
+            )}
+
             {this.props.isAuthorized("integrations", "", [
               "get",
               "create",

+ 2 - 0
dashboard/src/shared/routing.tsx

@@ -16,6 +16,7 @@ export type PorterUrl =
   | "preview-environments"
   | "apps"
   | "addons"
+  | "compliance"
   | "stacks";
 
 export const PorterUrls = [
@@ -34,6 +35,7 @@ export const PorterUrls = [
   "preview-environments",
   "apps",
   "addons",
+  "compliance",
   "stacks",
 ];
 

+ 1 - 1
dashboard/src/shared/themes/standard.ts

@@ -7,7 +7,7 @@ const theme = {
     bg: "linear-gradient(180deg, #26292e, #24272c)",
   },
   text: {
-    primary: "#ffffff",
+    primary: "#fefefe",
   },
 }
 

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно