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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

90.19
/frontend/src/js/components/auditlogs/AuditLogs.tsx
1
// Copyright 2020 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { useCallback, useEffect, useRef, useState } from 'react';
2✔
15
import { useDispatch, useSelector } from 'react-redux';
2✔
16

2✔
17
import { Typography } from '@mui/material';
2✔
18
import { makeStyles } from 'tss-react/mui';
2✔
19

2✔
20
import EnterpriseNotification, { DefaultUpgradeNotification } from '@northern.tech/common-ui/EnterpriseNotification';
2✔
21
import { AUDIT_LOGS_TYPES, BEGINNING_OF_TIME, BENEFITS, SORTING_OPTIONS, SP_AUDIT_LOGS_TYPES, TIMEOUTS } from '@northern.tech/store/constants';
2✔
22
import { useLocationParams } from '@northern.tech/store/liststatehook';
2✔
23
import {
2✔
24
  getAuditLog,
2✔
25
  getAuditLogEntry,
2✔
26
  getAuditLogSelectionState,
2✔
27
  getCurrentSession,
2✔
28
  getFeatures,
2✔
29
  getGroupNames,
2✔
30
  getIsServiceProvider,
2✔
31
  getTenantCapabilities,
2✔
32
  getUserCapabilities
2✔
33
} from '@northern.tech/store/selectors';
2✔
34
import { getAuditLogs, getAuditLogsCsvLink, getUserList, setAuditlogsState } from '@northern.tech/store/thunks';
2✔
35
import { createDownload, getISOStringBoundaries } from '@northern.tech/utils/helpers';
2✔
36
import dayjs from 'dayjs';
2✔
37

2✔
38
import { HELPTOOLTIPS } from '../helptips/HelpTooltips';
2✔
39
import { MenderHelpTooltip } from '../helptips/MenderTooltip';
2✔
40
import AuditLogsFilter from './AuditLogsFilter';
2✔
41
import AuditLogsList from './AuditLogsList';
2✔
42
import AuditlogsView from './AuditlogsView';
2✔
43
import { ActionDescriptor, ChangeDescriptor, ChangeDetailsDescriptor, TimeWrapper, TypeDescriptor, UserDescriptor } from './ColumnComponents';
2✔
44
import EventDetailsDrawer from './EventDetailsDrawer';
2✔
45
import EventDetailsDrawerContentMap from './EventDetailsDrawerContentMap';
2✔
46
import EventDetailsFallbackComponent from './eventdetails/FallbackComponent';
2✔
47

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

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

2✔
65
const locationDefaults = { sort: { direction: SORTING_OPTIONS.desc } };
6✔
66

