Grafana-UI: Enhances for TimeRangePicker and TimeRangeInput (#30102)

This commit is contained in:
Bogdan Matei 2021-01-15 16:53:57 +02:00 committed by GitHub
parent 9734b7069c
commit 46167785e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 437 additions and 582 deletions

View File

@ -39,6 +39,7 @@
"@types/react-color": "3.0.1", "@types/react-color": "3.0.1",
"@types/react-select": "3.0.8", "@types/react-select": "3.0.8",
"@types/react-table": "7.0.12", "@types/react-table": "7.0.12",
"@testing-library/jest-dom": "5.11.9",
"@sentry/browser": "5.25.0", "@sentry/browser": "5.25.0",
"@types/slate": "0.47.1", "@types/slate": "0.47.1",
"@types/slate-react": "0.22.5", "@types/slate-react": "0.22.5",

View File

@ -1,9 +1,11 @@
import { Story } from '@storybook/react';
import React from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { dateTime, TimeFragment } from '@grafana/data'; import { dateTime, DefaultTimeZone, TimeRange, TimeZone } from '@grafana/data';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState'; import { UseState } from '../../utils/storybook/UseState';
import { TimeRangeInput } from '@grafana/ui'; import { TimeRangeInput } from '@grafana/ui';
import { TimeRangeInputProps } from './TimeRangeInput';
import mdx from './TimeRangeInput.mdx'; import mdx from './TimeRangeInput.mdx';
export default { export default {
@ -17,51 +19,75 @@ export default {
}, },
}; };
export const basic = () => { interface State {
return ( value: TimeRange;
<UseState timeZone: TimeZone;
initialState={{ }
from: dateTime(),
to: dateTime(), const getComponentWithState = (initialState: State, props: TimeRangeInputProps) => (
raw: { from: 'now-6h' as TimeFragment, to: 'now' as TimeFragment }, <UseState initialState={initialState}>
}} {(state, updateValue) => {
> return (
{(value, updateValue) => { <TimeRangeInput
return ( {...props}
<TimeRangeInput value={state.value}
value={value} timeZone={state.timeZone}
onChange={timeRange => { onChange={value => {
action('onChange fired')(timeRange); action('onChange fired')(value);
updateValue(timeRange); updateValue({
}} ...state,
/> value,
); });
}} }}
</UseState> onChangeTimeZone={timeZone => {
action('onChangeTimeZone fired')(timeZone);
updateValue({
...state,
timeZone,
});
}}
/>
);
}}
</UseState>
);
export const Relative: Story<TimeRangeInputProps> = props => {
const to = dateTime();
const from = to.subtract(6, 'h');
return getComponentWithState(
{
value: {
from,
to,
raw: {
from: 'now-6h',
to: 'now',
},
},
timeZone: DefaultTimeZone,
},
props
); );
}; };
export const clearable = () => { export const Absolute: Story<TimeRangeInputProps> = props => {
return ( const to = dateTime();
<UseState const from = to.subtract(6, 'h');
initialState={{
from: dateTime(), return getComponentWithState(
to: dateTime(), {
raw: { from: 'now-6h' as TimeFragment, to: 'now' as TimeFragment }, value: {
}} from,
> to,
{(value, updateValue) => { raw: {
return ( from,
<TimeRangeInput to,
clearable },
value={value} },
onChange={timeRange => { timeZone: DefaultTimeZone,
action('onChange fired')(timeRange); },
updateValue(timeRange); props
}}
/>
);
}}
</UseState>
); );
}; };

View File

