Browse Source

Merge pull request #2074 from porter-dev/master

Frontend fixes -> staging
Nicolas Frati 4 years ago
parent
commit
e0d2a77679

+ 31 - 67
dashboard/src/components/form-components/KeyValueArray.tsx

@@ -3,9 +3,11 @@ import styled from "styled-components";
 import Modal from "../../main/home/modals/Modal";
 import LoadEnvGroupModal from "../../main/home/modals/LoadEnvGroupModal";
 import EnvEditorModal from "../../main/home/modals/EnvEditorModal";
+import { dotenv_parse } from "shared/string_utils";
 
 import sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
+import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray";
 
 export type KeyValue = {
   key: string;
@@ -136,23 +138,34 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
                 spellCheck={false}
               />
               <Spacer />
-              <Input
-                placeholder="ex: value"
-                width="270px"
-                value={value}
-                onChange={(e: any) => {
-                  this.state.values[i].value = e.target.value;
-                  this.setState({ values: this.state.values });
-
-                  let obj = this.valuesToObject();
-                  this.props.setValues(obj);
-                }}
-                disabled={
-                  this.props.disabled || value?.includes("PORTERSECRET")
-                }
-                type={value?.includes("PORTERSECRET") ? "password" : "text"}
-                spellCheck={false}
-              />
+              {value?.includes("PORTERSECRET") ? (
+                <Input
+                  placeholder="ex: value"
+                  width="270px"
+                  value={value}
+                  disabled
+                  type={"password"}
+                  spellCheck={false}
+                />
+              ) : (
+                <MultiLineInput
+                  placeholder="ex: value"
+                  width="270px"
+                  value={value}
+                  onChange={(e: any) => {
+                    this.state.values[i].value = e.target.value;
+                    this.setState({ values: this.state.values });
+
+                    let obj = this.valuesToObject();
+                    this.props.setValues(obj);
+                  }}
+                  disabled={
+                    this.props.disabled || value?.includes("PORTERSECRET")
+                  }
+                  spellCheck={false}
+                  rows={value?.split("\n").length}
+                />
+              )}
               {this.renderDeleteButton(i)}
               {this.renderHiddenOption(value?.includes("PORTERSECRET"), i)}
             </InputWrapper>
@@ -204,57 +217,8 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
     }
   };
 
