Quellcode durchsuchen

[POR-1989] Implement file picker for Dockerfile and porter.yaml in v2 (#3851)

Feroze Mohideen vor 2 Jahren
Ursprung
Commit
42eef13499

+ 35 - 0
dashboard/package-lock.json

@@ -108,6 +108,7 @@
         "@types/qs": "^6.9.5",
         "@types/react": "^18.0.0",
         "@types/react-beautiful-dnd": "^13.1.4",
+        "@types/react-collapse": "^5.0.3",
         "@types/react-color": "^3.0.6",
         "@types/react-datepicker": "^4.4.2",
         "@types/react-dom": "^18.0.0",
@@ -131,6 +132,7 @@
         "prettier": "2.2.1",
         "qs": "^6.9.4",
         "react-beautiful-dnd": "^13.1.1",
+        "react-collapse": "^5.1.1",
         "react-refresh": "^0.10.0",
         "source-map-loader": "^1.1.0",
         "style-loader": "^2.0.0",
@@ -3201,6 +3203,15 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-collapse": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/@types/react-collapse/-/react-collapse-5.0.3.tgz",
+      "integrity": "sha512-/UBdZtLac+JBaHehz8S1UDKUEGNA9FxMXHEYZXHKXqGDcoM/rdexc9UMInBNs3iDLDytIc26N4Ad6ou9P0CnvQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@types/react-color": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.6.tgz",
@@ -11107,6 +11118,15 @@
         "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
       }
     },
+    "node_modules/react-collapse": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/react-collapse/-/react-collapse-5.1.1.tgz",
+      "integrity": "sha512-k6cd7csF1o9LBhQ4AGBIdxB60SUEUMQDAnL2z1YvYNr9KoKr+nDkhN6FK7uGaBd/rYrYfrMpzpmJEIeHRYogBw==",
+      "dev": true,
+      "peerDependencies": {
+        "react": ">=16.3.0"
+      }
+    },
     "node_modules/react-color": {
       "version": "2.19.3",
       "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
@@ -17581,6 +17601,15 @@
         "@types/react": "*"
       }
     },
+    "@types/react-collapse": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/@types/react-collapse/-/react-collapse-5.0.3.tgz",
+      "integrity": "sha512-/UBdZtLac+JBaHehz8S1UDKUEGNA9FxMXHEYZXHKXqGDcoM/rdexc9UMInBNs3iDLDytIc26N4Ad6ou9P0CnvQ==",
+      "dev": true,
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/react-color": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.6.tgz",
@@ -24003,6 +24032,12 @@
         "use-memo-one": "^1.1.1"
       }
     },
+    "react-collapse": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/react-collapse/-/react-collapse-5.1.1.tgz",
+      "integrity": "sha512-k6cd7csF1o9LBhQ4AGBIdxB60SUEUMQDAnL2z1YvYNr9KoKr+nDkhN6FK7uGaBd/rYrYfrMpzpmJEIeHRYogBw==",
+      "dev": true
+    },
     "react-color": {
       "version": "2.19.3",
       "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",

+ 2 - 0
dashboard/package.json

@@ -113,6 +113,7 @@
     "@types/qs": "^6.9.5",
     "@types/react": "^18.0.0",
     "@types/react-beautiful-dnd": "^13.1.4",
+    "@types/react-collapse": "^5.0.3",
     "@types/react-color": "^3.0.6",
     "@types/react-datepicker": "^4.4.2",
     "@types/react-dom": "^18.0.0",
@@ -136,6 +137,7 @@
     "prettier": "2.2.1",
     "qs": "^6.9.4",
     "react-beautiful-dnd": "^13.1.1",
+    "react-collapse": "^5.1.1",
     "react-refresh": "^0.10.0",
     "source-map-loader": "^1.1.0",
     "style-loader": "^2.0.0",

Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 0
dashboard/src/assets/file-branch.svg


+ 3 - 0
dashboard/src/assets/file_v2.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M15.0002 2.3999V5.9999C15.0002 6.66264 15.5374 7.1999 16.2002 7.1999H19.8002M18.0002 4.1999C17.4661 3.72204 16.9119 3.15528 16.562 2.78718C16.3292 2.54224 16.0075 2.3999 15.6696 2.3999H6.59989C5.27441 2.3999 4.1999 3.47441 4.19989 4.79989L4.1998 19.1998C4.19979 20.5253 5.2743 21.5998 6.59979 21.5998L17.3998 21.5999C18.7253 21.5999 19.7998 20.5254 19.7998 19.2L19.8002 6.47773C19.8002 6.1709 19.6831 5.87594 19.4702 5.65503C19.0764 5.24655 18.4188 4.57442 18.0002 4.1999Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/folder_v2.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.40105 8.41649L2.40096 17.9408C2.40095 19.0454 3.29638 19.9408 4.40096 19.9408L19.5997 19.9409C20.7042 19.9409 21.5997 19.0455 21.5997 17.9409L21.6 8.01226C21.6 7.45996 21.1523 7.01223 20.6 7.01223H12.0837L9.31869 4.05859H3.4004C2.84797 4.05859 2.40019 4.50581 2.40037 5.05824C2.40067 6.00905 2.40106 7.43597 2.40105 8.41649Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 1 - 1
dashboard/src/components/porter/Button.tsx

@@ -189,7 +189,7 @@ const StyledButton = styled.button<{
     if (props.alt || props.color === "fg") {
       return props.theme.fg;
     }
-    return props.disabled && !props.color
+    return props.disabled
       ? "#aaaabb"
       : props.color || props.theme.button;
   }};

