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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

87.5
/src/js/actions/organizationActions.js
1
// Copyright 2020 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14
import { jwtDecode } from 'jwt-decode';
15
import hashString from 'md5';
16
import Cookies from 'universal-cookie';
17

18
import Api, { apiUrl, headerNames } from '../api/general-api';
19
import { SET_ANNOUNCEMENT, SORTING_OPTIONS, TIMEOUTS, locations } from '../constants/appConstants';
20
import { DEVICE_LIST_DEFAULTS } from '../constants/deviceConstants';
21
import {
22
  RECEIVE_AUDIT_LOGS,
23
  RECEIVE_CURRENT_CARD,
24
  RECEIVE_EXTERNAL_DEVICE_INTEGRATIONS,
25
  RECEIVE_SAML_CONFIGS,
26
  RECEIVE_SETUP_INTENT,
27
  RECEIVE_WEBHOOK_EVENTS,
28
  SET_AUDITLOG_STATE,
29
  SET_ORGANIZATION
30
} from '../constants/organizationConstants';
31
import { deepCompare } from '../helpers';
32
import { getCurrentSession, getTenantCapabilities } from '../selectors';
33
import { commonErrorFallback, commonErrorHandler, setFirstLoginAfterSignup, setSnackbar } from './appActions';
34
import { deviceAuthV2, iotManagerBaseURL } from './deviceActions';
35

36
const cookies = new Cookies();
183✔
37
export const auditLogsApiUrl = `${apiUrl.v1}/auditlogs`;
183✔
38
export const tenantadmApiUrlv1 = `${apiUrl.v1}/tenantadm`;
183✔
39
export const tenantadmApiUrlv2 = `${apiUrl.v2}/tenantadm`;
183✔
40
export const samlIdpApiUrlv1 = `${apiUrl.v1}/useradm/sso/idp/metadata`;
183✔
41

42
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
183✔
43

44
export const cancelRequest = (tenantId, reason) => dispatch =>
183✔
45
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/cancel`, { reason: reason }).then(() =>
1✔
46
    Promise.resolve(dispatch(setSnackbar('Deactivation request was sent successfully', TIMEOUTS.fiveSeconds, '')))
1✔
47
  );
48

49
export const getTargetLocation = key => {
183✔
50
  if (devLocations.includes(window.location.hostname)) {
15✔
51
    return '';
7✔
52
  }
53
  let subdomainSections = window.location.hostname.substring(0, window.location.hostname.indexOf(locations.us.location)).split('.');
8✔
54
  subdomainSections = subdomainSections.splice(0, subdomainSections.length - 1);
8✔
55
  if (!subdomainSections.find(section => section === key)) {
8✔
56
    subdomainSections = key === locations.us.key ? subdomainSections.filter(section => !locations[section]) : [...subdomainSections, key];
7✔
57
    return `https://${[...subdomainSections, ...locations.us.location.split('.')].join('.')}`;
7✔
58
  }
59
  return `https://${window.location.hostname}`;
1✔
60
};
61

62
const devLocations = ['localhost', 'docker.mender.io'];
183✔
63
export const createOrganizationTrial = data => dispatch => {
183✔
64
  const { key } = locations[data.location];
3✔
65
  const targetLocation = getTargetLocation(key);
3✔
66
  const target = `${targetLocation}${tenantadmApiUrlv2}/tenants/trial`;
3✔
67
  return Api.postUnauthorized(target, data)
3✔
68
    .catch(err => {
69
      if (err.response.status >= 400 && err.response.status < 500) {
×
70
        dispatch(setSnackbar(err.response.data.error, TIMEOUTS.fiveSeconds, ''));
×
71
        return Promise.reject(err);
×
72
      }
73
    })
74
    .then(({ headers }) => {
75
      cookies.remove('oauth');
3✔
76
      cookies.remove('externalID');
3✔
77
      cookies.remove('email');
3✔
78
      dispatch(setFirstLoginAfterSignup(true));
3✔
79
      return new Promise(resolve =>
3✔
80
        setTimeout(() => {
3✔
81
          window.location.assign(`${targetLocation}${headers.location || ''}`);
2✔
82
          return resolve();
2✔
83
        }, TIMEOUTS.fiveSeconds)
84
      );
85
    });
86
};
87

