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

mendersoftware / gui / 1057188406

01 Nov 2023 04:24AM UTC coverage: 82.824% (-17.1%) from 99.964%
1057188406

Pull #4134

gitlab-ci

web-flow
chore: Bump uuid from 9.0.0 to 9.0.1

Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.0 to 9.0.1.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4134: chore: Bump uuid from 9.0.0 to 9.0.1

4349 of 6284 branches covered (0.0%)

8313 of 10037 relevant lines covered (82.82%)

200.97 hits per line

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

81.91
/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 moment from 'moment';
21

22
import historyImage from '../../../assets/img/history.png';
23
import { getAuditLogs, getAuditLogsCsvLink, setAuditlogsState } from '../../actions/organizationActions';
24
import { getUserList } from '../../actions/userActions';
25
import { BEGINNING_OF_TIME, BENEFITS, SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
26
import { AUDIT_LOGS_TYPES } from '../../constants/organizationConstants';
27
import { createDownload, getISOStringBoundaries } from '../../helpers';
28
import { getGroupNames, getTenantCapabilities, getUserCapabilities } from '../../selectors';
29
import { useLocationParams } from '../../utils/liststatehook';
30
import EnterpriseNotification, { DefaultUpgradeNotification } from '../common/enterpriseNotification';
31
import { ControlledAutoComplete } from '../common/forms/autocomplete';
32
import ClickFilter from '../common/forms/clickfilter';
33
import Filters from '../common/forms/filters';
34
import TimeframePicker from '../common/forms/timeframe-picker';
35
import { InfoHintContainer } from '../common/info-hint';
36
import Loader from '../common/loader';
37
import { HELPTOOLTIPS, MenderHelpTooltip } from '../helptips/helptooltips';
38
import AuditLogsList from './auditlogslist';
39

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

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

60
const getOptionLabel = option => option.title || option.email || option;
130✔
61

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

64
const isUserOptionEqualToValue = ({ email, id }, value) => id === value || email === value || email === value?.email;
6✔
65

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

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

76
export const AuditLogs = props => {
4✔
77
  const [csvLoading, setCsvLoading] = useState(false);
19✔
78

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

82
  const isInitialized = useRef();
18✔
83
  const [locationParams, setLocationParams] = useLocationParams('auditlogs', { today, tonight, defaults: locationDefaults });
18✔
84
  const { classes } = useStyles();
18✔
85
  const dispatch = useDispatch();
18✔
86
  const events = useSelector(state => state.organization.auditlog.events);
32✔
87
  const groups = useSelector(getGroupNames);
18✔
88
  const selectionState = useSelector(state => state.organization.auditlog.selectionState);
32✔
89
  const userCapabilities = useSelector(getUserCapabilities);
18✔
90
  const tenantCapabilities = useSelector(getTenantCapabilities);
18✔
91
  const users = useSelector(state => state.users.byId);
32✔
92
  const { canReadUsers } = userCapabilities;
18✔
93
  const { hasAuditlogs } = tenantCapabilities;
18✔
94
  const [detailsReset, setDetailsReset] = useState('');
18✔
95
  const [dirtyField, setDirtyField] = useState('');
18✔
96

97
  const { detail, isLoading, perPage, endDate, user, sort, startDate, total, type = '' } = selectionState;
18✔
98

99
  useEffect(() => {
18✔
100
    if (!hasAuditlogs || !isInitialized.current) {
6✔
101
      return;
4✔
102
    }
103
    setLocationParams({ pageState: selectionState });
2✔
104
    // eslint-disable-next-line react-hooks/exhaustive-deps
105
  }, [detail, endDate, hasAuditlogs, perPage, selectionState.page, selectionState.selectedId, setLocationParams, startDate, type, user]);
106

107
  useEffect(() => {
18✔
108
    if (!isInitialized.current) {
3!
109
      return;
3✔
110
    }
111
    setDetailsReset('detail');
×
112
    setTimeout(() => setDetailsReset(''), TIMEOUTS.debounceShort);
×
113
  }, [type?.value]);
114

115
  useEffect(() => {
18✔
116
    if (canReadUsers) {
3!
117
      dispatch(getUserList());
3✔
118
    }
119
  }, [canReadUsers, dispatch]);
120

121
  const initAuditlogState = useCallback(
18✔
122
    (result, state) => {
123
      isInitialized.current = true;
3✔
124
      const { detail, endDate, startDate, type, user } = state;
3✔
125
      const resultList = result ? Object.values(result.events) : [];
3!
126
      if (resultList.length && startDate === today) {
3✔
127
        let newStartDate = new Date(resultList[resultList.length - 1].time);
2✔
128
        const { start } = getISOStringBoundaries(newStartDate);
2✔
129
        state.startDate = start;
2✔
130
      }
131
      dispatch(setAuditlogsState(state));
3✔
132
      setTimeout(() => {
3✔
133
        let field = Object.entries({ detail, type, user }).reduce((accu, [key, value]) => (accu || value ? key : accu), '');
9!
134
        field = field || (endDate !== tonight ? 'endDate' : field);
3!
135
        field = field || (state.startDate !== today ? 'startDate' : field);
3!
136
        setDirtyField(field);
3✔
137
      }, TIMEOUTS.debounceDefault);
138
    },
139
    [dispatch, today, tonight]
140
  );
141

142
  useEffect(() => {
18✔
143
    if (!hasAuditlogs || isInitialized.current !== undefined) {
4✔
144
      return;
1✔
145
    }
146
    isInitialized.current = false;
3✔
147
    const { id, open, detail, endDate, startDate, type, user } = locationParams;
3✔
148
    let state = { ...locationParams };
3✔
149
    if (id && Boolean(open)) {
3!
150
      state.selectedId = id[0];
×
151
      const [eventAction, eventTime] = atob(state.selectedId).split('|');
×
152
      if (eventTime && !events.some(item => item.time === eventTime && item.action === eventAction)) {
×
153
        const { start, end } = getISOStringBoundaries(new Date(eventTime));
×
154
        state.endDate = end;
×
155
        state.startDate = start;
×
156
      }
157
      let field = endDate !== tonight ? 'endDate' : '';
×
158
      field = field || (startDate !== today ? 'startDate' : field);
×
159
      setDirtyField(field);
×
160
      isInitialized.current = true;
×
161
      dispatch(setAuditlogsState(state));
×
162
      return;
×
163
    }
164
    dispatch(
3✔
165
      getAuditLogs({ page: state.page ?? 1, perPage: 50, startDate: startDate !== today ? startDate : BEGINNING_OF_TIME, endDate, user, type, detail })
9✔
166
    ).then(result => initAuditlogState(result, state));
3✔
167
    // eslint-disable-next-line react-hooks/exhaustive-deps
168
  }, [dispatch, hasAuditlogs, JSON.stringify(events), JSON.stringify(locationParams), initAuditlogState, today, tonight]);
169

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

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

186
  const onChangePagination = (page, currentPerPage = perPage) => dispatch(setAuditlogsState({ page, perPage: currentPerPage }));
18!
187

188
  const onFiltersChange = useCallback(
18✔
189
    ({ endDate, detail, startDate, user, type }) => {
190
      const selectedUser = Object.values(users).find(item => isUserOptionEqualToValue(item, user));
6✔
191
      dispatch(setAuditlogsState({ page: 1, detail, startDate, endDate, user: selectedUser, type }));
3✔
192
    },
193
    // eslint-disable-next-line react-hooks/exhaustive-deps
194
    [dispatch, JSON.stringify(users)]
195
  );
196

197
  const typeOptionsMap = {
18✔
198
    Deployment: groups,
199
    User: Object.values(users)
200
  };
201
  const detailOptions = typeOptionsMap[type?.title] ?? [];
18✔
202

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

302
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