Skip to content

Commit cf3f9a5

Browse files
feat: Add version toggle in TextViewer for default/alpine variants
- Add version selection toggle (Default/Alpine) in TextViewer component - Load both default and alpine versions of CT and install scripts - Display correct script content based on selected version - Pass script object to TextViewer to detect alpine variants - Show toggle buttons only when alpine variant exists
1 parent 6ad18e1 commit cf3f9a5

File tree

2 files changed

+197
-55
lines changed

2 files changed

+197
-55
lines changed

src/app/_components/ScriptDetailModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,7 @@ export function ScriptDetailModal({
829829
?.script?.split("/")
830830
.pop() ?? `${script.slug}.sh`
831831
}
832+
script={script}
832833
isOpen={textViewerOpen}
833834
onClose={() => setTextViewerOpen(false)}
834835
/>

src/app/_components/TextViewer.tsx

Lines changed: 196 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,85 +4,164 @@ import { useState, useEffect, useCallback } from 'react';
44
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
55
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';
66
import { Button } from './ui/button';
7+
import type { Script } from '../../types/script';
78

89
interface TextViewerProps {
910
scriptName: string;
1011
isOpen: boolean;
1112
onClose: () => void;
13+
script?: Script | null;
1214
}
1315

1416
interface ScriptContent {
1517
ctScript?: string;
1618
installScript?: string;
19+
alpineCtScript?: string;
20+
alpineInstallScript?: string;
1721
}
1822

19-
export function TextViewer({ scriptName, isOpen, onClose }: TextViewerProps) {
23+
export function TextViewer({ scriptName, isOpen, onClose, script }: TextViewerProps) {
2024
const [scriptContent, setScriptContent] = useState<ScriptContent>({});
2125
const [isLoading, setIsLoading] = useState(false);
2226
const [error, setError] = useState<string | null>(null);
2327
const [activeTab, setActiveTab] = useState<'ct' | 'install'>('ct');
28+
const [selectedVersion, setSelectedVersion] = useState<'default' | 'alpine'>('default');
2429

2530
// Extract slug from script name (remove .sh extension)
26-
const slug = scriptName.replace(/\.sh$/, '');
31+
const slug = scriptName.replace(/\.sh$/, '').replace(/^alpine-/, '');
32+
33+
// Check if alpine variant exists
34+
const hasAlpineVariant = script?.install_methods?.some(
35+
method => method.type === 'alpine' && method.script?.startsWith('ct/')
36+
);
37+
38+
// Get script names for default and alpine versions
39+
const defaultScriptName = scriptName.replace(/^alpine-/, '');
40+
const alpineScriptName = scriptName.startsWith('alpine-') ? scriptName : `alpine-${scriptName}`;
2741

2842
const loadScriptContent = useCallback(async () => {
2943
setIsLoading(true);
3044
setError(null);
3145

3246
try {
33-
// Try to load from different possible locations
34-
const [ctResponse, toolsResponse, vmResponse, vwResponse, installResponse] = await Promise.allSettled([
35-
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `ct/${scriptName}` } }))}`),
36-
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `tools/pve/${scriptName}` } }))}`),
37-
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `vm/${scriptName}` } }))}`),
38-
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `vw/${scriptName}` } }))}`),
47+
// Build fetch requests for default version
48+
const requests: Promise<Response>[] = [];
49+
50+
// Default CT script
51+
requests.push(
52+
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `ct/${defaultScriptName}` } }))}`)
53+
);
54+
55+
// Tools, VM, VW scripts
56+
requests.push(
57+
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `tools/pve/${defaultScriptName}` } }))}`)
58+
);
59+
requests.push(
60+
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `vm/${defaultScriptName}` } }))}`)
61+
);
62+
requests.push(
63+
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `vw/${defaultScriptName}` } }))}`)
64+
);
65+
66+
// Default install script
67+
requests.push(
3968
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `install/${slug}-install.sh` } }))}`)
40-
]);
69+
);
70+
71+
// Alpine versions if variant exists
72+
if (hasAlpineVariant) {
73+
requests.push(
74+
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `ct/${alpineScriptName}` } }))}`)
75+
);
76+
requests.push(
77+
fetch(`/api/trpc/scripts.getScriptContent?input=${encodeURIComponent(JSON.stringify({ json: { path: `install/alpine-${slug}-install.sh` } }))}`)
78+
);
79+
}
80+
81+
const responses = await Promise.allSettled(requests);
4182

4283
const content: ScriptContent = {};
84+
let responseIndex = 0;
4385

