Skip to content

Commit

Permalink
Debug bundle form, adjusted details od Debug Bundle overview
Browse files Browse the repository at this point in the history
  • Loading branch information
jvorcak committed Oct 15, 2024
1 parent d2845ae commit 0bca65c
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 100 deletions.
91 changes: 47 additions & 44 deletions frontend/src/components/debugBundle/DebugBundleLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,71 @@ import React from 'react';
import { DebugBundleStatus, DeleteDebugBundleFileForBroker, GetDebugBundleStatusResponse_DebugBundleBrokerStatus } from '../../protogen/redpanda/api/console/v1alpha1/debug_bundle_pb';
import { config } from '../../config';
import { api } from '../../state/backendApi';
import { Button, IconButton } from '@redpanda-data/ui';
import { Box, Button, Text, IconButton } from '@redpanda-data/ui';
import { MdDeleteOutline } from 'react-icons/md';

const DebugBundleLink = ({ statuses, showDeleteButton = false }: { statuses: GetDebugBundleStatusResponse_DebugBundleBrokerStatus[], showDeleteButton?: boolean }) => {
const statusWithFilename = statuses.find(status => status.value.case === 'bundleStatus' && status.value.value.filename)
const filename = (statusWithFilename?.value.value as DebugBundleStatus)?.filename;
const DebugBundleLink = ({ statuses, showDeleteButton = false, showDatetime = true }: { statuses: GetDebugBundleStatusResponse_DebugBundleBrokerStatus[], showDeleteButton?: boolean, showDatetime?: boolean }) => {
const statusWithFilename = statuses.find(status => status.value.case === 'bundleStatus' && status.value.value.filename)?.value.value as DebugBundleStatus
const downloadFilename = 'debug-bundle.zip'


if(statuses.length === 0) {
return null
}

if(!filename) {
if(!statusWithFilename.filename) {
return null
}

return (
<>
<Button
variant="link"
onClick={() => {
config.fetch(`${config.restBasePath}/debug_bundle/file.zip`).then(async response => {
const url = window.URL.createObjectURL(await response.blob());
<Box>
<Box>
<Button
variant="link"
onClick={() => {
config.fetch(`${config.restBasePath}/debug_bundle/file.zip`).then(async response => {
const url = window.URL.createObjectURL(await response.blob());

// Create a new anchor element
const a = document.createElement('a');
// Create a new anchor element
const a = document.createElement('a');

// Set the download URL and filename
a.href = url;
a.download = downloadFilename;
// Set the download URL and filename
a.href = url;
a.download = downloadFilename;

// Append the anchor to the document body (necessary for Firefox)
document.body.appendChild(a);
// Append the anchor to the document body (necessary for Firefox)
document.body.appendChild(a);

// Programmatically trigger the download
a.click();
// Programmatically trigger the download
a.click();

// Remove the anchor from the DOM
document.body.removeChild(a);
// Remove the anchor from the DOM
document.body.removeChild(a);

// Revoke the temporary URL to free memory
window.URL.revokeObjectURL(url);
});
}}
px={0}
>
{downloadFilename}
</Button>
{showDeleteButton && <IconButton
variant="ghost"
icon={<MdDeleteOutline/>}
aria-label="Delete file"
onClick={() => {
void api.deleteDebugBundleFile({
file: {
filename,
} as DeleteDebugBundleFileForBroker
});
}}
/>}
</>
// Revoke the temporary URL to free memory
window.URL.revokeObjectURL(url);
});
}}
px={0}
>
{downloadFilename}
</Button>
{showDeleteButton && <IconButton
variant="ghost"
icon={<MdDeleteOutline/>}
aria-label="Delete file"
onClick={() => {
void api.deleteDebugBundleFile({
file: {
filename: statusWithFilename.filename,
} as DeleteDebugBundleFileForBroker
});
}}
/>}
</Box>
{showDatetime && <Text>
Generated {statusWithFilename.createdAt?.toDate().toLocaleString()}
</Text>}
</Box>
);
};

Expand Down
164 changes: 141 additions & 23 deletions frontend/src/components/pages/admin/Admin.DebugBundle.New.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import { api, } from '../../../state/backendApi';
import '../../../utils/arrayExtensions';
import { makeObservable, observable } from 'mobx';
import { DefaultSkeleton } from '../../../utils/tsxUtils';
import { Alert, AlertIcon, Box, Button, Flex, FormField, Input, Text } from '@redpanda-data/ui';
import { Alert, AlertIcon, Box, Button, Flex, FormField, Grid, GridItem, Input, PasswordInput, Text } from '@redpanda-data/ui';
import { PageComponent, PageInitHelper } from '../Page';
import { appGlobal } from '../../../state/appGlobal';
import { SingleSelect } from '../../misc/Select';
import { FC, useState } from 'react';
import { SCRAMAuth } from '../../../protogen/redpanda/api/console/v1alpha1/debug_bundle_pb';
import { MdDeleteOutline } from 'react-icons/md';

@observer
export default class AdminPageDebugBundleNew extends PageComponent<{}> {
Expand All @@ -29,8 +30,9 @@ export default class AdminPageDebugBundleNew extends PageComponent<{}> {
@observable createBundleError: string | undefined = '';

initPage(p: PageInitHelper): void {
p.title = 'Admin';
p.title = 'Generate debug bundle';
p.addBreadcrumb('Admin', '/admin');
p.addBreadcrumb('Generate debug bundle', '/admin/debug-bundle/progress');

this.refreshData(true);
appGlobal.onRefresh = () => this.refreshData(true);
Expand All @@ -52,6 +54,10 @@ export default class AdminPageDebugBundleNew extends PageComponent<{}> {
return (
<Box>
<Text>Collect environment data that can help debug and diagnose issues with a Redpanda cluster, a broker, or the machine it’s running on. This will bundle the collected data into a ZIP file.</Text>
<Alert status="info" my={2}>
<AlertIcon/>
This is an advanced feature, best used if you have received direction to do so from Redpanda support.
</Alert>

{this.createBundleError && <Alert status="error">
<AlertIcon/>
Expand Down Expand Up @@ -83,16 +89,35 @@ const NewDebugBundleForm: FC<{ onSubmit: () => void }> = observer(({onSubmit}) =
const [advancedForm, setAdvancedForm] = useState(false);

const formState = useLocalObservable(() => ({
scramAuth: {
username: '',
password: '',
} as SCRAMAuth,
skipTlsVerification: false,
brokerIds: '' as string,
controllerLogsSizeLimitBytes: '' as string,
cpuProfilerWaitSeconds: '' as string,
logsSince: '' as string,
logsSizeLimitBytes: '' as string,
logsUntil: '' as string,
metricsIntervalSeconds: '' as string,
metricsSamples: '' as string,
namespace: '' as string,
partitions: '' as string,
labelSelectors: [
{
key: '',
value: '',
}
] as Array<{key: string, value: string}>,

// Setters
setUsername(username: string) {
this.scramAuth.username = username;
},
setPassword(password: string) {
this.scramAuth.password = password;
},
setBrokerIds(ids: string) {
this.brokerIds = ids;
},
Expand All @@ -114,9 +139,30 @@ const NewDebugBundleForm: FC<{ onSubmit: () => void }> = observer(({onSubmit}) =
setMetricsIntervalSeconds(seconds: string) {
this.metricsIntervalSeconds = seconds;
},
setMetricsSamples(samples: string) {
this.metricsSamples = samples;
},
setNamespace(namespace: string) {
this.namespace = namespace;
},
setPartitions(partitions: string) {
this.partitions = partitions;
},
addLabelSelector() {
this.labelSelectors.push({
key: '',
value: '',
});
},
removeLabelSelector(idx: number) {
this.labelSelectors.splice(idx, 1);
},
setLabelSelectorKey(value: string, idx: number) {
this.labelSelectors[idx].key = value;
},
setLabelSelectorValue(value: string, idx: number) {
this.labelSelectors[idx].value = value;
},
}));

return (
Expand All @@ -125,59 +171,131 @@ const NewDebugBundleForm: FC<{ onSubmit: () => void }> = observer(({onSubmit}) =
flexDirection="column"
width={{
base: 'full',
sm: 400,
sm: 500,
}}
gap={2}
>
<FormField label="Broker">
<FormField label="SCRAM user">
<Input
data-testid="scram-user-input"
value={formState.scramAuth.username}
onChange={(e) => formState.setUsername(e.target.value)}
/>
</FormField>
<FormField label="Password">
<PasswordInput
data-testid="scram-user-password"
value={formState.scramAuth.password}
onChange={(e) => formState.setPassword(e.target.value)}
/>
</FormField>
<FormField label="Broker(s)" description="Specify broker IDs (comma-separated, or leave blank for all)">
<Input
data-testid="broker-input"
data-testid="broker-ids-input"
value={formState.brokerIds}
onChange={(e) => formState.setBrokerIds(e.target.value)}
/>
</FormField>
<FormField label="Controller log size limit">
<SingleSelect
<FormField label="Controller log size limit" description={'The size limit of the controller logs that can be stored in the bundle (e.g. 3MB, 1GiB) (default "132MB")'}>
<Input
data-testid="controller-log-size-input"
value={formState.controllerLogsSizeLimitBytes}
onChange={(value) => formState.setControllerLogsSizeLimitBytes(value)}
options={[{label: '100MB', value: '104857600'}]}
onChange={(e) => formState.setControllerLogsSizeLimitBytes(e.target.value)}
/>
</FormField>
<FormField label="CPU profiler wait (in seconds)">
<FormField label="CPU profiler wait" description="How long in seconds to collect samples for the CPU profiler. Must be higher than 15s (default 30s)">
<Input
data-testid="cpu-profiler-input"
value={formState.cpuProfilerWaitSeconds}
type="number"
onChange={(e) => formState.setCpuProfilerWaitSeconds(e.target.value)}
/>
</FormField>
<FormField label="Logs since">
<SingleSelect
<FormField label="Logs since" description="Include logs dated from specified date onward; (journalctl date format: YYYY-MM-DD, 'yesterday', or 'today'). Default 'yesterday'.">
<Input
data-testid="logs-since-input"
value={formState.logsSince}
onChange={(date) => formState.setLogsSince(date)}
options={[]} // You need to provide date-related options
onChange={(e) => formState.setLogsSince(e.target.value)}
/>
</FormField>
<FormField label="Log size limit in MiB, up to X">
<FormField label="Logs until" description="Include logs older than the specified date; (journalctl date format: YYYY-MM-DD, 'yesterday', or 'today').">
<Input
value={formState.logsSizeLimitBytes}
onChange={(e) => formState.setLogsSizeLimitBytes(e.target.value)}
data-testid="logs-until-input"
value={formState.logsUntil}
onChange={(e) => formState.setLogsUntil(e.target.value)}
/>
</FormField>
<FormField label="Logs until">
<FormField label="Log size limit" description="Read the logs until the given size is reached (e.g. 3MB, 1GiB). Default 100MiB.">
<Input
value={formState.logsUntil}
onChange={(e) => formState.setLogsUntil(e.target.value)}
data-testid="log-size-limit-input"
value={formState.logsSizeLimitBytes}
onChange={(e) => formState.setLogsSizeLimitBytes(e.target.value)}
/>
</FormField>
<FormField label="Metrics interval (in seconds)">
<FormField label="Metrics interval duration" description="Interval between metrics snapshots (e.g. 30s, 1.5m) (default 10s)">
<Input
data-testid="metrics-interval-duration-input"
value={formState.metricsIntervalSeconds}
onChange={(e) => formState.setMetricsIntervalSeconds(e.target.value)}
/>
</FormField>
<FormField label="Partition">
<FormField label="Metrics samples" description="Number of metrics samples to take (at the interval of 'metrics interval duration'). Must be >= 2">
<Input
data-testid="metrics-samples-in put"
value={formState.metricsSamples}
onChange={(e) => formState.setMetricsSamples(e.target.value)}
/>
</FormField>
<FormField label="Namespace" description='The namespace to use to collect the resources from (k8s only). Default "redpanda".'>
<Input
data-testid="namespace-input"
value={formState.namespace}
onChange={(e) => formState.setNamespace(e.target.value)}
/>
</FormField>
<FormField label="Partition(s)" description="Comma-separated partition IDs.">
<Input
data-testid="partitions-input"
value={formState.partitions}
onChange={(e) => formState.setPartitions(e.target.value)}
/>
</FormField>
<FormField label="Label selectors" description="Label selectors to filter your resources.">
{formState.labelSelectors.map((labelSelector, idx) =>
<Grid gap={2} key={idx} templateColumns="1fr 1fr auto">
<GridItem>
<Text fontSize="sm">Key</Text>
<Input
value={labelSelector.key}
onChange={(e) => {
formState.setLabelSelectorKey(e.target.value, idx);
}}
/>
</GridItem>
<GridItem>
<Text fontSize="sm">Value</Text>
<Input
value={labelSelector.value}
onChange={(e) => {
formState.setLabelSelectorValue(e.target.value, idx);
}}
/>
</GridItem>
<GridItem display="flex" alignItems="flex-end">
<Button variant="ghost" onClick={() => {
formState.removeLabelSelector(idx);
}}>
<MdDeleteOutline/>
</Button>
</GridItem>
</Grid>
)}
<Box>
<Button variant="outline" my={2} onClick={() => {
formState.addLabelSelector();
}}>Add</Button>
</Box>
</FormField>
</Flex>}

<Flex gap={2} mt={4}>
Expand Down
Loading

0 comments on commit 0bca65c

Please sign in to comment.