Skip to content

Commit

Permalink
Display human readable time ranges in AG filters (#4288)
Browse files Browse the repository at this point in the history
# What this PR does

Display human readable time ranges in AG filters

## Which issue(s) this PR closes

Closes #4272

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.

---------

Co-authored-by: Michael Derynck <[email protected]>
  • Loading branch information
Maxim Mordasov and mderynck authored May 2, 2024
1 parent da79bff commit 713c51c
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 80 deletions.
6 changes: 3 additions & 3 deletions engine/apps/api/tests/test_alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def test_get_filter_started_at(alert_group_internal_api_setup, make_user_auth_he

url = reverse("api-internal:alertgroup-list")
response = client.get(
url + "?started_at=1970-01-01T00:00:00/2099-01-01T23:59:59",
url + "?started_at=1970-01-01T00:00:00_2099-01-01T23:59:59",
format="json",
**make_user_auth_headers(user, token),
)
Expand All @@ -126,7 +126,7 @@ def test_get_filter_resolved_at_alertgroup_empty_result(alert_group_internal_api

url = reverse("api-internal:alertgroup-list")
response = client.get(
url + "?resolved_at=1970-01-01T00:00:00/1970-01-01T23:59:59",
url + "?resolved_at=1970-01-01T00:00:00_1970-01-01T23:59:59",
format="json",
**make_user_auth_headers(user, token),
)
Expand All @@ -153,7 +153,7 @@ def test_get_filter_resolved_at(alert_group_internal_api_setup, make_user_auth_h

url = reverse("api-internal:alertgroup-list")
response = client.get(
url + "?resolved_at=1970-01-01T00:00:00/2099-01-01T23:59:59",
url + "?resolved_at=1970-01-01T00:00:00_2099-01-01T23:59:59",
format="json",
**make_user_auth_headers(user, token),
)
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/api/views/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ def filters(self, request):
now = timezone.now()
week_ago = now - timedelta(days=7)

default_datetime_range = "{}/{}".format(
default_datetime_range = "{}_{}".format(
week_ago.strftime(DateRangeFilterMixin.DATE_FORMAT),
now.strftime(DateRangeFilterMixin.DATE_FORMAT),
)
Expand Down
2 changes: 1 addition & 1 deletion engine/common/api_helpers/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def parse_custom_datetime_range(cls, value):
if not value:
return None, None

date_entries = value.split("/")
date_entries = value.split("_")

if len(date_entries) != 2:
raise BadRequest(detail="Invalid range value")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { convertRelativeToAbsoluteDate } from 'utils/datetime';

import { FilterOption } from './RemoteFilters.types';

const normalize = (value: any) => {
Expand Down Expand Up @@ -33,8 +31,6 @@ export function parseFilters(
value = [rawValue];
}
value = value.map(normalize);
} else if (filterOption.type === 'daterange') {
value = convertRelativeToAbsoluteDate(value);
} else if ((filterOption.type === 'boolean' && rawValue === '') || rawValue === 'true') {
value = true;
} else if (rawValue === 'false') {
Expand Down
18 changes: 3 additions & 15 deletions grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { SelectOption, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import { LocationHelper } from 'utils/LocationHelper';
import { PAGE } from 'utils/consts';
import { convertTimerangeToFilterValue, getValueForDateRangeFilterType } from 'utils/datetime';
import { allFieldsEmpty } from 'utils/utils';

import { parseFilters } from './RemoteFilters.helpers';
Expand Down Expand Up @@ -314,22 +315,11 @@ class _RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
);

case 'daterange':
const dates = values[filter.name] ? values[filter.name].split('/') : undefined;

const value = {
from: dates ? moment(dates[0] + 'Z') : undefined,
to: dates ? moment(dates[1] + 'Z') : undefined,
raw: {
from: dates ? dates[0] : '',
to: dates ? dates[1] : '',
},
};
const value = getValueForDateRangeFilterType(values[filter.name]);

return (
<TimeRangeInput
timeZone={moment.tz.guess()}
autoFocus={autoFocus}
// @ts-ignore
value={value}
onChange={this.getDateRangeFilterChangeHandler(filter.name)}
hideTimeZone
Expand Down Expand Up @@ -387,9 +377,7 @@ class _RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {

getDateRangeFilterChangeHandler = (name: FilterOption['name']) => {
return (timeRange: TimeRange) => {
const value =
timeRange.from.utc().format('YYYY-MM-DDTHH:mm:ss') + '/' + timeRange.to.utc().format('YYYY-MM-DDTHH:mm:ss');

const value = convertTimerangeToFilterValue(timeRange);
this.onFiltersValueChange(name, value);
};
};
Expand Down
18 changes: 15 additions & 3 deletions grafana-plugin/src/models/alertgroup/alertgroup.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { runInAction, makeAutoObservable } from 'mobx';
import qs from 'query-string';

import { convertFiltersToBackendFormat } from 'models/filters/filters.helpers';
import { ActionKey } from 'models/loader/action-keys';
import { makeRequest } from 'network/network';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { onCallApi } from 'network/oncall-api/http-client';
import { RootStore } from 'state/rootStore';
import { SelectOption } from 'state/types';
import { LocationHelper } from 'utils/LocationHelper';
import { GENERIC_ERROR } from 'utils/consts';
import { GENERIC_ERROR, PAGE } from 'utils/consts';
import { AutoLoadingState, WithGlobalNotification } from 'utils/decorators';

import { AlertGroupHelper } from './alertgroup.helpers';
Expand Down Expand Up @@ -53,12 +54,18 @@ export class AlertGroupStore {
);
const timestamp = new Date().getTime();
this.latestFetchAlertGroupsTimestamp = timestamp;

const incidentFilters = convertFiltersToBackendFormat(
this.incidentFilters,
this.rootStore.filtersStore.options[PAGE.Incidents]
);

const {
data: { results, next: nextRaw, previous: previousRaw, page_size },
} = await onCallApi().GET('/alertgroups/', {
params: {
query: {
...this.incidentFilters,
...incidentFilters,
search,
perpage: this.alertsSearchResult?.page_size,
cursor: this.incidentsCursor,
Expand Down Expand Up @@ -201,8 +208,13 @@ export class AlertGroupStore {
}

async fetchStats(status: IncidentStatus) {
const incidentFilters = convertFiltersToBackendFormat(
this.incidentFilters,
this.rootStore.filtersStore.options[PAGE.Incidents]
);

const { data } = await onCallApi().GET('/alertgroups/stats/', {
params: { query: { ...this.incidentFilters, status: [status] } },
params: { query: { ...incidentFilters, status: [status] } },
});

runInAction(() => {
Expand Down
14 changes: 14 additions & 0 deletions grafana-plugin/src/models/filters/filters.helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { convertRelativeToAbsoluteDate } from 'utils/datetime';

import { FilterOption, FiltersValues } from './filters.types';

export const getApiPathByPage = (page: string) => {
return (
{
Expand All @@ -7,3 +11,13 @@ export const getApiPathByPage = (page: string) => {
}[page] || page
);
};

export const convertFiltersToBackendFormat = (filters: FiltersValues, filterOptions: FilterOption[]) => {
const newFilters = { ...filters };
filterOptions.forEach((filterOption) => {
if (filterOption.type === 'daterange' && newFilters[filterOption.name]) {
newFilters[filterOption.name] = convertRelativeToAbsoluteDate(filters[filterOption.name]);
}
});
return newFilters;
};
2 changes: 1 addition & 1 deletion grafana-plugin/src/pages/incidents/Incidents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
team: [],
status: [IncidentStatus.Firing, IncidentStatus.Acknowledged],
mine: false,
started_at: `${defaultStart.format('YYYY-MM-DDTHH:mm:ss')}/${defaultEnd.format('YYYY-MM-DDTHH:mm:ss')}`,
started_at: `${defaultStart.format('YYYY-MM-DDTHH:mm:ss')}_${defaultEnd.format('YYYY-MM-DDTHH:mm:ss')}`,
}}
/>
</div>
Expand Down
48 changes: 48 additions & 0 deletions grafana-plugin/src/utils/datetime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import moment from 'moment-timezone';

import { convertRelativeToAbsoluteDate, getValueForDateRangeFilterType } from './datetime';

const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ss';

describe('convertRelativeToAbsoluteDate', () => {
it(`should convert relative date to absolute dates pair separated by an underscore`, () => {
const result = convertRelativeToAbsoluteDate('now-24h_now');

const now = moment().utc();
const nowString = now.format(DATE_FORMAT);
const dayBefore = now.subtract('1', 'day');
const dayBeforeString = dayBefore.format(DATE_FORMAT);

expect(result).toBe(`${dayBeforeString}_${nowString}`);
});
});

describe('getValueForDateRangeFilterType', () => {
it(`should convert relative date range string to a suitable format for TimeRangeInput component`, () => {
const input = 'now-2d_now';
const [from, to] = input.split('_');
const result = getValueForDateRangeFilterType(input);

const now = moment();
const twoDaysBefore = now.subtract('2', 'day');

expect(result.from.diff(twoDaysBefore, 'seconds') < 1).toBe(true);
expect(result.raw.from).toBe(from);
expect(result.from.diff(twoDaysBefore, 'seconds') < 1).toBe(true);
expect(result.raw.to).toBe(to);
});

it(`should convert absolute date range string to a suitable format for TimeRangeInput component`, () => {
const input = '2024-03-31T23:00:00_2024-04-15T22:59:59';
const [from, to] = input.split('_');
const result = getValueForDateRangeFilterType(input);

const fromMoment = moment(from + 'Z');
const toMoment = moment(to + 'Z');

expect(result.from.diff(fromMoment, 'seconds') < 1).toBe(true);
expect(result.raw.from).toBe(from);
expect(result.from.diff(toMoment, 'seconds') < 1).toBe(true);
expect(result.raw.to).toBe(to);
});
});
106 changes: 54 additions & 52 deletions grafana-plugin/src/utils/datetime.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,70 @@
import { TimeOption, TimeRange, rangeUtil } from '@grafana/data';
import { TimeZone } from '@grafana/schema';
import moment from 'moment-timezone';

// Valid mapping accepted by @grafana/ui and @grafana/data packages
export const quickOptions = [
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' },
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' },
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes' },
{ from: 'now-1h', to: 'now', display: 'Last 1 hour' },
{ from: 'now-3h', to: 'now', display: 'Last 3 hours' },
{ from: 'now-6h', to: 'now', display: 'Last 6 hours' },
{ from: 'now-12h', to: 'now', display: 'Last 12 hours' },
{ from: 'now-24h', to: 'now', display: 'Last 24 hours' },
{ from: 'now-2d', to: 'now', display: 'Last 2 days' },
{ from: 'now-7d', to: 'now', display: 'Last 7 days' },
{ from: 'now-30d', to: 'now', display: 'Last 30 days' },
{ from: 'now-90d', to: 'now', display: 'Last 90 days' },
{ from: 'now-6M', to: 'now', display: 'Last 6 months' },
{ from: 'now-1y', to: 'now', display: 'Last 1 year' },
{ from: 'now-2y', to: 'now', display: 'Last 2 years' },
{ from: 'now-5y', to: 'now', display: 'Last 5 years' },
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday' },
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday' },
{ from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week' },
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week' },
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month' },
{ from: 'now-1Q/fQ', to: 'now-1Q/fQ', display: 'Previous fiscal quarter' },
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year' },
{ from: 'now-1y/fy', to: 'now-1y/fy', display: 'Previous fiscal year' },
{ from: 'now/d', to: 'now/d', display: 'Today' },
{ from: 'now/d', to: 'now', display: 'Today so far' },
{ from: 'now/w', to: 'now/w', display: 'This week' },
{ from: 'now/w', to: 'now', display: 'This week so far' },
{ from: 'now/M', to: 'now/M', display: 'This month' },
{ from: 'now/M', to: 'now', display: 'This month so far' },
{ from: 'now/y', to: 'now/y', display: 'This year' },
{ from: 'now/y', to: 'now', display: 'This year so far' },
{ from: 'now/fQ', to: 'now', display: 'This fiscal quarter so far' },
{ from: 'now/fQ', to: 'now/fQ', display: 'This fiscal quarter' },
{ from: 'now/fy', to: 'now', display: 'This fiscal year so far' },
{ from: 'now/fy', to: 'now/fy', display: 'This fiscal year' },
];
export const DATE_RANGE_DELIMITER = '_';

export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => {
return rangeUtil.convertRawToRange({ from: option.from, to: option.to }, timeZone);
};

export function convertRelativeToAbsoluteDate(dateRangeString: string) {
const [from, to] = dateRangeString?.split('/') || [];
const isValidMapping = quickOptions.find((option) => option.from === from && option.to === to);
if (!dateRangeString) {
return undefined;
}

if (isValidMapping) {
const { from: startDate, to: endDate } = mapOptionToTimeRange({ from, to } as TimeOption);
const [from, to] = dateRangeString?.split(DATE_RANGE_DELIMITER) || [];
if (rangeUtil.isRelativeTimeRange({ from, to })) {
const { from: startDate, to: endDate } = rangeUtil.convertRawToRange({ from, to });

// Return in the format used by on call filters
return `${startDate.format('YYYY-MM-DDTHH:mm:ss')}/${endDate.format('YYYY-MM-DDTHH:mm:ss')}`;
return `${startDate.utc().format('YYYY-MM-DDTHH:mm:ss')}${DATE_RANGE_DELIMITER}${endDate
.utc()
.format('YYYY-MM-DDTHH:mm:ss')}`;
}
return dateRangeString;
}

const quickOption = quickOptions.find((option) => option.display.toLowerCase() === dateRangeString.toLowerCase());

if (quickOption) {
const { from: startDate, to: endDate } = mapOptionToTimeRange(quickOption as TimeOption);
export const convertTimerangeToFilterValue = (timeRange: TimeRange) => {
const isRelative = rangeUtil.isRelativeTimeRange(timeRange.raw);

return `${startDate.format('YYYY-MM-DDTHH:mm:ss')}/${endDate.format('YYYY-MM-DDTHH:mm:ss')}`;
if (isRelative) {
return timeRange.raw.from + DATE_RANGE_DELIMITER + timeRange.raw.to;
} else if (timeRange.from.isValid() && timeRange.to.isValid()) {
return (
timeRange.from.utc().format('YYYY-MM-DDTHH:mm:ss') +
DATE_RANGE_DELIMITER +
timeRange.to.utc().format('YYYY-MM-DDTHH:mm:ss')
);
}
return '';
};

return dateRangeString;
}
export const getValueForDateRangeFilterType = (rawInput: string) => {
let value = { from: undefined, to: undefined, raw: { from: '', to: '' } };
if (rawInput) {
const [fromString, toString] = rawInput.split(DATE_RANGE_DELIMITER);
const isRelative = rangeUtil.isRelativeTimeRange({ from: fromString, to: toString });

const raw = {
from: fromString,
to: toString,
};

if (isRelative) {
const absolute = convertRelativeToAbsoluteDate(rawInput);
const [absoluteFrom, absoluteTo] = absolute.split(DATE_RANGE_DELIMITER);
value = {
from: moment(absoluteFrom + 'Z'),
to: moment(absoluteTo + 'Z'),
raw,
};
} else {
value = {
from: moment(fromString + 'Z'),
to: moment(toString + 'Z'),
raw,
};
}
}
return value;
};

0 comments on commit 713c51c

Please sign in to comment.