44-
if (ctResponse.status === 'fulfilled' && ctResponse.value.ok) {
86+
// Default CT script
87+
const ctResponse = responses[responseIndex];
88+
if (ctResponse?.status === 'fulfilled' && ctResponse.value.ok) {
4589
const ctData = await ctResponse.value.json() as { result?: { data?: { json?: { success?: boolean; content?: string } } } };
4690
if (ctData.result?.data?.json?.success) {
4791
content.ctScript = ctData.result.data.json.content;
4892
}
4993
}
5094

51-
if (toolsResponse.status === 'fulfilled' && toolsResponse.value.ok) {
95+
responseIndex++;
96+
// Tools script
97+
const toolsResponse = responses[responseIndex];
98+
if (toolsResponse?.status === 'fulfilled' && toolsResponse.value.ok) {
5299
const toolsData = await toolsResponse.value.json() as { result?: { data?: { json?: { success?: boolean; content?: string } } } };
53100
if (toolsData.result?.data?.json?.success) {
54101
content.ctScript = toolsData.result.data.json.content; // Use ctScript field for tools scripts too
55102
}
56103
}
57104

58-
if (vmResponse.status === 'fulfilled' && vmResponse.value.ok) {
105+
responseIndex++;
106+
// VM script
107+
const vmResponse = responses[responseIndex];
108+
if (vmResponse?.status === 'fulfilled' && vmResponse.value.ok) {
59109
const vmData = await vmResponse.value.json() as { result?: { data?: { json?: { success?: boolean; content?: string } } } };
60110
if (vmData.result?.data?.json?.success) {
61111
content.ctScript = vmData.result.data.json.content; // Use ctScript field for VM scripts too
62112
}
63113
}
64114

65-
if (vwResponse.status === 'fulfilled' && vwResponse.value.ok) {
115+
responseIndex++;
116+
// VW script
117+
const vwResponse = responses[responseIndex];
118+
if (vwResponse?.status === 'fulfilled' && vwResponse.value.ok) {
66119
const vwData = await vwResponse.value.json() as { result?: { data?: { json?: { success?: boolean; content?: string } } } };
67120
if (vwData.result?.data?.json?.success) {
68121
content.ctScript = vwData.result.data.json.content; // Use ctScript field for VW scripts too
69122
}
70123
}
71124

72-
if (installResponse.status === 'fulfilled' && installResponse.value.ok) {
125+
responseIndex++;
126+
// Default install script
127+
const installResponse = responses[responseIndex];
128+
if (installResponse?.status === 'fulfilled' && installResponse.value.ok) {
73129
const installData = await installResponse.value.json() as { result?: { data?: { json?: { success?: boolean; content?: string } } } };
74130
if (installData.result?.data?.json?.success) {
75131
content.installScript = installData.result.data.json.content;
76132
}
77133
}
134+
responseIndex++;
135+
// Alpine CT script
136+
if (hasAlpineVariant) {
137+
const alpineCtResponse = responses[responseIndex];
138+
if (alpineCtResponse?.status === 'fulfilled' && alpineCtResponse.value.ok) {
139+
const alpineCtData = await alpineCtResponse.value.json() as { result?: { data?: { json?: { success?: boolean; content?: string } } } };
140+
if (alpineCtData.result?.data?.json?.success) {
141+
content.alpineCtScript = alpineCtData.result.data.json.content;
142+
}
143+
}
144+
responseIndex++;
145+
}
146+
147+
// Alpine install script
148+
if (hasAlpineVariant) {
149+
const alpineInstallResponse = responses[responseIndex];
150+
if (alpineInstallResponse?.status === 'fulfilled' && alpineInstallResponse.value.ok) {
151+
const alpineInstallData = await alpineInstallResponse.value.json() as { result?: { data?: { json?: { success?: boolean; content?: string } } } };
152+
if (alpineInstallData.result?.data?.json?.success) {
153+
content.alpineInstallScript = alpineInstallData.result.data.json.content;
154+
}
155+
}
156+
}
78157

79158
setScriptContent(content);
80159
} catch (err) {
81160
setError(err instanceof Error ? err.message : 'Failed to load script content');
82161
} finally {
83162
setIsLoading(false);
84163
}
85-
}, [scriptName, slug]);
164+
}, [defaultScriptName, alpineScriptName, slug, hasAlpineVariant]);
86165

