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

mendersoftware / mender-server / 1495380963

14 Oct 2024 03:35PM UTC coverage: 70.373% (-2.5%) from 72.904%
1495380963

Pull #101

gitlab-ci

mineralsfree
feat: tenant list added

Ticket: MEN-7568
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #101: feat: tenant list added

4406 of 6391 branches covered (68.94%)

Branch coverage included in aggregate %.

88 of 183 new or added lines in 10 files covered. (48.09%)

2623 existing lines in 65 files now uncovered.

36673 of 51982 relevant lines covered (70.55%)

31.07 hits per line

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

77.73
/frontend/src/js/store/organizationSlice/thunks.ts
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
// @ts-nocheck
15
import storeActions from '@northern.tech/store/actions';
16
import Api from '@northern.tech/store/api/general-api';
17
import { DEVICE_LIST_DEFAULTS, SORTING_OPTIONS, TIMEOUTS, deviceAuthV2, headerNames, iotManagerBaseURL, locations } from '@northern.tech/store/constants';
18
import { getCurrentSession, getTenantCapabilities } from '@northern.tech/store/selectors';
19
import { commonErrorFallback, commonErrorHandler } from '@northern.tech/store/store';
20
import { setFirstLoginAfterSignup } from '@northern.tech/store/thunks';
21
import { createAsyncThunk } from '@reduxjs/toolkit';
22
import { jwtDecode } from 'jwt-decode';
23
import hashString from 'md5';
24
import Cookies from 'universal-cookie';
25

26
import { actions, sliceName } from '.';
27
import { Tenant } from '../../components/tenants/types';
28
import { deepCompare } from '../../helpers';
29
import { SSO_TYPES, auditLogsApiUrl, ssoIdpApiUrlv1, tenantadmApiUrlv1, tenantadmApiUrlv2 } from './constants';
30
import { getAuditlogState, getOrganization } from './selectors';
31

32
const cookies = new Cookies();
101✔
33

34
const { setAnnouncement, setSnackbar } = storeActions;
101✔
35
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
101✔
36

37
export const cancelRequest = createAsyncThunk(`${sliceName}/cancelRequest`, (reason, { dispatch, getState }) => {
101✔
38
  const { id: tenantId } = getOrganization(getState());
1✔
39
  return Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/cancel`, { reason }).then(() =>
1✔
40
    Promise.resolve(dispatch(setSnackbar({ message: 'Deactivation request was sent successfully', autoHideDuration: TIMEOUTS.fiveSeconds })))
1✔
41
  );
42
});
43

44
export const getTargetLocation = key => {
101✔
45
  if (devLocations.includes(window.location.hostname)) {
14✔
46
    return '';
6✔
47
  }
48
  let subdomainSections = window.location.hostname.substring(0, window.location.hostname.indexOf(locations.us.location)).split('.');
8✔
49
  subdomainSections = subdomainSections.splice(0, subdomainSections.length - 1);
8✔
50
  if (!subdomainSections.find(section => section === key)) {
8✔
51
    subdomainSections = key === locations.us.key ? subdomainSections.filter(section => !locations[section]) : [...subdomainSections, key];
7✔
52
    return `https://${[...subdomainSections, ...locations.us.location.split('.')].join('.')}`;
7✔
53
  }
54
  return `https://${window.location.hostname}`;
1✔
55
};
56

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

83
export const startCardUpdate = createAsyncThunk(`${sliceName}/startCardUpdate`, (_, { dispatch }) =>
101✔
84
  Api.post(`${tenantadmApiUrlv2}/billing/card`)
1✔
85
    .then(({ data }) => {
86
      dispatch(actions.receiveSetupIntent(data.intent_id));
1✔
87
      return Promise.resolve(data.secret);
1✔
88
    })
89
    .catch(err => commonErrorHandler(err, `Updating the card failed:`, dispatch))
×
90
);
91

92
export const confirmCardUpdate = createAsyncThunk(`${sliceName}/confirmCardUpdate`, (_, { dispatch, getState }) =>
101✔
93
  Api.post(`${tenantadmApiUrlv2}/billing/card/${getState().organization.intentId}/confirm`)
1✔
94
    .then(() => Promise.all([dispatch(setSnackbar('Payment card was updated successfully')), dispatch(actions.receiveSetupIntent(null))]))
1✔
95
    .catch(err => commonErrorHandler(err, `Updating the card failed:`, dispatch))
×
96
);
97

