Skip to content

Commit a4c7225

Browse files
#12271: Rules manager - clear cache on standalone geofence and multiple gs instances does not work fully (#12279) (#12285)
* #12271: Rules manager - clear cache on standalone geofence and multiple gs instances does not work fully Description: - handle clear cache functionality for multi gs instances for stand-alone geofence - create menu gs instances called CSCleanCacheMenu - add translations - add unit tests * - fix renaming issue of comp name * - update some unit tests for RuleService - remove api function 'cleanCacheGSInstance' in favor ofof expanding 'cleanCache' function - resolvve review comments (cherry picked from commit 3ddf970) Co-authored-by: mahmoud adel <58145645+mahmoudadel54@users.noreply.github.com>
1 parent 4475a57 commit a4c7225

17 files changed

Lines changed: 427 additions & 31 deletions

File tree

web/client/actions/__tests__/rulesmanager-test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import {
3333
GS_INSTANCES_SELECTED,
3434
SAVE_GS_INSTANCE,
3535
STORING_GS_INSTANCES_DD,
36+
CACHE_CLEAN,
37+
CACHE_CLEAN_MULTI,
38+
onCacheClean,
39+
onCacheCleanMulti,
3640
cleanEditingGSInstance,
3741
delGSInstance,
3842
gsInstancesSelected,
@@ -166,4 +170,27 @@ describe('test rules manager actions', () => {
166170
expect(action.type).toEqual(STORING_GS_INSTANCES_DD);
167171
expect(action.instances).toEqual(instances);
168172
});
173+
it('onCacheClean with no url [integrated geoserver geofence]', () => {
174+
const action = onCacheClean();
175+
expect(action).toExist();
176+
expect(action.type).toBe(CACHE_CLEAN);
177+
});
178+
it('onCacheClean with gs url [stand-alone geofence]', () => {
179+
const url = 'http://geoserver.org';
180+
const action = onCacheClean(url);
181+
expect(action).toExist();
182+
expect(action.type).toBe(CACHE_CLEAN);
183+
expect(action.gsInstanceUrl).toBe(url);
184+
});
185+
186+
it('onCacheCleanMulti', () => {
187+
const gsInstances = [
188+
{ url: 'url1', name: 'GS_Production' },
189+
{ url: 'url2', name: 'GS_Test' }
190+
];
191+
const action = onCacheCleanMulti(gsInstances);
192+
expect(action).toExist();
193+
expect(action.type).toBe(CACHE_CLEAN_MULTI);
194+
expect(action.gsInstances).toBe(gsInstances);
195+
});
169196
});

web/client/actions/rulesmanager.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,18 @@ export const SAVE_GS_INSTANCE = "RULES_MANAGER:SAVE_GS_INSTANCE";
2727
export const GS_INSTSANCE_SAVED = "RULES_MANAGER:GS_INSTSANCE_SAVED";
2828
export const CLEAN_EDITING_GS_INSTANCE = "RULES_MANAGER:CLEAN_EDITING_GS_INSTANCE";
2929
export const STORING_GS_INSTANCES_DD = "RULES_MANAGER:STORING_GS_INSTANCES_DD";
30+
export const CACHE_CLEAN_MULTI = 'RULES_MANAGER:CACHE_CLEAN_MULTI';
3031

