Pagination refactoring/cleanup (#3205)

# What this PR does

- Pagination cleanup so that frontend no longer hardcodes page size
- Moved pagination to the filters store instead for all pages that have
pagination enabled
- Prevent UI triggering polling if window is inactive or if the previous
request didn't complete

## Which issue(s) this PR fixes

#2931 
#2462

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [ ] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
This commit is contained in:
Rares Mardare 2023-10-30 16:20:14 +02:00 committed by GitHub
parent c78ab58b44
commit 871860c399
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 286 additions and 252 deletions

View file

@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Simplify Direct Paging workflow. Now when using Direct Paging you either simply specify a team, or one or more users
to page by @joeyorlando ([#3128](https://github.com/grafana/oncall/pull/3128))
- Enable timing options for mobile push notifications, allow multi-select by @Ferril ([#3187](https://github.com/grafana/oncall/pull/3187))
- Removed the hardcoding of page size on frontend ([#3205](https://github.com/grafana/oncall/pull/#3205))
- Prevent additional polling on Incidents if the previous request didn't complete
([#3205](https://github.com/grafana/oncall/pull/#3205))
### Fixed

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { SelectableValue, TimeRange } from '@grafana/data';
import { KeyValue, SelectableValue, TimeRange } from '@grafana/data';
import {
InlineSwitch,
MultiSelect,
@ -31,16 +31,15 @@ import LocationHelper from 'utils/LocationHelper';
import { PAGE } from 'utils/consts';
import { parseFilters } from './RemoteFilters.helpers';
import { FilterOption, RemoteFiltersType } from './RemoteFilters.types';
import { FilterOption } from './RemoteFilters.types';
import styles from './RemoteFilters.module.css';
const cx = cn.bind(styles);
interface RemoteFiltersProps extends WithStoreProps {
value: RemoteFiltersType;
onChange: (filters: { [key: string]: any }, isOnMount: boolean, invalidateFn: () => boolean) => void;
query: { [key: string]: any };
query: KeyValue;
page: PAGE;
defaultFilters?: FiltersValues;
extraFilters?: (state, setState, onFiltersValueChange) => React.ReactNode;
@ -82,11 +81,18 @@ class RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
}
async componentDidMount() {
const { query, page, store, defaultFilters } = this.props;
const { filtersStore } = store;
const {
query,
page,
store: { filtersStore },
defaultFilters,
} = this.props;
const filterOptions = await filtersStore.updateOptionsForPage(page);
const currentTablePageNum = parseInt(filtersStore.currentTablePageNum[page] || query.p || 1, 10);
// 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);
@ -422,10 +428,7 @@ class RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
}
const currentRequestId = this.getNewRequestId();
this.setState({
lastRequestId: currentRequestId,
});
this.setState({ lastRequestId: currentRequestId });
LocationHelper.update({ ...values }, 'partial');
onChange(values, isOnMount, this.invalidateFn.bind(this, currentRequestId));
@ -443,4 +446,6 @@ class RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
debouncedOnChange = debounce(this.onChange, 500);
}
export default withMobXProviderContext(RemoteFilters);
export default withMobXProviderContext(RemoteFilters) as unknown as React.ComponentClass<
Omit<RemoteFiltersProps, 'store'>
>;

View file

@ -28,7 +28,7 @@ export class AlertReceiveChannelStore extends BaseStore {
searchResult: Array<AlertReceiveChannel['id']>;
@observable.shallow
paginatedSearchResult: { count?: number; results?: Array<AlertReceiveChannel['id']> } = {};
paginatedSearchResult: { count?: number; results?: Array<AlertReceiveChannel['id']>; page_size?: number } = {};
@observable.shallow
items: { [id: string]: AlertReceiveChannel } = {};
@ -81,6 +81,7 @@ export class AlertReceiveChannelStore extends BaseStore {
}
return {
page_size: this.paginatedSearchResult.page_size,
count: this.paginatedSearchResult.count,
results:
this.paginatedSearchResult.results &&
@ -133,7 +134,7 @@ export class AlertReceiveChannelStore extends BaseStore {
async updatePaginatedItems(query: any = '', page = 1, updateCounters = false, invalidateFn = undefined) {
const filters = typeof query === 'string' ? { search: query } : query;
const { count, results } = await makeRequest(this.path, { params: { ...filters, page } });
const { count, results, page_size } = await makeRequest(this.path, { params: { ...filters, page } });
if (invalidateFn?.()) {
return undefined;
@ -155,6 +156,7 @@ export class AlertReceiveChannelStore extends BaseStore {
this.paginatedSearchResult = {
count,
results: results.map((item: AlertReceiveChannel) => item.id),
page_size,
};
if (updateCounters) {

View file

@ -41,10 +41,14 @@ export class AlertGroupStore extends BaseStore {
incidentsCursor?: string;
@observable
incidentsItemsPerPage?: number;
@observable
alertsSearchResult: any = {};
alertsSearchResult: {
[key: string]: {
prev?: string;
next?: string;
results?: string[];
page_size?: number;
};
} = {};
@observable
alerts = new Map<string, Alert>();
@ -89,29 +93,6 @@ export class AlertGroupStore extends BaseStore {
}).catch(showApiError);
}
@action // FIXME for `attach to` feature ONLY
async updateItems(query = '') {
const { results } = await makeRequest(`${this.path}`, {
params: { search: query, resolved: false, is_root: true },
});
this.items = {
...this.items,
...results.reduce(
(acc: { [key: string]: Alert }, item: Alert) => ({
...acc,
[item.pk]: item,
}),
{}
),
};
this.searchResult = {
...this.searchResult,
[query]: results.map((item: Alert) => item.pk),
};
}
async updateItem(id: Alert['pk']) {
const item = await this.getById(id);
@ -220,12 +201,13 @@ export class AlertGroupStore extends BaseStore {
// TODO check if methods are dublicating existing ones
@action
async updateIncidents() {
this.getNewIncidentsStats();
this.getAcknowledgedIncidentsStats();
this.getResolvedIncidentsStats();
this.getSilencedIncidentsStats();
this.updateAlertGroups();
await Promise.all([
this.getNewIncidentsStats(),
this.getAcknowledgedIncidentsStats(),
this.getResolvedIncidentsStats(),
this.getSilencedIncidentsStats(),
this.updateAlertGroups(),
]);
this.liveUpdatesPaused = false;
}
@ -238,7 +220,7 @@ export class AlertGroupStore extends BaseStore {
this.incidentFilters = params;
this.updateIncidents();
await this.updateIncidents();
}
@action
@ -256,9 +238,8 @@ export class AlertGroupStore extends BaseStore {
}
@action
async setIncidentsItemsPerPage(value: number) {
async setIncidentsItemsPerPage() {
this.setIncidentsCursor(undefined);
this.incidentsItemsPerPage = value;
this.updateAlertGroups();
}
@ -271,11 +252,12 @@ export class AlertGroupStore extends BaseStore {
results,
next: nextRaw,
previous: previousRaw,
page_size,
} = await makeRequest(`${this.path}`, {
params: {
...this.incidentFilters,
perpage: this.alertsSearchResult?.['default']?.page_size,
cursor: this.incidentsCursor,
perpage: this.incidentsItemsPerPage,
is_root: true,
},
}).catch(refreshPageError);
@ -298,17 +280,24 @@ export class AlertGroupStore extends BaseStore {
prev: prevCursor,
next: nextCursor,
results: results.map((alert: Alert) => alert.pk),
page_size,
};
this.alertGroupsLoading = false;
}
getAlertSearchResult(query: string) {
if (!this.alertsSearchResult[query]) {
return undefined;
const result = this.alertsSearchResult[query];
if (!result) {
return {};
}
return this.alertsSearchResult[query].results.map((pk: Alert['pk']) => this.alerts.get(pk));
return {
prev: result.prev,
next: result.next,
page_size: result.page_size,
results: result.results.map((pk: Alert['pk']) => this.alerts.get(pk)),
};
}
@action

View file

@ -3,6 +3,7 @@ import { action, observable } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
import { RootStore } from 'state';
import { PAGE } from 'utils/consts';
import { getItem, setItem } from 'utils/localStorage';
import { getApiPathByPage } from './filters.helpers';
@ -17,6 +18,9 @@ export class FiltersStore extends BaseStore {
@observable.shallow
public values: { [page: string]: FiltersValues } = {};
@observable.shallow
public currentTablePageNum: { [page: string]: number } = {};
private _globalValues: FiltersValues = {};
@observable
@ -65,4 +69,9 @@ export class FiltersStore extends BaseStore {
[page]: value,
};
}
@action
setCurrentTablePageNum(page: PAGE, currentTablePageNum: number) {
this.currentTablePageNum[page] = currentTablePageNum;
}
}

View file

@ -17,7 +17,7 @@ import { User } from './user.types';
export class UserStore extends BaseStore {
@observable.shallow
searchResult: { count?: number; results?: Array<User['pk']> } = {};
searchResult: { count?: number; results?: Array<User['pk']>; page_size?: number } = {};
@observable.shallow
items: { [pk: string]: User } = {};
@ -122,7 +122,7 @@ export class UserStore extends BaseStore {
return;
}
const { count, results } = response;
const { count, results, page_size } = response;
this.items = {
...this.items,
@ -140,6 +140,7 @@ export class UserStore extends BaseStore {
this.searchResult = {
count,
page_size,
results: results.map((item: User) => item.pk),
};
@ -148,6 +149,7 @@ export class UserStore extends BaseStore {
getSearchResult() {
return {
page_size: this.searchResult.page_size,
count: this.searchResult.count,
results: this.searchResult.results && this.searchResult.results.map((userPk: User['pk']) => this.items?.[userPk]),
};

View file

@ -1,8 +1,7 @@
import React, { ReactElement, SyntheticEvent } from 'react';
import React, { SyntheticEvent } from 'react';
import { Button, HorizontalGroup, Icon, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
import { Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { get } from 'lodash-es';
import { observer } from 'mobx-react';
import moment from 'moment-timezone';
import Emoji from 'react-emoji-render';
@ -40,19 +39,6 @@ interface Pagination {
start: number;
end: number;
}
function withSkeleton(fn: (alert: AlertType) => ReactElement | ReactElement[]) {
const WithSkeleton = (alert: AlertType) => {
if (alert.short) {
return <LoadingPlaceholder text={''} />;
}
return fn(alert);
};
return WithSkeleton;
}
interface IncidentsPageProps extends WithStoreProps, PageProps, RouteComponentProps {}
interface IncidentsPageState {
@ -63,9 +49,14 @@ interface IncidentsPageState {
showAddAlertGroupForm: boolean;
}
const ITEMS_PER_PAGE = 25;
const POLLING_NUM_SECONDS = 15;
const PAGINATION_OPTIONS = [
{ label: '25', value: 25 },
{ label: '50', value: 50 },
{ label: '100', value: 100 },
];
@observer
class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState> {
constructor(props: IncidentsPageProps) {
@ -76,12 +67,10 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
query: { cursor: cursorQuery, start: startQuery, perpage: perpageQuery },
} = props;
const cursor = cursorQuery || undefined;
const start = !isNaN(startQuery) ? Number(startQuery) : 1;
const itemsPerPage = !isNaN(perpageQuery) ? Number(perpageQuery) : ITEMS_PER_PAGE;
const pageSize = !isNaN(perpageQuery) ? Number(perpageQuery) : undefined;
store.alertGroupStore.incidentsCursor = cursor;
store.alertGroupStore.incidentsItemsPerPage = itemsPerPage;
store.alertGroupStore.incidentsCursor = cursorQuery || undefined;
this.state = {
selectedIncidentIds: [],
@ -89,16 +78,20 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
showAddAlertGroupForm: false,
pagination: {
start,
end: start + itemsPerPage - 1,
end: start + pageSize,
},
};
store.alertGroupStore.updateBulkActions();
store.alertGroupStore.updateSilenceOptions();
}
private pollingIntervalId: NodeJS.Timer = undefined;
componentDidMount() {
const { alertGroupStore } = this.props.store;
alertGroupStore.updateBulkActions();
alertGroupStore.updateSilenceOptions();
}
componentWillUnmount(): void {
this.clearPollingInterval();
}
@ -279,8 +272,12 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
this.setState({ showAddAlertGroupForm: true });
};
handleFiltersChange = (filters: IncidentsFiltersType, isOnMount: boolean) => {
const { store } = this.props;
handleFiltersChange = async (filters: IncidentsFiltersType, isOnMount: boolean) => {
const {
store: { alertGroupStore },
} = this.props;
const { start } = this.state.pagination;
this.setState({
filters,
@ -288,45 +285,50 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
});
if (!isOnMount) {
this.setState({
pagination: {
start: 1,
end: store.alertGroupStore.incidentsItemsPerPage,
},
});
this.setPagination(1, alertGroupStore.alertsSearchResult['default'].page_size);
}
this.clearPollingInterval();
this.setPollingInterval(filters, isOnMount);
this.fetchIncidentData(filters, isOnMount);
await this.fetchIncidentData(filters, isOnMount);
if (isOnMount) {
this.setPagination(start, start + alertGroupStore.alertsSearchResult['default'].page_size - 1);
}
};
fetchIncidentData = (filters: IncidentsFiltersType, isOnMount: boolean) => {
setPagination = (start = this.state.pagination?.start, end = this.state.pagination?.end) => {
this.setState({
pagination: {
start,
end,
},
});
};
fetchIncidentData = async (filters: IncidentsFiltersType, isOnMount: boolean) => {
const { store } = this.props;
store.alertGroupStore.updateIncidentFilters(filters, isOnMount); // this line fetches incidents
await store.alertGroupStore.updateIncidentFilters(filters, isOnMount); // this line fetches the incidents
LocationHelper.update({ ...store.alertGroupStore.incidentFilters }, 'partial');
};
onChangeCursor = (cursor: string, direction: 'prev' | 'next') => {
const { store } = this.props;
const { alertGroupStore } = this.props.store;
const pageSize = alertGroupStore.alertsSearchResult['default'].page_size;
store.alertGroupStore.updateIncidentsCursor(cursor);
alertGroupStore.updateIncidentsCursor(cursor);
this.setState(
{
selectedIncidentIds: [],
pagination: {
start:
this.state.pagination.start + store.alertGroupStore.incidentsItemsPerPage * (direction === 'prev' ? -1 : 1),
end:
this.state.pagination.end + store.alertGroupStore.incidentsItemsPerPage * (direction === 'prev' ? -1 : 1),
start: this.state.pagination.start + pageSize * (direction === 'prev' ? -1 : 1),
end: this.state.pagination.end + pageSize * (direction === 'prev' ? -1 : 1),
},
},
() => {
LocationHelper.update(
{ start: this.state.pagination.start, perpage: store.alertGroupStore.incidentsItemsPerPage },
'partial'
);
LocationHelper.update({ start: this.state.pagination.start, perpage: pageSize }, 'partial');
}
);
};
@ -334,15 +336,22 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
handleChangeItemsPerPage = (value: number) => {
const { store } = this.props;
store.alertGroupStore.setIncidentsItemsPerPage(value);
store.alertGroupStore.alertsSearchResult['default'] = {
...store.alertGroupStore.alertsSearchResult['default'],
page_size: value,
};
store.alertGroupStore.setIncidentsItemsPerPage();
this.setState({
selectedIncidentIds: [],
pagination: {
start: 1,
end: store.alertGroupStore.incidentsItemsPerPage,
end: value,
},
});
LocationHelper.update({ start: 1, perpage: value }, 'partial');
};
renderBulkActions = () => {
@ -353,7 +362,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
return null;
}
const results = store.alertGroupStore.getAlertSearchResult('default');
const { results } = store.alertGroupStore.getAlertSearchResult('default');
const hasSelected = selectedIncidentIds.length > 0;
const hasInvalidatedAlert = Boolean(
@ -431,14 +440,9 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
renderTable() {
const { selectedIncidentIds, pagination } = this.state;
const {
store,
store: { alertGroupStore, filtersStore },
} = this.props;
const { alertGroupStore, filtersStore } = this.props.store;
const results = alertGroupStore.getAlertSearchResult('default');
const prev = get(alertGroupStore.alertsSearchResult, `default.prev`);
const next = get(alertGroupStore.alertsSearchResult, `default.next`);
const { results, prev, next } = alertGroupStore.getAlertSearchResult('default');
const isLoading = alertGroupStore.alertGroupsLoading || filtersStore.options['incidents'] === undefined;
if (results && !results.length) {
@ -462,57 +466,6 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
);
}
const columns = [
{
width: '140px',
title: 'Status',
key: 'time',
render: withSkeleton(this.renderStatus),
},
{
width: '10%',
title: 'ID',
key: 'id',
render: withSkeleton(this.renderId),
},
{
width: '35%',
title: 'Title',
key: 'title',
render: withSkeleton(this.renderTitle),
},
{
width: '5%',
title: 'Alerts',
key: 'alerts',
render: withSkeleton(this.renderAlertsCounter),
},
{
width: '15%',
title: 'Integration',
key: 'source',
render: withSkeleton(this.renderSource),
},
{
width: '10%',
title: 'Created',
key: 'created',
render: withSkeleton(this.renderStartedAt),
},
{
width: '10%',
title: 'Team',
key: 'team',
render: withSkeleton((item: AlertType) => this.renderTeam(item, store.grafanaTeamStore.items)),
},
{
width: '15%',
title: 'Users',
key: 'users',
render: withSkeleton(renderRelatedUsers),
},
];
return (
<div className={cx('root')}>
{this.renderBulkActions()}
@ -526,33 +479,25 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
}}
rowKey="pk"
data={results}
columns={columns}
columns={this.getTableColumns()}
/>
<div className={cx('pagination')}>
<CursorPagination
current={`${pagination.start}-${pagination.end}`}
itemsPerPage={alertGroupStore.incidentsItemsPerPage}
itemsPerPageOptions={[
{ label: '25', value: 25 },
{ label: '50', value: 50 },
{ label: '100', value: 100 },
]}
prev={prev}
next={next}
onChange={this.onChangeCursor}
onChangeItemsPerPage={this.handleChangeItemsPerPage}
/>
</div>
{this.shouldShowPagination() && (
<div className={cx('pagination')}>
<CursorPagination
current={`${pagination.start}-${pagination.end}`}
itemsPerPage={alertGroupStore.alertsSearchResult?.['default']?.page_size}
itemsPerPageOptions={PAGINATION_OPTIONS}
prev={prev}
next={next}
onChange={this.onChangeCursor}
onChangeItemsPerPage={this.handleChangeItemsPerPage}
/>
</div>
)}
</div>
);
}
handleSelectedIncidentIdsChange = (ids: Array<Alert['pk']>) => {
this.setState({ selectedIncidentIds: ids }, () => {
ids.length > 0 ? this.clearPollingInterval() : this.setPollingInterval();
});
};
renderId(record: AlertType) {
return (
<TextEllipsisTooltip placement="top" content={`#${record.inside_organization_number}`}>
@ -565,11 +510,8 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
renderTitle = (record: AlertType) => {
const { store, query } = this.props;
const {
pagination: { start },
} = this.state;
const { incidentsItemsPerPage, incidentsCursor } = store.alertGroupStore;
const { start } = this.state.pagination || {};
const { incidentsCursor } = store.alertGroupStore;
return (
<div>
@ -580,7 +522,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
page: 'alert-groups',
id: record.pk,
cursor: incidentsCursor,
perpage: incidentsItemsPerPage,
perpage: store.alertGroupStore.alertsSearchResult?.['default']?.page_size,
start,
...query,
}}
@ -649,6 +591,77 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
);
}
shouldShowPagination() {
const { alertGroupStore } = this.props.store;
return Boolean(
this.state.pagination?.start &&
this.state.pagination?.end &&
alertGroupStore.alertsSearchResult?.['default']?.page_size
);
}
handleSelectedIncidentIdsChange = (ids: Array<Alert['pk']>) => {
this.setState({ selectedIncidentIds: ids }, () => {
ids.length > 0 ? this.clearPollingInterval() : this.setPollingInterval();
});
};
getTableColumns(): Array<{ width: string; title: string; key: string; render }> {
const { store } = this.props;
return [
{
width: '140px',
title: 'Status',
key: 'time',
render: this.renderStatus,
},
{
width: '10%',
title: 'ID',
key: 'id',
render: this.renderId,
},
{
width: '35%',
title: 'Title',
key: 'title',
render: this.renderTitle,
},
{
width: '5%',
title: 'Alerts',
key: 'alerts',
render: this.renderAlertsCounter,
},
{
width: '15%',
title: 'Integration',
key: 'source',
render: this.renderSource,
},
{
width: '10%',
title: 'Created',
key: 'created',
render: this.renderStartedAt,
},
{
width: '10%',
title: 'Team',
key: 'team',
render: (item: AlertType) => this.renderTeam(item, store.grafanaTeamStore.items),
},
{
width: '15%',
title: 'Users',
key: 'users',
render: renderRelatedUsers,
},
];
}
getOnActionButtonClick = (incidentId: string, action: AlertAction): ((e: SyntheticEvent) => Promise<void>) => {
const { store } = this.props;
@ -719,13 +732,28 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
clearPollingInterval() {
clearInterval(this.pollingIntervalId);
this.pollingIntervalId = undefined;
this.pollingIntervalId = null;
}
setPollingInterval(filters: IncidentsFiltersType = this.state.filters, isOnMount = false) {
this.pollingIntervalId = setInterval(() => {
this.fetchIncidentData(filters, isOnMount);
}, POLLING_NUM_SECONDS * 1000);
const startPolling = (delayed = false) => {
this.pollingIntervalId = setTimeout(
async () => {
const isBrowserWindowInactive = document.hidden;
if (!isBrowserWindowInactive) {
await this.fetchIncidentData(filters, isOnMount);
}
if (this.pollingIntervalId === null) {
return;
}
startPolling(isBrowserWindowInactive);
},
delayed ? 60 * 1000 : POLLING_NUM_SECONDS * 1000
);
};
startPolling();
}
}

View file

@ -42,7 +42,6 @@ import styles from './Integrations.module.scss';
const cx = cn.bind(styles);
const FILTERS_DEBOUNCE_MS = 500;
const ITEMS_PER_PAGE = 15;
interface IntegrationsState extends PageBaseState {
integrationsFilters: Record<string, any>;
@ -66,15 +65,11 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
constructor(props: IntegrationsProps) {
super(props);
const { query, store } = props;
this.state = {
integrationsFilters: { searchTerm: '' },
errorData: initErrorDataState(),
confirmationModal: undefined,
};
store.currentPage['integrations'] = Number(store.currentPage['integrations'] || query.p || 1);
}
async componentDidMount() {
@ -121,7 +116,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
update = () => {
const { store } = this.props;
const { integrationsFilters } = this.state;
const page = store.currentPage['integrations'];
const page = store.filtersStore.currentTablePageNum[PAGE.Integrations];
LocationHelper.update({ p: page }, 'partial');
@ -135,7 +130,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
const { alertReceiveChannelId, confirmationModal } = this.state;
const { alertReceiveChannelStore } = store;
const { count, results } = alertReceiveChannelStore.getPaginatedSearchResult();
const { count, results, page_size } = alertReceiveChannelStore.getPaginatedSearchResult();
return (
<>
@ -178,8 +173,8 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
className={cx('integrations-table')}
rowClassName={cx('integrations-table-row')}
pagination={{
page: store.currentPage['integrations'],
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
page: store.filtersStore.currentTablePageNum[PAGE.Integrations],
total: results ? Math.ceil((count || 0) / page_size) : 0,
onChange: this.handleChangePage,
}}
/>
@ -520,13 +515,13 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
invalidateRequestFn = (requestedPage: number) => {
const { store } = this.props;
return requestedPage !== store.getCurrentPage(PAGE.Integrations);
return requestedPage !== store.filtersStore.currentTablePageNum[PAGE.Integrations];
};
handleChangePage = (page: number) => {
const { store } = this.props;
store.currentPage['integrations'] = page;
store.filtersStore.currentTablePageNum[PAGE.Integrations] = page;
this.update();
};
@ -578,12 +573,12 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
const { alertReceiveChannelStore } = store;
const { integrationsFilters } = this.state;
const newPage = isOnMount ? store.getCurrentPage(PAGE.Integrations) : 1;
const newPage = isOnMount ? store.filtersStore.currentTablePageNum[PAGE.Integrations] : 1;
return alertReceiveChannelStore
.updatePaginatedItems(integrationsFilters, newPage, false, () => this.invalidateRequestFn(newPage))
.then(() => {
store.setCurrentPage(PAGE.Integrations, newPage);
store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage;
LocationHelper.update({ p: newPage }, 'partial');
});
};

View file

@ -234,7 +234,7 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
);
}
handleFiltersChange = (filters: FiltersValues, isOnMount) => {
handleFiltersChange = (filters: FiltersValues, isOnMount: boolean) => {
const { store } = this.props;
const { outgoingWebhookStore } = store;

View file

@ -37,7 +37,6 @@ import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
import styles from './Schedules.module.css';
const cx = cn.bind(styles);
const PAGE_SIZE_DEFAULT = 15;
interface SchedulesPageProps extends WithStoreProps, RouteComponentProps, PageProps {}
@ -47,7 +46,6 @@ interface SchedulesPageState {
showNewScheduleSelector: boolean;
expandedRowKeys: Array<Schedule['id']>;
scheduleIdToEdit?: Schedule['id'];
page: number;
}
@observer
@ -63,7 +61,6 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
showNewScheduleSelector: false,
expandedRowKeys: [],
scheduleIdToEdit: undefined,
page: !isNaN(Number(props.query.p)) ? Number(props.query.p) : 1,
};
}
@ -77,11 +74,12 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
render() {
const { store, query } = this.props;
const { showNewScheduleSelector, expandedRowKeys, scheduleIdToEdit, page, startMoment } = this.state;
const { showNewScheduleSelector, expandedRowKeys, scheduleIdToEdit, startMoment } = this.state;
const { results, count, page_size } = store.scheduleStore.getSearchResult();
const page = store.filtersStore.currentTablePageNum[PAGE.Schedules];
const users = store.userStore.getSearchResult().results;
return (
@ -118,9 +116,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
query={query}
page={PAGE.Schedules}
grafanaTeamStore={store.grafanaTeamStore}
onChange={(filters, isOnMount: boolean, invalidateFn: () => boolean) => {
this.handleSchedulesFiltersChange(filters, isOnMount, invalidateFn);
}}
onChange={this.handleSchedulesFiltersChange}
/>
</div>
@ -130,7 +126,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
loading={!results}
pagination={{
page,
total: Math.ceil((count || 0) / (page_size || PAGE_SIZE_DEFAULT)),
total: results ? Math.ceil((count || 0) / page_size) : 0,
onChange: this.handlePageChange,
}}
rowKey="id"
@ -384,27 +380,32 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
};
};
handleSchedulesFiltersChange = (filters: RemoteFiltersType, isOnMount: boolean, invalidateFn: () => boolean) => {
this.setState({ filters, page: isOnMount ? this.state.page : 1 }, () => {
handleSchedulesFiltersChange = (filters: RemoteFiltersType, _isOnMount: boolean, invalidateFn: () => boolean) => {
this.setState({ filters }, () => {
this.applyFilters(invalidateFn);
});
};
applyFilters = (invalidateFn?: () => boolean) => {
const { scheduleStore } = this.props.store;
const { page, filters } = this.state;
const { scheduleStore, filtersStore } = this.props.store;
const { filters } = this.state;
const currentTablePage = filtersStore.currentTablePageNum[PAGE.Schedules];
LocationHelper.update({ p: page }, 'partial');
scheduleStore.updateItems(filters, page, invalidateFn);
LocationHelper.update({ p: currentTablePage }, 'partial');
scheduleStore.updateItems(filters, currentTablePage, invalidateFn);
};
handlePageChange = (page: number) => {
this.setState({ page, expandedRowKeys: [] }, this.applyFilters);
const { store } = this.props;
store.filtersStore.currentTablePageNum[PAGE.Schedules] = page;
this.setState({ expandedRowKeys: [] }, this.applyFilters);
};
update = () => {
const { store } = this.props;
const { page, startMoment } = this.state;
const { startMoment } = this.state;
const page = store.filtersStore.currentTablePageNum[PAGE.Schedules];
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment, 9, true);

View file

@ -25,7 +25,7 @@ import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import LocationHelper from 'utils/LocationHelper';
import { generateMissingPermissionMessage, isUserActionAllowed, UserActions } from 'utils/authorization';
import { PLUGIN_ROOT } from 'utils/consts';
import { PAGE, PLUGIN_ROOT } from 'utils/consts';
import { getUserRowClassNameFn } from './Users.helpers';
@ -35,11 +35,9 @@ const cx = cn.bind(styles);
interface UsersProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
const ITEMS_PER_PAGE = 100;
const REQUIRED_PERMISSION_TO_VIEW_USERS = UserActions.UserSettingsWrite;
interface UsersState extends PageBaseState {
page: number;
isWrongTeam: boolean;
userPkToEdit?: UserType['pk'] | 'new';
usersFilters?: {
@ -54,10 +52,10 @@ class Users extends React.Component<UsersProps, UsersState> {
const {
query: { p },
store: { filtersStore },
} = props;
this.state = {
page: p ? Number(p) : 1,
isWrongTeam: false,
userPkToEdit: undefined,
usersFilters: {
@ -66,6 +64,10 @@ class Users extends React.Component<UsersProps, UsersState> {
errorData: initErrorDataState(),
};
// Users component doesn't rely on RemoteFilters
// therefore we need to initialize the page in the constructor instead
filtersStore.currentTablePageNum[PAGE.Users] = p ? Number(p) : 1;
}
async componentDidMount() {
@ -74,8 +76,9 @@ class Users extends React.Component<UsersProps, UsersState> {
updateUsers = async (invalidateFn?: () => boolean) => {
const { store } = this.props;
const { usersFilters, page } = this.state;
const { userStore } = store;
const { usersFilters } = this.state;
const { userStore, filtersStore } = store;
const page = filtersStore.currentTablePageNum[PAGE.Users];
if (!isUserActionAllowed(REQUIRED_PERMISSION_TO_VIEW_USERS)) {
return;
@ -84,7 +87,6 @@ class Users extends React.Component<UsersProps, UsersState> {
LocationHelper.update({ p: page }, 'partial');
await userStore.updateItems(usersFilters, page, invalidateFn);
// otherwise MobX doesn't update :(
this.forceUpdate();
};
@ -171,12 +173,14 @@ class Users extends React.Component<UsersProps, UsersState> {
renderContentIfAuthorized(authorizedToViewUsers: boolean) {
const {
store: { userStore },
store: { userStore, filtersStore },
} = this.props;
const { usersFilters, page, userPkToEdit } = this.state;
const { usersFilters, userPkToEdit } = this.state;
const { count, results } = userStore.getSearchResult();
const page = filtersStore.currentTablePageNum[PAGE.Users];
const { count, results, page_size } = userStore.getSearchResult();
const columns = this.getTableColumns();
const handleClear = () =>
@ -209,7 +213,7 @@ class Users extends React.Component<UsersProps, UsersState> {
rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
pagination={{
page,
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
total: results ? Math.ceil((count || 0) / page_size) : 0,
onChange: this.handleChangePage,
}}
/>
@ -417,11 +421,19 @@ class Users extends React.Component<UsersProps, UsersState> {
}
handleChangePage = (page: number) => {
this.setState({ page }, this.updateUsers);
const { filtersStore } = this.props.store;
filtersStore.currentTablePageNum[PAGE.Users] = page;
this.updateUsers();
};
handleUsersFiltersChange = (usersFilters: any, invalidateFn: () => boolean) => {
this.setState({ usersFilters, page: 1 }, () => {
const { filtersStore } = this.props.store;
filtersStore.currentTablePageNum[PAGE.Users] = 1;
this.setState({ usersFilters }, () => {
this.updateUsers(invalidateFn);
});
};

View file

@ -37,7 +37,6 @@ import {
CLOUD_VERSION_REGEX,
GRAFANA_LICENSE_CLOUD,
GRAFANA_LICENSE_OSS,
PAGE,
PLUGIN_ROOT,
} from 'utils/consts';
import FaroHelper from 'utils/faro';
@ -80,9 +79,6 @@ export class RootBaseStore {
@observable
incidentsPage: any = this.initialQuery.p ? Number(this.initialQuery.p) : 1;
@observable
currentPage: { [key: string]: number } = {};
@observable
onCallApiUrl: string;
@ -312,13 +308,4 @@ export class RootBaseStore {
const settings = await PluginState.getGrafanaPluginSettings();
return settings.jsonData?.onCallApiUrl;
}
getCurrentPage = (page: PAGE): number => {
return this.currentPage[page];
};
@action
setCurrentPage = (page: PAGE, value: number) => {
this.currentPage[page] = value;
};
}

View file

@ -50,6 +50,7 @@ export enum PAGE {
Incidents = 'incidents',
Webhooks = 'webhooks',
Schedules = 'schedules',
Users = 'users',
}
export const TEXT_ELLIPSIS_CLASS = 'overflow-child';