87166
useEffect(() => {
88167
if (isOpen && scriptName) {
@@ -106,11 +185,30 @@ export function TextViewer({ scriptName, isOpen, onClose }: TextViewerProps) {
106185
<div className="bg-card rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] flex flex-col border border-border mx-4 sm:mx-0">
107186
{/* Header */}
108187
<div className="flex items-center justify-between p-6 border-b border-border">
109-
<div className="flex items-center space-x-4">
188+
<div className="flex items-center space-x-4 flex-1">
110189
<h2 className="text-2xl font-bold text-foreground">
111-
Script Viewer: {scriptName}
190+
Script Viewer: {defaultScriptName}
112191
</h2>
113-
{scriptContent.ctScript && scriptContent.installScript && (
192+
{hasAlpineVariant && (
193+
<div className="flex space-x-2">
194+
<Button
195+
variant={selectedVersion === 'default' ? 'default' : 'outline'}
196+
onClick={() => setSelectedVersion('default')}
197+
className="px-3 py-1 text-sm"
198+
>
199+
Default
200+
</Button>
201+
<Button
202+
variant={selectedVersion === 'alpine' ? 'default' : 'outline'}
203+
onClick={() => setSelectedVersion('alpine')}
204+
className="px-3 py-1 text-sm"
205+
>
206+
Alpine
207+
</Button>
208+
</div>
209+
)}
210+
{((selectedVersion === 'default' && (scriptContent.ctScript || scriptContent.installScript)) ||
211+
(selectedVersion === 'alpine' && (scriptContent.alpineCtScript || scriptContent.alpineInstallScript))) && (
114212
<div className="flex space-x-2">
115213
<Button
116214
variant={activeTab === 'ct' ? 'outline' : 'ghost'}
@@ -151,44 +249,87 @@ export function TextViewer({ scriptName, isOpen, onClose }: TextViewerProps) {
151249
</div>
152250
) : (
153251
<div className="flex-1 overflow-auto">
154-
{activeTab === 'ct' && scriptContent.ctScript ? (
155-
<SyntaxHighlighter
156-
language="bash"
157-
style={tomorrow}
158-
customStyle={{
159-
margin: 0,
160-
padding: '1rem',
161-
fontSize: '14px',
162-
lineHeight: '1.5',
163-
minHeight: '100%'
164-
}}
165-
showLineNumbers={true}
166-
wrapLines={true}
167-
>
168-
{scriptContent.ctScript}
169-
</SyntaxHighlighter>
170-
) : activeTab === 'install' && scriptContent.installScript ? (
171-
<SyntaxHighlighter
172-
language="bash"
173-
style={tomorrow}
174-
customStyle={{
175-
margin: 0,
176-
padding: '1rem',
177-
fontSize: '14px',
178-
lineHeight: '1.5',
179-
minHeight: '100%'
180-
}}
181-
showLineNumbers={true}
182-
wrapLines={true}
183-
>
184-
{scriptContent.installScript}
185-
</SyntaxHighlighter>
186-
) : (
187-
<div className="flex items-center justify-center h-full">
188-
<div className="text-lg text-muted-foreground">
189-
{activeTab === 'ct' ? 'CT script not found' : 'Install script not found'}
252+
{activeTab === 'ct' && (
253+
selectedVersion === 'default' && scriptContent.ctScript ? (
254+
<SyntaxHighlighter
255+
language="bash"
256+
style={tomorrow}
257+
customStyle={{
258+
margin: 0,
259+
padding: '1rem',
260+
fontSize: '14px',
261+
lineHeight: '1.5',
262+
minHeight: '100%'
263+
}}
264+
showLineNumbers={true}
265+
wrapLines={true}
266+
>
267+
{scriptContent.ctScript}
268+
</SyntaxHighlighter>
269+
) : selectedVersion === 'alpine' && scriptContent.alpineCtScript ? (
270+
<SyntaxHighlighter
271+
language="bash"
272+
style={tomorrow}
273+
customStyle={{
274+
margin: 0,
275+
padding: '1rem',
276+
fontSize: '14px',
277+
lineHeight: '1.5',
278+
minHeight: '100%'
279+
}}
280+
showLineNumbers={true}
281+
wrapLines={true}
282+
>
283+
{scriptContent.alpineCtScript}
284+
</SyntaxHighlighter>
285+
) : (
286+
<div className="flex items-center justify-center h-full">
287+
<div className="text-lg text-muted-foreground">
288+
{selectedVersion === 'default' ? 'Default CT script not found' : 'Alpine CT script not found'}
289+
</div>
290+
</div>
291+
)
292+
)}
293+
{activeTab === 'install' && (
294+
selectedVersion === 'default' && scriptContent.installScript ? (
295+
<SyntaxHighlighter
296+
language="bash"
297+
style={tomorrow}
298+
customStyle={{
299+
margin: 0,
300+
padding: '1rem',
301+
fontSize: '14px',
302+
lineHeight: '1.5',
303+
minHeight: '100%'
304+
}}
305+
showLineNumbers={true}
306+
wrapLines={true}
307+
>
308+
{scriptContent.installScript}
309+
</SyntaxHighlighter>
310+
) : selectedVersion === 'alpine' && scriptContent.alpineInstallScript ? (
311+
<SyntaxHighlighter
312+
language="bash"
313+
style={tomorrow}
314+
customStyle={{
315+
margin: 0,
316+
padding: '1rem',
317+
fontSize: '14px',
318+
lineHeight: '1.5',
319+
minHeight: '100%'
320+
}}
321+
showLineNumbers={true}
322+
wrapLines={true}
323+
>
324+
{scriptContent.alpineInstallScript}
325+
</SyntaxHighlighter>
326+
) : (
327+
<div className="flex items-center justify-center h-full">
328+
<div className="text-lg text-muted-foreground">
329+
{selectedVersion === 'default' ? 'Default install script not found' : 'Alpine install script not found'}
330+
</div>
190331
</div>
191-
</div>
332+
)
192333
)}
193334
</div>
194335
)}

0 commit comments

Comments
 (0)