diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index 07b1485eef770..684272503d01a 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -22,9 +22,12 @@ import { waitForLoaderToBeRemoved, } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; +import type { FileTree } from "utils/filetree"; import type { MonacoEditorProps } from "./MonacoEditor"; import { Language } from "./PublishTemplateVersionDialog"; -import TemplateVersionEditorPage from "./TemplateVersionEditorPage"; +import TemplateVersionEditorPage, { + findEntrypointFile, +} from "./TemplateVersionEditorPage"; const { API } = apiModule; @@ -409,3 +412,127 @@ function renderEditorPage(queryClient: QueryClient) { , ); } + +describe("Find entrypoint", () => { + it("empty tree", () => { + const ft: FileTree = {}; + const mainFile = findEntrypointFile(ft); + expect(mainFile).toBeUndefined(); + }); + it("flat structure, main.tf in root", () => { + const ft: FileTree = { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + "nnn.tf": "foobaz", + }; + + const mainFile = findEntrypointFile(ft); + expect(mainFile).toBe("main.tf"); + }); + it("flat structure, no main.tf", () => { + const ft: FileTree = { + "aaa.tf": "hello", + "bbb.tf": "world", + "ccc.tf": "foobaz", + "nnn.tf": "foobaz", + }; + + const mainFile = findEntrypointFile(ft); + expect(mainFile).toBe("nnn.tf"); + }); + it("with dirs, single main.tf", () => { + const ft: FileTree = { + "aaa-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + }, + "bbb-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + }, + "main.tf": "foobar", + "nnn.tf": "foobaz", + }; + + const mainFile = findEntrypointFile(ft); + expect(mainFile).toBe("main.tf"); + }); + it("with dirs, multiple main.tf's", () => { + const ft: FileTree = { + "aaa-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + }, + "bbb-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + }, + "ccc-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + }, + "main.tf": "foobar", + "nnn.tf": "foobaz", + "zzz-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + }, + }; + + const mainFile = findEntrypointFile(ft); + expect(mainFile).toBe("main.tf"); + }); + it("with dirs, multiple main.tf, no main.tf in root", () => { + const ft: FileTree = { + "aaa-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + }, + "bbb-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + }, + "ccc-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + }, + "nnn.tf": "foobaz", + "zzz-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + }, + }; + + const mainFile = findEntrypointFile(ft); + expect(mainFile).toBe("aaa-dir/main.tf"); + }); + it("with dirs, multiple main.tf, unordered file tree", () => { + const ft: FileTree = { + "ccc-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + }, + "aaa-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + }, + "zzz-dir": { + "aaa.tf": "hello", + "bbb.tf": "world", + "main.tf": "foobar", + }, + }; + + const mainFile = findEntrypointFile(ft); + expect(mainFile).toBe("aaa-dir/main.tf"); + }); +}); diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index b3090eb6d3f47..0158c872aed50 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -90,7 +90,7 @@ export const TemplateVersionEditorPage: FC = () => { // File navigation // It can be undefined when a selected file is deleted const activePath: string | undefined = - searchParams.get("path") ?? findInitialFile(fileTree ?? {}); + searchParams.get("path") ?? findEntrypointFile(fileTree ?? {}); const onActivePathChange = (path: string | undefined) => { if (path) { searchParams.set("path", path); @@ -357,10 +357,33 @@ const publishVersion = async (options: { return Promise.all(publishActions); }; -const findInitialFile = (fileTree: FileTree): string | undefined => { +const defaultMainTerraformFile = "main.tf"; + +// findEntrypointFile function locates the entrypoint file to open in the Editor. +// It browses the filetree following these steps: +// 1. If "main.tf" exists in root, return it. +// 2. Traverse through sub-directories. +// 3. If "main.tf" exists in a sub-directory, skip further browsing, and return the path. +// 4. If "main.tf" was not found, return the last reviewed "".tf" file. +export const findEntrypointFile = (fileTree: FileTree): string | undefined => { let initialFile: string | undefined; - traverse(fileTree, (content, filename, path) => { + if (Object.keys(fileTree).find((key) => key === defaultMainTerraformFile)) { + return defaultMainTerraformFile; + } + + let skip = false; + traverse(fileTree, (_, filename, path) => { + if (skip) { + return; + } + + if (filename === defaultMainTerraformFile) { + initialFile = path; + skip = true; + return; + } + if (filename.endsWith(".tf")) { initialFile = path; } diff --git a/site/src/utils/filetree.test.ts b/site/src/utils/filetree.test.ts index 21746baa6a54c..e4aadaabbe424 100644 --- a/site/src/utils/filetree.test.ts +++ b/site/src/utils/filetree.test.ts @@ -122,6 +122,6 @@ test("traverse() go trough all the file tree files", () => { traverse(fileTree, (_content, _filename, fullPath) => { filePaths.push(fullPath); }); - const expectedFilePaths = ["main.tf", "images", "images/java.Dockerfile"]; + const expectedFilePaths = ["images", "images/java.Dockerfile", "main.tf"]; expect(filePaths).toEqual(expectedFilePaths); }); diff --git a/site/src/utils/filetree.ts b/site/src/utils/filetree.ts index 757ed133e55f7..2f7d8ea84533b 100644 --- a/site/src/utils/filetree.ts +++ b/site/src/utils/filetree.ts @@ -96,7 +96,9 @@ export const traverse = ( ) => void, parent?: string, ) => { - for (const [filename, content] of Object.entries(fileTree)) { + for (const [filename, content] of Object.entries(fileTree).sort(([a], [b]) => + a.localeCompare(b), + )) { const fullPath = parent ? `${parent}/${filename}` : filename; callback(content, filename, fullPath); if (typeof content === "object") {