98
export const getCurrentCard = createAsyncThunk(`${sliceName}/getCurrentCard`, (_, { dispatch }) =>
101✔
99
  Api.get(`${tenantadmApiUrlv2}/billing`).then(res => {
2✔
100
    const { last4, exp_month, exp_year, brand } = res.data.card || {};
2!
101
    return Promise.resolve(dispatch(actions.receiveCurrentCard({ brand, last4, expiration: { month: exp_month, year: exp_year } })));
2✔
102
  })
103
);
104

105
export const startUpgrade = createAsyncThunk(`${sliceName}/startUpgrade`, (tenantId, { dispatch }) =>
101✔
106
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/upgrade/start`)
1✔
107
    .then(({ data }) => Promise.resolve(data.secret))
1✔
108
    .catch(err => commonErrorHandler(err, `There was an error upgrading your account:`, dispatch))
×
109
);
110

111
export const cancelUpgrade = createAsyncThunk(`${sliceName}/cancelUpgrade`, tenantId => Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/upgrade/cancel`));
101✔
112

113
export const completeUpgrade = createAsyncThunk(`${sliceName}/completeUpgrade`, ({ tenantId, plan }, { dispatch }) =>
101✔
114
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/upgrade/complete`, { plan })
1✔
115
    .catch(err => commonErrorHandler(err, `There was an error upgrading your account:`, dispatch))
×
116
    .then(() => Promise.resolve(dispatch(getUserOrganization())))
1✔
117
);
118

119
const prepareAuditlogQuery = ({ startDate, endDate, user: userFilter, type, detail: detailFilter, sort = {} }) => {
101✔
120
  const userId = userFilter?.id || userFilter;
8✔
121
  const detail = detailFilter?.id || detailFilter;
8✔
122
  const createdAfter = startDate ? `&created_after=${Math.round(Date.parse(startDate) / 1000)}` : '';
8✔
123
  const createdBefore = endDate ? `&created_before=${Math.round(Date.parse(endDate) / 1000)}` : '';
8✔
124
  const typeSearch = type ? `&object_type=${type.value}`.toLowerCase() : '';
8!
125
  const userSearch = userId ? `&actor_id=${userId}` : '';
8!
126
  const objectSearch = type && detail ? `&${type.queryParameter}=${encodeURIComponent(detail)}` : '';
8!
127
  const { direction = SORTING_OPTIONS.desc } = sort;
8✔
128
  return `${createdAfter}${createdBefore}${userSearch}${typeSearch}${objectSearch}&sort=${direction}`;
8✔
129
};
130

131
export const getAuditLogs = createAsyncThunk(`${sliceName}/getAuditLogs`, (selectionState, { dispatch, getState }) => {
101✔
132
  const { page, perPage } = selectionState;
7✔
133
  const { hasAuditlogs } = getTenantCapabilities(getState());
7✔
134
  if (!hasAuditlogs) {
7✔
135
    return Promise.resolve();
1✔
136
  }
137
  return Api.get(`${auditLogsApiUrl}/logs?page=${page}&per_page=${perPage}${prepareAuditlogQuery(selectionState)}`)
6✔
138
    .then(({ data, headers }) => {
139
      let total = headers[headerNames.total];
6✔
140
      total = Number(total || data.length);
6!
141
      return Promise.resolve(dispatch(actions.receiveAuditLogs({ events: data, total })));
6✔
142
    })
143
    .catch(err => commonErrorHandler(err, `There was an error retrieving audit logs:`, dispatch));
×
144
});
145

146
export const getAuditLogsCsvLink = createAsyncThunk(`${sliceName}/getAuditLogsCsvLink`, (_, { getState }) =>
101✔
147
  Promise.resolve(`${window.location.origin}${auditLogsApiUrl}/logs/export?limit=20000${prepareAuditlogQuery(getAuditlogState(getState()))}`)
2✔
148
);
149

150
export const setAuditlogsState = createAsyncThunk(`${sliceName}/setAuditlogsState`, (selectionState, { dispatch, getState }) => {
101✔
151
  const currentState = getAuditlogState(getState());
4✔
152
  let nextState = {
4✔
153
    ...currentState,
154
    ...selectionState,
155
    sort: { ...currentState.sort, ...selectionState.sort }
156
  };
157
  let tasks = [];
4✔
158
  // eslint-disable-next-line no-unused-vars
159
  const { isLoading: currentLoading, selectedIssue: currentIssue, ...currentRequestState } = currentState;
4✔
160
  // eslint-disable-next-line no-unused-vars
161
  const { isLoading: selectionLoading, selectedIssue: selectionIssue, ...selectionRequestState } = nextState;
4✔
162
  if (!deepCompare(currentRequestState, selectionRequestState)) {
4!
163
    nextState.isLoading = true;
4✔
164
    tasks.push(dispatch(getAuditLogs(nextState)).finally(() => dispatch(actions.setAuditLogState({ isLoading: false }))));
4✔
165
  }
166
  tasks.push(dispatch(actions.setAuditLogState(nextState)));
4✔
167
  return Promise.all(tasks);
4✔
168
});
169

170
/*
171
  Tenant management + Hosted Mender
172
*/
173
export const tenantDataDivergedMessage = 'The system detected there is a change in your plan or purchased add-ons. Please log out and log in again';
101✔
174

175
const tenantListRetrieval = async (config): Promise<[Tenant[], number]> => {
101✔
NEW
176
  const { page, perPage } = config;
×
NEW
177
  const params = new URLSearchParams({ page, per_page: perPage }).toString();
×
NEW
178
  const tenantList = await Api.get(`${tenantadmApiUrlv2}/tenants?${params}`);
×
NEW
179
  const pageCount = tenantList.headers[headerNames.total] || 20;
×
NEW
180
  return [tenantList.data, pageCount];
×
181
};
182
export const getTenants = createAsyncThunk(`${sliceName}/getTenants`, async (_, { dispatch, getState }) => {
101✔
NEW
183
  console.log(getState());
×
NEW
184
  const currentState = getState().organization.organization.tenantList;
×
NEW
185
  const [tenants, pageCount] = await tenantListRetrieval(currentState);
×
NEW
186
  dispatch(actions.setTenantListState({ ...currentState, total: pageCount, tenants }));
×
187
});
188

189
export const setTenantsListState = createAsyncThunk(`${sliceName}/setTenantsListState`, async (selectionState: any, { dispatch, getState }) => {
101✔
NEW
190
  const currentState = getState().organization.organization.tenantList;
×
NEW
191
  const nextState = {
×
192
    ...currentState,
193
    ...selectionState
194
  };
NEW
195
  if (!deepCompare(currentState, selectionState)) {
×
NEW
196
    const [tenants, pageCount] = await tenantListRetrieval(nextState);
×
NEW
197
    return dispatch(actions.setTenantListState({ ...nextState, tenants, total: pageCount }));
×
198
  }
NEW
199
  return dispatch(actions.setTenantListState({ ...nextState, tenants }));
×
200
});
201

202
export const getUserOrganization = createAsyncThunk(`${sliceName}/getUserOrganization`, (_, { dispatch, getState }) => {
101✔
203
  return Api.get(`${tenantadmApiUrlv1}/user/tenant`).then(res => {
9✔
204
    let tasks = [dispatch(actions.setOrganization(res.data))];
8✔
205
    const { addons, plan, trial } = res.data;
8✔
206
    const { token } = getCurrentSession(getState());
8✔
207
    const jwt = jwtDecode(token);
8✔
208
    const jwtData = { addons: jwt['mender.addons'], plan: jwt['mender.plan'], trial: jwt['mender.trial'] };
8✔
209
    if (!deepCompare({ addons, plan, trial }, jwtData)) {
8!
210
      const hash = hashString(tenantDataDivergedMessage);
8✔
211
      cookies.remove(`${jwt.sub}${hash}`);
8✔
212
      tasks.push(dispatch(setAnnouncement(tenantDataDivergedMessage)));
8✔
213
    }
214
    return Promise.all(tasks);
8✔
215
  });
216
});
217

218
export const sendSupportMessage = createAsyncThunk(`${sliceName}/sendSupportMessage`, (content, { dispatch }) =>
101✔
219
  Api.post(`${tenantadmApiUrlv2}/contact/support`, content)
1✔
220
    .catch(err => commonErrorHandler(err, 'There was an error sending your request', dispatch, commonErrorFallback))
×
221
    .then(() => Promise.resolve(dispatch(setSnackbar({ message: 'Your request was sent successfully', autoHideDuration: TIMEOUTS.fiveSeconds }))))
1✔
222
);
223

224
export const requestPlanChange = createAsyncThunk(`${sliceName}/requestPlanChange`, ({ content, tenantId }, { dispatch }) =>
101✔
225
  Api.post(`${tenantadmApiUrlv2}/tenants/${tenantId}/plan`, content)
1✔
226
    .catch(err => commonErrorHandler(err, 'There was an error sending your request', dispatch, commonErrorFallback))
×
227
    .then(() => Promise.resolve(dispatch(setSnackbar({ message: 'Your request was sent successfully', autoHideDuration: TIMEOUTS.fiveSeconds }))))
1✔
228
);
229

230
export const downloadLicenseReport = createAsyncThunk(`${sliceName}/downloadLicenseReport`, (_, { dispatch }) =>
101✔
231
  Api.get(`${deviceAuthV2}/reports/devices`)
1✔
232
    .catch(err => commonErrorHandler(err, 'There was an error downloading the report', dispatch, commonErrorFallback))
×
233
    .then(res => res.data)
1✔
234
);
235

236
// eslint-disable-next-line no-unused-vars
237
export const createIntegration = createAsyncThunk(`${sliceName}/createIntegration`, ({ id, ...integration }, { dispatch }) =>
101✔
238
  Api.post(`${iotManagerBaseURL}/integrations`, integration)
1✔
239
    .catch(err => commonErrorHandler(err, 'There was an error creating the integration', dispatch, commonErrorFallback))
×
240
    .then(() => Promise.all([dispatch(setSnackbar('The integration was set up successfully')), dispatch(getIntegrations())]))
1✔
241
);
242

243
export const changeIntegration = createAsyncThunk(`${sliceName}/changeIntegration`, ({ id, credentials }, { dispatch }) =>
101✔
244
  Api.put(`${iotManagerBaseURL}/integrations/${id}/credentials`, credentials)
1✔
245
    .catch(err => commonErrorHandler(err, 'There was an error updating the integration', dispatch, commonErrorFallback))
×
246
    .then(() => Promise.all([dispatch(setSnackbar('The integration was updated successfully')), dispatch(getIntegrations())]))
1✔
247
);
248

249
export const deleteIntegration = createAsyncThunk(`${sliceName}/deleteIntegration`, ({ id, provider }, { dispatch, getState }) =>
101✔
250
  Api.delete(`${iotManagerBaseURL}/integrations/${id}`, {})
1✔
251
    .catch(err => commonErrorHandler(err, 'There was an error removing the integration', dispatch, commonErrorFallback))
×
252
    .then(() => {
253
      const integrations = getState().organization.externalDeviceIntegrations.filter(item => provider !== item.provider);
1✔
254
      return Promise.all([
1✔
255
        dispatch(setSnackbar('The integration was removed successfully')),
256
        dispatch(actions.receiveExternalDeviceIntegrations(integrations))
257
      ]);
258
    })
259
);
260

261
export const getIntegrations = createAsyncThunk(`${sliceName}/getIntegrations`, (_, { dispatch, getState }) =>
101✔
262
  Api.get(`${iotManagerBaseURL}/integrations`)
15✔
263
    .catch(err => commonErrorHandler(err, 'There was an error retrieving the integration', dispatch, commonErrorFallback))
×
264
    .then(({ data }) => {
265
      const existingIntegrations = getState().organization.externalDeviceIntegrations;
15✔
266
      const integrations = data.reduce((accu, item) => {
15✔
267
        const existingIntegration = existingIntegrations.find(integration => item.id === integration.id) ?? {};
30✔
268
        const integration = { ...existingIntegration, ...item };
30✔
269
        accu.push(integration);
30✔
270
        return accu;
30✔
271
      }, []);
272
      return Promise.resolve(dispatch(actions.receiveExternalDeviceIntegrations(integrations)));
15✔
273
    })
274
);
275

276
export const getWebhookEvents = createAsyncThunk(`${sliceName}/getWebhookEvents`, (config = {}, { dispatch, getState }) => {
101✔
277
  const { isFollowUp, page = defaultPage, perPage = defaultPerPage } = config;
3✔
278
  return Api.get(`${iotManagerBaseURL}/events?page=${page}&per_page=${perPage}`)
3✔
279
    .catch(err => commonErrorHandler(err, 'There was an error retrieving activity for this integration', dispatch, commonErrorFallback))
×
280
    .then(({ data }) => {
281
      let tasks = [
3✔
282
        dispatch(
283
          actions.receiveWebhookEvents({
284
            value: isFollowUp ? getState().organization.webhooks.events : data,
3✔
285
            total: (page - 1) * perPage + data.length
286
          })
287
        )
288
      ];
289
      if (data.length >= perPage && !isFollowUp) {
3✔
290
        tasks.push(dispatch(getWebhookEvents({ isFollowUp: true, page: page + 1, perPage: 1 })));
1✔
291
      }
292
      return Promise.all(tasks);
3✔
293
    });
294
});
295

296
const ssoConfigActions = {
101✔
297
  create: { success: 'stored', error: 'storing' },
298
  edit: { success: 'updated', error: 'updating' },
299
  read: { success: '', error: 'retrieving' },
300
  remove: { success: 'removed', error: 'removing' },
301
  readMultiple: { success: '', error: 'retrieving' }
302
};
303

304
const ssoConfigActionErrorHandler = (err, type) => dispatch =>
101✔
305
  commonErrorHandler(err, `There was an error ${ssoConfigActions[type].error} the SSO configuration.`, dispatch, commonErrorFallback);
×
306

307
const ssoConfigActionSuccessHandler = type => dispatch => dispatch(setSnackbar(`The SSO configuration was ${ssoConfigActions[type].success} successfully`));
101✔
308

309
export const storeSsoConfig = createAsyncThunk(`${sliceName}/storeSsoConfig`, ({ config, contentType }, { dispatch }) =>
101✔
310
  Api.post(ssoIdpApiUrlv1, config, { headers: { 'Content-Type': contentType, Accept: 'application/json' } })
1✔
311
    .catch(err => dispatch(ssoConfigActionErrorHandler(err, 'create')))
×
312
    .then(() => Promise.all([dispatch(ssoConfigActionSuccessHandler('create')), dispatch(getSsoConfigs())]))
1✔
313
);
314

315
export const changeSsoConfig = createAsyncThunk(`${sliceName}/changeSsoConfig`, ({ config, contentType }, { dispatch }) =>
101✔
316
  Api.put(`${ssoIdpApiUrlv1}/${config.id}`, config, { headers: { 'Content-Type': contentType, Accept: 'application/json' } })
1✔
317
    .catch(err => dispatch(ssoConfigActionErrorHandler(err, 'edit')))
×
318
    .then(() => Promise.all([dispatch(ssoConfigActionSuccessHandler('edit')), dispatch(getSsoConfigs())]))
1✔
319
);
320

321
export const deleteSsoConfig = createAsyncThunk(`${sliceName}/deleteSsoConfig`, ({ id }, { dispatch, getState }) =>
101✔
322
  Api.delete(`${ssoIdpApiUrlv1}/${id}`)
1✔
323
    .catch(err => dispatch(ssoConfigActionErrorHandler(err, 'remove')))
×
324
    .then(() => {
325
      const configs = getState().organization.ssoConfigs.filter(item => id !== item.id);
2✔
326
      return Promise.all([dispatch(ssoConfigActionSuccessHandler('remove')), dispatch(actions.receiveSsoConfigs(configs))]);
1✔
327
    })
328
);
329

330
export const getSsoConfigById = createAsyncThunk(`${sliceName}/getSsoConfigById`, (config, { dispatch }) =>
101✔
331
  Api.get(`${ssoIdpApiUrlv1}/${config.id}`)
10✔
332
    .catch(err => dispatch(ssoConfigActionErrorHandler(err, 'read')))
×
333
    .then(({ data, headers }) => {
334
      const sso = Object.values(SSO_TYPES).find(({ contentType }) => contentType === headers['content-type']);
20✔
335
      return sso ? Promise.resolve({ ...config, config: data, type: sso.id }) : Promise.reject('Unsupported SSO config content type.');
10!
336
    })
337
);
338

339
export const getSsoConfigs = createAsyncThunk(`${sliceName}/getSsoConfigs`, (_, { dispatch }) =>
101✔
340
  Api.get(ssoIdpApiUrlv1)
5✔
341
    .catch(err => dispatch(ssoConfigActionErrorHandler(err, 'readMultiple')))
×
342
    .then(({ data }) =>
343
      Promise.all(data.map(config => dispatch(getSsoConfigById(config)).unwrap()))
10✔
344
        .then(configs => dispatch(actions.receiveSsoConfigs(configs)))
5✔
345
        .catch(err => commonErrorHandler(err, err, dispatch, ''))
×
346
    )
347
);
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