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

mendersoftware / gui / 963124858

pending completion
963124858

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

4368 of 6355 branches covered (68.73%)

91 of 118 new or added lines in 22 files covered. (77.12%)

1753 existing lines in 162 files now uncovered.

8246 of 10042 relevant lines covered (82.12%)

193.52 hits per line

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

43.85
/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 { BENEFITS } from '../../../constants/appConstants';
24
import { DEVICE_FILTERING_OPTIONS, emptyFilter } from '../../../constants/deviceConstants';
25
import { deepCompare } from '../../../helpers';
26
import { getDeviceFilters, getFilterAttributes, getIsEnterprise, getSelectedGroupInfo, getTenantCapabilities } from '../../../selectors';
27
import EnterpriseNotification from '../../common/enterpriseNotification';
28
import { InfoHintContainer } from '../../common/info-hint';
29
import MenderTooltip from '../../common/mendertooltip';
30
import FilterItem from './filteritem';
31

32
export const getFilterLabelByKey = (key, attributes) => {
13✔
33
  const attr = attributes.find(attr => attr.key === key);
136✔
34
  return attr?.value ?? key ?? '';
14✔
35
};
36

37
const MAX_PREVIOUS_FILTERS_COUNT = 3;
13✔
38

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

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

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

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

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

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

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

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

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

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

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

195
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