• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 18371528919

09 Oct 2025 09:16AM UTC coverage: 76.738% (-0.05%) from 76.789%
18371528919

Pull #11572

github

web-flow
Merge 62e9c9670 into 2686c544e
Pull Request #11572: Feat: #11527 Add the tabbed view for the dashboard

31855 of 49574 branches covered (64.26%)

94 of 155 new or added lines in 10 files covered. (60.65%)

3 existing lines in 2 files now uncovered.

39633 of 51647 relevant lines covered (76.74%)

37.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.18
/web/client/epics/dashboard.js
1
/*
2
 * Copyright 2017, GeoSolutions Sas.
3
 * All rights reserved.
4
 *
5
 * This source code is licensed under the BSD-style license found in the
6
 * LICENSE file in the root directory of this source tree.
7
 */
8
import Rx from 'rxjs';
9
import { mapValues, isObject, keys, isNil } from 'lodash';
10

11
import { NEW, INSERT, EDIT, OPEN_FILTER_EDITOR, editNewWidget, onEditorChange} from '../actions/widgets';
12

13
import {
14
    setEditing,
15
    dashboardSaved,
16
    dashboardLoaded,
17
    dashboardLoading,
18
    triggerSave,
19
    triggerSaveAs,
20
    loadDashboard,
21
    dashboardSaveError,
22
    SAVE_DASHBOARD,
23
    DASHBOARD_EXPORT,
24
    LOAD_DASHBOARD,
25
    DASHBOARD_IMPORT,
26
    dashboardLoadError
27
} from '../actions/dashboard';
28

29
import { setControlProperty, TOGGLE_CONTROL, toggleControl } from '../actions/controls';
30
import { featureTypeSelected } from '../actions/wfsquery';
31
import { show, error } from '../actions/notifications';
32
import { loadFilter, QUERY_FORM_SEARCH } from '../actions/queryform';
33
import { CHECK_LOGGED_USER, LOGIN_SUCCESS, LOGOUT } from '../actions/security';
34
import { isDashboardEditing, isDashboardAvailable } from '../selectors/dashboard';
35
import { isLoggedIn } from '../selectors/security';
36
import { getEditingWidgetLayer, getEditingWidgetFilter, getWidgetFilterKey } from '../selectors/widgets';
37
import { pathnameSelector } from '../selectors/router';
38
import { download, readJson } from '../utils/FileUtils';
39
import { createResource, updateResource, getResource, updateResourceAttribute } from '../api/persistence';
40
import { wrapStartStop } from '../observables/epics';
41
import { LOCATION_CHANGE, push } from 'connected-react-router';
42
import { convertDependenciesMappingForCompatibility, updateDependenciesForMultiViewCompatibility } from "../utils/WidgetsUtils";
43
const getFTSelectedArgs = (state) => {
1✔
44
    let layer = getEditingWidgetLayer(state);
2✔
45
    let url = layer.search && layer.search.url;
2✔
46
    let typeName = layer.name;
2✔
47
    return [url, typeName];
2✔
48
};
49

50
// Basic interactions with dashboard editor
51
export const openDashboardWidgetEditor = (action$, {getState = () => {}} = {}) => action$.ofType(NEW, EDIT)
4!
52
    .filter( () => isDashboardAvailable(getState()))
2✔
53
    .switchMap(() => Rx.Observable.of(
1✔
54
        setEditing(true)
55
    ));
56
// Basic interactions with dashboard editor
57
export const closeDashboardWidgetEditorOnFinish = (action$, {getState = () => {}} = {}) => action$.ofType(INSERT)
4!
58
    .filter( () => isDashboardAvailable(getState()))
2✔
59
    .switchMap(() => Rx.Observable.of(setEditing(false)));
1✔
60

61
// Basic interactions with dashboard editor
62
export const initDashboardEditorOnNew = (action$, {getState = () => {}} = {}) => action$.ofType(NEW)
4!
63
    .filter( () => isDashboardAvailable(getState()))
2✔
64
    .switchMap((w) => Rx.Observable.of(editNewWidget({
1✔
65
        legend: false,
66
        mapSync: false,
67
        cartesian: true,
68
        yAxis: true,
69
        ...w,
70
        // override action's type
71
        type: undefined
72
    }, {step: 0})));