31-
export function onCacheClean() {
32+
export function onCacheClean(url) {
3233
return {
33-
type: CACHE_CLEAN
34+
type: CACHE_CLEAN,
35+
gsInstanceUrl: url
36+
};
37+
}
38+
export function onCacheCleanMulti(gsInstances) {
39+
return {
40+
type: CACHE_CLEAN_MULTI,
41+
gsInstances
3442
};
3543
}
3644
export function delRules(ids) {

web/client/api/geofence/RuleService.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ const processFilterValues = (rulesFiltersValues) => {
108108
* @param {function} config.getGeoServerInstance function that returns the instance object `{id: 1, url: "some-url"}`
109109
*/
110110
const Api = ({addBaseUrl, addBaseUrlGS, getGeoServerInstance}) => ({
111-
cleanCache: () => {
112-
return axios.get('rest/geofence/ruleCache/invalidate', addBaseUrlGS())
111+
cleanCache: (gsInstanceURL) => {
112+
return axios.get('rest/geofence/ruleCache/invalidate', addBaseUrlGS({}, gsInstanceURL))
113113
.then((response) => {
114114
return response.data;
115115
}

web/client/api/geofence/__tests__/RuleService-test.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import RULES from 'raw-loader!../../../test-resources/geofence/rest/rules/rules_
1515
import axios from '../../../libs/ajax';
1616
import GF_RULE from '../../../test-resources/geofence/rest/rules/full_rule1.json';
1717
import ruleServiceFactory, { cleanConstraints } from '../RuleService';
18+
import ConfigUtils from '../../../utils/ConfigUtils';
1819

1920
const RuleService = ruleServiceFactory({
2021
addBaseUrl: (opts) => ({...opts, baseURL: BASE_URL}),
21-
getGeoServerInstance: () => ({url: BASE_URL})
22+
getGeoServerInstance: () => ({url: BASE_URL}),
23+
addBaseUrlGS: (options = {}, gsInstanceURL) => (gsInstanceURL ? {...options, baseURL: gsInstanceURL} : {...options, baseURL: ''})
2224
});
2325

2426
// const RULES_JSON = require('../../../test-resources/geofence/rest/rules/rules_1.json');
@@ -161,4 +163,36 @@ describe('RuleService API for GeoFence StandAlone', () => {
161163
}
162164
});
163165
});
166+
it('cleanCache', (done) => {
167+
const mockData = { status: "success" };
168+
mockAxios.onGet().reply(() => {
169+
return [200, mockData];
170+
});
171+
172+
RuleService.cleanCache().then((data) => {
173+
expect(data).toEqual(mockData);
174+
expect(mockAxios.history.get.length).toBe(1);
175+
expect(mockAxios.history.get[0].url).toBe('rest/geofence/ruleCache/invalidate');
176+
expect(mockAxios.history.get[0].method).toBe('get');
177+
expect(mockAxios.history.get[0].baseURL).toContain('');
178+
done();
179+
}).catch(done);
180+
});
181+
it('cleanCache with gs instance [stand-alone geofence]', (done) => {
182+
ConfigUtils.setConfigProp("geoFenceServiceType", "geofence");
183+
const mockData = { status: "success" };
184+
const gsUrl = "http://localhost:8080/geoserver";
185+
mockAxios.onGet().reply(() => {
186+
return [200, mockData];
187+
});
188+
189+
RuleService.cleanCache(gsUrl).then((data) => {
190+
expect(data).toEqual(mockData);
191+
expect(mockAxios.history.get.length).toBe(1);
192+
expect(mockAxios.history.get[0].url).toContain('rest/geofence/ruleCache/invalidate');
193+
expect(mockAxios.history.get[0].baseURL).toContain(gsUrl); // gs instance url check is crutial
194+
done();
195+
}).catch(done);
196+
ConfigUtils.removeConfigProp("geoFenceServiceType");
197+
});
164198
});

web/client/api/geoserver/GeoFence.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ const GS_INSTANCES_SERVICES = {
7777
*/
7878
var Api = {
7979
// RULES
80-
cleanCache: () => {
80+
cleanCache: (gsUrl) => {
81+
const isStandAloneGeofence = Api.getRuleServiceType() === 'geofence';
82+
if (isStandAloneGeofence) {
83+
return Api.getRuleService().cleanCache(gsUrl);
84+
}
8185
return Api.getRuleService().cleanCache();
8286
},
8387
loadRules: (page, rulesFiltersValues, entries = 10) => {

web/client/components/manager/rulesmanager/ModalDialog.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Message from '../../I18N/Message';
1111
import Portal from '../../misc/Portal';
1212
import ResizableModal from '../../misc/ResizableModal';
1313

14-
export default ({title = "", showDialog = false, buttons = [], closeAction = () => {}, msg = "Missing message"}) => {
14+
export default ({title = "", showDialog = false, buttons = [], closeAction = () => {}, msg = "Missing message", msgParams = {}}) => {
1515
return (
1616
<Portal>
1717
<ResizableModal
@@ -22,7 +22,7 @@ export default ({title = "", showDialog = false, buttons = [], closeAction = ()
2222
buttons={buttons}>
2323
<div className="ms-alert">
2424
<div className="ms-alert-center rm-alert-padded">
25-
<Message msgId={msg}/>
25+
<Message msgId={msg} msgParams={msgParams}/>
2626
</div>
2727
</div>
2828
</ResizableModal>

web/client/epics/rulesmanager.js

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Rx from 'rxjs';
2-
import { SAVE_RULE, setLoading, RULE_SAVED, DELETE_RULES, CACHE_CLEAN, DELETE_GS_INSTSANCES, GS_INSTSANCE_SAVED, SAVE_GS_INSTANCE } from '../actions/rulesmanager';
2+
import { SAVE_RULE, setLoading, RULE_SAVED, DELETE_RULES, CACHE_CLEAN, DELETE_GS_INSTSANCES, GS_INSTSANCE_SAVED, SAVE_GS_INSTANCE, CACHE_CLEAN_MULTI } from '../actions/rulesmanager';
33
import { error, success } from '../actions/notifications';
44
import { drawSupportReset } from '../actions/draw';
55
import { updateRule, createRule, deleteRule, cleanCache, deleteGSInstance, createGSInstance, updateGSInstance } from '../observables/rulesmanager';
@@ -43,14 +43,59 @@ export default {
4343
return Rx.Observable.combineLatest(ids.map(id => deleteRule(id))).let(saveRule);
4444
}),
4545
onCacheClean: action$ => action$.ofType(CACHE_CLEAN)
46-
.exhaustMap( () =>
47-
cleanCache()
46+
.exhaustMap((action) =>{
47+
return cleanCache(action?.gsInstanceUrl)
4848
.mapTo(success({title: "rulesmanager.errorTitle", message: "rulesmanager.cacheCleaned"}))
4949
.startWith(setLoading(true))
5050
.catch(() => {
5151
return Rx.Observable.of(error({title: "rulesmanager.errorTitle", message: "rulesmanager.errorCleaningCache"}));
5252
})
53-
.concat(Rx.Observable.of(setLoading(false)))),
53+
.concat(Rx.Observable.of(setLoading(false)));
54+
}),
55+
onCacheCleanMulti: action$ => action$.ofType(CACHE_CLEAN_MULTI)
56+
.exhaustMap((action) => {
57+
// Create an array of observables, one for each instance
58+
const cleanRequests = action.gsInstances.map(instance =>
59+
cleanCache(instance.url)
60+
.map(() => ({ success: true, name: instance.name }))
61+
.catch(() => {
62+
return Rx.Observable.of({ success: false, name: instance.name });
63+
})
64+
);
65+
66+
// forkJoin waits for all requests to finish
67+
return Rx.Observable.forkJoin(cleanRequests)
68+
.switchMap(results => {
69+
const successfulNames = results.filter(r => r.success).map(r => r.name);
70+
const failedNames = results.filter(r => !r.success).map(r => r.name);
71+
72+
const actions = [];
73+
74+
// 1. If there are successes, add a success notification
75+
if (successfulNames.length > 0) {
76+
actions.push(success({
77+
title: "rulesmanager.errorTitle",
78+
message: "rulesmanager.cacheCleanedFor",
79+
values: { instancesNames: successfulNames.join(', ') },
80+
uid: "cacheCleanedFor"
81+
}));
82+
}
83+
84+
// 2. If there are failures, add an error notification
85+
if (failedNames.length > 0) {
86+
actions.push(error({
87+
title: "rulesmanager.errorTitle",
88+
message: "rulesmanager.errorCleaningCacheFor",
89+
values: { instancesNames: failedNames.join(', ') },
90+
uid: "errorCleaningCacheFor"
91+
}));
92+
}
93+
94+
return Rx.Observable.from(actions);
95+
})
96+
.startWith(setLoading(true))
97+
.concat(Rx.Observable.of(setLoading(false)));
98+
}),
5499
// for gs instances
55100
onDeleteGSInstance: (action$, { getState }) => action$.ofType(DELETE_GS_INSTSANCES)
56101
.switchMap(({ ids = get(getState(), "rulesmanager.selectedGSInstances", []).map(row => ({id: row.id, title: row.name})) }) => {

web/client/observables/rulesmanager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export const getStylesAndAttributes = (layer, workspace, url) => {
145145
}), ({style, ly}, {properties, type}) => ({styles: style || [], properties, type, layer: ly}));
146146

147147
};
148-
export const cleanCache = () => Rx.Observable.defer(() => GeoFence.cleanCache());
148+
export const cleanCache = (url) => Rx.Observable.defer(() => GeoFence.cleanCache(url));
149149

150150
// for gs instances
151151

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
2+
/*
3+
* Copyright 2026, GeoSolutions Sas.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree.
8+
*/
9+
10+
import React, { useEffect, useState, useRef } from 'react';
11+
import PropTypes from 'prop-types';
12+
import { Dropdown, MenuItem } from 'react-bootstrap';
13+
import GeoFence from '../../api/geoserver/GeoFence';
14+
import Message from '../../components/I18N/Message';
15+
16+
/**
17+
* A dropdown menu to select GeoServer instances for cleaning their cache.
18+
*
19+
* @param {Object[]} gsInstances - List of available GeoServer instances (name, url, etc.).
20+
* @param {boolean} show - Controls whether the menu is visible or not.
21+
* @param {Function} storeGSInstancesDDList - Redux action to save the fetched instances to the store.
22+
* @param {Function} onClose - Function to close the menu, used if an error occurs.
23+
* @param {Function} onError - Function to show an error notification.
24+
* @param {Function} onSelectAll - Function called when 'Clear For All Instances' is clicked.
25+
* @param {Function} onSelectInstance - Function called when a specific instance is clicked.
26+
*/
27+
const GSCleanCacheMenu = ({
28+
gsInstances = [],
29+
show,
30+
storeGSInstancesDDList,
31+
onClose,
32+
onError,
33+
onSelectAll,
34+
onSelectInstance
35+
}) => {
36+
const [loading, setLoading] = useState(gsInstances.length ? false : true);
37+
const menuRef = useRef(null);
38+
const handleGetGSInstances = () => {
39+
GeoFence.getGSInstancesForDD().then(response => {
40+
storeGSInstancesDDList(response.data);
41+
setLoading(false);
42+
}).catch(() => {
43+
setLoading(false);
44+
onClose();
45+
onError({
46+
title: "rulesmanager.errorTitle",
47+
message: "rulesmanager.errorLoadingGSInstances"
48+
});
49+
}
50+
);
51+
52+
};
53+
useEffect(() => {
54+
if (gsInstances.length) return;
55+
handleGetGSInstances();
56+
}, []);
57+
useEffect(() => {
58+
// Function to check if the click was outside the menu
59+
const handleClickOutside = (event) => {
60+
if (menuRef.current && !menuRef.current.contains(event.target)) {
61+
onClose(); // Call the close handler passed from props
62+
}
63+
};
64+
65+
// Only add the listener if the menu is showing
66+
if (show) {
67+
document.addEventListener('mousedown', handleClickOutside);
68+
}
69+
70+
// Cleanup the listener when the menu closes or unmounts
71+
return () => {
72+
document.removeEventListener('mousedown', handleClickOutside);
73+
};
74+
}, [show, onClose]);
75+
// Handler to wrap selection and close the menu
76+
const handleSelectInstance = (instance) => {
77+
onSelectInstance(instance);
78+
};
79+
80+
const handleSelectAll = (e) => {
81+
e.stopPropagation();
82+
onSelectAll();
83+
};
84+
85+
// If not showing, render null to keep DOM clean
86+
if (!show) return null;
87+
88+
return (
89+
<div ref={menuRef}
90+
className="gs-cache-menu-container"
91+
style={{
92+
marginTop: '5px'
93+
}}
94+
>
95+
{loading ? <div className={`toolbar-loader ${loading ? 'ms-circle-loader-md' : ''}`}/> : (
96+
<Dropdown open id="gs-cache-dropdown">
97+
<Dropdown.Toggle style={{ display: 'none' }} /> {/** hide arrow icon of DD */}
98+
<Dropdown.Menu>
99+
{/* Instance List */}
100+
{gsInstances.length === 0 ? (
101+
<MenuItem disabled>
102+
<Message msgId="rulesmanager.noAvailGsInstances" />
103+
</MenuItem>
104+
) : (
105+
gsInstances.map((instance, idx) => {
106+
const name = instance.name;
107+
return (
108+
<MenuItem
109+
key={idx}
110+
onClick={(e) => {
111+
if (e && e.stopPropagation) e.stopPropagation();
112+
handleSelectInstance(instance);
113+
}}
114+
title={name}
115+
>
116+
{name}
117+
</MenuItem>
118+
);
119+
})
120+
)}
121+
<MenuItem divider />
122+
{/* Clear All Item */}
123+
<MenuItem
124+
disabled={gsInstances.length === 0}
125+
onClick={handleSelectAll}
126+
className="gs-clear-all-item"
127+
>
128+
<strong><Message msgId="rulesmanager.clearAllgsInstances" /></strong>
129+
</MenuItem>
130+
131+
</Dropdown.Menu>
132+
</Dropdown>)
133+
}
134+
</div>
135+
);
136+
};
137+
GSCleanCacheMenu.propTypes = {
138+
gsInstances: PropTypes.array,
139+
show: PropTypes.bool,
140+
storeGSInstancesDDList: PropTypes.func,
141+
onClose: PropTypes.func,
142+
onError: PropTypes.func,
143+
onSelectAll: PropTypes.func,
144+
onSelectInstance: PropTypes.func
145+
};
146+
147+
export default GSCleanCacheMenu;

0 commit comments

Comments
 (0)