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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

82.11
/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 { getCurrentSession, 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 = {
3✔
41
  Deployment: 'to device group',
42
  User: 'email'
43
};
44

45
const useStyles = makeStyles()(theme => ({
3✔
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 = {
3✔
67
  autoSelect: true,
68
  filterSelectedOptions: true,
69
  getOptionLabel,
70
  handleHomeEndKeys: true,
71
  renderOption
72
};
73

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

76
export const AuditLogs = props => {
3✔
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
  const { token } = useSelector(getCurrentSession);
18✔
97

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

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

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

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

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

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

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

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

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

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

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

205
  return (
18✔
206
    <div className="fadeIn margin-left flexbox column" style={{ marginRight: '5%' }}>
207
      <div className="flexbox center-aligned">
208
        <h3 className="margin-right-small">Audit log</h3>
209
        <InfoHintContainer>
210
          <EnterpriseNotification id={BENEFITS.auditlog.id} />
211
        </InfoHintContainer>
212
      </div>
213
      <ClickFilter disabled={!hasAuditlogs}>
214
        <Filters
215
          initialValues={{ startDate, endDate, user, type, detail }}
216
          defaultValues={{ startDate: today, endDate: tonight, user: '', type: '', detail: '' }}
217
          fieldResetTrigger={detailsReset}
218
          dirtyField={dirtyField}
219
          clearDirty={setDirtyField}
220
          filters={[
221
            {
222
              key: 'user',
223
              title: 'By user',
224
              Component: ControlledAutoComplete,
225
              componentProps: {
226
                ...autoSelectProps,
227
                freeSolo: true,
228
                isOptionEqualToValue: isUserOptionEqualToValue,
229
                options: Object.values(users),
230
                renderInput: params => <TextField {...params} placeholder="Select a user" InputProps={{ ...params.InputProps }} />
21✔
231
              }
232
            },
233
            {
234
              key: 'type',
235
              title: 'Change type',
236
              Component: ControlledAutoComplete,
237
              componentProps: {
238
                ...autoSelectProps,
239
                options: AUDIT_LOGS_TYPES,
240
                isOptionEqualToValue: (option, value) => option.value === value.value && option.object_type === value.object_type,
144✔
241
                renderInput: params => <TextField {...params} placeholder="Type" InputProps={{ ...params.InputProps }} />
28✔
242
              }
243
            },
244
            {
245
              key: 'detail',
246
              title: '',
247
              Component: ControlledAutoComplete,
248
              componentProps: {
249
                ...autoSelectProps,
250
                freeSolo: true,
251
                options: detailOptions,
252
                disabled: !type,
253
                renderInput: params => <TextField {...params} placeholder={detailsMap[type] || '-'} InputProps={{ ...params.InputProps }} />
21✔
254
              }
255
            },
256
            {
257
              key: 'timeframe',
258
              title: 'Start time',
259
              Component: TimeframePicker,
260
              componentProps: {
261
                tonight
262
              }
263
            }
264
          ]}
265
          onChange={onFiltersChange}
266
        />
267
      </ClickFilter>
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 }}>
35✔
271
          Download results as csv
272
        </Button>
273
      </div>
274
      {!!total && (
36✔
275
        <AuditLogsList
276
          {...props}
277
          items={events}
278
          loading={isLoading}
279
          onChangePage={onChangePagination}
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) && hasAuditlogs && (
45!
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
      {!hasAuditlogs && (
18!
295
        <div className={`dashboard-placeholder flexbox ${classes.upgradeNote}`}>
296
          <DefaultUpgradeNotification className="margin-right-small" />
297
          <MenderHelpTooltip id={HELPTOOLTIPS.auditlogExplanation.id} />
298
        </div>
299
      )}
300
    </div>
301
  );
302
};
303

304
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