Skip to content

Commit 51c2b8b

Browse files
authored
Merge pull request #4701 from alokdangre/feat/rollout-undo-phase2
Implement rollback to specific revision and Revision history view
2 parents d36a68d + 49a2f56 commit 51c2b8b

34 files changed

+1030
-141
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright 2025 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Box from '@mui/material/Box';
18+
import Chip from '@mui/material/Chip';
19+
import Table from '@mui/material/Table';
20+
import TableBody from '@mui/material/TableBody';
21+
import TableCell from '@mui/material/TableCell';
22+
import TableContainer from '@mui/material/TableContainer';
23+
import TableHead from '@mui/material/TableHead';
24+
import TableRow from '@mui/material/TableRow';
25+
import Typography from '@mui/material/Typography';
26+
import { useEffect, useState } from 'react';
27+
import { useTranslation } from 'react-i18next';
28+
import { KubeObject } from '../../../lib/k8s/KubeObject';
29+
import type { RevisionInfo } from '../../../lib/k8s/rollback';
30+
import { DateLabel } from '../Label';
31+
import SectionBox from '../SectionBox';
32+
33+
interface RevisionHistorySectionProps {
34+
/** The resource to show revision history for */
35+
resource: KubeObject;
36+
}
37+
38+
/**
39+
* Helper to extract revision history from a resource that supports it.
40+
*/
41+
function getRevisionHistoryFn(resource: KubeObject): (() => Promise<RevisionInfo[]>) | undefined {
42+
if ('getRevisionHistory' in resource && typeof resource.getRevisionHistory === 'function') {
43+
return () => (resource as any).getRevisionHistory();
44+
}
45+
return undefined;
46+
}
47+
48+
/**
49+
* RevisionHistorySection shows a table of revision history for rollbackable resources.
50+
* Displays revision number, creation date, container images, and current status.
51+
*
52+
* This is added as an extraSection on Deployment, DaemonSet, and StatefulSet details pages.
53+
*/
54+
export default function RevisionHistorySection(props: RevisionHistorySectionProps) {
55+
const { resource } = props;
56+
const { t } = useTranslation(['translation']);
57+
58+
const [revisions, setRevisions] = useState<RevisionInfo[]>([]);
59+
const [loading, setLoading] = useState(true);
60+
const [error, setError] = useState<string | null>(null);
61+
62+
const getHistory = getRevisionHistoryFn(resource);
63+
64+
useEffect(() => {
65+
const historyGetter = getRevisionHistoryFn(resource);
66+
let isActive = true;
67+
68+
if (!historyGetter) {
69+
setRevisions([]);
70+
setError(null);
71+
setLoading(false);
72+
return () => {
73+
isActive = false;
74+
};
75+
}
76+
77+
setLoading(true);
78+
setError(null);
79+
historyGetter()
80+
.then(history => {
81+
if (!isActive) {
82+
return;
83+
}
84+
setRevisions(history);
85+
setLoading(false);
86+
})
87+
.catch(err => {
88+
if (!isActive) {
89+
return;
90+
}
91+
setError(err instanceof Error ? err.message : String(err));
92+
setLoading(false);
93+
});
94+
95+
return () => {
96+
isActive = false;
97+
};
98+
}, [resource?.metadata?.uid, resource?.metadata?.resourceVersion]);
99+
100+
if (!getHistory) {
101+
return null;
102+
}
103+
104+
return (
105+
<SectionBox title={t('translation|Revision History')}>
106+
{loading && (
107+
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
108+
{t('translation|Loading revision history…')}
109+
</Typography>
110+
)}
111+
{error && (
112+
<Typography variant="body2" color="error" sx={{ py: 2 }}>
113+
{t('translation|Failed to load revision history: {{ error }}', { error })}
114+
</Typography>
115+
)}
116+
{!loading && !error && revisions.length === 0 && (
117+
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
118+
{t('translation|No revision history available.')}
119+
</Typography>
120+
)}
121+
{!loading && !error && revisions.length > 0 && (
122+
<TableContainer>
123+
<Table size="small">
124+
<TableHead>
125+
<TableRow>
126+
<TableCell>{t('translation|Revision')}</TableCell>
127+
<TableCell>{t('translation|Created')}</TableCell>
128+
<TableCell>{t('translation|Images')}</TableCell>
129+
<TableCell>{t('translation|Status')}</TableCell>
130+
</TableRow>
131+
</TableHead>
132+
<TableBody>
133+
{revisions.map(rev => (
134+
<TableRow key={rev.revision}>
135+
<TableCell>
136+
<Typography variant="body2">{rev.revision}</Typography>
137+
</TableCell>
138+
<TableCell>
139+
<DateLabel date={rev.createdAt} />
140+
</TableCell>
141+
<TableCell>
142+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
143+
{rev.images.map((img, i) => (
144+
<Typography key={i} variant="body2" sx={{ fontFamily: 'monospace' }}>
145+
{img}
146+
</Typography>
147+
))}
148+
</Box>
149+
</TableCell>
150+
<TableCell>
151+
{rev.isCurrent ? (
152+
<Chip
153+
label={t('translation|Current')}
154+
size="small"
155+
color="primary"
156+
variant="outlined"
157+
/>
158+
) : (
159+
<Typography variant="body2" color="text.secondary">
160+
{t('translation|Previous')}
161+
</Typography>
162+
)}
163+
</TableCell>
164+
</TableRow>
165+
))}
166+
</TableBody>
167+
</Table>
168+
</TableContainer>
169+
)}
170+
</SectionBox>
171+
);
172+
}

