Alerting: Add DateTimePicker on view rule page (#38592)

* export datetimepicker

* minor fixes to the datetime picker

* correct datetime to picker

* move datetime calc to function

* set maxDate

* set maxDate

* wrap in useCallback
This commit is contained in:
Peter Holmberg
2021-08-30 11:42:37 +02:00
committed by GitHub
parent 38b398feb4
commit 74afe809af
4 changed files with 79 additions and 16 deletions

View File

@@ -3,7 +3,7 @@ import { useMedia } from 'react-use';
import Calendar from 'react-calendar/dist/entry.nostyle';
import { css, cx } from '@emotion/css';
import { dateTimeFormat, DateTime, dateTime, GrafanaTheme2, isDateTime } from '@grafana/data';
import { Button, ClickOutsideWrapper, Field, HorizontalGroup, Icon, Input, Portal } from '../..';
import { Button, ClickOutsideWrapper, HorizontalGroup, Icon, InlineField, Input, Portal } from '../..';
import { TimeOfDayPicker } from '../TimeOfDayPicker';
import { getBodyStyles, getStyles as getCalendarStyles } from '../TimeRangePicker/TimePickerCalendar';
import { useStyles2, useTheme2 } from '../../../themes';
@@ -16,11 +16,13 @@ export interface Props {
onChange: (date: DateTime) => void;
/** label for the input field */
label?: ReactNode;
/** Set the latest selectable date */
maxDate?: Date;
}
const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopPropagation();
export const DateTimePicker: FC<Props> = ({ date, label, onChange }) => {
export const DateTimePicker: FC<Props> = ({ date, maxDate, label, onChange }) => {
const [isOpen, setOpen] = useState(false);
const theme = useTheme2();
@@ -50,7 +52,13 @@ export const DateTimePicker: FC<Props> = ({ date, label, onChange }) => {
{isOpen ? (
isFullscreen ? (
<ClickOutsideWrapper onClick={() => setOpen(false)}>
<DateTimeCalendar date={date} onChange={onApply} isFullscreen={true} onClose={() => setOpen(false)} />
<DateTimeCalendar
date={date}
onChange={onApply}
isFullscreen={true}
onClose={() => setOpen(false)}
maxDate={maxDate}
/>
</ClickOutsideWrapper>
) : (
<Portal>
@@ -72,6 +80,7 @@ interface DateTimeCalendarProps {
onChange: (date: DateTime) => void;
onClose: () => void;
isFullscreen: boolean;
maxDate?: Date;
}
interface InputProps {
@@ -127,11 +136,13 @@ const DateTimeInput: FC<InputProps> = ({ date, label, onChange, isFullscreen, on
const icon = <Button icon="calendar-alt" variant="secondary" onClick={onOpen} />;
return (
<Field
<InlineField
label={label}
onClick={stopPropagation}
invalid={!!(internalDate.value && internalDate.invalid)}
error="Incorrect date format"
className={css`
margin-bottom: 0;
`}
>
<Input
onClick={stopPropagation}
@@ -143,11 +154,11 @@ const DateTimeInput: FC<InputProps> = ({ date, label, onChange, isFullscreen, on
data-testid="date-time-input"
placeholder="Select date/time"
/>
</Field>
</InlineField>
);
};
const DateTimeCalendar: FC<DateTimeCalendarProps> = ({ date, onClose, onChange, isFullscreen }) => {
const DateTimeCalendar: FC<DateTimeCalendarProps> = ({ date, onClose, onChange, isFullscreen, maxDate }) => {
const calendarStyles = useStyles2(getBodyStyles);
const styles = useStyles2(getStyles);
const [internalDate, setInternalDate] = useState<Date>(() => {
@@ -188,6 +199,7 @@ const DateTimeCalendar: FC<DateTimeCalendarProps> = ({ date, onClose, onChange,
locale="en"
className={calendarStyles.body}
tileClassName={calendarStyles.title}
maxDate={maxDate}
/>
<div className={styles.time}>
<TimeOfDayPicker showSeconds={true} onChange={onChangeTime} value={dateTime(internalDate)} />
@@ -210,6 +222,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
border: 1px ${theme.colors.border.weak} solid;
border-radius: ${theme.shape.borderRadius(1)};
background-color: ${theme.colors.background.primary};
z-index: ${theme.zIndex.modal};
`,
fullScreen: css`
position: absolute;

View File

@@ -29,6 +29,7 @@ export {
DatePickerWithInput,
DatePickerWithInputProps,
} from './DateTimePickers/DatePickerWithInput/DatePickerWithInput';
export { DateTimePicker } from './DateTimePickers/DateTimePicker/DateTimePicker';
export { List } from './List/List';
export { TagsInput } from './TagsInput/TagsInput';
export { Pagination } from './Pagination/Pagination';

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useObservable } from 'react-use';
import { css } from '@emotion/css';
import { GrafanaTheme2, LoadingState, PanelData } from '@grafana/data';
@@ -27,6 +27,7 @@ import { AlertLabels } from './components/AlertLabels';
import { RuleDetailsExpression } from './components/rules/RuleDetailsExpression';
import { RuleDetailsAnnotations } from './components/rules/RuleDetailsAnnotations';
import * as ruleId from './utils/rule-id';
import { AlertQuery } from '../../../types/unified-alerting-dto';
type RuleViewerProps = GrafanaRouteComponentProps<{ id?: string; sourceName?: string }>;
@@ -41,7 +42,8 @@ export function RuleViewer({ match }: RuleViewerProps) {
const { loading, error, result: rule } = useCombinedRule(identifier, sourceName);
const runner = useMemo(() => new AlertingQueryRunner(), []);
const data = useObservable(runner.get());
const queries = useMemo(() => alertRuleToQueries(rule), [rule]);
const queries2 = useMemo(() => alertRuleToQueries(rule), [rule]);
const [queries, setQueries] = useState<AlertQuery[]>([]);
const onRunQueries = useCallback(() => {
if (queries.length > 0) {
@@ -49,6 +51,10 @@ export function RuleViewer({ match }: RuleViewerProps) {
}
}, [queries, runner]);
useEffect(() => {
setQueries(queries2);
}, [queries2]);
useEffect(() => {
onRunQueries();
}, [onRunQueries]);
@@ -57,6 +63,17 @@ export function RuleViewer({ match }: RuleViewerProps) {
return () => runner.destroy();
}, [runner]);
const onChangeQuery = useCallback((query: AlertQuery) => {
setQueries((queries) =>
queries.map((q) => {
if (q.refId === query.refId) {
return query;
}
return q;
})
);
}, []);
if (!sourceName) {
return (
<RuleViewerLayout title={pageTitle}>
@@ -143,7 +160,11 @@ export function RuleViewer({ match }: RuleViewerProps) {
{queries.map((query) => {
return (
<div key={query.refId} className={styles.query}>
<RuleViewerVisualization query={query} data={data && data[query.refId]} />
<RuleViewerVisualization
query={query}
data={data && data[query.refId]}
onChangeQuery={onChangeQuery}
/>
</div>
);
})}

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { css } from '@emotion/css';
import { DataSourceInstanceSettings, GrafanaTheme2, PanelData, urlUtil } from '@grafana/data';
import { DataSourceInstanceSettings, DateTime, dateTime, GrafanaTheme2, PanelData, urlUtil } from '@grafana/data';
import { config, getDataSourceSrv, PanelRenderer } from '@grafana/runtime';
import { Alert, CodeEditor, LinkButton, useStyles2, useTheme2 } from '@grafana/ui';
import { Alert, CodeEditor, DateTimePicker, LinkButton, useStyles2, useTheme2 } from '@grafana/ui';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { PanelOptions } from 'app/plugins/panel/table/models.gen';
import { AlertQuery } from 'app/types/unified-alerting-dto';
@@ -13,6 +13,7 @@ import { TABLE, TIMESERIES } from '../../utils/constants';
type RuleViewerVisualizationProps = {
data?: PanelData;
query: AlertQuery;
onChangeQuery: (query: AlertQuery) => void;
};
const headerHeight = 4;
@@ -20,15 +21,35 @@ const headerHeight = 4;
export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JSX.Element | null {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const { data, query } = props;
const { data, query, onChangeQuery } = props;
const defaultPanel = isExpressionQuery(query.model) ? TABLE : TIMESERIES;
const [panel, setPanel] = useState<SupportedPanelPlugins>(defaultPanel);
const dsSettings = getDataSourceSrv().getInstanceSettings(query.datasourceUid);
const relativeTimeRange = query.relativeTimeRange;
const [options, setOptions] = useState<PanelOptions>({
frameIndex: 0,
showHeader: true,
});
const onTimeChange = useCallback(
(newDateTime: DateTime) => {
const now = dateTime().unix() - newDateTime.unix();
if (relativeTimeRange) {
const interval = relativeTimeRange.from - relativeTimeRange.to;
onChangeQuery({
...query,
relativeTimeRange: { from: now + interval, to: now },
});
}
},
[onChangeQuery, query, relativeTimeRange]
);
const setDateTime = useCallback((relativeTimeRangeTo: number) => {
return relativeTimeRangeTo === 0 ? dateTime() : dateTime().subtract(relativeTimeRangeTo, 'seconds');
}, []);
if (!data) {
return null;
}
@@ -62,12 +83,19 @@ export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JS
<span className={styles.dataSource}>({dsSettings.name})</span>
</div>
<div className={styles.actions}>
<PanelPluginsButtonGroup onChange={setPanel} value={panel} size="sm" />
{!isExpressionQuery(query.model) && relativeTimeRange ? (
<DateTimePicker
date={setDateTime(relativeTimeRange.to)}
onChange={onTimeChange}
maxDate={new Date()}
/>
) : null}
<PanelPluginsButtonGroup onChange={setPanel} value={panel} size="md" />
{!isExpressionQuery(query.model) && (
<>
<div className={styles.spacing} />
<LinkButton
size="sm"
size="md"
variant="secondary"
icon="compass"
target="_blank"