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

mendersoftware / mender-server / 1568834739

02 Dec 2024 10:01AM UTC coverage: 73.562% (+0.8%) from 72.786%
1568834739

Pull #211

gitlab-ci

mineralsfree
test: added upgrade unit tests

Ticket: MEN-7469
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #211: MEN-7469-feat: updated upgrades and add-on page

4251 of 6156 branches covered (69.05%)

Branch coverage included in aggregate %.

166 of 200 new or added lines in 18 files covered. (83.0%)

47 existing lines in 4 files now uncovered.

40029 of 54038 relevant lines covered (74.08%)

17.83 hits per line

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

70.22
/frontend/src/js/components/auditlogs/auditlogs.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 React, { useCallback, useEffect, useRef, useState } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16

17
import { Button, TextField } from '@mui/material';
18
import { makeStyles } from 'tss-react/mui';
19

20
import EnterpriseNotification, { DefaultUpgradeNotification } from '@northern.tech/common-ui/enterpriseNotification';
21
import { ControlledAutoComplete } from '@northern.tech/common-ui/forms/autocomplete';
22
import ClickFilter from '@northern.tech/common-ui/forms/clickfilter';
23
import Filters from '@northern.tech/common-ui/forms/filters';
24
import TimeframePicker from '@northern.tech/common-ui/forms/timeframe-picker';
25
import { InfoHintContainer } from '@northern.tech/common-ui/info-hint';
26
import Loader from '@northern.tech/common-ui/loader';
27
import { HELPTOOLTIPS, MenderHelpTooltip } from '@northern.tech/helptips/helptooltips';
28
import { AUDIT_LOGS_TYPES, BEGINNING_OF_TIME, BENEFITS, SORTING_OPTIONS, SP_AUDIT_LOGS_TYPES, TIMEOUTS } from '@northern.tech/store/constants';
29
import {
30
  getAuditLog,
31
  getAuditLogEntry,
32
  getAuditLogSelectionState,
33
  getCurrentSession,
34
  getGroupNames,
35
  getIsServiceProvider,
36
  getTenantCapabilities,
37
  getUserCapabilities
38
} from '@northern.tech/store/selectors';
39
import { getAuditLogs, getAuditLogsCsvLink, getUserList, setAuditlogsState } from '@northern.tech/store/thunks';
40
import { createDownload, getISOStringBoundaries } from '@northern.tech/utils/helpers';
41
import { useLocationParams } from '@northern.tech/utils/liststatehook';
42
import dayjs from 'dayjs';
43

44
import historyImage from '../../../assets/img/history.png';
45
import AuditLogsList from './auditlogslist';
46

47
const detailsMap = {
3✔
48
  Deployment: 'to device group',
49
  User: 'email'
50
};
51

52
const useStyles = makeStyles()(theme => ({
3✔
53
  filters: {
54
    backgroundColor: theme.palette.background.lightgrey,
55
    padding: '0px 25px 5px',
56
    display: 'grid',
57
    gridTemplateColumns: '400px 250px 250px 1fr',
58
    gridColumnGap: theme.spacing(2),
59
    gridRowGap: theme.spacing(2)
60
  },
61
  filterReset: { alignSelf: 'flex-end', marginBottom: 5 },
62
  timeframe: { gridColumnStart: 2, gridColumnEnd: 4, marginLeft: 7.5 },
63
  typeDetails: { marginRight: 15, marginTop: theme.spacing(2) },
64
  upgradeNote: { marginTop: '5vh', placeSelf: 'center' }
65
}));
66

67
const getOptionLabel = option => option.title ?? option.email ?? option;
116✔
68

69
const renderOption = (props, option) => <li {...props}>{getOptionLabel(option)}</li>;
17✔
70

71
const isUserOptionEqualToValue = ({ email, id }, value) => id === value || email === value || email === value?.email;
3!
72

73
const autoSelectProps = {
3✔
74
  autoSelect: true,
75
  filterSelectedOptions: true,
76
  getOptionLabel,
77
  handleHomeEndKeys: true,
78
  renderOption
79
};
80

