Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
201d197
feat(setup-card): add release listing API and readiness hooks
kaviththiranga May 11, 2026
6138576
refactor(workload-config): decouple release creation from save flow
kaviththiranga May 11, 2026
cfff8ef
feat(setup-card): split release creation from deployment
kaviththiranga May 11, 2026
d430489
refactor(setup-card): unify Create release into workload flow, simpli…
kaviththiranga May 11, 2026
746dbed
feat(overrides): show View diff on deploy flows, not just promote
kaviththiranga May 11, 2026
d59de10
feat(openchoreo): replace release dropdown with full-featured browser…
kaviththiranga May 12, 2026
6803506
feat(openchoreo): flag overrides not in the release's workload
kaviththiranga May 12, 2026
cc8f31a
feat(openchoreo): simplify setup card under auto-deploy
kaviththiranga May 12, 2026
2b10553
feat(openchoreo): action-aware titles on configure overrides page
kaviththiranga May 13, 2026
2c3f4a0
feat(detail-page-layout): add Esc shortcut and fix viewport height bound
kaviththiranga May 13, 2026
3e2e937
feat(deploy): polish setup card UX and deploy panel layout
kaviththiranga May 13, 2026
f280b73
feat(deploy-canvas): differentiate setup tile from env tiles
kaviththiranga May 13, 2026
0f37ffd
fix(yaml-diff): make merge view scroll as a single unit
kaviththiranga May 13, 2026
ade0e70
fix(release-picker): correct image path so search and meta render
kaviththiranga May 13, 2026
85a24a5
chore(deploy): unify auto-deploy copy and rename diff button
kaviththiranga May 13, 2026
dfa4c34
feat(release-browser): add compare mode to diff two releases
kaviththiranga May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions plugins/openchoreo-backend/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,28 @@ export async function createRouter({
res.json({ success: true, data: { items } });
});

router.get('/component-releases', async (req, res) => {
const { componentName, namespaceName } = req.query;

if (!componentName || !namespaceName) {
throw new InputError(
'componentName and namespaceName are required query parameters',
);
}

const userToken = getUserTokenFromRequest(req);

const rawReleases = await environmentInfoService.listComponentReleases(
{
componentName: componentName as string,
namespaceName: namespaceName as string,
},
userToken,
);
const items = (rawReleases as any)?.items ?? [];
res.json({ success: true, data: { items } });
});

