1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-27 01:19:00 +02:00

feat: search page - improved change request tooltip (#9750)

improved and tested tooltip for change requests and adjusted column width
This commit is contained in:
Tymoteusz Czech 2025-04-15 13:22:50 +02:00 committed by GitHub
parent 01c1ec5c29
commit 3fd74262bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 184 additions and 35 deletions

View File

@ -69,13 +69,10 @@ export interface IHtmlTooltipProps extends TooltipProps {
maxHeight?: SpacingArgument;
fontSize?: string;
tabIndex?: number;
disableFocusListener?: boolean;
}
export const HtmlTooltip = (props: IHtmlTooltipProps) => {
if (!props.title) return props.children;
return (
<StyledHtmlTooltip {...props} disableFocusListener>
{props.children}
</StyledHtmlTooltip>
);
return <StyledHtmlTooltip {...props}>{props.children}</StyledHtmlTooltip>;
};

View File

@ -54,7 +54,13 @@ export const FeatureLifecycleCell: VFC<IFeatureLifecycleProps> = ({
: [];
return (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box
sx={(theme) => ({
display: 'flex',
justifyContent: 'center',
padding: theme.spacing(0, expanded ? 1 : 0),
})}
>
<FeatureLifecycle
onArchive={onArchive}
onComplete={onComplete}

View File

@ -197,7 +197,6 @@ export const FeatureToggleListTable: FC = () => {
<StatusCell {...original} />
),
enableSorting: false,
size: 80,
}),
columnHelper.accessor('project', {
header: 'Project',
@ -208,10 +207,12 @@ export const FeatureToggleListTable: FC = () => {
)?.name;
return (
<LinkCell
title={projectName || projectId}
to={`/projects/${projectId}`}
/>
<Box sx={{ minWidth: '180px' }}>
<LinkCell
title={projectName || projectId}
to={`/projects/${projectId}`}
/>
</Box>
);
},
}),

View File

@ -0,0 +1,84 @@
import { render } from 'utils/testRenderer';
import { StatusCell } from './StatusCell';
import { screen } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
describe('StatusCell', () => {
it('displays "" as default status', () => {
const { getByText } = render(
<StatusCell
project='test-project'
lifecycle={{
stage: 'live',
enteredStageAt: '2025-04-01T00:00:00Z',
}}
environments={[
{
name: 'production',
type: 'production',
enabled: true,
hasStrategies: true,
hasEnabledStrategies: true,
changeRequestIds: [],
},
]}
/>,
);
expect(getByText('')).toBeInTheDocument();
});
it('shows "change requests" icon', () => {
const { getByTestId } = render(
<StatusCell
project='test-project'
lifecycle={{
stage: 'live',
enteredStageAt: '2025-04-01T00:00:00Z',
}}
environments={[
{
name: 'production',
type: 'production',
enabled: true,
hasStrategies: true,
hasEnabledStrategies: true,
changeRequestIds: [123],
},
]}
/>,
);
expect(getByTestId('change-requests-icon')).toBeInTheDocument();
});
it('shows change requests on focus', async () => {
const ui = (
<StatusCell
project='test-project'
lifecycle={{
stage: 'live',
enteredStageAt: '2025-04-01T00:00:00Z',
}}
environments={[
{
name: 'production',
type: 'production',
enabled: true,
hasStrategies: true,
hasEnabledStrategies: true,
changeRequestIds: [123],
},
]}
/>
);
const { getByTestId, getByText, rerender } = render(ui);
expect(await screen.queryByText('Change requests:')).toBeNull();
await userEvent.hover(getByTestId('change-requests-icon'));
await screen.findByRole('tooltip');
expect(
await screen.queryByText('Change requests:'),
).toBeInTheDocument();
expect(getByText('#123')).toBeInTheDocument();
});
});

View File

@ -5,25 +5,39 @@ import { getStatus } from './getStatus';
import DifferenceIcon from '@mui/icons-material/Difference';
import { Link } from 'react-router-dom';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { Truncator } from 'component/common/Truncator/Truncator';
const Container = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
gap: theme.spacing(0.5),
minWidth: '180px',
}));
const ChangeRequestIcon = styled(DifferenceIcon)(({ theme }) => ({
color: theme.palette.primary.main,
fontSize: theme.spacing(2.5),
marginLeft: theme.spacing(0.5),
fontSize: theme.spacing(3.5),
padding: theme.spacing(0.5),
}));
export const StatusCell: FC<FeatureSearchResponseSchema> = ({
lifecycle,
environments,
project,
}) => {
const ChangeRequestTooltip = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: theme.spacing(0.5),
ul: {
listStyle: 'none',
padding: 0,
margin: 0,
display: 'flex',
flexDirection: 'column',
},
}));
export const StatusCell: FC<
Pick<FeatureSearchResponseSchema, 'lifecycle' | 'environments' | 'project'>
> = ({ lifecycle, environments, project }) => {
const status = useMemo(
() => getStatus({ lifecycle, environments }),
[lifecycle, environments],
@ -35,28 +49,35 @@ export const StatusCell: FC<FeatureSearchResponseSchema> = ({
return (
<Container>
<div>{status}</div>
<Truncator title={status} lines={2}>
{status}
</Truncator>
{changeRequestIds.length > 0 && (
<HtmlTooltip
arrow
title={
<div>
<span>Change requests:</span>
<br />
{changeRequestIds.map((id) => (
<Link
key={id}
to={`/projects/${project}/change-requests/${id}`}
target='_blank'
rel='noopener noreferrer'
>
{`#${id}`}
</Link>
))}
</div>
<ChangeRequestTooltip>
<div>Change requests:</div>
<ul>
{changeRequestIds.map((id) => (
<li key={id}>
<Link
to={`/projects/${project}/change-requests/${id}`}
target='_blank'
rel='noopener noreferrer'
>
{`#${id}`}
</Link>
</li>
))}
</ul>
</ChangeRequestTooltip>
}
>
<ChangeRequestIcon />
<ChangeRequestIcon
data-testid='change-requests-icon'
tabIndex={0}
/>
</HtmlTooltip>
)}
</Container>

View File

@ -148,4 +148,44 @@ describe('getStatus', () => {
).toBe('No strategies');
});
});
describe('release plan - milestones', () => {
it('should show the release plan', () => {
expect(
getStatus({
environments: [
{
...prodEnvEnabled,
totalMilestones: 2,
milestoneOrder: 0,
milestoneName: 'First step',
},
],
lifecycle: {
stage: 'live',
enteredStageAt: null as any,
},
}),
).toBe('Milestone: First step (1 of 2)');
});
it('should not show the milestone if a flag is disabled', () => {
expect(
getStatus({
environments: [
{
...prodEnvDisabled,
totalMilestones: 2,
milestoneOrder: 0,
milestoneName: 'First step',
},
],
lifecycle: {
stage: 'live',
enteredStageAt: null as any,
},
}),
).not.toBe('Release plan');
});
});
});