-  // Parses src into an Object
-  parseEnv = (src: any, options: any) => {
-    const debug = Boolean(options && options.debug);
-    const obj = {} as Record<string, string>;
-    const NEWLINE = "\n";
-    const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/;
-    const RE_NEWLINES = /\\n/g;
-    const NEWLINES_MATCH = /\n|\r|\r\n/;
-
-    // convert Buffers before splitting into lines and processing
-    src
-      .toString()
-      .split(NEWLINES_MATCH)
-      .forEach(function (line: any, idx: any) {
-        // matching "KEY' and 'VAL' in 'KEY=VAL'
-        const keyValueArr = line.match(RE_INI_KEY_VAL);
-        // matched?
-        if (keyValueArr != null) {
-          const key = keyValueArr[1];
-          // default undefined or missing values to empty string
-          let val = keyValueArr[2] || "";
-          const end = val.length - 1;
-          const isDoubleQuoted = val[0] === '"' && val[end] === '"';
-          const isSingleQuoted = val[0] === "'" && val[end] === "'";
-
-          // if single or double quoted, remove quotes
-          if (isSingleQuoted || isDoubleQuoted) {
-            val = val.substring(1, end);
-
-            // if double quoted, expand newlines
-            if (isDoubleQuoted) {
-              val = val.replace(RE_NEWLINES, NEWLINE);
-            }
-          } else {
-            // remove surrounding whitespace
-            val = val.trim();
-          }
-
-          obj[key] = val;
-        } else if (debug) {
-          console.log(
-            `did not match key and value when parsing line ${idx + 1}: ${line}`
-          );
-        }
-      });
-
-    return obj;
-  };
-
   readFile = (env: string) => {
-    let envObj = this.parseEnv(env, null);
+    let envObj = dotenv_parse(env);
     let push = true;
 
     for (let key in envObj) {

+ 122 - 110
dashboard/src/components/porter-form/field-components/KeyValueArray.tsx

@@ -20,6 +20,7 @@ import Heading from "components/form-components/Heading";
 import Loading from "components/Loading";
 import api from "shared/api";
 import { Context } from "shared/Context";
+import { dotenv_parse } from "shared/string_utils";
 
 interface Props extends KeyValueArrayField {
   id: string;
@@ -102,51 +103,7 @@ const KeyValueArray: React.FC<Props> = (props) => {
   }
 
   const parseEnv = (src: any, options: any) => {
-    const debug = Boolean(options && options.debug);
-    const obj = {} as Record<string, string>;
-    const NEWLINE = "\n";
-    const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/;
-    const RE_NEWLINES = /\\n/g;
-    const NEWLINES_MATCH = /\n|\r|\r\n/;
-
-    // convert Buffers before splitting into lines and processing
-    src
-      .toString()
-      .split(NEWLINES_MATCH)
-      .forEach(function (line: any, idx: any) {
-        // matching "KEY' and 'VAL' in 'KEY=VAL'
-        const keyValueArr = line.match(RE_INI_KEY_VAL);
-        // matched?
-        if (keyValueArr != null) {
-          const key = keyValueArr[1];
-          // default undefined or missing values to empty string
-          let val = keyValueArr[2] || "";
-          const end = val.length - 1;
-          const isDoubleQuoted = val[0] === '"' && val[end] === '"';
-          const isSingleQuoted = val[0] === "'" && val[end] === "'";
-
-          // if single or double quoted, remove quotes
-          if (isSingleQuoted || isDoubleQuoted) {
-            val = val.substring(1, end);
-
-            // if double quoted, expand newlines
-            if (isDoubleQuoted) {
-              val = val.replace(RE_NEWLINES, NEWLINE);
-            }
-          } else {
-            // remove surrounding whitespace
-            val = val.trim();
-          }
-
-          obj[key] = val;
-        } else if (debug) {
-          console.log(
-            `did not match key and value when parsing line ${idx + 1}: ${line}`
-          );
-        }
-      });
-
-    return obj;
+    return dotenv_parse(src);
   };
 
   const readFile = (env: string) => {
@@ -333,7 +290,7 @@ const KeyValueArray: React.FC<Props> = (props) => {
 
           return (
             <InputWrapper key={i}>
-              <Input
+              <KeyInput
                 placeholder="ex: key"
                 width="270px"
                 value={entry.key}
@@ -360,30 +317,40 @@ const KeyValueArray: React.FC<Props> = (props) => {
                 }
               />
               <Spacer />
-              <Input
-                placeholder="ex: value"
-                width="270px"
-                value={value}
-                onChange={(e: any) => {
-                  e.persist();
-                  setState((prev) => {
-                    return {
-                      values: prev.values?.map((t, j) => {
-                        if (j == i) {
-                          return {
-                            ...t,
-                            value: e.target.value,
-                          };
-                        }
-                        return t;
-                      }),
-                    };
-                  });
-                }}
-                disabled={props.isReadOnly || value.includes("PORTERSECRET")}
-                type={value.includes("PORTERSECRET") ? "password" : "text"}
-                spellCheck={false}
-              />
+              {value?.includes("PORTERSECRET") ? (
+                <KeyInput
+                  placeholder="ex: value"
+                  width="270px"
+                  disabled
+                  type={"password"}
+                  spellCheck={false}
+                />
+              ) : (
+                <MultiLineInput
+                  placeholder="ex: value"
+                  width="270px"
+                  value={value}
+                  onChange={(e: any) => {
+                    e.persist();
+                    setState((prev) => {
+                      return {
+                        values: prev.values?.map((t, j) => {
+                          if (j == i) {
+                            return {
+                              ...t,
+                              value: e.target.value,
+                            };
+                          }
+                          return t;
+                        }),
+                      };
+                    });
+                  }}
+                  disabled={props.isReadOnly}
+                  spellCheck={false}
+                  rows={value?.split("\n").length}
+                />
+              )}
               {renderDeleteButton(i)}
               {renderHiddenOption(value.includes("PORTERSECRET"), i)}
               {checkOverridedKey(entry.key)}
@@ -486,24 +453,27 @@ export const getFinalVariablesForKeyValueArray: GetFinalVariablesFunction = (
     };
   }
 
+  const isNumber = (s: string) => {
+    return !isNaN(!s ? NaN : Number(String(s).trim()));
+  };
+
+  const rg = /(?:^|[^\\])(\\n)/g;
+  const fixNewlines = (s: string) => {
+    while (rg.test(s)) {
+      s = s.replace(rg, (str) => {
+        if (str.length == 2) return "\n";
+        if (str[0] != "\\") return str[0] + "\n";
+        return "\\n";
+      });
+    }
+    return s;
+  };
+
   if (props.variable.includes("env")) {
     let obj = {
       normal: {},
     } as any;
-    const rg = /(?:^|[^\\])(\\n)/g;
-    const fixNewlines = (s: string) => {
-      while (rg.test(s)) {
-        s = s.replace(rg, (str) => {
-          if (str.length == 2) return "\n";
-          if (str[0] != "\\") return str[0] + "\n";
-          return "\\n";
-        });
-      }
-      return s;
-    };
-    const isNumber = (s: string) => {
-      return !isNaN(!s ? NaN : Number(String(s).trim()));
-    };
+
     state.values.forEach((entry: any, i: number) => {
       if (isNumber(entry.value)) {
         obj.normal[entry.key] = entry.value;
@@ -539,20 +509,7 @@ export const getFinalVariablesForKeyValueArray: GetFinalVariablesFunction = (
     };
   } else {
     let obj = {} as any;
-    const rg = /(?:^|[^\\])(\\n)/g;
-    const fixNewlines = (s: string) => {
-      while (rg.test(s)) {
-        s = s.replace(rg, (str) => {
-          if (str.length == 2) return "\n";
-          if (str[0] != "\\") return str[0] + "\n";
-          return "\\n";
-        });
-      }
-      return s;
-    };
-    const isNumber = (s: string) => {
-      return !isNaN(!s ? NaN : Number(String(s).trim()));
-    };
+
     state.values.forEach((entry: any, i: number) => {
       if (isNumber(entry.value)) {
         obj[entry.key] = entry.value;
@@ -579,7 +536,7 @@ export const getMetadata: GetMetadataFunction<KeyValueArrayMetadata> = (
   state: KeyValueArrayFieldState
 ) => {
   // We don't need any metadata for other key-value-array fields yet so we return null for that variable
-  if (!props?.variable?.includes("env")) {
+  if (!state || !props?.variable?.includes("env")) {
     return {
       [props.variable]: null,
     };
@@ -654,22 +611,35 @@ const ExpandableEnvGroup: React.FC<{
 
                     return (
                       <InputWrapper key={i}>
-                        <Input
+                        <KeyInput
                           placeholder="ex: key"
                           width="270px"
                           value={key}
                           disabled
                         />
                         <Spacer />
-                        <Input
-                          placeholder="ex: value"
-                          width="270px"
-                          value={value}
-                          disabled
-                          type={
-                            value.includes("PORTERSECRET") ? "password" : "text"
-                          }
-                        />
+                        {value?.includes("PORTERSECRET") ? (
+                          <KeyInput
+                            placeholder="ex: value"
+                            width="270px"
+                            value={value}
+                            disabled
+                            type={
+                              value.includes("PORTERSECRET")
+                                ? "password"
+                                : "text"
+                            }
+                          />
+                        ) : (
+                          <MultiLineInput
+                            placeholder="ex: value"
+                            width="270px"
+                            value={value}
+                            disabled
+                            rows={value?.split("\n").length}
+                            spellCheck={false}
+                          ></MultiLineInput>
+                        )}
                       </InputWrapper>
                     );
                   }
@@ -822,7 +792,7 @@ type InputProps = {
   borderColor?: string;
 };
 
-const Input = styled.input<InputProps>`
+const KeyInput = styled.input<InputProps>`
   outline: none;
   border: none;
   margin-bottom: 5px;
@@ -837,6 +807,48 @@ const Input = styled.input<InputProps>`
   height: 35px;
 `;
 
+export const MultiLineInput = styled.textarea<InputProps>`
+  outline: none;
+  border: none;
+  margin-bottom: 5px;
+  font-size: 13px;
+  background: #ffffff11;
+  border: 1px solid
+    ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")};
+  border-radius: 3px;
+  min-width: ${(props) => (props.width ? props.width : "270px")};
+  max-width: ${(props) => (props.width ? props.width : "270px")};
+  color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
+  padding: 8px 10px 5px 10px;
+  min-height: 35px;
+  max-height: 100px;
+  white-space: nowrap;
+
+  ::-webkit-scrollbar {
+    width: 8px;
+    :horizontal {
+      height: 8px;
+    }
+  }
+
+  ::-webkit-scrollbar-corner {
+    width: 10px;
+    background: #ffffff11;
+    color: white;
+  }
+
+  ::-webkit-scrollbar-track {
+    width: 10px;
+    -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+    box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+  }
+
+  ::-webkit-scrollbar-thumb {
+    background-color: darkgrey;
+    outline: 1px solid slategrey;
+  }
+`;
+
 const Label = styled.div`
   color: #ffffff;
   margin-bottom: 10px;

+ 33 - 34
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArray.tsx

@@ -4,7 +4,8 @@ import Modal from "main/home/modals/Modal";
 import EnvEditorModal from "main/home/modals/EnvEditorModal";
 
 import upload from "assets/upload.svg";
-import { parseStringToEnvObject } from "./utils";
+import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray";
+import { dotenv_parse } from "shared/string_utils";
 
 export type KeyValueType = {
   key: string;
@@ -44,7 +45,7 @@ const EnvGroupArray = ({
   }, [values]);
 
   const readFile = (env: string) => {
-    const envObj = parseStringToEnvObject(env, null);
+    const envObj = dotenv_parse(env);
     const _values = values;
 
     for (const key in envObj) {
@@ -99,19 +100,36 @@ const EnvGroupArray = ({
                     spellCheck={false}
                   />
                   <Spacer />
-                  <Input
-                    placeholder="ex: value"
-                    width="270px"
-                    value={entry.value}
-                    onChange={(e: any) => {
-                      let _values = values;
-                      _values[i].value = e.target.value;
-                      setValues(_values);
-                    }}
-                    disabled={disabled || entry.locked}
-                    type={entry.hidden ? "password" : "text"}
-                    spellCheck={false}
-                  />
+
+                  {entry.hidden ? (
+                    <Input
+                      placeholder="ex: value"
+                      width="270px"
+                      value={entry.value}
+                      onChange={(e: any) => {
+                        let _values = values;
+                        _values[i].value = e.target.value;
+                        setValues(_values);
+                      }}
+                      disabled={disabled || entry.locked}
+                      type={entry.hidden ? "password" : "text"}
+                      spellCheck={false}
+                    />
+                  ) : (
+                    <MultiLineInput
+                      placeholder="ex: value"
+                      width="270px"
+                      value={entry.value}
+                      onChange={(e: any) => {
+                        let _values = values;
+                        _values[i].value = e.target.value;
+                        setValues(_values);
+                      }}
+                      rows={entry.value?.split("\n").length}
+                      disabled={disabled || entry.locked}
+                      spellCheck={false}
+                    />
+                  )}
 
                   {secretOption && (
                     <HideButton
@@ -227,25 +245,6 @@ const AddRowButton = styled.div`
   }
 `;
 
-const LoadButton = styled(AddRowButton)`
-  background: none;
-  border: 1px solid #ffffff55;
-  > i {
-    color: #ffffff44;
-    font-size: 16px;
-    margin-left: 8px;
-    margin-right: 10px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-  > img {
-    width: 14px;
-    margin-left: 10px;
-    margin-right: 12px;
-  }
-`;
-
 const UploadButton = styled(AddRowButton)`
   background: none;
   position: relative;

+ 0 - 49
dashboard/src/main/home/cluster-dashboard/env-groups/utils.ts

@@ -1,49 +0,0 @@
-export const parseStringToEnvObject = (src: any, options: any) => {
-  const debug = Boolean(options && options.debug);
-  const obj = {} as Record<string, string>;
-  const NEWLINE = "\n";
-  const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/;
-  const RE_NEWLINES = /\\n/g;
-  const NEWLINES_MATCH = /\n|\r|\r\n/;
-
-  // convert Buffers before splitting into lines and processing
-  src
-    .toString()
-    .split(NEWLINES_MATCH)
-    .forEach(function (line: any, idx: any) {
-      // matching "KEY' and 'VAL' in 'KEY=VAL'
-      const keyValueArr = line.match(RE_INI_KEY_VAL);
-      // matched?
-      if (keyValueArr != null) {
-        const key = keyValueArr[1];
-        // default undefined or missing values to empty string
-        let val = keyValueArr[2] || "";
-        const end = val.length - 1;
-        const isDoubleQuoted = val[0] === '"' && val[end] === '"';
-        const isSingleQuoted = val[0] === "'" && val[end] === "'";
-
-        // if single or double quoted, remove quotes
-        if (isSingleQuoted || isDoubleQuoted) {
-          val = val.substring(1, end);
-
-          // if double quoted, expand newlines
-          if (isDoubleQuoted) {
-            val = val.replace(RE_NEWLINES, NEWLINE);
-          }
-        } else {
-          // remove surrounding whitespace
-          val = val.trim();
-        }
-
-        obj[key] = val;
-      } else if (debug) {
-        /*
-        console.log(
-          `did not match key and value when parsing line ${idx + 1}: ${line}`
-        );
-        */
-      }
-    });
-
-  return obj;
-};

+ 34 - 29
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -231,8 +231,8 @@ export const ExpandedJobChartFC: React.FC<{
                 }}
                 isDeployedFromGithub={!!chart?.git_action_config?.git_repo}
                 repositoryUrl={chart?.git_action_config?.git_repo}
-                currentChartVersion={Number(chart.version)}
-                latestChartVersion={Number(chart.latest_version)}
+                currentChartVersion={Number(chart?.version)}
+                latestChartVersion={Number(chart?.latest_version)}
               />
             </>
           )}
@@ -265,7 +265,7 @@ export const ExpandedJobChartFC: React.FC<{
           setShowDeleteOverlay={(showOverlay: boolean) => {
             if (showOverlay) {
               setCurrentOverlay({
-                message: `Are you sure you want to delete ${chart.name}?`,
+                message: `Are you sure you want to delete ${chart?.name}?`,
                 onYes: handleDeleteChart,
                 onNo: () => setCurrentOverlay(null),
               });
@@ -293,6 +293,7 @@ export const ExpandedJobChartFC: React.FC<{
         <ExpandedJobHeader
           chart={chart}
           jobs={jobs}
+          disableRevisions
           closeChart={closeChart}
           refreshChart={refreshChart}
           upgradeChart={upgradeChart}
@@ -303,7 +304,7 @@ export const ExpandedJobChartFC: React.FC<{
         <Placeholder>
           <TextWrap>
             <Header>
-              <Spinner src={loading} /> Deleting "{chart.name}"
+              <Spinner src={loading} /> Deleting "{chart?.name}"
             </Header>
             You will be automatically redirected after deletion is complete.
           </TextWrap>
@@ -346,7 +347,7 @@ export const ExpandedJobChartFC: React.FC<{
             <PorterFormWrapper
               formData={formData}
               valuesToOverride={{
-                namespace: chart.namespace,
+                namespace: chart?.namespace,
                 clusterId: currentCluster?.id,
               }}
               renderTabContents={renderTabContents}
@@ -393,6 +394,7 @@ const ExpandedJobHeader: React.FC<{
   upgradeChart: () => Promise<void>;
   loadChartWithSpecificRevision: (revision: number) => void;
   setDisableForm: (disable: boolean) => void;
+  disableRevisions?: boolean;
 }> = ({
   chart,
   closeChart,
@@ -401,13 +403,14 @@ const ExpandedJobHeader: React.FC<{
   upgradeChart,
   loadChartWithSpecificRevision,
   setDisableForm,
+  disableRevisions,
 }) => (
   <HeaderWrapper>
     <BackButton onClick={closeChart}>
       <BackButtonImg src={backArrow} />
     </BackButton>
-    <TitleSection icon={chart.chart.metadata.icon} iconWidth="33px">
-      {chart.name}
+    <TitleSection icon={chart?.chart.metadata.icon} iconWidth="33px">
+      {chart?.name}
       <DeploymentType currentChart={chart} />
       <TagWrapper>
         Namespace <NamespaceTag>{chart.namespace}</NamespaceTag>
@@ -423,28 +426,30 @@ const ExpandedJobHeader: React.FC<{
         {" " + readableDate(chart.info.last_deployed)}
       </LastDeployed>
     </InfoWrapper>
-    <RevisionSection
-      chart={chart}
-      refreshChart={() => refreshChart()}
-      setRevision={(chart, isCurrent) => {
-        loadChartWithSpecificRevision(chart?.version);
-        setDisableForm(!isCurrent);
-      }}
-      forceRefreshRevisions={false}
-      refreshRevisionsOff={() => {}}
-      shouldUpdate={
-        chart.latest_version &&
-        chart.latest_version !== chart.chart.metadata.version
-      }
-      latestVersion={chart.latest_version}
-      upgradeVersion={(_version, cb) => {
-        upgradeChart().then(() => {
-          if (typeof cb === "function") {
-            cb();
-          }
-        });
-      }}
-    />
+    {!disableRevisions ? (
+      <RevisionSection
+        chart={chart}
+        refreshChart={() => refreshChart()}
+        setRevision={(chart, isCurrent) => {
+          loadChartWithSpecificRevision(chart?.version);
+          setDisableForm(!isCurrent);
+        }}
+        forceRefreshRevisions={false}
+        refreshRevisionsOff={() => {}}
+        shouldUpdate={
+          chart?.latest_version &&
+          chart?.latest_version !== chart?.chart.metadata.version
+        }
+        latestVersion={chart?.latest_version}
+        upgradeVersion={(_version, cb) => {
+          upgradeChart().then(() => {
+            if (typeof cb === "function") {
+              cb();
+            }
+          });
+        }}
+      />
+    ) : null}
   </HeaderWrapper>
 );
 

+ 23 - 5
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/useJobs.ts

@@ -3,7 +3,7 @@ import { useContext, useEffect, useRef, useState } from "react";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
-import { ChartType } from "shared/types";
+import { ChartType, ChartTypeWithExtendedConfig } from "shared/types";
 import yaml from "js-yaml";
 import { usePrevious } from "shared/hooks/usePrevious";
 import { useRouting } from "shared/routing";
@@ -41,6 +41,27 @@ export const useJobs = (chart: ChartType) => {
     closeWebsocket,
   } = useWebsockets();
 
+  const isBeingDeployed = (latestJob: any) => {
+    const currentChart: ChartTypeWithExtendedConfig = chart;
+    const chartImage = currentChart.config.image.repository;
+
+    let latestImageDetected =
+      latestJob?.spec?.template?.spec?.containers[0]?.image;
+
+    if (!PORTER_IMAGE_TEMPLATES.includes(chartImage)) {
+      return false;
+    }
+
+    if (
+      latestImageDetected &&
+      !PORTER_IMAGE_TEMPLATES.includes(latestImageDetected)
+    ) {
+      return false;
+    }
+
+    return true;
+  };
+
   const sortJobsAndSave = (newJobs: any[]) => {
     // Set job run from URL if needed
     const urlParams = new URLSearchParams(location.search);
@@ -51,10 +72,7 @@ export const useJobs = (chart: ChartType) => {
 
     newJobs.sort((job1, job2) => getTime(job2) - getTime(job1));
 
-    let latestImageDetected =
-      newJobs[0]?.spec?.template?.spec?.containers[0]?.image;
-    if (!PORTER_IMAGE_TEMPLATES.includes(latestImageDetected)) {
-      // this.setState({ jobs, newestImage, imageIsPlaceholder: false });
+    if (isBeingDeployed(newJobs[0])) {
       setHasPorterImageTemplate(false);
     }
     jobsRef.current = newJobs;

+ 1 - 0
dashboard/src/shared/hooks/useChart.ts

@@ -100,6 +100,7 @@ export const useChart = (oldChart: ChartType, closeChart: () => void) => {
    * Delete/Uninstall chart
    */
   const deleteChart = async () => {
+    setStatus("deleting");
     try {
       const syncedEnvGroups = chart.config?.container?.env?.synced || [];
       const removeApplicationToEnvGroupPromises = syncedEnvGroups.map(

+ 42 - 0
dashboard/src/shared/string_utils.ts

@@ -11,3 +11,45 @@ export const readableDate = (s: string) => {
 export const capitalize = (s: string) => {
   return s.charAt(0).toUpperCase() + s.substring(1).toLowerCase();
 };
+
+const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
+
+export const dotenv_parse = (src: string): Record<string, string> => {
+  // Parser src into an Object
+
+  const obj = {} as Record<string, string>;
+
+  // Convert buffer to string
+  let lines = src.toString();
+
+  // Convert line breaks to same format
+  lines = lines.replace(/\r\n?/gm, "\n");
+
+  let match;
+  while ((match = LINE.exec(lines)) != null) {
+    const key = match[1];
+
+    // Default undefined or null to empty string
+    let value = match[2] || "";
+
+    // Remove whitespace
+    value = value.trim();
+
+    // Check if double quoted
+    const maybeQuote = value[0];
+
+    // Remove surrounding quotes
+    value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
+
+    // Expand newlines if double quoted
+    if (maybeQuote === '"') {
+      value = value.replace(/\\n/g, "\n");
+      value = value.replace(/\\r/g, "\r");
+    }
+
+    // Add to object
+    obj[key] = value;
+  }
+
+  return obj;
+};