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

mendersoftware / gui / 901187442

pending completion
901187442

Pull #3795

gitlab-ci

mzedel
feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

Ticket: None
Changelog: None
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3795: feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

4389 of 6365 branches covered (68.96%)

5 of 5 new or added lines in 1 file covered. (100.0%)

1729 existing lines in 165 files now uncovered.

8274 of 10019 relevant lines covered (82.58%)

144.86 hits per line

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

60.34
/src/js/components/auditlogs/auditlogslist.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, { useMemo } from 'react';
15
import { Link } from 'react-router-dom';
16

17
import { ArrowRightAlt as ArrowRightAltIcon, Sort as SortIcon } from '@mui/icons-material';
18

19
import { SORTING_OPTIONS, canAccess } from '../../constants/appConstants';
20
import { DEPLOYMENT_ROUTES } from '../../constants/deploymentConstants';
21
import Loader from '../common/loader';
22
import Pagination from '../common/pagination';
23
import Time from '../common/time';
24
import EventDetailsDrawer from './eventdetailsdrawer';
25

26
export const defaultRowsPerPage = 20;
5✔
27

28
const ArtifactLink = ({ item }) => <Link to={`/releases/${item.object.artifact.name}`}>View artifact</Link>;
5✔
29
const DeploymentLink = ({ item }) => <Link to={`${DEPLOYMENT_ROUTES.finished.route}?open=true&id=${item.object.id}`}>View deployment</Link>;
11✔
30
const DeviceLink = ({ item }) => <Link to={`/devices?id=${item.object.id}`}>View device</Link>;
5✔
31
const DeviceRejectedLink = ({ item }) => <Link to={`/devices/rejected?id=${item.object.id}`}>View device</Link>;
5✔
32
const TerminalSessionLink = () => <a>View session log</a>;
11✔
33
const UserChange = ({ item: { change = '-' } }) => {
5!
34
  const formatChange = change => {
11✔
35
    const diff = change.split(/@@.*@@/);
11✔
36
    return diff.length > 1 ? diff[1].trim() : diff;
11!
37
  };
38
  return (
11✔
39
    <div className="capitalized" style={{ alignItems: 'flex-start', whiteSpace: 'pre-line' }}>
40
      {formatChange(change)}
41
    </div>
42
  );
43
};
44

45
const fallbackFormatter = data => {
5✔
UNCOV
46
  let result = '';
×
UNCOV
47
  try {
×
UNCOV
48
    result = JSON.stringify(data);
×
49
  } catch (error) {
UNCOV
50
    console.log(error);
×
51
  }
UNCOV
52
  return result;
×
53
};
54

55
const defaultAccess = canAccess;
5✔
56
const changeMap = {
5✔
57
  default: { component: 'div', actionFormatter: fallbackFormatter, title: 'defaultTitle', accessCheck: defaultAccess },
UNCOV
58
  artifact: { actionFormatter: data => decodeURIComponent(data.artifact.name), component: ArtifactLink, accessCheck: ({ canReadReleases }) => canReadReleases },
×
59
  deployment: {
60
    actionFormatter: data => decodeURIComponent(data.deployment.name),
11✔
61
    component: DeploymentLink,
62
    accessCheck: ({ canReadDeployments }) => canReadDeployments
11✔
63
  },
UNCOV
64
  deviceDecommissioned: { actionFormatter: data => decodeURIComponent(data.id), component: 'div', accessCheck: defaultAccess },
×
UNCOV
65
  deviceRejected: { actionFormatter: data => decodeURIComponent(data.id), component: DeviceRejectedLink, accessCheck: ({ canReadDevices }) => canReadDevices },
×
UNCOV
66
  deviceGeneral: { actionFormatter: data => decodeURIComponent(data.id), component: DeviceLink, accessCheck: ({ canReadDevices }) => canReadDevices },
×
67
  deviceTerminalSession: { actionFormatter: data => decodeURIComponent(data.id), component: TerminalSessionLink, accessCheck: defaultAccess },
11✔
68
  user: { component: UserChange, actionFormatter: data => data.user.email, accessCheck: defaultAccess }
11✔
69
};
70

71
const mapChangeToContent = item => {
5✔
72
  let content = changeMap[item.object.type];
66✔
73
  if (content) {
66✔
74
    return content;
44✔
75
  } else if (item.object.type === 'device' && item.action.includes('terminal')) {
22!
76
    content = changeMap.deviceTerminalSession;
22✔
UNCOV
77
  } else if (item.object.type === 'device' && item.action.includes('reject')) {
×
UNCOV
78
    content = changeMap.deviceRejected;
×
UNCOV
79
  } else if (item.object.type === 'device' && item.action.includes('decommission')) {
×
UNCOV
80
    content = changeMap.deviceDecommissioned;
×
UNCOV
81
  } else if (item.object.type === 'device') {
×
UNCOV
82
    content = changeMap.deviceGeneral;
×
83
  } else {
UNCOV
84
    content = changeMap.default;
×
85
  }
86
  return content;
22✔
87
};
88

