AG filters (#4718)
# What this PR does - Refactored filters to allow for easier customization part of `extraInformation` - Refactored filters to render with portals - Always initialize mandatory fields with default values - Added `TimeRangePickerWrapper` that mimics logic of the same selector used in Grafana from `@grafana/scenes`, specifically `ScenesTimePicker` ~~- Bumped all `@grafana/*` dependencies to latest version~~ (postponed to be done in a separate PR) ## Which issue(s) this PR closes Related to https://github.com/grafana/oncall/issues/4464
This commit is contained in:
parent
443844348e
commit
a5587031b1
6 changed files with 323 additions and 73 deletions
|
|
@ -5,7 +5,6 @@ import { GrafanaTheme2, KeyValue, SelectableValue, TimeRange } from '@grafana/da
|
|||
import {
|
||||
InlineSwitch,
|
||||
MultiSelect,
|
||||
TimeRangeInput,
|
||||
Select,
|
||||
LoadingPlaceholder,
|
||||
Input,
|
||||
|
|
@ -19,33 +18,53 @@ import { capitalCase } from 'change-case';
|
|||
import { debounce, isUndefined, omitBy, pickBy } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Emoji from 'react-emoji-render';
|
||||
|
||||
import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { LabelsFilter } from 'containers/Labels/LabelsFilter';
|
||||
import { RemoteSelect } from 'containers/RemoteSelect/RemoteSelect';
|
||||
import { TeamName } from 'containers/TeamName/TeamName';
|
||||
import { FiltersValues } from 'models/filters/filters.types';
|
||||
import { FilterExtraInformation, FilterExtraInformationValues } from 'models/filters/filters.types';
|
||||
import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
|
||||
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';
|
||||
import { FilterOption } from './RemoteFilters.types';
|
||||
import { TimeRangePickerWrapper } from './TimeRangePickerWrapper';
|
||||
|
||||
interface RemoteFiltersProps extends WithStoreProps, Themeable2 {
|
||||
onChange: (filters: Record<string, any>, isOnMount: boolean, invalidateFn: () => boolean) => void;
|
||||
query: KeyValue;
|
||||
page: PAGE;
|
||||
defaultFilters?: FiltersValues;
|
||||
extraFilters?: (state, setState, onFiltersValueChange) => React.ReactNode;
|
||||
grafanaTeamStore: GrafanaTeamStore;
|
||||
extraInformation?: FilterExtraInformation;
|
||||
extraFilters?: (state, setState, onFiltersValueChange) => React.ReactNode;
|
||||
skipFilterOptionFn?: (filterOption: FilterOption) => boolean;
|
||||
}
|
||||
|
||||
export function filterExtraInformation(object: FilterExtraInformationValues): FilterExtraInformationValues {
|
||||
const defaultValues: Partial<FilterExtraInformationValues> = {
|
||||
isClearable: true,
|
||||
showInputLabel: true,
|
||||
};
|
||||
|
||||
const result = { ...object };
|
||||
|
||||
Object.keys(defaultValues).forEach((key) => {
|
||||
if (!result.hasOwnProperty(key)) {
|
||||
result[key] = defaultValues[key];
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface RemoteFiltersState {
|
||||
filterOptions?: FilterOption[];
|
||||
filters: FilterOption[];
|
||||
|
|
@ -86,12 +105,12 @@ class _RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
|
|||
query,
|
||||
page,
|
||||
store: { filtersStore },
|
||||
defaultFilters,
|
||||
skipFilterOptionFn,
|
||||
} = this.props;
|
||||
|
||||
let filterOptions = await filtersStore.updateOptionsForPage(page);
|
||||
const currentTablePageNum = parseInt(filtersStore.currentTablePageNum[page] || query.p || 1, 10);
|
||||
const defaultFilters = this.extractDefaultValuesFromExtraInformation();
|
||||
|
||||
if (skipFilterOptionFn) {
|
||||
filterOptions = filterOptions.filter((option: FilterOption) => !skipFilterOptionFn(option));
|
||||
|
|
@ -100,11 +119,11 @@ class _RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
|
|||
// set the current page from filters/query or default it to 1
|
||||
filtersStore.setCurrentTablePageNum(page, currentTablePageNum);
|
||||
|
||||
let { filters, values } = parseFilters({ ...query, ...filtersStore.globalValues }, filterOptions, query);
|
||||
|
||||
if (allFieldsEmpty(values)) {
|
||||
({ filters, values } = parseFilters(defaultFilters, filterOptions, query));
|
||||
}
|
||||
let { filters, values } = parseFilters(
|
||||
{ ...defaultFilters, ...query, ...filtersStore.globalValues },
|
||||
filterOptions,
|
||||
query
|
||||
);
|
||||
|
||||
this.setState({ filterOptions, filters, values }, () => this.onChange(true));
|
||||
}
|
||||
|
|
@ -147,28 +166,8 @@ class _RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
|
|||
|
||||
return (
|
||||
<div className={styles.filters}>
|
||||
{filters.map((filterOption: FilterOption) => (
|
||||
<div key={filterOption.name} className={styles.filter}>
|
||||
<Text withBackground wrap={false} type="primary">
|
||||
{filterOption.display_name || capitalCase(filterOption.name)}
|
||||
{filterOption.description && (
|
||||
<span className={styles.infoIcon}>
|
||||
<Tooltip content={filterOption.description}>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
{this.renderFilterOption(filterOption)}
|
||||
<Button
|
||||
size="md"
|
||||
icon="times"
|
||||
tooltip="Remove filter"
|
||||
variant="secondary"
|
||||
onClick={this.getDeleteFilterClickHandler(filterOption.name)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{filters.map(this.renderFilterBlock)}
|
||||
|
||||
<div className={styles.filterOptions}>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
|
|
@ -187,6 +186,67 @@ class _RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
|
|||
);
|
||||
};
|
||||
|
||||
renderFilterBlock = (filterOption: FilterOption) => {
|
||||
const { theme, extraInformation } = this.props;
|
||||
const showInputLabel = this.getExtraInformationField(filterOption, 'showInputLabel');
|
||||
const isInputClearable = this.getExtraInformationField(filterOption, 'isClearable');
|
||||
|
||||
const styles = getStyles(theme);
|
||||
|
||||
const filterElement = (
|
||||
<div key={filterOption.name} className={styles.filter}>
|
||||
<RenderConditionally shouldRender={showInputLabel}>
|
||||
<Text withBackground wrap={false} type="primary">
|
||||
{filterOption.display_name || capitalCase(filterOption.name)}
|
||||
{filterOption.description && (
|
||||
<span className={styles.infoIcon}>
|
||||
<Tooltip content={filterOption.description}>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
</RenderConditionally>
|
||||
|
||||
{this.renderFilterOption(filterOption)}
|
||||
|
||||
<RenderConditionally shouldRender={isInputClearable}>
|
||||
<Button
|
||||
size="md"
|
||||
icon="times"
|
||||
tooltip="Remove filter"
|
||||
variant="secondary"
|
||||
onClick={this.getDeleteFilterClickHandler(filterOption.name)}
|
||||
/>
|
||||
</RenderConditionally>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (extraInformation?.[filterOption.name]?.portal?.current) {
|
||||
return ReactDOM.createPortal(filterElement, extraInformation[filterOption.name].portal.current);
|
||||
}
|
||||
|
||||
return filterElement;
|
||||
};
|
||||
|
||||
getExtraInformationField = (filterOption: FilterOption, key: keyof FilterExtraInformationValues) => {
|
||||
return filterExtraInformation(this.props.extraInformation?.[filterOption.name])?.[key];
|
||||
};
|
||||
|
||||
extractDefaultValuesFromExtraInformation = (): { [key: string]: FilterExtraInformationValues } => {
|
||||
const { extraInformation } = this.props;
|
||||
|
||||
return extraInformation
|
||||
? Object.keys(extraInformation).reduce((acc, key) => {
|
||||
if (extraInformation[key].value) {
|
||||
acc[key] = extraInformation[key].value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
};
|
||||
|
||||
handleSearch = (query: string) => {
|
||||
const { filters } = this.state;
|
||||
|
||||
|
|
@ -324,12 +384,10 @@ class _RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
|
|||
const value = getValueForDateRangeFilterType(values[filter.name]);
|
||||
|
||||
return (
|
||||
<TimeRangeInput
|
||||
<TimeRangePickerWrapper
|
||||
timeZone={moment.tz.guess()}
|
||||
value={value}
|
||||
onChange={this.getDateRangeFilterChangeHandler(filter.name)}
|
||||
hideTimeZone
|
||||
clearable={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -465,7 +523,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||
filters: css`
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid ${theme.colors.border.weak}
|
||||
border-radius: 2px;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { dateMath, DateTime, TimeRange, toUtc } from '@grafana/data';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { TimeRangePicker } from '@grafana/ui';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
interface TimeRangePickerProps {
|
||||
value: TimeRange;
|
||||
timeZone: string;
|
||||
|
||||
onChange: (timeRange: TimeRange) => void;
|
||||
}
|
||||
|
||||
export function TimeRangePickerWrapper(props: TimeRangePickerProps) {
|
||||
const { onChange } = props;
|
||||
|
||||
const [value, setValue] = useState<TimeRange>(props.value);
|
||||
const [timezone, setTimezone] = useState<string>(props.timeZone);
|
||||
|
||||
return (
|
||||
<TimeRangePicker
|
||||
isOnCanvas
|
||||
timeZone={timezone}
|
||||
value={value}
|
||||
onChange={onPickerChange}
|
||||
onZoom={onZoom}
|
||||
onMoveBackward={onMoveBackward}
|
||||
onMoveForward={onMoveForward}
|
||||
onChangeTimeZone={onTimezoneChange}
|
||||
onChangeFiscalYearStartMonth={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
function onPickerChange(timeRange: TimeRange) {
|
||||
setValue(timeRange);
|
||||
onChange(timeRange);
|
||||
}
|
||||
|
||||
function onZoom() {
|
||||
const zoomedTimeRange = getZoomedTimeRange(value, 2);
|
||||
onPickerChange(zoomedTimeRange);
|
||||
}
|
||||
|
||||
function onMoveBackward() {
|
||||
onPickerChange(getShiftedTimeRange(TimeRangeDirection.Backward, value, Date.now()));
|
||||
}
|
||||
|
||||
function onMoveForward() {
|
||||
onPickerChange(getShiftedTimeRange(TimeRangeDirection.Forward, value, Date.now()));
|
||||
}
|
||||
|
||||
function onTimezoneChange(timeZone: TimeZone) {
|
||||
setTimezone(timeZone);
|
||||
onPickerChange(evaluateTimeRange(value.from, value.to, timeZone));
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateTimeRange(
|
||||
from: string | DateTime,
|
||||
to: string | DateTime,
|
||||
timeZone: TimeZone,
|
||||
fiscalYearStartMonth?: number,
|
||||
delay?: string
|
||||
): TimeRange {
|
||||
const hasDelay = delay && to === 'now';
|
||||
|
||||
return {
|
||||
from: dateMath.parse(from, false, timeZone, fiscalYearStartMonth)!,
|
||||
to: dateMath.parse(hasDelay ? 'now-' + delay : to, true, timeZone, fiscalYearStartMonth)!,
|
||||
raw: {
|
||||
from: from,
|
||||
to: to,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getZoomedTimeRange(timeRange: TimeRange, factor: number): TimeRange {
|
||||
const timespan = timeRange.to.valueOf() - timeRange.from.valueOf();
|
||||
const center = timeRange.to.valueOf() - timespan / 2;
|
||||
// If the timepsan is 0, zooming out would do nothing, so we force a zoom out to 30s
|
||||
const newTimespan = timespan === 0 ? 30000 : timespan * factor;
|
||||
|
||||
const to = center + newTimespan / 2;
|
||||
const from = center - newTimespan / 2;
|
||||
|
||||
return { from: toUtc(from), to: toUtc(to), raw: { from: toUtc(from), to: toUtc(to) } };
|
||||
}
|
||||
|
||||
enum TimeRangeDirection {
|
||||
Backward,
|
||||
Forward,
|
||||
}
|
||||
|
||||
function getShiftedTimeRange(dir: TimeRangeDirection, timeRange: TimeRange, upperLimit: number): TimeRange {
|
||||
const oldTo = timeRange.to.valueOf();
|
||||
const oldFrom = timeRange.from.valueOf();
|
||||
const halfSpan = (oldTo - oldFrom) / 2;
|
||||
|
||||
let fromRaw: number;
|
||||
let toRaw: number;
|
||||
if (dir === TimeRangeDirection.Backward) {
|
||||
fromRaw = oldFrom - halfSpan;
|
||||
toRaw = oldTo - halfSpan;
|
||||
} else {
|
||||
fromRaw = oldFrom + halfSpan;
|
||||
toRaw = oldTo + halfSpan;
|
||||
|
||||
if (toRaw > upperLimit && oldTo < upperLimit) {
|
||||
toRaw = upperLimit;
|
||||
fromRaw = oldFrom;
|
||||
}
|
||||
}
|
||||
|
||||
const from = toUtc(fromRaw);
|
||||
const to = toUtc(toRaw);
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
raw: { from, to },
|
||||
};
|
||||
}
|
||||
|
|
@ -172,6 +172,7 @@ export class AlertGroupStore {
|
|||
this.liveUpdatesPaused = value;
|
||||
}
|
||||
|
||||
@WithGlobalNotification({ failure: PROCESSING_REQUEST_ERROR })
|
||||
@AutoLoadingState(ActionKey.UPDATE_FILTERS_AND_FETCH_INCIDENTS)
|
||||
async updateIncidentFiltersAndRefetchIncidentsAndStats(params: any, keepCursor = false) {
|
||||
if (!keepCursor) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SelectOption } from 'state/types';
|
||||
|
||||
export interface FiltersValues {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface FilterExtraInformationValues {
|
||||
isClearable?: boolean;
|
||||
value?: any;
|
||||
portal?: React.RefObject<any>;
|
||||
showInputLabel?: boolean;
|
||||
}
|
||||
|
||||
export interface FilterExtraInformation {
|
||||
[key: string]: FilterExtraInformationValues;
|
||||
}
|
||||
|
||||
export interface FilterOption {
|
||||
name: string;
|
||||
type: 'search' | 'options' | 'boolean' | 'daterange' | 'team_select';
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React, { SyntheticEvent } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { GrafanaTheme2, durationToMilliseconds, parseDuration, SelectableValue } from '@grafana/data';
|
||||
import { LabelTag } from '@grafana/labels';
|
||||
import {
|
||||
Button,
|
||||
HorizontalGroup,
|
||||
Icon,
|
||||
LoadingPlaceholder,
|
||||
RadioButtonGroup,
|
||||
RefreshPicker,
|
||||
Tooltip,
|
||||
VerticalGroup,
|
||||
withTheme2,
|
||||
|
|
@ -79,16 +79,18 @@ interface IncidentsPageState {
|
|||
isSelectorColumnMenuOpen: boolean;
|
||||
isHorizontalScrolling: boolean;
|
||||
isFirstIncidentsFetchDone: boolean;
|
||||
refreshInterval: string;
|
||||
}
|
||||
|
||||
const POLLING_NUM_SECONDS = 10;
|
||||
|
||||
const PAGINATION_OPTIONS = [
|
||||
{ label: '25', value: 25 },
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '100', value: 100 },
|
||||
];
|
||||
|
||||
const REFRESH_OPTIONS = ['5s', '10s', '15s', '30s', '1m', '5m'];
|
||||
const REFRESH_DEFAULT_VALUE = '10s';
|
||||
|
||||
const TABLE_SCROLL_OPTIONS: SelectableValue[] = [
|
||||
{
|
||||
value: false,
|
||||
|
|
@ -124,10 +126,12 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
store.alertGroupStore.incidentsCursor = cursorQuery || undefined;
|
||||
|
||||
this.rootElRef = React.createRef<HTMLDivElement>();
|
||||
this.filtersPortalRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
this.state = {
|
||||
selectedIncidentIds: [],
|
||||
showAddAlertGroupForm: false,
|
||||
refreshInterval: REFRESH_DEFAULT_VALUE,
|
||||
pagination: {
|
||||
start,
|
||||
end: start + pageSize,
|
||||
|
|
@ -138,6 +142,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
};
|
||||
}
|
||||
|
||||
private filtersPortalRef: React.RefObject<HTMLDivElement>;
|
||||
private rootElRef: React.RefObject<HTMLDivElement>;
|
||||
private pollingIntervalId: ReturnType<typeof setInterval> = undefined;
|
||||
|
||||
|
|
@ -160,25 +165,46 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
|
||||
render() {
|
||||
const { history } = this.props;
|
||||
const { showAddAlertGroupForm } = this.state;
|
||||
const { refreshInterval, showAddAlertGroupForm } = this.state;
|
||||
|
||||
const {
|
||||
theme,
|
||||
store,
|
||||
store: { alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
const styles = getStyles(theme);
|
||||
|
||||
const isLoading = LoaderHelper.isLoading(store.loaderStore, [
|
||||
ActionKey.FETCH_INCIDENTS,
|
||||
ActionKey.FETCH_INCIDENTS_POLLING,
|
||||
ActionKey.FETCH_INCIDENTS_AND_STATS,
|
||||
ActionKey.INCIDENTS_BULK_UPDATE,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className={styles.title}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title level={3}>Alert Groups</Text.Title>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsDirectPaging}>
|
||||
<Button icon="plus" onClick={this.handleOnClickEscalateTo}>
|
||||
Escalation
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
<div className={styles.rightSideFilters}>
|
||||
<div ref={this.filtersPortalRef} />
|
||||
<RefreshPicker
|
||||
onIntervalChanged={this.onIntervalRefreshChange}
|
||||
onRefresh={this.onRefresh}
|
||||
intervals={REFRESH_OPTIONS}
|
||||
value={refreshInterval}
|
||||
isLoading={isLoading}
|
||||
isOnCanvas
|
||||
showAutoInterval={false}
|
||||
/>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsDirectPaging}>
|
||||
<Button icon="plus" onClick={this.handleOnClickEscalateTo}>
|
||||
Escalation
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
{this.renderIncidentFilters()}
|
||||
|
|
@ -322,32 +348,54 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
query={query}
|
||||
page={PAGE.Incidents}
|
||||
onChange={this.handleFiltersChange}
|
||||
extraInformation={{
|
||||
started_at: {
|
||||
isClearable: false,
|
||||
value: 'now-30d_now',
|
||||
portal: this.filtersPortalRef,
|
||||
showInputLabel: false,
|
||||
},
|
||||
team: {
|
||||
value: [],
|
||||
},
|
||||
status: {
|
||||
value: [IncidentStatus.Firing, IncidentStatus.Acknowledged],
|
||||
},
|
||||
mine: {
|
||||
value: false,
|
||||
},
|
||||
}}
|
||||
extraFilters={(...args) => {
|
||||
return this.renderCards(...args, store, theme);
|
||||
}}
|
||||
grafanaTeamStore={store.grafanaTeamStore}
|
||||
defaultFilters={{
|
||||
team: [],
|
||||
status: [IncidentStatus.Firing, IncidentStatus.Acknowledged],
|
||||
mine: false,
|
||||
started_at: 'now-30d_now',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onRefresh = async () => {
|
||||
this.clearPollingInterval();
|
||||
await this.props.store.alertGroupStore.fetchIncidentsAndStats(true);
|
||||
this.setPollingInterval();
|
||||
};
|
||||
|
||||
onIntervalRefreshChange = (value: string) => {
|
||||
this.clearPollingInterval();
|
||||
this.setState({ refreshInterval: value }, () => value && this.setPollingInterval());
|
||||
};
|
||||
|
||||
handleOnClickEscalateTo = () => {
|
||||
this.setState({ showAddAlertGroupForm: true });
|
||||
};
|
||||
|
||||
handleFiltersChange = async (filters: IncidentsFiltersType, isOnMount: boolean) => {
|
||||
const {
|
||||
store: { alertGroupStore },
|
||||
} = this.props;
|
||||
|
||||
const { alertGroupStore } = this.props.store;
|
||||
const { start } = this.state.pagination;
|
||||
|
||||
// Clear polling whenever filters change
|
||||
this.clearPollingInterval();
|
||||
|
||||
this.setState({
|
||||
filters,
|
||||
selectedIncidentIds: [],
|
||||
|
|
@ -357,7 +405,12 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
this.setPagination(1, alertGroupStore.alertsSearchResult.page_size);
|
||||
}
|
||||
|
||||
await this.fetchIncidentData(filters);
|
||||
try {
|
||||
await this.fetchIncidentData(filters);
|
||||
} finally {
|
||||
// Re-enable polling after query is done
|
||||
this.setPollingInterval();
|
||||
}
|
||||
|
||||
if (isOnMount) {
|
||||
this.setPagination(start, start + alertGroupStore.alertsSearchResult.page_size - 1);
|
||||
|
|
@ -437,15 +490,8 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
return null;
|
||||
}
|
||||
|
||||
const hasSelected = selectedIncidentIds.length > 0;
|
||||
const isLoading = LoaderHelper.isLoading(store.loaderStore, [
|
||||
ActionKey.FETCH_INCIDENTS,
|
||||
ActionKey.FETCH_INCIDENTS_POLLING,
|
||||
ActionKey.FETCH_INCIDENTS_AND_STATS,
|
||||
ActionKey.INCIDENTS_BULK_UPDATE,
|
||||
]);
|
||||
|
||||
const styles = getStyles(theme);
|
||||
const hasSelected = selectedIncidentIds.length > 0;
|
||||
const isBulkUpdate = LoaderHelper.isLoading(store.loaderStore, ActionKey.INCIDENTS_BULK_UPDATE);
|
||||
|
||||
return (
|
||||
|
|
@ -493,7 +539,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
<Text type="secondary">
|
||||
<Text type="secondary" className={styles.alertsSelected}>
|
||||
{hasSelected
|
||||
? `${selectedIncidentIds.length} Alert Group${selectedIncidentIds.length > 1 ? 's' : ''} selected`
|
||||
: 'No Alert Groups selected'}
|
||||
|
|
@ -501,10 +547,6 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
</div>
|
||||
|
||||
<div className={styles.fieldsDropdown}>
|
||||
<RenderConditionally shouldRender={isLoading}>
|
||||
<LoadingPlaceholder text="Loading..." className={styles.loadingPlaceholder} />
|
||||
</RenderConditionally>
|
||||
|
||||
<RenderConditionally shouldRender={store.hasFeature(AppFeature.Labels)}>
|
||||
<RadioButtonGroup
|
||||
options={TABLE_SCROLL_OPTIONS}
|
||||
|
|
@ -964,6 +1006,12 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
|
||||
setPollingInterval() {
|
||||
const startPolling = () => {
|
||||
if (!this.state.refreshInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pollingNum = durationToMilliseconds(parseDuration(this.state.refreshInterval));
|
||||
|
||||
this.pollingIntervalId = setTimeout(async () => {
|
||||
const isBrowserWindowInactive = document.hidden;
|
||||
const { liveUpdatesPaused } = this.props.store.alertGroupStore;
|
||||
|
|
@ -984,7 +1032,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
}
|
||||
|
||||
startPolling();
|
||||
}, POLLING_NUM_SECONDS * 1000);
|
||||
}, pollingNum);
|
||||
};
|
||||
|
||||
startPolling();
|
||||
|
|
@ -997,6 +1045,15 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||
width: 400px;
|
||||
`,
|
||||
|
||||
alertsSelected: css`
|
||||
white-space: nowrap;
|
||||
`,
|
||||
|
||||
rightSideFilters: css`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`,
|
||||
|
||||
bau: css`
|
||||
${[1, 2, 3].map(
|
||||
(num) => `
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AppRootProps as BaseAppRootProps, AppPluginMeta, CurrentUserDTO, PluginConfigPageProps } from '@grafana/data';
|
||||
import { AppRootProps as BaseAppRootProps, AppPluginMeta, PluginConfigPageProps, CurrentUserDTO } from '@grafana/data';
|
||||
|
||||
export type OnCallPluginMetaJSONData = {
|
||||
stackId: number;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue