diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/ImageSettingsTab.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/ImageSettingsTab.tsx index 85bbccdc9f..f5a91743b8 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/ImageSettingsTab.tsx +++ b/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(); const { projectId, latestRevision, latestProto } = useLatestRevision(); @@ -34,46 +35,47 @@ const ImageSettingsTab: React.FC = () => { return ""; }, [isSubmitting, errors]); - return ( - <> - {match(source) - .with({ type: "docker-registry" }, (source) => ( - <> - - - - - Update command - - If you have the Porter CLI installed, you can update your application image tag by running the following command: - - - {`$ porter app update-tag ${latestProto.name} --tag latest`} - - - - - - - - )) - .otherwise(() => null) - } - - ); + return match(source) + .with({ type: "docker-registry" }, (source) => ( + <> + 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: "" })} + /> + + + + Update command + + If you have the Porter CLI installed, you can update your application image tag by running the following command: + + + {`$ porter app update-tag ${latestProto.name} --tag latest`} + + + + + + + + )) + .otherwise(() => null); }; export default ImageSettingsTab; diff --git a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx index 70120fb2e3..c202661219 100644 --- a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx +++ b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx @@ -582,7 +582,14 @@ const CreateApp: React.FC = ({ history }) => { )} */} ) : ( - + 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} diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx index 4f8090ad6c..8770ec4fb4 100644 --- a/dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx +++ b/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 }) => { 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 }) => { ); 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 }) => { ); 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 }) => { 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( "", @@ -678,6 +686,14 @@ const ExpandedApp: React.FC = ({ ...props }) => { setBuildView={setBuildView} /> ); + case "image-settings": + return ( + ) => setTempPorterApp(PorterApp.setAttributes(tempPorterApp, attrs))} + updatePorterApp={updatePorterApp} + /> + ) case "settings": return = ({ ...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)} diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/ImageSettingsTab.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/ImageSettingsTab.tsx new file mode 100644 index 0000000000..c34efc9987 --- /dev/null +++ b/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) => Promise; + setTempPorterApp: (app: PorterApp) => void; +} +const ImageSettingsTab: React.FC = ({ + 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 ( + <> + 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: "" } })} + /> + + + + Update command + + If you have the Porter CLI installed, you can update your application image tag by running the following command: + + + {`$ porter app update-tag ${porterApp.name} --tag latest`} + + + + + + + + ); +}; + +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; + } +`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/image-settings/ImageSettings.tsx b/dashboard/src/main/home/app-dashboard/image-settings/ImageSettings.tsx index b3af881b94..37013a7fac 100644 --- a/dashboard/src/main/home/app-dashboard/image-settings/ImageSettings.tsx +++ b/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 = ({ projectId, - source, + imageUri, + imageTag, + setImageUri, + setImageTag, + resetImageInfo, }) => { - const { control, setValue } = useFormContext(); const [images, setImages] = useState([]); const [selectedImage, setSelectedImage] = useState(undefined); const { data: registries, isLoading: isLoadingRegistries } = useQuery( @@ -36,7 +41,7 @@ const ImageSettings: React.FC = ({ ) 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 = ({ 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 = ({ Specify your image URL. - {(!source.image || !source.image.repository) && ( - ( - <> - - { - setSelectedImage(image); - onChange({ - repository: image.uri, - }); - }} - images={images} - loading={isLoadingImages || isLoadingRegistries} - /> - - - - - )} - /> + {!imageUri && ( + <> + + { + setSelectedImage(image); + setImageUri(image.uri); + }} + images={images} + loading={isLoadingImages || isLoadingRegistries} + /> + + + + )} - {source.image && source.image.repository && ( + {imageUri && ( <> { }} placeholder="" /> { - setValue("source.image", { - repository: "", - tag: "", - }); - }} + onClick={resetImageInfo} > keyboard_backspace Select image URL @@ -125,46 +117,36 @@ const ImageSettings: React.FC = ({ Specify your image tag. - {!source.image.tag && ( - ( - - { - onChange({ - repository: source.image.repository, - tag, - }); - } + {!imageTag && ( + <> + + { + setImageTag(tag); } - /> - - )} - /> + } + /> + + )} - {source.image.tag && ( + {imageTag && ( <> { }} placeholder="" /> { - setValue("source.image", { - repository: source.image.repository, - tag: "", - }); + setImageTag("") }} > keyboard_backspace diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx index 758a4bce9f..7d2f819e1a 100644 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx +++ b/dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx @@ -77,7 +77,7 @@ const NewAppFlow: React.FC = ({ ...props }) => { const [porterApp, setPorterApp] = useState(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(false); const [deploymentError, setDeploymentError] = useState(undefined); @@ -470,6 +470,11 @@ const NewAppFlow: React.FC = ({ ...props }) => { setImageTag={setImageTag} buildView={buildView} setBuildView={setBuildView} + projectId={currentProject?.id ?? 0} + resetImageInfo={() => { + setPorterApp(PorterApp.setAttribute(porterApp, "image_repo_uri", "")); + setImageTag(""); + }} /> , <> diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx index 93ca434ccf..061f753b70 100644 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx +++ b/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 = ({ @@ -34,8 +34,8 @@ const SourceSettings: React.FC = ({ setPorterApp, buildView, setBuildView, - location, - history, + projectId, + resetImageInfo, }) => { return ( @@ -51,31 +51,15 @@ const SourceSettings: React.FC = ({ buildView={buildView} setBuildView={setBuildView} /> - ) : ( - - - Specify the container image you would like to connect to this - template. - - - pushFiltered({ location, history }, "/integrations/registry", ["project_id"]) - } - > - Manage Docker registries - - - - -
-
) + ) : + }
diff --git a/dashboard/src/main/home/app-dashboard/types/porterApp.ts b/dashboard/src/main/home/app-dashboard/types/porterApp.ts index 9683385adb..ae3d8c9052 100644 --- a/dashboard/src/main/home/app-dashboard/types/porterApp.ts +++ b/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: (