Browse Source

Image Settings for Porter Apps V1 (#3731) (#3736)

Feroze Mohideen 2 years ago
parent
commit
fcf8f93252

+ 43 - 41
dashboard/src/main/home/app-dashboard/app-view/tabs/ImageSettingsTab.tsx

@@ -5,18 +5,19 @@ import { useLatestRevision } from "../LatestRevisionContext";
 import Spacer from "components/porter/Spacer";
 import Button from "components/porter/Button";
 import Error from "components/porter/Error";
-import { match } from "ts-pattern";
 import styled from "styled-components";
 import copy from "assets/copy-left.svg"
 import CopyToClipboard from "components/CopyToClipboard";
 import Link from "components/porter/Link";
 import Text from "components/porter/Text";
 import ImageSettings from "../../image-settings/ImageSettings";
+import { match } from "ts-pattern";
 
 const ImageSettingsTab: React.FC = () => {
     const {
         watch,
         formState: { isSubmitting, errors },
+        setValue,
     } = useFormContext<PorterAppFormData>();
     const { projectId, latestRevision, latestProto } = useLatestRevision();
 
@@ -34,46 +35,47 @@ const ImageSettingsTab: React.FC = () => {
         return "";
     }, [isSubmitting, errors]);
 
-    return (
-        <>
-            {match(source)
-                .with({ type: "docker-registry" }, (source) => (
-                    <>
-                        <ImageSettings
-                            projectId={projectId}
-                            source={source}
-                        />
-                        <Spacer y={1} />
-                        <Button
-                            type="submit"
-                            status={buttonStatus}
-                            disabled={
-                                isSubmitting ||
-                                latestRevision.status === "CREATED" ||
-                                latestRevision.status === "AWAITING_BUILD_ARTIFACT"
-                            }
-                        >
-                            Save image settings
-                        </Button>
-                        <Spacer y={1} />
-                        <Text size={16}>Update command</Text>
-                        <Spacer y={0.5} />
-                        <Text color="helper">If you have the <Link to="https://docs.porter.run/standard/cli/command-reference/porter-update" target="_blank"><Text>Porter CLI</Text></Link> installed, you can update your application image tag by running the following command: </Text>
-                        <Spacer y={0.5} />
-                        <IdContainer>
-                            <Code>{`$ porter app update-tag ${latestProto.name} --tag latest`}</Code>
-                            <CopyContainer>
-                                <CopyToClipboard text={`porter app update-tag ${latestProto.name} --tag latest`}>
-                                    <CopyIcon src={copy} alt="copy" />
-                                </CopyToClipboard>
-                            </CopyContainer>
-                        </IdContainer>
-                    </>
-                ))
-                .otherwise(() => null)
-            }
-        </>
-    );
+    return match(source)
+        .with({ type: "docker-registry" }, (source) => (
+            <>
+                <ImageSettings
+                    projectId={projectId}
+                    imageUri={source.image?.repository ?? ""}
+                    setImageUri={(uri: string) => setValue("source.image", { ...(source?.image ?? {}), repository: uri })}
+                    imageTag={source.image?.tag ?? ""}
+                    setImageTag={(tag: string) => setValue("source.image", { ...(source?.image ?? {}), tag })}
+                    resetImageInfo={() => setValue("source.image", { repository: "", tag: "" })}
+                />
+                <Spacer y={1} />
+                <Button
+                    type="submit"
+                    status={buttonStatus}
+                    disabled={
+                        isSubmitting
+                        || latestRevision.status === "CREATED"
+                        || latestRevision.status === "AWAITING_BUILD_ARTIFACT"
+                        || !source.image?.repository
+                        || !source.image?.tag
+                    }
+                >
+                    Save image settings
+                </Button>
+                <Spacer y={1} />
+                <Text size={16}>Update command</Text>
+                <Spacer y={0.5} />
+                <Text color="helper">If you have the <Link to="https://docs.porter.run/standard/cli/command-reference/porter-update" target="_blank"><Text>Porter CLI</Text></Link> installed, you can update your application image tag by running the following command: </Text>
+                <Spacer y={0.5} />
+                <IdContainer>
+                    <Code>{`$ porter app update-tag ${latestProto.name} --tag latest`}</Code>
+                    <CopyContainer>
+                        <CopyToClipboard text={`porter app update-tag ${latestProto.name} --tag latest`}>
+                            <CopyIcon src={copy} alt="copy" />
+                        </CopyToClipboard>
+                    </CopyContainer>
+                </IdContainer>
+            </>
+        ))
+        .otherwise(() => null);
 };
 
 export default ImageSettingsTab;

