Przeglądaj źródła

Implemented new app resource form for expanded stack

jnfrati 3 lat temu
rodzic
commit
9723ff8297

+ 14 - 40
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx

@@ -2,7 +2,7 @@ import Loading from "components/Loading";
 import Placeholder from "components/Placeholder";
 import TabSelector from "components/TabSelector";
 import TitleSection from "components/TitleSection";
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useState } from "react";
 import backArrow from "assets/back_arrow.png";
 import { useParams } from "react-router";
 import api from "shared/api";
@@ -11,13 +11,11 @@ import { useRouting } from "shared/routing";
 import { readableDate } from "shared/string_utils";
 import styled from "styled-components";
 import ChartList from "../../chart/ChartList";
-import SortSelector from "../../SortSelector";
 import Status from "../components/Status";
 import {
   Br,
   InfoWrapper,
   LastDeployed,
-  LineBreak,
   NamespaceTag,
   SepDot,
   Text,
@@ -29,50 +27,29 @@ import RevisionList from "./_RevisionList";
 import SourceConfig from "./_SourceConfig";
 import { NavLink } from "react-router-dom";
 import Settings from "./components/Settings";
+import { ExpandedStackStore } from "./Store";
+import DynamicLink from "components/DynamicLink";
 
 const ExpandedStack = () => {
-  const { namespace, stack_id } = useParams<{
+  const { namespace } = useParams<{
     namespace: string;
     stack_id: string;
   }>();
 
+  const { stack, refreshStack } = useContext(ExpandedStackStore);
+
   const { pushFiltered } = useRouting();
 
   const { currentProject, currentCluster, setCurrentError } = useContext(
     Context
   );
 
-  const [stack, setStack] = useState<Stack>();
-  const [isLoading, setIsLoading] = useState(true);
   const [isDeleting, setIsDeleting] = useState(false);
   const [currentTab, setCurrentTab] = useState("apps");
 
-  const [currentRevision, setCurrentRevision] = useState<FullStackRevision>();
-
-  const getStack = async () => {
-    setIsLoading(true);
-    try {
-      const newStack = await api
-        .getStack<Stack>(
-          "<token>",
-          {},
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-            stack_id: stack_id,
-            namespace,
-          }
-        )
-        .then((res) => res.data);
-
-      setStack(newStack);
-      setCurrentRevision(newStack.latest_revision);
-      setIsLoading(false);
-    } catch (error) {
-      setCurrentError(error);
-      pushFiltered("/stacks", []);
-    }
-  };
+  const [currentRevision, setCurrentRevision] = useState<FullStackRevision>(
+    () => stack.latest_revision
+  );
 
   const handleDelete = () => {
     setIsDeleting(true);
@@ -96,12 +73,8 @@ const ExpandedStack = () => {
       });
   };
 
-  useEffect(() => {
-    getStack();
-  }, [stack_id]);
-
-  if (isLoading) {
-    return <Loading />;
+  if (stack === null) {
+    return null;
   }
 
   if (isDeleting) {
@@ -163,7 +136,7 @@ const ExpandedStack = () => {
         stackId={stack.id}
         stackNamespace={namespace}
         onRevisionClick={(revision) => setCurrentRevision(revision)}
-        onRollback={() => getStack()}
+        onRollback={() => refreshStack()}
       ></RevisionList>
       <Br />
       <TabSelector
@@ -175,6 +148,7 @@ const ExpandedStack = () => {
             component: (
               <>
                 <Gap></Gap>
+                <DynamicLink to={`new-app-resource`}>Add new app</DynamicLink>
                 {currentRevision.id !== stack.latest_revision.id ? (
                   <ChartListWrapper>
                     <Placeholder>
@@ -209,7 +183,7 @@ const ExpandedStack = () => {
                   namespace={namespace}
                   revision={currentRevision}
                   readOnly={stack.latest_revision.id !== currentRevision.id}
-                  onSourceConfigUpdate={() => getStack()}
+                  onSourceConfigUpdate={() => refreshStack()}
                 ></SourceConfig>
               </>
             ),

+ 156 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_Settings.tsx

@@ -1,7 +1,161 @@
-import React from "react";
+import { AxiosError } from "axios";
+import { PopulatedEnvGroup } from "components/porter-form/types";
+import React, { useContext, useEffect, useState } from "react";
+import { useParams } from "react-router";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { useRouting } from "shared/routing";
+import NewAppResourceForm from "../../components/NewAppResourceForm";
+import { CreateStackBody } from "../../types";
+import { ExpandedStackStore } from "../Store";
+
+const parsePopulatedEnvGroup = (envGroup: PopulatedEnvGroup) => {
+  const variables = Object.entries(envGroup.variables)
+    .filter(([_, value]) => !value.includes("PORTERSECRET"))
+    .reduce(
+      (acc, [key, value]) => ({ ...acc, [key]: value }),
+      {} as Record<string, string>
+    );
+  const secret_variables = Object.entries(envGroup.variables)
+    .filter(([_, value]) => value.includes("PORTERSECRET"))
+    .reduce(
+      (acc, [key, value]) => ({ ...acc, [key]: value }),
+      {} as Record<string, string>
+    );
+
+  return {
+    name: envGroup.name,
+    variables,
+    secret_variables,
+    linked_applications: envGroup.applications as string[],
+  };
+};
 
 const Settings = () => {
-  return <div>Settings</div>;
+  const params = useParams<{
+    template_name: string;
+    template_version: string;
+    namespace: string;
+  }>();
+  const { stack, refreshStack } = useContext(ExpandedStackStore);
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+  const [availableEnvGroups, setAvailableEnvGroups] = useState<
+    {
+      name: string;
+      variables: Record<string, string>;
+      secret_variables: Record<string, string>;
+      linked_applications: string[];
+    }[]
+  >([]);
+
+  const { pushFiltered } = useRouting();
+
+  const populateEnvGroups = async () => {
+    const stackEnvGroups = stack.latest_revision.env_groups;
+    const envGroupsPromises = stackEnvGroups.map((envGroup) =>
+      api
+        .getEnvGroup<PopulatedEnvGroup>(
+          "<token>",
+          {},
+          {
+            id: currentProject.id,
+            cluster_id: currentCluster.id,
+            name: envGroup.name,
+            namespace: params.namespace,
+            version: envGroup.env_group_version,
+          }
+        )
+        .then((res) => res.data)
+    );
+
+    try {
+      const response = await Promise.allSettled(envGroupsPromises);
+
+      const envGroups = response
+        .map((res) => {
+          if (res.status === "fulfilled") {
+            return res.value;
+          }
+          return undefined;
+        })
+        .filter(Boolean);
+
+      return envGroups;
+    } catch (error) {
+      setCurrentError(error);
+      throw error;
+    }
+  };
+
+  useEffect(() => {
+    let isSubscribed = true;
+
+    populateEnvGroups().then((populatedEnvGroups) => {
+      if (!isSubscribed) {
+        return;
+      }
+
+      if (Array.isArray(populatedEnvGroups)) {
+        const availableEnvGroups = populatedEnvGroups.map(
+          parsePopulatedEnvGroup
+        );
+
+        setAvailableEnvGroups(availableEnvGroups);
+      }
+    });
+
+    return () => {
+      isSubscribed = false;
+    };
+  }, [stack, params, currentProject, currentCluster]);
+
+  const handleSubmit = async (
+    appResource: CreateStackBody["app_resources"][0]
+  ) => {
+    try {
+      await api.addStackAppResource(
+        "<token>",
+        {
+          app_resource: appResource,
+        },
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          namespace: params.namespace,
+          stack_id: stack.id,
+        }
+      );
+
+      await refreshStack();
+
+      pushFiltered(`/stacks/${params.namespace}/${stack.id}`, []);
+    } catch (error) {
+      const axiosError: AxiosError = error;
+      if (axiosError.code === "409") {
+        throw "Application resource name already exists.";
+      }
+
+      throw "Unexpected error, please try again.";
+    }
+  };
+
+  return (
+    <NewAppResourceForm
+      availableEnvGroups={availableEnvGroups}
+      namespace={params.namespace}
+      sourceConfig={stack.latest_revision.source_configs[0]}
+      templateInfo={{
+        name: params.template_name,
+        version: params.template_version,
+      }}
+      onCancel={() => {
+        pushFiltered(`../template-selector`, []);
+      }}
+      onSubmit={handleSubmit}
+    />
+  );
 };
 
 export default Settings;

+ 10 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx

@@ -4,9 +4,10 @@ import { PorterTemplate } from "shared/types";
 import semver from "semver";
 import Loading from "components/Loading";
 import Placeholder from "components/Placeholder";
-import { Card } from "../../launch/components/styles";
+import { BackButton, Card } from "../../launch/components/styles";
 import DynamicLink from "components/DynamicLink";
 import { VersionSelector } from "../../launch/components/VersionSelector";
+import TitleSection from "components/TitleSection";
 
 const TemplateSelector = () => {
   const [templates, setTemplates] = useState<PorterTemplate[]>([]);
@@ -107,7 +108,14 @@ const TemplateSelector = () => {
 
   return (
     <>
-      <h2>Select the template</h2>
+      <TitleSection>
+        <DynamicLink to={`../`}>
+          <BackButton>
+            <i className="material-icons">keyboard_backspace</i>
+          </BackButton>
+        </DynamicLink>
+        Select a template
+      </TitleSection>
       <Card.Grid>
         {templates.map((template) => {
           return (

+ 28 - 8
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/Store.tsx

@@ -9,10 +9,12 @@ import type { Stack } from "../types";
 
 interface StoreType {
   stack: Stack;
+  refreshStack: () => Promise<void>;
 }
 
 const defaultValues: StoreType = {
   stack: {} as Stack,
+  refreshStack: async () => {},
 };
 
 export const ExpandedStackStore = createContext(defaultValues);
@@ -23,6 +25,7 @@ const ExpandedStackStoreProvider: React.FC = ({ children }) => {
   );
 
   const [stack, setStack] = useState<Stack>(null);
+  const [isLoading, setIsLoading] = useState(true);
 
   const { namespace, stack_id } = useParams<{
     namespace: string;
@@ -30,9 +33,8 @@ const ExpandedStackStoreProvider: React.FC = ({ children }) => {
   }>();
   const { pushFiltered } = useRouting();
 
-  useEffect(() => {
-    let isSubscribed = true;
-
+  const getStack = async (props: { subscribed: boolean }) => {
+    setIsLoading(true);
     api
       .getStack<Stack>(
         "<token>",
@@ -45,23 +47,34 @@ const ExpandedStackStoreProvider: React.FC = ({ children }) => {
         }
       )
       .then((res) => {
-        if (isSubscribed) {
+        if (props.subscribed) {
           setStack(res.data);
         }
       })
       .catch(() => {
-        if (isSubscribed) {
+        if (props.subscribed) {
           setCurrentError("Couldn't find any stack with the given ID");
           pushFiltered("/stacks", []);
         }
+      })
+      .finally(() => {
+        if (props.subscribed) {
+          setIsLoading(false);
+        }
       });
+  };
+
+  useEffect(() => {
+    let isSubscribed = { subscribed: true };
+
+    getStack(isSubscribed);
 
     return () => {
-      isSubscribed = false;
+      isSubscribed.subscribed = false;
     };
   }, [currentCluster, currentProject, namespace, stack_id]);
 
-  if (stack === null) {
+  if (isLoading) {
     return (
       <Placeholder>
         <Loading />
@@ -70,7 +83,14 @@ const ExpandedStackStoreProvider: React.FC = ({ children }) => {
   }
 
   return (
-    <ExpandedStackStore.Provider value={{ stack }}>
+    <ExpandedStackStore.Provider
+      value={{
+        stack,
+        refreshStack: async () => {
+          await getStack({ subscribed: true });
+        },
+      }}
+    >
       {children}
     </ExpandedStackStore.Provider>
   );

+ 10 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/routes.tsx

@@ -1,5 +1,11 @@
 import React from "react";
-import { Route, Switch, useRouteMatch } from "react-router";
+import {
+  Redirect,
+  Route,
+  Switch,
+  useLocation,
+  useRouteMatch,
+} from "react-router";
 
 import ExpandedStack from "./ExpandedStack";
 import NewAppResourceRoutes from "./NewAppResource";
@@ -8,17 +14,19 @@ import ExpandedStackStoreProvider from "./Store";
 
 const ExpandedStackRoutes = () => {
   const { path } = useRouteMatch();
+  const { pathname } = useLocation();
 
   return (
     <ExpandedStackStoreProvider>
       <Switch>
+        <Redirect from="/:url*(/+)" to={pathname.slice(0, -1)} />
         <Route path={`${path}/new-env-group`} exact>
           <NewEnvGroup />
         </Route>
         <Route path={`${path}/new-app-resource`}>
           <NewAppResourceRoutes />
         </Route>
-        <Route path={`${path}/`} exact>
+        <Route path={`${path}`} exact>
           <ExpandedStack />
         </Route>
         <Route path={`*`}>

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/stacks/components/NewAppResourceForm.tsx

@@ -244,7 +244,7 @@ const NewAppResourceForm = (props: {
   return (
     <>
       <TitleSection>
-        <BackButton>
+        <BackButton onClick={onCancel}>
           <i className="material-icons">keyboard_backspace</i>
         </BackButton>
         <Polymer>

+ 2 - 8
dashboard/src/main/home/cluster-dashboard/stacks/routes.tsx

@@ -1,11 +1,5 @@
 import React, { useContext } from "react";
-import {
-  Redirect,
-  Route,
-  Switch,
-  useLocation,
-  useRouteMatch,
-} from "react-router";
+import { Redirect, Route, Switch, useRouteMatch } from "react-router";
 import { Context } from "shared/Context";
 import Dashboard from "./Dashboard";
 import ExpandedStackRoutes from "./ExpandedStack/routes";
@@ -27,7 +21,7 @@ const routes = () => {
       <Route path={`${path}/:namespace/:stack_id`}>
         <ExpandedStackRoutes />
       </Route>
-      <Route path={`${path}/`} exact>
+      <Route path={`${path}`} exact>
         <Dashboard />
       </Route>
       <Route path={`*`}>