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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

94.44
/frontend/src/js/components/tenants/TenantList.tsx
1
// Copyright 2024 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { useCallback, useEffect } from 'react';
2✔
15
import { useDispatch, useSelector } from 'react-redux';
2✔
16

2✔
17
import { Check as CheckIcon, Warning as WarningIcon } from '@mui/icons-material';
2✔
18

2✔
19
import DetailsIndicator from '@northern.tech/common-ui/DetailsIndicator';
2✔
20
import { ColumnHeader, CommonList, ListItemComponentProps, RendererProp } from '@northern.tech/common-ui/List';
2✔
21
import { SORTING_OPTIONS } from '@northern.tech/store/constants';
2✔
22
import { useLocationParams } from '@northern.tech/store/liststatehook';
2✔
23
import { getTenantsList } from '@northern.tech/store/selectors';
2✔
24
import { AppDispatch } from '@northern.tech/store/store';
2✔
25
import { setTenantsListState } from '@northern.tech/store/thunks';
2✔
26
import dayjs from 'dayjs';
2✔
27

2✔
28
import { LIMIT_THRESHOLD } from '../header/DeviceCount';
2✔
29
import { ExpandedTenant } from './ExpandedTenant';
2✔
30
import { Tenant } from './types';
2✔
31

2✔
32
export const defaultTextRender = (props: RendererProp<Tenant>) => {
8✔
33
  const { column, item } = props;
10✔
34
  const attributeValue = item?.[column.attribute.name];
10✔
35
  return typeof attributeValue === 'object' ? JSON.stringify(attributeValue) : attributeValue;
10!
36
};
2✔
37
export const DeviceLimitRender = (props: RendererProp<Tenant>) => {
8✔
38
  const { column, item } = props;
6✔
39
  const attributeValue = item?.[column.attribute.name] ?? 0;
6!
40
  const deviceCount = item?.device_count ?? 0;
6!
41
  return (
6✔
42
    <div className="flexbox center-aligned">
2✔
43
      {deviceCount}/{attributeValue}
2✔
44
      {Number(deviceCount) / Number(attributeValue) > LIMIT_THRESHOLD && <WarningIcon className="margin-left-small" fontSize="small" />}
2!
45
    </div>
2✔
46
  );
2✔
47
};
2✔
48
export const BoolRender = (props: RendererProp<Tenant>) => {
8✔
49
  const { column, item } = props;
6✔
50
  return <div>{item?.[column.attribute.name] ? <CheckIcon /> : <div>-</div>}</div>;
6!
51
};
2✔
52
const AttributeRenderer = ({ content, textContent }) => (
8✔
53
  <div title={typeof textContent === 'string' ? textContent : ''}>
10!
54
    <div className="text-overflow">{content}</div>
2✔
55
  </div>
2✔
56
);
2✔
57
const DateRender = (props: RendererProp<Tenant>) => {
8✔
58
  const { column, item } = props;
6✔
59
  const attributeValue = dayjs(item?.[column.attribute.name]).format('YYYY-MM-DD HH:mm');
6✔
60
  return <AttributeRenderer content={attributeValue} textContent={item?.[column.attribute.name]} />;
6✔
61
};
2✔
62
export const columnHeaders: ColumnHeader<Tenant>[] = [
8✔
63
  {
2✔
UNCOV
64
    component: () => <></>,
2✔
65
    title: 'Name',
2✔
66
    attribute: {
2✔
67
      name: 'name',
2✔
68
      scope: ''
2✔
69
    },
2✔
70
    sortable: false,
2✔
71
    textRender: defaultTextRender
2✔
72
  },
2✔
73
  {
2✔
74
    title: 'Devices',
2✔
75
    attribute: {
2✔
76
      name: 'device_limit',
2✔
77
      scope: ''
2✔
78
    },
2✔
79
    sortable: false,
2✔
80
    component: DeviceLimitRender
2✔
81
  },
2✔
82
  {
2✔
83
    title: 'Delta updates enabled ',
2✔
84
    attribute: {
2✔
85
      name: 'binary_delta',
2✔
86
      scope: ''
2✔
87
    },
2✔
88
    sortable: false,
2✔
89
    component: BoolRender
2✔
90
  },
2✔
91
  {
2✔
92
    title: 'Created',
2✔
93
    attribute: {
2✔
94
      name: 'created_at',
2✔
95
      scope: ''
2✔
96
    },
2✔
97
    sortable: false,
2✔
98
    component: DateRender
2✔
99
  },
2✔
100
  {
2✔
101
    title: 'More details',
2✔
102
    attribute: {
2✔
103
      name: '',
2✔
104
      scope: ''
2✔
105
    },
2✔
106
    sortable: false,
2✔
107
    component: DetailsIndicator
2✔
108
  }
2✔
109
];
2✔
110

2✔
111
export const TenantListItem = (props: ListItemComponentProps<Tenant>) => {
8✔
112
  const { listItem, columnHeaders, onClick } = props;
6✔
113
  const handleOnClick = useCallback(() => {
6✔
114
    onClick(listItem);
3✔
115
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
116
  }, [listItem.id, onClick]);
2✔
117

2✔
118
  return (
6✔
119
    <div onClick={handleOnClick} className={`deviceListRow deviceListItem clickable`}>
2✔
120
      {columnHeaders.map((column: ColumnHeader<Tenant>) => {
2✔
121
        const { classes = {}, component: Component, textRender } = column;
22✔
122
        if (textRender) {
22✔
123
          return <AttributeRenderer content={textRender({ item: listItem, column })} key={column.title} textContent={textRender({ item: listItem, column })} />;
6✔
124
        }
2✔
125
        return <Component classes={classes} column={column} item={listItem} key={column.title} />;
18✔
126
      })}
2✔
127
    </div>
2✔
128
  );
2✔
129
};
2✔
130
export const TenantList = () => {
8✔
131
  const tenantListState = useSelector(getTenantsList);
3✔
132
  const { tenants, perPage, selectedTenant, sort = {} } = tenantListState;
3✔
133
  const dispatch: AppDispatch = useDispatch();
3✔
134

2✔
135
  const [locationParams, setLocationParams] = useLocationParams('tenants', {
3✔
136
    defaults: {
2✔
137
      direction: SORTING_OPTIONS.desc,
2✔
138
      key: 'name',
2✔
139
      sort: {}
2✔
140
    }
2✔
141
  });
2✔
142

2✔
143
  useEffect(() => {
3✔
144
    const { selectedTenant: selectedTenantName } = locationParams;
3✔
145
    if (selectedTenantName) {
3!
146
      dispatch(setTenantsListState({ selectedTenant: selectedTenantName }));
2✔
147
    }
2✔
148
  }, [dispatch, locationParams]);
2✔
149

2✔
150
  useEffect(() => {
3✔
151
    if (selectedTenant) {
3!
152
      setLocationParams({ pageState: { ...tenantListState, selectedTenant } });
2✔
153
    }
2✔
154
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
155
  }, [setLocationParams, JSON.stringify(sort), selectedTenant]);
2✔
156

2✔
157
  const onExpandClick = useCallback((tenant: Tenant) => dispatch(setTenantsListState({ selectedTenant: tenant.id })), [dispatch]);
3✔
158

2✔
159
  const onCloseClick = useCallback(() => {
3✔
160
    setLocationParams({ pageState: { ...tenantListState, selectedTenant: '' } });
2✔
161
    return dispatch(setTenantsListState({ selectedTenant: null }));
2✔
162
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
163
  }, [dispatch, setLocationParams, JSON.stringify(tenantListState)]);
2✔
164

2✔
165
  const onChangePagination = useCallback(
3✔
166
    (page, currentPerPage = perPage) => {
2!
167
      dispatch(setTenantsListState({ page, perPage: currentPerPage }));
2✔
168
    },
2✔
169
    [dispatch, perPage]
2✔
170
  );
2✔
171

2✔
172
  const tenant = selectedTenant && tenants.find((tenant: Tenant) => selectedTenant === tenant.id);
3!
173
  return (
3✔
174
    <div>
2✔
175
      <CommonList
2✔
176
        columnHeaders={columnHeaders}
2✔
177
        listItems={tenants}
2✔
178
        listState={tenantListState}
2✔
UNCOV
179
        onChangeRowsPerPage={newPerPage => onChangePagination(1, newPerPage)}
2✔
180
        onExpandClick={onExpandClick}
2✔
181
        onPageChange={onChangePagination}
2✔
182
        onResizeColumns={false}
2✔
183
        onSelect={false}
2✔
184
        pageLoading={false}
2✔
185
        ListItemComponent={TenantListItem}
2✔
186
      />
2✔
187
      {selectedTenant && tenant && <ExpandedTenant onCloseClick={onCloseClick} tenant={tenant} />}
2!
188
    </div>
2✔
189
  );
2✔
190
};
2✔
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