router.put('/update-release-binding', requireAuth, async (req, res) => {
const {
componentName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,60 @@ export class EnvironmentInfoService implements EnvironmentService {
}
}

/**
* Lists component releases for a specific component within a namespace.
*
* @param request.componentName - Name of the component to filter releases by
* @param request.namespaceName - Name of the namespace
* @returns Paginated list of component releases (frozen workload snapshots)
*/
async listComponentReleases(
request: {
componentName: string;
namespaceName: string;
},
token?: string,
) {
const startTime = Date.now();
this.logger.debug(
`Listing component releases for ${request.componentName}`,
);

try {
const client = createOpenChoreoApiClient({
baseUrl: this.baseUrl,
token,
logger: this.logger,
});

const { data, error, response } = await client.GET(
'/api/v1/namespaces/{namespaceName}/componentreleases',
{
params: {
path: { namespaceName: request.namespaceName },
query: { component: request.componentName },
},
},
);

assertApiResponse({ data, error, response }, 'list component releases');

const totalTime = Date.now() - startTime;
this.logger.debug(
`Component releases listed for ${request.componentName}: Total: ${totalTime}ms`,
);

return data;
} catch (error: unknown) {
const totalTime = Date.now() - startTime;
this.logger.error(
`Error listing component releases for ${request.componentName} (${totalTime}ms):`,
error as Error,
);
throw error;
}
}

/**
* Creates or updates a release binding for deploy/promote actions.
* If the binding doesn't exist, creates it with POST.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { DetailPageLayout } from './DetailPageLayout';

describe('DetailPageLayout', () => {
it('calls onBack when Escape is pressed', () => {
const onBack = jest.fn();
render(
<DetailPageLayout title="Test" onBack={onBack}>
<div>body</div>
</DetailPageLayout>,
);

fireEvent.keyDown(window, { key: 'Escape' });
expect(onBack).toHaveBeenCalledTimes(1);
});

it('does not call onBack when Escape is pressed while an input is focused', () => {
const onBack = jest.fn();
render(
<DetailPageLayout title="Test" onBack={onBack}>
<input data-testid="field" />
</DetailPageLayout>,
);

const input = screen.getByTestId('field') as HTMLInputElement;
input.focus();
expect(document.activeElement).toBe(input);

fireEvent.keyDown(window, { key: 'Escape' });
expect(onBack).not.toHaveBeenCalled();
});

it('renders the Esc shortcut chip in the header', () => {
render(
<DetailPageLayout title="Test" onBack={jest.fn()}>
<div />
</DetailPageLayout>,
);
expect(screen.getByLabelText(/press escape to go back/i)).toHaveTextContent(
'Esc',
);
});
});
Original file line number Diff line number Diff line change
@@ -1,35 +1,60 @@
import { Box, IconButton, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
import type { ReactNode } from 'react';
import { useEffect, type ReactNode } from 'react';

const useStyles = makeStyles(theme => ({
container: {
display: 'flex',
flexDirection: 'column',
// Constrain height to prevent page-level scrolling
// Accounts for: Backstage header (~72px) + tabs (~69px) + Content padding (48px) + buffer
height: 'calc(100vh - 240px)',
maxHeight: 'calc(100vh - 240px)',
// Match the deploy list view's height contract so the inner page never
// pushes Backstage's <Page> into external scroll. 200px accounts for the
// entity header (~104px) + tab strip (~50px) + <Content> 24px top + 24px
// bottom + a small bottom buffer. `min-height` covers tiny viewports.
height: 'calc(100vh - 200px)',
minHeight: 480,
overflow: 'hidden',
},
header: {
backgroundColor: theme.palette.background.paper,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(2, 0),
padding: theme.spacing(2),
borderBottom: `1px solid ${theme.palette.divider}`,
flexShrink: 0,
},
backControl: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 2,
marginRight: theme.spacing(1),
},
kbdChip: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: 22,
height: 14,
padding: '0 4px',
border: `1px solid ${theme.palette.divider}`,
borderRadius: 3,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.secondary,
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
fontSize: 9,
lineHeight: 1,
fontWeight: 500,
letterSpacing: '0.04em',
textTransform: 'uppercase',
},
headerLeft: {
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
},
backButton: {
marginRight: theme.spacing(1),
},
titleContainer: {
display: 'flex',
flexDirection: 'column',
Expand All @@ -51,12 +76,12 @@ const useStyles = makeStyles(theme => ({
content: {
flex: 1,
overflowY: 'auto',
paddingTop: theme.spacing(2),
padding: theme.spacing(2),
},
}));

export interface DetailPageLayoutProps {
title: string;
title: ReactNode;
subtitle?: ReactNode;
onBack: () => void;
actions?: ReactNode;
Expand All @@ -76,18 +101,45 @@ export const DetailPageLayout = ({
}: DetailPageLayoutProps) => {
const classes = useStyles();

// Esc closes the page via the same path as the back arrow, so the
// unsaved-changes dialog (when present) still fires. Skip when the user
// is typing in a field — Esc-while-typing is a common reflex.
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
const target = document.activeElement as HTMLElement | null;
if (!target) {
onBack();
return;
}
const tag = target.tagName;
const isEditable =
tag === 'INPUT' ||
tag === 'TEXTAREA' ||
tag === 'SELECT' ||
target.isContentEditable;
if (isEditable) return;
onBack();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onBack]);

return (
<Box className={classes.container}>
<Box className={classes.header}>
<Box className={classes.headerLeft}>
<IconButton
onClick={onBack}
size="small"
className={classes.backButton}
title="Back"
>
<ArrowBackIcon />
</IconButton>
<Box className={classes.backControl}>
<IconButton onClick={onBack} size="small" title="Back (Esc)">
<ArrowBackIcon />
</IconButton>
<kbd
className={classes.kbdChip}
aria-label="Press Escape to go back"
>
Esc
</kbd>
</Box>
<Box className={classes.titleContainer}>
<Typography variant="h5" className={classes.title}>
{title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@ const useStyles = makeStyles((theme: Theme) => ({
backgroundColor: theme.palette.success.light,
color: theme.palette.success.contrastText,
},
extra: {
backgroundColor: theme.palette.warning.light,
color: theme.palette.warning.contrastText,
},
}));

const statusConfig: Record<
EnvVarStatus,
{
label: string;
tooltip: string;
className: 'inherited' | 'overridden' | 'new';
className: 'inherited' | 'overridden' | 'new' | 'extra';
}
> = {
inherited: {
Expand All @@ -55,6 +59,11 @@ const statusConfig: Record<
tooltip: 'New environment variable',
className: 'new',
},
extra: {
label: 'Extra',
tooltip: 'Not in current workload',
className: 'extra',
},
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ const useStyles = makeStyles((theme: Theme) => ({
backgroundColor: theme.palette.success.light,
color: theme.palette.success.contrastText,
},
extra: {
backgroundColor: theme.palette.warning.light,
color: theme.palette.warning.contrastText,
},
}));

const statusConfig: Record<
FileVarStatus,
{
label: string;
tooltip: string;
className: 'inherited' | 'overridden' | 'new';
className: 'inherited' | 'overridden' | 'new' | 'extra';
}
> = {
inherited: {
Expand All @@ -58,6 +62,11 @@ const statusConfig: Record<
tooltip: 'New file mount',
className: 'new',
},
extra: {
label: 'Extra',
tooltip: 'Not in current workload',
className: 'extra',
},
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { useState, type FC, type ReactNode } from 'react';
import { Box, Typography, IconButton, Collapse, Chip } from '@material-ui/core';
import {
Box,
Typography,
IconButton,
Collapse,
Chip,
Tooltip,
} from '@material-ui/core';
import HelpOutlineIcon from '@material-ui/icons/HelpOutline';
import { makeStyles, Theme } from '@material-ui/core/styles';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import ExpandLessIcon from '@material-ui/icons/ExpandLess';
Expand All @@ -9,6 +17,8 @@ export type GroupedSectionStatus = 'overridden' | 'new' | 'inherited';
export interface GroupedSectionProps {
/** Section title (falls back to default based on status if not provided) */
title?: string;
/** Optional tooltip explaining the section; renders a help icon next to the title. */
titleTooltip?: string;
/** Number of items in section */
count: number;
/** Status type for color accent */
Expand Down Expand Up @@ -77,6 +87,10 @@ const useStyles = makeStyles((theme: Theme) => ({
fontSize: '1rem',
color: theme.palette.text.disabled,
},
helpIcon: {
fontSize: '0.95rem',
color: theme.palette.text.secondary,
},
content: {
paddingLeft: theme.spacing(1.5),
paddingTop: theme.spacing(1),
Expand All @@ -95,6 +109,7 @@ const statusTitles: Record<GroupedSectionStatus, string> = {
*/
export const GroupedSection: FC<GroupedSectionProps> = ({
title,
titleTooltip,
count,
status,
defaultExpanded = true,
Expand Down Expand Up @@ -136,6 +151,14 @@ export const GroupedSection: FC<GroupedSectionProps> = ({
<Box className={classes.header} onClick={handleToggle}>
<span className={`${classes.accentBar} ${getAccentClass()}`} />
<Typography className={classes.title}>{displayTitle}</Typography>
{titleTooltip && (
<Tooltip title={titleTooltip} arrow placement="top">
<HelpOutlineIcon
className={classes.helpIcon}
onClick={e => e.stopPropagation()}
/>
</Tooltip>
)}
<Chip
label={count}
size="small"
Expand Down
Loading
Loading