Procházet zdrojové kódy

First version of new app

jnfrati před 3 roky
rodič
revize
c5699e9ac0

+ 88 - 2
dashboard/src/main/home/cluster-dashboard/stacks/launch/NewApp.tsx

@@ -1,7 +1,93 @@
-import React from "react";
+import InputRow from "components/form-components/InputRow";
+import Loading from "components/Loading";
+import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
+import React, { useContext, useEffect, useState } from "react";
+import { useParams } from "react-router";
+import api from "shared/api";
+import { useRouting } from "shared/routing";
+import { ExpandedPorterTemplate } from "shared/types";
+import { StacksLaunchContext } from "./Store";
 
 const NewApp = () => {
-  return <div>NewApp</div>;
+  const { addAppResource, newStack } = useContext(StacksLaunchContext);
+
+  const params = useParams<{
+    template_name: string;
+    version: string;
+  }>();
+
+  const [template, setTemplate] = useState<ExpandedPorterTemplate>();
+  const [isLoading, setIsLoading] = useState(true);
+  const [hasError, setHasError] = useState(false);
+
+  const [appName, setAppName] = useState("");
+
+  const { pushFiltered } = useRouting();
+
+  useEffect(() => {
+    let isSubscribed = true;
+    if (!params.template_name || !params.version) {
+      return () => {
+        isSubscribed = false;
+      };
+    }
+
+    setHasError(false);
+
+    api
+      .getTemplateInfo<ExpandedPorterTemplate>(
+        "<token>",
+        {},
+        { name: params.template_name, version: params.version }
+      )
+      .then((res) => {
+        if (isSubscribed) {
+          setTemplate(res.data);
+        }
+      })
+      .catch((err) => {
+        setHasError(true);
+      })
+      .finally(() => {
+        if (isSubscribed) {
+          setIsLoading(false);
+        }
+      });
+
+    return () => {
+      isSubscribed = false;
+    };
+  }, [params]);
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
+  if (hasError) {
+    return <>Unexpected error</>;
+  }
+
+  const handleSubmit = (values: any) => {
+    addAppResource({
+      name: appName,
+      source_config_name: newStack.source_configs[0]?.name || "",
+      template_name: params.template_name,
+      template_version: params.version,
+      values,
+    });
+    pushFiltered("/stacks/launch/overview", []);
+  };
+
+  return (
+    <div>
+      <InputRow
+        type="string"
+        value={appName}
+        setValue={(val: string) => setAppName(val)}
+      />
+      <PorterFormWrapper formData={template.form} onSubmit={handleSubmit} />
+    </div>
+  );
 };
 
 export default NewApp;

+ 266 - 3
dashboard/src/main/home/cluster-dashboard/stacks/launch/Overview.tsx

@@ -1,12 +1,16 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useRef, useState } from "react";
+import semver from "semver";
 import { StacksLaunchContext } from "./Store";
 import InputRow from "components/form-components/InputRow";
 import Selector from "components/Selector";
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { ClusterType } from "shared/types";
+import { ClusterType, PorterTemplate } from "shared/types";
 import useAuth from "shared/auth/useAuth";
 import DynamicLink from "components/DynamicLink";
+import styled from "styled-components";
+import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
+import { capitalize } from "shared/string_utils";
 
 const Overview = () => {
   const {
@@ -132,9 +136,268 @@ const Overview = () => {
         <div key={app.name}>{app.name}</div>
       ))}
 
-      <DynamicLink to="/stacks/launch/new-app"> New Application </DynamicLink>
+      <AddResourceButton />
     </>
   );
 };
 
 export default Overview;
