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

mendersoftware / gui / 963002358

pending completion
963002358

Pull #3870

gitlab-ci

mzedel
chore: cleaned up left over onboarding tooltips & aligned with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3870: MEN-5413

4348 of 6319 branches covered (68.81%)

95 of 122 new or added lines in 24 files covered. (77.87%)

1734 existing lines in 160 files now uncovered.

8174 of 9951 relevant lines covered (82.14%)

178.12 hits per line

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

44.36
/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 { useDispatch, useSelector } 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 { getDeviceFilters, getFilterAttributes, getIsEnterprise, getOrganization, getSelectedGroupInfo, getTenantCapabilities } 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);
136✔
32
  return attr?.value ?? key ?? '';
14✔
33
};
34

35
const MAX_PREVIOUS_FILTERS_COUNT = 3;
13✔
36

37
export const Filters = ({ className = '', filters: propsFilters, isModification = true, onFilterChange, onGroupClick, open }) => {
13✔
38
  const [adding, setAdding] = useState(isModification);
27✔
39
  const [newFilter, setNewFilter] = useState(emptyFilter);
27✔
40
  const [currentFilters, setCurrentFilters] = useState([]);
27✔
41
  const [editedIndex, setEditedIndex] = useState(0);
27✔
42
  const dispatch = useDispatch();
27✔
43
  const { plan = 'os' } = useSelector(getOrganization);
27!
44
  const { groupFilters, selectedGroup } = useSelector(getSelectedGroupInfo);
27✔
45
  const attributes = useSelector(getFilterAttributes);
27✔
46
  const { hasFullFiltering: canFilterMultiple } = useSelector(getTenantCapabilities);
27✔
47
  const stateFilters = useSelector(getDeviceFilters);
27✔
48
  const filters = propsFilters || stateFilters;
27✔
49
  const isEnterprise = useSelector(getIsEnterprise);
27✔
50
  const previousFilters = useSelector(state => state.users.globalSettings.previousFilters);
54✔
51

52
  useEffect(() => {
27✔
53
    setCurrentFilters(filters);
4✔
54
    setEditedIndex(filters.length);
4✔
55
    dispatch(getDeviceAttributes());
4✔
56
  }, [dispatch, filters, open]);
57

58
  useEffect(() => {
27✔
59
    setAdding(adding && groupFilters.length ? isModification : true);
7!
60
    setNewFilter(emptyFilter);
7✔
61
  }, [isModification, groupFilters.length, adding]);
62

63
  const updateFilter = newFilter => {
27✔
UNCOV
64
    setNewFilter(newFilter);
×
UNCOV
65
    saveUpdatedFilter(newFilter);
×
UNCOV
66
    let changedFilters = [...currentFilters];
×
UNCOV
67
    if (editedIndex == currentFilters.length) {
×
UNCOV
68
      changedFilters.push(newFilter);
×
UNCOV
69
      return handleFilterChange(changedFilters);
×
70
    }
UNCOV
71
    changedFilters[editedIndex] = newFilter;
×
UNCOV
72
    handleFilterChange(changedFilters);
×
73
  };
74

75
  const saveUpdatedFilter = newFilter => {
27✔
UNCOV
76
    let changedPreviousFilters = [...previousFilters];
×
UNCOV
77
    if (!changedPreviousFilters.find(filter => deepCompare(filter, newFilter))) {
×
UNCOV
78
      changedPreviousFilters.push(newFilter);
×
UNCOV
79
      dispatch(saveGlobalSettings({ previousFilters: changedPreviousFilters.slice(-1 * MAX_PREVIOUS_FILTERS_COUNT) }));
×
80
    }
81
  };
82

83
  const resetIdFilter = () => dispatch(setDeviceListState({ selectedId: undefined, setOnly: true }));
27✔
84

85
  const removeFilter = removedFilter => {
27✔
UNCOV
86
    if (removedFilter.key === 'id') {
×
UNCOV
87
      resetIdFilter();
×
88
    }
UNCOV
89
    let changedFilters = filters.filter(filter => !deepCompare(filter, removedFilter));
×
UNCOV
90
    handleFilterChange(changedFilters);
×
UNCOV
91
    if (deepCompare(newFilter, removedFilter)) {
×
UNCOV
92
      setNewFilter(emptyFilter);
×
93
    }
UNCOV
94
    const currentFilters = changedFilters.filter(filter => !deepCompare(filter, newFilter));
×
UNCOV
95
    setCurrentFilters(currentFilters);
×
UNCOV
96
    setEditedIndex(currentFilters.length);
×
UNCOV
97
    if (!currentFilters.length) {
×
UNCOV
98
      setAdding(true);
×
99
    }
100
  };
101

102
  const clearFilters = () => {
27✔
UNCOV
103
    handleFilterChange([]);
×
UNCOV
104
    resetIdFilter();
×
UNCOV
105
    setCurrentFilters([]);
×
UNCOV
106
    setEditedIndex(0);
×
UNCOV
107
    setNewFilter(emptyFilter);
×
108
  };
109

110
  const onAddClick = () => {
27✔
UNCOV
111
    setAdding(true);
×
UNCOV
112
    setEditedIndex(filters.length);
×
UNCOV
113
    if (Object.values(newFilter).every(thing => !!thing)) {
×
UNCOV
114
      setCurrentFilters([...currentFilters, newFilter]);
×
115
    }
UNCOV
116
    setNewFilter(emptyFilter);
×
117
  };
118

119
  const handleFilterChange = filters => {
27✔
UNCOV
120
    const activeFilters = filters.filter(item => item.value !== '');
×
UNCOV
121
    dispatch(setDeviceFilters(activeFilters));
×
UNCOV
122
    onFilterChange();
×
UNCOV
123
    if (activeFilters.length === 0) {
×
UNCOV
124
      setAdding(true);
×
125
    }
126
  };
127

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

194
export default 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