89
const actorMap = {
5✔
90
  user: 'email',
91
  device: 'id'
92
};
93

94
const UserDescriptor = (item, index) => <div key={`${item.time}-${index} `}>{item.actor[actorMap[item.actor.type]]}</div>;
33✔
95
const ActionDescriptor = (item, index) => (
5✔
96
  <div className="uppercased" key={`${item.time}-${index}`}>
33✔
97
    {item.action}
98
  </div>
99
);
100
const TypeDescriptor = (item, index) => (
5✔
101
  <div className="capitalized" key={`${item.time}-${index}`}>
33✔
102
    {item.object.type}
103
  </div>
104
);
105
const ChangeDescriptor = (item, index) => <div key={`${item.time}-${index}`}>{mapChangeToContent(item).actionFormatter(item.object)}</div>;
33✔
106
const ChangeDetailsDescriptor = (item, index, userCapabilities) => {
5✔
107
  const { component: Comp, accessCheck } = mapChangeToContent(item);
33✔
108
  const key = `${item.time}-${index}`;
33✔
109
  return accessCheck(userCapabilities) ? <Comp key={key} item={item} /> : <div key={key} />;
33!
110
};
111
const TimeWrapper = (item, index) => <Time key={`${item.time}-${index}`} value={item.time} />;
33✔
112

113
const auditLogColumns = [
5✔
114
  { title: 'User', sortable: false, render: UserDescriptor },
115
  { title: 'Action', sortable: false, render: ActionDescriptor },
116
  { title: 'Type', sortable: false, render: TypeDescriptor },
117
  { title: 'Changed', sortable: false, render: ChangeDescriptor },
118
  { title: 'More details', sortable: false, render: ChangeDetailsDescriptor },
119
  { title: 'Time', sortable: true, render: TimeWrapper }
120
];
121

122
export const AuditLogsList = ({ items, loading, onChangePage, onChangeRowsPerPage, onChangeSorting, selectionState, setAuditlogsState, userCapabilities }) => {
5✔
123
  const { page, perPage, selectedId, sort = {}, total: count } = selectionState;
11!
124

125
  const onIssueSelection = selectedIssue =>
11✔
126
    setAuditlogsState({ selectedId: selectedIssue ? btoa(`${selectedIssue.action}|${selectedIssue.time}`) : undefined });
1!
127

128
  const eventItem = useMemo(() => {
11✔
129
    if (!selectedId) {
4!
130
      return;
4✔
131
    }
UNCOV
132
    const [eventAction, eventTime] = atob(selectedId).split('|');
×
UNCOV
133
    return items.find(item => item.action === eventAction && item.time === eventTime);
×
134
  }, [items, selectedId]);
135

136
  return (
11✔
137
    !!items.length && (
22✔
138
      <div className="fadeIn deploy-table-contain auditlogs-list">
139
        <div className="auditlogs-list-item auditlogs-list-item-header muted">
140
          {auditLogColumns.map((column, index) => (
141
            <div
66✔
142
              className="columnHeader"
143
              key={`columnHeader-${index}`}
UNCOV
144
              onClick={() => (column.sortable ? onChangeSorting() : null)}
×
145
              style={column.sortable ? {} : { cursor: 'initial' }}
66✔
146
            >
147
              {column.title}
148
              {column.sortable ? <SortIcon className={`sortIcon selected ${(sort.direction === SORTING_OPTIONS.desc).toString()}`} /> : null}
66✔
149
            </div>
150
          ))}
151
          <div />
152
        </div>
153
        <div className="auditlogs-list">
154
          {items.map(item => {
155
            const allowsExpansion = !!item.change || item.action.includes('terminal') || item.action.includes('portforward');
33!
156
            return (
33✔
157
              <div
158
                className={`auditlogs-list-item ${allowsExpansion ? 'clickable' : ''}`}
33!
159
                key={`event-${item.time}`}
160
                onClick={() => onIssueSelection(allowsExpansion ? item : undefined)}
1!
161
              >
162
                {auditLogColumns.map((column, index) => column.render(item, index, userCapabilities))}
198✔
163
                {allowsExpansion ? (
33!
164
                  <div className="uppercased link-color bold">
165
                    view details <ArrowRightAltIcon />
166
                  </div>
167
                ) : (
168
                  <div />
169
                )}
170
              </div>
171
            );
172
          })}
173
        </div>
174
        <Loader show={loading} />
175
        <Pagination count={count} rowsPerPage={perPage} onChangeRowsPerPage={onChangeRowsPerPage} page={page} onChangePage={onChangePage} />
UNCOV
176
        <EventDetailsDrawer eventItem={eventItem} open={Boolean(eventItem)} onClose={() => onIssueSelection()} />
×
177
      </div>
178
    )
179
  );
180
};
181

182
export default AuditLogsList;
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