@ -14,7 +14,7 @@ const isValidTimeRange = (range: any) => {
return dateMath.isValid(range.from) && dateMath.isValid(range.to); return dateMath.isValid(range.from) && dateMath.isValid(range.to);
}; };
export interface Props { export interface TimeRangeInputProps {
value: TimeRange; value: TimeRange;
timeZone?: TimeZone; timeZone?: TimeZone;
onChange: (timeRange: TimeRange) => void; onChange: (timeRange: TimeRange) => void;
@ -22,18 +22,22 @@ export interface Props {
hideTimeZone?: boolean; hideTimeZone?: boolean;
placeholder?: string; placeholder?: string;
clearable?: boolean; clearable?: boolean;
isReversed?: boolean;
hideQuickRanges?: boolean;
} }
const noop = () => {}; const noop = () => {};
export const TimeRangeInput: FC<Props> = ({ export const TimeRangeInput: FC<TimeRangeInputProps> = ({
value, value,
onChange, onChange,
onChangeTimeZone, onChangeTimeZone = noop,
clearable, clearable,
hideTimeZone = true, hideTimeZone = true,
timeZone = 'browser', timeZone = 'browser',
placeholder = 'Select time range', placeholder = 'Select time range',
isReversed = true,
hideQuickRanges = false,
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
@ -84,10 +88,11 @@ export const TimeRangeInput: FC<Props> = ({
onChange={onRangeChange} onChange={onRangeChange}
otherOptions={otherOptions} otherOptions={otherOptions}
quickOptions={quickOptions} quickOptions={quickOptions}
onChangeTimeZone={onChangeTimeZone || noop} onChangeTimeZone={onChangeTimeZone}
className={styles.content} className={styles.content}
hideTimeZone={hideTimeZone} hideTimeZone={hideTimeZone}
isReversed isReversed={isReversed}
hideQuickRanges={hideQuickRanges}
/> />
</ClickOutsideWrapper> </ClickOutsideWrapper>
)} )}

View File

@ -1,10 +1,12 @@
import { Story } from '@storybook/react';
import React from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { TimeRangePicker } from '@grafana/ui'; import { Button, TimeRangePicker } from '@grafana/ui';
import { UseState } from '../../utils/storybook/UseState'; import { UseState } from '../../utils/storybook/UseState';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { TimeFragment, dateTime } from '@grafana/data'; import { dateTime, TimeRange, DefaultTimeZone, TimeZone, isDateTime } from '@grafana/data';
import { TimeRangePickerProps } from './TimeRangePicker';
export default { export default {
title: 'Pickers and Editors/TimePickers/TimeRangePicker', title: 'Pickers and Editors/TimePickers/TimeRangePicker',
@ -12,24 +14,37 @@ export default {
decorators: [withCenteredStory], decorators: [withCenteredStory],
}; };
export const basic = () => { interface State {
return ( value: TimeRange;
<UseState timeZone: TimeZone;
initialState={{ history: TimeRange[];
from: dateTime(), }
to: dateTime(),
raw: { from: 'now-6h' as TimeFragment, to: 'now' as TimeFragment }, const getComponentWithState = (initialState: State, props: TimeRangePickerProps) => (
}} <UseState initialState={initialState}>
> {(state, updateValue) => {
{(value, updateValue) => { return (
return ( <>
<TimeRangePicker <TimeRangePicker
onChangeTimeZone={() => {}} {...props}
timeZone="browser" timeZone={state.timeZone}
value={value} value={state.value}
onChange={timeRange => { history={state.history}
action('onChange fired')(timeRange); onChange={value => {
updateValue(timeRange); action('onChange fired')(value);
updateValue({
...state,
value,
history:
isDateTime(value.raw.from) && isDateTime(value.raw.to) ? [...state.history, value] : state.history,
});
}}
onChangeTimeZone={timeZone => {
action('onChangeTimeZone fired')(timeZone);
updateValue({
...state,
timeZone,
});
}} }}
onMoveBackward={() => { onMoveBackward={() => {
action('onMoveBackward fired')(); action('onMoveBackward fired')();
@ -41,8 +56,60 @@ export const basic = () => {
action('onZoom fired')(); action('onZoom fired')();
}} }}
/> />
); <Button
}} onClick={() => {
</UseState> updateValue({
...state,
history: [],
});
}}
>
Clear history
</Button>
</>
);
}}
</UseState>
);
export const Relative: Story<TimeRangePickerProps> = props => {
const to = dateTime();
const from = to.subtract(6, 'h');
return getComponentWithState(
{
value: {
from,
to,
raw: {
from: 'now-6h',
to: 'now',
},
},
timeZone: DefaultTimeZone,
history: [],
},
props
);
};
export const Absolute: Story<TimeRangePickerProps> = props => {
const to = dateTime();
const from = to.subtract(6, 'h');
return getComponentWithState(
{
value: {
from,
to,
raw: {
from,
to,
},
},
timeZone: DefaultTimeZone,
history: [],
},
props
); );
}; };

View File

@ -1,21 +1,21 @@
import React from 'react';
import { mount } from 'enzyme';
import { UnthemedTimeRangePicker } from './TimeRangePicker';
import { dateTime, TimeRange } from '@grafana/data'; import { dateTime, TimeRange } from '@grafana/data';
import { render } from '@testing-library/react';
import React from 'react';
import dark from '../../themes/dark'; import dark from '../../themes/dark';
import { UnthemedTimeRangePicker } from './TimeRangePicker';
const from = '2019-12-17T07:48:27.433Z'; const from = dateTime('2019-12-17T07:48:27.433Z');
const to = '2019-12-18T07:48:27.433Z'; const to = dateTime('2019-12-18T07:48:27.433Z');
const value: TimeRange = { const value: TimeRange = {
from: dateTime(from), from,
to: dateTime(to), to,
raw: { from: dateTime(from), to: dateTime(to) }, raw: { from, to },
}; };
describe('TimePicker', () => { describe('TimePicker', () => {
it('renders buttons correctly', () => { it('renders buttons correctly', () => {
const wrapper = mount( const container = render(
<UnthemedTimeRangePicker <UnthemedTimeRangePicker
onChangeTimeZone={() => {}} onChangeTimeZone={() => {}}
onChange={value => {}} onChange={value => {}}
@ -26,6 +26,7 @@ describe('TimePicker', () => {
theme={dark} theme={dark}
/> />
); );
expect(wrapper.exists('.navbar-button')).toBe(true);
expect(container.queryByLabelText(/timepicker open button/i)).toBeInTheDocument();
}); });
}); });

View File

@ -56,7 +56,7 @@ const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
}; };
}); });
export interface Props extends Themeable { export interface TimeRangePickerProps extends Themeable {
hideText?: boolean; hideText?: boolean;
value: TimeRange; value: TimeRange;
timeZone?: TimeZone; timeZone?: TimeZone;
@ -68,13 +68,14 @@ export interface Props extends Themeable {
onMoveForward: () => void; onMoveForward: () => void;
onZoom: () => void; onZoom: () => void;
history?: TimeRange[]; history?: TimeRange[];
hideQuickRanges?: boolean;
} }
export interface State { export interface State {
isOpen: boolean; isOpen: boolean;
} }
export class UnthemedTimeRangePicker extends PureComponent<Props, State> { export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps, State> {
state: State = { state: State = {
isOpen: false, isOpen: false,
}; };
@ -107,6 +108,7 @@ export class UnthemedTimeRangePicker extends PureComponent<Props, State> {
theme, theme,
history, history,
onChangeTimeZone, onChangeTimeZone,
hideQuickRanges,
} = this.props; } = this.props;
const { isOpen } = this.state; const { isOpen } = this.state;
@ -146,6 +148,7 @@ export class UnthemedTimeRangePicker extends PureComponent<Props, State> {
history={history} history={history}
showHistory showHistory
onChangeTimeZone={onChangeTimeZone} onChangeTimeZone={onChangeTimeZone}
hideQuickRanges={hideQuickRanges}
/> />
</ClickOutsideWrapper> </ClickOutsideWrapper>
)} )}
@ -192,7 +195,7 @@ const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; time
); );
}; };
type LabelProps = Pick<Props, 'hideText' | 'value' | 'timeZone'>; type LabelProps = Pick<TimeRangePickerProps, 'hideText' | 'value' | 'timeZone'>;
export const TimePickerButtonLabel = memo<LabelProps>(({ hideText, value, timeZone }) => { export const TimePickerButtonLabel = memo<LabelProps>(({ hideText, value, timeZone }) => {
const theme = useTheme(); const theme = useTheme();

View File

@ -1,59 +1,167 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TimePickerContentWithScreenSize } from './TimePickerContent';
import { dateTime, TimeRange } from '@grafana/data'; import { dateTime, TimeRange } from '@grafana/data';
import { render, RenderResult, screen } from '@testing-library/react';
import React from 'react';
import { PropsWithScreenSize, TimePickerContentWithScreenSize } from './TimePickerContent';
describe('TimePickerContent', () => { describe('TimePickerContent', () => {
it('renders correctly in full screen', () => { const absoluteValue = createAbsoluteTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:49:27.433Z');
const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z'); const relativeValue = createRelativeTimeRange();
const wrapper = shallow( const history = [
<TimePickerContentWithScreenSize createAbsoluteTimeRange('2019-12-17T07:48:27.433Z', '2019-12-17T07:49:27.433Z'),
onChangeTimeZone={() => {}} createAbsoluteTimeRange('2019-10-18T07:50:27.433Z', '2019-10-18T07:51:27.433Z'),
onChange={value => {}} ];
timeZone="utc"
value={value} describe('Wide Screen', () => {
isFullscreen={true} it('renders with history', () => {
/> renderComponent({ value: absoluteValue, history });
); expect(screen.queryByText(/recently used absolute ranges/i)).toBeInTheDocument();
expect(wrapper).toMatchSnapshot(); expect(screen.queryByText(/2019-12-17 07:48:27 to 2019-12-17 07:49:27/i)).toBeInTheDocument();
expect(screen.queryByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).toBeInTheDocument();
});
it('renders with empty history', () => {
renderComponent({ value: absoluteValue });
expect(screen.queryByText(/recently used absolute ranges/i)).not.toBeInTheDocument();
expect(
screen.queryByText(
/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\./i
)
).toBeInTheDocument();
});
it('renders without history', () => {
renderComponent({ value: absoluteValue, history, showHistory: false });
expect(screen.queryByText(/recently used absolute ranges/i)).not.toBeInTheDocument();
expect(screen.queryByText(/2019-12-17 07:48:27 to 2019-12-17 07:49:27/i)).not.toBeInTheDocument();
expect(screen.queryByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).not.toBeInTheDocument();
});
it('renders with relative picker', () => {
renderComponent({ value: absoluteValue });
expect(screen.queryByText(/relative time ranges/i)).toBeInTheDocument();
expect(screen.queryByText(/other quick ranges/i)).toBeInTheDocument();
});
it('renders without relative picker', () => {
renderComponent({ value: absoluteValue, hideQuickRanges: true });
expect(screen.queryByText(/relative time ranges/i)).not.toBeInTheDocument();
expect(screen.queryByText(/other quick ranges/i)).not.toBeInTheDocument();
});
it('renders with timezone picker', () => {
renderComponent({ value: absoluteValue, hideTimeZone: false });
expect(screen.queryByText(/coordinated universal time/i)).toBeInTheDocument();
});
it('renders without timezone picker', () => {
renderComponent({ value: absoluteValue, hideTimeZone: true });
expect(screen.queryByText(/coordinated universal time/i)).not.toBeInTheDocument();
});
}); });
it('renders correctly in narrow screen', () => { describe('Narrow Screen', () => {
const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z'); it('renders with history', () => {
const wrapper = shallow( renderComponent({ value: absoluteValue, history, isFullscreen: false });
<TimePickerContentWithScreenSize expect(screen.queryByText(/recently used absolute ranges/i)).toBeInTheDocument();
onChangeTimeZone={() => {}} expect(screen.queryByText(/2019-12-17 07:48:27 to 2019-12-17 07:49:27/i)).toBeInTheDocument();
onChange={value => {}} expect(screen.queryByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).toBeInTheDocument();
timeZone="utc" });
value={value}
isFullscreen={false}
/>
);
expect(wrapper).toMatchSnapshot();
});
it('renders recent absolute ranges correctly', () => { it('renders with empty history', () => {
const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z'); renderComponent({ value: absoluteValue, isFullscreen: false });
const history = [ expect(screen.queryByText(/recently used absolute ranges/i)).not.toBeInTheDocument();
createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z'), expect(
createTimeRange('2019-10-17T07:48:27.433Z', '2019-10-18T07:48:27.433Z'), screen.queryByText(
]; /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\./i
)
).not.toBeInTheDocument();
});
const wrapper = shallow( it('renders without history', () => {
<TimePickerContentWithScreenSize renderComponent({ value: absoluteValue, isFullscreen: false, history, showHistory: false });
onChangeTimeZone={() => {}} expect(screen.queryByText(/recently used absolute ranges/i)).not.toBeInTheDocument();
onChange={value => {}} expect(screen.queryByText(/2019-12-17 07:48:27 to 2019-12-17 07:49:27/i)).not.toBeInTheDocument();
timeZone="utc" expect(screen.queryByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).not.toBeInTheDocument();
value={value} });
isFullscreen={true}
history={history} it('renders with relative picker', () => {
/> renderComponent({ value: absoluteValue, isFullscreen: false });
); expect(screen.queryByText(/relative time ranges/i)).toBeInTheDocument();
expect(wrapper).toMatchSnapshot(); expect(screen.queryByText(/other quick ranges/i)).toBeInTheDocument();
});
it('renders without relative picker', () => {
renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true });
expect(screen.queryByText(/relative time ranges/i)).not.toBeInTheDocument();
expect(screen.queryByText(/other quick ranges/i)).not.toBeInTheDocument();
});
it('renders with absolute picker when absolute value and quick ranges are visible', () => {
renderComponent({ value: absoluteValue, isFullscreen: false });
expect(screen.queryByLabelText(/timepicker from field/i)).toBeInTheDocument();
});
it('renders with absolute picker when absolute value and quick ranges are hidden', () => {
renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true });
expect(screen.queryByLabelText(/timepicker from field/i)).toBeInTheDocument();
});
it('renders without absolute picker when narrow screen and quick ranges are visible', () => {
renderComponent({ value: relativeValue, isFullscreen: false });
expect(screen.queryByLabelText(/timepicker from field/i)).not.toBeInTheDocument();
});
it('renders with absolute picker when narrow screen and quick ranges are hidden', () => {
renderComponent({ value: relativeValue, isFullscreen: false, hideQuickRanges: true });
expect(screen.queryByLabelText(/timepicker from field/i)).toBeInTheDocument();
});
it('renders without timezone picker', () => {
renderComponent({ value: absoluteValue, hideTimeZone: true });
expect(screen.queryByText(/coordinated universal time/i)).not.toBeInTheDocument();
});
}); });
}); });
function createTimeRange(from: string, to: string): TimeRange { function noop(): {} {
return {};
}
function renderComponent({
value,
isFullscreen = true,
showHistory = true,
history = [],
hideQuickRanges = false,
hideTimeZone = false,
}: Pick<PropsWithScreenSize, 'value'> & Partial<PropsWithScreenSize>): RenderResult {
return render(
<TimePickerContentWithScreenSize
onChangeTimeZone={noop}
onChange={noop}
timeZone="utc"
value={value}
isFullscreen={isFullscreen}
showHistory={showHistory}
history={history}
hideQuickRanges={hideQuickRanges}
hideTimeZone={hideTimeZone}
/>
);
}
function createRelativeTimeRange(): TimeRange {
const now = dateTime();
const now5m = now.subtract(5, 'm');
return {
from: now5m,
to: now,
raw: { from: 'now-5m', to: 'now' },
};
}
function createAbsoluteTimeRange(from: string, to: string): TimeRange {
return { return {
from: dateTime(from), from: dateTime(from),
to: dateTime(to), to: dateTime(to),

View File

@ -11,7 +11,7 @@ import { TimeRangeForm } from './TimeRangeForm';
import { TimeRangeList } from './TimeRangeList'; import { TimeRangeList } from './TimeRangeList';
import { TimePickerFooter } from './TimePickerFooter'; import { TimePickerFooter } from './TimePickerFooter';
const getStyles = stylesFactory((theme: GrafanaTheme, isReversed) => { const getStyles = stylesFactory((theme: GrafanaTheme, isReversed, hideQuickRanges, isContainerTall) => {
const containerBorder = theme.isDark ? theme.palette.dark9 : theme.palette.gray5; const containerBorder = theme.isDark ? theme.palette.dark9 : theme.palette.gray5;
return { return {
@ -19,12 +19,12 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isReversed) => {
background: ${theme.colors.bodyBg}; background: ${theme.colors.bodyBg};
box-shadow: 0px 0px 20px ${theme.colors.dropdownShadow}; box-shadow: 0px 0px 20px ${theme.colors.dropdownShadow};
position: absolute; position: absolute;
z-index: ${theme.zIndex.modal}; z-index: ${theme.zIndex.dropdown};
width: 546px; width: 546px;
top: 116%; top: 116%;
border-radius: 2px; border-radius: 2px;
border: 1px solid ${containerBorder}; border: 1px solid ${containerBorder};
right: ${isReversed ? 'unset' : 0}; ${isReversed ? 'left' : 'right'}: 0;
@media only screen and (max-width: ${theme.breakpoints.lg}) { @media only screen and (max-width: ${theme.breakpoints.lg}) {
width: 262px; width: 262px;
@ -32,19 +32,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isReversed) => {
`, `,
body: css` body: css`
display: flex; display: flex;
height: 381px; height: ${isContainerTall ? '381px' : '217px'};
`, `,
leftSide: css` leftSide: css`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: ${isReversed ? 'none' : `1px solid ${theme.colors.border1}`}; border-right: ${isReversed ? 'none' : `1px solid ${theme.colors.border1}`};
width: 60%; width: ${!hideQuickRanges ? '60%' : '100%'};
overflow: hidden; overflow: hidden;
order: ${isReversed ? 1 : 0}; order: ${isReversed ? 1 : 0};
@media only screen and (max-width: ${theme.breakpoints.lg}) {
display: none;
}
`, `,
rightSide: css` rightSide: css`
width: 40% !important; width: 40% !important;
@ -61,8 +57,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isReversed) => {
}); });
const getNarrowScreenStyles = stylesFactory((theme: GrafanaTheme) => { const getNarrowScreenStyles = stylesFactory((theme: GrafanaTheme) => {
const formBackground = theme.isDark ? theme.palette.gray15 : theme.palette.gray98;
return { return {
header: css` header: css`
display: flex; display: flex;
@ -74,7 +68,6 @@ const getNarrowScreenStyles = stylesFactory((theme: GrafanaTheme) => {
`, `,
body: css` body: css`
border-bottom: 1px solid ${theme.colors.border1}; border-bottom: 1px solid ${theme.colors.border1};
background: ${formBackground};
box-shadow: inset 0px 2px 2px ${theme.colors.dropdownShadow}; box-shadow: inset 0px 2px 2px ${theme.colors.dropdownShadow};
`, `,
form: css` form: css`
@ -83,12 +76,12 @@ const getNarrowScreenStyles = stylesFactory((theme: GrafanaTheme) => {
}; };
}); });
const getFullScreenStyles = stylesFactory((theme: GrafanaTheme) => { const getFullScreenStyles = stylesFactory((theme: GrafanaTheme, hideQuickRanges?: boolean) => {
return { return {
container: css` container: css`
padding-top: 9px; padding-top: 9px;
padding-left: 11px; padding-left: 11px;
padding-right: 20%; padding-right: ${!hideQuickRanges ? '20%' : '11px'};
`, `,
title: css` title: css`
margin-bottom: 11px; margin-bottom: 11px;
@ -135,51 +128,74 @@ interface Props {
hideTimeZone?: boolean; hideTimeZone?: boolean;
/** Reverse the order of relative and absolute range pickers. Used to left align the picker in forms */ /** Reverse the order of relative and absolute range pickers. Used to left align the picker in forms */
isReversed?: boolean; isReversed?: boolean;
hideQuickRanges?: boolean;
} }
interface PropsWithScreenSize extends Props { export interface PropsWithScreenSize extends Props {
isFullscreen: boolean; isFullscreen: boolean;
} }
interface FormProps extends Omit<Props, 'history'> { interface FormProps extends Omit<Props, 'history'> {
visible: boolean;
historyOptions?: TimeOption[]; historyOptions?: TimeOption[];
} }
export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = props => { 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 = useTheme(); const theme = useTheme();
const styles = getStyles(theme, props.isReversed); const styles = getStyles(theme, isReversed, hideQuickRanges, isContainerTall);
const historyOptions = mapToHistoryOptions(props.history, props.timeZone); const historyOptions = mapToHistoryOptions(history, timeZone);
const { quickOptions = [], otherOptions = [], isFullscreen } = props;
return ( return (
<div className={cx(styles.container, props.className)}> <div className={cx(styles.container, className)}>
<div className={styles.body}> <div className={styles.body}>
<div className={styles.leftSide}> {isFullscreen && (
<FullScreenForm {...props} visible={isFullscreen} historyOptions={historyOptions} /> <div className={styles.leftSide}>
</div> <FullScreenForm {...props} historyOptions={historyOptions} />
<CustomScrollbar className={styles.rightSide}> </div>
<NarrowScreenForm {...props} visible={!isFullscreen} historyOptions={historyOptions} /> )}
<TimeRangeList {(!isFullscreen || !hideQuickRanges) && (
title="Relative time ranges" <CustomScrollbar className={styles.rightSide}>
options={quickOptions} {!isFullscreen && <NarrowScreenForm {...props} historyOptions={historyOptions} />}
onSelect={props.onChange} {!hideQuickRanges && (
value={props.value} <>
timeZone={props.timeZone} <TimeRangeList
/> title="Relative time ranges"
<div className={styles.spacing} /> options={quickOptions}
<TimeRangeList onSelect={onChange}
title="Other quick ranges" value={value}
options={otherOptions} timeZone={timeZone}
onSelect={props.onChange} />
value={props.value} <div className={styles.spacing} />
timeZone={props.timeZone} <TimeRangeList
/> title="Other quick ranges"
</CustomScrollbar> options={otherOptions}
onSelect={onChange}
value={value}
timeZone={timeZone}
/>
</>
)}
</CustomScrollbar>
)}
</div> </div>
{!props.hideTimeZone && isFullscreen && ( {!hideTimeZone && isFullscreen && <TimePickerFooter timeZone={timeZone} onChangeTimeZone={onChangeTimeZone} />}
<TimePickerFooter timeZone={props.timeZone} onChangeTimeZone={props.onChangeTimeZone} />
)}
</div> </div>
); );
}; };
@ -192,43 +208,40 @@ export const TimePickerContent: React.FC<Props> = props => {
}; };
const NarrowScreenForm: React.FC<FormProps> = props => { const NarrowScreenForm: React.FC<FormProps> = props => {
const { value, hideQuickRanges, onChange, timeZone, historyOptions = [], showHistory } = props;
const theme = useTheme(); const theme = useTheme();
const styles = getNarrowScreenStyles(theme); const styles = getNarrowScreenStyles(theme);
const isAbsolute = isDateTime(props.value.raw.from) || isDateTime(props.value.raw.to); const isAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
const [collapsed, setCollapsed] = useState(isAbsolute); const [collapsedFlag, setCollapsedFlag] = useState(!isAbsolute);
const collapsed = hideQuickRanges ? false : collapsedFlag;
if (!props.visible) {
return null;
}
return ( return (
<> <>
<div <div
aria-label="TimePicker absolute time range" aria-label="TimePicker absolute time range"
className={styles.header} className={styles.header}
onClick={() => setCollapsed(!collapsed)} onClick={() => {
if (!hideQuickRanges) {
setCollapsedFlag(!collapsed);
}
}}
> >
<TimePickerTitle>Absolute time range</TimePickerTitle> <TimePickerTitle>Absolute time range</TimePickerTitle>
{<Icon name={collapsed ? 'angle-up' : 'angle-down'} />} {!hideQuickRanges && <Icon name={!collapsed ? 'angle-up' : 'angle-down'} />}
</div> </div>
{collapsed && ( {!collapsed && (
<div className={styles.body}> <div className={styles.body}>
<div className={styles.form}> <div className={styles.form}>
<TimeRangeForm <TimeRangeForm value={value} onApply={onChange} timeZone={timeZone} isFullscreen={false} />
value={props.value}
onApply={props.onChange}
timeZone={props.timeZone}
isFullscreen={false}
/>
</div> </div>
{props.showHistory && ( {showHistory && (
<TimeRangeList <TimeRangeList
title="Recently used absolute ranges" title="Recently used absolute ranges"
options={props.historyOptions || []} options={historyOptions}
onSelect={props.onChange} onSelect={onChange}
value={props.value} value={value}
placeholderEmpty={null} placeholderEmpty={null}
timeZone={props.timeZone} timeZone={timeZone}
/> />
)} )}
</div> </div>
@ -239,11 +252,7 @@ const NarrowScreenForm: React.FC<FormProps> = props => {
const FullScreenForm: React.FC<FormProps> = props => { const FullScreenForm: React.FC<FormProps> = props => {
const theme = useTheme(); const theme = useTheme();
const styles = getFullScreenStyles(theme); const styles = getFullScreenStyles(theme, props.hideQuickRanges);
if (!props.visible) {
return null;
}
return ( return (
<> <>

View File

@ -32,7 +32,7 @@ interface InputState {
const errorMessage = 'Please enter a past date or "now"'; const errorMessage = 'Please enter a past date or "now"';
export const TimeRangeForm: React.FC<Props> = props => { export const TimeRangeForm: React.FC<Props> = props => {
const { value, isFullscreen = false, timeZone, onApply: onApplyFromProps } = props; const { value, isFullscreen = false, timeZone, onApply: onApplyFromProps, isReversed } = props;
const [from, setFrom] = useState<InputState>(valueToState(value.raw.from, false, timeZone)); const [from, setFrom] = useState<InputState>(valueToState(value.raw.from, false, timeZone));
const [to, setTo] = useState<InputState>(valueToState(value.raw.to, true, timeZone)); const [to, setTo] = useState<InputState>(valueToState(value.raw.to, true, timeZone));
@ -122,7 +122,7 @@ export const TimeRangeForm: React.FC<Props> = props => {
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onChange={onChange} onChange={onChange}
timeZone={timeZone} timeZone={timeZone}
isReversed={props.isReversed} isReversed={isReversed}
/> />
</> </>
); );

View File

@ -1,370 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TimePickerContent renders correctly in full screen 1`] = `
<div
className="css-ajr8sn"
>
<div
className="css-ooqtr4"
>
<div
className="css-1f2wc71"
>
<FullScreenForm
historyOptions={Array []}
isFullscreen={true}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={true}
/>
</div>
<CustomScrollbar
autoHeightMax="100%"
autoHeightMin="0"
autoHide={false}
autoHideDuration={200}
autoHideTimeout={200}
className="css-10t714z"
hideTracksWhenNotNeeded={false}
setScrollTop={[Function]}
>
<NarrowScreenForm
historyOptions={Array []}
isFullscreen={true}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={false}
/>
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Relative time ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
<div
className="css-1ogeuxc"
/>
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Other quick ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
</CustomScrollbar>
</div>
<TimePickerFooter
onChangeTimeZone={[Function]}
timeZone="utc"
/>
</div>
`;
exports[`TimePickerContent renders correctly in narrow screen 1`] = `
<div
className="css-ajr8sn"
>
<div
className="css-ooqtr4"
>
<div
className="css-1f2wc71"
>
<FullScreenForm
historyOptions={Array []}
isFullscreen={false}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={false}
/>
</div>
<CustomScrollbar
autoHeightMax="100%"
autoHeightMin="0"
autoHide={false}
autoHideDuration={200}
autoHideTimeout={200}
className="css-10t714z"
hideTracksWhenNotNeeded={false}
setScrollTop={[Function]}
>
<NarrowScreenForm
historyOptions={Array []}
isFullscreen={false}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={true}
/>
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Relative time ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
<div
className="css-1ogeuxc"
/>
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Other quick ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
</CustomScrollbar>
</div>
</div>
`;
exports[`TimePickerContent renders recent absolute ranges correctly 1`] = `
<div
className="css-ajr8sn"
>
<div
className="css-ooqtr4"
>
<div
className="css-1f2wc71"
>
<FullScreenForm
history={
Array [
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
},
Object {
"from": "2019-10-17T07:48:27.433Z",
"raw": Object {
"from": "2019-10-17T07:48:27.433Z",
"to": "2019-10-18T07:48:27.433Z",
},
"to": "2019-10-18T07:48:27.433Z",
},
]
}
historyOptions={
Array [
Object {
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
"from": "2019-12-17 07:48:27",
"section": 3,
"to": "2019-12-18 07:48:27",
},
Object {
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
"from": "2019-10-17 07:48:27",
"section": 3,
"to": "2019-10-18 07:48:27",
},
]
}
isFullscreen={true}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={true}
/>
</div>
<CustomScrollbar
autoHeightMax="100%"
autoHeightMin="0"
autoHide={false}
autoHideDuration={200}
autoHideTimeout={200}
className="css-10t714z"
hideTracksWhenNotNeeded={false}
setScrollTop={[Function]}
>
<NarrowScreenForm
history={
Array [
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
},
Object {
"from": "2019-10-17T07:48:27.433Z",
"raw": Object {
"from": "2019-10-17T07:48:27.433Z",
"to": "2019-10-18T07:48:27.433Z",
},
"to": "2019-10-18T07:48:27.433Z",
},
]
}
historyOptions={
Array [
Object {
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
"from": "2019-12-17 07:48:27",
"section": 3,
"to": "2019-12-18 07:48:27",
},
Object {
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
"from": "2019-10-17 07:48:27",
"section": 3,
"to": "2019-10-18 07:48:27",
},
]
}
isFullscreen={true}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={false}
/>
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Relative time ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
<div
className="css-1ogeuxc"
/>
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Other quick ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
</CustomScrollbar>
</div>
<TimePickerFooter
onChangeTimeZone={[Function]}
timeZone="utc"
/>
</div>
`;

View File

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import { LocalStorageValueProvider } from '../LocalStorageValueProvider'; import { LocalStorageValueProvider } from '../LocalStorageValueProvider';
import { TimeRange, isDateTime, toUtc } from '@grafana/data'; import { TimeRange, isDateTime, toUtc } from '@grafana/data';
import { Props as TimePickerProps, TimeRangePicker } from '@grafana/ui/src/components/TimePicker/TimeRangePicker'; import { TimeRangePickerProps, TimeRangePicker } from '@grafana/ui/src/components/TimePicker/TimeRangePicker';
const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history'; const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history';
interface Props extends Omit<TimePickerProps, 'history' | 'theme'> {} interface Props extends Omit<TimeRangePickerProps, 'history' | 'theme'> {}
export const TimePickerWithHistory: React.FC<Props> = props => { export const TimePickerWithHistory: React.FC<Props> = props => {
return ( return (

View File

@ -5731,6 +5731,20 @@
lodash "^4.17.15" lodash "^4.17.15"
redent "^3.0.0" redent "^3.0.0"
"@testing-library/jest-dom@5.11.9":
version "5.11.9"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.9.tgz#e6b3cd687021f89f261bd53cbe367041fbd3e975"
integrity sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ==
dependencies:
"@babel/runtime" "^7.9.2"
"@types/testing-library__jest-dom" "^5.9.1"
aria-query "^4.2.2"
chalk "^3.0.0"
css "^3.0.0"
css.escape "^1.5.1"
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/react-hooks@^3.2.1": "@testing-library/react-hooks@^3.2.1":
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.2.1.tgz#19b6caa048ef15faa69d439c469033873ea01294" resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.2.1.tgz#19b6caa048ef15faa69d439c469033873ea01294"
@ -22933,15 +22947,6 @@ rollup@^0.25.8:
minimist "^1.2.0" minimist "^1.2.0"
source-map-support "^0.3.2" source-map-support "^0.3.2"
rollup@^0.25.8:
version "0.25.8"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.25.8.tgz#bf6ce83b87510d163446eeaa577ed6a6fc5835e0"
integrity sha1-v2zoO4dRDRY0Ru6qV37WpvxYNeA=
dependencies:
chalk "^1.1.1"
minimist "^1.2.0"
source-map-support "^0.3.2"
rollup@^0.63.4: rollup@^0.63.4:
version "0.63.5" version "0.63.5"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.63.5.tgz#5543eecac9a1b83b7e1be598b5be84c9c0a089db" resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.63.5.tgz#5543eecac9a1b83b7e1be598b5be84c9c0a089db"