+ 8 - 1
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -582,7 +582,14 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                               )} */}
                           </>
                         ) : (
-                          <ImageSettings projectId={currentProject.id} source={source} />
+                          <ImageSettings
+                            projectId={currentProject.id}
+                            imageUri={image?.repository ?? ""}
+                            setImageUri={(uri: string) => setValue("source.image", { ...image, repository: uri })}
+                            imageTag={image?.tag ?? ""}
+                            setImageTag={(tag: string) => setValue("source.image", { ...image, tag })}
+                            resetImageInfo={() => setValue("source.image", { ...image, repository: "", tag: "" })}
+                          />
                         )
                       ) : null}
                     </AnimateHeight>

+ 27 - 7
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -49,6 +49,7 @@ import EventFocusView from "./activity-feed/events/focus-views/EventFocusView";
 import HelmValuesTab from "./HelmValuesTab";
 import SettingsTab from "./SettingsTab";
 import PorterAppRevisionSection from "./PorterAppRevisionSection";
+import ImageSettingsTab from "./ImageSettingsTab";
 
 type Props = RouteComponentProps & {};
 
@@ -69,6 +70,7 @@ const validTabs = [
   "debug",
   "environment",
   "build-settings",
+  "image-settings",
   "settings",
   "helm-values",
   "job-history",
@@ -230,8 +232,17 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       setSyncedEnvGroups(filteredEnvGroups || []);
       setPorterJson(porterJson);
       setAppData(newAppData);
+      const globalImage = resChartData.data.config?.global?.image
+      const hasBuiltImage = globalImage != null &&
+        globalImage.repository != null &&
+        globalImage.tag != null &&
+        !(globalImage.repository === ImageInfo.BASE_IMAGE.repository &&
+          globalImage.tag === ImageInfo.BASE_IMAGE.tag)
       // annoying that we have to parse buildpacks like this but alas
       const parsedPorterApp = { ...resPorterApp?.data, buildpacks: newAppData.app.buildpacks?.split(",") ?? [] };
+      if (parsedPorterApp.image_repo_uri && hasBuiltImage) {
+        parsedPorterApp.image_info = { repository: globalImage.repository, tag: globalImage.tag };
+      }
       setPorterApp(parsedPorterApp);
       setTempPorterApp(parsedPorterApp);
       setBuildView(!_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks")
@@ -249,12 +260,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       );
       setPorterYaml(finalPorterYaml);
       // Only check GHA status if no built image is set
-      const globalImage = resChartData.data.config?.global?.image
-      const hasBuiltImage = globalImage != null &&
-        globalImage.repository != null &&
-        globalImage.tag != null &&
-        globalImage.repository !== ImageInfo.BASE_IMAGE.repository &&
-        globalImage.tag !== ImageInfo.BASE_IMAGE.tag
       if (hasBuiltImage || !resPorterApp.data.repo_name) {
         setWorkflowCheckPassed(true);
         setHasBuiltImage(true);
@@ -402,7 +407,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         );
         const yamlString = yaml.dump(finalPorterYaml);
         const base64Encoded = btoa(yamlString);
-        const updatedPorterApp = {
+        let updatedPorterApp = {
           porter_yaml: base64Encoded,
           override_release: true,
           ...PorterApp.empty(),
@@ -423,6 +428,9 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           updatedPorterApp.buildpacks = tempPorterApp.buildpacks.join(",");
           updatedPorterApp.dockerfile = "null";
         }
+        if (tempPorterApp.image_info?.repository && tempPorterApp.image_info?.tag) {
+          updatedPorterApp = { ...updatedPorterApp, image_info: tempPorterApp.image_info, image_repo_uri: tempPorterApp.image_info.repository }
+        }
 
         await api.createPorterApp(
           "<token>",
@@ -678,6 +686,14 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             setBuildView={setBuildView}
           />
         );
+      case "image-settings":
+        return (
+          <ImageSettingsTab
+            porterApp={tempPorterApp}
+            setTempPorterApp={(attrs: Partial<PorterApp>) => setTempPorterApp(PorterApp.setAttributes(tempPorterApp, attrs))}
+            updatePorterApp={updatePorterApp}
+          />
+        )
       case "settings":
         return <SettingsTab
           appName={appData.app.name}
@@ -935,6 +951,10 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                     label: "Build settings",
                     value: "build-settings",
                   },
+                  hasBuiltImage && !appData.app.git_repo_id && {
+                    label: "Image settings",
+                    value: "image-settings",
+                  },
                   { label: "Settings", value: "settings" },
                   (user.email.endsWith("porter.run") || currentProject.helm_values_enabled) && { label: "Helm values", value: "helm-values" },
                 ].filter((x) => x)}

+ 119 - 0
dashboard/src/main/home/app-dashboard/expanded-app/ImageSettingsTab.tsx

@@ -0,0 +1,119 @@
+import React, { useContext, useState } from "react";
+import Spacer from "components/porter/Spacer";
+import Button from "components/porter/Button";
+import Error from "components/porter/Error";
+import styled from "styled-components";
+import copy from "assets/copy-left.svg"
+import CopyToClipboard from "components/CopyToClipboard";
+import Link from "components/porter/Link";
+import Text from "components/porter/Text";
+import ImageSettings from "../image-settings/ImageSettings";
+import { Context } from "shared/Context";
+import { CreateUpdatePorterAppOptions } from "shared/types";
+import { PorterApp } from "../types/porterApp";
+
+type Props = {
+    porterApp: PorterApp;
+    updatePorterApp: (options: Partial<CreateUpdatePorterAppOptions>) => Promise<void>;
+    setTempPorterApp: (app: PorterApp) => void;
+}
+const ImageSettingsTab: React.FC<Props> = ({
+    porterApp,
+    updatePorterApp,
+    setTempPorterApp,
+}) => {
+    const { currentProject } = useContext(Context);
+
+    const [buttonStatus, setButtonStatus] = useState<
+        "loading" | "success" | string
+    >("");
+
+    const saveConfig = async () => {
+        try {
+            await updatePorterApp({});
+        } catch (err) {
+            console.log(err);
+        }
+    };
+
+    const handleSave = async () => {
+        setButtonStatus("loading");
+
+        try {
+            await saveConfig();
+            setButtonStatus("success");
+        } catch (error) {
+            setButtonStatus("Something went wrong");
+            console.log(error);
+        }
+    };
+
+    return (
+        <>
+            <ImageSettings
+                projectId={currentProject?.id ?? 0}
+                imageUri={porterApp.image_info?.repository ?? ""}
+                setImageUri={(uri: string) => setTempPorterApp({ ...porterApp, image_info: { ...porterApp.image_info, repository: uri } })}
+                imageTag={porterApp.image_info?.tag ?? ""}
+                setImageTag={(tag: string) => setTempPorterApp({ ...porterApp, image_info: { ...porterApp.image_info, tag: tag } })}
+                resetImageInfo={() => setTempPorterApp({ ...porterApp, image_info: { ...porterApp.image_info, repository: "", tag: "" } })}
+            />
+            <Spacer y={1} />
+            <Button
+                type="button"
+                status={buttonStatus}
+                disabled={!porterApp.image_info?.repository || !porterApp.image_info?.tag}
+                onClick={handleSave}
+            >
+                Save image settings
+            </Button>
+            <Spacer y={1} />
+            <Text size={16}>Update command</Text>
+            <Spacer y={0.5} />
+            <Text color="helper">If you have the <Link to="https://docs.porter.run/standard/cli/command-reference/porter-update" target="_blank"><Text>Porter CLI</Text></Link> installed, you can update your application image tag by running the following command: </Text>
+            <Spacer y={0.5} />
+            <IdContainer>
+                <Code>{`$ porter app update-tag ${porterApp.name} --tag latest`}</Code>
+                <CopyContainer>
+                    <CopyToClipboard text={`porter app update-tag ${porterApp.name} --tag latest`}>
+                        <CopyIcon src={copy} alt="copy" />
+                    </CopyToClipboard>
+                </CopyContainer>
+            </IdContainer>
+        </>
+    );
+};
+
+export default ImageSettingsTab;
+
+const Code = styled.span`
+  font-family: monospace;
+`;
+
+const IdContainer = styled.div`
+    background: #000000;  
+    border-radius: 5px;
+    padding: 10px;
+    display: flex;
+    width: 100%;
+    border-radius: 5px;
+    border: 1px solid ${({ theme }) => theme.border};
+    align-items: center;
+`;
+
+const CopyContainer = styled.div`
+  display: flex;
+  align-items: center;
+  margin-left: auto;
+`;
+
+const CopyIcon = styled.img`
+  cursor: pointer;
+  margin-left: 5px;
+  margin-right: 5px;
+  width: 15px;
+  height: 15px;
+  :hover {
+    opacity: 0.8;
+  }
+`;

