Skip to content

Commit 2cc0192

Browse files
Feat/download logs for pods (#896)
* feat: Simplify DownloadLogsButton for client-side log downloads * feat: Update LogModal to pass log content to download button * feat: Update panels to pass log content to download button * feat: Add modal to display file size before downloading logs * feat: add download logs button for pod resources in list view * style: Standardize toast message styling to match application patterns
1 parent f5723a5 commit 2cc0192

File tree

6 files changed

+301
-51
lines changed

6 files changed

+301
-51
lines changed

src/components/DownloadLogsButton.tsx

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React, { useState } from 'react';
2+
import { Download } from 'lucide-react';
3+
import { toast } from 'react-hot-toast';
4+
import DownloadLogsModal from './DownloadLogsModal';
5+
import useTheme from '../stores/themeStore';
6+
7+
interface DownloadLogsButtonProps {
8+
cluster: string;
9+
namespace: string;
10+
podName: string;
11+
className?: string;
12+
previous?: boolean;
13+
logContent?: string; // Added prop to receive current log content
14+
}
15+
16+
const DownloadLogsButton: React.FC<DownloadLogsButtonProps> = ({
17+
cluster,
18+
namespace,
19+
podName,
20+
className = '',
21+
logContent = '', // Default to empty string if not provided
22+
}) => {
23+
const [isLoading, setIsLoading] = useState(false);
24+
const [showModal, setShowModal] = useState(false);
25+
const theme = useTheme(state => state.theme);
26+
27+
// Function to download logs directly from the browser
28+
const downloadLogs = async () => {
29+
try {
30+
setIsLoading(true);
31+
32+
// Create filename with timestamp to avoid duplicates
33+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
34+
const filename = `logs-${cluster}-${podName}-${timestamp}.log`;
35+
36+
// Use the log content directly or fetch it if not provided
37+
let content = logContent;
38+
39+
// If no content was provided, create a placeholder message
40+
if (!content || content.trim() === '') {
41+
content =
42+
`Logs for pod ${podName} in namespace ${namespace} on cluster ${cluster}\n` +
43+
`Generated at: ${new Date().toLocaleString()}\n\n` +
44+
`Log content not available directly. Please use the streaming logs feature.`;
45+
}
46+
47+
// Create a blob with the content
48+
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
49+
50+
// Create object URL from blob
51+
const url = URL.createObjectURL(blob);
52+
53+
// Create download link
54+
const link = document.createElement('a');
55+
link.href = url;
56+
link.download = filename;
57+
link.style.display = 'none';
58+
59+
// Append to document, click, then remove
60+
document.body.appendChild(link);
61+
link.click();
62+
63+
// Clean up
64+
setTimeout(() => {
65+
document.body.removeChild(link);
66+
URL.revokeObjectURL(url);
67+
}, 100);
68+
69+
// Show success notification
70+
toast.success(`Downloaded logs for ${podName}`, {
71+
duration: 3000,
72+
});
73+
} catch (error) {
74+
console.error('Error downloading logs:', error);
75+
toast.error('Failed to download logs', {
76+
duration: 3000,
77+
});
78+
} finally {
79+
setIsLoading(false);
80+
}
81+
};
82+
83+
// Function to calculate the size of the log content
84+
const calculateSize = () => {
85+
const contentSize = new Blob([logContent], { type: 'text/plain' }).size;
86+
setShowModal(true);
87+
return contentSize;
88+
};
89+
90+
return (
91+
<>
92+
<button
93+
onClick={() => calculateSize()}
94+
disabled={isLoading}
95+
className={`${
96+
theme === 'dark'
97+
? 'bg-gray-700 text-white hover:bg-gray-600'
98+
: 'bg-gray-300 text-gray-800 hover:bg-gray-200'
99+
} rounded px-2 py-1 transition-colors ${className}`}
100+
title="Download logs"
101+
>
102+
{isLoading ? <span className="inline-block animate-spin"></span> : <Download size={16} />}
103+
</button>
104+
105+
{showModal && (
106+
<DownloadLogsModal
107+
size={new Blob([logContent], { type: 'text/plain' }).size}
108+
onClose={() => setShowModal(false)}
109+
onSave={downloadLogs}
110+
/>
111+
)}
112+
</>
113+
);
114+
};
115+
116+
export default DownloadLogsButton;

src/components/DownloadLogsModal.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
import { X } from 'lucide-react';
3+
import useTheme from '../stores/themeStore';
4+
5+
interface DownloadLogsModalProps {
6+
size: number;
7+
onClose: () => void;
8+
onSave: () => void;
9+
}
10+
11+
const DownloadLogsModal: React.FC<DownloadLogsModalProps> = ({ size, onClose, onSave }) => {
12+
const theme = useTheme(state => state.theme);
13+
14+
// Format file size
15+
const formatSize = (bytes: number) => {
16+
if (bytes === 0) return '0 B';
17+
18+
const units = ['B', 'KB', 'MB', 'GB'];
19+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
20+
21+
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
22+
};
23+
24+
return (
25+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60 backdrop-blur-sm">
26+
<div
27+
className={`w-80 rounded-lg p-5 shadow-xl ${
28+
theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-gray-800'
29+
}`}
30+
>
31+
<div className="mb-3 flex items-center justify-between">
32+
<h2 className="text-lg font-medium">Download Logs</h2>
33+
<button
34+
onClick={onClose}
35+
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
36+
>
37+
<X size={18} />
38+
</button>
39+
</div>
40+
41+
<div className="mb-4">
42+
<p className="mb-2 text-sm">File size: {formatSize(size)}</p>
43+
<div className="h-1 w-full rounded-full bg-gray-200 dark:bg-gray-700">
44+
<div className="h-1 rounded-full bg-blue-500" style={{ width: '100%' }}></div>
45+
</div>
46+
</div>
47+
48+
<div className="flex justify-end space-x-2">
49+
<button
50+
onClick={onClose}
51+
className={`rounded px-3 py-1.5 text-sm ${
52+
theme === 'dark'
53+
? 'bg-gray-700 text-white hover:bg-gray-600'
54+
: 'bg-gray-200 text-gray-800 hover:bg-gray-300'
55+
}`}
56+
>
57+
Cancel
58+
</button>
59+
<button
60+
onClick={onSave}
61+
className="rounded bg-blue-500 px-3 py-1.5 text-sm text-white hover:bg-blue-600"
62+
>
63+
Download
64+
</button>
65+
</div>
66+
</div>
67+
</div>
68+
);
69+
};
70+
71+
export default DownloadLogsModal;

src/components/DynamicDetailsPanel.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import useTheme from '../stores/themeStore'; // Import the useTheme hook
2929
import '@fortawesome/fontawesome-free/css/all.min.css';
3030
import { api } from '../lib/api';
3131
import { useResourceLogsWebSocket } from '../hooks/useWebSocket';
32+
import DownloadLogsButton from './DownloadLogsButton';
3233

3334
interface DynamicDetailsProps {
3435
namespace: string;
@@ -819,20 +820,33 @@ const DynamicDetailsPanel = ({
819820
</Box>
820821
)}
821822
{tabValue === 2 && (
822-
<Box
823-
sx={{
824-
maxHeight: '500px',
825-
bgcolor: theme === 'dark' ? '#1E1E1E' : '#FFFFFF',
826-
borderRadius: 1,
827-
p: 1,
828-
overflow: 'auto',
829-
}}
830-
>
831-
<div
832-
ref={terminalRef}
833-
style={{ height: '100%', width: '100%', overflow: 'auto' }}
834-
/>
835-
</Box>
823+
<>
824+
{/* Add download logs button if the resource is a pod */}
825+
{type.toLowerCase() === 'pod' && (
826+
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
827+
<DownloadLogsButton
828+
cluster={(resourceData?.cluster as string) || 'default'}
829+
namespace={namespace}
830+
podName={name}
831+
logContent={logs.join('\n')}
832+
/>
833+
</Box>
834+
)}
835+
<Box
836+
sx={{
837+
maxHeight: '500px',
838+
bgcolor: theme === 'dark' ? '#1E1E1E' : '#FFFFFF',
839+
borderRadius: 1,
840+
p: 1,
841+
overflow: 'auto',
842+
}}
843+
>
844+
<div
845+
ref={terminalRef}
846+
style={{ height: '100%', width: '100%', overflow: 'auto' }}
847+
/>
848+
</Box>
849+
</>
836850
)}
837851
</Box>
838852
</Box>

src/components/ListViewComponent.tsx

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Box, Typography, Button } from '@mui/material';
1+
import { Box, Typography, Button, Tooltip } from '@mui/material';
22
import { useEffect, useState, useCallback, useRef } from 'react';
33
import useTheme from '../stores/themeStore';
44
import ListViewSkeleton from './ui/ListViewSkeleton';
55
import { api } from '../lib/api';
6+
import DownloadLogsButton from './DownloadLogsButton';
67

78
// Define the response interfaces
89
export interface ResourceItem {
@@ -720,19 +721,35 @@ const ListViewComponent = ({
720721
>
721722
{/* Name and namespace section */}
722723
<Box sx={{ overflow: 'hidden' }}>
723-
<Typography
724-
sx={{
725-
color: theme === 'dark' ? '#fff' : '#6B7280',
726-
fontWeight: 500,
727-
fontSize: '1rem',
728-
whiteSpace: 'nowrap',
729-
overflow: 'hidden',
730-
textOverflow: 'ellipsis',
731-
}}
732-
>
733-
{resource.name}
734-
</Typography>
735-
{resource.namespace != '' && (
724+
<Box sx={{ display: 'flex', alignItems: 'center' }}>
725+
<Typography
726+
sx={{
727+
color: theme === 'dark' ? '#fff' : '#6B7280',
728+
fontWeight: 500,
729+
fontSize: '1rem',
730+
whiteSpace: 'nowrap',
731+
overflow: 'hidden',
732+
textOverflow: 'ellipsis',
733+
}}
734+
>
735+
{resource.name}
736+
</Typography>
737+
738+
{/* Add download logs button for pod resources */}
739+
{resource.kind.toLowerCase() === 'pod' && (
740+
<Tooltip title="Download logs">
741+
<span className="ml-2">
742+
<DownloadLogsButton
743+
cluster={resource.context || 'default'}
744+
namespace={resource.namespace}
745+
podName={resource.name}
746+
/>
747+
</span>
748+
</Tooltip>
749+
)}
750+
</Box>
751+
752+
{resource.namespace !== '' && (
736753
<Typography
737754
sx={{
738755
color: theme === 'dark' ? '#A5ADBA' : '#6B7280',

src/components/LogModal.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ import { Terminal } from 'xterm';
44
import { FitAddon } from 'xterm-addon-fit';
55
import 'xterm/css/xterm.css';
66
import useTheme from '../stores/themeStore';
7+
import DownloadLogsButton from './DownloadLogsButton';
78

89
interface LogModalProps {
910
namespace: string;
1011
deploymentName: string;
1112
onClose: () => void;
13+
cluster?: string; // Added cluster prop
1214
}
1315

14-
const LogModal = ({ namespace, deploymentName, onClose }: LogModalProps) => {
16+
const LogModal = ({ namespace, deploymentName, onClose, cluster = 'default' }: LogModalProps) => {
1517
const terminalRef = useRef<HTMLDivElement>(null);
1618
const terminalInstance = useRef<Terminal | null>(null);
1719
const [loading, setLoading] = useState(true);
1820
const [error, setError] = useState<string | null>(null);
21+
const [logContent, setLogContent] = useState<string>('');
1922
const theme = useTheme(state => state.theme);
2023

2124
useEffect(() => {
@@ -56,7 +59,10 @@ const LogModal = ({ namespace, deploymentName, onClose }: LogModalProps) => {
5659
};
5760

5861
socket.onmessage = event => {
62+
// Add the log line to the terminal
5963
term.writeln(event.data);
64+
// Also append to our captured log content
65+
setLogContent(prev => prev + event.data + '\n');
6066
setError(null);
6167
};
6268

@@ -93,16 +99,25 @@ const LogModal = ({ namespace, deploymentName, onClose }: LogModalProps) => {
9399
<h2 className="text-2xl font-bold">
94100
Logs: <span className="text-2xl text-blue-400">{deploymentName}</span>
95101
</h2>
96-
<button
97-
onClick={onClose}
98-
className={`transition duration-200 ${
99-
theme === 'dark'
100-
? 'bg-gray-900 hover:text-red-600'
101-
: 'border-none bg-white hover:text-red-600'
102-
}`}
103-
>
104-
<X size={22} />
105-
</button>
102+
<div className="flex items-center space-x-2">
103+
<DownloadLogsButton
104+
cluster={cluster}
105+
namespace={namespace}
106+
podName={deploymentName}
107+
className="mr-2"
108+
logContent={logContent}
109+
/>
110+
<button
111+
onClick={onClose}
112+
className={`transition duration-200 ${
113+
theme === 'dark'
114+
? 'bg-gray-900 hover:text-red-600'
115+
: 'border-none bg-white hover:text-red-600'
116+
}`}
117+
>
118+
<X size={22} />
119+
</button>
120+
</div>
106121
</div>
107122

108123
{/* Terminal Section */}

0 commit comments

Comments
 (0)