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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

42.86
/src/js/components/devices/widgets/filters.js
1
// Copyright 2015 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, { useEffect, useState } from 'react';
15
import { connect } from 'react-redux';
16

17
import { Add as AddIcon } from '@mui/icons-material';
18
// material ui
19
import { Button, Chip, Collapse } from '@mui/material';
20

21
import { getDeviceAttributes, setDeviceFilters, setDeviceListState } from '../../../actions/deviceActions';
22
import { saveGlobalSettings } from '../../../actions/userActions';
23
import { DEVICE_FILTERING_OPTIONS, emptyFilter } from '../../../constants/deviceConstants';
24
import { deepCompare } from '../../../helpers';
25
import { getFilterAttributes } from '../../../selectors';
26
import EnterpriseNotification from '../../common/enterpriseNotification';
27
import MenderTooltip from '../../common/mendertooltip';
28
import FilterItem from './filteritem';
29

30
export const getFilterLabelByKey = (key, attributes) => {
13✔
31
  const attr = attributes.find(attr => attr.key === key);
22✔
32
  return attr?.value ?? key ?? '';
13✔
33
};
34

35
const MAX_PREVIOUS_FILTERS_COUNT = 3;
13✔
36

37
export const Filters = ({
13✔
38
  attributes,
39
  canFilterMultiple,
40
  className = '',
2✔
41
  filters,
42
  getDeviceAttributes,
43
  groupFilters,
44
  isEnterprise,
45
  isHosted,
46
  isModification = true,
2✔
47
  onFilterChange,
48
  onGroupClick,
49
  open,
50
  plan,
51
  previousFilters,
52
  saveGlobalSettings,
53
  selectedGroup,
54
  setDeviceFilters,
55
  setDeviceListState
56
}) => {
57
  const [adding, setAdding] = useState(isModification);
12✔
58
  const [newFilter, setNewFilter] = useState(emptyFilter);
12✔
59
  const [currentFilters, setCurrentFilters] = useState([]);
12✔
60
  const [editedIndex, setEditedIndex] = useState(0);
12✔
61

62
  useEffect(() => {
12✔
63
    setCurrentFilters(filters);
4✔
64
    setEditedIndex(filters.length);
4✔
65
    getDeviceAttributes();
4✔
66
  }, [open]);
67

68
  useEffect(() => {
12✔
69
    setAdding(adding && groupFilters.length ? isModification : true);
4!
70
    setNewFilter(emptyFilter);
4✔
71
  }, [isModification, groupFilters.length]);
72

73
  const updateFilter = newFilter => {
12✔
74
    setNewFilter(newFilter);
×
75
    saveUpdatedFilter(newFilter);
×
76
    let changedFilters = [...currentFilters];
×
77
    if (editedIndex == currentFilters.length) {
×
78
      changedFilters.push(newFilter);
×
79
      return handleFilterChange(changedFilters);
×
80
    }
81
    changedFilters[editedIndex] = newFilter;
×
82
    handleFilterChange(changedFilters);
×
83
  };
84

85
  const saveUpdatedFilter = newFilter => {
12✔
86
    let changedPreviousFilters = [...previousFilters];
×
87
    if (!changedPreviousFilters.find(filter => deepCompare(filter, newFilter))) {
×
88
      changedPreviousFilters.push(newFilter);
×
89
      saveGlobalSettings({ previousFilters: changedPreviousFilters.slice(-1 * MAX_PREVIOUS_FILTERS_COUNT) });
×
90
    }
91
  };
92

93
  const resetIdFilter = () => setDeviceListState({ selectedId: undefined, setOnly: true });
12✔
94

95
  const removeFilter = removedFilter => {
12✔
96
    if (removedFilter.key === 'id') {
×
97
      resetIdFilter();
×
98
    }
99
    let changedFilters = filters.filter(filter => !deepCompare(filter, removedFilter));
×
100
    handleFilterChange(changedFilters);
×
101
    if (deepCompare(newFilter, removedFilter)) {
×
102
      setNewFilter(emptyFilter);
×
103
    }
104
    const currentFilters = changedFilters.filter(filter => !deepCompare(filter, newFilter));
×
105
    setCurrentFilters(currentFilters);
×
106
    setEditedIndex(currentFilters.length);
×
107
    if (!currentFilters.length) {
×
108
      setAdding(true);
×
109
    }
110
  };
111

112
  const clearFilters = () => {
12✔
113
    handleFilterChange([]);
×
114
    resetIdFilter();
×
115
    setCurrentFilters([]);
×
116
    setEditedIndex(0);
×
117
    setNewFilter(emptyFilter);
×
118
  };
119

120
  const onAddClick = () => {
12✔
121
    setAdding(true);
×
122
    setEditedIndex(filters.length);
×
123
    if (Object.values(newFilter).every(thing => !!thing)) {
×
124
      setCurrentFilters([...currentFilters, newFilter]);
×
125
    }
126
    setNewFilter(emptyFilter);
×
127
  };
128

129
  const handleFilterChange = filters => {
12✔
130
    const activeFilters = filters.filter(item => item.value !== '');
×
131
    setDeviceFilters(activeFilters);
×
132
    onFilterChange();
×
133
    if (activeFilters.length === 0) {
×
134
      setAdding(true);
×
135
    }
136
  };
137

138
  const filter = filters.find(filter => deepCompare(filter, newFilter)) || newFilter;
12✔
139
  const isFilterDefined = filter && Object.values(filter).every(thing => !!thing);
12✔
140
  const addButton = <Chip icon={<AddIcon />} label="Add a rule" color="primary" onClick={onAddClick} />;
12✔
141
  return (
12✔
142
    <Collapse in={open} timeout="auto" className={`${className} filter-wrapper`} unmountOnExit>
143
      <>
144
        <div className="flexbox">
145
          <div className="margin-right" style={{ marginTop: currentFilters.length ? 8 : 25 }}>
12!
146
            Devices matching:
147
          </div>
148
          <div>
149
            {currentFilters.length ? (
12!
150
              <div className="filter-list">
151
                {currentFilters.map(item => (
152
                  <Chip
×
153
                    className="margin-right-small"
154
                    key={`filter-${item.key}-${item.operator}-${item.value}`}
155
                    label={`${getFilterLabelByKey(item.key, attributes)} ${DEVICE_FILTERING_OPTIONS[item.operator].shortform} ${
156
                      item.operator !== '$exists' && item.operator !== '$nexists' ? (item.operator === '$regex' ? `${item.value}.*` : item.value) : ''
×
157
                    }`}
158
                    onDelete={() => removeFilter(item)}
×
159
                  />
160
                ))}
161
                {!adding && addButton}
×
162
              </div>
163
            ) : null}
164
            {adding && <FilterItem attributes={attributes} filter={filter} onRemove={removeFilter} onSelect={updateFilter} plan={plan} />}
21✔
165
            {isFilterDefined && addButton}
12!
166
          </div>
167
        </div>
168
        <div className="flexbox column margin-top-small margin-bottom-small" style={{ alignItems: 'flex-end' }}>
169
          {!!filters.length && !groupFilters.length && (
12!
170
            <span className="link margin-small margin-top-none" onClick={clearFilters}>
171
              Clear filter
172
            </span>
173
          )}
174
          <EnterpriseNotification
175
            isEnterprise={isEnterprise || (isHosted && plan === 'enterprise')}
24!
176
            benefit="filtering by multiple attributes to improve the device overview and the creation of dynamic groups to ease device management"
177
          />
178
          {canFilterMultiple && (plan === 'enterprise' || isEnterprise) && filters.length >= 1 && (
12!
179
            <>
180
              {selectedGroup ? (
×
181
                !!groupFilters.length && (
×
182
                  <MenderTooltip
183
                    title="Saved changes will not change the target devices of any ongoing deployments to this group, but will take effect for new deployments"
184
                    arrow
185
                  >
186
                    <Button variant="contained" color="secondary" onClick={onGroupClick}>
187
                      Save group
188
                    </Button>
189
                  </MenderTooltip>
190
                )
191
              ) : (
192
                <Button variant="contained" color="secondary" onClick={onGroupClick}>
193
                  Create group with this filter
194
                </Button>
195
              )}
196
            </>
197
          )}
198
        </div>
199
      </>
200
    </Collapse>
201
  );
202
};
203

204
const actionCreators = {
13✔
205
  getDeviceAttributes,
206
  saveGlobalSettings,
207
  setDeviceFilters,
208
  setDeviceListState
209
};
210

211
const mapStateToProps = (state, ownProps) => {
13✔
212
  const { plan = 'os' } = state.organization.organization;
11!
213
  const selectedGroup = state.devices.groups.selectedGroup;
11✔
214
  const groupFilters = state.devices.groups.byId[selectedGroup]?.filters ?? [];
11✔
215
  return {
11✔
216
    attributes: getFilterAttributes(state),
217
    canFilterMultiple: state.app.features.isEnterprise || (state.app.features.isHosted && plan !== 'os'),
22!
218
    filters: ownProps.filters || state.devices.filters || [],
21!
219
    groupFilters,
220
    isHosted: state.app.features.isHosted,
221
    isEnterprise: state.app.features.isEnterprise,
222
    plan,
223
    previousFilters: state.users.globalSettings.previousFilters,
224
    selectedGroup
225
  };
226
};
227

228
export default connect(mapStateToProps, actionCreators)(Filters);
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