88
export const startCardUpdate = () => dispatch =>
183✔
89
  Api.post(`${tenantadmApiUrlv2}/billing/card`)
1✔
90
    .then(res => {
91
      dispatch({
1✔
92
        type: RECEIVE_SETUP_INTENT,
93
        intentId: res.data.intent_id
94
      });
95
      return Promise.resolve(res.data.secret);
1✔
96
    })
97
    .catch(err => commonErrorHandler(err, `Updating the card failed:`, dispatch));
×
98

99
export const confirmCardUpdate = () => (dispatch, getState) =>
183✔
100
  Api.post(`${tenantadmApiUrlv2}/billing/card/${getState().organization.intentId}/confirm`)
1✔
101
    .then(() =>
102
      Promise.all([
1✔
103
        dispatch(setSnackbar('Payment card was updated successfully')),
104
        dispatch({
105
          type: RECEIVE_SETUP_INTENT,
106
          intentId: null
107
        })
108
      ])
109
    )
110
    .catch(err => commonErrorHandler(err, `Updating the card failed:`, dispatch));
×
111

112
export const getCurrentCard = () => dispatch =>
183✔
113
  Api.get(`${tenantadmApiUrlv2}/billing`).then(res => {
2✔
114
    const { last4, exp_month, exp_year, brand } = res.data.card || {};
1!
115
    return Promise.resolve(
1✔
116
      dispatch({
117
        type: RECEIVE_CURRENT_CARD,
118
        card: {
119
          brand,
120
          last4,
121
          expiration: { month: exp_month, year: exp_year }
122
        }
123
      })
124
    );
125
  });
126

127
export const startUpgrade = tenantId => dispatch =>
183✔
128
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/upgrade/start`)
1✔
129
    .then(({ data }) => Promise.resolve(data.secret))
1✔
130
    .catch(err => commonErrorHandler(err, `There was an error upgrading your account:`, dispatch));
×
131

132
export const cancelUpgrade = tenantId => () => Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/upgrade/cancel`);
183✔
133

