Bladeren bron

Implemented first version of onboarding flow with new project screen separated

jnfrati 4 jaren geleden
bovenliggende
commit
a065c62df6

+ 13 - 0
dashboard/package-lock.json

@@ -9185,6 +9185,11 @@
         "ipaddr.js": "1.9.1"
       }
     },
+    "proxy-compare": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.0.2.tgz",
+      "integrity": "sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A=="
+    },
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -11456,6 +11461,14 @@
       "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==",
       "dev": true
     },
+    "valtio": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.2.4.tgz",
+      "integrity": "sha512-FipYZHGJXsSKObKNGOHwqqiA6T84T7LHhrPfM7ptt3e2uFao4djD5/u4JEb/z2O14fv1CFxIO05UWCuk3VT/qg==",
+      "requires": {
+        "proxy-compare": "2.0.2"
+      }
+    },
     "value-equal": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",

+ 2 - 1
dashboard/package.json

@@ -44,7 +44,8 @@
     "regenerator-runtime": "^0.13.9",
     "semver": "^7.3.5",
     "stacktrace-js": "^2.0.2",
-    "styled-components": "^5.2.0"
+    "styled-components": "^5.2.0",
+    "valtio": "^1.2.4"
   },
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",

+ 2 - 0
dashboard/src/components/form-components/InputRow.tsx

