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

mendersoftware / mender-server / 1593965839

18 Dec 2024 10:58AM UTC coverage: 73.514% (+0.7%) from 72.829%
1593965839

Pull #253

gitlab-ci

mineralsfree
chore(gui): aligned tests with edit billing profile

Ticket: MEN-7466
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #253: MEN-7466-feat: updated billing section in My Organization settings

4257 of 6185 branches covered (68.83%)

Branch coverage included in aggregate %.

53 of 87 new or added lines in 11 files covered. (60.92%)

43 existing lines in 11 files now uncovered.

40083 of 54130 relevant lines covered (74.05%)

22.98 hits per line

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

67.88
/frontend/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 { makeStyles } from 'tss-react/mui';
18

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

37
import historyImage from '../../../assets/img/history.png';
38
import AuditLogsFilter from './AuditLogsFilter';
39
import AuditlogsView from './AuditlogsView';
40
import { ActionDescriptor, ChangeDescriptor, ChangeDetailsDescriptor, TimeWrapper, TypeDescriptor, UserDescriptor } from './ColumnComponents';
41
import EventDetailsDrawerContentMap from './EventDetailsDrawerContentMap';
42
import AuditLogsList from './auditlogslist';
43
import EventDetailsFallbackComponent from './eventdetails/FallbackComponent';
44
import EventDetailsDrawer from './eventdetailsdrawer';
45

46
const useStyles = makeStyles()(theme => ({
3✔
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 isUserOptionEqualToValue = ({ email, id }, value) => id === value || email === value || email === value?.email;
3!
62

63
const locationDefaults = { sort: { direction: SORTING_OPTIONS.desc } };
3✔
64

65
export const AuditLogs = () => {
3✔
66
  const [csvLoading, setCsvLoading] = useState(false);
11✔
67

68
  const [date] = useState(getISOStringBoundaries(new Date()));
10✔
69
  const { start: today, end: tonight } = date;
10✔
70

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

91
  useEffect(() => {
10✔
92
    if (isSP) {
3!
UNCOV
93
      setAuditLogsTypes(SP_AUDIT_LOGS_TYPES);
×
94
    }
95
  }, [isSP]);
96

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

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

113
  useEffect(() => {
10✔
114
    if (canReadUsers) {
3!
115
      dispatch(getUserList());
3✔
116
    }
117
  }, [canReadUsers, dispatch]);
118

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

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

169
  const createCsvDownload = () => {
10✔
170
    setCsvLoading(true);
1✔
171
    dispatch(getAuditLogsCsvLink())
1✔
172
      .unwrap()
173
      .then(address => {
174
        createDownload(encodeURI(address), `Mender-AuditLog-${dayjs(startDate).format('YYYY-MM-DD')}-${dayjs(endDate).format('YYYY-MM-DD')}.csv`, token);
1✔
175
        setCsvLoading(false);
1✔
176
      });
177
  };
178

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

184
  const onChangePagination = (page, currentPerPage = perPage) => dispatch(setAuditlogsState({ page, perPage: currentPerPage }));
10!
185

186
  const onIssueSelection = selectedIssue =>
10✔
187
    dispatch(setAuditlogsState({ selectedId: selectedIssue ? btoa(`${selectedIssue.action}|${selectedIssue.time}`) : undefined }));
1!
188

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

201
  return (
10✔
202
    <AuditlogsView
203
      createCsvDownload={createCsvDownload}
204
      hasAuditlogs={hasAuditlogs}
205
      total={total}
206
      csvLoading={csvLoading}
207
      infoHintComponent={<EnterpriseNotification id={BENEFITS.auditlog.id} />}
208
      auditLogsFilter={
209
        <AuditLogsFilter
210
          groups={groups}
211
          users={users}
212
          disabled={!hasAuditlogs}
213
          onFiltersChange={onFiltersChange}
214
          detailsReset={detailsReset}
215
          selectionState={selectionState}
216
          auditLogsTypes={auditLogsTypes}
217
          dirtyField={dirtyField}
218
          setDirtyField={setDirtyField}
219
        />
220
      }
221
    >
222
      {!!total && (
20✔
223
        <AuditLogsList
224
          items={events}
225
          onChangePage={onChangePagination}
UNCOV
226
          onChangeRowsPerPage={newPerPage => onChangePagination(1, newPerPage)}
×
227
          onChangeSorting={onChangeSorting}
228
          selectionState={selectionState}
229
          onIssueSelection={onIssueSelection}
230
          userCapabilities={userCapabilities}
231
          auditLogColumns={[
232
            { title: 'Performed by', sortable: false, render: UserDescriptor },
233
            { title: 'Action', sortable: false, render: ActionDescriptor },
234
            { title: 'Type', sortable: false, render: TypeDescriptor },
235
            { title: 'Changed', sortable: false, render: ChangeDescriptor },
236
            { title: 'More details', sortable: false, render: ChangeDetailsDescriptor },
237
            { title: 'Time', sortable: true, render: TimeWrapper }
238
          ]}
239
        />
240
      )}
241
      {!(isLoading || total) && hasAuditlogs && (
28!
242
        <div className="dashboard-placeholder">
243
          <p>No log entries were found.</p>
244
          <p>Try adjusting the filters.</p>
245
          <img src={historyImage} alt="Past" />
246
        </div>
247
      )}
248
      {!hasAuditlogs && (
11✔
249
        <div className={`dashboard-placeholder flexbox ${classes.upgradeNote}`}>
250
          <DefaultUpgradeNotification className="margin-right-small" />
251
          <MenderHelpTooltip id={HELPTOOLTIPS.auditlogExplanation.id} />
252
        </div>
253
      )}
254
      <EventDetailsDrawer
255
        mapChangeToContent={EventDetailsDrawerContentMap}
256
        fallbackComponent={EventDetailsFallbackComponent}
257
        eventItem={eventItem}
258
        open={Boolean(eventItem)}
UNCOV
259
        onClose={() => onIssueSelection()}
×
260
      />
261
    </AuditlogsView>
262
  );
263
};
264

265
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