134
export const completeUpgrade = (tenantId, plan) => dispatch =>
183✔
135
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/upgrade/complete`, { plan })
1✔
136
    .catch(err => commonErrorHandler(err, `There was an error upgrading your account:`, dispatch))
×
137
    .then(() => Promise.resolve(dispatch(getUserOrganization())));
1✔
138

139
const prepareAuditlogQuery = ({ startDate, endDate, user: userFilter, type, detail: detailFilter, sort = {} }) => {
183✔
140
  const userId = userFilter?.id || userFilter;
13✔
141
  const detail = detailFilter?.id || detailFilter;
13✔
142
  const createdAfter = startDate ? `&created_after=${Math.round(Date.parse(startDate) / 1000)}` : '';
13✔
143
  const createdBefore = endDate ? `&created_before=${Math.round(Date.parse(endDate) / 1000)}` : '';
13✔
144
  const typeSearch = type ? `&object_type=${type.value}`.toLowerCase() : '';
13!
145
  const userSearch = userId ? `&actor_id=${userId}` : '';
13!
146
  const objectSearch = type && detail ? `&${type.queryParameter}=${encodeURIComponent(detail)}` : '';
13!
147
  const { direction = SORTING_OPTIONS.desc } = sort;
13✔
148
  return `${createdAfter}${createdBefore}${userSearch}${typeSearch}${objectSearch}&sort=${direction}`;
13✔
149
};
150

151
export const getAuditLogs = selectionState => (dispatch, getState) => {
183✔
152
  const { page, perPage } = selectionState;
12✔
153
  const { hasAuditlogs } = getTenantCapabilities(getState());
12✔
154
  if (!hasAuditlogs) {
12✔
155
    return Promise.resolve();
1✔
156
  }
157
  return Api.get(`${auditLogsApiUrl}/logs?page=${page}&per_page=${perPage}${prepareAuditlogQuery(selectionState)}`)
11✔
158
    .then(res => {
159
      let total = res.headers[headerNames.total];
11✔
160
      total = Number(total || res.data.length);
11!
161
      return Promise.resolve(dispatch({ type: RECEIVE_AUDIT_LOGS, events: res.data, total }));
11✔
162
    })
163
    .catch(err => commonErrorHandler(err, `There was an error retrieving audit logs:`, dispatch));
×
164
};
165

166
export const getAuditLogsCsvLink = () => (dispatch, getState) =>
183✔
167
  Promise.resolve(`${auditLogsApiUrl}/logs/export?limit=20000${prepareAuditlogQuery(getState().organization.auditlog.selectionState)}`);
2✔
168

169
export const setAuditlogsState = selectionState => (dispatch, getState) => {
183✔
170
  const currentState = getState().organization.auditlog.selectionState;
16✔
171
  let nextState = {
16✔
172
    ...currentState,
173
    ...selectionState,
174
    sort: { ...currentState.sort, ...selectionState.sort }
175
  };
176
  let tasks = [];
16✔
177
  // eslint-disable-next-line no-unused-vars
178
  const { isLoading: currentLoading, selectedIssue: currentIssue, ...currentRequestState } = currentState;
16✔
179
  // eslint-disable-next-line no-unused-vars
180
  const { isLoading: selectionLoading, selectedIssue: selectionIssue, ...selectionRequestState } = nextState;
16✔
181
  if (!deepCompare(currentRequestState, selectionRequestState)) {
16✔
182
    nextState.isLoading = true;
8✔
183
    tasks.push(dispatch(getAuditLogs(nextState)).finally(() => dispatch(setAuditlogsState({ isLoading: false }))));
8✔
184
  }
185
  tasks.push(dispatch({ type: SET_AUDITLOG_STATE, state: nextState }));
16✔
186
  return Promise.all(tasks);
16✔
187
};
188

189
/*
190
  Tenant management + Hosted Mender
191
*/
192
export const tenantDataDivergedMessage = 'The system detected there is a change in your plan or purchased add-ons. Please log out and log in again';
183✔
193
export const getUserOrganization = () => (dispatch, getState) =>
183✔
194
  Api.get(`${tenantadmApiUrlv1}/user/tenant`).then(res => {
8✔
195
    let tasks = [dispatch({ type: SET_ORGANIZATION, organization: res.data })];
7✔
196
    const { addons, plan, trial } = res.data;
7✔
197
    const { token } = getCurrentSession(getState());
7✔
198
    const jwt = jwtDecode(token);
7✔
199
    const jwtData = { addons: jwt['mender.addons'], plan: jwt['mender.plan'], trial: jwt['mender.trial'] };
7✔
200
    if (!deepCompare({ addons, plan, trial }, jwtData)) {
7!
201
      const hash = hashString(tenantDataDivergedMessage);
7✔
202
      cookies.remove(`${jwt.sub}${hash}`);
7✔
203
      tasks.push(dispatch({ type: SET_ANNOUNCEMENT, announcement: tenantDataDivergedMessage }));
7✔
204
    }
205
    return Promise.all(tasks);
7✔
206
  });
207

208
export const sendSupportMessage = content => dispatch =>
183✔
209
  Api.post(`${tenantadmApiUrlv2}/contact/support`, content)
1✔
210
    .catch(err => commonErrorHandler(err, 'There was an error sending your request', dispatch, commonErrorFallback))
×
211
    .then(() => Promise.resolve(dispatch(setSnackbar('Your request was sent successfully', TIMEOUTS.fiveSeconds, ''))));
1✔
212

213
export const requestPlanChange = (tenantId, content) => dispatch =>
183✔
214
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/plan`, content)
1✔
215
    .catch(err => commonErrorHandler(err, 'There was an error sending your request', dispatch, commonErrorFallback))
