Skip to content

feat(site): allow any file extension on template editor #12000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { type ComponentProps, type FC } from "react";
import Editor, { DiffEditor, loader } from "@monaco-editor/react";
import * as monaco from "monaco-editor";
import { useCoderTheme } from "./coderTheme";
import { useTheme } from "@emotion/react";

loader.config({ monaco });

interface SyntaxHighlighterProps {
value: string;
language: string;
language?: string;
editorProps?: ComponentProps<typeof Editor> &
ComponentProps<typeof DiffEditor>;
compareWith?: string;
Expand All @@ -20,6 +21,7 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
editorProps,
}) => {
const hasDiff = compareWith && value !== compareWith;
const theme = useTheme();
const coderTheme = useCoderTheme();
const commonProps = {
language,
Expand All @@ -45,6 +47,7 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
css={{
padding: "8px 0",
height: "100%",
backgroundColor: theme.monaco.colors["editor.background"],
}}
>
{hasDiff ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import { TemplateFileTree } from "./TemplateFileTree";
import { FileTree } from "utils/filetree";
import { useTheme } from "@emotion/react";

const fileTree: FileTree = {
"main.tf": "resource aws_instance my_instance {}",
"variables.tf": "variable my_var {}",
"outputs.tf": "output my_output {}",
folder: {
"nested.tf": "resource aws_instance my_instance {}",
},
};

const meta: Meta<typeof TemplateFileTree> = {
title: "modules/templates/TemplateFileTree",
parameters: { chromatic },
component: TemplateFileTree,
args: {
fileTree,
activePath: "main.tf",
},
decorators: [
(Story) => {
const theme = useTheme();
return (
<div
css={{
maxWidth: 260,
borderRadius: 8,
border: `1px solid ${theme.palette.divider}`,
}}
>
<Story />
</div>
);
},
],
};

export default meta;
type Story = StoryObj<typeof TemplateFileTree>;

export const Example: Story = {};

export const NestedOpen: Story = {
args: {
activePath: "folder/nested.tf",
},
};

export const GroupEmptyFolders: Story = {
args: {
activePath: "folder/other-folder/another/nested.tf",
fileTree: {
"main.tf": "resource aws_instance my_instance {}",
"variables.tf": "variable my_var {}",
"outputs.tf": "output my_output {}",
folder: {
"other-folder": {
another: {
"nested.tf": "resource aws_instance my_instance {}",
},
},
},
},
},
};

export const GreyOutHiddenFiles: Story = {
args: {
fileTree: {
".vite": {
"config.json": "resource aws_instance my_instance {}",
},
".nextjs": {
"nested.tf": "resource aws_instance my_instance {}",
},
".terraform.lock.hcl": "{}",
"main.tf": "resource aws_instance my_instance {}",
"variables.tf": "variable my_var {}",
"outputs.tf": "output my_output {}",
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,59 @@ type ContextMenu = {
clientY: number;
};

interface FileTreeViewProps {
interface TemplateFilesTreeProps {
onSelect: (path: string) => void;
onDelete: (path: string) => void;
onRename: (path: string) => void;
onDelete?: (path: string) => void;
onRename?: (path: string) => void;
fileTree: FileTree;
activePath?: string;
Label?: FC<{
path: string;
filename: string;
label: string;
isFolder: boolean;
}>;
}

export const FileTreeView: FC<FileTreeViewProps> = ({
export const TemplateFileTree: FC<TemplateFilesTreeProps> = ({
fileTree,
activePath,
onDelete,
onRename,
onSelect,
Label,
}) => {
const [contextMenu, setContextMenu] = useState<ContextMenu | undefined>();

const isFolder = (content?: FileTree | string): content is FileTree =>
typeof content === "object";

const buildTreeItems = (
label: string,
filename: string,
content?: FileTree | string,
parentPath?: string,
): JSX.Element => {
const currentPath = parentPath ? `${parentPath}/${filename}` : filename;
const isFolder = typeof content === "object";
let icon: JSX.Element | null = isFolder ? null : (
// Used to group empty folders in one single label like VSCode does
const shouldGroupFolder =
isFolder(content) &&
Object.keys(content).length === 1 &&
isFolder(Object.values(content)[0]);
const isHiddenFile = currentPath.startsWith(".");

if (shouldGroupFolder) {
const firstChildFileName = Object.keys(content)[0];
const child = content[firstChildFileName];
return buildTreeItems(
`${label} / ${firstChildFileName}`,
firstChildFileName,
child,
currentPath,
);
}

let icon: JSX.Element | null = isFolder(content) ? null : (
<FormatAlignLeftOutlined />
);

Expand All @@ -69,26 +98,40 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
<TreeItem
nodeId={currentPath}
key={currentPath}
label={filename}
label={
Label ? (
<Label
path={currentPath}
label={label}
filename={filename}
isFolder={isFolder(content)}
/>
) : (
label
)
}
css={(theme) => css`
overflow: hidden;
user-select: none;

& > .MuiTreeItem-content {
padding: 2px 16px;
color: ${theme.palette.text.secondary};
color: ${isHiddenFile
? theme.palette.text.disabled
: theme.palette.text.secondary};
height: 32px;

& svg {
width: 12px;
height: 12px;
color: ${theme.palette.text.secondary};
color: currentColor;
}

& > .MuiTreeItem-label {
margin-left: 4px;
font-size: 13px;
color: inherit;
white-space: nowrap;
}

&.Mui-selected {
Expand All @@ -103,17 +146,22 @@ export const FileTreeView: FC<FileTreeViewProps> = ({

& .MuiTreeItem-group {
margin-left: 0;
position: relative;

// We need to find a better way to recursive padding here
& .MuiTreeItem-content {
padding-left: calc(var(--level) * 40px);
padding-left: calc(8px + (var(--level) + 1) * 8px);
}
}
`}
onClick={() => {
onSelect(currentPath);
}}
onContextMenu={(event) => {
const hasContextActions = onRename || onDelete;
if (!hasContextActions) {
return;
}
event.preventDefault(); // Avoid default browser behavior
event.stopPropagation(); // Avoid trigger parent context menu
setContextMenu(
Expand All @@ -133,12 +181,12 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
} as CSSProperties
}
>
{isFolder &&
{isFolder(content) &&
Object.keys(content)
.sort(sortFileTree(content))
.map((filename) => {
const child = content[filename];
return buildTreeItems(filename, child, currentPath);
return buildTreeItems(filename, filename, child, currentPath);
})}
</TreeItem>
);
Expand All @@ -149,13 +197,14 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
aria-label="Files"
defaultExpanded={activePath ? expandablePaths(activePath) : []}
defaultSelected={activePath}
>
{Object.keys(fileTree)
.sort(sortFileTree(fileTree))
.map((filename) => {
const child = fileTree[filename];
return buildTreeItems(filename, child);
return buildTreeItems(filename, filename, child);
})}

<Menu
Expand Down Expand Up @@ -184,7 +233,7 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
if (!contextMenu) {
return;
}
onRename(contextMenu.path);
onRename && onRename(contextMenu.path);
setContextMenu(undefined);
}}
>
Expand All @@ -195,7 +244,7 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
if (!contextMenu) {
return;
}
onDelete(contextMenu.path);
onDelete && onDelete(contextMenu.path);
setContextMenu(undefined);
}}
>
Expand Down Expand Up @@ -232,3 +281,12 @@ const FileTypeMarkdown: FC = () => (
<polygon points="22.955 20.636 18.864 16.136 21.591 16.136 21.591 11.364 24.318 11.364 24.318 16.136 27.045 16.136 22.955 20.636" />
</svg>
);

const expandablePaths = (path: string) => {
const paths = path.split("/");
const result = [];
for (let i = 1; i < paths.length; i++) {
result.push(paths.slice(0, i).join("/"));
}
return result;
};
12 changes: 10 additions & 2 deletions site/src/modules/templates/TemplateFiles/TemplateFiles.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { action } from "@storybook/addon-actions";
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import { TemplateFiles } from "./TemplateFiles";
Expand All @@ -19,7 +18,6 @@ const meta: Meta<typeof TemplateFiles> = {
args: {
currentFiles: exampleFiles,
baseFiles: exampleFiles,
tab: { value: "0", set: action("change tab") },
},
};

Expand All @@ -28,4 +26,14 @@ type Story = StoryObj<typeof TemplateFiles>;

const Example: Story = {};

export const WithDiff: Story = {
args: {
currentFiles: {
...exampleFiles,
"main.tf": `${exampleFiles["main.tf"]} - with changes`,
},
baseFiles: exampleFiles,
},
};

export { Example as TemplateFiles };
Loading