feat: implement file icons in <TemplateFiles /> (#22369)

This pull-request takes our icons shown in the sidebar tree and shows
them alongside the names of the files in the `Source Code` page of our
templates.

Also does a quick de-mui of this page.

<img width="637" height="345" alt="image"
src="https://github.com/user-attachments/assets/f3013eb6-9572-4d05-a683-10bb99b4e802"
/>
This commit is contained in:
Jake Howell
2026-02-27 22:53:09 +11:00
committed by GitHub
parent c6638f5547
commit d204e6fb84
3 changed files with 112 additions and 139 deletions
@@ -0,0 +1,77 @@
import { DockerIcon } from "components/Icons/DockerIcon";
import {
BracesIcon,
FileCodeIcon,
FileIcon,
FolderIcon,
TerminalIcon,
} from "lucide-react";
import type { ComponentProps, ElementType, FC } from "react";
const FileTypeTerraform: FC<ComponentProps<"svg">> = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
fill="#813cf3"
{...props}
>
<title>file_type_terraform</title>
<polygon points="12.042 6.858 20.071 11.448 20.071 20.462 12.042 15.868 12.042 6.858 12.042 6.858" />
<polygon points="20.5 20.415 28.459 15.84 28.459 6.887 20.5 11.429 20.5 20.415 20.5 20.415" />
<polygon points="3.541 11.01 11.571 15.599 11.571 6.59 3.541 2 3.541 11.01 3.541 11.01" />
<polygon points="12.042 25.41 20.071 30 20.071 20.957 12.042 16.368 12.042 25.41 12.042 25.41" />
</svg>
);
const FileTypeMarkdown: FC<ComponentProps<"svg">> = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
fill="#755838"
role="img"
aria-label="Markdown icon"
{...props}
>
<rect
x="2.5"
y="7.955"
width="27"
height="16.091"
style={{
fill: "none",
stroke: "#755838",
}}
/>
<polygon points="5.909 20.636 5.909 11.364 8.636 11.364 11.364 14.773 14.091 11.364 16.818 11.364 16.818 20.636 14.091 20.636 14.091 15.318 11.364 18.727 8.636 15.318 8.636 20.636 5.909 20.636" />
<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>
);
export const getTemplateFileIcon = (
filename: string,
isFolder: boolean,
): ElementType => {
if (isFolder) {
return FolderIcon;
}
if (filename.endsWith(".tf")) {
return FileTypeTerraform;
}
if (filename.endsWith(".md")) {
return FileTypeMarkdown;
}
if (filename.endsWith("Dockerfile")) {
return DockerIcon;
}
if (filename.endsWith(".sh")) {
return TerminalIcon;
}
if (filename.endsWith(".json")) {
return BracesIcon;
}
if (filename.endsWith(".yaml") || filename.endsWith(".yml")) {
return FileCodeIcon;
}
// Default icon for files without a specific icon.
return FileIcon;
};
@@ -2,24 +2,10 @@ import { css } from "@emotion/react";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { SimpleTreeView, TreeItem } from "@mui/x-tree-view";
import { DockerIcon } from "components/Icons/DockerIcon";
import {
BracesIcon,
ChevronDownIcon,
ChevronRightIcon,
FileCodeIcon,
FileIcon,
FolderIcon,
TerminalIcon,
} from "lucide-react";
import {
type CSSProperties,
type ElementType,
type FC,
type JSX,
useState,
} from "react";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import { type CSSProperties, type FC, type JSX, useState } from "react";
import type { FileTree } from "utils/filetree";
import { getTemplateFileIcon } from "./TemplateFileIcon";
const isFolder = (content?: FileTree | string): content is FileTree =>
typeof content === "object";
@@ -94,29 +80,11 @@ export const TemplateFileTree: FC<TemplateFilesTreeProps> = ({
);
}
let icon: ElementType | undefined;
if (isFolder(content)) {
icon = FolderIcon;
} else if (filename.endsWith(".tf")) {
icon = FileTypeTerraform;
} else if (filename.endsWith(".md")) {
icon = FileTypeMarkdown;
} else if (filename.endsWith("Dockerfile")) {
icon = DockerIcon;
} else if (filename.endsWith(".sh")) {
icon = TerminalIcon;
} else if (filename.endsWith(".json")) {
icon = BracesIcon;
} else if (filename.endsWith(".yaml") || filename.endsWith(".yml")) {
icon = FileCodeIcon;
} else {
// Default icon for files without a specific icon.
icon = FileIcon;
}
const templateFileIcon = getTemplateFileIcon(filename, isFolder(content));
return (
<TreeItem
slots={{ icon }}
slots={{ icon: templateFileIcon }}
itemId={currentPath}
key={currentPath}
label={
@@ -273,39 +241,6 @@ export const TemplateFileTree: FC<TemplateFilesTreeProps> = ({
);
};
const FileTypeTerraform: FC = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#813cf3">
<title>file_type_terraform</title>
<polygon points="12.042 6.858 20.071 11.448 20.071 20.462 12.042 15.868 12.042 6.858 12.042 6.858" />
<polygon points="20.5 20.415 28.459 15.84 28.459 6.887 20.5 11.429 20.5 20.415 20.5 20.415" />
<polygon points="3.541 11.01 11.571 15.599 11.571 6.59 3.541 2 3.541 11.01 3.541 11.01" />
<polygon points="12.042 25.41 20.071 30 20.071 20.957 12.042 16.368 12.042 25.41 12.042 25.41" />
</svg>
);
const FileTypeMarkdown: FC = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
fill="#755838"
role="img"
aria-label="Markdown icon"
>
<rect
x="2.5"
y="7.955"
width="27"
height="16.091"
style={{
fill: "none",
stroke: "#755838",
}}
/>
<polygon points="5.909 20.636 5.909 11.364 8.636 11.364 11.364 14.773 14.091 11.364 16.818 11.364 16.818 20.636 14.091 20.636 14.091 15.318 11.364 18.727 8.636 15.318 8.636 20.636 5.909 20.636" />
<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 = [];
@@ -1,13 +1,13 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter";
import set from "lodash/set";
import { EditIcon } from "lucide-react";
import { linkToTemplate, useLinks } from "modules/navigation";
import { type FC, useCallback, useMemo } from "react";
import { Link } from "react-router";
import { Link as RouterLink } from "react-router";
import { cn } from "utils/cn";
import type { FileTree } from "utils/filetree";
import type { TemplateVersionFiles } from "utils/templateVersion";
import { getTemplateFileIcon } from "./TemplateFileIcon";
import { TemplateFileTree } from "./TemplateFileTree";
interface TemplateFilesProps {
@@ -29,7 +29,6 @@ export const TemplateFiles: FC<TemplateFilesProps> = ({
baseFiles,
}) => {
const getLink = useLinks();
const theme = useTheme();
const fileInfo = useCallback(
(filename: string) => {
@@ -61,8 +60,8 @@ export const TemplateFiles: FC<TemplateFilesProps> = ({
return (
<div>
<div css={{ display: "flex", alignItems: "flex-start", gap: 32 }}>
<div css={styles.sidebar}>
<div className="flex items-start gap-8">
<div className="sticky top-8 w-[240px] shrink-0 overflow-auto rounded-lg border border-solid border-surface-quaternary py-1">
<TemplateFileTree
fileTree={fileTree}
onSelect={(path: string) => {
@@ -75,9 +74,7 @@ export const TemplateFiles: FC<TemplateFilesProps> = ({
const hasDiff = fileInfo(path).hasDiff;
return (
<span
css={hasDiff && { color: theme.roles.warning.fill.outline }}
>
<span className={cn(hasDiff && "text-content-warning")}>
{filename}
</span>
);
@@ -85,42 +82,42 @@ export const TemplateFiles: FC<TemplateFilesProps> = ({
/>
</div>
<div css={styles.files} data-testid="template-files-content">
<div
className="flex flex-1 flex-col gap-4"
data-testid="template-files-content"
>
{Object.keys(currentFiles)
.sort((a, b) => a.localeCompare(b))
.map((filename) => {
const TemplateFileIcon = getTemplateFileIcon(filename, false);
const info = fileInfo(filename);
return (
<div key={filename} css={styles.filePanel} id={filename}>
<header css={styles.fileHeader}>
<span
className={cn({
"text-content-warning": info.hasDiff,
})}
>
{filename}
</span>
<div
key={filename}
id={filename}
className="overflow-hidden rounded-lg border border-solid border-surface-quaternary"
>
<header className="flex items-center gap-2 border-0 border-b border-solid border-surface-quaternary px-4 py-2 text-[13px] font-medium">
<div className="flex items-center gap-2">
<TemplateFileIcon className="text-content-secondary size-icon-xs" />
<span
className={cn({
"text-content-warning": info.hasDiff,
})}
>
{filename}
</span>
</div>
<div css={{ marginLeft: "auto" }}>
<Link
<div className="ml-auto">
<RouterLink
to={`${versionLink}/edit?path=${filename}`}
css={{
display: "flex",
gap: 4,
alignItems: "center",
fontSize: 14,
color: theme.palette.text.secondary,
textDecoration: "none",
"&:hover": {
color: theme.palette.text.primary,
},
}}
className="flex items-center gap-1 text-sm no-underline text-content-secondary hover:text-content-primary"
>
<EditIcon className="text-inherit size-icon-xs" />
Edit
</Link>
</RouterLink>
</div>
</header>
<SyntaxHighlighter
@@ -170,39 +167,3 @@ const getLanguage = (filename: string) => {
const numberOfLines = (content: string) => {
return content.split("\n").length;
};
const styles = {
sidebar: (theme) => ({
width: 240,
flexShrink: 0,
borderRadius: 8,
overflow: "auto",
border: `1px solid ${theme.palette.divider}`,
padding: "4px 0",
position: "sticky",
top: 32,
}),
files: {
display: "flex",
flexDirection: "column",
gap: 16,
flex: 1,
},
filePanel: (theme) => ({
borderRadius: 8,
border: `1px solid ${theme.palette.divider}`,
overflow: "hidden",
}),
fileHeader: (theme) => ({
padding: "8px 16px",
borderBottom: `1px solid ${theme.palette.divider}`,
fontSize: 13,
fontWeight: 500,
display: "flex",
gap: 8,
alignItems: "center",
}),
} satisfies Record<string, Interpolation<Theme>>;