×
216
    .then(() => Promise.resolve(dispatch(setSnackbar('Your request was sent successfully', TIMEOUTS.fiveSeconds, ''))));
1✔
217

218
export const downloadLicenseReport = () => dispatch =>
183✔
219
  Api.get(`${deviceAuthV2}/reports/devices`)
1✔
220
    .catch(err => commonErrorHandler(err, 'There was an error downloading the report', dispatch, commonErrorFallback))
×
221
    .then(res => res.data);
1✔
222

223
export const createIntegration = integration => dispatch => {
183✔
224
  // eslint-disable-next-line no-unused-vars
225
  const { credentials, id, provider, ...remainder } = integration;
1✔
226
  return Api.post(`${iotManagerBaseURL}/integrations`, { provider, credentials, ...remainder })
1✔
227
    .catch(err => commonErrorHandler(err, 'There was an error creating the integration', dispatch, commonErrorFallback))
×
228
    .then(() => Promise.all([dispatch(setSnackbar('The integration was set up successfully')), dispatch(getIntegrations())]));
1✔
229
};
230

231
export const changeIntegration = integration => dispatch =>
183✔
232
  Api.put(`${iotManagerBaseURL}/integrations/${integration.id}/credentials`, integration.credentials)
1✔
233
    .catch(err => commonErrorHandler(err, 'There was an error updating the integration', dispatch, commonErrorFallback))
×
234
    .then(() => Promise.all([dispatch(setSnackbar('The integration was updated successfully')), dispatch(getIntegrations())]));
1✔
235

236
export const deleteIntegration = integration => (dispatch, getState) =>
183✔
237
  Api.delete(`${iotManagerBaseURL}/integrations/${integration.id}`, {})
1✔
238
    .catch(err => commonErrorHandler(err, 'There was an error removing the integration', dispatch, commonErrorFallback))
×
239
    .then(() => {
240
      const integrations = getState().organization.externalDeviceIntegrations.filter(item => integration.provider !== item.provider);
1✔
241
      return Promise.all([
1✔
242
        dispatch(setSnackbar('The integration was removed successfully')),
243
        dispatch({ type: RECEIVE_EXTERNAL_DEVICE_INTEGRATIONS, value: integrations })
244
      ]);
245
    });
246

247
export const getIntegrations = () => (dispatch, getState) =>
183✔
248
  Api.get(`${iotManagerBaseURL}/integrations`)
9✔
249
    .catch(err => commonErrorHandler(err, 'There was an error retrieving the integration', dispatch, commonErrorFallback))
×
250
    .then(({ data }) => {
251
      const existingIntegrations = getState().organization.externalDeviceIntegrations;
6✔
252
      const integrations = data.reduce((accu, item) => {
6✔
253
        const existingIntegration = existingIntegrations.find(integration => item.id === integration.id) ?? {};
12✔
254
        const integration = { ...existingIntegration, ...item };
12✔
255
        accu.push(integration);
12✔
256
        return accu;
12✔
257
      }, []);
258
      return Promise.resolve(dispatch({ type: RECEIVE_EXTERNAL_DEVICE_INTEGRATIONS, value: integrations }));
6✔
259
    });
260

261
export const getWebhookEvents =
262
  (config = {}) =>