2✔
67
export const AuditLogs = () => {
6✔
68
  const [csvLoading, setCsvLoading] = useState(false);
19✔
69

2✔
70
  const [date] = useState(getISOStringBoundaries(new Date()));
19✔
71
  const { start: today, end: tonight } = date;
19✔
72

2✔
73
  const isInitialized = useRef();
19✔
74
  const [locationParams, setLocationParams] = useLocationParams('auditlogs', { today, tonight, defaults: locationDefaults });
19✔
75
  const { classes } = useStyles();
19✔
76
  const dispatch = useDispatch();
19✔
77
  const events = useSelector(getAuditLog);
19✔
78
  const eventItem = useSelector(getAuditLogEntry);
19✔
79
  const { isHosted } = useSelector(getFeatures);
19✔
80
  const groups = useSelector(getGroupNames);
19✔
81
  const selectionState = useSelector(getAuditLogSelectionState);
19✔
82
  const userCapabilities = useSelector(getUserCapabilities);
19✔
83
  const tenantCapabilities = useSelector(getTenantCapabilities);
19✔
84
  const users = useSelector(state => state.users.byId);
33✔
85
  const { canReadUsers } = userCapabilities;
19✔
86
  const { hasAuditlogs } = tenantCapabilities;
19✔
87
  const [detailsReset, setDetailsReset] = useState('');
19✔
88
  const [dirtyField, setDirtyField] = useState('');
19✔
89
  const { token } = useSelector(getCurrentSession);
19✔
90
  const isSP = useSelector(getIsServiceProvider);
19✔
91
  const { detail, perPage, endDate, user, sort, startDate, type, total, isLoading } = selectionState;
19✔
92
  const [auditLogsTypes, setAuditLogsTypes] = useState(AUDIT_LOGS_TYPES);
19✔
93
  const timers = useRef({ init: null, detailsReset: null, dirtyField: null });
19✔
94

2✔
95
  useEffect(() => {
19✔
96
    if (isSP) {
5!
97
      setAuditLogsTypes(SP_AUDIT_LOGS_TYPES);
2✔
98
    }
2✔
99
  }, [isSP]);
2✔
100

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

2✔
109
  useEffect(() => {
19✔
110
    if (!isInitialized.current) {
5!
111
      return;
5✔
112
    }
2✔
113
    setDetailsReset('detail');
2✔
114
    clearTimeout(timers.current.detailsReset);
2✔
115
    timers.current.detailsReset = setTimeout(() => setDetailsReset(''), TIMEOUTS.debounceShort);
2✔
116
  }, [type?.value]);
2✔
117

2✔
118
  useEffect(() => {
19✔
119
    if (canReadUsers) {
5!
120
      dispatch(getUserList());
5✔
121
    }
2✔
122
  }, [canReadUsers, dispatch]);
2✔
123

2✔
124
  const initAuditlogState = useCallback(
19✔
125
    (result, state) => {
2✔
126
      const { detail, endDate, startDate, type, user } = state;
4✔
127
      const resultList = result ? Object.values(result.events) : [];
4!
128
      if (resultList.length && startDate === today) {
4✔
129
        const newStartDate = new Date(resultList[resultList.length - 1].time);
3✔
130
        const { start } = getISOStringBoundaries(newStartDate);
3✔
131
        state.startDate = start;
3✔
132
      }
2✔
133
      dispatch(setAuditlogsState(state));
4✔
134
      clearTimeout(timers.current.dirtyField);
4✔
135
      timers.current.dirtyField = setTimeout(() => {
4✔
136
        let field = Object.entries({ detail, type, user }).reduce((accu, [key, value]) => (accu || value ? key : accu), '');
5!
137
        field = field || (endDate !== tonight ? 'endDate' : field);
3!
138
        field = field || (state.startDate !== today ? 'startDate' : field);
3!
139
        setDirtyField(field);
3✔
140
      }, TIMEOUTS.debounceDefault);
2✔
141
      // 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
2✔
142
      clearTimeout(timers.current.init);
4✔
143
      timers.current.init = setTimeout(() => (isInitialized.current = true), TIMEOUTS.oneSecond + TIMEOUTS.debounceDefault);
4✔
144
    },
2✔
145
    [dispatch, today, tonight]
2✔
146
  );
2✔
147

2✔
148
  const updateState = useCallback(
19✔
149
    nextState => {
2✔
150
      const state = { ...nextState };
2✔
151
      if (state.id && Boolean(state.open)) {
2!
152
        state.selectedId = state.id[0];
2✔
153
        const [eventAction, eventTime] = atob(state.selectedId).split('|');
2✔
154
        if (eventTime && !events.some(item => item.time === eventTime && item.action === eventAction)) {
2!
155
          const { start, end } = getISOStringBoundaries(new Date(eventTime));
2✔
156
          state.endDate = end;
2✔
157
          state.startDate = start;
2✔
158
        }
2✔
159
        let field = endDate !== tonight ? 'endDate' : '';
2!
160
        field = field || (startDate !== today ? 'startDate' : field);
2!
161
        setDirtyField(field);
2✔
162
      }
2✔
163
      // 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
2✔
164
      dispatch(setAuditlogsState(state)).then(() => {
2✔
165
        clearTimeout(timers.current.init);
2✔
166
        timers.current.init = setTimeout(() => (isInitialized.current = true), TIMEOUTS.oneSecond + TIMEOUTS.debounceDefault);
2✔
167
      });
2✔
168
      return;
2✔
169
    },
2✔
170
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
171
    [dispatch, endDate, JSON.stringify(events), startDate, today, tonight]
2✔
172
  );
2✔
173

2✔
174
  useEffect(() => {
19✔
175
    if (!hasAuditlogs || isInitialized.current !== undefined) {
7✔
176
      return;
5✔
177
    }
2✔
178
    isInitialized.current = false;
4✔
179
    const { id, open, detail, endDate, startDate, type, user } = locationParams;
4✔
180
    const state = { ...locationParams };
4✔
181
    if (id && Boolean(open)) {
4!
182
      updateState(state);
2✔
183
      return;
2✔
184
    }
2✔
185
    dispatch(getAuditLogs({ page: state.page ?? 1, perPage: 50, startDate: startDate !== today ? startDate : BEGINNING_OF_TIME, endDate, user, type, detail }))
4✔
186
      .unwrap()
2✔
187
      .then(({ payload: result }) => initAuditlogState(result, state));
4✔
188
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
189
  }, [dispatch, hasAuditlogs, JSON.stringify(events), JSON.stringify(locationParams), initAuditlogState, updateState, today, tonight]);
2✔
190

2✔
191
  useEffect(() => {
19✔
192
    const currentTimers = timers.current;
5✔
193
    return () => {
5✔
194
      Object.values(currentTimers).forEach(clearTimeout);
5✔
195
    };
2✔
196
  }, []);
2✔
197

2✔
198
  const createCsvDownload = () => {
19✔
199
    setCsvLoading(true);
3✔
200
    dispatch(getAuditLogsCsvLink())
3✔
201
      .unwrap()
2✔
202
      .then(address => {
2✔
203
        createDownload(encodeURI(address), `Mender-AuditLog-${dayjs(startDate).format('YYYY-MM-DD')}-${dayjs(endDate).format('YYYY-MM-DD')}.csv`, token);
3✔
204
        setCsvLoading(false);
3✔
205
      });
2✔
206
  };
2✔
207

2✔
208
  const onChangeSorting = () => {
19✔
209
    const currentSorting = sort.direction === SORTING_OPTIONS.desc ? SORTING_OPTIONS.asc : SORTING_OPTIONS.desc;
2!
210
    dispatch(setAuditlogsState({ page: 1, sort: { direction: currentSorting } }));
2✔
211
  };
2✔
212

2✔
213
  const onChangePagination = (page, currentPerPage = perPage) => dispatch(setAuditlogsState({ page, perPage: currentPerPage }));
19!
214

2✔
215
  const onIssueSelection = selectedIssue =>
19✔
216
    dispatch(setAuditlogsState({ selectedId: selectedIssue ? btoa(`${selectedIssue.action}|${selectedIssue.time}`) : undefined }));
3!
217

2✔
218
  const onFiltersChange = useCallback(
19✔
219
    ({ endDate, detail, startDate, user, type }) => {
2✔
220
      if (!isInitialized.current) {
8!
221
        return;
8✔
222
      }
2✔
223
      const selectedUser = Object.values(users).find(item => isUserOptionEqualToValue(item, user));
2✔
224
      dispatch(setAuditlogsState({ page: 1, detail, startDate, endDate, user: selectedUser, type }));
2✔
225
    },
2✔
226
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
227
    [dispatch, JSON.stringify(users)]
2✔
228
  );
2✔
229

2✔
230
  return (
19✔
231
    <AuditlogsView
2✔
232
      createCsvDownload={createCsvDownload}
2✔
233
      hasAuditlogs={hasAuditlogs}
2✔
234
      total={total}
2✔
235
      csvLoading={csvLoading}
2✔
236
      infoHintComponent={<EnterpriseNotification id={BENEFITS.auditlog.id} />}
2✔
237
      auditLogsFilter={
2✔
238
        <AuditLogsFilter
2✔
239
          groups={groups}
2✔
240
          users={users}
2✔
241
          disabled={!hasAuditlogs}
2✔
242
          onFiltersChange={onFiltersChange}
2✔
243
          detailsReset={detailsReset}
2✔
244
          isHosted={isHosted}
2✔
245
          selectionState={selectionState}
2✔
246
          auditLogsTypes={auditLogsTypes}
2✔
247
          dirtyField={dirtyField}
2✔
248
          setDirtyField={setDirtyField}
2✔
249
        />
2✔
250
      }
2✔
251
    >
2✔
252
      {!!total && (
2✔
253
        <AuditLogsList
2✔
254
          items={events}
2✔
255
          onChangePage={onChangePagination}
2✔
UNCOV
256
          onChangeRowsPerPage={newPerPage => onChangePagination(1, newPerPage)}
2✔
257
          onChangeSorting={onChangeSorting}
2✔
258
          selectionState={selectionState}
2✔
259
          onIssueSelection={onIssueSelection}
2✔
260
          userCapabilities={userCapabilities}
2✔
261
          auditLogColumns={[
2✔
262
            { title: 'Performed by', sortable: false, render: UserDescriptor },
2✔
263
            { title: 'Action', sortable: false, render: ActionDescriptor },
2✔
264
            { title: 'Type', sortable: false, render: TypeDescriptor },
2✔
265
            { title: 'Changed', sortable: false, render: ChangeDescriptor },
2✔
266
            { title: 'More details', sortable: false, render: ChangeDetailsDescriptor },
2✔
267
            { title: 'Time', sortable: true, render: TimeWrapper }
2✔
268
          ]}
2✔
269
        />
2✔
270
      )}
2✔
271
      {!(isLoading || total) && hasAuditlogs && (
2!
272
        <div className="dashboard-placeholder margin-top">
2✔
273
          <Typography>No log entries were found.</Typography>
2✔
274
          <Typography>Try adjusting the filters to show more entries.</Typography>
2✔
275
        </div>
2✔
276
      )}
2✔
277
      {!hasAuditlogs && (
2✔
278
        <div className={`dashboard-placeholder flexbox ${classes.upgradeNote}`}>
2✔
279
          <DefaultUpgradeNotification className="margin-right-small" />
2✔
280
          <MenderHelpTooltip id={HELPTOOLTIPS.auditlogExplanation.id} />
2✔
281
        </div>
2✔
282
      )}
2✔
283
      <EventDetailsDrawer
2✔
284
        mapChangeToContent={EventDetailsDrawerContentMap}
2✔
285
        fallbackComponent={EventDetailsFallbackComponent}
2✔
286
        eventItem={eventItem}
2✔
287
        open={Boolean(eventItem)}
2✔
UNCOV
288
        onClose={() => onIssueSelection()}
2✔
289
      />
2✔
290
    </AuditlogsView>
2✔
291
  );
2✔
292
};
2✔
293

2✔
294
export default AuditLogs;
2✔
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