frontend/src/components/common/Resource/RollbackButton.tsx

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useState } from 'react';
17+
import { useCallback, useState } from 'react';
1818
import { useTranslation } from 'react-i18next';
1919
import { useDispatch } from 'react-redux';
2020
import { useLocation } from 'react-router';
2121
import DaemonSet from '../../../lib/k8s/daemonSet';
2222
import Deployment from '../../../lib/k8s/deployment';
2323
import { KubeObject } from '../../../lib/k8s/KubeObject';
24-
import { RollbackResult } from '../../../lib/k8s/rollback';
24+
import type { RevisionInfo, RollbackResult } from '../../../lib/k8s/rollback';
2525
import StatefulSet from '../../../lib/k8s/statefulSet';
2626
import { clusterAction } from '../../../redux/clusterActionSlice';
2727
import {
@@ -31,14 +31,15 @@ import {
3131
} from '../../../redux/headlampEventSlice';
3232
import { AppDispatch } from '../../../redux/stores/store';
3333
import ActionButton, { ButtonStyle } from '../ActionButton';
34-
import ConfirmDialog from '../ConfirmDialog';
3534
import AuthVisible from './AuthVisible';
35+
import RollbackDialog from './RollbackDialog';
3636

3737
/**
3838
* Interface for resources that support rollback.
3939
*/
4040
export interface RollbackableResource extends KubeObject {
41-
rollback: () => Promise<RollbackResult>;
41+
rollback: (toRevision?: number) => Promise<RollbackResult>;
42+
getRevisionHistory: () => Promise<RevisionInfo[]>;
4243
}
4344

4445
/**
@@ -59,10 +60,10 @@ export interface RollbackButtonProps {
5960
}
6061

6162
/**
62-
* RollbackButton component for rolling back Workloads to their previous revision.
63+
* RollbackButton component for rolling back Workloads to a selected revision.
6364
*
64-
* This component provides a button that, when clicked, shows a confirmation dialog
65-
* and then initiates a rollback operation.
65+
* This component provides a button that, when clicked, shows a revision selection
66+
* dialog where the user can pick a specific revision to rollback to.
6667
*
6768
* Supported Resources:
6869
* - Deployment (via ReplicaSets)
@@ -84,20 +85,21 @@ export function RollbackButton(props: RollbackButtonProps) {
8485

8586
const resource = item;
8687
const resourceKind = resource.kind;
88+
const getRevisionHistory = useCallback(() => resource.getRevisionHistory(), [resource]);
8789

88-
async function performRollback() {
89-
const result = await resource.rollback();
90+
async function performRollback(toRevision?: number) {
91+
const result = await resource.rollback(toRevision);
9092
if (!result.success) {
9193
throw new Error(result.message);
9294
}
9395
return result;
9496
}
9597

96-
function handleConfirm() {
98+
function handleConfirm(toRevision?: number) {
9799
const itemName = resource.metadata.name;
98100

99101
dispatch(
100-
clusterAction(() => performRollback(), {
102+
clusterAction(() => performRollback(toRevision), {
101103
startMessage: t('Rolling back {{ itemName }} to previous version…', { itemName }),
102104
cancelledMessage: t('Cancelled rollback of {{ itemName }}.', { itemName }),
103105
successMessage: t('Rolled back {{ itemName }} to previous version.', { itemName }),
@@ -137,19 +139,13 @@ export function RollbackButton(props: RollbackButtonProps) {
137139
}}
138140
icon="mdi:undo-variant"
139141
/>
140-
<ConfirmDialog
142+
<RollbackDialog
141143
open={openDialog}
142-
title={t('translation|Rollback {{ kind }}', { kind: resourceKind })}
143-
description={t(
144-
'translation|Are you sure you want to rollback "{{ name }}" to the previous version? This will replace the current pod template with the one from the previous revision.',
145-
{
146-
name: resource.metadata.name,
147-
}
148-
)}
149-
handleClose={() => setOpenDialog(false)}
144+
resourceKind={resourceKind}
145+
resourceName={resource.metadata.name}
146+
getRevisionHistory={getRevisionHistory}
147+
onClose={() => setOpenDialog(false)}
150148
onConfirm={handleConfirm}
151-
cancelLabel={t('Cancel')}
152-
confirmLabel={t('Rollback')}
153149
/>
154150
</AuthVisible>
155151
);

0 commit comments

Comments
 (0)