diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx index df4c73908b627..425f35b067026 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx @@ -8,15 +8,17 @@ import React, { useContext, useEffect, useState } from 'react'; import useIntersection from 'react-use/lib/useIntersection'; import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + import { isScreenshotImageBlob, isScreenshotRef, ScreenshotRefImageData, -} from '../../../../../../common/runtime_types/ping'; +} from '../../../../../../common/runtime_types'; import { useFetcher, FETCH_STATUS } from '../../../../../../../observability/public'; import { getJourneyScreenshot } from '../../../../../state/api/journey'; import { UptimeSettingsContext } from '../../../../../contexts'; + import { NoImageDisplay } from './no_image_display'; import { StepImageCaption } from './step_image_caption'; import { StepImagePopover } from './step_image_popover'; @@ -129,9 +131,12 @@ export const PingTimestamp = ({ label, checkGroup, initialStepNo = 1 }: Props) = )} - - {label} - + + {label && ( + + {label} + + )} ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx index a2858348ed59c..3fa94e45f8937 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx @@ -6,10 +6,13 @@ */ import React, { MouseEvent, useEffect } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { nextAriaLabel, prevAriaLabel } from './translations'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; + import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { ScreenshotRefImageData } from '../../../../../../common/runtime_types'; +import { useBreakpoints } from '../../../../../hooks'; + +import { nextAriaLabel, prevAriaLabel } from './translations'; export interface StepImageCaptionProps { captionContent: string; @@ -23,13 +26,6 @@ export interface StepImageCaptionProps { isLoading: boolean; } -const ImageCaption = euiStyled.div` - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - display: inline-block; - width: 100%; - text-decoration: none; -`; - export const StepImageCaption: React.FC = ({ captionContent, imgRef, @@ -41,6 +37,9 @@ export const StepImageCaption: React.FC = ({ label, onVisible, }) => { + const { euiTheme } = useEuiTheme(); + const breakpoints = useBreakpoints(); + useEffect(() => { onVisible(true); return () => { @@ -49,8 +48,10 @@ export const StepImageCaption: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isSmall = breakpoints.down('m'); + return ( - { // we don't want this to be captured by row click which leads to step list page evt.stopPropagation(); @@ -59,8 +60,9 @@ export const StepImageCaption: React.FC = ({
{(imgSrc || imgRef) && ( - + ) => { setStepNumber(stepNumber - 1); @@ -74,10 +76,11 @@ export const StepImageCaption: React.FC = ({ - {captionContent} + {captionContent} - + ) => { setStepNumber(stepNumber + 1); @@ -93,8 +96,21 @@ export const StepImageCaption: React.FC = ({ )} - {label} + + {label} +
-
+ ); }; + +const CaptionWrapper = euiStyled.div` + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + display: inline-block; + width: 100%; + text-decoration: none; +`; + +const SecondaryText = euiStyled(EuiText)((props) => ({ + color: props.theme.eui.euiTextColor, +})); diff --git a/x-pack/plugins/uptime/public/hooks/index.ts b/x-pack/plugins/uptime/public/hooks/index.ts index 3e4714384e654..e96d746a05514 100644 --- a/x-pack/plugins/uptime/public/hooks/index.ts +++ b/x-pack/plugins/uptime/public/hooks/index.ts @@ -12,4 +12,5 @@ export * from './use_search_text'; export * from './use_cert_status'; export * from './use_telemetry'; export * from './use_url_params'; +export * from './use_breakpoints'; export { useIndexPattern } from '../contexts/uptime_index_pattern_context'; diff --git a/x-pack/plugins/uptime/public/hooks/use_breakpoints.test.ts b/x-pack/plugins/uptime/public/hooks/use_breakpoints.test.ts new file mode 100644 index 0000000000000..d417d98dcb76d --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_breakpoints.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BREAKPOINTS } from '@elastic/eui'; +import { renderHook } from '@testing-library/react-hooks'; +import { useBreakpoints } from './use_breakpoints'; + +describe('use_breakpoints', () => { + describe('useBreakpoints', () => { + const width = global.innerWidth; + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + afterAll(() => { + (global as { innerWidth: number }).innerWidth = width; + }); + + it('should only return up => false and down => true for "xs" when width is less than BREAKPOINTS.xs', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.xs - 1; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('xs')).toBeFalsy(); + expect(result.current.down('xs')).toBeTruthy(); + }); + + it('should only return up => true and down => false for "xs" when width is above or equal BREAKPOINTS.xs', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.xs; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('xs')).toBeTruthy(); + expect(result.current.down('xs')).toBeFalsy(); + }); + + it('should return down => true for "m" when width equals BREAKPOINTS.l', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.l; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('m')).toBeTruthy(); + expect(result.current.down('m')).toBeFalsy(); + }); + + it('should return `between` => true for "m" and "xl" when width equals BREAKPOINTS.l', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.l; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.between('m', 'xl')).toBeTruthy(); + }); + + it('should return `between` => true for "s" and "m" when width equals BREAKPOINTS.s', () => { + (global as { innerWidth: number }).innerWidth = BREAKPOINTS.s; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.between('s', 'm')).toBeTruthy(); + }); + + it('should return up => true for all when size is > xxxl+', () => { + (global as { innerWidth: number }).innerWidth = 3000; + const { result } = renderHook(() => useBreakpoints()); + + expect(result.current.up('xs')).toBeTruthy(); + expect(result.current.up('s')).toBeTruthy(); + expect(result.current.up('m')).toBeTruthy(); + expect(result.current.up('l')).toBeTruthy(); + expect(result.current.up('xl')).toBeTruthy(); + expect(result.current.up('xxl')).toBeTruthy(); + expect(result.current.up('xxxl')).toBeTruthy(); + }); + + it('should determine `isIpad (Portrait)', () => { + (global as { innerWidth: number }).innerWidth = 768; + const { result } = renderHook(() => useBreakpoints()); + + const isIpad = result.current.up('m') && result.current.down('l'); + expect(isIpad).toEqual(true); + }); + + it('should determine `isMobile (Portrait)`', () => { + (global as { innerWidth: number }).innerWidth = 480; + const { result } = renderHook(() => useBreakpoints()); + + const isMobile = result.current.up('xs') && result.current.down('s'); + expect(isMobile).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/hooks/use_breakpoints.ts b/x-pack/plugins/uptime/public/hooks/use_breakpoints.ts new file mode 100644 index 0000000000000..9398a5fcd15fe --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_breakpoints.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState } from 'react'; +import useWindowSize from 'react-use/lib/useWindowSize'; +import useDebounce from 'react-use/lib/useDebounce'; + +import { BREAKPOINTS, EuiBreakpointSize } from '@elastic/eui'; + +// Custom breakpoints +const BREAKPOINT_XL = 1599; // Overriding the theme's default 'xl' breakpoint +const BREAKPOINT_XXL = 1599; +const BREAKPOINT_XXXL = 2000; + +export type BreakpointKey = EuiBreakpointSize | 'xxl' | 'xxxl'; + +type BreakpointPredicate = (breakpointKey: BreakpointKey) => boolean; +type BreakpointRangePredicate = (from: BreakpointKey, to: BreakpointKey) => boolean; + +/** + * Returns the predicates functions used to determine whether the current device's width is above or below the asked + * breakpoint. (Implementation inspired by React Material UI). + * + * @example + * const { breakpoints } = useBreakpoints(); + * const isMobile = breakpoint.down('m'); + * + * @example + * const { breakpoints } = useBreakpoints(); + * const isTablet = breakpoint.between('m', 'l'); + * + * @param debounce {number} Debounce interval for optimization + * + * @returns { {up: BreakpointPredicate, down: BreakpointPredicate, between: BreakpointRangePredicate} } + * Returns object containing predicates which determine whether the current device's width lies above, below or + * in-between the given breakpoint(s) + * { + * up => Returns `true` if the current width is equal or above (inclusive) the given breakpoint size, + * or `false` otherwise. + * down => Returns `true` if the current width is below (exclusive) the given breakpoint size, or `false` otherwise. + * between => Returns `true` if the current width is equal or above (inclusive) the corresponding size of + * `fromBreakpointKey` AND is below (exclusive) the corresponding width of `toBreakpointKey`. + * Returns `false` otherwise. + * } + */ +export function useBreakpoints(debounce = 50) { + const { width } = useWindowSize(); + const [debouncedWidth, setDebouncedWidth] = useState(width); + + const up = useCallback( + (breakpointKey: BreakpointKey) => isUp(debouncedWidth, breakpointKey), + [debouncedWidth] + ); + const down = useCallback( + (breakpointKey: BreakpointKey) => isDown(debouncedWidth, breakpointKey), + [debouncedWidth] + ); + + const between = useCallback( + (fromBreakpointKey: BreakpointKey, toBreakpointKey: BreakpointKey) => + isBetween(debouncedWidth, fromBreakpointKey, toBreakpointKey), + [debouncedWidth] + ); + + useDebounce( + () => { + setDebouncedWidth(width); + }, + debounce, + [width] + ); + + return { up, down, between, debouncedWidth }; +} + +/** + * Returns the corresponding device width against the provided breakpoint key, either the overridden value or the + * default value from theme. + * @param key {BreakpointKey} string key representing the device breakpoint e.g. 'xs', 's', 'xxxl' + */ +function getSizeForBreakpointKey(key: BreakpointKey): number { + switch (key) { + case 'xxxl': + return BREAKPOINT_XXXL; + case 'xxl': + return BREAKPOINT_XXL; + case 'xl': + return BREAKPOINT_XL; + case 'l': + return BREAKPOINTS.l; + case 'm': + return BREAKPOINTS.m; + case 's': + return BREAKPOINTS.s; + } + + return BREAKPOINTS.xs; +} + +function isUp(size: number, breakpointKey: BreakpointKey) { + return size >= getSizeForBreakpointKey(breakpointKey); +} + +function isDown(size: number, breakpointKey: BreakpointKey) { + return size < getSizeForBreakpointKey(breakpointKey); +} + +function isBetween(size: number, fromBreakpointKey: BreakpointKey, toBreakpointKey: BreakpointKey) { + return isUp(size, fromBreakpointKey) && isDown(size, toBreakpointKey); +}