73
// Basic interactions with dashboard editor
74
export const closeDashboardEditorOnExit = (action$, {getState = () => {}} = {}) => action$.ofType(LOCATION_CHANGE)
2!
75
    .filter( () => isDashboardAvailable(getState()))
×
76
    .filter( () => isDashboardEditing(getState()) )
×
77
    .switchMap(() => Rx.Observable.of(setEditing(false)));
×
78

79
/**
80
     * Manages interaction with QueryPanel and Dashboard
81
     */
82
export const handleDashboardWidgetsFilterPanel = (action$, {getState = () => {}} = {}) => action$.ofType(OPEN_FILTER_EDITOR)
4!
83
    .filter(() => isDashboardAvailable(getState()))
2✔
84
    .switchMap(() =>
85
    // open and setup query form
86
        Rx.Observable.of(
2✔
87
            featureTypeSelected(...getFTSelectedArgs(getState())),
88
            loadFilter(getEditingWidgetFilter(getState())),
89
            setControlProperty('queryPanel', "enabled", true)
90
            // wait for any filter update(search) or query form close event
91
        ).concat(
92
            Rx.Observable.race(
93
                action$.ofType(QUERY_FORM_SEARCH).take(1),
94
                action$.ofType(TOGGLE_CONTROL).filter(({control, property} = {}) => control === "queryPanel" && (!property || property === "enabled")).take(1)
×
95
            )
96
            // then close the query panel, open widget form and update the current filter for the widget in editing
97
                .switchMap( action =>
98
                    (action.filterObj
1!
99
                        ? Rx.Observable.of(onEditorChange(getWidgetFilterKey(getState()), action.filterObj))
100
                        : Rx.Observable.empty()
101
                    )
102
                        .merge(Rx.Observable.of(
103
                            setControlProperty("widgetBuilder", "enabled", true)
104
                        ))
105
                )
106
            // if the widgetBuilder is closed or the page is changed, do not listen anymore
107
        ).takeUntil(
108
            action$.ofType(LOCATION_CHANGE, EDIT)
109
                .merge(action$.ofType(TOGGLE_CONTROL).filter(({control, property} = {}) => control === "widgetBuilder" && (!property === false))))
×
110
            .concat(
111
                Rx.Observable.of(// drawSupportReset(),
112
                    setControlProperty('queryPanel', "enabled", false)
113
                )
114
            )
115
    );
116
export const filterAnonymousUsersForDashboard = (actions$, store) => actions$
4✔
117
    .ofType(CHECK_LOGGED_USER, LOGOUT)
118
    .filter(() => pathnameSelector(store.getState()) === "/dashboard")
2✔
119
    .switchMap( ({}) => {
120
        return !isLoggedIn(store.getState()) ? Rx.Observable.of(dashboardLoadError({status: 403})) : Rx.Observable.empty();
2!
121
    });
122

123
// dashboard loading from resource ID.
124
export const loadDashboardStream = (action$, {getState = () => {}}) => action$
2!
125
    .ofType(LOAD_DASHBOARD)
126
    .switchMap( ({id}) =>
127
        getResource(id)
×
NEW
128
            .map(({ data, ...resource }) => dashboardLoaded(resource, updateDependenciesForMultiViewCompatibility(convertDependenciesMappingForCompatibility(data))))
×
129
            .let(wrapStartStop(
130
                dashboardLoading(true, "loading"),
131
                dashboardLoading(false, "loading"),
132
                e => {
133
                    const page = window.location.href.match('dashboard-embedded')
×
134
                        ? 'dashboardEmbedded'
135
                        : 'dashboard';
136
                    let message = page + ".errors.loading.unknownError";
×
137
                    if (e.status === 403 ) {
×
138
                        message = page + ".errors.loading.pleaseLogin";
×
139
                        if ( isLoggedIn(getState())) {
×
140
                            message = page + ".errors.loading.dashboardNotAccessible";
×
141
                        }
142
                    } if (e.status === 404) {
×
143
                        message = page + ".errors.loading.dashboardDoesNotExist";
×
144
                    }
145
                    return Rx.Observable.of(
×
146
                        error({
147
                            title: page + ".errors.loading.title",
148
                            message
149
                        }),
150
                        dashboardLoadError({...e, messageId: message})
151
                    );
152
                }
153
            ))
154
    );