81
const locationDefaults = { sort: { direction: SORTING_OPTIONS.desc } };
3✔
82

83
export const AuditLogs = props => {
3✔
84
  const [csvLoading, setCsvLoading] = useState(false);
11✔
85

86
  const [date] = useState(getISOStringBoundaries(new Date()));
10✔
87
  const { start: today, end: tonight } = date;
10✔
88

89
  const isInitialized = useRef();
10✔
90
  const [locationParams, setLocationParams] = useLocationParams('auditlogs', { today, tonight, defaults: locationDefaults });
10✔
91
  const { classes } = useStyles();
10✔
92
  const dispatch = useDispatch();
10✔
93
  const events = useSelector(getAuditLog);
10✔
94
  const eventItem = useSelector(getAuditLogEntry);
10✔
95
  const groups = useSelector(getGroupNames);
10✔
96
  const selectionState = useSelector(getAuditLogSelectionState);
10✔
97
  const userCapabilities = useSelector(getUserCapabilities);
10✔
98
  const tenantCapabilities = useSelector(getTenantCapabilities);
10✔
99
  const users = useSelector(state => state.users.byId);
34✔
100
  const { canReadUsers } = userCapabilities;
10✔
101
  const { hasAuditlogs } = tenantCapabilities;
10✔
102
  const [detailsReset, setDetailsReset] = useState('');
10✔
103
  const [dirtyField, setDirtyField] = useState('');
10✔
104
  const { token } = useSelector(getCurrentSession);
10✔
105
  const isSP = useSelector(getIsServiceProvider);
10✔
106

107
  const { detail, isLoading, perPage, endDate, user, sort, startDate, total, type } = selectionState;
10✔
108

109
  useEffect(() => {
10✔
110
    if (!hasAuditlogs || !isInitialized.current) {
6!
111
      return;
6✔
112
    }
UNCOV
113
    setLocationParams({ pageState: selectionState });
×
114
    // eslint-disable-next-line react-hooks/exhaustive-deps
115
  }, [detail, endDate, hasAuditlogs, perPage, selectionState.page, selectionState.selectedId, setLocationParams, startDate, type, user]);
116

117
  useEffect(() => {
10✔
118
    if (!isInitialized.current) {
3!
119
      return;
3✔
120
    }
UNCOV
121
    setDetailsReset('detail');
×
UNCOV
122
    setTimeout(() => setDetailsReset(''), TIMEOUTS.debounceShort);
×
123
  }, [type?.value]);
124

125
  useEffect(() => {
10✔
126
    if (canReadUsers) {
3!
127
      dispatch(getUserList());
3✔
128
    }
129
  }, [canReadUsers, dispatch]);
130

131
  const initAuditlogState = useCallback(
10✔
132
    (result, state) => {
133
      const { detail, endDate, startDate, type, user } = state;
2✔
134
      const resultList = result ? Object.values(result.events) : [];
2!
135
      if (resultList.length && startDate === today) {
2✔
136
        let newStartDate = new Date(resultList[resultList.length - 1].time);
1✔
137
        const { start } = getISOStringBoundaries(newStartDate);
1✔
138
        state.startDate = start;
1✔
139
      }
140
      dispatch(setAuditlogsState(state));
2✔
141
      setTimeout(() => {
2✔
142
        let field = Object.entries({ detail, type, user }).reduce((accu, [key, value]) => (accu || value ? key : accu), '');
6!
143
        field = field || (endDate !== tonight ? 'endDate' : field);
2!
144
        field = field || (state.startDate !== today ? 'startDate' : field);
2!
145
        setDirtyField(field);
2✔
146
      }, TIMEOUTS.debounceDefault);
147
      // the timeout here is slightly longer than the debounce in the filter component, otherwise the population of the filters with the url state would trigger a reset to page 1
148
      setTimeout(() => (isInitialized.current = true), TIMEOUTS.oneSecond + TIMEOUTS.debounceDefault);
2✔
149
    },
150
    [dispatch, today, tonight]
151
  );
152

153
  useEffect(() => {
10✔
154
    if (!hasAuditlogs || isInitialized.current !== undefined) {
3✔
155
      return;
1✔
156
    }
157
    isInitialized.current = false;
2✔
158
    const { id, open, detail, endDate, startDate, type, user } = locationParams;
2✔
159
    let state = { ...locationParams };
2✔
160
    if (id && Boolean(open)) {
2!
161
      state.selectedId = id[0];
×
UNCOV
162
      const [eventAction, eventTime] = atob(state.selectedId).split('|');
×
UNCOV
163
      if (eventTime && !events.some(item => item.time === eventTime && item.action === eventAction)) {
×
UNCOV
164
        const { start, end } = getISOStringBoundaries(new Date(eventTime));
×
UNCOV
165
        state.endDate = end;
×
UNCOV
166
        state.startDate = start;
×
167
      }
UNCOV
168
      let field = endDate !== tonight ? 'endDate' : '';
×
UNCOV
169
      field = field || (startDate !== today ? 'startDate' : field);
×
UNCOV
170
      setDirtyField(field);
×
171
      // the timeout here is slightly longer than the debounce in the filter component, otherwise the population of the filters with the url state would trigger a reset to page 1
UNCOV
172
      dispatch(setAuditlogsState(state)).then(() => setTimeout(() => (isInitialized.current = true), TIMEOUTS.oneSecond + TIMEOUTS.debounceDefault));
×
UNCOV
173
      return;
×
174
    }
175
    dispatch(getAuditLogs({ page: state.page ?? 1, perPage: 50, startDate: startDate !== today ? startDate : BEGINNING_OF_TIME, endDate, user, type, detail }))
2✔
176
      .unwrap()
177
      .then(({ payload: result }) => initAuditlogState(result, state));
2✔
178
    // eslint-disable-next-line react-hooks/exhaustive-deps
179
  }, [dispatch, hasAuditlogs, JSON.stringify(events), JSON.stringify(locationParams), initAuditlogState, today, tonight]);
180

181
  const createCsvDownload = () => {
10✔
182
    setCsvLoading(true);
1✔
183
    dispatch(getAuditLogsCsvLink())
1✔
184
      .unwrap()
185
      .then(address => {
186
        createDownload(encodeURI(address), `Mender-AuditLog-${dayjs(startDate).format('YYYY-MM-DD')}-${dayjs(endDate).format('YYYY-MM-DD')}.csv`, token);
1✔
187
        setCsvLoading(false);
1✔
188
      });
189
  };
190

191
  const onChangeSorting = () => {
10✔
UNCOV
192
    const currentSorting = sort.direction === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
×
UNCOV
193
    dispatch(setAuditlogsState({ page: 1, sort: { direction: currentSorting } }));
×
194
  };
195

196
  const onChangePagination = (page, currentPerPage = perPage) => dispatch(setAuditlogsState({ page, perPage: currentPerPage }));
10!
197

198
  const onFiltersChange = useCallback(
10✔
199
    ({ endDate, detail, startDate, user, type }) => {
200
      if (!isInitialized.current) {
3!
201
        return;
3✔
202
      }
UNCOV
203
      const selectedUser = Object.values(users).find(item => isUserOptionEqualToValue(item, user));
×
UNCOV
204
      dispatch(setAuditlogsState({ page: 1, detail, startDate, endDate, user: selectedUser, type }));
×
205
    },
206
    // eslint-disable-next-line react-hooks/exhaustive-deps
207
    [dispatch, JSON.stringify(users)]
208
  );
209

210
  const typeOptionsMap = {
10✔
211
    Deployment: groups,
212
    User: Object.values(users)
213
  };
214
  const detailOptions = typeOptionsMap[type?.title] ?? [];
10✔
215

216
  return (
10✔
217
    <div className="fadeIn margin-left flexbox column" style={{ marginRight: '5%' }}>
218
      <div className="flexbox center-aligned">
219
        <h3 className="margin-right-small">Audit log</h3>
220
        <InfoHintContainer>
221
          <EnterpriseNotification id={BENEFITS.auditlog.id} />
222
        </InfoHintContainer>
223
      </div>
224
      <ClickFilter disabled={!hasAuditlogs}>
225
        <Filters
226
          initialValues={{ startDate, endDate, user, type, detail }}
227
          defaultValues={{ startDate: today, endDate: tonight, user: '', type: null, detail: '' }}
228
          fieldResetTrigger={detailsReset}
229
          dirtyField={dirtyField}
230
          clearDirty={setDirtyField}
231
          filters={[
232
            {
233
              key: 'user',
234
              title: 'Performed by',
235
              Component: ControlledAutoComplete,
236
              componentProps: {
237
                ...autoSelectProps,
238
                freeSolo: true,
239
                isOptionEqualToValue: isUserOptionEqualToValue,
240
                options: Object.values(users),
241
                renderInput: params => <TextField {...params} placeholder="Select a user" InputProps={{ ...params.InputProps }} />
23✔
242
              }
243
            },
244
            {
245
              key: 'type',
246
              title: 'Filter by changes',
247
              Component: ControlledAutoComplete,
248
              componentProps: {
249
                ...autoSelectProps,
250
                options: isSP ? SP_AUDIT_LOGS_TYPES : AUDIT_LOGS_TYPES,
10!
UNCOV
251
                isOptionEqualToValue: (option, value) => option.value === value.value && option.object_type === value.object_type,
×
252
                renderInput: params => <TextField {...params} placeholder="Type" InputProps={{ ...params.InputProps }} />
29✔
253
              }
254
            },
255
            {
256
              key: 'detail',
257
              title: '',
258
              Component: ControlledAutoComplete,
259
              componentProps: {
260
                ...autoSelectProps,
261
                freeSolo: true,
262
                options: detailOptions,
263
                disabled: !type,
264
                renderInput: params => <TextField {...params} placeholder={detailsMap[type] || '-'} InputProps={{ ...params.InputProps }} />
23✔
265
              }
266
            },
267
            {
268
              key: 'timeframe',
269
              title: 'Start time',
270
              Component: TimeframePicker,
271
              componentProps: {
272
                tonight
273
              }
274
            }
275
          ]}
276
          onChange={onFiltersChange}
277
        />
278
      </ClickFilter>
279
      <div className="flexbox center-aligned" style={{ justifyContent: 'flex-end' }}>
280
        <Loader show={csvLoading} />
281
        <Button variant="contained" color="secondary" disabled={csvLoading || !total} onClick={createCsvDownload} style={{ marginLeft: 15 }}>
18✔
282
          Download results as csv
283
        </Button>
284
      </div>
285
      {!!total && (
20✔
286
        <AuditLogsList
287
          {...props}
288
          items={events}
289
          eventItem={eventItem}
290
          loading={isLoading}
291
          onChangePage={onChangePagination}
UNCOV
292
          onChangeRowsPerPage={newPerPage => onChangePagination(1, newPerPage)}
×
293
          onChangeSorting={onChangeSorting}
294
          selectionState={selectionState}
295
          setAuditlogsState={state => dispatch(setAuditlogsState(state))}
1✔
296
          userCapabilities={userCapabilities}
297
        />
298
      )}
299
      {!(isLoading || total) && hasAuditlogs && (
28!
300
        <div className="dashboard-placeholder">
301
          <p>No log entries were found.</p>
302
          <p>Try adjusting the filters.</p>
303
          <img src={historyImage} alt="Past" />
304
        </div>
305
      )}
306
      {!hasAuditlogs && (
11✔
307
        <div className={`dashboard-placeholder flexbox ${classes.upgradeNote}`}>
308
          <DefaultUpgradeNotification className="margin-right-small" />
309
          <MenderHelpTooltip id={HELPTOOLTIPS.auditlogExplanation.id} />
310
        </div>
311
      )}
312
    </div>
313
  );
314
};
315

316
export default AuditLogs;
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