@@ -14,6 +14,7 @@ type PropsType = {
   disabled?: boolean;
   isRequired?: boolean;
   className?: string;
+  maxLength?: number;
 };
 
 type StateType = {
@@ -74,6 +75,7 @@ export default class InputRow extends Component<PropsType, StateType> {
             type={type}
             value={value}
             onChange={this.handleChange}
+            maxLength={this.props.maxLength}
           />
           {unit ? <Unit>{unit}</Unit> : null}
         </InputWrapper>

+ 1 - 0
dashboard/src/main/Main.tsx

@@ -182,6 +182,7 @@ export default class Main extends Component<PropsType, StateType> {
           path={`/:baseRoute/:cluster?/:namespace?`}
           render={(routeProps) => {
             const baseRoute = routeProps.match.params.baseRoute;
+            console.log(PorterUrls.includes(baseRoute), baseRoute);
             if (
               this.state.isLoggedIn &&
               this.state.initialized &&

+ 5 - 3
dashboard/src/main/home/Home.tsx

@@ -32,6 +32,7 @@ import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import EditInviteOrCollaboratorModal from "./modals/EditInviteOrCollaboratorModal";
 import AccountSettingsModal from "./modals/AccountSettingsModal";
 import discordLogo from "../../assets/discord.svg";
+import Onboarding from "./onboarding/Onboarding";
 // Guarded components
 const GuardedProjectSettings = fakeGuardedRoute("settings", "", [
   "get",
@@ -336,8 +337,9 @@ class Home extends Component<PropsType, StateType> {
 
   renderContents = () => {
     let currentView = this.props.currentRoute;
+    console.log({ currentView });
 
-    if (this.context.currentProject && currentView !== "new-project") {
+    if (this.context.currentProject && currentView !== "onboarding") {
       if (
         currentView === "cluster-dashboard" ||
         currentView === "applications" ||
@@ -362,8 +364,8 @@ class Home extends Component<PropsType, StateType> {
         return <GuardedProjectSettings />;
       }
       return <Templates />;
-    } else if (currentView === "new-project") {
-      return <NewProject />;
+    } else if (currentView === "onboarding") {
+      return <Onboarding />;
     }
   };
 

+ 125 - 0
dashboard/src/main/home/onboarding/NewProject.tsx

@@ -0,0 +1,125 @@
+import React, { useEffect } from "react";
+import styled from "styled-components";
+
+import gradient from "assets/gradient.png";
+import { isAlphanumeric } from "shared/common";
+
+import InputRow from "components/form-components/InputRow";
+import Helper from "components/form-components/Helper";
+import TitleSection from "components/TitleSection";
+import { useSnapshot } from "valtio";
+import { actions, OnboardingState } from "./OnboardingState";
+
+export const NewProjectFC = () => {
+  const snap = useSnapshot(OnboardingState);
+
+  useEffect(() => {
+    if (snap.userId !== null) {
+      window.analytics.track("provision_new-project", {
+        userId: snap.userId,
+      });
+    }
+  }, [snap.userId]);
+
+  return (
+    <StyledNewProject>
+      <TitleSection>New Project</TitleSection>
+      <Helper>
+        Project name
+        <Warning
+          highlight={
+            !isAlphanumeric(snap.projectName) && snap.projectName !== ""
+          }
+        >
+          (lowercase letters, numbers, and "-" only) 25 letters max length
+        </Warning>
+        <Required>*</Required>
+      </Helper>
+      <InputWrapper>
+        <ProjectIcon>
+          <ProjectImage src={gradient} />
+          <Letter>
+            {snap.projectName ? snap.projectName[0].toUpperCase() : "-"}
+          </Letter>
+        </ProjectIcon>
+        <InputRow
+          type="string"
+          value={snap.projectName}
+          setValue={(x: string) => actions.setProjectName(x)}
+          placeholder="ex: perspective-vortex"
+          width="470px"
+          maxLength={25}
+        />
+      </InputWrapper>
+    </StyledNewProject>
+  );
+};
+
+const Required = styled.div`
+  margin-left: 8px;
+  color: #fc4976;
+  display: inline-block;
+`;
+
+const Letter = styled.div`
+  height: 100%;
+  width: 100%;
+  position: absolute;
+  background: #00000028;
+  top: 0;
+  left: 0;
+  display: flex;
+  color: white;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  font-weight: 500;
+  font-family: "Work Sans", sans-serif;
+`;
+
+const ProjectImage = styled.img`
+  width: 100%;
+  height: 100%;
+`;
+
+const ProjectIcon = styled.div`
+  width: 45px;
+  min-width: 45px;
+  height: 45px;
+  border-radius: 5px;
+  overflow: hidden;
+  position: relative;
+  margin-right: 15px;
+  font-weight: 400;
+  margin-top: 9px;
+`;
+
+const InputWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: -15px;
+`;
+
+const Warning = styled.span`
+  color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.highlight ? "#f5cb42" : ""};
+  margin-left: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.makeFlush ? "" : "5px"};
+`;
+
+const Title = styled.div`
+  font-size: 20px;
+  font-weight: 500;
+  font-family: "Work Sans", sans-serif;
+  color: #ffffff;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const StyledNewProject = styled.div`
+  width: calc(90% - 130px);
+  min-width: 300px;
+  position: relative;
+  margin-top: calc(50vh - 340px);
+`;

+ 16 - 0
dashboard/src/main/home/onboarding/Onboarding.tsx

@@ -0,0 +1,16 @@
+import React, { useContext, useEffect } from "react";
+import { Context } from "shared/Context";
+import { actions } from "./OnboardingState";
+import Routes from "./Routes";
+
+const Onboarding = () => {
+  const context = useContext(Context);
+
+  useEffect(() => {
+    actions.initFromGlobalContext(context);
+  }, [context]);
+
+  return <Routes />;
+};
+
+export default Onboarding;

+ 43 - 0
dashboard/src/main/home/onboarding/OnboardingState.ts

@@ -0,0 +1,43 @@
+import { ContextProps } from "shared/types";
+import { proxy } from "valtio";
+
+export type OnboardingStateType = {
+  projectName: string;
+  // Null when is not setted yet.
+  isProvisionerEnabled: boolean | null;
+  userId: number | null;
+};
+
+export const OnboardingState = proxy<OnboardingStateType>({
+  projectName: "",
+  isProvisionerEnabled: null,
+  userId: null,
+});
+
+export const actions = {
+  setProjectName: (name: string) => {
+    OnboardingState.projectName = name;
+  },
+  setIsProvisionerEnabled: (provStatus: boolean) => {
+    OnboardingState.isProvisionerEnabled = provStatus;
+  },
+  setUserId: (userId: number) => {
+    OnboardingState.userId = userId;
+  },
+  initFromGlobalContext: (context: Partial<ContextProps>) => {
+    const provisionerStatus = context?.capabilities?.provisioner;
+
+    if (typeof provisionerStatus === "boolean") {
+      actions.setIsProvisionerEnabled(provisionerStatus);
+    } else {
+      actions.setIsProvisionerEnabled(null);
+    }
+
+    const userId = context?.user?.id;
+    if (typeof userId === "boolean") {
+      actions.setIsProvisionerEnabled(userId);
+    } else {
+      actions.setIsProvisionerEnabled(null);
+    }
+  },
+};

+ 29 - 0
dashboard/src/main/home/onboarding/Routes.tsx

@@ -0,0 +1,29 @@
+import React from "react";
+import { Route, Switch, useRouteMatch } from "react-router";
+import { useSnapshot } from "valtio";
+import ProvisionerSettings from "../provisioner/ProvisionerSettings";
+import { NewProjectFC } from "./NewProject";
+import { OnboardingState } from "./OnboardingState";
+
+export const Routes = () => {
+  const snap = useSnapshot(OnboardingState);
+
+  return (
+    <>
+      <Switch>
+        <Route path={`/onboarding/new-project`}>
+          <NewProjectFC />
+        </Route>
+        <Route path={`/onboarding/provision`}>
+          <ProvisionerSettings
+            isInNewProject={true}
+            projectName={snap.projectName}
+            provisioner={snap.isProvisionerEnabled}
+          />
+        </Route>
+      </Switch>
+    </>
+  );
+};
+
+export default Routes;

+ 3 - 1
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -78,7 +78,9 @@ class ProjectSection extends Component<PropsType, StateType> {
               selected={false}
               lastItem={true}
               onClick={() =>
-                pushFiltered(this.props, "/new-project", ["project_id"])
+                pushFiltered(this.props, "/onboarding/new-project", [
+                  "project_id",
+                ])
               }
             >
               <ProjectIconAlt>+</ProjectIconAlt>

+ 3 - 1
dashboard/src/shared/routing.tsx

@@ -7,7 +7,8 @@ export type PorterUrl =
   | "project-settings"
   | "applications"
   | "env-groups"
-  | "jobs";
+  | "jobs"
+  | "onboarding";
 
 export const PorterUrls = [
   "dashboard",
@@ -19,6 +20,7 @@ export const PorterUrls = [
   "applications",
   "env-groups",
   "jobs",
+  "onboarding",
 ];
 
 // TODO: consolidate with pushFiltered