Skip to content

Commit a418467

Browse files
committed
Show stack files
1 parent 3957240 commit a418467

File tree

11 files changed

+171
-206
lines changed

11 files changed

+171
-206
lines changed

site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ loader.config({ monaco });
77

88
interface SyntaxHighlighterProps {
99
value: string;
10-
language: string;
10+
language?: string;
1111
editorProps?: ComponentProps<typeof Editor> &
1212
ComponentProps<typeof DiffEditor>;
1313
compareWith?: string;

site/src/pages/TemplateVersionEditorPage/FileTreeView.tsx renamed to site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ type ContextMenu = {
2828
clientY: number;
2929
};
3030

31-
interface FileTreeViewProps {
31+
interface TemplateFilesTreeProps {
3232
onSelect: (path: string) => void;
3333
onDelete: (path: string) => void;
3434
onRename: (path: string) => void;
3535
fileTree: FileTree;
3636
activePath?: string;
3737
}
3838

39-
export const FileTreeView: FC<FileTreeViewProps> = ({
39+
export const TemplateFileTree: FC<TemplateFilesTreeProps> = ({
4040
fileTree,
4141
activePath,
4242
onDelete,

site/src/modules/templates/TemplateFiles/TemplateFiles.stories.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { action } from "@storybook/addon-actions";
21
import type { Meta, StoryObj } from "@storybook/react";
32
import { chromatic } from "testHelpers/chromatic";
43
import { TemplateFiles } from "./TemplateFiles";
@@ -19,7 +18,6 @@ const meta: Meta<typeof TemplateFiles> = {
1918
args: {
2019
currentFiles: exampleFiles,
2120
baseFiles: exampleFiles,
22-
tab: { value: "0", set: action("change tab") },
2321
},
2422
};
2523

Lines changed: 132 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,186 +1,173 @@
1-
import { type Interpolation, type Theme } from "@emotion/react";
2-
import { useEffect, type FC } from "react";
3-
import { DockerIcon } from "components/Icons/DockerIcon";
4-
import { MarkdownIcon } from "components/Icons/MarkdownIcon";
5-
import { TerraformIcon } from "components/Icons/TerraformIcon";
1+
import { useTheme, type Interpolation, type Theme } from "@emotion/react";
2+
import { type FC } from "react";
63
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter";
7-
import { UseTabResult, useTab } from "hooks/useTab";
84
import { TemplateVersionFiles } from "utils/templateVersion";
9-
import InsertDriveFileOutlined from "@mui/icons-material/InsertDriveFileOutlined";
10-
11-
const iconByExtension: Record<string, JSX.Element> = {
12-
tf: <TerraformIcon />,
13-
md: <MarkdownIcon />,
14-
mkd: <MarkdownIcon />,
15-
Dockerfile: <DockerIcon />,
16-
protobuf: <InsertDriveFileOutlined />,
17-
sh: <InsertDriveFileOutlined />,
18-
tpl: <InsertDriveFileOutlined />,
19-
};
20-
21-
const getExtension = (filename: string) => {
22-
if (filename.includes(".")) {
23-
const [_, extension] = filename.split(".");
24-
return extension;
25-
}
26-
27-
return filename;
28-
};
5+
import RadioButtonCheckedOutlined from "@mui/icons-material/RadioButtonCheckedOutlined";
6+
import { Pill } from "components/Pill/Pill";
7+
import { Link } from "react-router-dom";
298

309
const languageByExtension: Record<string, string> = {
3110
tf: "hcl",
11+
hcl: "hcl",
3212
md: "markdown",
3313
mkd: "markdown",
3414
Dockerfile: "dockerfile",
35-
sh: "bash",
15+
sh: "shell",
3616
tpl: "tpl",
3717
protobuf: "protobuf",
18+
nix: "dockerfile",
3819
};
39-
4020
interface TemplateFilesProps {
4121
currentFiles: TemplateVersionFiles;
4222
/**
4323
* Files used to compare with current files
4424
*/
4525
baseFiles?: TemplateVersionFiles;
46-
tab: UseTabResult;
4726
}
4827

4928
export const TemplateFiles: FC<TemplateFilesProps> = ({
5029
currentFiles,
5130
baseFiles,
52-
tab,
5331
}) => {
5432
const filenames = Object.keys(currentFiles);
55-
const selectedFilename = filenames[Number(tab.value)];
56-
const currentFile = currentFiles[selectedFilename];
57-
const previousFile = baseFiles && baseFiles[selectedFilename];
33+
const theme = useTheme();
34+
const filesWithDiff = filenames.filter(
35+
(filename) => fileInfo(filename).hasDiff,
36+
);
5837

59-
return (
60-
<div css={styles.files}>
61-
<div css={styles.tabs}>
62-
{filenames.map((filename, index) => {
63-
const tabValue = index.toString();
64-
const extension = getExtension(filename);
65-
const icon = iconByExtension[extension];
66-
const hasDiff =
67-
baseFiles &&
68-
baseFiles[filename] &&
69-
currentFiles[filename] !== baseFiles[filename];
38+
function fileInfo(filename: string) {
39+
const value = currentFiles[filename].trim();
40+
const previousValue = baseFiles ? baseFiles[filename].trim() : undefined;
41+
const hasDiff = previousValue && value !== previousValue;
7042

71-
return (
72-
<button
73-
css={[styles.tab, tabValue === tab.value && styles.tabActive]}
74-
onClick={() => {
75-
tab.set(tabValue);
76-
}}
77-
key={filename}
78-
>
79-
{icon}
80-
{filename}
81-
{hasDiff && <div css={styles.tabDiff} />}
82-
</button>
83-
);
84-
})}
85-
</div>
43+
return {
44+
value,
45+
previousValue,
46+
hasDiff,
47+
};
48+
}
8649

87-
<SyntaxHighlighter
88-
value={currentFile}
89-
compareWith={previousFile}
90-
language={languageByExtension[getExtension(selectedFilename)]}
91-
/>
50+
return (
51+
<div>
52+
{filesWithDiff.length > 0 && (
53+
<div
54+
css={{
55+
display: "flex",
56+
alignItems: "center",
57+
gap: 16,
58+
marginBottom: 24,
59+
}}
60+
>
61+
<span
62+
css={(theme) => ({
63+
fontSize: 13,
64+
fontWeight: 500,
65+
color: theme.roles.warning.fill.outline,
66+
})}
67+
>
68+
{filesWithDiff.length} files have changes
69+
</span>
70+
<ul
71+
css={{
72+
listStyle: "none",
73+
margin: 0,
74+
padding: 0,
75+
display: "flex",
76+
alignItems: "center",
77+
gap: 4,
78+
}}
79+
>
80+
{filesWithDiff.map((filename) => (
81+
<li key={filename}>
82+
<a
83+
href={`#${encodeURIComponent(filename)}`}
84+
css={{
85+
textDecoration: "none",
86+
color: theme.roles.warning.fill.text,
87+
fontSize: 13,
88+
fontWeight: 500,
89+
backgroundColor: theme.roles.warning.background,
90+
display: "inline-block",
91+
padding: "0 8px",
92+
borderRadius: 4,
93+
border: `1px solid ${theme.roles.warning.fill.solid}`,
94+
lineHeight: "1.6",
95+
}}
96+
>
97+
{filename}
98+
</a>
99+
</li>
100+
))}
101+
</ul>
102+
</div>
103+
)}
104+
<div css={styles.files}>
105+
{[...filenames]
106+
.sort((a, b) => a.localeCompare(b))
107+
.map((filename) => {
108+
const info = fileInfo(filename);
109+
110+
return (
111+
<div key={filename} css={styles.filePanel} id={filename}>
112+
<header css={styles.fileHeader}>
113+
{filename}
114+
{info.hasDiff && (
115+
<RadioButtonCheckedOutlined
116+
css={{
117+
width: 14,
118+
height: 14,
119+
color: theme.roles.warning.fill.outline,
120+
}}
121+
/>
122+
)}
123+
</header>
124+
<SyntaxHighlighter
125+
language={
126+
languageByExtension[filename.split(".").pop() ?? ""]
127+
}
128+
value={info.value}
129+
compareWith={info.previousValue}
130+
editorProps={{
131+
// 18 is the editor line height
132+
height: Math.min(numberOfLines(info.value) * 18, 560),
133+
onMount: (editor) => {
134+
editor.updateOptions({
135+
scrollBeyondLastLine: false,
136+
});
137+
},
138+
}}
139+
/>
140+
</div>
141+
);
142+
})}
143+
</div>
92144
</div>
93145
);
94146
};
95147

96-
export const useFileTab = (templateFiles: TemplateVersionFiles | undefined) => {
97-
// Tabs The default tab is the tab that has main.tf but until we loads the
98-
// files and check if main.tf exists we don't know which tab is the default
99-
// one so we just use empty string
100-
const tab = useTab("file", "");
101-
const isLoaded = tab.value !== "";
102-
useEffect(() => {
103-
if (templateFiles && !isLoaded) {
104-
const terraformFileIndex = Object.keys(templateFiles).indexOf("main.tf");
105-
// If main.tf exists use the index if not just use the first tab
106-
tab.set(terraformFileIndex !== -1 ? terraformFileIndex.toString() : "0");
107-
}
108-
}, [isLoaded, tab, templateFiles]);
109-
110-
return {
111-
...tab,
112-
isLoaded,
113-
};
148+
const numberOfLines = (content: string) => {
149+
return content.split("\n").length;
114150
};
115151

116152
const styles = {
117-
tabs: (theme) => ({
118-
display: "flex",
119-
alignItems: "baseline",
120-
borderBottom: `1px solid ${theme.palette.divider}`,
121-
gap: 1,
122-
overflowX: "auto",
123-
}),
124-
125-
tab: (theme) => ({
126-
background: "transparent",
127-
border: 0,
128-
padding: "0 24px",
153+
files: {
129154
display: "flex",
130-
alignItems: "center",
131-
height: 48,
132-
opacity: 0.85,
133-
cursor: "pointer",
134-
gap: 4,
135-
position: "relative",
136-
color: theme.palette.text.secondary,
137-
whiteSpace: "nowrap",
138-
139-
"& svg": {
140-
width: 22,
141-
maxHeight: 16,
142-
},
143-
144-
"&:hover": {
145-
backgroundColor: theme.palette.action.hover,
146-
},
147-
}),
148-
149-
tabActive: (theme) => ({
150-
opacity: 1,
151-
background: theme.palette.action.hover,
152-
color: theme.palette.text.primary,
153-
154-
"&:after": {
155-
content: '""',
156-
display: "block",
157-
height: 1,
158-
width: "100%",
159-
bottom: 0,
160-
left: 0,
161-
backgroundColor: theme.palette.primary.main,
162-
position: "absolute",
163-
},
164-
}),
165-
166-
tabDiff: (theme) => ({
167-
height: 6,
168-
width: 6,
169-
backgroundColor: theme.palette.warning.light,
170-
borderRadius: "100%",
171-
marginLeft: 4,
172-
}),
173-
174-
codeWrapper: (theme) => ({
175-
background: theme.palette.background.paper,
176-
}),
155+
flexDirection: "column",
156+
gap: 16,
157+
},
177158

178-
files: (theme) => ({
159+
filePanel: (theme) => ({
179160
borderRadius: 8,
180161
border: `1px solid ${theme.palette.divider}`,
181162
}),
182163

183-
prism: {
184-
borderRadius: 0,
185-
},
164+
fileHeader: (theme) => ({
165+
padding: "8px 16px",
166+
borderBottom: `1px solid ${theme.palette.divider}`,
167+
fontSize: 13,
168+
fontWeight: 500,
169+
display: "flex",
170+
gap: 8,
171+
alignItems: "center",
172+
}),
186173
} satisfies Record<string, Interpolation<Theme>>;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export function isBinaryData(s: string): boolean {
2+
// Remove unicode characters from the string like emojis.
3+
const asciiString = s.replace(/[\u007F-\uFFFF]/g, "");
4+
5+
// Create a set of all printable ASCII characters (and some control characters).
6+
const textChars = new Set(
7+
[7, 8, 9, 10, 12, 13, 27].concat(
8+
Array.from({ length: 128 }, (_, i) => i + 32),
9+
),
10+
);
11+
12+
const isBinaryString = (str: string): boolean => {
13+
for (let i = 0; i < str.length; i++) {
14+
if (!textChars.has(str.charCodeAt(i))) {
15+
return true;
16+
}
17+
}
18+
return false;
19+
};
20+
21+
return isBinaryString(asciiString);
22+
}

0 commit comments

Comments
 (0)