183✔
263
  (dispatch, getState) => {
3✔
264
    const { isFollowUp, page = defaultPage, perPage = defaultPerPage } = config;
3✔
265
    return Api.get(`${iotManagerBaseURL}/events?page=${page}&per_page=${perPage}`)
3✔
266
      .catch(err => commonErrorHandler(err, 'There was an error retrieving activity for this integration', dispatch, commonErrorFallback))
×
267
      .then(({ data }) => {
268
        let tasks = [
3✔
269
          dispatch({
270
            type: RECEIVE_WEBHOOK_EVENTS,
271
            value: isFollowUp ? getState().organization.webhooks.events : data,
3✔
272
            total: (page - 1) * perPage + data.length
273
          })
274
        ];
275
        if (data.length >= perPage && !isFollowUp) {
3✔
276
          tasks.push(dispatch(getWebhookEvents({ isFollowUp: true, page: page + 1, perPage: 1 })));
1✔
277
        }
278
        return Promise.all(tasks);
3✔
279
      });
280
  };
281

282
const samlConfigActions = {
183✔
283
  create: { success: 'stored', error: 'storing' },
284
  edit: { success: 'updated', error: 'updating' },
285
  read: { success: '', error: 'retrieving' },
286
  remove: { success: 'removed', error: 'removing' }
287
};
288

289
const samlConfigActionErrorHandler = (err, type) => dispatch =>
183✔
290
  commonErrorHandler(err, `There was an error ${samlConfigActions[type].error} the SAML configuration.`, dispatch, commonErrorFallback);
×
291

292
const samlConfigActionSuccessHandler = type => dispatch => dispatch(setSnackbar(`The SAML configuration was ${samlConfigActions[type].success} successfully`));
183✔
293

294
export const storeSamlConfig = config => dispatch =>
183✔
295
  Api.post(samlIdpApiUrlv1, config, { headers: { 'Content-Type': 'application/samlmetadata+xml', Accept: 'application/json' } })
1✔
296
    .catch(err => dispatch(samlConfigActionErrorHandler(err, 'create')))
×
297
    .then(() => Promise.all([dispatch(samlConfigActionSuccessHandler('create')), dispatch(getSamlConfigs())]));
1✔
298

299
export const changeSamlConfig =
300
  ({ id, config }) =>
183✔
301
  dispatch =>
1✔
302
    Api.put(`${samlIdpApiUrlv1}/${id}`, config, { headers: { 'Content-Type': 'application/samlmetadata+xml', Accept: 'application/json' } })
1✔
303
      .catch(err => dispatch(samlConfigActionErrorHandler(err, 'edit')))
×
304
      .then(() => Promise.all([dispatch(samlConfigActionSuccessHandler('edit')), dispatch(getSamlConfigs())]));
1✔
305

306
export const deleteSamlConfig =
307
  ({ id }) =>
183✔
308
  (dispatch, getState) =>
3✔
309
    Api.delete(`${samlIdpApiUrlv1}/${id}`)
3✔
310
      .catch(err => dispatch(samlConfigActionErrorHandler(err, 'remove')))
×
311
      .then(() => {
312
        const configs = getState().organization.samlConfigs.filter(item => id !== item.id);
5✔
313
        return Promise.all([dispatch(samlConfigActionSuccessHandler('remove')), dispatch({ type: RECEIVE_SAML_CONFIGS, value: configs })]);
3✔
314
      });
315

316
const getSamlConfigById = config => dispatch =>
183✔
317
  Api.get(`${samlIdpApiUrlv1}/${config.id}`)
12✔
318
    .catch(err => dispatch(samlConfigActionErrorHandler(err, 'read')))
×
319
    .then(({ data }) => Promise.resolve({ ...config, config: data }));
12✔
320

321
export const getSamlConfigs = () => dispatch =>
183✔
322
  Api.get(samlIdpApiUrlv1)
6✔
323
    .catch(err => commonErrorHandler(err, 'There was an error retrieving SAML configurations', dispatch, commonErrorFallback))
×
324
    .then(({ data }) =>
325
      Promise.all(data.map(config => Promise.resolve(dispatch(getSamlConfigById(config))))).then(configs => {
12✔
326
        return dispatch({ type: RECEIVE_SAML_CONFIGS, value: configs });
6✔
327
      })
328
    );
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

© 2025 Coveralls, Inc