[Uptime] fix step detail on mobile resolutions [fix #121919] (#122171) (#122551)

(cherry picked from commit 91cea8afec)

Co-authored-by: Lucas F. da Costa <lucas@lucasfcosta.com>
This commit is contained in:
Kibana Machine 2022-01-10 11:16:20 -05:00 committed by GitHub
parent 6589c97aea
commit 44391b7aa4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 143 additions and 20 deletions

View file

@ -34,6 +34,10 @@ export const UptimePageTemplateComponent: React.FC<Props> = ({ path, pageHeader,
.euiPageHeaderContent > .euiFlexGroup {
flex-wrap: wrap;
}
.euiPageHeaderContent > .euiFlexGroup > .euiFlexItem {
align-items: center;
}
`;
}, [PageTemplateComponent]);

View file

@ -16,14 +16,14 @@ interface Props {
export const StepImage = ({ step }: Props) => {
return (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexGroup alignItems="center" gutterSize="s" wrap>
<EuiFlexItem grow={false}>
<PingTimestamp
checkGroup={step.monitor.check_group}
initialStepNo={step.synthetics?.step?.index}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false} style={{ minWidth: 80 }}>
<EuiText>{step.synthetics?.step?.name}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -8,7 +8,8 @@
import React from 'react';
import { JourneyStep } from '../../../../common/runtime_types/ping';
import { StepsList } from './steps_list';
import { render } from '../../../lib/helper/rtl_helpers';
import { render, forDesktopOnly, forMobileOnly } from '../../../lib/helper/rtl_helpers';
import { VIEW_PERFORMANCE } from '../../monitor/synthetics/translations';
describe('StepList component', () => {
let steps: JourneyStep[];
@ -80,7 +81,7 @@ describe('StepList component', () => {
it('renders a link to the step detail view', () => {
const { getByTitle, getByTestId } = render(<StepsList data={[steps[0]]} loading={false} />);
expect(getByTestId('step-detail-link')).toHaveAttribute('href', '/journey/fake-group/step/1');
expect(getByTitle(`Failed`));
expect(forDesktopOnly(getByTitle, 'title')(`Failed`));
});
it.each([
@ -91,7 +92,7 @@ describe('StepList component', () => {
const step = steps[0];
step.synthetics!.payload!.status = status;
const { getByText } = render(<StepsList data={[step]} loading={false} />);
expect(getByText(expectedStatus));
expect(forDesktopOnly(getByText)(expectedStatus));
});
it('creates expected message for all succeeded', () => {
@ -149,4 +150,31 @@ describe('StepList component', () => {
expect(getByTestId('row-fake-group'));
expect(getByTestId('row-fake-group-1'));
});
describe('Mobile Designs', () => {
// We don't need to resize the window here because EUI
// does all the manipulation of what is displayed through
// CSS. Therefore, it's enough to check what's actually
// rendered and its classes.
it('renders the step name and index', () => {
const { getByText } = render(<StepsList data={steps} loading={false} />);
expect(forMobileOnly(getByText)('1. load page')).toBeInTheDocument();
expect(forMobileOnly(getByText)('2. go to login')).toBeInTheDocument();
});
it('does not render the link to view step details', async () => {
const { queryByText } = render(<StepsList data={steps} loading={false} />);
expect(forMobileOnly(queryByText)(VIEW_PERFORMANCE)).not.toBeInTheDocument();
});
it('renders the status label', () => {
steps[0].synthetics!.payload!.status = 'succeeded';
steps[1].synthetics!.payload!.status = 'skipped';
const { getByText } = render(<StepsList data={steps} loading={false} />);
expect(forMobileOnly(getByText)('Succeeded')).toBeInTheDocument();
expect(forMobileOnly(getByText)('Skipped')).toBeInTheDocument();
});
});
});

View file

@ -5,7 +5,15 @@
* 2.0.
*/
import { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, EuiTitle } from '@elastic/eui';
import {
EuiBasicTable,
EuiBasicTableColumn,
EuiButtonIcon,
EuiTitle,
EuiFlexItem,
EuiText,
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { MouseEvent, useState } from 'react';
import styled from 'styled-components';
@ -89,12 +97,37 @@ export const StepsList = ({ data, error, loading }: Props) => {
render: (pingStatus: string, item) => (
<StatusBadge status={pingStatus} stepNo={item.synthetics?.step?.index!} />
),
mobileOptions: {
render: (item) => (
<EuiFlexItem grow={false}>
<StatusBadge
isMobile={true}
status={item.synthetics?.payload?.status}
stepNo={item.synthetics?.step?.index!}
/>
</EuiFlexItem>
),
width: '20%',
header: STATUS_LABEL,
enlarge: false,
},
},
{
align: 'left',
field: 'timestamp',
name: STEP_NAME_LABEL,
render: (_timestamp: string, item) => <StepImage step={item} />,
mobileOptions: {
render: (item: JourneyStep) => (
<EuiText>
<strong>
{item.synthetics?.step?.index!}. {item.synthetics?.step?.name}
</strong>
</EuiText>
),
header: 'Step',
enlarge: true,
},
},
{
name: 'Step duration',
@ -107,6 +140,12 @@ export const StepsList = ({ data, error, loading }: Props) => {
/>
);
},
mobileOptions: {
header: i18n.translate('xpack.uptime.pingList.stepDurationHeader', {
defaultMessage: 'Step duration',
}),
show: true,
},
},
{
align: 'left',
@ -120,11 +159,12 @@ export const StepsList = ({ data, error, loading }: Props) => {
{VIEW_PERFORMANCE}
</StepDetailLink>
),
mobileOptions: { show: false },
},
{
align: 'right',
width: '24px',
width: '40px',
align: RIGHT_ALIGNMENT,
isExpander: true,
render: (journeyStep: JourneyStep) => {
return (
@ -143,7 +183,6 @@ export const StepsList = ({ data, error, loading }: Props) => {
const { monitor } = item;
return {
height: '85px',
'data-test-subj': `row-${monitor.check_group}`,
onClick: (evt: MouseEvent) => {
const targetElem = evt.target as HTMLElement;

View file

@ -30,4 +30,10 @@ describe('StatusBadge', () => {
expect(getByText('3.'));
expect(getByText('Skipped'));
});
it('hides the step number on mobile', () => {
const { queryByText } = render(<StatusBadge status="skipped" stepNo={3} isMobile />);
expect(queryByText('3.')).not.toBeInTheDocument();
expect(queryByText('Skipped')).toBeInTheDocument();
});
});

View file

@ -12,6 +12,7 @@ import { UptimeAppColors } from '../../apps/uptime_app';
import { UptimeThemeContext } from '../../contexts';
interface StatusBadgeProps {
isMobile?: boolean;
status?: string;
stepNo: number;
}
@ -46,15 +47,17 @@ export function textFromStatus(status?: string) {
}
}
export const StatusBadge: FC<StatusBadgeProps> = ({ status, stepNo }) => {
export const StatusBadge: FC<StatusBadgeProps> = ({ status, stepNo, isMobile }) => {
const theme = useContext(UptimeThemeContext);
return (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText>
<strong>{stepNo}.</strong>
</EuiText>
</EuiFlexItem>
{!isMobile && (
<EuiFlexItem grow={false}>
<EuiText>
<strong>{stepNo}.</strong>
</EuiText>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiBadge color={colorFromStatus(theme.colors, status)}>{textFromStatus(status)}</EuiBadge>
</EuiFlexItem>

View file

@ -256,3 +256,38 @@ export const makeUptimePermissionsCore = (
},
};
};
// This function filters out the queried elements which appear only
// either on mobile or desktop.
//
// It does so by filtering those with the class passed as the `classWrapper`.
// For mobile, we filter classes which tell elements to be hidden on desktop.
// For desktop, we do the opposite.
//
// We have this function because EUI will manipulate the visibility of some
// elements through pure CSS, which we can't assert on tests. Therefore,
// we look for the corresponding class wrapper.
const finderWithClassWrapper =
(classWrapper: string) =>
(
getterFn: (f: MatcherFunction) => HTMLElement | null,
customAttribute?: keyof Element | keyof HTMLElement
) =>
(text: string): HTMLElement | null =>
getterFn((_content: string, node: Nullish<Element>) => {
if (!node) return false;
// There are actually properties that are not in Element but which
// appear on the `node`, so we must cast the customAttribute as a keyof Element
const content = node[(customAttribute as keyof Element) ?? 'innerHTML'];
if (content === text && wrappedInClass(node, classWrapper)) return true;
return false;
});
const wrappedInClass = (element: HTMLElement | Element, classWrapper: string): boolean => {
if (element.className.includes(classWrapper)) return true;
if (element.parentElement) return wrappedInClass(element.parentElement, classWrapper);
return false;
};
export const forMobileOnly = finderWithClassWrapper('hideForDesktop');
export const forDesktopOnly = finderWithClassWrapper('hideForMobile');

View file

@ -12,6 +12,7 @@ import { useHistory } from 'react-router-dom';
import moment from 'moment';
import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types/ping';
import { getShortTimeStamp } from '../../components/overview/monitor_list/columns/monitor_status_column';
import { useBreakpoints } from '../../../public/hooks/use_breakpoints';
interface Props {
timestamp: string;
@ -20,11 +21,15 @@ interface Props {
export const ChecksNavigation = ({ timestamp, details }: Props) => {
const history = useHistory();
const { down } = useBreakpoints();
const isMobile = down('s');
return (
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="none">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size={isMobile ? 'xs' : 'm'}
iconType="arrowLeft"
isDisabled={!details?.previous}
onClick={() => {
@ -37,11 +42,14 @@ export const ChecksNavigation = ({ timestamp, details }: Props) => {
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiText className="eui-textNoWrap">{getShortTimeStamp(moment(timestamp))}</EuiText>
<EuiFlexItem grow={false}>
<EuiText size={isMobile ? 'xs' : 'm'} className="eui-textNoWrap">
{getShortTimeStamp(moment(timestamp))}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size={isMobile ? 'xs' : 'm'}
iconType="arrowRight"
iconSide="right"
isDisabled={!details?.next}