+
+const AddResourceButton = () => {
+  const [templates, setTemplates] = useState<PorterTemplate[]>([]);
+  const [currentTemplate, setCurrentTemplate] = useState<PorterTemplate>();
+  const [currentVersion, setCurrentVersion] = useState("");
+
+  const getTemplates = async () => {
+    try {
+      const res = await api.getTemplates<PorterTemplate[]>(
+        "<token>",
+        {
+          repo_url: process.env.APPLICATION_CHART_REPO_URL,
+        },
+        {}
+      );
+      let sortedVersionData = res.data
+        .map((template: PorterTemplate) => {
+          let versions = template.versions.reverse();
+
+          versions = template.versions.sort(semver.rcompare);
+
+          return {
+            ...template,
+            versions,
+            currentVersion: versions[0],
+          };
+        })
+        .sort((a, b) => {
+          if (a.name < b.name) {
+            return -1;
+          }
+          if (a.name > b.name) {
+            return 1;
+          }
+          return 0;
+        });
+
+      return sortedVersionData;
+    } catch (err) {}
+  };
+
+  useEffect(() => {
+    getTemplates().then((templates) => {
+      setTemplates(templates);
+      setCurrentTemplate(templates[0]);
+      setCurrentVersion(templates[0].currentVersion);
+    });
+  }, []);
+
+  return (
+    <AddResourceButtonStyles.Wrapper>
+      <AddResourceButtonStyles.Flex>
+        Add a new{" "}
+        <TemplateSelector
+          options={templates}
+          value={currentTemplate}
+          onChange={(template) => {
+            setCurrentTemplate(template);
+            setCurrentVersion(template.currentVersion);
+          }}
+        />
+        <VersionSelector
+          options={currentTemplate?.versions || []}
+          value={currentVersion}
+          onChange={setCurrentVersion}
+        />
+      </AddResourceButtonStyles.Flex>
+
+      <DynamicLink
+        to={`/stacks/launch/new-app/${currentTemplate?.name}/${currentVersion}`}
+      >
+        Create
+      </DynamicLink>
+    </AddResourceButtonStyles.Wrapper>
+  );
+};
+
+const TemplateSelector = ({
+  value,
+  options,
+  onChange,
+}: {
+  value: PorterTemplate;
+  options: PorterTemplate[];
+  onChange: (newValue: PorterTemplate) => void;
+}) => {
+  const wrapperRef = useRef();
+
+  const [isExpanded, setIsExpanded] = useState(false);
+
+  useOutsideAlerter(wrapperRef, () => setIsExpanded(false));
+
+  const getName = (template: PorterTemplate) => {
+    if (template?.name === "web") {
+      return "Web Application";
+    }
+    return capitalize(template?.name || "");
+  };
+
+  return (
+    <>
+      <SelectorStyles.Wrapper ref={wrapperRef}>
+        <SelectorStyles.Button
+          expanded={isExpanded}
+          onClick={() => setIsExpanded((prev) => !prev)}
+        >
+          {getName(value)}
+          <i className="material-icons">arrow_drop_down</i>
+        </SelectorStyles.Button>
+
+        {isExpanded ? (
+          <SelectorStyles.Dropdown>
+            {options.map((template) => (
+              <SelectorStyles.Option
+                className={template.name === value.name ? "active" : ""}
+                onClick={() => {
+                  onChange(template);
+                  setIsExpanded(false);
+                }}
+              >
+                <SelectorStyles.OptionText>
+                  {getName(template)}
+                </SelectorStyles.OptionText>
+              </SelectorStyles.Option>
+            ))}
+          </SelectorStyles.Dropdown>
+        ) : null}
+      </SelectorStyles.Wrapper>
+    </>
+  );
+};
+
+const VersionSelector = ({
+  value,
+  options,
+  onChange,
+}: {
+  value: string;
+  options: string[];
+  onChange: (newValue: string) => void;
+}) => {
+  const wrapperRef = useRef();
+
+  const [isExpanded, setIsExpanded] = useState(false);
+
+  useOutsideAlerter(wrapperRef, () => setIsExpanded(false));
+
+  return (
+    <>
+      <SelectorStyles.Wrapper ref={wrapperRef}>
+        <SelectorStyles.Button
+          expanded={isExpanded}
+          onClick={() => setIsExpanded((prev) => !prev)}
+        >
+          {capitalize(value)}
+          <i className="material-icons">arrow_drop_down</i>
+        </SelectorStyles.Button>
+
+        {isExpanded ? (
+          <SelectorStyles.Dropdown>
+            {options.map((version) => (
+              <SelectorStyles.Option
+                className={version === value ? "active" : ""}
+                onClick={() => {
+                  onChange(version);
+                  setIsExpanded(false);
+                }}
+              >
+                {capitalize(version)}
+              </SelectorStyles.Option>
+            ))}
+          </SelectorStyles.Dropdown>
+        ) : null}
+      </SelectorStyles.Wrapper>
+    </>
+  );
+};
+
+const AddResourceButtonStyles = {
+  Wrapper: styled.div`
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  `,
+  Text: styled.span`
+    font-size: 20px;
+  `,
+  Flex: styled.div`
+    display: flex;
+    align-items: center;
+  `,
+};
+
+const SelectorStyles = {
+  Wrapper: styled.div`
+    max-width: 200px;
+    position: relative;
+    font-size: 13px;
+
+    margin-left: 10px;
+  `,
+  Button: styled.div`
+    background-color: #ffffff11;
+    border: 1px solid #ffffff22;
+    border-radius: 5px;
+    min-width: 115px;
+    min-height: 30px;
+    padding: 0 15px;
+
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    white-space: nowrap;
+    overflow-y: hidden;
+    text-overflow: ellipsis;
+    cursor: pointer;
+
+    > i {
+      font-size: 20px;
+      transform: ${(props: { expanded: boolean }) =>
+        props.expanded ? "rotate(180deg)" : ""};
+    }
+  `,
+  Dropdown: styled.div`
+    position: absolute;
+    background-color: #26282f;
+    width: 100%;
+    max-height: 200px;
+    overflow-y: auto;
+  `,
+  Option: styled.div`
+    min-height: 35px;
+    padding: 0 15px;
+
+    display: flex;
+    align-items: center;
+
+    cursor: pointer;
+
+    &.active {
+      background-color: #32343c;
+    }
+
+    :hover {
+      background-color: #32343c;
+    }
+
+    :not(:last-child) {
+      border-bottom: 1px solid #ffffff15;
+    }
+  `,
+  OptionText: styled.span`
+    max-width: 115px;
+    overflow-x: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  `,
+};

