Pārlūkot izejas kodu

add local source type to allow for app creation from cli (#4476)

ianedwards 2 gadi atpakaļ
vecāks
revīzija
b90f86ce54
19 mainītis faili ar 518 papildinājumiem un 466 dzēšanām
  1. 6 0
      dashboard/src/assets/git-scm.svg
  2. 1 1
      dashboard/src/lib/hooks/useAppValidation.ts
  3. 1 4
      dashboard/src/lib/hooks/usePorterYaml.ts
  4. 6 0
      dashboard/src/lib/porter-apps/index.ts
  5. 1 23
      dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx
  6. 83 74
      dashboard/src/main/home/app-dashboard/app-view/AppHeader.tsx
  7. 10 1
      dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx
  8. 24 14
      dashboard/src/main/home/app-dashboard/app-view/tabs/BuildSettingsTab.tsx
  9. 3 3
      dashboard/src/main/home/app-dashboard/apps/AppGrid.tsx
  10. 15 7
      dashboard/src/main/home/app-dashboard/apps/AppMeta.tsx
  11. 201 0
      dashboard/src/main/home/app-dashboard/create-app/BuildSettings.tsx
  12. 10 6
      dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx
  13. 9 182
      dashboard/src/main/home/app-dashboard/create-app/RepoSettings.tsx
  14. 18 13
      dashboard/src/main/home/app-dashboard/new-app-flow/SourceSelector.tsx
  15. 6 7
      dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvSettings.tsx
  16. 6 5
      dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvVariables.tsx
  17. 44 36
      dashboard/src/main/home/app-dashboard/validate-apply/build-settings/buildpacks/BuildpackSettings.tsx
  18. 72 64
      dashboard/src/main/home/app-dashboard/validate-apply/build-settings/docker/DockerfileSettings.tsx
  19. 2 26
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 6 - 0
dashboard/src/assets/git-scm.svg


+ 1 - 1
dashboard/src/lib/hooks/useAppValidation.ts

@@ -147,7 +147,7 @@ export const useAppValidation = ({
             source: src,
           });
         })
