|
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"; |
6 | 3 | import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter";
|
7 |
| -import { UseTabResult, useTab } from "hooks/useTab"; |
8 | 4 | 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"; |
29 | 8 |
|
30 | 9 | const languageByExtension: Record<string, string> = {
|
31 | 10 | tf: "hcl",
|
| 11 | + hcl: "hcl", |
32 | 12 | md: "markdown",
|
33 | 13 | mkd: "markdown",
|
34 | 14 | Dockerfile: "dockerfile",
|
35 |
| - sh: "bash", |
| 15 | + sh: "shell", |
36 | 16 | tpl: "tpl",
|
37 | 17 | protobuf: "protobuf",
|
| 18 | + nix: "dockerfile", |
38 | 19 | };
|
39 |
| - |
40 | 20 | interface TemplateFilesProps {
|
41 | 21 | currentFiles: TemplateVersionFiles;
|
42 | 22 | /**
|
43 | 23 | * Files used to compare with current files
|
44 | 24 | */
|
45 | 25 | baseFiles?: TemplateVersionFiles;
|
46 |
| - tab: UseTabResult; |
47 | 26 | }
|
48 | 27 |
|
49 | 28 | export const TemplateFiles: FC<TemplateFilesProps> = ({
|
50 | 29 | currentFiles,
|
51 | 30 | baseFiles,
|
52 |
| - tab, |
53 | 31 | }) => {
|
54 | 32 | 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 | + ); |
58 | 37 |
|
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; |
70 | 42 |
|
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 | + } |
86 | 49 |
|
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> |
92 | 144 | </div>
|
93 | 145 | );
|
94 | 146 | };
|
95 | 147 |
|
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; |
114 | 150 | };
|
115 | 151 |
|
116 | 152 | 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: { |
129 | 154 | 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 | + }, |
177 | 158 |
|
178 |
| - files: (theme) => ({ |
| 159 | + filePanel: (theme) => ({ |
179 | 160 | borderRadius: 8,
|
180 | 161 | border: `1px solid ${theme.palette.divider}`,
|
181 | 162 | }),
|
182 | 163 |
|
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 | + }), |
186 | 173 | } satisfies Record<string, Interpolation<Theme>>;
|
0 commit comments