Skip to content

Commit a552189

Browse files
Merge pull request #834 from anwesha-palit-redhat/feat/SRVKP-9607
SRVKP-9607: added redux state for pipeline overview filter to persist
1 parent df5a499 commit a552189

File tree

10 files changed

+251
-47
lines changed

10 files changed

+251
-47
lines changed

console-extensions.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
[
2+
{
3+
"type": "console.redux-reducer",
4+
"properties": {
5+
"scope": "pipelinesOverviewFilters",
6+
"reducer": {
7+
"$codeRef": "pipelinesOverviewFiltersReducer.pipelinesOverviewFiltersReducer"
8+
}
9+
}
10+
},
211
{
312
"type": "console.flag/model",
413
"properties": {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@
160160
"taskDetails": "./components/tasks",
161161
"pacComponent": "./components/pac/",
162162
"yamlTemplates": "./components/templates",
163-
"topology": "./components/topology"
163+
"topology": "./components/topology",
164+
"pipelinesOverviewFiltersReducer": "./redux/reducers/pipelines-overview-filters"
164165
},
165166
"dependencies": {
166167
"@console/pluginAPI": ">=4.15"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as React from 'react';
2+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
3+
// @ts-ignore: FIXME missing exports due to out-of-sync @types/react-redux version
4+
import { useDispatch, useSelector } from 'react-redux';
5+
import { useLocation } from 'react-router-dom-v5-compat';
6+
import {
7+
setInterval as setIntervalAction,
8+
setTimespan as setTimespanAction,
9+
} from '../../redux/actions/pipelines-overview-filters';
10+
import {
11+
getPipelinesOverviewInterval,
12+
getPipelinesOverviewTimespan,
13+
} from '../../redux/selectors/pipelines-overview-filters';
14+
import { useQueryParams } from '../pipelines-overview/utils';
15+
16+
export const usePersistedTimespanWithUrl = (
17+
defaultValue: number,
18+
options: {
19+
displayFormat: (v: number) => string;
20+
loadFormat: (v: string) => number;
21+
options: Record<string, any>;
22+
},
23+
namespace: string,
24+
) => {
25+
const dispatch = useDispatch();
26+
const reduxTimespan = useSelector(getPipelinesOverviewTimespan);
27+
const location = useLocation();
28+
const prevNamespaceRef = React.useRef(namespace);
29+
const [timespan, setTimespanValue] = React.useState(() => {
30+
const urlParams = new URLSearchParams(location.search);
31+
const urlValue = urlParams.has('timerange')
32+
? urlParams.get('timerange')
33+
: null;
34+
if (urlValue) {
35+
return options.loadFormat(urlValue);
36+
}
37+
return reduxTimespan ?? defaultValue;
38+
});
39+
40+
// Reset to default when namespace changes
41+
React.useEffect(() => {
42+
if (prevNamespaceRef.current !== namespace) {
43+
prevNamespaceRef.current = namespace;
44+
setTimespanValue(defaultValue);
45+
}
46+
}, [namespace, defaultValue]);
47+
48+
// Persist to Redux whenever value changes
49+
React.useEffect(() => {
50+
dispatch(setTimespanAction(timespan));
51+
}, [timespan, dispatch]);
52+
53+
useQueryParams({
54+
key: 'timerange',
55+
value: timespan,
56+
setValue: setTimespanValue,
57+
defaultValue: timespan,
58+
...options,
59+
});
60+
61+
return [timespan, setTimespanValue];
62+
};
63+
64+
export const usePersistedIntervalWithUrl = (
65+
defaultValue: number,
66+
options: {
67+
displayFormat: (v: number | null) => string;
68+
loadFormat: (v: string) => number | null;
69+
options: Record<string, any>;
70+
},
71+
namespace: string,
72+
) => {
73+
const dispatch = useDispatch();
74+
const reduxInterval = useSelector(getPipelinesOverviewInterval);
75+
const location = useLocation();
76+
const prevNamespaceRef = React.useRef(namespace);
77+
const [interval, setIntervalValue] = React.useState(() => {
78+
const urlParams = new URLSearchParams(location.search);
79+
const urlValue = urlParams.has('refreshinterval')
80+
? urlParams.get('refreshinterval')
81+
: null;
82+
if (urlValue) {
83+
return options.loadFormat(urlValue);
84+
}
85+
// to handle refresh interval "off" state
86+
return reduxInterval !== undefined ? reduxInterval : defaultValue;
87+
});
88+
89+
// Reset to default when namespace changes
90+
React.useEffect(() => {
91+
if (prevNamespaceRef.current !== namespace) {
92+
prevNamespaceRef.current = namespace;
93+
setIntervalValue(defaultValue);
94+
}
95+
}, [namespace, defaultValue]);
96+
97+
// Persist to Redux whenever value changes
98+
React.useEffect(() => {
99+
dispatch(setIntervalAction(interval));
100+
}, [interval, dispatch]);
101+
102+
useQueryParams({
103+
key: 'refreshinterval',
104+
value: interval,
105+
setValue: setIntervalValue,
106+
defaultValue: interval,
107+
...options,
108+
});
109+
110+
return [interval, setIntervalValue];
111+
};

src/components/pipelines-overview/PipelinesOverviewPage.tsx

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,39 @@ import NameSpaceDropdown from './NamespaceDropdown';
1414
import PipelineRunsListPage from './list-pages/PipelineRunsListPage';
1515
import TimeRangeDropdown from './TimeRangeDropdown';
1616
import RefreshDropdown from './RefreshDropdown';
17-
import { IntervalOptions, TimeRangeOptions, useQueryParams } from './utils';
17+
import { IntervalOptions, TimeRangeOptions } from './utils';
1818
import { ALL_NAMESPACES_KEY } from '../../consts';
1919
import AllProjectsPage from '../projects-list/AllProjectsPage';
2020
import { FLAGS } from '../../types';
21+
import {
22+
usePersistedTimespanWithUrl,
23+
usePersistedIntervalWithUrl,
24+
} from '../hooks/usePersistedFiltersForPipelineOverview';
2125

2226
const PipelinesOverviewPage: React.FC = () => {
2327
const { t } = useTranslation('plugin__pipelines-console-plugin');
2428
const canListNS = useFlag(FLAGS.CAN_LIST_NS);
2529
const [activeNamespace, setActiveNamespace] = useActiveNamespace();
26-
const [timespan, setTimespan] = React.useState(parsePrometheusDuration('1d'));
27-
const [interval, setInterval] = React.useState(
28-
parsePrometheusDuration('30s'),
29-
);
3030

31-
useQueryParams({
32-
key: 'refreshinterval',
33-
value: interval,
34-
setValue: setInterval,
35-
defaultValue: parsePrometheusDuration('30s'),
36-
options: { ...IntervalOptions(), off: 'OFF_KEY' },
37-
displayFormat: (v) => (v ? formatPrometheusDuration(v) : 'off'),
38-
loadFormat: (v) => (v == 'off' ? null : parsePrometheusDuration(v)),
39-
});
31+
const [timespan, setTimespan] = usePersistedTimespanWithUrl(
32+
parsePrometheusDuration('1d'),
33+
{
34+
options: TimeRangeOptions(),
35+
displayFormat: formatPrometheusDuration,
36+
loadFormat: parsePrometheusDuration,
37+
},
38+
activeNamespace,
39+
);
4040

41-
useQueryParams({
42-
key: 'timerange',
43-
value: timespan,
44-
setValue: setTimespan,
45-
defaultValue: parsePrometheusDuration('1w'),
46-
options: TimeRangeOptions(),
47-
displayFormat: formatPrometheusDuration,
48-
loadFormat: parsePrometheusDuration,
49-
});
41+
const [interval, setInterval] = usePersistedIntervalWithUrl(
42+
parsePrometheusDuration('30s'),
43+
{
44+
options: { ...IntervalOptions(), off: 'OFF_KEY' },
45+
displayFormat: (v) => (v ? formatPrometheusDuration(v) : 'off'),
46+
loadFormat: (v) => (v == 'off' ? null : parsePrometheusDuration(v)),
47+
},
48+
activeNamespace,
49+
);
5050

5151
if (!canListNS && activeNamespace === ALL_NAMESPACES_KEY) {
5252
return <AllProjectsPage pageTitle={t('Overview')} />;

src/components/pipelines-overview/PipelinesOverviewPageK8s.tsx

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { formatPrometheusDuration, parsePrometheusDuration } from './dateTime';
99
import NameSpaceDropdown from './NamespaceDropdown';
1010
import TimeRangeDropdown from './TimeRangeDropdown';
1111
import RefreshDropdown from './RefreshDropdown';
12-
import { IntervalOptions, TimeRangeOptionsK8s, useQueryParams } from './utils';
12+
import { IntervalOptions, TimeRangeOptionsK8s } from './utils';
1313
import PipelineRunsStatusCardK8s from './PipelineRunsStatusCardK8s';
1414
import PipelineRunsNumbersChartK8s from './PipelineRunsNumbersChartK8s';
1515
import PipelineRunsTotalCardK8s from './PipelineRunsTotalCardK8s';
@@ -19,36 +19,36 @@ import { K8sDataLimitationAlert } from './K8sDataLimitationAlert';
1919
import { FLAGS } from '../../types';
2020
import { ALL_NAMESPACES_KEY } from '../../consts';
2121
import AllProjectsPage from '../projects-list/AllProjectsPage';
22+
import {
23+
usePersistedTimespanWithUrl,
24+
usePersistedIntervalWithUrl,
25+
} from '../hooks/usePersistedFiltersForPipelineOverview';
2226
import './PipelinesOverview.scss';
2327

2428
const PipelinesOverviewPageK8s: React.FC = () => {
2529
const { t } = useTranslation('plugin__pipelines-console-plugin');
2630
const canListNS = useFlag(FLAGS.CAN_LIST_NS);
2731
const [activeNamespace, setActiveNamespace] = useActiveNamespace();
28-
const [timespan, setTimespan] = React.useState(parsePrometheusDuration('1d'));
29-
const [interval, setInterval] = React.useState(
30-
parsePrometheusDuration('30s'),
31-
);
3232

33-
useQueryParams({
34-
key: 'refreshinterval',
35-
value: interval,
36-
setValue: setInterval,
37-
defaultValue: parsePrometheusDuration('30s'),
38-
options: { ...IntervalOptions(), off: 'OFF_KEY' },
39-
displayFormat: (v) => (v ? formatPrometheusDuration(v) : 'off'),
40-
loadFormat: (v) => (v == 'off' ? null : parsePrometheusDuration(v)),
41-
});
33+
const [timespan, setTimespan] = usePersistedTimespanWithUrl(
34+
parsePrometheusDuration('1d'),
35+
{
36+
options: TimeRangeOptionsK8s(),
37+
displayFormat: formatPrometheusDuration,
38+
loadFormat: parsePrometheusDuration,
39+
},
40+
activeNamespace,
41+
);
4242

43-
useQueryParams({
44-
key: 'timerange',
45-
value: timespan,
46-
setValue: setTimespan,
47-
defaultValue: parsePrometheusDuration('1w'),
48-
options: TimeRangeOptionsK8s(),
49-
displayFormat: formatPrometheusDuration,
50-
loadFormat: parsePrometheusDuration,
51-
});
43+
const [interval, setInterval] = usePersistedIntervalWithUrl(
44+
parsePrometheusDuration('30s'),
45+
{
46+
options: { ...IntervalOptions(), off: 'OFF_KEY' },
47+
displayFormat: (v) => (v ? formatPrometheusDuration(v) : 'off'),
48+
loadFormat: (v) => (v == 'off' ? null : parsePrometheusDuration(v)),
49+
},
50+
activeNamespace,
51+
);
5252

5353
if (!canListNS && activeNamespace === ALL_NAMESPACES_KEY) {
5454
return <AllProjectsPage pageTitle={t('Overview')} />;

src/components/pipelines-overview/__tests__/PipelinesOverview.spec.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
useActiveNamespace,
88
useFlag,
99
} from '@openshift-console/dynamic-plugin-sdk';
10+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
11+
// @ts-ignore: FIXME missing exports due to out-of-sync @types/react-redux version
12+
import { useDispatch, useSelector } from 'react-redux';
13+
import { useLocation } from 'react-router-dom-v5-compat';
1014
import PipelinesOverviewPage from '../PipelinesOverviewPage';
1115
import { getResultsSummary } from '../../utils/summary-api';
1216
import * as utils from '../utils';
@@ -27,6 +31,13 @@ jest.mock('../../utils/tekton-results', () => ({
2731
jest.mock('../../utils/summary-api', () => ({
2832
getResultsSummary: jest.fn(),
2933
}));
34+
jest.mock('react-redux', () => ({
35+
useDispatch: jest.fn(),
36+
useSelector: jest.fn(),
37+
}));
38+
jest.mock('react-router-dom-v5-compat', () => ({
39+
useLocation: jest.fn(),
40+
}));
3041
(VirtualizedTable as jest.Mock).mockImplementation((props) => {
3142
virtualizedTableRenderProps(props);
3243
return null;
@@ -40,6 +51,9 @@ const useK8sWatchResourceMock = useK8sWatchResource as jest.Mock;
4051
const useActiveColumnsMock = useActiveColumns as jest.Mock;
4152
const getResultsSummaryMock = getResultsSummary as jest.Mock;
4253
const useFlagMock = useFlag as jest.Mock;
54+
const useDispatchMock = useDispatch as jest.Mock;
55+
const useSelectorMock = useSelector as jest.Mock;
56+
const useLocationMock = useLocation as jest.Mock;
4357

4458
describe('Pipeline Overview page', () => {
4559
beforeEach(() => {
@@ -52,6 +66,9 @@ describe('Pipeline Overview page', () => {
5266
useActiveColumnsMock.mockReturnValue([[]]);
5367
getResultsSummaryMock.mockReturnValue(Promise.resolve({}));
5468
useFlagMock.mockReturnValue(true);
69+
useDispatchMock.mockReturnValue(jest.fn());
70+
useSelectorMock.mockReturnValue(null);
71+
useLocationMock.mockReturnValue({ search: '', pathname: '/' });
5572
});
5673

5774
it('should render Pipeline Overview', async () => {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ActionType } from '../reducers/pipelines-overview-filters';
2+
3+
export const setTimespan = (timespan: number) => ({
4+
type: ActionType.SET_TIMESPAN,
5+
payload: { timespan },
6+
});
7+
8+
export const setInterval = (interval: number) => ({
9+
type: ActionType.SET_INTERVAL,
10+
payload: { interval },
11+
});
12+

src/redux/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './actions/pipelines-overview-filters';
2+
export * from './reducers/pipelines-overview-filters';
3+
export * from './selectors/pipelines-overview-filters';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export type PipelinesOverviewFiltersState = {
2+
timespan: number | null;
3+
interval: number | null;
4+
};
5+
6+
export enum ActionType {
7+
SET_TIMESPAN = 'SET_PIPELINES_OVERVIEW_TIMESPAN',
8+
SET_INTERVAL = 'SET_PIPELINES_OVERVIEW_INTERVAL',
9+
}
10+
11+
type Action = {
12+
type: ActionType;
13+
payload?: {
14+
timespan?: number;
15+
interval?: number;
16+
};
17+
};
18+
19+
const initialState: PipelinesOverviewFiltersState = {
20+
timespan: null,
21+
interval: null,
22+
};
23+
24+
export const pipelinesOverviewFiltersReducer = (
25+
state: PipelinesOverviewFiltersState = initialState,
26+
action: Action,
27+
): PipelinesOverviewFiltersState => {
28+
switch (action.type) {
29+
case ActionType.SET_TIMESPAN:
30+
return { ...state, timespan: action.payload?.timespan ?? null };
31+
case ActionType.SET_INTERVAL:
32+
return { ...state, interval: action.payload?.interval ?? null };
33+
default:
34+
return state;
35+
}
36+
};
37+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { PipelinesOverviewFiltersState } from '../reducers/pipelines-overview-filters';
2+
3+
export const getPipelinesOverviewTimespan = (state: {
4+
plugins?: { pipelinesOverviewFilters?: PipelinesOverviewFiltersState };
5+
}): number | null => {
6+
return state.plugins?.pipelinesOverviewFilters?.timespan ?? null;
7+
};
8+
9+
export const getPipelinesOverviewInterval = (state: {
10+
plugins?: { pipelinesOverviewFilters?: PipelinesOverviewFiltersState };
11+
}): number | null => {
12+
return state.plugins?.pipelinesOverviewFilters?.interval ?? null;
13+
};
14+

0 commit comments

Comments
 (0)