+ 23 - 0
dashboard/src/components/porter/CollapsibleContainer.tsx

@@ -0,0 +1,23 @@
+import React, { ReactNode } from "react";
+
+import { Collapse } from "react-collapse";
+import './collapsible-container.css';
+
+type Props = {
+    isOpened: boolean;
+    children: ReactNode;
+};
+
+const CollapsibleContainer: React.FC<Props> = ({
+    isOpened,
+    children,
+}) => {
+
+  return (
+    <Collapse isOpened={isOpened}>
+        {children}
+    </Collapse>
+  );
+};
+
+export default CollapsibleContainer;

+ 6 - 0
dashboard/src/components/porter/Input.tsx

@@ -17,6 +17,7 @@ type Props = {
   disabled?: boolean;
   disabledTooltip?: string;
   onValueChange?: (value: string) => void;
+  hideCursor?: boolean;
 };
 
 const Input: React.FC<Props> = ({
@@ -33,6 +34,7 @@ const Input: React.FC<Props> = ({
   disabled,
   disabledTooltip,
   onValueChange,
+  hideCursor = false,
 }) => {
   const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const inputValue = e.target.value;
@@ -55,6 +57,7 @@ const Input: React.FC<Props> = ({
           type={type || "text"}
           hasError={(error && true) || error === ""}
           disabled={disabled ? disabled : false}
+          hideCursor={hideCursor}
         />
         {error && (
           <Error>
@@ -78,6 +81,7 @@ const Input: React.FC<Props> = ({
         type={type || "text"}
         hasError={(error && true) || error === ""}
         disabled={disabled ? disabled : false}
+        hideCursor={hideCursor}
       />
       {error && (
         <Error>
@@ -124,6 +128,7 @@ const StyledInput = styled.input<{
   height: string;
   hasError: boolean;
   disabled: boolean;
+  hideCursor: boolean;
 }>`
   height: ${(props) => props.height || "35px"};
   padding: 5px 10px;
@@ -143,4 +148,5 @@ const StyledInput = styled.input<{
       border: 1px solid ${props.hasError ? "#ff3b62" : "#7a7b80"};
     }
   `}
+  ${(props) => props.hideCursor && "caret-color: transparent;"}
 `;

+ 3 - 0
dashboard/src/components/porter/collapsible-container.css

@@ -0,0 +1,3 @@
+.ReactCollapse--collapse {
+    transition: height 500ms;
+}

+ 77 - 0
dashboard/src/lib/hooks/useGithubContents.ts

@@ -0,0 +1,77 @@
+import { useQuery } from "@tanstack/react-query";
+import _ from "lodash";
+import { useEffect, useState } from "react";
+import api from "shared/api";
+import { z } from "zod";
+
+type UseGithubContentsOptions = {
+    repoId: number;
+    repoOwner: string;
+    repoName: string;
+    branch: string;
+    path: string;
+    projectId: number;
+};
+
+const githubContentsValidator = z.discriminatedUnion("type", [
+    z.object({
+        path: z.string(),
+        type: z.literal("file"),
+    }),
+    z.object({
+        path: z.string(),
+        type: z.literal("dir"),
+    }),
+]);
+type GithubContents = z.infer<typeof githubContentsValidator>;
+
+export const useGithubContents = ({
+    repoId,
+    repoOwner,
+    repoName,
+    branch,
+    path,
+    projectId,
+}: UseGithubContentsOptions) => {
+    const [contents, setContents] = useState<GithubContents[]>([]);
+    
+    const result = useQuery(
+        ["getGithubContentsAtPath", repoOwner, repoName, branch, path],
+        async () => {
+            const res = await api.getBranchContents(
+                "<token>", 
+                {
+                    dir: path,
+                },
+                {
+                    kind: "github",
+                    project_id: projectId,
+                    git_repo_id: repoId,
+                    owner: repoOwner,
+                    name: repoName,
+                    branch,
+                }
+            );
+
+            const parsed = await z.array(githubContentsValidator).parseAsync(res.data);
+            return parsed;
+        }
+    );
+
+    useEffect(() => {
+        if (result.isSuccess) {
+            const folders = result.data.filter((c) => c.type === "dir").sort((a, b) => a.path.localeCompare(b.path));
+            const files = result.data.filter((c) => c.type === "file").sort((a, b) => a.path.localeCompare(b.path));
+            const updatedContents = [...folders, ...files];
+    
+            if (!_.isEqual(updatedContents, contents)) {
+                setContents(updatedContents);
+            }
+        }
+    }, [result, contents]);
+
+    return {
+        contents,
+        isLoading: result.isLoading,
+    }
+};

+ 0 - 1
dashboard/src/main/home/app-dashboard/app-view/tabs/ImageSettingsTab.tsx

@@ -4,7 +4,6 @@ import { PorterAppFormData } from "lib/porter-apps";
 import { useLatestRevision } from "../LatestRevisionContext";
 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";

+ 9 - 0
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -612,6 +612,15 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                                         onChange(porterYamlPath);
                                       }}
                                       porterYamlPath={value}
+                                      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}
                                     />
                                   )}
                                 />

+ 57 - 11
dashboard/src/main/home/app-dashboard/create-app/PorterYamlModal.tsx

@@ -7,16 +7,32 @@ import Modal from "components/porter/Modal";
 import Text from "components/porter/Text";
 import Input from "components/porter/Input";
 import Button from "components/porter/Button";
+import FileSelector from "../validate-apply/build-settings/FileSelector";
 
 type Props = {
     close: () => void;
     setPorterYamlPath: (path: string) => void;
     porterYamlPath: string;
+    projectId: number;
+    repoId: number;
+    repoOwner: string;
+    repoName: string;
+    branch: string;
 }
 
-const PorterYamlModal: React.FC<Props> = ({ close, setPorterYamlPath, porterYamlPath }) => {
+const PorterYamlModal: React.FC<Props> = ({ 
+    close, 
+    setPorterYamlPath, 
+    porterYamlPath,
+    projectId,
+    repoId,
+    repoOwner,
+    repoName,
+    branch, 
+}) => {
     const [possiblePorterYamlPath, setPossiblePorterYamlPath] = useState<string>("");
     const [showModal, setShowModal] = useState<boolean>(true);
+    const [showFileSelector, setShowFileSelector] = useState<boolean>(false);
 
     return showModal ? (
         <Modal closeModal={() => setShowModal(false)}>
@@ -44,15 +60,39 @@ const PorterYamlModal: React.FC<Props> = ({ close, setPorterYamlPath, porterYaml
                 </span>
             </div>
             <Spacer y={0.5} />
-            <Text color="helper">Path to <Code>porter.yaml</Code> from repository root (i.e. starting with ./):</Text>
+            <Text color="helper">Path to <Code>porter.yaml</Code> from repository root:</Text>
             <Spacer y={0.5} />
-            <Input
-                disabled={false}
-                placeholder="ex: ./subdirectory/porter.yaml"
-                value={possiblePorterYamlPath}
-                width="100%"
-                setValue={setPossiblePorterYamlPath}
-            />
+            <InputWrapper
+                onClick={(e) => {
+                    e.stopPropagation();
+                    if (!showFileSelector) {
+                        setShowFileSelector(true);
+                        setPossiblePorterYamlPath("");
+                    }
+                }}
+            >
+                <Input
+                    placeholder="ex: ./subdirectory/porter.yaml"
+                    value={possiblePorterYamlPath}
+                    width="100%"
+                    setValue={setPossiblePorterYamlPath}
+                    hideCursor={true}
+                />
+            </InputWrapper>
+            {showFileSelector && 
+                <div>
+                    <FileSelector 
+                        projectId={projectId} 
+                        repoId={repoId} 
+                        repoOwner={repoOwner} 
+                        repoName={repoName} 
+                        branch={branch}
+                        onFileSelect={(path: string) => setPossiblePorterYamlPath(`./${path}`)} 
+                        isFileSelectable={(path: string) => path.endsWith(".yaml")}
+                        headerText={"Select your porter.yaml:"}
+                    />
+                </div>
+            }
             <Spacer y={1} />
             <div style={{ display: "flex", justifyContent: "space-between" }}>
                 <Button
@@ -66,9 +106,9 @@ const PorterYamlModal: React.FC<Props> = ({ close, setPorterYamlPath, porterYaml
                         setPorterYamlPath(possiblePorterYamlPath);
                         setShowModal(false);
                     }}
-                    color="#616fee"
+                    disabled={possiblePorterYamlPath === ""}
                 >
-                    Update path
+                    Confirm path
                 </Button>
             </div>
         </Modal>
@@ -79,4 +119,10 @@ export default PorterYamlModal;
 
 const Code = styled.span`
   font-family: monospace;
+`;
+
+const InputWrapper = styled.div`
+    width: 500px;
+    display: flex;
+    justify-content: space-between;
 `;

+ 15 - 14
dashboard/src/main/home/app-dashboard/create-app/RepoSettings.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useState } from "react";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
 import { useQuery } from "@tanstack/react-query";
 import api from "shared/api";
 import { Controller, useFormContext } from "react-hook-form";
@@ -8,7 +8,7 @@ import styled from "styled-components";
 import Input from "components/porter/Input";
 import { ControlledInput } from "components/porter/ControlledInput";
 import Select from "components/porter/Select";
-import AnimateHeight from "react-animate-height";
+import AnimateHeight, { Height } from "react-animate-height";
 import { z } from "zod";
 import { PorterAppFormData, SourceOptions } from "lib/porter-apps";
 import RepositorySelector from "../build-settings/RepositorySelector";
@@ -17,6 +17,9 @@ import BuildpackSettings, { DEFAULT_BUILDERS } from "../validate-apply/build-set
 import { match } from "ts-pattern";
 import { BuildOptions } from "lib/porter-apps/build";
 import Loading from "components/Loading";
+import DockerfileSettings from "../validate-apply/build-settings/docker/DockerfileSettings";
+import useResizeObserver from "lib/hooks/useResizeObserver";
+import CollapsibleContainer from "components/porter/CollapsibleContainer";
 
 type Props = {
   projectId: number;
@@ -249,8 +252,10 @@ const RepoSettings: React.FC<Props> = ({
                   )}
                 </StyledAdvancedBuildSettings>
               }
-
-              <AnimateHeight height={showSettings ? "auto" : 0} duration={1000}>
+              <AnimateHeight
+                duration={500}
+                height={showSettings ? "auto" : 0}
+              >
                 <StyledSourceBox>
                   <Controller
                     name="app.build.method"
@@ -282,17 +287,13 @@ const RepoSettings: React.FC<Props> = ({
                     .with({ method: "docker" }, () => (
                       <>
                         <Spacer y={0.5} />
-                        <Text color="helper">
-                          Dockerfile path (absolute path)
-                        </Text>
-                        <Spacer y={0.5} />
-                        <ControlledInput
-                          width="300px"
-                          placeholder="ex: ./Dockerfile"
-                          type="text"
-                          {...register("app.build.dockerfile")}
+                        <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}
                         />
-                        <Spacer y={0.5} />
                       </>
                     ))
                     .with({ method: "pack" }, (b) => (

+ 4 - 5
dashboard/src/main/home/app-dashboard/image-settings/ImageSettings.tsx

@@ -77,11 +77,10 @@ const ImageSettings: React.FC<Props> = ({
         <div>
             <Text size={16}>Image settings</Text>
             <Spacer y={0.5} />
-            <Text color="helper">Specify your image URL.</Text>
-            <Spacer y={0.5} />
-
             {!imageUri && (
                 <>
+                    <Text color="helper">Specify your image URL.</Text>
+                    <Spacer y={0.5} />
                     <ExpandedWrapper>
                         <ImageList
                             setSelectedImage={(image: ImageType) => {
@@ -115,10 +114,10 @@ const ImageSettings: React.FC<Props> = ({
                         Select image URL
                     </BackButton>
                     <Spacer y={1} />
-                    <Text color="helper">Specify your image tag.</Text>
-                    <Spacer y={0.5} />
                     {!imageTag && (
                         <>
+                            <Text color="helper">Specify your image tag.</Text>
+                            <Spacer y={0.5} />
                             <ExpandedWrapper>
                                 <TagList
                                     selectedImage={selectedImage}

+ 180 - 0
dashboard/src/main/home/app-dashboard/validate-apply/build-settings/FileSelector.tsx

@@ -0,0 +1,180 @@
+import { useGithubContents } from "lib/hooks/useGithubContents";
+import React, { useState } from "react";
+import styled from "styled-components";
+import file from "assets/file_v2.svg";
+import folder from "assets/folder_v2.svg";
+import file_branch from "assets/file-branch.svg";
+import { match } from "ts-pattern";
+import Loading from "components/Loading";
+
+type Props = {
+    repoId: number;
+    repoOwner: string;
+    repoName: string;
+    branch: string;
+    projectId: number;
+    onFileSelect: (path: string) => void;
+    isFileSelectable?: (path: string) => boolean;
+    widthPixels?: number;
+    heightPixels?: number;
+    headerText: string;
+}
+const FileSelector: React.FC<Props> = ({
+    repoId,
+    repoOwner,
+    repoName,
+    branch,
+    projectId,
+    onFileSelect,
+    isFileSelectable = () => true,
+    widthPixels = 500,
+    heightPixels = 275,
+    headerText,
+}) => {
+    const [ path, setPath ] = useState<string>("./");
+    const { contents, isLoading } = useGithubContents({
+        repoId,
+        repoOwner,
+        repoName,
+        branch,
+        path,
+        projectId,
+    })
+
+    return (
+        <div>
+            <StyledFileSelector widthPixels={widthPixels} heightPixels={heightPixels}>
+                {isLoading ? (
+                    <Loading />
+                ) : (
+                    <>
+                    {path !== "./" && path !== "" ? (
+                    <Item
+                        onClick={() => {
+                            const parentPath = path.split("/").slice(0, -2).join("/") + "/";
+                            setPath(parentPath);
+                        }}
+                        isHeaderItem={false}
+                    >
+                        <img src={folder} />
+                        ..
+                    </Item>
+                ) : (
+                    <Item
+                        onClick={() => {}}
+                        isHeaderItem
+                    >
+                        <img src={file_branch} />
+                        {headerText}
+                    </Item>
+                )}
+                {contents.map((content, i) => 
+                    {
+                        // this is the path in the scope of the current directory
+                        // e.g. if the path is ./foo/bar, then the relative path is bar
+                        const relativePath = content.path.split("/").slice(-1)[0];
+                        const isSelectable = isFileSelectable(relativePath);
+                        return match(content)
+                            .with({ type: "file" }, (content) => (
+                                <FileItem 
+                                    key={i} 
+                                    onClick={() => {
+                                        if (isSelectable) {
+                                            onFileSelect(content.path);
+                                        }
+                                    }}
+                                    isFileSelectable={isSelectable}
+                                    isHeaderItem={false}
+                                >
+                                    <img src={file} />
+                                    {relativePath}
+                                </FileItem>
+                            ))
+                            .with({ type: "dir" }, (content) => (
+                                <Item 
+                                    key={i} 
+                                    onClick={() => setPath(`${path}${relativePath}/`)}
+                                    isHeaderItem={false}
+                                >
+                                    <img src={folder} />
+                                    {relativePath}
+                                </Item>
+                            ))
+                        .exhaustive();
+                    }
+                )}
+                    </>
+                )}
+            </StyledFileSelector>
+        </div>
+    );
+};
+
+export default FileSelector;
+
+const StyledFileSelector = styled.div<{ widthPixels: number, heightPixels: number }>`
+  margin-top: 10px;
+  border-radius: 3px;
+  border: 1px solid #ffffff44;
+  max-height: 275px;
+  overflow-y: auto;
+  width: ${({ widthPixels }) => widthPixels}px;
+  height: ${({ heightPixels }) => heightPixels}px;
+`;
+
+const Item = styled.div`
+  display: flex;
+  width: 100%;
+  font-size: 13px;
+  border: 1px solid #494b4f;
+  color: #ffffff;
+  user-select: none;
+  align-items: center;
+  padding: 10px 0px;
+  cursor: ${(props: { isHeaderItem: boolean }) =>
+    props.isHeaderItem ? "default" : "pointer"};
+  background:  ${(props) =>
+    props.isHeaderItem ?  `${props.theme.fg2}` : `${props.theme.clickable.bg}`};
+  :hover {
+    border: ${(props) =>
+        props.isHeaderItem ? `1px solid #494b4f` : `1px solid #7a7b80`};
+  }
+
+  > img {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+  }
+
+  animation: slideIn 0.5s 0s;
+  animation-fill-mode: forwards;
+  @keyframes slideIn {
+    from {
+      margin-left: -10px;
+      opacity: 0;
+      margin-right: 10px;
+    }
+    to {
+      margin-left: 0;
+      opacity: 1;
+      margin-right: 0;
+    }
+  }
+`;
+
+const FileItem = styled(Item)`
+  cursor: ${(props: { isFileSelectable: boolean }) =>
+    props.isFileSelectable ? "pointer" : "default"};
+  color: ${(props: { isFileSelectable: boolean }) =>
+    props.isFileSelectable ? "#ffffff" : "#ffffff55"};
+  :hover {
+    border: ${(props) =>
+        props.isFileSelectable ?  `1px solid #7a7b80`: `1px solid #494b4f`};
+  }
+
+  img {
+    opacity: ${(props: { isFileSelectable: boolean }) =>
+        props.isFileSelectable ? "1" : "0.5"} !important;
+  }
+`;

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

@@ -0,0 +1,93 @@
+import React, { useState } from 'react';
+import Text from "components/porter/Text";
+import Spacer from 'components/porter/Spacer';
+import { Controller, useFormContext } from 'react-hook-form';
+import { PorterAppFormData } from 'lib/porter-apps';
+import Input from 'components/porter/Input';
+import FileSelector from '../FileSelector';
+import styled from 'styled-components';
+import CollapsibleContainer from 'components/porter/CollapsibleContainer';
+import Button from 'components/porter/Button';
+
+type Props = {
+    projectId: number;
+    repoId: number;
+    repoOwner: string;
+    repoName: string;
+    branch: string;
+}
+const DockerfileSettings: React.FC<Props> = ({
+    projectId,
+    repoId,
+    repoOwner,
+    repoName,
+    branch,
+}) => {
+    const { control, watch } = useFormContext<PorterAppFormData>();
+    const [showFileSelector, setShowFileSelector] = useState<boolean>(false);
+
+    const path = watch("app.build.dockerfile", "")
+
+    return (
+        <Controller
+            name="app.build.dockerfile"
+            control={control}
+            render={({ field: { onChange } }) => (
+                <div>
+                    <Text>
+                        Dockerfile path
+                    </Text>
+                    <Spacer y={0.5} />
+                    <InputWrapper
+                        onClick={(e) => {
+                            e.stopPropagation();
+                            if (!showFileSelector) {
+                                onChange("");
+                                setShowFileSelector(true);
+                            }
+                        }}
+                    >
+                        <Input
+                            width="300px"
+                            placeholder="ex: ./Dockerfile"
+                            value={path}
+                            setValue={() => ({})}
+                            hideCursor={true}
+                        />
+                        {showFileSelector &&
+                            <Button
+                                disabled={path === ""}
+                                onClick={() => setShowFileSelector(false)}
+                                color="#b91133"
+                            >
+                                Close
+                            </Button>
+                        }
+                    </InputWrapper>
+                    {showFileSelector && <div>
+                        <Spacer y={0.5} />
+                        <FileSelector 
+                            projectId={projectId} 
+                            repoId={repoId} 
+                            repoOwner={repoOwner} 
+                            repoName={repoName} 
+                            branch={branch}
+                            onFileSelect={(path: string) => onChange(`./${path}`)} 
+                            isFileSelectable={(path: string) => path.includes("Dockerfile")}
+                            headerText={"Select your Dockerfile:"}
+                        />
+                    </div>}
+                </div>
+            )}
+        />
+    );
+};
+
+export default DockerfileSettings;
+
+const InputWrapper = styled.div`
+    margin-bottom: -7px;
+    width: 500px;
+    display: flex;
+    justify-content: space-between;
+`;

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.