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

mendersoftware / gui / 963002358

pending completion
963002358

Pull #3870

gitlab-ci

mzedel
chore: cleaned up left over onboarding tooltips & aligned with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3870: MEN-5413

4348 of 6319 branches covered (68.81%)

95 of 122 new or added lines in 24 files covered. (77.87%)

1734 existing lines in 160 files now uncovered.

8174 of 9951 relevant lines covered (82.14%)

178.12 hits per line

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

62.94
/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, useRef, useState } from 'react';
15
import { useDispatch, useSelector } 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 { AUDIT_LOGS_TYPES } from '../../constants/organizationConstants';
28
import { createDownload, getISOStringBoundaries } from '../../helpers';
29
import { getGroupNames, getTenantCapabilities, getUserCapabilities } from '../../selectors';
30
import { useDebounce } from '../../utils/debouncehook';
31
import { useLocationParams } from '../../utils/liststatehook';
32
import Loader from '../common/loader';
33
import TimeframePicker from '../common/timeframe-picker';
34
import TimerangePicker from '../common/timerange-picker';
35
import AuditLogsList from './auditlogslist';
36

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

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

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

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

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

60
const locationDefaults = { sort: { direction: SORTING_OPTIONS.desc } };
4✔
61

62
export const AuditLogs = props => {
4✔
63
  const navigate = useNavigate();
14✔
64
  const [csvLoading, setCsvLoading] = useState(false);
13✔
65

66
  const [date] = useState(getISOStringBoundaries(new Date()));
13✔
67
  const { start: today, end: tonight } = date;
13✔
68

69
  const [detailValue, setDetailValue] = useState(null);
13✔
70
  const [userValue, setUserValue] = useState(null);
13✔
71
  const [typeValue, setTypeValue] = useState(null);
13✔
72
  const isInitialized = useRef();
13✔
73
  const [locationParams, setLocationParams] = useLocationParams('auditlogs', { today, tonight, defaults: locationDefaults });
13✔
74
  const { classes } = useStyles();
13✔
75
  const dispatch = useDispatch();
13✔
76
  const events = useSelector(state => state.organization.auditlog.events);
24✔
77
  const groups = useSelector(getGroupNames);
13✔
78
  const selectionState = useSelector(state => state.organization.auditlog.selectionState);
24✔
79
  const userCapabilities = useSelector(getUserCapabilities);
13✔
80
  const tenantCapabilities = useSelector(getTenantCapabilities);
13✔
81
  const users = useSelector(state => state.users.byId);
24✔
82
  const { canReadUsers } = userCapabilities;
13✔
83
  const { hasAuditlogs } = tenantCapabilities;
13✔
84

85
  const debouncedDetail = useDebounce(detailValue, TIMEOUTS.debounceDefault);
13✔
86
  const debouncedType = useDebounce(typeValue, TIMEOUTS.debounceDefault);
13✔
87
  const debouncedUser = useDebounce(userValue, TIMEOUTS.debounceDefault);
13✔
88

89
  const { detail, isLoading, perPage, endDate, user, reset: resetList, sort, startDate, total, type } = selectionState;
13✔
90

91
  useEffect(() => {
13✔
92
    if (!hasAuditlogs || !isInitialized.current) {
10!
93
      return;
10✔
94
    }
UNCOV
95
    setLocationParams({ pageState: selectionState });
×
96
    // eslint-disable-next-line react-hooks/exhaustive-deps
97
  }, [detail, endDate, hasAuditlogs, perPage, selectionState.page, selectionState.selectedId, setLocationParams, startDate, type, user]);
98

99
  useEffect(() => {
13✔
100
    if (!hasAuditlogs || !isInitialized.current) {
3!
101
      return;
3✔
102
    }
UNCOV
103
    dispatch(setAuditlogsState({ page: 1, detail: debouncedDetail, type: debouncedType, user: debouncedUser }));
×
104
  }, [debouncedDetail, debouncedType, debouncedUser, dispatch, hasAuditlogs]);
105

106
  useEffect(() => {
13✔
107
    if (canReadUsers) {
3!
108
      dispatch(getUserList());
3✔
109
    }
110
  }, [canReadUsers, dispatch]);
111

112
  useEffect(() => {
13✔
113
    const user = users[debouncedUser?.id];
3✔
114
    if (debouncedUser?.id || !user) {
3!
115
      return;
3✔
116
    }
UNCOV
117
    setUserValue(user);
×
118
    // eslint-disable-next-line react-hooks/exhaustive-deps
119
  }, [debouncedUser, JSON.stringify(users)]);
120

121
  useEffect(() => {
13✔
122
    if (!hasAuditlogs) {
4!
UNCOV
123
      return;
×
124
    }
125
    let state = { ...locationParams };
4✔
126
    if (locationParams.id && Boolean(locationParams.open)) {
4!
UNCOV
127
      state.selectedId = locationParams.id[0];
×
UNCOV
128
      const [eventAction, eventTime] = atob(state.selectedId).split('|');
×
UNCOV
129
      if (eventTime && !events.some(item => item.time === eventTime && item.action === eventAction)) {
×
UNCOV
130
        const { start, end } = getISOStringBoundaries(new Date(eventTime));
×
UNCOV
131
        state.endDate = end;
×
UNCOV
132
        state.startDate = start;
×
133
      }
134
    }
135
    dispatch(setAuditlogsState(state));
4✔
136
    Object.entries({ detail: setDetailValue, user: setUserValue, type: setTypeValue }).map(([key, setter]) => (state[key] ? setter(state[key]) : undefined));
12!
137
    setTimeout(() => (isInitialized.current = true), TIMEOUTS.debounceDefault);
4✔
138
    // eslint-disable-next-line react-hooks/exhaustive-deps
139
  }, [dispatch, hasAuditlogs, JSON.stringify(events), JSON.stringify(locationParams)]);
140

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

159
  const createCsvDownload = () => {
13✔
160
    setCsvLoading(true);
1✔
161
    dispatch(getAuditLogsCsvLink()).then(address => {
1✔
162
      createDownload(
1✔
163
        encodeURI(address),
164
        `Mender-AuditLog-${moment(startDate).format(moment.HTML5_FMT.DATE)}-${moment(endDate).format(moment.HTML5_FMT.DATE)}.csv`
165
      );
166
      setCsvLoading(false);
1✔
167
    });
168
  };
169

170
  const onChangeSorting = () => {
13✔
UNCOV
171
    const currentSorting = sort.direction === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
×
UNCOV
172
    dispatch(setAuditlogsState({ page: 1, sort: { direction: currentSorting } }));
×
173
  };
174

175
  const onUserFilterChange = (e, value, reason) => {
13✔
UNCOV
176
    if (!e || reason === 'blur') {
×
UNCOV
177
      return;
×
178
    }
UNCOV
179
    setUserValue(value);
×
180
  };
181

182
  const onTypeFilterChange = (e, value, reason) => {
13✔
UNCOV
183
    if (!e || reason === 'blur') {
×
UNCOV
184
      return;
×
185
    }
UNCOV
186
    if (!value) {
×
UNCOV
187
      setDetailValue(null);
×
188
    }
UNCOV
189
    setTypeValue(value);
×
190
  };
191

192
  const onDetailFilterChange = (e, value) => {
13✔
UNCOV
193
    if (!e) {
×
UNCOV
194
      return;
×
195
    }
UNCOV
196
    setDetailValue(value);
×
197
  };
198

199
  const onTimeFilterChange = (currentStartDate = startDate, currentEndDate = endDate) =>
13!
200
    dispatch(setAuditlogsState({ page: 1, startDate: currentStartDate, endDate: currentEndDate }));
1✔
201

202
  const onChangePagination = (page, currentPerPage = perPage) => dispatch(setAuditlogsState({ page, perPage: currentPerPage }));
13!
203

204
  const typeOptionsMap = {
13✔
205
    Deployment: groups,
206
    User: Object.values(users),
207
    Device: []
208
  };
209
  const detailOptions = typeOptionsMap[type?.title] ?? [];
13✔
210

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

298
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