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

mendersoftware / gui / 963124858

pending completion
963124858

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

4368 of 6355 branches covered (68.73%)

91 of 118 new or added lines in 22 files covered. (77.12%)

1753 existing lines in 162 files now uncovered.

8246 of 10042 relevant lines covered (82.12%)

193.52 hits per line

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

62.36
/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 { BENEFITS, 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 EnterpriseNotification, { DefaultUpgradeNotification } from '../common/enterpriseNotification';
33
import ClickFilter from '../common/forms/clickfilter';
34
import { InfoHintContainer } from '../common/info-hint';
35
import Loader from '../common/loader';
36
import TimeframePicker from '../common/timeframe-picker';
37
import TimerangePicker from '../common/timerange-picker';
38
import { HELPTOOLTIPS, MenderHelpTooltip } from '../helptips/helptooltips';
39
import AuditLogsList from './auditlogslist';
40

41
const detailsMap = {
4✔
42
  Deployment: 'to device group',
43
  User: 'email'
44
};
45

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

61
const getOptionLabel = option => option.title || option.email || option;
4!
62

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

65
const autoSelectProps = {
4✔
66
  autoSelect: true,
67
  filterSelectedOptions: true,
68
  getOptionLabel,
69
  handleHomeEndKeys: true,
70
  renderOption
71
};
72

73
const locationDefaults = { sort: { direction: SORTING_OPTIONS.desc } };
4✔
74

75
export const AuditLogs = props => {
4✔
76
  const navigate = useNavigate();
14✔
77
  const [csvLoading, setCsvLoading] = useState(false);
13✔
78

79
  const [date] = useState(getISOStringBoundaries(new Date()));
13✔
80
  const { start: today, end: tonight } = date;
13✔
81

82
  const [detailValue, setDetailValue] = useState(null);
13✔
83
  const [userValue, setUserValue] = useState(null);
13✔
84
  const [typeValue, setTypeValue] = useState(null);
13✔
85
  const isInitialized = useRef();
13✔
86
  const [locationParams, setLocationParams] = useLocationParams('auditlogs', { today, tonight, defaults: locationDefaults });
13✔
87
  const { classes } = useStyles();
13✔
88
  const dispatch = useDispatch();
13✔
89
  const events = useSelector(state => state.organization.auditlog.events);
24✔
90
  const groups = useSelector(getGroupNames);
13✔
91
  const selectionState = useSelector(state => state.organization.auditlog.selectionState);
24✔
92
  const userCapabilities = useSelector(getUserCapabilities);
13✔
93
  const tenantCapabilities = useSelector(getTenantCapabilities);
13✔
94
  const users = useSelector(state => state.users.byId);
24✔
95
  const { canReadUsers } = userCapabilities;
13✔
96
  const { hasAuditlogs } = tenantCapabilities;
13✔
97

98
  const debouncedDetail = useDebounce(detailValue, TIMEOUTS.debounceDefault);
13✔
99
  const debouncedType = useDebounce(typeValue, TIMEOUTS.debounceDefault);
13✔
100
  const debouncedUser = useDebounce(userValue, TIMEOUTS.debounceDefault);
13✔
101

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

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

112
  useEffect(() => {
13✔
113
    if (!hasAuditlogs || !isInitialized.current) {
3!
114
      return;
3✔
115
    }
UNCOV
116
    dispatch(setAuditlogsState({ page: 1, detail: debouncedDetail, type: debouncedType, user: debouncedUser }));
×
117
  }, [debouncedDetail, debouncedType, debouncedUser, dispatch, hasAuditlogs]);
118

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

125
  useEffect(() => {
13✔
126
    const user = users[debouncedUser?.id];
3✔
127
    if (debouncedUser?.id || !user) {
3!
128
      return;
3✔
129
    }
UNCOV
130
    setUserValue(user);
×
131
    // eslint-disable-next-line react-hooks/exhaustive-deps
132
  }, [debouncedUser, JSON.stringify(users)]);
133

134
  useEffect(() => {
13✔
135
    if (!hasAuditlogs) {
4!
UNCOV
136
      return;
×
137
    }
138
    let state = { ...locationParams };
4✔
139
    if (locationParams.id && Boolean(locationParams.open)) {
4!
UNCOV
140
      state.selectedId = locationParams.id[0];
×
UNCOV
141
      const [eventAction, eventTime] = atob(state.selectedId).split('|');
×
UNCOV
142
      if (eventTime && !events.some(item => item.time === eventTime && item.action === eventAction)) {
×
UNCOV
143
        const { start, end } = getISOStringBoundaries(new Date(eventTime));
×
UNCOV
144
        state.endDate = end;
×
UNCOV
145
        state.startDate = start;
×
146
      }
147
    }
148
    dispatch(setAuditlogsState(state));
4✔
149
    Object.entries({ detail: setDetailValue, user: setUserValue, type: setTypeValue }).map(([key, setter]) => (state[key] ? setter(state[key]) : undefined));
12!
150
    setTimeout(() => (isInitialized.current = true), TIMEOUTS.debounceDefault);
4✔
151
    // eslint-disable-next-line react-hooks/exhaustive-deps
152
  }, [dispatch, hasAuditlogs, JSON.stringify(events), JSON.stringify(locationParams)]);
153

154
  const reset = () => {
13✔
155
    dispatch(
2✔
156
      setAuditlogsState({
157
        detail: null,
158
        endDate: tonight,
159
        page: 1,
160
        reset: !resetList,
161
        startDate: today,
162
        type: null,
163
        user: null
164
      })
165
    );
166
    setDetailValue(null);
2✔
167
    setTypeValue(null);
2✔
168
    setUserValue(null);
2✔
169
    navigate('/auditlog');
2✔
170
  };
171

172
  const createCsvDownload = () => {
13✔
173
    setCsvLoading(true);
1✔
174
    dispatch(getAuditLogsCsvLink()).then(address => {
1✔
175
      createDownload(
1✔
176
        encodeURI(address),
177
        `Mender-AuditLog-${moment(startDate).format(moment.HTML5_FMT.DATE)}-${moment(endDate).format(moment.HTML5_FMT.DATE)}.csv`
178
      );
179
      setCsvLoading(false);
1✔
180
    });
181
  };
182

183
  const onChangeSorting = () => {
13✔
UNCOV
184
    const currentSorting = sort.direction === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
×
UNCOV
185
    dispatch(setAuditlogsState({ page: 1, sort: { direction: currentSorting } }));
×
186
  };
187

188
  const onUserFilterChange = (e, value, reason) => {
13✔
UNCOV
189
    if (!e || reason === 'blur') {
×
UNCOV
190
      return;
×
191
    }
UNCOV
192
    setUserValue(value);
×
193
  };
194

195
  const onTypeFilterChange = (e, value, reason) => {
13✔
UNCOV
196
    if (!e || reason === 'blur') {
×
UNCOV
197
      return;
×
198
    }
UNCOV
199
    if (!value) {
×
UNCOV
200
      setDetailValue(null);
×
201
    }
UNCOV
202
    setTypeValue(value);
×
203
  };
204

205
  const onDetailFilterChange = (e, value) => {
13✔
UNCOV
206
    if (!e) {
×
UNCOV
207
      return;
×
208
    }
UNCOV
209
    setDetailValue(value);
×
210
  };
211

212
  const onTimeFilterChange = (currentStartDate = startDate, currentEndDate = endDate) =>
13!
213
    dispatch(setAuditlogsState({ page: 1, startDate: currentStartDate, endDate: currentEndDate }));
1✔
214

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

217
  const typeOptionsMap = {
13✔
218
    Deployment: groups,
219
    User: Object.values(users),
220
    Device: []
221
  };
222
  const detailOptions = typeOptionsMap[type?.title] ?? [];
13✔
223

224
  return (
13✔
225
    <div className="fadeIn margin-left flexbox column" style={{ marginRight: '5%' }}>
226
      <div className="flexbox center-aligned">
227
        <h3 className="margin-right-small">Audit log</h3>
228
        <InfoHintContainer>
229
          <EnterpriseNotification id={BENEFITS.auditlog.id} />
230
        </InfoHintContainer>
231
      </div>
232
      <ClickFilter disabled={!hasAuditlogs}>
233
        <div className={`margin-bottom margin-top-small ${classes.filters}`}>
234
          <Autocomplete
235
            {...autoSelectProps}
236
            disabled={!hasAuditlogs}
237
            id="audit-log-user-selection"
238
            freeSolo
239
            options={Object.values(users)}
240
            onChange={onUserFilterChange}
UNCOV
241
            isOptionEqualToValue={({ email, id }, value) => id === value || email === value || email === value.email}
×
242
            value={userValue}
243
            renderInput={params => (
244
              <TextField
13✔
245
                {...params}
246
                label="Filter by user"
247
                placeholder="Select a user"
248
                InputLabelProps={{ shrink: true }}
249
                InputProps={{ ...params.InputProps }}
250
              />
251
            )}
252
            style={{ maxWidth: 250 }}
253
          />
254
          <Autocomplete
255
            {...autoSelectProps}
256
            disabled={!hasAuditlogs}
257
            id="audit-log-type-selection"
258
            onChange={onTypeFilterChange}
259
            options={AUDIT_LOGS_TYPES}
260
            renderInput={params => (
261
              <TextField {...params} label="Filter by change" placeholder="Type" InputLabelProps={{ shrink: true }} InputProps={{ ...params.InputProps }} />
13✔
262
            )}
263
            style={{ marginLeft: 7.5 }}
264
            value={typeValue}
265
          />
266
          <Autocomplete
267
            {...autoSelectProps}
268
            className={classes.typeDetails}
269
            id="audit-log-type-details-selection"
270
            key={`audit-log-type-details-selection-${type}`}
271
            disabled={!type || !hasAuditlogs}
13!
272
            freeSolo
273
            value={detailValue}
274
            onInputChange={onDetailFilterChange}
275
            options={detailOptions}
276
            renderInput={params => <TextField {...params} placeholder={detailsMap[type] || '-'} InputProps={{ ...params.InputProps }} />}
16✔
277
          />
278
          <div />
279
          <TimerangePicker disabled={!hasAuditlogs} endDate={endDate} onChange={onTimeFilterChange} startDate={startDate} />
280
          <div className={classes.timeframe}>
281
            <TimeframePicker disabled={!hasAuditlogs} onChange={onTimeFilterChange} endDate={endDate} startDate={startDate} tonight={tonight} />
282
          </div>
283
          {hasAuditlogs && !!(user || type || detail || startDate !== today || endDate !== tonight) && (
91✔
284
            <span className={`link ${classes.filterReset} ${hasAuditlogs ? '' : 'muted'}`} onClick={reset}>
5!
285
              clear filter
286
            </span>
287
          )}
288
        </div>
289
      </ClickFilter>
290
      <div className="flexbox center-aligned" style={{ justifyContent: 'flex-end' }}>
291
        <Loader show={csvLoading} />
292
        <Button variant="contained" color="secondary" disabled={csvLoading || !total} onClick={createCsvDownload} style={{ marginLeft: 15 }}>
25✔
293
          Download results as csv
294
        </Button>
295
      </div>
296
      {!!total && (
26✔
297
        <AuditLogsList
298
          {...props}
299
          items={events}
300
          loading={isLoading}
301
          onChangePage={onChangePagination}
UNCOV
302
          onChangeRowsPerPage={newPerPage => onChangePagination(1, newPerPage)}
×
303
          onChangeSorting={onChangeSorting}
304
          selectionState={selectionState}
305
          setAuditlogsState={state => dispatch(setAuditlogsState(state))}
1✔
306
          userCapabilities={userCapabilities}
307
        />
308
      )}
309
      {!(isLoading || total) && hasAuditlogs && (
29!
310
        <div className="dashboard-placeholder">
311
          <p>No log entries were found.</p>
312
          <p>Try adjusting the filters.</p>
313
          <img src={historyImage} alt="Past" />
314
        </div>
315
      )}
316
      {!hasAuditlogs && (
13!
317
        <div className={`dashboard-placeholder flexbox ${classes.upgradeNote}`}>
318
          <DefaultUpgradeNotification className="margin-right-small" />
319
          <MenderHelpTooltip id={HELPTOOLTIPS.AuditlogExplanation.id} />
320
        </div>
321
      )}
322
    </div>
323
  );
324
};
325

326
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