155
export const reloadDashboardOnLoginLogout = (action$) =>
1✔
156
    action$.ofType(LOAD_DASHBOARD).switchMap(
2✔
157
        ({ id }) => action$
×
158
            .ofType(LOGIN_SUCCESS, LOGOUT)
159
            .switchMap(() => Rx.Observable.of(loadDashboard(id)).delay(1000))
×
160
            .takeUntil(action$.ofType(LOCATION_CHANGE))
161
    );
162
// saving dashboard flow (both creation and update)
163
export const saveDashboard = action$ => action$
5✔
164
    .ofType(SAVE_DASHBOARD)
165
    .exhaustMap(({resource} = {}) =>{
×
166
        // convert to json if attribute is an object
167
        const attributesFixed = mapValues(resource.attributes, attr => {
3✔
168
            if (isObject(attr)) {
3!
169
                let json = null;
×
170
                try {
×
171
                    json = JSON.stringify(attr);
×
172
                } catch (e) {
173
                    json = null;
×
174
                }
175
                return json;
×
176
            }
177
            return attr;
3✔
178
        });
179
        // filter out invalid attributes
180
            // thumbnails and details are handled separately(linked resources)
181
        const validAttributesNames = keys(attributesFixed)
3✔
182
            .filter(attrName => attrName !== 'thumbnail' && attrName !== 'details' && !isNil(attributesFixed[attrName]));
3✔
183
        return Rx.Observable.forkJoin(
3✔
184
            (!resource.id ? createResource(resource) : updateResource(resource)))
3!
185
            .switchMap(([rid]) => (validAttributesNames.length > 0 ?
1!
186
                Rx.Observable.forkJoin(validAttributesNames.map(attrName => updateResourceAttribute({
×
187
                    id: rid,
188
                    name: attrName,
189
                    value: attributesFixed[attrName]
190
                }))) : Rx.Observable.of([])) .switchMap(() => Rx.Observable.of(
1✔
191
                dashboardSaved(rid),
192
                resource.id ? triggerSave(false) : triggerSaveAs(false),
1!
193
                !resource.id
1!
194
                    ? push(`/dashboard/${rid}`)
195
                    : loadDashboard(rid)
196
            ).merge(
197
                Rx.Observable.of(show({
198
                    id: "DASHBOARD_SAVE_SUCCESS",
199
                    title: "saveDialog.saveSuccessTitle",
200
                    message: "saveDialog.saveSuccessMessage"
201
                })).delay(!resource.id ? 1000 : 0) // delay to allow loading
1!
202
            ))
203
                .let(wrapStartStop(
204
                    dashboardLoading(true, "saving"),
205
                    dashboardLoading(false, "saving")
206
                )
207
                ));
208
    }).catch(
209
        ({ status, statusText, data, message, ...other } = {}) => Rx.Observable.of(dashboardSaveError(status ? { status, statusText, data } : message || other), dashboardLoading(false, "saving"))
2!
210
    );
211

212
export const exportDashboard = action$ => action$
3✔
213
    .ofType(DASHBOARD_EXPORT)
214
    .switchMap(({data, fileName}) =>
215
        Rx.Observable.of([JSON.stringify({...data}), fileName, 'application/json'])
1✔
216
            .do((downloadArgs) => download(...downloadArgs))
1✔
217
            .map(() => toggleControl('export'))
1✔
218
    );
219

220
export const importDashboard = action$ => action$
4✔
221
    .ofType(DASHBOARD_IMPORT)
222
    .switchMap(({file, resource}) => (
223
        Rx.Observable.defer(() => readJson(file[0]).then((data) => data))
2✔
224
            .switchMap((dashboard) => Rx.Observable.of(
1✔
225
                dashboardLoaded(resource, dashboard),
226
                toggleControl('import')
227
            ))
228
            .catch((e) => Rx.Observable.of(
1✔
229
                error({ title: "dashboard.errors.loading.title" }),
230
                dashboardLoadError({...e})
231
            ))
232
    ));
233

234
export default {
235
    openDashboardWidgetEditor,
236
    closeDashboardWidgetEditorOnFinish,
237
    initDashboardEditorOnNew,
238
    closeDashboardEditorOnExit,
239
    handleDashboardWidgetsFilterPanel,
240
    filterAnonymousUsersForDashboard,
241
    loadDashboardStream,
242
    reloadDashboardOnLoginLogout,
243
    saveDashboard,
244
    exportDashboard,
245
    importDashboard
246
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc