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

mendersoftware / mender-server / 10425

11 Nov 2025 05:02PM UTC coverage: 78.221% (+3.8%) from 74.435%
10425

Pull #1022

gitlab-ci

mzedel
chore(gui): aligned snapshots w/ updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #1022: MEN-8452 - device filters + device page design adjustments

3865 of 5388 branches covered (71.73%)

Branch coverage included in aggregate %.

34 of 38 new or added lines in 13 files covered. (89.47%)

7 existing lines in 1 file now uncovered.

6845 of 8304 relevant lines covered (82.43%)

68.06 hits per line

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

50.0
/frontend/src/js/components/devices/widgets/Filters.tsx
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 { useCallback, 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, Divider, Typography } from '@mui/material';
20

21
import EnterpriseNotification from '@northern.tech/common-ui/EnterpriseNotification';
22
import { InfoHintContainer } from '@northern.tech/common-ui/InfoHint';
23
import MenderTooltip from '@northern.tech/common-ui/helptips/MenderTooltip';
24
import storeActions from '@northern.tech/store/actions';
25
import { BENEFITS, DEVICE_FILTERING_OPTIONS, emptyFilter } from '@northern.tech/store/constants';
26
import {
27
  getDeviceFilters,
28
  getFilterAttributes,
29
  getGlobalSettings,
30
  getIsEnterprise,
31
  getSelectedGroupInfo,
32
  getTenantCapabilities,
33
  getUserCapabilities
34
} from '@northern.tech/store/selectors';
35
import { getDeviceAttributes, saveGlobalSettings, setDeviceListState } from '@northern.tech/store/thunks';
36
import { filtersFilter } from '@northern.tech/store/utils';
37
import { deepCompare, toggle } from '@northern.tech/utils/helpers';
38

39
import FilterItem from './FilterItem';
40

41
const { setDeviceFilters } = storeActions;
11✔
42

43
export const getFilterLabelByKey = (key, attributes) => {
11✔
44
  const attr = attributes.find(attr => attr.key === key);
77✔
45
  return attr?.value ?? key ?? '';
19✔
46
};
47

48
const MAX_PREVIOUS_FILTERS_COUNT = 3;
11✔
49

50
export const Filters = ({ className = '', onGroupClick, open }) => {
11✔
51
  const [reset, setReset] = useState(false);
41✔
52
  const [newFilter, setNewFilter] = useState(emptyFilter);
41✔
53

54
  const dispatch = useDispatch();
41✔
55
  const { hasFullFiltering, plan } = useSelector(getTenantCapabilities);
41✔
56
  const { canManageUsers } = useSelector(getUserCapabilities);
41✔
57
  const { groupFilters, selectedGroup } = useSelector(getSelectedGroupInfo);
41✔
58
  const attributes = useSelector(getFilterAttributes);
41✔
59
  const filters = useSelector(getDeviceFilters);
41✔
60
  const isEnterprise = useSelector(getIsEnterprise);
41✔
61
  const { previousFilters = [] } = useSelector(getGlobalSettings);
41✔
62

63
  useEffect(() => {
41✔
64
    if (open) {
4✔
65
      dispatch(getDeviceAttributes());
1✔
66
    }
67
  }, [dispatch, open]);
68

69
  const saveUpdatedFilter = useCallback(
41✔
70
    updatedFilter => {
71
      if (canManageUsers && !previousFilters.find(filter => deepCompare(filter, updatedFilter))) {
×
72
        const changedPreviousFilters = [...previousFilters, updatedFilter];
×
73
        dispatch(saveGlobalSettings({ previousFilters: changedPreviousFilters.slice(-1 * MAX_PREVIOUS_FILTERS_COUNT) }));
×
74
      }
75
    },
76
    [canManageUsers, dispatch, previousFilters]
77
  );
78

79
  const handleFilterChange = useCallback(
41✔
80
    filters => {
81
      const activeFilters = filters.filter(filtersFilter).filter(item => item.value !== '');
×
82
      dispatch(setDeviceFilters(activeFilters));
×
83
      dispatch(setDeviceListState({ selectedId: undefined, page: 1, shouldSelectDevices: true, forceRefresh: true, filterSelection: undefined }));
×
84
    },
85
    [dispatch]
86
  );
87

88
  // We want to preview the resulting list while user types / selects a filter before saving
89
  const applyPreviewFilter = useCallback(
41✔
90
    updatedFilter => {
91
      const activeFilters = [...filters, updatedFilter].filter(filtersFilter).filter(item => item.key && item.value !== '');
×
92
      dispatch(setDeviceListState({ selectedId: undefined, page: 1, shouldSelectDevices: true, forceRefresh: true, filterSelection: activeFilters }));
×
93
    },
94
    [dispatch, filters]
95
  );
96

97
  const updateFilter = useCallback(
41✔
98
    updatedFilter => {
99
      saveUpdatedFilter(updatedFilter);
×
100
      handleFilterChange([...filters, updatedFilter]);
×
101
      setReset(toggle);
×
102
    },
103
    [filters, handleFilterChange, saveUpdatedFilter]
104
  );
105

106
  const resetIdFilter = () => dispatch(setDeviceListState({ selectedId: undefined, setOnly: true }));
41✔
107

108
  const removeFilter = removedFilter => {
41✔
109
    if (removedFilter.key === 'id') {
×
110
      resetIdFilter();
×
111
    }
112
    const changedFilters = filters.filter(filter => !deepCompare(filter, removedFilter));
×
113
    handleFilterChange(changedFilters);
×
114
  };
115

116
  const clearFilters = () => {
41✔
117
    handleFilterChange([]);
×
118
    resetIdFilter();
×
119
    setReset(toggle);
×
120
  };
121

122
  const onAddClick = () => updateFilter(newFilter);
41✔
123

124
  const isFilterDefined = Object.values(newFilter).every(thing => !!thing);
41✔
125
  const currentFilters = filters.filter(filtersFilter);
41✔
126
  const isFiltering = !!currentFilters.length || isFilterDefined;
41✔
127
  return (
41✔
128
    <Collapse in={open} timeout="auto" className={`${className} filter-wrapper`} unmountOnExit>
129
      <>
130
        <div className="flexbox">
131
          <Typography>Devices matching:</Typography>
132
          <div className="margin-left-small filter-list">
133
            {currentFilters.map(item => (
NEW
134
              <Chip
×
135
                className="margin-right-small"
136
                key={`filter-${item.key}-${item.operator}-${item.value}`}
137
                label={`${getFilterLabelByKey(item.key, attributes)} ${DEVICE_FILTERING_OPTIONS[item.operator].shortform} ${
138
                  item.operator !== DEVICE_FILTERING_OPTIONS.$exists.key && item.operator !== DEVICE_FILTERING_OPTIONS.$nexists.key
×
139
                    ? item.operator === DEVICE_FILTERING_OPTIONS.$regex.key
×
140
                      ? `${item.value}.*`
141
                      : item.value
142
                    : ''
143
                }`}
144
                size="small"
NEW
145
                onDelete={() => removeFilter(item)}
×
146
              />
147
            ))}
148
          </div>
149
          <InfoHintContainer>
150
            <EnterpriseNotification id={BENEFITS.fullFiltering.id} />
151
            {hasFullFiltering && <EnterpriseNotification id={BENEFITS.dynamicGroups.id} />}
61✔
152
          </InfoHintContainer>
153
        </div>
154
        <div className="flexbox column">
155
          <FilterItem attributes={attributes} onChange={setNewFilter} onSelect={applyPreviewFilter} onSave={updateFilter} plan={plan} reset={reset} />
156
          <Button
157
            className="align-self-start margin-bottom-small"
158
            color="info"
159
            disabled={!(isFilterDefined && hasFullFiltering)}
41!
160
            onClick={onAddClick}
161
            startIcon={<AddIcon />}
162
            variant="outlined"
163
          >
164
            Add rule
165
          </Button>
166
        </div>
167
        {!!filters.length && (
41!
168
          <>
169
            <Divider />
170
            <div className="flexbox space-between margin-top-small margin-bottom-small">
171
              {!groupFilters.length && (
×
172
                <Button disabled={!isFiltering} onClick={clearFilters} variant="outlined" color="info">
173
                  Clear filter
174
                </Button>
175
              )}
176
              {isEnterprise ? (
×
177
                <div>
178
                  {selectedGroup ? (
×
179
                    !!groupFilters.length && (
×
180
                      <MenderTooltip
181
                        title="Saved changes will not change the target devices of any ongoing deployments to this group, but will take effect for new deployments"
182
                        arrow
183
                      >
184
                        <Button variant="contained" onClick={onGroupClick}>
185
                          Save group
186
                        </Button>
187
                      </MenderTooltip>
188
                    )
189
                  ) : (
190
                    <Button variant="contained" onClick={onGroupClick}>
191
                      Create group with this filter
192
                    </Button>
193
                  )}
194
                </div>
195
              ) : (
196
                <div />
197
              )}
198
            </div>
199
          </>
200
        )}
201
      </>
202
    </Collapse>
203
  );
204
};
205

206
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