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

mendersoftware / gui / 897020239

pending completion
897020239

Pull #3785

gitlab-ci

mzedel
fix: fixed issues that prevented navigating to a filtered audit log from device details & from within the audit log

Ticket: MEN-6510
Changelog: Title
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3785: MEN-6510 + MEN-6512

4396 of 6391 branches covered (68.78%)

13 of 19 new or added lines in 2 files covered. (68.42%)

1700 existing lines in 165 files now uncovered.

8065 of 9787 relevant lines covered (82.41%)

127.45 hits per line

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

59.75
/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, { useEffect, useState } from 'react';
15
import { connect } from 'react-redux';
16
import { useNavigate } from 'react-router-dom';
17

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

21
import moment from 'moment';
22

23
import historyImage from '../../../assets/img/history.png';
24
import { getAuditLogsCsvLink, setAuditlogsState } from '../../actions/organizationActions';
25
import { getUserList } from '../../actions/userActions';
26
import { SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
27
import { ALL_DEVICES, UNGROUPED_GROUP } from '../../constants/deviceConstants';
28
import { AUDIT_LOGS_TYPES } from '../../constants/organizationConstants';
29
import { createDownload, getISOStringBoundaries } from '../../helpers';
30
import { getTenantCapabilities, getUserCapabilities } from '../../selectors';
31
import { useDebounce } from '../../utils/debouncehook';
32
import { useLocationParams } from '../../utils/liststatehook';
33
import Loader from '../common/loader';
34
import TimeframePicker from '../common/timeframe-picker';
35
import TimerangePicker from '../common/timerange-picker';
36
import AuditLogsList from './auditlogslist';
37

38
const detailsMap = {
4✔
39
  Deployment: 'to device group',
40
  User: 'email'
41
};
42

43
const useStyles = makeStyles()(theme => ({
4✔
44
  filters: {
45
    backgroundColor: theme.palette.background.lightgrey
46
  }
47
}));
48

49
const getOptionLabel = option => option.title || option.email || option;
4!
50

51
const renderOption = (props, option) => <li {...props}>{getOptionLabel(option)}</li>;
4✔
52

53
const autoSelectProps = {
4✔
54
  autoSelect: true,
55
  filterSelectedOptions: true,
56
  getOptionLabel,
57
  handleHomeEndKeys: true,
58
  renderOption
59
};
60

61
export const AuditLogs = ({
4✔
62
  events,
63
  getAuditLogsCsvLink,
64
  getUserList,
65
  groups,
66
  selectionState,
67
  setAuditlogsState,
68
  tenantCapabilities,
69
  userCapabilities,
70
  users,
71
  ...props
72
}) => {
73
  const navigate = useNavigate();
10✔
74
  const [csvLoading, setCsvLoading] = useState(false);
10✔
75

76
  const [date] = useState(getISOStringBoundaries(new Date()));
10✔
77
  const { start: today, end: tonight } = date;
10✔
78

79
  const [detailValue, setDetailValue] = useState(null);
10✔
80
  const [userValue, setUserValue] = useState(null);
10✔
81
  const [typeValue, setTypeValue] = useState(null);
10✔
82
  const [locationParams, setLocationParams] = useLocationParams('auditlogs', { today, tonight, defaults: { sort: { direction: SORTING_OPTIONS.desc } } });
10✔
83
  const { canReadUsers } = userCapabilities;
10✔
84
  const { hasAuditlogs } = tenantCapabilities;
10✔
85
  const { classes } = useStyles();
10✔
86

87
  const debouncedDetail = useDebounce(detailValue, TIMEOUTS.debounceDefault);
10✔
88
  const debouncedType = useDebounce(typeValue, TIMEOUTS.debounceDefault);
10✔
89
  const debouncedUser = useDebounce(userValue, TIMEOUTS.debounceDefault);
10✔
90

91
  const { detail, isLoading, perPage, endDate, user, reset: resetList, sort, startDate, total, type } = selectionState;
10✔
92

93
  useEffect(() => {
10✔
94
    if (!hasAuditlogs) {
3!
UNCOV
95
      return;
×
96
    }
97
    let state = { ...locationParams, reset: !resetList };
3✔
98
    if (locationParams.id && Boolean(locationParams.open)) {
3!
UNCOV
99
      state.selectedId = locationParams.id[0];
×
UNCOV
100
      const [eventAction, eventTime] = atob(state.selectedId).split('|');
×
UNCOV
101
      if (eventTime && !events.some(item => item.time === eventTime && item.action === eventAction)) {
×
UNCOV
102
        const { start, end } = getISOStringBoundaries(new Date(eventTime));
×
UNCOV
103
        state.endDate = end;
×
UNCOV
104
        state.startDate = start;
×
105
      }
106
    }
107
    setAuditlogsState(state);
3✔
108
    Object.entries({ detail: setDetailValue, user: setUserValue, type: setTypeValue }).map(([key, setter]) => (state[key] ? setter(state[key]) : undefined));
9!
109
  }, [hasAuditlogs]);
110

111
  useEffect(() => {
10✔
112
    if (!hasAuditlogs) {
3!
UNCOV
113
      return;
×
114
    }
115
    setAuditlogsState({ page: 1, detail: debouncedDetail, type: debouncedType, user: debouncedUser });
3✔
116
  }, [debouncedDetail, debouncedType, debouncedUser, hasAuditlogs]);
117

118
  useEffect(() => {
10✔
119
    if (canReadUsers) {
3!
120
      getUserList();
3✔
121
    }
122
  }, [canReadUsers]);
123

124
  useEffect(() => {
10✔
125
    if (!hasAuditlogs) {
3!
UNCOV
126
      return;
×
127
    }
128
    setLocationParams({ pageState: selectionState });
3✔
129
  }, [detail, endDate, hasAuditlogs, JSON.stringify(sort), perPage, selectionState.page, selectionState.selectedId, startDate, type, user]);
130

131
  useEffect(() => {
10✔
132
    const user = users[debouncedUser];
3✔
133
    if (debouncedUser?.id || !user) {
3!
134
      return;
3✔
135
    }
NEW
136
    setUserValue(user);
×
137
  }, [debouncedUser, JSON.stringify(users)]);
138

139
  const reset = () => {
10✔
140
    setAuditlogsState({
2✔
141
      detail: null,
142
      endDate: tonight,
143
      page: 1,
144
      reset: !resetList,
145
      startDate: today,
146
      type: null,
147
      user: null
148
    });
149
    setDetailValue(null);
2✔
150
    setTypeValue(null);
2✔
151
    setUserValue(null);
2✔
152
    navigate('/auditlog');
2✔
153
  };
154

155
  const createCsvDownload = () => {
10✔
156
    setCsvLoading(true);
1✔
157
    getAuditLogsCsvLink().then(address => {
1✔
158
      createDownload(
1✔
159
        encodeURI(address),
160
        `Mender-AuditLog-${moment(startDate).format(moment.HTML5_FMT.DATE)}-${moment(endDate).format(moment.HTML5_FMT.DATE)}.csv`
161
      );
162
      setCsvLoading(false);
1✔
163
    });
164
  };
165

166
  const onChangeSorting = () => {
10✔
UNCOV
167
    const currentSorting = sort.direction === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
×
UNCOV
168
    setAuditlogsState({ page: 1, sort: { direction: currentSorting } });
×
169
  };
170

171
  const onUserFilterChange = (e, value, reason) => {
10✔
UNCOV
172
    if (!e || reason === 'blur') {
×
UNCOV
173
      return;
×
174
    }
NEW
175
    setUserValue(value);
×
176
  };
177

178
  const onTypeFilterChange = (e, value, reason) => {
10✔
UNCOV
179
    if (!e || reason === 'blur') {
×
UNCOV
180
      return;
×
181
    }
UNCOV
182
    if (!value) {
×
NEW
183
      setDetailValue(null);
×
184
    }
NEW
185
    setTypeValue(value);
×
186
  };
187

188
  const onDetailFilterChange = (e, value) => {
10✔
UNCOV
189
    if (!e) {
×
UNCOV
190
      return;
×
191
    }
NEW
192
    setDetailValue(value);
×
193
  };
194

195
  const onTimeFilterChange = (currentStartDate = startDate, currentEndDate = endDate) =>
10!
196
    setAuditlogsState({ page: 1, startDate: currentStartDate, endDate: currentEndDate });
1✔
197

198
  const onChangePagination = (page, currentPerPage = perPage) => setAuditlogsState({ page, perPage: currentPerPage });
10!
199

200
  const typeOptionsMap = {
10✔
201
    Deployment: groups,
202
    User: Object.values(users),
203
    Device: []
204
  };
205
  const detailOptions = typeOptionsMap[type?.title] ?? [];
10✔
206

207
  return (
10✔
208
    <div className="fadeIn margin-left flexbox column" style={{ marginRight: '5%' }}>
209
      <h3>Audit log</h3>
210
      <div className={`auditlogs-filters margin-bottom margin-top-small ${classes.filters}`}>
211
        <Autocomplete
212
          {...autoSelectProps}
213
          id="audit-log-user-selection"
214
          freeSolo
215
          options={Object.values(users)}
216
          onChange={onUserFilterChange}
NEW
217
          isOptionEqualToValue={({ email, id }, value) => id === value || email === value || email === value.email}
×
218
          value={userValue}
219
          renderInput={params => (
220
            <TextField
10✔
221
              {...params}
222
              label="Filter by user"
223
              placeholder="Select a user"
224
              InputLabelProps={{ shrink: true }}
225
              InputProps={{ ...params.InputProps }}
226
            />
227
          )}
228
          style={{ maxWidth: 250 }}
229
        />
230
        <Autocomplete
231
          {...autoSelectProps}
232
          id="audit-log-type-selection"
233
          onChange={onTypeFilterChange}
234
          options={AUDIT_LOGS_TYPES}
235
          renderInput={params => (
236
            <TextField {...params} label="Filter by change" placeholder="Type" InputLabelProps={{ shrink: true }} InputProps={{ ...params.InputProps }} />
10✔
237
          )}
238
          style={{ marginLeft: 7.5 }}
239
          value={typeValue}
240
        />
241
        <Autocomplete
242
          {...autoSelectProps}
243
          id="audit-log-type-details-selection"
244
          key={`audit-log-type-details-selection-${type}`}
245
          disabled={!type}
246
          freeSolo
247
          value={detailValue}
248
          onInputChange={onDetailFilterChange}
249
          options={detailOptions}
250
          renderInput={params => <TextField {...params} placeholder={detailsMap[type] || '-'} InputProps={{ ...params.InputProps }} />}
10✔
251
          style={{ marginRight: 15, marginTop: 16 }}
252
        />
253
        <div />
254
        <TimerangePicker endDate={endDate} onChange={onTimeFilterChange} startDate={startDate} />
255
        <div style={{ gridColumnStart: 2, gridColumnEnd: 4, marginLeft: 7.5 }}>
256
          <TimeframePicker onChange={onTimeFilterChange} endDate={endDate} startDate={startDate} tonight={tonight} />
257
        </div>
258
        {!!(user || type || detail || startDate !== today || endDate !== tonight) && (
60!
259
          <span className="link margin-bottom-small" onClick={reset} style={{ alignSelf: 'flex-end' }}>
260
            clear filter
261
          </span>
262
        )}
263
      </div>
264
      <div className="flexbox center-aligned" style={{ justifyContent: 'flex-end' }}>
265
        <Loader show={csvLoading} />
266
        <Button variant="contained" color="secondary" disabled={csvLoading || !total} onClick={createCsvDownload} style={{ marginLeft: 15 }}>
19✔
267
          Download results as csv
268
        </Button>
269
      </div>
270
      {!!total && (
20✔
271
        <AuditLogsList
272
          {...props}
273
          items={events}
274
          loading={isLoading}
275
          onChangePage={onChangePagination}
UNCOV
276
          onChangeRowsPerPage={newPerPage => onChangePagination(1, newPerPage)}
×
277
          onChangeSorting={onChangeSorting}
278
          selectionState={selectionState}
279
          setAuditlogsState={setAuditlogsState}
280
          userCapabilities={userCapabilities}
281
        />
282
      )}
283
      {!(isLoading || total) && (
30!
284
        <div className="dashboard-placeholder">
285
          <p>No log entries were found.</p>
286
          <p>Try adjusting the filters.</p>
287
          <img src={historyImage} alt="Past" />
288
        </div>
289
      )}
290
    </div>
291
  );
292
};
293

294
const actionCreators = { getAuditLogsCsvLink, getUserList, setAuditlogsState };
4✔
295

296
const mapStateToProps = state => {
4✔
297
  // eslint-disable-next-line no-unused-vars
298
  const { [UNGROUPED_GROUP.id]: ungrouped, ...groups } = state.devices.groups.byId;
3✔
299
  return {
3✔
300
    events: state.organization.auditlog.events,
301
    groups: [ALL_DEVICES, ...Object.keys(groups).sort()],
302
    selectionState: state.organization.auditlog.selectionState,
303
    userCapabilities: getUserCapabilities(state),
304
    tenantCapabilities: getTenantCapabilities(state),
305
    users: state.users.byId
306
  };
307
};
308

309
export default connect(mapStateToProps, actionCreators)(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