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

Merge pull request #1159 from porter-dev/staging

Company form merge to prod
jusrhee 4 жил өмнө
parent
commit
1210fdd055

+ 15 - 9
dashboard/package-lock.json

@@ -3090,11 +3090,12 @@
       "dev": true
       "dev": true
     },
     },
     "@types/react": {
     "@types/react": {
-      "version": "16.14.2",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.2.tgz",
-      "integrity": "sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ==",
+      "version": "16.14.14",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.14.tgz",
+      "integrity": "sha512-uwIWDYW8LznHzEMJl7ag9St1RsK0gw/xaFZ5+uI1ZM1HndwUgmPH3/wQkSb87GkOVg7shUxnpNW8DcN0AzvG5Q==",
       "requires": {
       "requires": {
         "@types/prop-types": "*",
         "@types/prop-types": "*",
+        "@types/scheduler": "*",
         "csstype": "^3.0.2"
         "csstype": "^3.0.2"
       }
       }
     },
     },
@@ -3162,6 +3163,11 @@
         "@types/react": "*"
         "@types/react": "*"
       }
       }
     },
     },
+    "@types/scheduler": {
+      "version": "0.16.2",
+      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+      "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+    },
     "@types/semver": {
     "@types/semver": {
       "version": "7.3.5",
       "version": "7.3.5",
       "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.5.tgz",
       "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.5.tgz",
@@ -5638,9 +5644,9 @@
       }
       }
     },
     },
     "dom-helpers": {
     "dom-helpers": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz",
-      "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==",
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
       "requires": {
       "requires": {
         "@babel/runtime": "^7.8.7",
         "@babel/runtime": "^7.8.7",
         "csstype": "^3.0.2"
         "csstype": "^3.0.2"
@@ -9306,9 +9312,9 @@
       "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA=="
       "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA=="
     },
     },
     "react-transition-group": {
     "react-transition-group": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
-      "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
+      "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==",
       "requires": {
       "requires": {
         "@babel/runtime": "^7.5.5",
         "@babel/runtime": "^7.5.5",
         "dom-helpers": "^5.0.1",
         "dom-helpers": "^5.0.1",

+ 2 - 1
dashboard/package.json

@@ -38,6 +38,7 @@
     "react-modal": "^3.11.2",
     "react-modal": "^3.11.2",
     "react-router-dom": "^5.2.0",
     "react-router-dom": "^5.2.0",
     "react-table": "^7.7.0",
     "react-table": "^7.7.0",
+    "react-transition-group": "^4.4.2",
     "regenerator-runtime": "^0.13.9",
     "regenerator-runtime": "^0.13.9",
     "semver": "^7.3.5",
     "semver": "^7.3.5",
     "styled-components": "^5.2.0"
     "styled-components": "^5.2.0"
@@ -68,7 +69,7 @@
     "@types/node": "^12.12.62",
     "@types/node": "^12.12.62",
     "@types/qs": "^6.9.5",
     "@types/qs": "^6.9.5",
     "@types/random-words": "^1.1.0",
     "@types/random-words": "^1.1.0",
-    "@types/react": "^16.9.49",
+    "@types/react": "^16.14.14",
     "@types/react-dom": "^16.9.8",
     "@types/react-dom": "^16.9.8",
     "@types/react-modal": "^3.10.6",
     "@types/react-modal": "^3.10.6",
     "@types/react-router": "^5.1.8",
     "@types/react-router": "^5.1.8",

+ 4 - 3
dashboard/src/components/porter-form/field-components/ArrayInput.tsx

@@ -6,13 +6,14 @@ import {
   GetFinalVariablesFunction,
   GetFinalVariablesFunction,
 } from "../types";
 } from "../types";
 import useFormField from "../hooks/useFormField";
 import useFormField from "../hooks/useFormField";
+import { hasSetValue } from "../utils";
 
 
 const ArrayInput: React.FC<ArrayInputField> = (props) => {
 const ArrayInput: React.FC<ArrayInputField> = (props) => {
   const { state, variables, setVars } = useFormField<ArrayInputFieldState>(
   const { state, variables, setVars } = useFormField<ArrayInputFieldState>(
     props.id,
     props.id,
     {
     {
       initVars: {
       initVars: {
-        [props.variable]: props.value && props.value[0] ? props.value[0] : [],
+        [props.variable]: hasSetValue(props) ? props.value[0] : [],
       },
       },
     }
     }
   );
   );
@@ -100,10 +101,10 @@ export const getFinalVariablesForArrayInput: GetFinalVariablesFunction = (
   vars,
   vars,
   props: ArrayInputField
   props: ArrayInputField
 ) => {
 ) => {
-  return vars[props.variable]
+  return vars[props.variable] != undefined && vars[props.variable] != null
     ? {}
     ? {}
     : {
     : {
-        [props.variable]: props.value ? props.value[0] : [],
+        [props.variable]: hasSetValue(props) ? props.value[0] : [],
       };
       };
 };
 };
 
 

+ 2 - 2
dashboard/src/components/porter-form/field-components/Checkbox.tsx

@@ -26,7 +26,7 @@ const Checkbox: React.FC<Props> = ({
       validated: !required,
       validated: !required,
     },
     },
     initVars: {
     initVars: {
-      [variable]: value ? value[0] : !!settings?.default,
+      [variable]: value ? value[0] : false,
     },
     },
   });
   });
 
 
