mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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>>;
|
||||
|
||||
Reference in New Issue
Block a user