+ 47 - 65
dashboard/src/main/home/app-dashboard/image-settings/ImageSettings.tsx

@@ -1,27 +1,32 @@
 import React, { useEffect, useState } from "react";
 import { useQuery } from "@tanstack/react-query";
 import api from "shared/api";
-import { Controller, useFormContext } from "react-hook-form";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import styled from "styled-components";
 import Input from "components/porter/Input";
 import { z } from "zod";
-import { PorterAppFormData, SourceOptions } from "lib/porter-apps";
 import ImageList from "./ImageList";
 import TagList from "./TagList";
 import { ImageType } from "./types";
 
 type Props = {
     projectId: number;
-    source: SourceOptions & { type: "docker-registry" };
+    imageUri: string;
+    imageTag: string;
+    setImageUri: (uri: string) => void;
+    setImageTag: (tag: string) => void;
+    resetImageInfo: () => void;
 };
 
 const ImageSettings: React.FC<Props> = ({
     projectId,
-    source,
+    imageUri,
+    imageTag,
+    setImageUri,
+    setImageTag,
+    resetImageInfo,
 }) => {
-    const { control, setValue } = useFormContext<PorterAppFormData>();
     const [images, setImages] = useState<ImageType[]>([]);
     const [selectedImage, setSelectedImage] = useState<ImageType | undefined>(undefined);
     const { data: registries, isLoading: isLoadingRegistries } = useQuery(
@@ -36,7 +41,7 @@ const ImageSettings: React.FC<Props> = ({
     )
 
     const { data: imageResp, isLoading: isLoadingImages } = useQuery(
-        ["getImages", projectId, source],
+        ["getImages", projectId, imageTag, imageUri],
         async () => {
             if (registries == null) {
                 return [];
@@ -62,8 +67,8 @@ const ImageSettings: React.FC<Props> = ({
     useEffect(() => {
         if (imageResp) {
             setImages(imageResp);
-            if (source.image && source.image.repository) {
-                setSelectedImage(imageResp.find((image) => image.uri === source.image.repository));
+            if (imageUri) {
+                setSelectedImage(imageResp.find((image) => image.uri === imageUri));
             }
         }
     }, [imageResp]);
@@ -75,49 +80,36 @@ const ImageSettings: React.FC<Props> = ({
             <Text color="helper">Specify your image URL.</Text>
             <Spacer y={0.5} />
 
-            {(!source.image || !source.image.repository) && (
-                <Controller
-                    name="source.image"
-                    control={control}
-                    render={({ field: { onChange } }) => (
-                        <>
-                            <ExpandedWrapper>
-                                <ImageList
-                                    setSelectedImage={(image: ImageType) => {
-                                        setSelectedImage(image);
-                                        onChange({
-                                            repository: image.uri,
-                                        });
-                                    }}
-                                    images={images}
-                                    loading={isLoadingImages || isLoadingRegistries}
-                                />
-                            </ExpandedWrapper>
-                            <DarkMatter antiHeight="-4px" />
-                            <Spacer y={0.3} />
-                        </>
-                    )}
-                />
+            {!imageUri && (
+                <>
+                    <ExpandedWrapper>
+                        <ImageList
+                            setSelectedImage={(image: ImageType) => {
+                                setSelectedImage(image);
+                                setImageUri(image.uri);
+                            }}
+                            images={images}
+                            loading={isLoadingImages || isLoadingRegistries}
+                        />
+                    </ExpandedWrapper>
+                    <DarkMatter antiHeight="-4px" />
+                    <Spacer y={0.3} />
+                </>
             )}
 
-            {source.image && source.image.repository && (
+            {imageUri && (
                 <>
                     <Input
                         disabled={true}
                         label="Image URL:"
                         width="100%"
-                        value={selectedImage?.uri ?? source.image.repository}
+                        value={selectedImage?.uri ?? imageUri}
                         setValue={() => { }}
                         placeholder=""
                     />
                     <BackButton
                         width="170px"
-                        onClick={() => {
-                            setValue("source.image", {
-                                repository: "",
-                                tag: "",
-                            });
-                        }}
+                        onClick={resetImageInfo}
                     >
                         <i className="material-icons">keyboard_backspace</i>
                         Select image URL
@@ -125,46 +117,36 @@ const ImageSettings: React.FC<Props> = ({
                     <Spacer y={1} />
                     <Text color="helper">Specify your image tag.</Text>
                     <Spacer y={0.5} />
-                    {!source.image.tag && (
-                        <Controller
-                            name="source.image"
-                            control={control}
-                            render={({ field: { onChange } }) => (
-                                <ExpandedWrapper>
-                                    <TagList
-                                        selectedImage={selectedImage}
-                                        projectId={projectId}
-                                        setSelectedTag={
-                                            (tag: string) => {
-                                                onChange({
-                                                    repository: source.image.repository,
-                                                    tag,
-                                                });
-                                            }
+                    {!imageTag && (
+                        <>
+                            <ExpandedWrapper>
+                                <TagList
+                                    selectedImage={selectedImage}
+                                    projectId={projectId}
+                                    setSelectedTag={
+                                        (tag: string) => {
+                                            setImageTag(tag);
                                         }
-                                    />
-                                </ExpandedWrapper>
-                            )}
-                        />
+                                    }
+                                />
+                            </ExpandedWrapper>
+                        </>
                     )}
-                    {source.image.tag && (
+                    {imageTag && (
                         <>
                             <Input
                                 disabled={true}
                                 label="Image tag:"
                                 type="text"
                                 width="100%"
-                                value={source.image.tag}
+                                value={imageTag}
                                 setValue={() => { }}
                                 placeholder=""
                             />
                             <BackButton
                                 width="170px"
                                 onClick={() => {
-                                    setValue("source.image", {
-                                        repository: source.image.repository,
-                                        tag: "",
-                                    });
+                                    setImageTag("")
                                 }}
                             >
                                 <i className="material-icons">keyboard_backspace</i>

+ 7 - 2
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -77,7 +77,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [porterApp, setPorterApp] = useState<PorterApp>(PorterApp.empty());
   const [hovered, setHovered] = useState(false);
 
-  const [imageTag, setImageTag] = useState("latest");
+  const [imageTag, setImageTag] = useState("");
   const { currentCluster, currentProject } = useContext(Context);
   const [deploying, setDeploying] = useState<boolean>(false);
   const [deploymentError, setDeploymentError] = useState<string | undefined>(undefined);
@@ -328,8 +328,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       const porterAppRequest = {
         porter_yaml: base64Encoded,
         override_release: true,
-        image_info: imageInfo,
         ...PorterApp.empty(),
+        image_info: imageInfo,
         buildpacks: "",
         // for some reason I couldn't get the path to update the porterApp object correctly here so I just grouped it with the porter json :/
         porter_yaml_path: porterJsonWithPath?.porterYamlPath,
@@ -470,6 +470,11 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                   setImageTag={setImageTag}
                   buildView={buildView}
                   setBuildView={setBuildView}
+                  projectId={currentProject?.id ?? 0}
+                  resetImageInfo={() => {
+                    setPorterApp(PorterApp.setAttribute(porterApp, "image_repo_uri", ""));
+                    setImageTag("");
+                  }}
                 />
               </>,
               <>

+ 14 - 30
dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx

@@ -4,11 +4,9 @@ import Spacer from "components/porter/Spacer";
 import styled from "styled-components";
 import { SourceType } from "./SourceSelector";
 import { RouteComponentProps, withRouter } from "react-router";
-import { pushFiltered } from "shared/routing";
-import ImageSelector from "components/image-selector/ImageSelector";
 import SharedBuildSettings from "../build-settings/SharedBuildSettings";
-import Link from "components/porter/Link";
 import { BuildMethod, PorterApp } from "../types/porterApp";
+import ImageSettings from "../image-settings/ImageSettings";
 
 type Props = RouteComponentProps & {
   source: SourceType | undefined;
@@ -21,6 +19,8 @@ type Props = RouteComponentProps & {
   setPorterApp: (x: PorterApp) => void;
   buildView: BuildMethod;
   setBuildView: (buildView: BuildMethod) => void;
+  projectId: number;
+  resetImageInfo: () => void;
 };
 
 const SourceSettings: React.FC<Props> = ({
@@ -34,8 +34,8 @@ const SourceSettings: React.FC<Props> = ({
   setPorterApp,
   buildView,
   setBuildView,
-  location,
-  history,
+  projectId,
+  resetImageInfo,
 }) => {
   return (
     <SourceSettingsContainer>
@@ -51,31 +51,15 @@ const SourceSettings: React.FC<Props> = ({
             buildView={buildView}
             setBuildView={setBuildView}
           />
-        ) : (
-          <StyledSourceBox>
-            <Subtitle>
-              Specify the container image you would like to connect to this
-              template.
-              <Spacer inline width="5px" />
-              <Link
-                hasunderline
-                onClick={() =>
-                  pushFiltered({ location, history }, "/integrations/registry", ["project_id"])
-                }
-              >
-                Manage Docker registries
-              </Link>
-            </Subtitle>
-            <DarkMatter antiHeight="-4px" />
-            <ImageSelector
-              selectedTag={imageTag}
-              selectedImageUrl={imageUrl}
-              setSelectedImageUrl={setImageUrl}
-              setSelectedTag={setImageTag}
-              forceExpanded={true}
-            />
-            <br />
-          </StyledSourceBox>)
+        ) :
+          <ImageSettings
+            projectId={projectId}
+            imageTag={imageTag}
+            setImageTag={setImageTag}
+            imageUri={imageUrl}
+            setImageUri={setImageUrl}
+            resetImageInfo={resetImageInfo}
+          />
         }
       </AnimateHeight>
     </SourceSettingsContainer>

+ 8 - 0
dashboard/src/main/home/app-dashboard/types/porterApp.ts

@@ -9,6 +9,10 @@ export interface PorterApp {
     dockerfile: string;
     image_repo_uri: string;
     porter_yaml_path: string;
+    image_info?: {
+        repository: string;
+        tag: string;
+    }
 }
 
 export const PorterApp = {
@@ -23,6 +27,10 @@ export const PorterApp = {
         dockerfile: "",
         image_repo_uri: "",
         porter_yaml_path: "",
+        image_info: {
+            repository: "",
+            tag: "",
+        }
     }),
 
     setAttribute: <K extends keyof PorterApp>(