-        .with({ type: "docker-registry" }, () => {
+        .with({ type: "docker-registry" }, { type: "local" }, () => {
           return "";
         })
         .exhaustive();

+ 1 - 4
dashboard/src/lib/hooks/usePorterYaml.ts

@@ -89,10 +89,7 @@ export const usePorterYaml = ({
       return await z.string().parseAsync(res.data);
     },
     {
-      enabled:
-        source?.type === "github" &&
-        Boolean(source.git_repo_name) &&
-        Boolean(source.git_branch),
+      enabled: source?.type === "github",
       onError: () => {
         setPorterYamlFound(false);
       },

+ 6 - 0
dashboard/src/lib/porter-apps/index.ts

@@ -34,6 +34,11 @@ export const sourceValidator = z.discriminatedUnion("type", [
     git_repo_name: z.string().min(1),
     porter_yaml_path: z.string().default("./porter.yaml"),
   }),
+  z.object({
+    type: z.literal("local"),
+    git_branch: z.undefined(),
+    git_repo_name: z.undefined(),
+  }),
   z.object({
     type: z.literal("docker-registry"),
     // add branch and repo as undefined to allow for easy checks on changes to the source type
@@ -320,6 +325,7 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
   const proto = match(source)
     .with(
       { type: "github" },
+      { type: "local" },
       () =>
         new PorterApp({
           name: app.name.value,

+ 1 - 23
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -33,7 +33,6 @@ import {
   clientAppFromProto,
   porterAppFormValidator,
   type PorterAppFormData,
-  type SourceOptions,
 } from "lib/porter-apps";
 
 import api from "shared/api";
@@ -111,6 +110,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
     setPreviewRevision,
     latestClientNotifications,
     tabUrlGenerator,
+    latestSource,
   } = useLatestRevision();
   const { validateApp, setServiceDeletions } = useAppValidation({
     deploymentTargetID: deploymentTarget.id,
@@ -124,28 +124,6 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
     return DEFAULT_TAB;
   }, [tabParam]);
 
-  const latestSource: SourceOptions = useMemo(() => {
-    // because we store the image info in the app proto, we can refer to that for repository/tag instead of the app record
-    if (porterAppRecord.image_repo_uri && latestProto.image) {
-      return {
-        type: "docker-registry",
-        image: {
-          repository: latestProto.image.repository,
-          tag: latestProto.image.tag,
-        },
-      };
-    }
-
-    // the app proto does not contain the fields below, so we must pull them from the app record
-    return {
-      type: "github",
-      git_repo_id: porterAppRecord.git_repo_id ?? 0,
-      git_repo_name: porterAppRecord.repo_name ?? "",
-      git_branch: porterAppRecord.git_branch ?? "",
-      porter_yaml_path: porterAppRecord.porter_yaml_path ?? "./porter.yaml",
-    };
-  }, [porterAppRecord, latestProto]);
-
   const porterAppFormMethods = useForm<PorterAppFormData>({
     reValidateMode: "onSubmit",
     resolver: zodResolver(porterAppFormValidator),

+ 83 - 74
dashboard/src/main/home/app-dashboard/app-view/AppHeader.tsx

@@ -1,6 +1,7 @@
 import React, { useMemo } from "react";
 import { type PorterApp } from "@porter-dev/api-contracts";
 import styled from "styled-components";
+import { match } from "ts-pattern";
 
 import Container from "components/porter/Container";
 import Icon from "components/porter/Icon";
@@ -37,8 +38,13 @@ const icons = [
 export const HELLO_PORTER_PLACEHOLDER_TAG = "porter-initial-image";
 
 const AppHeader: React.FC = () => {
-  const { latestProto, porterApp, latestRevision, deploymentTarget } =
-    useLatestRevision();
+  const {
+    latestProto,
+    porterApp,
+    latestRevision,
+    deploymentTarget,
+    latestSource,
+  } = useLatestRevision();
 
   const gitCommitUrl = useMemo(() => {
     if (!porterApp.repo_name) {
@@ -52,9 +58,6 @@ const AppHeader: React.FC = () => {
   }, [JSON.stringify(latestProto), porterApp]);
 
   const displayCommitSha = useMemo(() => {
-    if (!porterApp.repo_name) {
-      return "";
-    }
     if (
       !latestProto.build?.commitSha ||
       latestProto.build.commitSha.length < 7
@@ -65,22 +68,6 @@ const AppHeader: React.FC = () => {
     return latestProto.build.commitSha.slice(0, 7);
   }, [JSON.stringify(latestProto), porterApp]);
 
-  const gitData = useMemo(() => {
-    if (
-      !porterApp.git_branch ||
-      !porterApp.repo_name ||
-      !porterApp.git_repo_id
-    ) {
-      return null;
-    }
-
-    return {
-      id: porterApp.git_repo_id,
-      branch: porterApp.git_branch,
-      repo: porterApp.repo_name,
-    };
-  }, [porterApp]);
-
   const getIconSvg = (build: PorterApp["build"]): JSX.Element => {
     if (!build) {
       return box;
@@ -161,48 +148,61 @@ const AppHeader: React.FC = () => {
         <Icon src={getIconSvg(latestProto.build)} height={"24px"} />
         <Spacer inline x={1} />
         <Text size={21}>{latestProto.name}</Text>
-        {gitData && (
-          <>
-            <Spacer inline x={1} />
-            <Container row>
-              <A target="_blank" href={`https://github.com/${gitData.repo}`}>
-                <SmallIcon src={github} />
-                <Text size={13}>{gitData.repo}</Text>
-              </A>
-            </Container>
-            <Spacer inline x={1} />
-            <TagWrapper preview={deploymentTarget.is_preview}>
-              {deploymentTarget.is_preview ? "Preview" : "Branch"}
-              <BranchTag preview={deploymentTarget.is_preview}>
-                <PullRequestIcon
-                  styles={{
-                    height: "14px",
-                    opacity: "0.65",
-                    marginRight: "5px",
-                    fill: deploymentTarget.is_preview ? "" : "#fff",
-                  }}
+        <Spacer inline x={1} />
+
+        <Container
+          row
+          style={{
+            display: "flex",
+            alignItems: "center",
+            height: "24px",
+          }}
+        >
+          {match(latestSource)
+            .with({ type: "github" }, (s) => (
+              <>
+                <Spacer inline x={1} />
+                <Container row>
+                  <A
+                    target="_blank"
+                    href={`https://github.com/${s.git_repo_name}`}
+                  >
+                    <SmallIcon src={github} />
+                    <Text size={13}>{s.git_repo_name}</Text>
+                  </A>
+                </Container>
+                <Spacer inline x={1} />
+                <TagWrapper preview={deploymentTarget.is_preview}>
+                  {deploymentTarget.is_preview ? "Preview" : "Branch"}
+                  <BranchTag preview={deploymentTarget.is_preview}>
+                    <PullRequestIcon
+                      styles={{
+                        height: "14px",
+                        opacity: "0.65",
+                        marginRight: "5px",
+                        fill: deploymentTarget.is_preview ? "" : "#fff",
+                      }}
+                    />
+                    {deploymentTarget.is_preview
+                      ? deploymentTarget.namespace
+                      : s.git_branch}
+                  </BranchTag>
+                </TagWrapper>
+              </>
+            ))
+            .with({ type: "docker-registry" }, (s) => (
+              <>
+                <SmallIcon
+                  height="19px"
+                  src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png"
                 />
-                {deploymentTarget.is_preview
-                  ? deploymentTarget.namespace
-                  : gitData.branch}
-              </BranchTag>
-            </TagWrapper>
-          </>
-        )}
-        {!gitData && latestProto.image && (
-          <>
-            <Spacer inline x={1} />
-            <Container row>
-              <SmallIcon
-                height="19px"
-                src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png"
-              />
-              <Text size={13} color="helper">
-                {`${latestProto.image.repository}`}
-              </Text>
-            </Container>
-          </>
-        )}
+                <Text size={13} color="helper">
+                  {s.image.repository}
+                </Text>
+              </>
+            ))
+            .otherwise(() => null)}
+        </Container>
       </Container>
       <Spacer y={0.5} />
       {displayDomain && (
@@ -229,20 +229,29 @@ const AppHeader: React.FC = () => {
         </div>
         <Spacer y={0.5} />
         <NoShrink>
-          {gitCommitUrl && displayCommitSha ? (
-            <ImageTagContainer>
-              <Link
-                to={gitCommitUrl}
-                target="_blank"
-                showTargetBlankIcon={false}
-              >
+          {match(latestSource)
+            .with({ type: "github" }, () => (
+              <ImageTagContainer>
+                <Link
+                  to={gitCommitUrl}
+                  target="_blank"
+                  showTargetBlankIcon={false}
+                >
+                  <CommitIcon src={pull_request_icon} />
+                  <Code>{displayCommitSha}</Code>
+                </Link>
+              </ImageTagContainer>
+            ))
+            .with({ type: "local" }, () => (
+              <ImageTagContainer>
                 <CommitIcon src={pull_request_icon} />
                 <Code>{displayCommitSha}</Code>
-              </Link>
-            </ImageTagContainer>
-          ) : latestProto.image?.tag ? (
-            renderTagBadge(latestProto.image.tag)
-          ) : null}
+              </ImageTagContainer>
+            ))
+            .with({ type: "docker-registry" }, (s) =>
+              renderTagBadge(s.image.tag)
+            )
+            .exhaustive()}
         </NoShrink>
         <Spacer y={0.5} />
       </LatestDeployContainer>

+ 10 - 1
dashboard/src/main/home/app-dashboard/app-view/LatestRevisionContext.tsx

@@ -63,6 +63,7 @@ type LatestRevisionContextType = {
   setPreviewRevision: Dispatch<SetStateAction<AppRevision | null>>;
   latestClientServices: ClientService[];
   loading: boolean;
+  latestSource: SourceOptions;
   tabUrlGenerator: ({
     tab,
     queryParams,
@@ -276,6 +277,12 @@ export const LatestRevisionProvider: React.FC<LatestRevisionProviderProps> = ({
       };
     }
 
+    if (!porterApp.git_repo_id) {
+      return {
+        type: "local",
+      };
+    }
+
     return {
       type: "github",
       git_repo_id: porterApp.git_repo_id ?? 0,
@@ -332,7 +339,8 @@ export const LatestRevisionProvider: React.FC<LatestRevisionProviderProps> = ({
     status === "loading" ||
     porterAppStatus === "loading" ||
     !appParamsExist ||
-    porterYamlLoading;
+    porterYamlLoading ||
+    !latestSource;
 
   if (loading) {
     if (!showLoader) {
@@ -379,6 +387,7 @@ export const LatestRevisionProvider: React.FC<LatestRevisionProviderProps> = ({
         previewRevision,
         setPreviewRevision,
         latestClientServices,
+        latestSource,
         appName,
         loading,
         tabUrlGenerator: ({ tab, queryParams }) =>

+ 24 - 14
dashboard/src/main/home/app-dashboard/app-view/tabs/BuildSettingsTab.tsx

@@ -3,8 +3,10 @@ import { useFormContext } from "react-hook-form";
 import { match } from "ts-pattern";
 
 import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
 import { type PorterAppFormData } from "lib/porter-apps";
 
+import { BuildSettings } from "../../create-app/BuildSettings";
 import RepoSettings from "../../create-app/RepoSettings";
 import { type ButtonStatus } from "../AppDataContainer";
 import AppSaveButton from "../AppSaveButton";
@@ -26,24 +28,32 @@ const BuildSettingsTab: React.FC<Props> = ({ buttonStatus }) => {
 
   return (
     <>
+      <Text size={16}>Build settings</Text>
+      <Spacer y={0.5} />
       {match(source)
         .with({ type: "github" }, (source) => (
-          <>
-            <RepoSettings
-              build={build}
-              source={source}
-              projectId={projectId}
-              appExists
-            />
-            <Spacer y={1} />
-            <AppSaveButton
-              status={buttonStatus}
-              isDisabled={isSubmitting}
-              disabledTooltipMessage="Please wait for the build to complete before updating build settings"
-            />
-          </>
+          <RepoSettings
+            build={build}
+            source={source}
+            projectId={projectId}
+            appExists
+          />
+        ))
+        .with({ type: "local" }, (source) => (
+          <BuildSettings
+            projectId={projectId}
+            source={source}
+            build={build}
+            appExists
+          />
         ))
         .otherwise(() => null)}
+      <Spacer y={1} />
+      <AppSaveButton
+        status={buttonStatus}
+        isDisabled={isSubmitting}
+        disabledTooltipMessage="Please wait for the build to complete before updating build settings"
+      />
     </>
   );
 };

+ 3 - 3
dashboard/src/main/home/app-dashboard/apps/AppGrid.tsx

@@ -121,7 +121,7 @@ const AppGrid: React.FC<AppGridProps> = ({
               <Link to={appLink} key={i}>
                 <Block
                   locked={false}
-                  appId={user.isPorterUser ? source.id : ""}
+                  appId={user.isPorterUser ? String(source.id) : ""}
                 >
                   <Container row>
                     <AppIcon
@@ -243,11 +243,11 @@ export const Block = styled.div<{ locked?: boolean; appId?: string }>`
 
     ::after {
       content: ${(props) =>
-    props.locked || !props.appId ? "''" : `"AppID: ${props.appId}"`};
+        props.locked || !props.appId ? "''" : `"AppID: ${props.appId}"`};
       position: absolute;
       top: 2px;
       right: 2px;
-      background:  ${(props) => props.appId && `#ffffff44`};
+      background: ${(props) => props.appId && `#ffffff44`};
       opacity: 0.3;
       padding: 5px;
       border-radius: 4px;

+ 15 - 7
dashboard/src/main/home/app-dashboard/apps/AppMeta.tsx

@@ -6,6 +6,7 @@ import Icon from "components/porter/Icon";
 import Text from "components/porter/Text";
 
 import box from "assets/box.png";
+import git_scm from "assets/git-scm.svg";
 import github from "assets/github.png";
 import web from "assets/web.png";
 
@@ -31,7 +32,18 @@ type SourceProps = {
 export const AppSource: React.FC<SourceProps> = ({ source }) => {
   return (
     <>
-      {source.repo_name ? (
+      {source.image_repo_uri ? (
+        <Container row>
+          <SmallIcon
+            opacity="0.7"
+            height="18px"
+            src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png"
+          />
+          <Text truncate={true} size={13} color="#ffffff44">
+            {source.image_repo_uri}
+          </Text>
+        </Container>
+      ) : source.repo_name ? (
         <Container row>
           <SmallIcon opacity="0.6" src={github} />
           <Text truncate={true} size={13} color="#ffffff44">
@@ -40,13 +52,9 @@ export const AppSource: React.FC<SourceProps> = ({ source }) => {
         </Container>
       ) : (
         <Container row>
-          <SmallIcon
-            opacity="0.7"
-            height="18px"
-            src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png"
-          />
+          <SmallIcon src={git_scm} />
           <Text truncate={true} size={13} color="#ffffff44">
-            {source.image_repo_uri}
+            {source.name}
           </Text>
         </Container>
       )}

+ 201 - 0
dashboard/src/main/home/app-dashboard/create-app/BuildSettings.tsx

@@ -0,0 +1,201 @@
+import React, { useState } from "react";
+import AnimateHeight from "react-animate-height";
+import { Controller, useFormContext } from "react-hook-form";
+import styled from "styled-components";
+import { match } from "ts-pattern";
+
+import { ControlledInput } from "components/porter/ControlledInput";
+import LoadingBar from "components/porter/LoadingBar";
+import Select from "components/porter/Select";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { type PorterAppFormData, type SourceOptions } from "lib/porter-apps";
+import { type BuildOptions } from "lib/porter-apps/build";
+
+import BuildpackSettings, {
+  DEFAULT_BUILDERS,
+} from "../validate-apply/build-settings/buildpacks/BuildpackSettings";
+import DockerfileSettings from "../validate-apply/build-settings/docker/DockerfileSettings";
+import PorterYamlInput from "./PorterYamlInput";
+
+type BuildSettingsProps = {
+  projectId: number;
+  source: SourceOptions & { type: "github" | "local" };
+  build: BuildOptions;
+  appExists?: boolean;
+  loadingBranchContents?: boolean;
+};
+
+export const BuildSettings: React.FC<BuildSettingsProps> = ({
+  projectId,
+  source,
+  build,
+  appExists,
+  loadingBranchContents,
+}) => {
+  const { control, register, setValue } = useFormContext<PorterAppFormData>();
+  const [showSettings, setShowSettings] = useState<boolean>(false);
+
+  return (
+    <>
+      {!appExists && (
+        <>
+          <Text color="helper">Specify your application root path.</Text>
+          <Spacer y={0.5} />
+        </>
+      )}
+      <ControlledInput
+        placeholder="ex: ./"
+        width="100%"
+        type="text"
+        {...register("app.build.context")}
+        label={"Application root path:"}
+      />
+      <Spacer y={1} />
+      {!appExists && source.type === "github" && (
+        <>
+          <Text color="helper">
+            (Optional) Specify your porter.yaml path.{" "}
+            <a
+              href="https://docs.porter.run/deploy/configuration-as-code/overview"
+              target="_blank"
+              rel="noreferrer"
+            >
+              &nbsp;(?)
+            </a>
+          </Text>
+          <Spacer y={0.5} />
+          <PorterYamlInput
+            projectId={projectId}
+            repoId={source.git_repo_id}
+            repoOwner={source.git_repo_name.split("/")[0]}
+            repoName={source.git_repo_name.split("/")[1]}
+            branch={source.git_branch}
+          />
+          <Spacer y={1} />
+        </>
+      )}
+      {loadingBranchContents && !appExists ? (
+        <AdvancedBuildTitle>
+          <LoadingBar />
+        </AdvancedBuildTitle>
+      ) : (
+        <StyledAdvancedBuildSettings
+          showSettings={showSettings}
+          onClick={() => {
+            setShowSettings(!showSettings);
+          }}
+        >
+          {build.method === "docker" ? (
+            <AdvancedBuildTitle>
+              <i className="material-icons dropdown">arrow_drop_down</i>
+              Configure Dockerfile settings
+            </AdvancedBuildTitle>
+          ) : (
+            <AdvancedBuildTitle>
+              <i className="material-icons dropdown">arrow_drop_down</i>
+              Configure buildpack settings
+            </AdvancedBuildTitle>
+          )}
+        </StyledAdvancedBuildSettings>
+      )}
+      <AnimateHeight duration={500} height={showSettings ? "auto" : 0}>
+        <StyledSourceBox>
+          <Controller
+            name="app.build.method"
+            control={control}
+            render={({ field: { value, onChange } }) => (
+              <Select
+                value={value}
+                width="300px"
+                options={[
+                  { value: "docker", label: "Docker" },
+                  { value: "pack", label: "Buildpacks" },
+                ]}
+                setValue={(option: string) => {
+                  if (option === "docker") {
+                    onChange("docker");
+                  } else if (option === "pack") {
+                    // if toggling from docker to pack, initialize buildpacks to empty array and builder to default
+                    onChange("pack");
+                    setValue("app.build.buildpacks", []);
+                    setValue("app.build.builder", DEFAULT_BUILDERS[0]);
+                  }
+                }}
+                label="Build method"
+                labelColor="#DFDFE1"
+              />
+            )}
+          />
+          {match(build)
+            .with({ method: "docker" }, () => (
+              <>
+                <Spacer y={0.5} />
+                <DockerfileSettings projectId={projectId} source={source} />
+              </>
+            ))
+            .with({ method: "pack" }, (b) => (
+              <>
+                <Spacer y={0.5} />
+                <BuildpackSettings
+                  projectId={projectId}
+                  build={b}
+                  source={source}
+                  populateBuildValuesOnceAfterDetection={!appExists}
+                />
+              </>
+            ))
+            .exhaustive()}
+        </StyledSourceBox>
+      </AnimateHeight>
+    </>
+  );
+};
+
+const StyledAdvancedBuildSettings = styled.div`
+  color: ${({ showSettings }) => (showSettings ? "#DFDFE1" : "#aaaabb")};
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+    color: white;
+  }
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-radius: 5px;
+  height: 40px;
+  font-size: 13px;
+  width: 100%;
+  padding-left: 10px;
+  cursor: pointer;
+  border-bottom-left-radius: ${({ showSettings }) => showSettings && "0px"};
+  border-bottom-right-radius: ${({ showSettings }) => showSettings && "0px"};
+  .dropdown {
+    margin-right: 8px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+    transform: ${(props: { showSettings: boolean }) =>
+      props.showSettings ? "" : "rotate(-90deg)"};
+  }
+`;
+
+const AdvancedBuildTitle = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const StyledSourceBox = styled.div`
+  width: 100%;
+  color: #ffffff;
+  padding: 25px 35px 25px;
+  position: relative;
+  font-size: 13px;
+  border-radius: 5px;
+  background: ${(props) => props.theme.fg};
+  border: 1px solid #494b4f;
+  border-top: 0px;
+  border-top-left-radius: 0px;
+  border-top-right-radius: 0px;
+`;

+ 10 - 6
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -420,7 +420,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
           errorMessage = `${errorMessage}.`;
         }
 
-       if (appErrors.includes("env")) {
+        if (appErrors.includes("env")) {
           errorMessage = "Environment variables are not properly configured";
           if (errors.app?.env?.root?.message ?? errors.app?.env?.message) {
             const envErrorMessage =
@@ -623,11 +623,15 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                       <Spacer y={1} />
                       {source?.type ? (
                         source.type === "github" ? (
-                          <RepoSettings
-                            build={build}
-                            source={source}
-                            projectId={currentProject.id}
-                          />
+                          <>
+                            <Text size={16}>Build settings</Text>
+                            <Spacer y={0.5} />
+                            <RepoSettings
+                              build={build}
+                              source={source}
+                              projectId={currentProject.id}
+                            />
+                          </>
                         ) : (
                           <ImageSettings
                             projectId={currentProject.id}

+ 9 - 182
dashboard/src/main/home/app-dashboard/create-app/RepoSettings.tsx

@@ -1,30 +1,20 @@
-import React, { useContext, useEffect, useMemo, useState } from "react";
+import React, { useEffect, useMemo } from "react";
 import { useQuery } from "@tanstack/react-query";
-import AnimateHeight from "react-animate-height";
 import { Controller, useFormContext } from "react-hook-form";
 import styled from "styled-components";
-import { match } from "ts-pattern";
 import { z } from "zod";
 
-import Loading from "components/Loading";
-import { ControlledInput } from "components/porter/ControlledInput";
 import Input from "components/porter/Input";
-import Select from "components/porter/Select";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import { type PorterAppFormData, type SourceOptions } from "lib/porter-apps";
 import { type BuildOptions } from "lib/porter-apps/build";
 
 import api from "shared/api";
-import { Context } from "shared/Context";
 
 import BranchSelector from "../build-settings/BranchSelector";
 import RepositorySelector from "../build-settings/RepositorySelector";
-import BuildpackSettings, {
-  DEFAULT_BUILDERS,
-} from "../validate-apply/build-settings/buildpacks/BuildpackSettings";
-import DockerfileSettings from "../validate-apply/build-settings/docker/DockerfileSettings";
-import PorterYamlInput from "./PorterYamlInput";
+import { BuildSettings } from "./BuildSettings";
 
 type Props = {
   projectId: number;
@@ -48,9 +38,7 @@ const RepoSettings: React.FC<Props> = ({
   build,
   appExists,
 }) => {
-  const { currentProject } = useContext(Context);
-  const { control, register, setValue } = useFormContext<PorterAppFormData>();
-  const [showSettings, setShowSettings] = useState<boolean>(false);
+  const { control, setValue } = useFormContext<PorterAppFormData>();
 
   const repoIsSet = useMemo(
     () => source.git_repo_name !== "",
@@ -114,8 +102,6 @@ const RepoSettings: React.FC<Props> = ({
 
   return (
     <div>
-      <Text size={16}>Build settings</Text>
-      <Spacer y={0.5} />
       {!appExists && (
         <>
           <Text color="helper">Specify your GitHub repository.</Text>
@@ -235,124 +221,13 @@ const RepoSettings: React.FC<Props> = ({
                 </>
               )}
               <Spacer y={0.5} />
-              {!appExists && (
-                <>
-                  <Text color="helper">
-                    Specify your application root path.
-                  </Text>
-                  <Spacer y={0.5} />
-                </>
-              )}
-              <ControlledInput
-                placeholder="ex: ./"
-                width="100%"
-                type="text"
-                {...register("app.build.context")}
-                label={"Application root path:"}
+              <BuildSettings
+                projectId={projectId}
+                source={source}
+                build={build}
+                appExists={appExists}
+                loadingBranchContents={isLoading}
               />
-              <Spacer y={1} />
-              {!appExists && currentProject?.id && (
-                <>
-                  <Text color="helper">
-                    (Optional) Specify your porter.yaml path.{" "}
-                    <a
-                      href="https://docs.porter.run/deploy/configuration-as-code/overview"
-                      target="_blank"
-                      rel="noreferrer"
-                    >
-                      &nbsp;(?)
-                    </a>
-                  </Text>
-                  <Spacer y={0.5} />
-                  <PorterYamlInput
-                    projectId={currentProject.id}
-                    repoId={source.git_repo_id}
-                    repoOwner={source.git_repo_name.split("/")[0]}
-                    repoName={source.git_repo_name.split("/")[1]}
-                    branch={source.git_branch}
-                  />
-                  <Spacer y={1} />
-                </>
-              )}
-              {isLoading && !appExists ? (
-                <AdvancedBuildTitle>
-                  <Loading />
-                </AdvancedBuildTitle>
-              ) : (
-                <StyledAdvancedBuildSettings
-                  showSettings={showSettings}
-                  onClick={() => {
-                    setShowSettings(!showSettings);
-                  }}
-                >
-                  {build.method === "docker" ? (
-                    <AdvancedBuildTitle>
-                      <i className="material-icons dropdown">arrow_drop_down</i>
-                      Configure Dockerfile settings
-                    </AdvancedBuildTitle>
-                  ) : (
-                    <AdvancedBuildTitle>
-                      <i className="material-icons dropdown">arrow_drop_down</i>
-                      Configure buildpack settings
-                    </AdvancedBuildTitle>
-                  )}
-                </StyledAdvancedBuildSettings>
-              )}
-              <AnimateHeight duration={500} height={showSettings ? "auto" : 0}>
-                <StyledSourceBox>
-                  <Controller
-                    name="app.build.method"
-                    control={control}
-                    render={({ field: { value, onChange } }) => (
-                      <Select
-                        value={value}
-                        width="300px"
-                        options={[
-                          { value: "docker", label: "Docker" },
-                          { value: "pack", label: "Buildpacks" },
-                        ]}
-                        setValue={(option: string) => {
-                          if (option === "docker") {
-                            onChange("docker");
-                          } else if (option === "pack") {
-                            // if toggling from docker to pack, initialize buildpacks to empty array and builder to default
-                            onChange("pack");
-                            setValue("app.build.buildpacks", []);
-                            setValue("app.build.builder", DEFAULT_BUILDERS[0]);
-                          }
-                        }}
-                        label="Build method"
-                        labelColor="#DFDFE1"
-                      />
-                    )}
-                  />
-                  {match(build)
-                    .with({ method: "docker" }, () => (
-                      <>
-                        <Spacer y={0.5} />
-                        <DockerfileSettings
-                          projectId={projectId}
-                          repoId={source.git_repo_id}
-                          repoOwner={source.git_repo_name.split("/")[0]}
-                          repoName={source.git_repo_name.split("/")[1]}
-                          branch={source.git_branch}
-                        />
-                      </>
-                    ))
-                    .with({ method: "pack" }, (b) => (
-                      <>
-                        <Spacer y={0.5} />
-                        <BuildpackSettings
-                          projectId={projectId}
-                          build={b}
-                          source={source}
-                          populateBuildValuesOnceAfterDetection={!appExists}
-                        />
-                      </>
-                    ))
-                    .exhaustive()}
-                </StyledSourceBox>
-              </AnimateHeight>
             </>
           )}
         </>
@@ -400,51 +275,3 @@ const BackButton = styled.div`
     margin-right: 6px;
   }
 `;
-
-const StyledAdvancedBuildSettings = styled.div`
-  color: ${({ showSettings }) => (showSettings ? "#DFDFE1" : "#aaaabb")};
-  background: ${({ theme }) => theme.fg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-    color: white;
-  }
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  border-radius: 5px;
-  height: 40px;
-  font-size: 13px;
-  width: 100%;
-  padding-left: 10px;
-  cursor: pointer;
-  border-bottom-left-radius: ${({ showSettings }) => showSettings && "0px"};
-  border-bottom-right-radius: ${({ showSettings }) => showSettings && "0px"};
-  .dropdown {
-    margin-right: 8px;
-    font-size: 20px;
-    cursor: pointer;
-    border-radius: 20px;
-    transform: ${(props: { showSettings: boolean }) =>
-      props.showSettings ? "" : "rotate(-90deg)"};
-  }
-`;
-
-const AdvancedBuildTitle = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const StyledSourceBox = styled.div`
-  width: 100%;
-  color: #ffffff;
-  padding: 25px 35px 25px;
-  position: relative;
-  font-size: 13px;
-  border-radius: 5px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-  border-top: 0px;
-  border-top-left-radius: 0px;
-  border-top-right-radius: 0px;
-`;

+ 18 - 13
dashboard/src/main/home/app-dashboard/new-app-flow/SourceSelector.tsx

@@ -1,22 +1,25 @@
+import { type SourceOptions } from "lib/porter-apps";
 import React from "react";
 import styled from "styled-components";
 
-export type SourceType = "github" | "docker-registry";
+type SourceType = SourceOptions['type']
 
-interface SourceSelectorProps {
+type SourceSelectorProps = {
   selectedSourceType: SourceType | undefined;
   setSourceType: (sourceType: SourceType) => void;
-}
+};
 
 const SourceSelector: React.FC<SourceSelectorProps> = ({
   selectedSourceType,
-  setSourceType
+  setSourceType,
 }) => {
   return (
     <BlockList>
       <Block
-        selected={selectedSourceType === 'github'}
-        onClick={() => setSourceType('github')}
+        selected={selectedSourceType === "github"}
+        onClick={() => {
+          setSourceType("github");
+        }}
       >
         <BlockIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
         <BlockTitle>Git repository</BlockTitle>
@@ -25,8 +28,10 @@ const SourceSelector: React.FC<SourceSelectorProps> = ({
         </BlockDescription>
       </Block>
       <Block
-        selected={selectedSourceType === 'docker-registry'}
-        onClick={() => setSourceType('docker-registry')}
+        selected={selectedSourceType === "docker-registry"}
+        onClick={() => {
+          setSourceType("docker-registry");
+        }}
       >
         <BlockIcon src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png" />
         <BlockTitle>Docker registry</BlockTitle>
@@ -34,10 +39,9 @@ const SourceSelector: React.FC<SourceSelectorProps> = ({
           Deploy a container from an image registry.
         </BlockDescription>
       </Block>
-
     </BlockList>
   );
-}
+};
 
 export default SourceSelector;
 
@@ -58,10 +62,11 @@ const Block = styled.div<{ selected?: boolean }>`
   position: relative;
   transition: all 0.2s;
   border-radius: 5px;
-  background: ${props => props.theme.clickable.bg};
-  border: ${props => props.selected ? "2px solid #8590ff" : "1px solid #494b4f"};
+  background: ${(props) => props.theme.clickable.bg};
+  border: ${(props) =>
+    props.selected ? "2px solid #8590ff" : "1px solid #494b4f"};
   :hover {
-    border: ${({ selected }) => (!selected && "1px solid #7a7b80")};
+    border: ${({ selected }) => !selected && "1px solid #7a7b80"};
   }
 
   animation: fadeIn 0.3s 0s;

+ 6 - 7
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvSettings.tsx

@@ -1,17 +1,16 @@
 import React from "react";
-import { SourceOptions } from "lib/porter-apps";
 
-import { PopulatedEnvGroup } from "./types";
-import EnvVariables from "./EnvVariables";
-import EnvGroups from "./EnvGroups";
-import { AppRevision } from "lib/revisions/types";
 import Spacer from "components/porter/Spacer";
+import { type AppRevision } from "lib/revisions/types";
+
+import EnvGroups from "./EnvGroups";
+import EnvVariables from "./EnvVariables";
+import { type PopulatedEnvGroup } from "./types";
 
 type Props = {
   appName?: string;
   revision?: AppRevision;
   baseEnvGroups?: PopulatedEnvGroup[];
-  latestSource?: SourceOptions;
   attachedEnvGroups?: PopulatedEnvGroup[];
 };
 
@@ -19,7 +18,7 @@ const EnvSettings: React.FC<Props> = (props) => {
   return (
     <>
       <Spacer y={1} />
-      <EnvVariables syncedEnvGroups={props.attachedEnvGroups}/>
+      <EnvVariables syncedEnvGroups={props.attachedEnvGroups} />
       <Spacer y={1} />
       <EnvGroups {...props} attachedEnvGroups={props.attachedEnvGroups} />
     </>

+ 6 - 5
dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvVariables.tsx

@@ -3,6 +3,8 @@ import { useFieldArray, useFormContext } from "react-hook-form";
 import styled from "styled-components";
 
 import { type NewPopulatedEnvGroup } from "components/porter-form/types";
+import Button from "components/porter/Button";
+import Image from "components/porter/Image";
 import Spacer from "components/porter/Spacer";
 import EnvEditorModal from "main/home/modals/EnvEditorModal";
 import Modal from "main/home/modals/Modal";
@@ -11,9 +13,7 @@ import { type PorterAppFormData } from "lib/porter-apps";
 import { dotenv_parse } from "shared/string_utils";
 import upload from "assets/upload.svg";
 
-import Image from "components/porter/Image";
 import EnvVarRow from "./EnvVarRow";
-import Button from "components/porter/Button";
 
 export type KeyValueType = {
   key: string;
@@ -46,7 +46,8 @@ const EnvVariables = ({ syncedEnvGroups }: PropsType) => {
     if (!syncedEnvGroups) return false;
     return syncedEnvGroups.some(
       (envGroup) =>
-        key in (envGroup.variables || []) || key in (envGroup.secret_variables || [])
+        key in (envGroup.variables || []) ||
+        key in (envGroup.secret_variables || [])
     );
   };
 
@@ -109,7 +110,7 @@ const EnvVariables = ({ syncedEnvGroups }: PropsType) => {
           }}
         >
           <Image src={upload} size={16} />
-          <Spacer inline x={.5} />
+          <Spacer inline x={0.5} />
           Copy from file
         </Button>
       </InputWrapper>
@@ -153,4 +154,4 @@ const InputWrapper = styled.div`
   display: flex;
   align-items: center;
   margin-bottom: 5px;
-`;
+`;

+ 44 - 36
dashboard/src/main/home/app-dashboard/validate-apply/build-settings/buildpacks/BuildpackSettings.tsx

@@ -1,32 +1,35 @@
 import React, { useEffect, useMemo, useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Controller, useFieldArray, useFormContext } from "react-hook-form";
 import styled, { keyframes } from "styled-components";
+import { z } from "zod";
+
+import Button from "components/porter/Button";
 import Error from "components/porter/Error";
-import { useQuery } from "@tanstack/react-query";
-import api from "shared/api";
+import Select from "components/porter/Select";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
 import {
-  Buildpack,
   DEFAULT_BUILDER_NAME,
   DEFAULT_HEROKU_STACK,
-  DetectedBuildpack,
   detectedBuildpackSchema,
+  type Buildpack,
+  type DetectedBuildpack,
 } from "main/home/app-dashboard/types/buildpack";
-import { z } from "zod";
-import Spacer from "components/porter/Spacer";
-import Button from "components/porter/Button";
-import BuildpackList from "./BuildpackList";
+import { type PorterAppFormData, type SourceOptions } from "lib/porter-apps";
+import { type BuildOptions } from "lib/porter-apps/build";
+
+import api from "shared/api";
+
 import BuildpackConfigurationModal from "./BuildpackConfigurationModal";
-import { Controller, useFieldArray, useFormContext } from "react-hook-form";
-import { PorterAppFormData, SourceOptions } from "lib/porter-apps";
-import { BuildOptions } from "lib/porter-apps/build";
-import Select from "components/porter/Select";
-import Text from "components/porter/Text";
+import BuildpackList from "./BuildpackList";
 
 type Props = {
   projectId: number;
   build: BuildOptions & {
     method: "pack";
   };
-  source: SourceOptions & { type: "github" };
+  source: SourceOptions & { type: "github" | "local" };
   populateBuildValuesOnceAfterDetection?: boolean;
 };
 
@@ -37,7 +40,7 @@ export const DEFAULT_BUILDERS = [
   "heroku/builder:22",
   "heroku/builder-classic:22",
   "heroku/buildpacks:18",
-]
+];
 
 const BuildpackSettings: React.FC<Props> = ({
   projectId,
@@ -45,7 +48,9 @@ const BuildpackSettings: React.FC<Props> = ({
   source,
   populateBuildValuesOnceAfterDetection,
 }) => {
-  const [populateBuild, setPopulateBuild] = useState<boolean>(populateBuildValuesOnceAfterDetection ?? false);
+  const [populateBuild, setPopulateBuild] = useState<boolean>(
+    populateBuildValuesOnceAfterDetection ?? false
+  );
   const [isModalOpen, setIsModalOpen] = useState(false);
   const [availableBuildpacks, setAvailableBuildpacks] = useState<Buildpack[]>(
     []
@@ -66,6 +71,10 @@ const BuildpackSettings: React.FC<Props> = ({
       isModalOpen,
     ],
     async () => {
+      if (source.type !== "github") {
+        return [];
+      }
+
       const detectBuildPackRes = await api.detectBuildpack<DetectedBuildpack[]>(
         "<token>",
         {
@@ -81,14 +90,14 @@ const BuildpackSettings: React.FC<Props> = ({
         }
       );
 
-      const detectedBuildpacks = z
+      const detectedBuildpacks = await z
         .array(detectedBuildpackSchema)
         .parseAsync(detectBuildPackRes.data);
 
       return detectedBuildpacks;
     },
     {
-      enabled: populateBuild || isModalOpen,
+      enabled: source.type === "github" && (populateBuild || isModalOpen),
       retry: 0,
       refetchOnWindowFocus: false,
     }
@@ -103,16 +112,17 @@ const BuildpackSettings: React.FC<Props> = ({
   );
 
   const builderOptions = useMemo(() => {
-    const allBuilderOptions = [
-      build.builder,
-      ...DEFAULT_BUILDERS
-    ].sort();
+    const allBuilderOptions = [build.builder, ...DEFAULT_BUILDERS].sort();
 
     return Array.from(new Set(allBuilderOptions)).map((builder) => ({
       label: builder,
       value: builder,
     }));
-  }, [build.builder])
+  }, [build.builder]);
+
+  const iseDetectingBuildpacks = useMemo(() => {
+    return status === "loading" && source.type === "github";
+  }, [status, source]);
 
   useEffect(() => {
     if (!data || data.length === 0) {
@@ -124,9 +134,7 @@ const BuildpackSettings: React.FC<Props> = ({
         (builder) => builder.name.toLowerCase() === DEFAULT_BUILDER_NAME
       ) ?? data[0];
 
-    const allBuildpacks = defaultBuilder.others.concat(
-      defaultBuilder.detected
-    );
+    const allBuildpacks = defaultBuilder.others.concat(defaultBuilder.detected);
 
     setAvailableBuildpacks(
       allBuildpacks.filter(
@@ -183,21 +191,22 @@ const BuildpackSettings: React.FC<Props> = ({
       {build.buildpacks.length > 0 && (
         <>
           <Spacer y={0.5} />
-          {populateBuildValuesOnceAfterDetection && 
+          {populateBuildValuesOnceAfterDetection && (
             <>
               <Text color="helper">
-                The following buildpacks were detected at your application's root path. You can also
-                manually add, remove, or re-order buildpacks here.
+                The following buildpacks were detected at your
+                application&apos;s root path. You can also manually add, remove,
+                or re-order buildpacks here.
               </Text>
               <Spacer y={0.5} />
             </>
-          }
+          )}
           <BuildpackList
             build={build}
             availableBuildpacks={availableBuildpacks}
             setAvailableBuildpacks={setAvailableBuildpacks}
             showAvailableBuildpacks={false}
-            isDetectingBuildpacks={status === "loading"}
+            isDetectingBuildpacks={iseDetectingBuildpacks}
             detectBuildpacksError={errorMessage}
             droppableId={"non-modal"}
           />
@@ -207,16 +216,15 @@ const BuildpackSettings: React.FC<Props> = ({
         <>
           <Spacer y={0.5} />
           <Text color="helper">
-            No buildpacks have been specified. Click the button below to add buildpacks detected at your application's root path.
+            No buildpacks have been specified. Click the button below to add
+            buildpacks detected at your application&apos;s root path.
           </Text>
         </>
       )}
       {errorMessage && (
         <>
           <Spacer y={1} />
-          <Error
-            message={errorMessage}
-          />
+          <Error message={errorMessage} />
         </>
       )}
       <Spacer y={1} />
@@ -235,7 +243,7 @@ const BuildpackSettings: React.FC<Props> = ({
           }}
           availableBuildpacks={availableBuildpacks}
           setAvailableBuildpacks={setAvailableBuildpacks}
-          isDetectingBuildpacks={status === "loading"}
+          isDetectingBuildpacks={iseDetectingBuildpacks}
           detectBuildpacksError={errorMessage}
         />
       )}

+ 72 - 64
dashboard/src/main/home/app-dashboard/validate-apply/build-settings/docker/DockerfileSettings.tsx

@@ -1,12 +1,14 @@
 import React, { useEffect, useRef, useState } from "react";
 import { Controller, useFormContext } from "react-hook-form";
 import styled from "styled-components";
+import { match } from "ts-pattern";
 
 import Container from "components/porter/Container";
+import { ControlledInput } from "components/porter/ControlledInput";
 import Input from "components/porter/Input";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
-import { type PorterAppFormData } from "lib/porter-apps";
+import { type PorterAppFormData, type SourceOptions } from "lib/porter-apps";
 
 import folder from "assets/folder_v2.svg";
 
@@ -14,30 +16,21 @@ import FileSelector from "../FileSelector";
 
 type Props = {
   projectId: number;
-  repoId: number;
-  repoOwner: string;
-  repoName: string;
-  branch: string;
+  source: SourceOptions & { type: "github" | "local" };
 };
-const DockerfileSettings: React.FC<Props> = ({
-  projectId,
-  repoId,
-  repoOwner,
-  repoName,
-  branch,
-}) => {
-  const { control, watch } = useFormContext<PorterAppFormData>();
+const DockerfileSettings: React.FC<Props> = ({ projectId, source }) => {
+  const { control, watch, register } = useFormContext<PorterAppFormData>();
   const [showFileSelector, setShowFileSelector] = useState<boolean>(false);
 
   const path = watch("app.build.dockerfile", "");
 
-  const fileSelectorRef = useRef(null);
+  const fileSelectorRef = useRef<HTMLDivElement | null>(null);
 
   useEffect(() => {
     const handleClickOutside = (event: { target: unknown }): void => {
       if (
         fileSelectorRef.current &&
-        !fileSelectorRef.current?.contains(event.target)
+        !fileSelectorRef.current?.contains(event.target as Node)
       ) {
         setShowFileSelector(false);
       }
@@ -48,57 +41,72 @@ const DockerfileSettings: React.FC<Props> = ({
     };
   }, [fileSelectorRef]);
 
-  return (
-    <Controller
-      name="app.build.dockerfile"
-      control={control}
-      render={({ field: { onChange } }) => (
-        <div>
-          <Text>Dockerfile path</Text>
-          <Spacer y={0.5} />
-          <Container row>
-            <Input
-              width="300px"
-              placeholder="ex: ./Dockerfile"
-              value={path}
-              setValue={(val: string) => {
-                onChange(val);
-              }}
-            />
-            <Spacer inline x={0.5} />
-            <FileDirectoryToggleButton
-              onClick={() => {
-                setShowFileSelector(!showFileSelector);
-              }}
-              color="#b91133"
-            >
-              <img src={folder} />
-            </FileDirectoryToggleButton>
-          </Container>
-          {showFileSelector && (
-            <div ref={fileSelectorRef}>
-              <FileSelector
-                projectId={projectId}
-                repoId={repoId}
-                repoOwner={repoOwner}
-                repoName={repoName}
-                branch={branch}
-                onFileSelect={(path: string) => {
-                  onChange(`./${path}`);
-                  setShowFileSelector(false);
+  return match(source)
+    .with({ type: "github" }, (s) => (
+      <Controller
+        name="app.build.dockerfile"
+        control={control}
+        render={({ field: { onChange } }) => (
+          <div>
+            <Text>Dockerfile path</Text>
+            <Spacer y={0.5} />
+            <Container row>
+              <Input
+                width="300px"
+                placeholder="ex: ./Dockerfile"
+                value={path}
+                setValue={(val: string) => {
+                  onChange(val);
                 }}
-                isFileSelectable={(path: string) =>
-                  path.toLowerCase().includes("dockerfile")
-                }
-                headerText={"Select your Dockerfile:"}
-                widthPercent={100}
               />
-            </div>
-          )}
-        </div>
-      )}
-    />
-  );
+              <Spacer inline x={0.5} />
+              <FileDirectoryToggleButton
+                onClick={() => {
+                  setShowFileSelector(!showFileSelector);
+                }}
+                color="#b91133"
+              >
+                <img src={folder} />
+              </FileDirectoryToggleButton>
+            </Container>
+            {showFileSelector && (
+              <div ref={fileSelectorRef}>
+                <FileSelector
+                  projectId={projectId}
+                  repoId={s.git_repo_id}
+                  repoOwner={s.git_repo_name.split("/")[0]}
+                  repoName={s.git_repo_name.split("/")[1]}
+                  branch={s.git_branch}
+                  onFileSelect={(path: string) => {
+                    onChange(`./${path}`);
+                    setShowFileSelector(false);
+                  }}
+                  isFileSelectable={(path: string) =>
+                    path.toLowerCase().includes("dockerfile")
+                  }
+                  headerText={"Select your Dockerfile:"}
+                  widthPercent={100}
+                />
+              </div>
+            )}
+          </div>
+        )}
+      />
+    ))
+    .with({ type: "local" }, () => (
+      <>
+        <Text color="helper">Dockerfile path (absolute path)</Text>
+        <Spacer y={0.5} />
+        <ControlledInput
+          width="300px"
+          placeholder="ex: ./Dockerfile"
+          type="text"
+          {...register("app.build.dockerfile")}
+        />
+        <Spacer y={0.5} />
+      </>
+    ))
+    .exhaustive();
 };
 
 export default DockerfileSettings;

+ 2 - 26
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx

@@ -19,11 +19,7 @@ import {
   clientAddonValidator,
 } from "lib/addons";
 import { useAppWithPreviewOverrides } from "lib/hooks/useAppWithPreviewOverrides";
-import {
-  basePorterAppFormValidator,
-  clientAppToProto,
-  type SourceOptions,
-} from "lib/porter-apps";
+import { basePorterAppFormValidator, clientAppToProto } from "lib/porter-apps";
 
 import api from "shared/api";
 
@@ -85,29 +81,9 @@ export const PreviewAppDataContainer: React.FC<Props> = ({
     clusterId,
     projectId,
     deploymentTarget,
+    latestSource,
   } = useLatestRevision();
 
-  const latestSource: SourceOptions = useMemo(() => {
-    if (porterApp.image_repo_uri) {
-      const [repository, tag] = porterApp.image_repo_uri.split(":");
-      return {
-        type: "docker-registry",
-        image: {
-          repository,
-          tag,
-        },
-      };
-    }
-
-    return {
-      type: "github",
-      git_repo_id: porterApp.git_repo_id ?? 0,
-      git_repo_name: porterApp.repo_name ?? "",
-      git_branch: porterApp.git_branch ?? "",
-      porter_yaml_path: porterApp.porter_yaml_path ?? "./porter.yaml",
-    };
-  }, [porterApp]);
-
   const withPreviewOverrides = useAppWithPreviewOverrides({
     latestApp: latestProto,
     detectedServices: servicesFromYaml,

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels