mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
316 lines
9.2 KiB
TypeScript
316 lines
9.2 KiB
TypeScript
import { GrafanaThemeV2, isDateTime, TimeOption, TimeRange, TimeZone } from '@grafana/data';
|
|
import { css, cx } from '@emotion/css';
|
|
import React, { memo, useState } from 'react';
|
|
import { useMedia } from 'react-use';
|
|
import { stylesFactory, useTheme2 } from '../../../themes';
|
|
import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
|
|
import { Icon } from '../../Icon/Icon';
|
|
import { mapRangeToTimeOption } from './mapper';
|
|
import { TimePickerTitle } from './TimePickerTitle';
|
|
import { TimeRangeForm } from './TimeRangeForm';
|
|
import { TimeRangeList } from './TimeRangeList';
|
|
import { TimePickerFooter } from './TimePickerFooter';
|
|
|
|
const getStyles = stylesFactory((theme: GrafanaThemeV2, isReversed, hideQuickRanges, isContainerTall) => {
|
|
return {
|
|
container: css`
|
|
background: ${theme.colors.background.secondary};
|
|
box-shadow: ${theme.shadows.z4};
|
|
position: absolute;
|
|
z-index: ${theme.zIndex.dropdown};
|
|
width: 546px;
|
|
top: 116%;
|
|
border-radius: 2px;
|
|
border: 1px solid ${theme.colors.border.weak};
|
|
${isReversed ? 'left' : 'right'}: 0;
|
|
|
|
@media only screen and (max-width: ${theme.breakpoints.values.lg}px) {
|
|
width: 262px;
|
|
}
|
|
`,
|
|
body: css`
|
|
display: flex;
|
|
height: ${isContainerTall ? '381px' : '217px'};
|
|
`,
|
|
leftSide: css`
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: ${isReversed ? 'none' : `1px solid ${theme.colors.border.weak}`};
|
|
width: ${!hideQuickRanges ? '60%' : '100%'};
|
|
overflow: hidden;
|
|
order: ${isReversed ? 1 : 0};
|
|
`,
|
|
rightSide: css`
|
|
width: 40% !important;
|
|
border-right: ${isReversed ? `1px solid ${theme.colors.border.weak}` : 'none'};
|
|
|
|
@media only screen and (max-width: ${theme.breakpoints.values.lg}px) {
|
|
width: 100% !important;
|
|
}
|
|
`,
|
|
spacing: css`
|
|
margin-top: 16px;
|
|
`,
|
|
};
|
|
});
|
|
|
|
const getNarrowScreenStyles = stylesFactory((theme: GrafanaThemeV2) => {
|
|
return {
|
|
header: css`
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 1px solid ${theme.colors.border.weak};
|
|
padding: 7px 9px 7px 9px;
|
|
`,
|
|
body: css`
|
|
border-bottom: 1px solid ${theme.colors.border.weak};
|
|
`,
|
|
form: css`
|
|
padding: 7px 9px 7px 9px;
|
|
`,
|
|
};
|
|
});
|
|
|
|
const getFullScreenStyles = stylesFactory((theme: GrafanaThemeV2, hideQuickRanges?: boolean) => {
|
|
return {
|
|
container: css`
|
|
padding-top: 9px;
|
|
padding-left: 11px;
|
|
padding-right: ${!hideQuickRanges ? '20%' : '11px'};
|
|
`,
|
|
title: css`
|
|
margin-bottom: 11px;
|
|
`,
|
|
recent: css`
|
|
flex-grow: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: flex-end;
|
|
padding-top: ${theme.spacing(1)};
|
|
`,
|
|
};
|
|
});
|
|
|
|
const getEmptyListStyles = stylesFactory((theme: GrafanaThemeV2) => {
|
|
return {
|
|
container: css`
|
|
padding: 12px;
|
|
margin: 12px;
|
|
|
|
a,
|
|
span {
|
|
font-size: 13px;
|
|
}
|
|
`,
|
|
link: css`
|
|
color: ${theme.colors.text.link};
|
|
`,
|
|
};
|
|
});
|
|
|
|
interface Props {
|
|
value: TimeRange;
|
|
onChange: (timeRange: TimeRange) => void;
|
|
onChangeTimeZone: (timeZone: TimeZone) => void;
|
|
timeZone?: TimeZone;
|
|
quickOptions?: TimeOption[];
|
|
otherOptions?: TimeOption[];
|
|
history?: TimeRange[];
|
|
showHistory?: boolean;
|
|
className?: string;
|
|
hideTimeZone?: boolean;
|
|
/** Reverse the order of relative and absolute range pickers. Used to left align the picker in forms */
|
|
isReversed?: boolean;
|
|
hideQuickRanges?: boolean;
|
|
}
|
|
|
|
export interface PropsWithScreenSize extends Props {
|
|
isFullscreen: boolean;
|
|
}
|
|
|
|
interface FormProps extends Omit<Props, 'history'> {
|
|
historyOptions?: TimeOption[];
|
|
}
|
|
|
|
export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = (props) => {
|
|
const {
|
|
quickOptions = [],
|
|
otherOptions = [],
|
|
isReversed,
|
|
isFullscreen,
|
|
hideQuickRanges,
|
|
timeZone,
|
|
value,
|
|
onChange,
|
|
history,
|
|
showHistory,
|
|
className,
|
|
hideTimeZone,
|
|
onChangeTimeZone,
|
|
} = props;
|
|
const isHistoryEmpty = !history?.length;
|
|
const isContainerTall =
|
|
(isFullscreen && showHistory) || (!isFullscreen && ((showHistory && !isHistoryEmpty) || !hideQuickRanges));
|
|
const theme = useTheme2();
|
|
const styles = getStyles(theme, isReversed, hideQuickRanges, isContainerTall);
|
|
const historyOptions = mapToHistoryOptions(history, timeZone);
|
|
|
|
return (
|
|
<div className={cx(styles.container, className)}>
|
|
<div className={styles.body}>
|
|
{isFullscreen && (
|
|
<div className={styles.leftSide}>
|
|
<FullScreenForm {...props} historyOptions={historyOptions} />
|
|
</div>
|
|
)}
|
|
{(!isFullscreen || !hideQuickRanges) && (
|
|
<CustomScrollbar className={styles.rightSide}>
|
|
{!isFullscreen && <NarrowScreenForm {...props} historyOptions={historyOptions} />}
|
|
{!hideQuickRanges && (
|
|
<>
|
|
<TimeRangeList
|
|
title="Relative time ranges"
|
|
options={quickOptions}
|
|
onSelect={onChange}
|
|
value={value}
|
|
timeZone={timeZone}
|
|
/>
|
|
<div className={styles.spacing} />
|
|
<TimeRangeList
|
|
title="Other quick ranges"
|
|
options={otherOptions}
|
|
onSelect={onChange}
|
|
value={value}
|
|
timeZone={timeZone}
|
|
/>
|
|
</>
|
|
)}
|
|
</CustomScrollbar>
|
|
)}
|
|
</div>
|
|
{!hideTimeZone && isFullscreen && <TimePickerFooter timeZone={timeZone} onChangeTimeZone={onChangeTimeZone} />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const TimePickerContent: React.FC<Props> = (props) => {
|
|
const theme = useTheme2();
|
|
const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.values.lg}px)`);
|
|
|
|
return <TimePickerContentWithScreenSize {...props} isFullscreen={isFullscreen} />;
|
|
};
|
|
|
|
const NarrowScreenForm: React.FC<FormProps> = (props) => {
|
|
const { value, hideQuickRanges, onChange, timeZone, historyOptions = [], showHistory } = props;
|
|
const theme = useTheme2();
|
|
const styles = getNarrowScreenStyles(theme);
|
|
const isAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
|
|
const [collapsedFlag, setCollapsedFlag] = useState(!isAbsolute);
|
|
const collapsed = hideQuickRanges ? false : collapsedFlag;
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
aria-label="TimePicker absolute time range"
|
|
className={styles.header}
|
|
onClick={() => {
|
|
if (!hideQuickRanges) {
|
|
setCollapsedFlag(!collapsed);
|
|
}
|
|
}}
|
|
>
|
|
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
|
{!hideQuickRanges && <Icon name={!collapsed ? 'angle-up' : 'angle-down'} />}
|
|
</div>
|
|
{!collapsed && (
|
|
<div className={styles.body}>
|
|
<div className={styles.form}>
|
|
<TimeRangeForm value={value} onApply={onChange} timeZone={timeZone} isFullscreen={false} />
|
|
</div>
|
|
{showHistory && (
|
|
<TimeRangeList
|
|
title="Recently used absolute ranges"
|
|
options={historyOptions}
|
|
onSelect={onChange}
|
|
value={value}
|
|
placeholderEmpty={null}
|
|
timeZone={timeZone}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const FullScreenForm: React.FC<FormProps> = (props) => {
|
|
const theme = useTheme2();
|
|
const styles = getFullScreenStyles(theme, props.hideQuickRanges);
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.container}>
|
|
<div aria-label="TimePicker absolute time range" className={styles.title}>
|
|
<TimePickerTitle>Absolute time range</TimePickerTitle>
|
|
</div>
|
|
<TimeRangeForm
|
|
value={props.value}
|
|
timeZone={props.timeZone}
|
|
onApply={props.onChange}
|
|
isFullscreen={true}
|
|
isReversed={props.isReversed}
|
|
/>
|
|
</div>
|
|
{props.showHistory && (
|
|
<div className={styles.recent}>
|
|
<TimeRangeList
|
|
title="Recently used absolute ranges"
|
|
options={props.historyOptions || []}
|
|
onSelect={props.onChange}
|
|
value={props.value}
|
|
placeholderEmpty={<EmptyRecentList />}
|
|
timeZone={props.timeZone}
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const EmptyRecentList = memo(() => {
|
|
const theme = useTheme2();
|
|
const styles = getEmptyListStyles(theme);
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<div>
|
|
<span>
|
|
It looks like you haven't used this time picker before. As soon as you enter some time intervals,
|
|
recently used intervals will appear here.
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<a
|
|
className={styles.link}
|
|
href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls"
|
|
target="_new"
|
|
>
|
|
Read the documentation
|
|
</a>
|
|
<span> to find out more about how to enter custom time ranges.</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
function mapToHistoryOptions(ranges?: TimeRange[], timeZone?: TimeZone): TimeOption[] {
|
|
if (!Array.isArray(ranges) || ranges.length === 0) {
|
|
return [];
|
|
}
|
|
return ranges.slice(ranges.length - 4).map((range) => mapRangeToTimeOption(range, timeZone));
|
|
}
|
|
|
|
EmptyRecentList.displayName = 'EmptyRecentList';
|