@@ -75,6 +75,6 @@ export const getFinalVariablesForCheckbox: GetFinalVariablesFunction = (
   }
   }
 
 
   return {
   return {
-    [props.variable]: props.value ? props.value[0] : !!props.settings?.default,
+    [props.variable]: props.value ? props.value[0] : false,
   };
   };
 };
 };

+ 22 - 19
dashboard/src/components/porter-form/field-components/Input.tsx

@@ -6,6 +6,7 @@ import {
   InputField,
   InputField,
   StringInputFieldState,
   StringInputFieldState,
 } from "../types";
 } from "../types";
+import { hasSetValue } from "../utils";
 
 
 const clipOffUnit = (unit: string, x: string) => {
 const clipOffUnit = (unit: string, x: string) => {
   if (typeof x === "string" && unit) {
   if (typeof x === "string" && unit) {
@@ -16,17 +17,19 @@ const clipOffUnit = (unit: string, x: string) => {
   return x;
   return x;
 };
 };
 
 
-const Input: React.FC<InputField> = ({
-  id,
-  variable,
-  label,
-  required,
-  placeholder,
-  info,
-  settings,
-  isReadOnly,
-  value,
-}) => {
+const Input: React.FC<InputField> = (props) => {
+  const {
+    id,
+    variable,
+    label,
+    required,
+    placeholder,
+    info,
+    settings,
+    isReadOnly,
+    value,
+  } = props;
+
   const {
   const {
     state,
     state,
     variables,
     variables,
@@ -34,14 +37,12 @@ const Input: React.FC<InputField> = ({
     setValidation,
     setValidation,
   } = useFormField<StringInputFieldState>(id, {
   } = useFormField<StringInputFieldState>(id, {
     initValidation: {
     initValidation: {
-      validated: value
-        ? value[0] !== undefined && value[0] !== "" && value[0] != null
-        : settings?.default != undefined,
+      validated: hasSetValue(props),
     },
     },
     initVars: {
     initVars: {
-      [variable]: value
+      [variable]: hasSetValue(props)
         ? clipOffUnit(settings?.unit, value[0])
         ? clipOffUnit(settings?.unit, value[0])
-        : settings?.default,
+        : undefined,
     },
     },
   });
   });
 
 
@@ -93,10 +94,12 @@ export const getFinalVariablesForStringInput: GetFinalVariablesFunction = (
   props: InputField
   props: InputField
 ) => {
 ) => {
   const val =
   const val =
-    vars[props.variable] ||
-    (props.value
+    vars[props.variable] != undefined && vars[props.variable] != null
+      ? vars[props.variable]
+      : hasSetValue(props)
       ? clipOffUnit(props.settings?.unit, props.value[0])
       ? clipOffUnit(props.settings?.unit, props.value[0])
-      : props.settings?.default);
+      : undefined;
+
   return {
   return {
     [props.variable]:
     [props.variable]:
       props.settings?.unit && !props.settings.omitUnitFromValue
       props.settings?.unit && !props.settings.omitUnitFromValue

+ 7 - 10
dashboard/src/components/porter-form/field-components/KeyValueArray.tsx

@@ -11,6 +11,7 @@ import useFormField from "../hooks/useFormField";
 import Modal from "../../../main/home/modals/Modal";
 import Modal from "../../../main/home/modals/Modal";
 import LoadEnvGroupModal from "../../../main/home/modals/LoadEnvGroupModal";
 import LoadEnvGroupModal from "../../../main/home/modals/LoadEnvGroupModal";
 import EnvEditorModal from "../../../main/home/modals/EnvEditorModal";
 import EnvEditorModal from "../../../main/home/modals/EnvEditorModal";
+import { hasSetValue } from "../utils";
 
 
 interface Props extends KeyValueArrayField {
 interface Props extends KeyValueArrayField {
   id: string;
   id: string;
@@ -21,12 +22,11 @@ const KeyValueArray: React.FC<Props> = (props) => {
     props.id,
     props.id,
     {
     {
       initState: {
       initState: {
-        values:
-          props.value && props.value[0]
-            ? (Object.entries(props.value[0])?.map(([k, v]) => {
-                return { key: k, value: v };
-              }) as any[])
-            : [],
+        values: hasSetValue(props)
+          ? (Object.entries(props.value[0])?.map(([k, v]) => {
+              return { key: k, value: v };
+            }) as any[])
+          : [],
         showEnvModal: false,
         showEnvModal: false,
         showEditorModal: false,
         showEditorModal: false,
       },
       },
@@ -349,12 +349,9 @@ export const getFinalVariablesForKeyValueArray: GetFinalVariablesFunction = (
   props: KeyValueArrayField,
   props: KeyValueArrayField,
   state: KeyValueArrayFieldState
   state: KeyValueArrayFieldState
 ) => {
 ) => {
-  console.log(vars);
-  console.log(props);
-  console.log(state);
   if (!state) {
   if (!state) {
     return {
     return {
-      [props.variable]: props.value ? props.value[0] : [],
+      [props.variable]: hasSetValue(props) ? props.value[0] : [],
     };
     };
   }
   }
 
 

+ 3 - 6
dashboard/src/components/porter-form/field-components/Select.tsx

@@ -8,15 +8,14 @@ import Selector from "../../Selector";
 import styled from "styled-components";
 import styled from "styled-components";
 import useFormField from "../hooks/useFormField";
 import useFormField from "../hooks/useFormField";
 import { Context } from "../../../shared/Context";
 import { Context } from "../../../shared/Context";
+import { hasSetValue } from "../utils";
 
 
 const Select: React.FC<SelectField> = (props) => {
 const Select: React.FC<SelectField> = (props) => {
   const { currentCluster } = useContext(Context);
   const { currentCluster } = useContext(Context);
   const { variables, setVars } = useFormField<SelectFieldState>(props.id, {
   const { variables, setVars } = useFormField<SelectFieldState>(props.id, {
     initVars: {
     initVars: {
-      [props.variable]: props.value
+      [props.variable]: hasSetValue(props)
         ? props.value[0]
         ? props.value[0]
-        : props.settings.default
-        ? props.settings.default
         : props.settings.type == "provider"
         : props.settings.type == "provider"
         ? ({
         ? ({
             gke: "gcp",
             gke: "gcp",
@@ -72,10 +71,8 @@ export const getFinalVariablesForSelect: GetFinalVariablesFunction = (
   return vars[props.variable]
   return vars[props.variable]
     ? {}
     ? {}
     : {
     : {
-        [props.variable]: props.value
+        [props.variable]: hasSetValue(props)
           ? props.value[0]
           ? props.value[0]
-          : props.settings.default
-          ? props.settings.default
           : props.settings.type == "provider"
           : props.settings.type == "provider"
           ? ({
           ? ({
               gke: "gcp",
               gke: "gcp",

+ 2 - 6
dashboard/src/components/porter-form/types.ts

@@ -18,7 +18,7 @@ export interface GenericInputField extends GenericField {
   settings?: any;
   settings?: any;
 
 
   // Read in value from Helm for existing revisions
   // Read in value from Helm for existing revisions
-  value?: any[];
+  value?: [any] | [];
 }
 }
 
 
 export interface HeadingField extends GenericField {
 export interface HeadingField extends GenericField {
@@ -61,9 +61,7 @@ export interface InputField extends GenericInputField {
 export interface CheckboxField extends GenericInputField {
 export interface CheckboxField extends GenericInputField {
   type: "checkbox";
   type: "checkbox";
   label?: string;
   label?: string;
-  settings?: {
-    default: boolean;
-  };
+  settings?: {};
 }
 }
 
 
 export interface KeyValueArrayField extends GenericInputField {
 export interface KeyValueArrayField extends GenericInputField {
@@ -88,11 +86,9 @@ export interface SelectField extends GenericInputField {
     | {
     | {
         type: "normal";
         type: "normal";
         options: { value: string; label: string }[];
         options: { value: string; label: string }[];
-        default?: string;
       }
       }
     | {
     | {
         type: "provider";
         type: "provider";
-        default?: string;
       };
       };
   width: string;
   width: string;
   label?: string;
   label?: string;

+ 5 - 0
dashboard/src/components/porter-form/utils.ts

@@ -0,0 +1,5 @@
+import { GenericInputField } from "./types";
+
+export const hasSetValue = (field: GenericInputField) => {
+  return field.value && field.value.length != 0 && field.value[0] != null;
+};

+ 20 - 1
dashboard/src/main/home/Home.tsx

@@ -12,6 +12,7 @@ import ConfirmOverlay from "components/ConfirmOverlay";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import ClusterDashboard from "./cluster-dashboard/ClusterDashboard";
 import ClusterDashboard from "./cluster-dashboard/ClusterDashboard";
 import Dashboard from "./dashboard/Dashboard";
 import Dashboard from "./dashboard/Dashboard";
+import WelcomeForm from "./WelcomeForm";
 import Integrations from "./integrations/Integrations";
 import Integrations from "./integrations/Integrations";
 import Templates from "./launch/Launch";
 import Templates from "./launch/Launch";
 import ClusterInstructionsModal from "./modals/ClusterInstructionsModal";
 import ClusterInstructionsModal from "./modals/ClusterInstructionsModal";
@@ -65,6 +66,7 @@ type StateType = {
 
 
   // Track last project id for refreshing clusters on project change
   // Track last project id for refreshing clusters on project change
   prevProjectId: number | null;
   prevProjectId: number | null;
+  showWelcomeForm: boolean;
 };
 };
 
 
 // TODO: Handle cluster connected but with some failed infras (no successful set)
 // TODO: Handle cluster connected but with some failed infras (no successful set)
@@ -78,6 +80,7 @@ class Home extends Component<PropsType, StateType> {
     sidebarReady: false,
     sidebarReady: false,
     handleDO: false,
     handleDO: false,
     ghRedirect: false,
     ghRedirect: false,
+    showWelcomeForm: true,
   };
   };
 
 
   // TODO: Refactor and prevent flash + multiple reload
   // TODO: Refactor and prevent flash + multiple reload
@@ -385,6 +388,22 @@ class Home extends Component<PropsType, StateType> {
             <Icon src={discordLogo} />
             <Icon src={discordLogo} />
             Join Our Discord
             Join Our Discord
           </DiscordButton>
           </DiscordButton>
+          {
+            (this.context?.capabilities?.version === "production" ||
+            this.context?.capabilities?.version === "staging") &&
+            this.state.showWelcomeForm &&
+            localStorage.getItem("welcomed") != "true" && (
+              <>
+                <WelcomeForm
+                  closeForm={() => this.setState({ showWelcomeForm: false })}
+                />
+                <Navbar
+                  logOut={this.props.logOut}
+                  currentView={this.props.currentRoute} // For form feedback
+                />
+              </>
+            )
+          }
         </>
         </>
       );
       );
     }
     }
@@ -658,7 +677,7 @@ const StyledHome = styled.div`
 
 
 const DiscordButton = styled.a`
 const DiscordButton = styled.a`
   position: absolute;
   position: absolute;
-  z-index: 100;
+  z-index: 1;
   text-decoration: none;
   text-decoration: none;
   bottom: 17px;
   bottom: 17px;
   display: flex;
   display: flex;

+ 305 - 0
dashboard/src/main/home/WelcomeForm.tsx

@@ -0,0 +1,305 @@
+import React, { useContext, useState } from "react";
+import styled from "styled-components";
+import { CSSTransition } from "react-transition-group";
+import api from "shared/api";
+
+import { Context } from "shared/Context";
+
+type Props = {
+  closeForm: () => void;
+};
+
+type StateType = {
+  active: boolean;
+};
+
+const WelcomeForm: React.FunctionComponent<Props> = ({}) => {
+  const context = useContext(Context);
+  const [active, setActive] = useState(true);
+  const [isCompany, setIsCompany] = useState(false);
+  const [role, setRole] = useState("unspecified");
+  const [company, setCompany] = useState("");
+
+  const submitForm = () => {
+    api
+      .getWelcome(
+        "<token>",
+        {
+          email: context.user && context.user.email,
+          isCompany,
+          company,
+          role,
+        },
+        {}
+      )
+      .then(() => {
+        localStorage.setItem("welcomed", "true");
+        setActive(false);
+      })
+      .catch((err) => console.log(err));
+  };
+
+  const renderContents = () => {
+    if (isCompany) {
+      return (
+        <FadeWrapper>
+          <Title>Welcome to Porter</Title>
+          <Subtitle>Just two things before getting started.</Subtitle>
+          <SubtitleAlt>
+            <Num>1</Num> What is your company name? *
+          </SubtitleAlt>
+          <Input
+            placeholder="ex: Acme"
+            value={company}
+            onChange={(e: any) => setCompany(e.target.value)}
+          />
+          <SubtitleAlt>
+            <Num>2</Num> What is your role? *
+          </SubtitleAlt>
+          <RadioButton
+            onClick={() => setRole("founder")}
+            selected={role === "founder"}
+          >
+            <i className="material-icons-round">
+              {role === "founder" ? "check_box" : "check_box_outline_blank"}
+            </i>{" "}
+            Founder
+          </RadioButton>
+          <RadioButton
+            onClick={() => setRole("developer")}
+            selected={role === "developer"}
+          >
+            <i className="material-icons-round">
+              {role === "developer" ? "check_box" : "check_box_outline_blank"}
+            </i>{" "}
+            Developer
+          </RadioButton>
+          <RadioButton
+            onClick={() => setRole("devops")}
+            selected={role === "devops"}
+          >
+            <i className="material-icons-round">
+              {role === "devops" ? "check_box" : "check_box_outline_blank"}
+            </i>{" "}
+            DevOps
+          </RadioButton>
+
+          <Submit
+            isDisabled={!company || role === "unspecified"}
+            onClick={() => company && role !== "unspecified" && submitForm()}
+          >
+            <i className="material-icons-round">check</i> Done
+          </Submit>
+        </FadeWrapper>
+      );
+    }
+    return (
+      <>
+        <Title>Welcome to Porter</Title>
+        <Subtitle delay="0.7s">I am interested in using Porter as:</Subtitle>
+        <Option onClick={() => setIsCompany(true)}>
+          <i className="material-icons-round">people</i> A Company
+        </Option>
+        <Option onClick={() => submitForm()}>
+          <i className="material-icons-round">person</i> An Individual
+        </Option>
+      </>
+    );
+  };
+
+  return (
+    <CSSTransition
+      in={active}
+      timeout={500}
+      classNames="alert"
+      unmountOnExit
+      onEnter={() => setActive(true)}
+      onExited={() => setActive(false)}
+    >
+      <StyledWelcomeForm>
+        <div>
+          {renderContents()}
+          <br />
+          <br />
+        </div>
+      </StyledWelcomeForm>
+    </CSSTransition>
+  );
+};
+
+export default WelcomeForm;
+
+const Circle = styled.div`
+  width: 13px;
+  height: 13px;
+  border-radius: 20px;
+  background: #ffffff11;
+  margin-right: 12px;
+  border: 1px solid #aaaabb;
+`;
+
+const FadeWrapper = styled.div`
+  background: #202227;
+  opacity: 0;
+  animation: fadeIn 0.7s 0s;
+  animation-fill-mode: forwards;
+`;
+
+const Num = styled.div`
+  display: flex;
+  align-items: center;
+  margin-right: 15px;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  border: 1px solid #ffffff;
+`;
+
+const Option = styled.div`
+  width: 500px;
+  max-width: 80vw;
+  height: 50px;
+  background: #ffffff22;
+  display: flex;
+  align-items: center;
+  margin-top: 15px;
+  border: 1px solid #aaaabb;
+  border-radius: 5px;
+  padding-left: 15px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff44;
+  }
+
+  > i {
+    font-size: 20px;
+    margin-right: 12px;
+    color: #aaaabb;
+  }
+
+  opacity: 0;
+  animation: slideIn 0.7s 1.3s;
+  animation-fill-mode: forwards;
+
+  @keyframes slideIn {
+    from {
+      opacity: 0;
+      transform: translateX(-30px);
+    }
+    to {
+      opacity: 1;
+      transform: translateX(0);
+    }
+  }
+`;
+
+const Submit = styled(Option)<{ isDisabled: boolean }>`
+  border: 0;
+  opacity: 0;
+  animation: fadeIn 0.7s 0.5s;
+  animation-fill-mode: forwards;
+  margin-top: 35px;
+  cursor: ${(props) => (props.isDisabled ? "not-allowed" : "pointer")};
+  background: ${(props) => (props.isDisabled ? "#aaaabb" : "#616FEEcc")};
+  :hover {
+    filter: ${(props) => (props.isDisabled ? "" : "brightness(130%)")};
+    background: ${(props) => (props.isDisabled ? "#aaaabb" : "#616FEEcc")};
+  }
+
+  > i {
+    color: #ffffff;
+  }
+`;
+
+const RadioButton = styled(Option)<{ selected: boolean }>`
+  opacity: 0;
+  background: ${(props) => (props.selected ? "#ffffff44" : "#ffffff22")};
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+
+  > div {
+    background: ${(props) => (props.selected ? "#ffffff44" : "")};
+  }
+`;
+
+const Input = styled.input`
+  width: 500px;
+  max-width: 80vw;
+  height: 50px;
+  background: #ffffff22;
+  font-size: 18px;
+  display: flex;
+  align-items: center;
+  margin-top: 0px;
+  color: #ffffff;
+  border: 1px solid #aaaabb;
+  border-radius: 5px;
+  padding-left: 15px;
+  margin-bottom: 40px;
+
+  opacity: 0;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+`;
+
+const Subtitle = styled.div<{ delay?: string }>`
+  margin: 20px 0 30px;
+  color: #aaaabb;
+
+  opacity: 0;
+  animation: fadeIn 0.5s ${(props) => props.delay || "0.2s"};
+  animation-fill-mode: forwards;
+`;
+
+const SubtitleAlt = styled(Subtitle)`
+  margin: -5px 0 30px;
+  color: white;
+  display: flex;
+  align-items: center;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+`;
+
+const Title = styled.div`
+  color: white;
+
+  font-size: 26px;
+  margin-bottom: 5px;
+  display: flex;
+  align-items: center;
+
+  opacity: 0;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const StyledWelcomeForm = styled.div`
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1;
+  background: #202227;
+
+  &.alert-exit {
+    opacity: 1;
+  }
+  &.alert-exit-active {
+    opacity: 0;
+    transform: translateY(-100px);
+    transition: opacity 500ms, transform 1000ms;
+  }
+`;

+ 6 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -378,8 +378,12 @@ const ExpandedChart: React.FC<Props> = (props) => {
                   <Spinner src={loadingSrc} /> This application is currently
                   <Spinner src={loadingSrc} /> This application is currently
                   being deployed
                   being deployed
                 </Header>
                 </Header>
-                Navigate to the <A
-                  href={props.currentChart.git_action_config && `https://github.com/${props.currentChart.git_action_config?.git_repo}/actions`}
+                Navigate to the{" "}
+                <A
+                  href={
+                    props.currentChart.git_action_config &&
+                    `https://github.com/${props.currentChart.git_action_config?.git_repo}/actions`
+                  }
                   target={"_blank"}
                   target={"_blank"}
                 >
                 >
                   Actions
                   Actions

+ 123 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -18,6 +18,8 @@ import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import ValuesYaml from "./ValuesYaml";
 import ValuesYaml from "./ValuesYaml";
 import DeploymentType from "./DeploymentType";
 import DeploymentType from "./DeploymentType";
+import Modal from "main/home/modals/Modal";
+import UpgradeChartModal from "main/home/modals/UpgradeChartModal";
 
 
 type PropsType = WithAuthProps & {
 type PropsType = WithAuthProps & {
   namespace: string;
   namespace: string;
@@ -42,6 +44,7 @@ type StateType = {
   saveValuesStatus: string | null;
   saveValuesStatus: string | null;
   formData: any;
   formData: any;
   devOpsMode: boolean;
   devOpsMode: boolean;
+  upgradeVersion: string;
 };
 };
 
 
 class ExpandedJobChart extends Component<PropsType, StateType> {
 class ExpandedJobChart extends Component<PropsType, StateType> {
@@ -59,6 +62,7 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
     deleting: false,
     deleting: false,
     saveValuesStatus: null as string | null,
     saveValuesStatus: null as string | null,
     formData: {} as any,
     formData: {} as any,
+    upgradeVersion: "",
     devOpsMode: localStorage.getItem("devOpsMode") === "true",
     devOpsMode: localStorage.getItem("devOpsMode") === "true",
   };
   };
 
 
@@ -438,8 +442,8 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
                 </Header>
                 </Header>
                 Navigate to the
                 Navigate to the
                 <A
                 <A
-                    href={`https://github.com/${this.props.currentChart.git_action_config.git_repo}/actions`}
-                    target={"_blank"}
+                  href={`https://github.com/${this.props.currentChart.git_action_config.git_repo}/actions`}
+                  target={"_blank"}
                 >
                 >
                   Actions tab
                   Actions tab
                 </A>{" "}
                 </A>{" "}
@@ -575,13 +579,91 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
       .catch(console.log);
       .catch(console.log);
   };
   };
 
 
+  handleUpgradeVersion = async (version: string, cb: () => void) => {
+    // convert current values to yaml
+    let values = this.state.currentChart.config;
+
+    let valuesYaml = yaml.dump({
+      ...(this.state.currentChart.config as Object),
+      ...values,
+    });
+
+    _.set(values, "paused", true);
+
+    const { currentChart } = this.state;
+    this.setState({ saveValuesStatus: "loading" });
+    this.getChartData(currentChart, currentChart.version);
+
+    try {
+      await api.upgradeChartValues(
+        "<token>",
+        {
+          namespace: currentChart.namespace,
+          storage: StorageType.Secret,
+          values: valuesYaml,
+          version: version,
+        },
+        {
+          id: this.context.currentProject.id,
+          name: currentChart.name,
+          cluster_id: this.context.currentCluster.id,
+        }
+      );
+      this.setState({ saveValuesStatus: "successful" });
+
+      window.analytics.track("Chart Upgraded", {
+        chart: currentChart.name,
+        values: valuesYaml,
+      });
+
+      cb && cb();
+    } catch (err) {
+      let parsedErr =
+        err?.response?.data?.errors && err.response.data.errors[0];
+
+      if (parsedErr) {
+        err = parsedErr;
+      }
+      this.setState({ saveValuesStatus: err });
+      this.context.setCurrentError(parsedErr);
+
+      window.analytics.track("Failed to Upgrade Chart", {
+        chart: currentChart.name,
+        values: valuesYaml,
+        error: err,
+      });
+    }
+  };
+
   render() {
   render() {
     let { closeChart } = this.props;
     let { closeChart } = this.props;
     let { currentChart } = this.state;
     let { currentChart } = this.state;
     let chart = currentChart;
     let chart = currentChart;
-
+    const displayUpdateButton =
+      chart.latest_version &&
+      chart.latest_version !== chart.chart.metadata.version;
     return (
     return (
       <>
       <>
+        {this.state.upgradeVersion && (
+          <Modal
+            onRequestClose={() => this.setState({ upgradeVersion: "" })}
+            width="500px"
+            height="450px"
+          >
+            <UpgradeChartModal
+              currentChart={chart}
+              closeModal={() => {
+                this.setState({ upgradeVersion: "" });
+              }}
+              onSubmit={() => {
+                this.handleUpgradeVersion(this.state.upgradeVersion, () => {
+                  this.setState({ loading: false });
+                });
+                this.setState({ upgradeVersion: "", loading: true });
+              }}
+            />
+          </Modal>
+        )}
         <StyledExpandedChart>
         <StyledExpandedChart>
           <HeaderWrapper>
           <HeaderWrapper>
             <BackButton onClick={closeChart}>
             <BackButton onClick={closeChart}>
@@ -605,6 +687,19 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
                 {" " + this.readableDate(chart.info.last_deployed)}
                 {" " + this.readableDate(chart.info.last_deployed)}
               </LastDeployed>
               </LastDeployed>
             </InfoWrapper>
             </InfoWrapper>
+            {displayUpdateButton && (
+              <RevisionUpdateMessage
+                onClick={(e) => {
+                  e.stopPropagation();
+                  this.setState({
+                    upgradeVersion: currentChart.latest_version,
+                  });
+                }}
+              >
+                <i className="material-icons">notification_important</i>
+                Template Update Available
+              </RevisionUpdateMessage>
+            )}
           </HeaderWrapper>
           </HeaderWrapper>
 
 
           {this.state.deleting ? (
           {this.state.deleting ? (
@@ -666,6 +761,31 @@ ExpandedJobChart.contextType = Context;
 
 
 export default withAuth(ExpandedJobChart);
 export default withAuth(ExpandedJobChart);
 
 
+const RevisionUpdateMessage = styled.button`
+  background: none;
+  color: white;
+  display: flex;
+  align-items: center;
+  padding: 4px 10px;
+  border-radius: 5px;
+  border: none;
+  margin-bottom: 14px;
+
+  :hover {
+    border: 1px solid white;
+    padding: 3px 9px;
+    cursor: pointer;
+  }
+
+  > i {
+    margin-right: 6px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+    transform: none;
+  }
+`;
+
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
   height: 2px;
   height: 2px;

+ 1 - 1
dashboard/src/main/home/modals/AccountSettingsModal.tsx

@@ -140,7 +140,7 @@ const User = styled.div`
 
 
 const ListWrapper = styled.div`
 const ListWrapper = styled.div`
   width: 100%;
   width: 100%;
-  height: 200px;
+  height: 250px;
   background: #ffffff11;
   background: #ffffff11;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;

+ 1 - 1
dashboard/src/main/home/modals/UpgradeChartModal.tsx

@@ -33,7 +33,7 @@ export default class UpgradeChartModal extends Component<PropsType, StateType> {
       .toLowerCase()
       .toLowerCase()
       .trim();
       .trim();
 
 
-    if (chartName == "web" || chartName == "worker") {
+    if (chartName == "web" || chartName == "worker" || chartName === "job") {
       repoURL = process.env.APPLICATION_CHART_REPO_URL;
       repoURL = process.env.APPLICATION_CHART_REPO_URL;
     }
     }
 
 

+ 1 - 1
dashboard/src/main/home/navbar/Navbar.tsx

@@ -55,7 +55,6 @@ class Navbar extends Component<PropsType, StateType> {
   };
   };
 
 
   renderFeedbackButton = () => {
   renderFeedbackButton = () => {
-    console.log("hi", this.context?.capabilities)
     if (this.context?.capabilities?.provisioner) {
     if (this.context?.capabilities?.provisioner) {
       return <Feedback currentView={this.props.currentView} />;
       return <Feedback currentView={this.props.currentView} />;
     }
     }
@@ -242,6 +241,7 @@ const StyledNavbar = styled.div`
   align-items: center;
   align-items: center;
   padding-right: 5px;
   padding-right: 5px;
   justify-content: flex-end;
   justify-content: flex-end;
+  z-index: 1;
 `;
 `;
 
 
 const NavButton = styled.a`
 const NavButton = styled.a`

+ 10 - 0
dashboard/src/shared/api.tsx

@@ -785,6 +785,15 @@ const getCapabilities = baseApi<{}, {}>("GET", () => {
   return `/api/capabilities`;
   return `/api/capabilities`;
 });
 });
 
 
+const getWelcome = baseApi<{
+  email: string,
+  isCompany: boolean,
+  company: string,
+  role: string
+}, {}>("GET", () => {
+  return `/api/welcome`;
+});
+
 const linkGithubProject = baseApi<
 const linkGithubProject = baseApi<
   {},
   {},
   {
   {
@@ -1087,6 +1096,7 @@ export default {
   getBranchContents,
   getBranchContents,
   getBranches,
   getBranches,
   getCapabilities,
   getCapabilities,
+  getWelcome,
   getChart,
   getChart,
   getCharts,
   getCharts,
   getChartComponents,
   getChartComponents,

+ 2 - 0
internal/config/config.go

@@ -71,6 +71,8 @@ type ServerConf struct {
 	ProvisionerCluster string `env:"PROVISIONER_CLUSTER"`
 	ProvisionerCluster string `env:"PROVISIONER_CLUSTER"`
 	IngressCluster     string `env:"INGRESS_CLUSTER"`
 	IngressCluster     string `env:"INGRESS_CLUSTER"`
 	SelfKubeconfig     string `env:"SELF_KUBECONFIG"`
 	SelfKubeconfig     string `env:"SELF_KUBECONFIG"`
+
+	WelcomeFormWebhook string `env:"WELCOME_FORM_WEBHOOK"`
 }
 }
 
 
 // DBConf is the database configuration: if generated from environment variables,
 // DBConf is the database configuration: if generated from environment variables,

+ 1 - 1
server/api/k8s_handler.go

@@ -35,7 +35,7 @@ func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 
 
 	if err != nil {
 	if err != nil {
-		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 		return
 	}
 	}
 
 

+ 34 - 0
server/api/welcome_handler.go

@@ -0,0 +1,34 @@
+package api
+
+import (
+	"net/http"
+	"net/url"
+)
+
+// HandleGetCapabilities gets the capabilities of the server
+func (app *App) HandleWelcome(w http.ResponseWriter, r *http.Request) {
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		return
+	}
+
+	req, err := http.NewRequest("GET", app.ServerConf.WelcomeFormWebhook, nil)
+
+	if err != nil {
+		return
+	}
+
+	q := req.URL.Query()
+	q.Add("email", vals["email"][0])
+	q.Add("isCompany", vals["isCompany"][0])
+	q.Add("company", vals["company"][0])
+	q.Add("role", vals["role"][0])
+	req.URL.RawQuery = q.Encode()
+
+	_, err = http.Get(req.URL.String())
+
+	if err != nil {
+		return
+	}
+}

+ 7 - 0
server/router/router.go

@@ -1697,6 +1697,13 @@ func New(a *api.App) *chi.Mux {
 				http.HandlerFunc(a.HandleGetCapabilities),
 				http.HandlerFunc(a.HandleGetCapabilities),
 			)
 			)
 
 
+			// welcome form
+			r.Method(
+				"GET",
+				"/welcome",
+				http.HandlerFunc(a.HandleWelcome),
+			)
+
 			// /api/projects/{project_id}/deploy routes
 			// /api/projects/{project_id}/deploy routes
 			r.Method(
 			r.Method(
 				"POST",
 				"POST",