+ 4 - 4
dashboard/src/main/home/cluster-dashboard/stacks/launch/Store.tsx

@@ -74,17 +74,17 @@ const StacksLaunchContextProvider: React.FC<{}> = ({ children }) => {
   const addSourceConfig: StacksLaunchContextType["addSourceConfig"] = (
     sourceConfig
   ) => {
-    const newSourceConfigName = (stackName: string, index: number) =>
+    const newSourceConfigName = (index: number) =>
       sourceConfig.build
-        ? `${stackName}-${sourceConfig.build.method}-${index}`
-        : `${stackName}-${sourceConfig.image_repo_uri}-${sourceConfig.image_tag}-${index}`;
+        ? `${sourceConfig.build.method}-${index}`
+        : `${sourceConfig.image_repo_uri}-${sourceConfig.image_tag}-${index}`;
 
     setNewStack((prev) => ({
       ...prev,
       source_configs: [
         ...prev.source_configs,
         {
-          name: newSourceConfigName(prev.name, prev.source_configs.length),
+          name: newSourceConfigName(prev.source_configs.length),
           ...sourceConfig,
         },
       ],

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/stacks/launch/index.tsx

@@ -17,7 +17,7 @@ const LaunchRoutes = () => {
         <Route path={`${path}/overview`}>
           <Overview />
         </Route>
-        <Route path={`${path}/new-app`}>
+        <Route path={`${path}/new-app/:template_name/:version`}>
           <NewApp />
         </Route>
         <Route path={`*`}>

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

@@ -1,3 +1,5 @@
+import ValuesYaml from "main/home/cluster-dashboard/expanded-chart/ValuesYaml";
+
 export interface ClusterType {
   id: number;
   name: string;
@@ -164,6 +166,13 @@ export interface PorterTemplate {
   repo_url?: string;
 }
 
+export interface ExpandedPorterTemplate {
+  form: FormYAML;
+  markdown: string;
+  metadata: ChartType["chart"]["metadata"];
+  values: ChartTypeWithExtendedConfig["config"];
+}
+
 // FormYAML represents a chart's values.yaml form abstraction
 export interface FormYAML {
   name?: string;