@flightctl/ui-components 0.0.5 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.d.ts +3 -1
  2. package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.d.ts.map +1 -1
  3. package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.js +2 -2
  4. package/dist/src/components/Device/DeviceDetails/DeviceDetailsPage.js.map +1 -1
  5. package/dist/src/components/Device/DevicesPage/DeviceTableToolbar.d.ts +2 -2
  6. package/dist/src/components/Device/DevicesPage/DeviceTableToolbar.d.ts.map +1 -1
  7. package/dist/src/components/Device/DevicesPage/DeviceTableToolbar.js +8 -8
  8. package/dist/src/components/Device/DevicesPage/DeviceTableToolbar.js.map +1 -1
  9. package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.d.ts +3 -3
  10. package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.d.ts.map +1 -1
  11. package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.js +23 -3
  12. package/dist/src/components/Device/DevicesPage/DeviceToolbarFilters.js.map +1 -1
  13. package/dist/src/components/Device/DevicesPage/DevicesPage.d.ts +7 -1
  14. package/dist/src/components/Device/DevicesPage/DevicesPage.d.ts.map +1 -1
  15. package/dist/src/components/Device/DevicesPage/DevicesPage.js +23 -23
  16. package/dist/src/components/Device/DevicesPage/DevicesPage.js.map +1 -1
  17. package/dist/src/components/Device/DevicesPage/EnrollmentRequestList.d.ts +0 -7
  18. package/dist/src/components/Device/DevicesPage/EnrollmentRequestList.d.ts.map +1 -1
  19. package/dist/src/components/Device/DevicesPage/EnrollmentRequestList.js +42 -51
  20. package/dist/src/components/Device/DevicesPage/EnrollmentRequestList.js.map +1 -1
  21. package/dist/src/components/Device/DevicesPage/useDeviceBackendFilters.d.ts +2 -0
  22. package/dist/src/components/Device/DevicesPage/useDeviceBackendFilters.d.ts.map +1 -1
  23. package/dist/src/components/Device/DevicesPage/useDeviceBackendFilters.js +10 -3
  24. package/dist/src/components/Device/DevicesPage/useDeviceBackendFilters.js.map +1 -1
  25. package/dist/src/components/Device/DevicesPage/useDevices.d.ts +10 -4
  26. package/dist/src/components/Device/DevicesPage/useDevices.d.ts.map +1 -1
  27. package/dist/src/components/Device/DevicesPage/useDevices.js +25 -26
  28. package/dist/src/components/Device/DevicesPage/useDevices.js.map +1 -1
  29. package/dist/src/components/EnrollmentRequest/PendingEnrollmentRequestsBadge.css +10 -0
  30. package/dist/src/components/EnrollmentRequest/PendingEnrollmentRequestsBadge.d.ts +5 -0
  31. package/dist/src/components/EnrollmentRequest/PendingEnrollmentRequestsBadge.d.ts.map +1 -0
  32. package/dist/src/components/EnrollmentRequest/PendingEnrollmentRequestsBadge.js +22 -0
  33. package/dist/src/components/EnrollmentRequest/PendingEnrollmentRequestsBadge.js.map +1 -0
  34. package/dist/src/components/Fleet/FleetDetails/FleetDevices.d.ts.map +1 -1
  35. package/dist/src/components/Fleet/FleetDetails/FleetDevices.js +7 -7
  36. package/dist/src/components/Fleet/FleetDetails/FleetDevices.js.map +1 -1
  37. package/dist/src/components/Fleet/FleetsPage.d.ts.map +1 -1
  38. package/dist/src/components/Fleet/FleetsPage.js +17 -23
  39. package/dist/src/components/Fleet/FleetsPage.js.map +1 -1
  40. package/dist/src/components/Fleet/useFleets.d.ts +18 -0
  41. package/dist/src/components/Fleet/useFleets.d.ts.map +1 -0
  42. package/dist/src/components/Fleet/useFleets.js +61 -0
  43. package/dist/src/components/Fleet/useFleets.js.map +1 -0
  44. package/dist/src/components/OverviewPage/Cards/Status/StatusCard.d.ts.map +1 -1
  45. package/dist/src/components/OverviewPage/Cards/Status/StatusCard.js +4 -3
  46. package/dist/src/components/OverviewPage/Cards/Status/StatusCard.js.map +1 -1
  47. package/dist/src/components/OverviewPage/Cards/ToDo/ToDoCard.d.ts.map +1 -1
  48. package/dist/src/components/OverviewPage/Cards/ToDo/ToDoCard.js +5 -9
  49. package/dist/src/components/OverviewPage/Cards/ToDo/ToDoCard.js.map +1 -1
  50. package/dist/src/components/OverviewPage/Overview.js +1 -1
  51. package/dist/src/components/OverviewPage/Overview.js.map +1 -1
  52. package/dist/src/components/Repository/CreateRepository/CreateRepository.js +2 -2
  53. package/dist/src/components/Repository/CreateRepository/CreateRepository.js.map +1 -1
  54. package/dist/src/components/Repository/RepositoryDetails/DeleteRepositoryModal.js +1 -1
  55. package/dist/src/components/Repository/RepositoryDetails/DeleteRepositoryModal.js.map +1 -1
  56. package/dist/src/components/Repository/RepositoryList.d.ts.map +1 -1
  57. package/dist/src/components/Repository/RepositoryList.js +1 -1
  58. package/dist/src/components/Repository/RepositoryList.js.map +1 -1
  59. package/dist/src/components/ResourceSync/RepositoryResourceSyncList.d.ts.map +1 -1
  60. package/dist/src/components/ResourceSync/RepositoryResourceSyncList.js +2 -2
  61. package/dist/src/components/ResourceSync/RepositoryResourceSyncList.js.map +1 -1
  62. package/dist/src/components/Table/Table.d.ts +12 -2
  63. package/dist/src/components/Table/Table.d.ts.map +1 -1
  64. package/dist/src/components/Table/Table.js +3 -3
  65. package/dist/src/components/Table/Table.js.map +1 -1
  66. package/dist/src/components/charts/DonutChart.css +5 -0
  67. package/dist/src/components/charts/DonutChart.js +1 -1
  68. package/dist/src/components/charts/DonutChart.js.map +1 -1
  69. package/dist/src/components/common/LeaveFormConfirmation.js +1 -1
  70. package/dist/src/components/common/LeaveFormConfirmation.js.map +1 -1
  71. package/dist/src/components/modals/massModals/MassDeleteRepositoryModal/MassDeleteRepositoryModal.d.ts.map +1 -1
  72. package/dist/src/components/modals/massModals/MassDeleteRepositoryModal/MassDeleteRepositoryModal.js +2 -2
  73. package/dist/src/components/modals/massModals/MassDeleteRepositoryModal/MassDeleteRepositoryModal.js.map +1 -1
  74. package/dist/src/hooks/useApiTableSort.d.ts +8 -0
  75. package/dist/src/hooks/useApiTableSort.d.ts.map +1 -0
  76. package/dist/src/hooks/useApiTableSort.js +44 -0
  77. package/dist/src/hooks/useApiTableSort.js.map +1 -0
  78. package/dist/src/hooks/useAppContext.d.ts +4 -4
  79. package/dist/src/hooks/useAppContext.d.ts.map +1 -1
  80. package/dist/src/hooks/useNavigate.d.ts +3 -3
  81. package/dist/src/hooks/useNavigate.d.ts.map +1 -1
  82. package/dist/src/hooks/useNavigate.js +3 -3
  83. package/dist/src/hooks/useNavigate.js.map +1 -1
  84. package/dist/src/hooks/usePendingEnrollmentRequestsCount.d.ts +2 -0
  85. package/dist/src/hooks/usePendingEnrollmentRequestsCount.d.ts.map +1 -0
  86. package/dist/src/hooks/usePendingEnrollmentRequestsCount.js +13 -0
  87. package/dist/src/hooks/usePendingEnrollmentRequestsCount.js.map +1 -0
  88. package/dist/src/utils/query.d.ts +6 -0
  89. package/dist/src/utils/query.d.ts.map +1 -0
  90. package/dist/src/utils/query.js +32 -0
  91. package/dist/src/utils/query.js.map +1 -0
  92. package/dist/src/utils/sort/generic.d.ts +1 -4
  93. package/dist/src/utils/sort/generic.d.ts.map +1 -1
  94. package/dist/src/utils/sort/generic.js +1 -28
  95. package/dist/src/utils/sort/generic.js.map +1 -1
  96. package/dist/src/utils/status/devices.d.ts +2 -1
  97. package/dist/src/utils/status/devices.d.ts.map +1 -1
  98. package/dist/src/utils/status/devices.js +1 -0
  99. package/dist/src/utils/status/devices.js.map +1 -1
  100. package/package.json +6 -6
  101. package/src/components/Device/DeviceDetails/DeviceDetailsPage.tsx +2 -2
  102. package/src/components/Device/DevicesPage/DeviceTableToolbar.tsx +13 -13
  103. package/src/components/Device/DevicesPage/DeviceToolbarFilters.tsx +35 -8
  104. package/src/components/Device/DevicesPage/DevicesPage.tsx +41 -27
  105. package/src/components/Device/DevicesPage/EnrollmentRequestList.tsx +91 -116
  106. package/src/components/Device/DevicesPage/useDeviceBackendFilters.ts +14 -3
  107. package/src/components/Device/DevicesPage/useDevices.ts +43 -32
  108. package/src/components/EnrollmentRequest/PendingEnrollmentRequestsBadge.css +10 -0
  109. package/src/components/EnrollmentRequest/PendingEnrollmentRequestsBadge.tsx +27 -0
  110. package/src/components/Fleet/FleetDetails/FleetDevices.tsx +12 -18
  111. package/src/components/Fleet/FleetsPage.tsx +39 -28
  112. package/src/components/Fleet/useFleets.ts +86 -0
  113. package/src/components/OverviewPage/Cards/Status/StatusCard.tsx +4 -3
  114. package/src/components/OverviewPage/Cards/ToDo/ToDoCard.tsx +6 -10
  115. package/src/components/OverviewPage/Overview.tsx +1 -1
  116. package/src/components/Repository/CreateRepository/CreateRepository.tsx +2 -2
  117. package/src/components/Repository/RepositoryDetails/DeleteRepositoryModal.tsx +1 -1
  118. package/src/components/Repository/RepositoryList.tsx +1 -0
  119. package/src/components/ResourceSync/RepositoryResourceSyncList.tsx +2 -1
  120. package/src/components/Table/Table.tsx +19 -5
  121. package/src/components/charts/DonutChart.css +5 -0
  122. package/src/components/charts/DonutChart.tsx +1 -1
  123. package/src/components/modals/massModals/MassDeleteRepositoryModal/MassDeleteRepositoryModal.tsx +4 -2
  124. package/src/hooks/useApiTableSort.ts +49 -0
  125. package/src/hooks/useNavigate.tsx +3 -3
  126. package/src/hooks/usePendingEnrollmentRequestsCount.ts +12 -0
  127. package/src/utils/query.ts +29 -0
  128. package/src/utils/sort/generic.ts +1 -30
  129. package/src/utils/status/devices.ts +1 -0
  130. package/dist/src/components/Device/DevicesPage/useDeviceFilters.d.ts +0 -8
  131. package/dist/src/components/Device/DevicesPage/useDeviceFilters.d.ts.map +0 -1
  132. package/dist/src/components/Device/DevicesPage/useDeviceFilters.js +0 -16
  133. package/dist/src/components/Device/DevicesPage/useDeviceFilters.js.map +0 -1
  134. package/dist/src/utils/sort/device.d.ts +0 -4
  135. package/dist/src/utils/sort/device.d.ts.map +0 -1
  136. package/dist/src/utils/sort/device.js +0 -49
  137. package/dist/src/utils/sort/device.js.map +0 -1
  138. package/dist/src/utils/sort/fleet.d.ts +0 -4
  139. package/dist/src/utils/sort/fleet.d.ts.map +0 -1
  140. package/dist/src/utils/sort/fleet.js +0 -18
  141. package/dist/src/utils/sort/fleet.js.map +0 -1
  142. package/src/components/Device/DevicesPage/useDeviceFilters.ts +0 -15
  143. package/src/utils/sort/device.ts +0 -60
  144. package/src/utils/sort/fleet.ts +0 -16
@@ -0,0 +1,27 @@
1
+ import * as React from 'react';
2
+ import { Badge } from '@patternfly/react-core';
3
+
4
+ import { useTranslation } from '../../hooks/useTranslation';
5
+ import { usePendingEnrollmentRequestsCount } from '../../hooks/usePendingEnrollmentRequestsCount';
6
+ import WithTooltip from '../common/WithTooltip';
7
+
8
+ import './PendingEnrollmentRequestsBadge.css';
9
+
10
+ const PendingEnrollmentRequestsBadge = () => {
11
+ const { t } = useTranslation();
12
+ const [count] = usePendingEnrollmentRequestsCount();
13
+ if (count === 0) {
14
+ return null;
15
+ }
16
+
17
+ const text = t('{{ count }} devices pending approval', { count });
18
+ return (
19
+ <Badge className="fctl-pending-ers-badge pf-v5-u-ml-lg" screenReaderText={text}>
20
+ <WithTooltip showTooltip content={text}>
21
+ <span>{count}</span>
22
+ </WithTooltip>
23
+ </Badge>
24
+ );
25
+ };
26
+
27
+ export default PendingEnrollmentRequestsBadge;
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { Grid, GridItem } from '@patternfly/react-core';
2
+ import { Flex, FlexItem } from '@patternfly/react-core';
3
3
 
4
4
  import { DevicesSummary } from '@flightctl/types';
5
5
  import { useTranslation } from '../../../hooks/useTranslation';
@@ -92,23 +92,17 @@ const DevicesByDeviceStatusChart = ({
92
92
 
93
93
  const FleetDevices = ({ devicesSummary, fleetId }: FleetDevicesProps) => {
94
94
  return (
95
- <Grid hasGutter>
96
- {devicesSummary.applicationStatus && (
97
- <GridItem md={6}>
98
- <DevicesByAppStatusChart fleetId={fleetId} applicationStatus={devicesSummary.applicationStatus} />
99
- </GridItem>
100
- )}
101
- {devicesSummary.summaryStatus && (
102
- <GridItem md={6}>
103
- <DevicesByDeviceStatusChart fleetId={fleetId} deviceStatus={devicesSummary.summaryStatus} />
104
- </GridItem>
105
- )}
106
- {devicesSummary.updateStatus && (
107
- <GridItem md={6}>
108
- <DevicesByUpdateStatusChart fleetId={fleetId} updateStatus={devicesSummary.updateStatus} />
109
- </GridItem>
110
- )}
111
- </Grid>
95
+ <Flex justifyContent={{ default: 'justifyContentSpaceAround' }}>
96
+ <FlexItem>
97
+ <DevicesByAppStatusChart fleetId={fleetId} applicationStatus={devicesSummary.applicationStatus} />
98
+ </FlexItem>
99
+ <FlexItem>
100
+ <DevicesByDeviceStatusChart fleetId={fleetId} deviceStatus={devicesSummary.summaryStatus} />
101
+ </FlexItem>
102
+ <FlexItem>
103
+ <DevicesByUpdateStatusChart fleetId={fleetId} updateStatus={devicesSummary.updateStatus} />
104
+ </FlexItem>
105
+ </Flex>
112
106
  );
113
107
  };
114
108
 
@@ -13,21 +13,16 @@ import {
13
13
  ToolbarGroup,
14
14
  ToolbarItem,
15
15
  } from '@patternfly/react-core';
16
- import { Tbody } from '@patternfly/react-table';
16
+ import { Tbody, ThProps } from '@patternfly/react-table';
17
17
  import { TopologyIcon } from '@patternfly/react-icons/dist/js/icons/topology-icon';
18
18
  import { Trans } from 'react-i18next';
19
19
  import { TFunction } from 'i18next';
20
20
 
21
- import { Fleet, FleetList } from '@flightctl/types';
22
- import { useFetchPeriodically } from '../../hooks/useFetchPeriodically';
21
+ import { Fleet } from '@flightctl/types';
23
22
  import ListPage from '../ListPage/ListPage';
24
23
  import ListPageBody from '../ListPage/ListPageBody';
25
- import { sortByName } from '../../utils/sort/generic';
26
- import { sortByStatus, sortFleetsByOSImg } from '../../utils/sort/fleet';
27
24
  import TableTextSearch from '../Table/TableTextSearch';
28
- import Table, { TableColumn } from '../Table/Table';
29
- import { useTableTextSearch } from '../../hooks/useTableTextSearch';
30
- import { useTableSort } from '../../hooks/useTableSort';
25
+ import Table, { ApiSortTableColumn } from '../Table/Table';
31
26
  import { useTableSelect } from '../../hooks/useTableSelect';
32
27
  import TableActions from '../Table/TableActions';
33
28
  import { getResourceId } from '../../utils/resource';
@@ -38,6 +33,8 @@ import { useTranslation } from '../../hooks/useTranslation';
38
33
  import { ROUTE, useNavigate } from '../../hooks/useNavigate';
39
34
  import DeleteFleetModal from './DeleteFleetModal/DeleteFleetModal';
40
35
  import FleetResourceSyncs from './FleetResourceSyncs';
36
+ import { useApiTableSort } from '../../hooks/useApiTableSort';
37
+ import { useFleetBackendFilters, useFleets } from './useFleets';
41
38
 
42
39
  const FleetPageActions = ({ createText }: { createText?: string }) => {
43
40
  const { t } = useTranslation();
@@ -78,37 +75,40 @@ const FleetEmptyState = () => {
78
75
  );
79
76
  };
80
77
 
81
- const getColumns = (t: TFunction): TableColumn<Fleet>[] => [
78
+ const getColumns = (t: TFunction): ApiSortTableColumn[] => [
82
79
  {
83
80
  name: t('Name'),
84
- onSort: sortByName,
81
+ sortableField: 'metadata.name',
82
+ defaultSort: true,
85
83
  },
86
84
  {
87
85
  name: t('System image'),
88
- onSort: sortFleetsByOSImg,
86
+ sortableField: 'spec.template.spec.os.image',
89
87
  },
90
88
  {
91
89
  name: t('Devices'),
92
90
  },
93
91
  {
94
92
  name: t('Status'),
95
- onSort: sortByStatus,
96
93
  },
97
94
  ];
98
95
 
99
- const getSearchText = (fleet: Fleet) => [fleet.metadata.name];
96
+ type FleetTableProps = {
97
+ fleetColumns: ApiSortTableColumn[];
98
+ fleetLoad: FleetLoad;
99
+ getSortParams: (columnIndex: number) => ThProps['sort'];
100
+ hasFiltersEnabled: boolean;
101
+ name: string | undefined;
102
+ setName: (name: string) => void;
103
+ };
100
104
 
101
- const FleetTable = ({ fleetLoad }: { fleetLoad: FleetLoad }) => {
105
+ const FleetTable = ({ name, setName, hasFiltersEnabled, getSortParams, fleetColumns, fleetLoad }: FleetTableProps) => {
102
106
  const { t } = useTranslation();
103
107
 
104
108
  const [isMassDeleteModalOpen, setIsMassDeleteModalOpen] = React.useState(false);
105
109
  const [fleetToDeleteId, setFleetToDeleteId] = React.useState<string>();
110
+ const [fleets, loading, error, isFilterUpdating, refetch] = fleetLoad;
106
111
 
107
- const [fleetList, loading, error, refetch] = fleetLoad;
108
- const columns = React.useMemo(() => getColumns(t), [t]);
109
- const fleets = fleetList?.items || [];
110
- const { search, setSearch, filteredData } = useTableTextSearch(fleets, getSearchText);
111
- const { getSortParams, sortedData: sortedFleets } = useTableSort(filteredData, columns);
112
112
  const { onRowSelect, isAllSelected, hasSelectedRows, isRowSelected, setAllSelected } = useTableSelect();
113
113
 
114
114
  return (
@@ -117,7 +117,7 @@ const FleetTable = ({ fleetLoad }: { fleetLoad: FleetLoad }) => {
117
117
  <ToolbarContent>
118
118
  <ToolbarGroup>
119
119
  <ToolbarItem variant="search-filter">
120
- <TableTextSearch value={search} setValue={setSearch} placeholder={t('Search by name')} />
120
+ <TableTextSearch value={name} setValue={setName} placeholder={t('Search by name')} />
121
121
  </ToolbarItem>
122
122
  </ToolbarGroup>
123
123
  <ToolbarItem>
@@ -134,15 +134,16 @@ const FleetTable = ({ fleetLoad }: { fleetLoad: FleetLoad }) => {
134
134
  </Toolbar>
135
135
  <Table
136
136
  aria-label={t('Fleets table')}
137
- columns={columns}
138
- emptyFilters={filteredData.length === 0}
137
+ loading={isFilterUpdating}
138
+ columns={fleetColumns}
139
+ emptyFilters={!hasFiltersEnabled}
139
140
  emptyData={fleets.length === 0}
140
141
  getSortParams={getSortParams}
141
142
  isAllSelected={isAllSelected}
142
143
  onSelectAll={setAllSelected}
143
144
  >
144
145
  <Tbody>
145
- {sortedFleets.map((fleet, rowIndex) => (
146
+ {fleets.map((fleet, rowIndex) => (
146
147
  <FleetRow
147
148
  key={getResourceId(fleet)}
148
149
  fleet={fleet}
@@ -171,7 +172,7 @@ const FleetTable = ({ fleetLoad }: { fleetLoad: FleetLoad }) => {
171
172
  {isMassDeleteModalOpen && (
172
173
  <MassDeleteFleetModal
173
174
  onClose={() => setIsMassDeleteModalOpen(false)}
174
- fleets={sortedFleets.filter(isRowSelected)}
175
+ fleets={fleets.filter(isRowSelected)}
175
176
  onDeleteSuccess={() => {
176
177
  setIsMassDeleteModalOpen(false);
177
178
  refetch();
@@ -182,20 +183,30 @@ const FleetTable = ({ fleetLoad }: { fleetLoad: FleetLoad }) => {
182
183
  );
183
184
  };
184
185
 
185
- type FleetLoad = [FleetList | undefined, boolean, unknown, VoidFunction, boolean];
186
+ type FleetLoad = [Fleet[], boolean, unknown, boolean, VoidFunction];
186
187
 
187
188
  const FleetsPage = () => {
188
189
  const { t } = useTranslation();
189
190
 
190
191
  // TODO move the fetch down to FleetTable when the API includes the filter for pending / errored resource syncs
191
- const fleetLoad = useFetchPeriodically<FleetList>({ endpoint: 'fleets?addDevicesCount=true' });
192
+ const columns = React.useMemo(() => getColumns(t), [t]);
193
+ const { name, setName, hasFiltersEnabled } = useFleetBackendFilters();
194
+ const { getSortParams, sortField, direction } = useApiTableSort(columns);
195
+ const fleetLoad = useFleets({ name, addDevicesCount: true, sortField, direction });
192
196
 
193
197
  return (
194
198
  <>
195
- <FleetResourceSyncs fleets={fleetLoad[0]?.items || []} />
199
+ <FleetResourceSyncs fleets={fleetLoad[0] || []} />
196
200
 
197
201
  <ListPage title={t('Fleets')}>
198
- <FleetTable fleetLoad={fleetLoad} />
202
+ <FleetTable
203
+ name={name}
204
+ setName={setName}
205
+ hasFiltersEnabled={hasFiltersEnabled}
206
+ getSortParams={getSortParams}
207
+ fleetLoad={fleetLoad}
208
+ fleetColumns={columns}
209
+ />
199
210
  </ListPage>
200
211
  </>
201
212
  );
@@ -0,0 +1,86 @@
1
+ import * as React from 'react';
2
+
3
+ import { useAppContext } from '../../hooks/useAppContext';
4
+ import { useFetchPeriodically } from '../../hooks/useFetchPeriodically';
5
+ import { Fleet, FleetList, SortOrder } from '@flightctl/types';
6
+ import { useDebounce } from 'use-debounce';
7
+
8
+ export enum FleetSearchParams {
9
+ Name = 'name',
10
+ }
11
+
12
+ type FleetsEndpointArgs = {
13
+ name?: string;
14
+ addDevicesCount?: boolean;
15
+ sortField?: string;
16
+ direction?: string;
17
+ };
18
+
19
+ export const useFleetBackendFilters = () => {
20
+ const {
21
+ router: { useSearchParams },
22
+ } = useAppContext();
23
+ const [searchParams, setSearchParams] = useSearchParams();
24
+ const paramsRef = React.useRef(searchParams);
25
+ const name = searchParams.get(FleetSearchParams.Name) || undefined;
26
+
27
+ const setName = React.useCallback(
28
+ (nameVal: string) => {
29
+ const newParams = new URLSearchParams({
30
+ [FleetSearchParams.Name]: nameVal,
31
+ });
32
+ paramsRef.current = newParams;
33
+ setSearchParams(newParams);
34
+ },
35
+ [setSearchParams],
36
+ );
37
+
38
+ const hasFiltersEnabled = !!name;
39
+
40
+ return {
41
+ name,
42
+ setName,
43
+ hasFiltersEnabled,
44
+ };
45
+ };
46
+
47
+ const getFleetsEndpoint = ({
48
+ addDevicesCount,
49
+ name,
50
+ sortField,
51
+ direction,
52
+ }: {
53
+ addDevicesCount?: boolean;
54
+ name?: string;
55
+ sortField?: string;
56
+ direction?: string;
57
+ }) => {
58
+ const params = new URLSearchParams();
59
+ if (name) {
60
+ params.set('fieldSelector', `metadata.name contains ${name}`);
61
+ }
62
+ if (addDevicesCount) {
63
+ params.set('addDevicesCount', 'true');
64
+ }
65
+
66
+ if (sortField) {
67
+ params.set('sortBy', sortField);
68
+ params.set('sortOrder', direction || SortOrder.ASC);
69
+ }
70
+ return params.size ? `fleets?${params.toString()}` : 'fleets';
71
+ };
72
+
73
+ const useFleetsEndpoint = (args: FleetsEndpointArgs): [string, boolean] => {
74
+ const endpoint = getFleetsEndpoint(args);
75
+ const [fleetsEndpointDebounced] = useDebounce(endpoint, 1000);
76
+ return [fleetsEndpointDebounced, endpoint !== fleetsEndpointDebounced];
77
+ };
78
+
79
+ export const useFleets = (args: FleetsEndpointArgs): [Fleet[], boolean, unknown, boolean, VoidFunction] => {
80
+ const [fleetsEndpoint, fleetsDebouncing] = useFleetsEndpoint(args);
81
+ const [fleetsList, fleetsLoading, fleetsError, fleetsRefetch, updating] = useFetchPeriodically<FleetList>({
82
+ endpoint: fleetsEndpoint,
83
+ });
84
+
85
+ return [fleetsList?.items || [], fleetsLoading, fleetsError, updating || fleetsDebouncing, fleetsRefetch];
86
+ };
@@ -35,12 +35,13 @@ const StatusCard = () => {
35
35
  labels,
36
36
  });
37
37
 
38
- // TODO remove "useDevices" (to fetch labels), and fetching of fleets when the new API endpoints are available
39
- const [devices, loading, error, , , allLabels] = useDevices({
38
+ // TODO https://issues.redhat.com/browse/EDM-684 Use the new API endpoint to retrieve device labels
39
+ const [, /* devices */ loading, error, , , allLabels] = useDevices({
40
40
  ownerFleets: fleets,
41
41
  labels,
42
42
  });
43
43
 
44
+ // TODO https://issues.redhat.com/browse/EDM-683 Use the new API endpoint to retrieve fleet names
44
45
  const [fleetsList, flLoading, flError] = useFetchPeriodically<FleetList>({
45
46
  endpoint: 'fleets',
46
47
  });
@@ -59,7 +60,7 @@ const StatusCard = () => {
59
60
  <Stack>
60
61
  <StackItem>
61
62
  <TextContent>
62
- <Text component={TextVariants.small}>{t('{{count}} Devices', { count: devices.length || 0 })}</Text>
63
+ <Text component={TextVariants.small}>{t('{{count}} Devices', { count: devicesSummary?.total || 0 })}</Text>
63
64
  </TextContent>
64
65
  </StackItem>
65
66
  <StackItem>
@@ -13,18 +13,15 @@ import {
13
13
  TextContent,
14
14
  TextVariants,
15
15
  } from '@patternfly/react-core';
16
- import { EnrollmentRequestList } from '@flightctl/types';
17
16
  import { useTranslation } from '../../../../hooks/useTranslation';
18
- import { useFetchPeriodically } from '../../../../hooks/useFetchPeriodically';
19
- import { EnrollmentRequestStatus, getApprovalStatus } from '../../../../utils/status/enrollmentRequest';
17
+ import { usePendingEnrollmentRequestsCount } from '../../../../hooks/usePendingEnrollmentRequestsCount';
20
18
  import { Link, ROUTE } from '../../../../hooks/useNavigate';
21
19
  import ErrorAlert from '../../../ErrorAlert/ErrorAlert';
22
20
 
23
21
  const ToDoCard = () => {
24
22
  const { t } = useTranslation();
25
- const [erList, loading, error] = useFetchPeriodically<EnrollmentRequestList>({
26
- endpoint: 'enrollmentrequests',
27
- });
23
+
24
+ const [pendingErCount, loading, error] = usePendingEnrollmentRequestsCount();
28
25
 
29
26
  let content: React.ReactNode;
30
27
  if (loading) {
@@ -36,15 +33,14 @@ const ToDoCard = () => {
36
33
  } else if (error) {
37
34
  content = <ErrorAlert error={error} />;
38
35
  } else {
39
- const pendingErs = erList?.items.filter((er) => getApprovalStatus(er) === EnrollmentRequestStatus.Pending);
40
- if (pendingErs?.length) {
36
+ if (pendingErCount) {
41
37
  content = (
42
38
  <List>
43
39
  <ListItem>
44
40
  <Split hasGutter>
45
- <SplitItem isFilled>{t('{{ count }} devices pending approval', { count: pendingErs.length })}</SplitItem>
41
+ <SplitItem isFilled>{t('{{ count }} devices pending approval', { count: pendingErCount })}</SplitItem>
46
42
  <SplitItem>
47
- <Link to={ROUTE.DEVICES}>{t('Review pending devices', { count: pendingErs.length })}</Link>
43
+ <Link to={ROUTE.DEVICES}>{t('Review pending devices', { count: pendingErCount })}</Link>
48
44
  </SplitItem>
49
45
  </Split>
50
46
  </ListItem>
@@ -9,7 +9,7 @@ const Overview = () => {
9
9
  <GridItem>
10
10
  <StatusCard />
11
11
  </GridItem>
12
- <GridItem span={4}>
12
+ <GridItem md={6} lg={4}>
13
13
  <ToDoCard />
14
14
  </GridItem>
15
15
  </Grid>
@@ -44,7 +44,7 @@ const CreateRepository = () => {
44
44
  try {
45
45
  const results = await Promise.allSettled([
46
46
  get<Repository>(`repositories/${repositoryId}`),
47
- get<ResourceSyncList>(`resourcesyncs?repository=${repositoryId}`),
47
+ get<ResourceSyncList>(`resourcesyncs?fieldSelector=spec.repository=${repositoryId}`),
48
48
  ]);
49
49
 
50
50
  if (isPromiseFulfilled(results[0])) {
@@ -70,7 +70,7 @@ const CreateRepository = () => {
70
70
  const reload = async () => {
71
71
  try {
72
72
  setIsLoading(true);
73
- const rsList = await get<ResourceSyncList>(`resourcesyncs?labelSelector=repository=${repositoryId}`);
73
+ const rsList = await get<ResourceSyncList>(`resourcesyncs?fieldSelector=spec.repository${repositoryId}`);
74
74
  setResourceSyncs(rsList.items);
75
75
  setRsError(undefined);
76
76
  } catch (e) {
@@ -46,7 +46,7 @@ const DeleteRepositoryModal = ({ repositoryId, onClose, onDeleteSuccess }: Delet
46
46
 
47
47
  const loadRS = React.useCallback(async () => {
48
48
  try {
49
- const resourceSyncs = await get<ResourceSyncList>(`resourcesyncs?repository=${repositoryId}`);
49
+ const resourceSyncs = await get<ResourceSyncList>(`resourcesyncs?fieldSelector=spec.repository=${repositoryId}`);
50
50
  setResourceSyncIds(resourceSyncs.items.map((rs) => rs.metadata.name || ''));
51
51
  setRsError(undefined);
52
52
  } catch (e) {
@@ -136,6 +136,7 @@ const RepositoryTable = () => {
136
136
  </Toolbar>
137
137
  <Table
138
138
  aria-label={t('Repositories table')}
139
+ loading={loading}
139
140
  emptyFilters={filteredData.length === 0}
140
141
  emptyData={(repositoryList?.items.length || 0) === 0}
141
142
  isAllSelected={isAllSelected}
@@ -177,7 +177,7 @@ const CreateResourceSyncModal = ({
177
177
 
178
178
  const RepositoryResourceSyncList = ({ repositoryId }: { repositoryId: string }) => {
179
179
  const [rsList, isLoading, error, refetch] = useFetchPeriodically<ResourceSyncList>({
180
- endpoint: `resourcesyncs?repository=${repositoryId}`,
180
+ endpoint: `resourcesyncs?fieldSelector=spec.repository=${repositoryId}`,
181
181
  });
182
182
 
183
183
  const resourceSyncs = rsList?.items || [];
@@ -249,6 +249,7 @@ const RepositoryResourceSyncList = ({ repositoryId }: { repositoryId: string })
249
249
  )}
250
250
  <Table
251
251
  aria-label={t('Resource syncs table')}
252
+ loading={isLoading}
252
253
  isAllSelected={isAllSelected}
253
254
  onSelectAll={setAllSelected}
254
255
  columns={columns}
@@ -1,9 +1,19 @@
1
1
  import * as React from 'react';
2
- import { Bullseye, PageSection } from '@patternfly/react-core';
2
+ import { Bullseye, PageSection, Spinner } from '@patternfly/react-core';
3
3
  import { Table as PFTable, Td, Th, ThProps, Thead, Tr } from '@patternfly/react-table';
4
4
  import { useTranslation } from '../../hooks/useTranslation';
5
5
  import WithHelperText from '../common/WithHelperText';
6
6
 
7
+ export type ApiSortTableColumn = {
8
+ name: string;
9
+ sortableField?: string;
10
+ defaultSort?: boolean;
11
+ helperText?: string;
12
+ thProps?: Omit<ThProps, 'sort'> & {
13
+ ref?: React.Ref<HTMLTableCellElement> | undefined;
14
+ };
15
+ };
16
+
7
17
  export type TableColumn<D> = {
8
18
  name: string;
9
19
  onSort?: (data: D[]) => D[];
@@ -17,8 +27,9 @@ export type TableColumn<D> = {
17
27
  type TableProps<D> = {
18
28
  columns: TableColumn<D>[];
19
29
  children: React.ReactNode;
20
- emptyFilters: boolean;
21
- emptyData: boolean;
30
+ loading: boolean;
31
+ emptyFilters?: boolean;
32
+ emptyData?: boolean;
22
33
  'aria-label': string;
23
34
  getSortParams: (columnIndex: number) => ThProps['sort'];
24
35
  onSelectAll?: (isSelected: boolean) => void;
@@ -30,6 +41,7 @@ type TableFC = <D>(props: TableProps<D>) => JSX.Element;
30
41
  const Table: TableFC = ({
31
42
  columns,
32
43
  children,
44
+ loading,
33
45
  emptyFilters,
34
46
  emptyData,
35
47
  getSortParams,
@@ -38,8 +50,10 @@ const Table: TableFC = ({
38
50
  ...rest
39
51
  }) => {
40
52
  const { t } = useTranslation();
41
- if (emptyFilters && !emptyData) {
42
- return (
53
+ if (emptyData && !emptyFilters) {
54
+ return loading ? (
55
+ <Spinner size="md" />
56
+ ) : (
43
57
  <PageSection variant="light">
44
58
  <Bullseye>{t('No resources are matching the current filters.')}</Bullseye>
45
59
  </PageSection>
@@ -3,6 +3,11 @@
3
3
  justify-content: center;
4
4
  }
5
5
 
6
+ .fctl-charts__donut-container {
7
+ width: 100%;
8
+ height: 100%;
9
+ }
10
+
6
11
  .fctl-charts__title {
7
12
  height: 100%;
8
13
  color: var(--pf-v5-global--Color--200);
@@ -75,7 +75,7 @@ const DonutChart = ({ data, title, helperText }: { data: Data[]; title: string;
75
75
  >
76
76
  <FlexItem className="fctl-charts__donut">
77
77
  <div style={{ height: '230px', width: '230px' }}>
78
- <ChartContainer>
78
+ <ChartContainer className="fctl-charts__donut-container">
79
79
  <foreignObject x="0" y="0" width="230px" height="230px">
80
80
  <Flex
81
81
  alignItems={{ default: 'alignItemsCenter' }}
@@ -37,7 +37,9 @@ const MassDeleteRepositoryModal: React.FC<MassDeleteRepositoryModalProps> = ({
37
37
  const rsCount = {};
38
38
  const promises = repositories.map(async (r) => {
39
39
  const repositoryId = r.metadata.name || '';
40
- const resourceSyncs = await get<ResourceSyncList>(`resourcesyncs?repository=${repositoryId}&limit=1`);
40
+ const resourceSyncs = await get<ResourceSyncList>(
41
+ `resourcesyncs?fieldSelector=spec.repository=${repositoryId}&limit=1`,
42
+ );
41
43
  rsCount[repositoryId] = getApiListCount(resourceSyncs);
42
44
  });
43
45
  await Promise.allSettled(promises);
@@ -51,7 +53,7 @@ const MassDeleteRepositoryModal: React.FC<MassDeleteRepositoryModalProps> = ({
51
53
  setProgress(0);
52
54
  const promises = repositories.map(async (r) => {
53
55
  const repositoryId = r.metadata.name || '';
54
- const resourceSyncs = await get<ResourceSyncList>(`resourcesyncs?repository=${repositoryId}`);
56
+ const resourceSyncs = await get<ResourceSyncList>(`resourcesyncs?fieldSelector=spec.repository=${repositoryId}`);
55
57
  const rsyncPromises = resourceSyncs.items.map((rsync) => remove(`resourcesyncs/${rsync.metadata.name}`));
56
58
  const rsyncResults = await Promise.allSettled(rsyncPromises);
57
59
  const rejectedResults = rsyncResults.filter(isPromiseRejected);
@@ -0,0 +1,49 @@
1
+ import * as React from 'react';
2
+ import { ThProps } from '@patternfly/react-table';
3
+
4
+ import { ApiSortTableColumn } from '../components/Table/Table';
5
+ import { SortOrder } from '@flightctl/types';
6
+
7
+ const getDefaultSortField = (columns: ApiSortTableColumn[]) => {
8
+ let defaultSortCol = columns.find((c) => c.defaultSort);
9
+ if (!defaultSortCol) {
10
+ defaultSortCol = columns.find((c) => !!c.sortableField);
11
+ }
12
+ return defaultSortCol?.sortableField || '';
13
+ };
14
+
15
+ export const useApiTableSort = (columns: ApiSortTableColumn[]) => {
16
+ const [activeSortField, setActiveSortField] = React.useState<string>(getDefaultSortField(columns));
17
+ const [activeSortDirection, setActiveSortDirection] = React.useState<SortOrder>(SortOrder.ASC);
18
+
19
+ const getSortParams = React.useCallback(
20
+ (columnIndex: number): ThProps['sort'] => {
21
+ const columnData = columns[columnIndex];
22
+ if (!columnData.sortableField) {
23
+ return undefined;
24
+ }
25
+
26
+ const activeColumnIndex = columns.findIndex((column) => column.sortableField === activeSortField);
27
+ return {
28
+ sortBy: {
29
+ index: activeColumnIndex === -1 ? 0 : activeColumnIndex,
30
+ direction: activeSortDirection === SortOrder.ASC ? 'asc' : 'desc',
31
+ defaultDirection: 'asc',
32
+ },
33
+ onSort: (_, index, direction) => {
34
+ const column = columns[index];
35
+ setActiveSortField(column.sortableField || '');
36
+ setActiveSortDirection(direction === 'asc' ? SortOrder.ASC : SortOrder.DESC);
37
+ },
38
+ columnIndex,
39
+ };
40
+ },
41
+ [columns, activeSortField, activeSortDirection],
42
+ );
43
+
44
+ return {
45
+ getSortParams,
46
+ sortField: activeSortField,
47
+ direction: activeSortField ? activeSortDirection : '',
48
+ };
49
+ };
@@ -12,7 +12,7 @@ export enum ROUTE {
12
12
  FLEETS = 'FLEETS',
13
13
  FLEET_CREATE = 'FLEET_CREATE',
14
14
  FLEET_IMPORT = 'FLEET_IMPORT',
15
- FLEET_DETAILS = ' FLEET_DETAILS',
15
+ FLEET_DETAILS = 'FLEET_DETAILS',
16
16
  FLEET_EDIT = 'FLEET_EDIT',
17
17
  DEVICES = 'DEVICES',
18
18
  DEVICE_DETAILS = 'DEVICE_DETAILS',
@@ -20,8 +20,8 @@ export enum ROUTE {
20
20
  REPOSITORIES = 'REPOSITORIES',
21
21
  REPO_CREATE = 'REPO_CREATE',
22
22
  REPO_EDIT = 'REPO_EDIT',
23
- REPO_DETAILS = ' REPO_DETAILS',
24
- RESOURCE_SYNC_DETAILS = ' RESOURCE_SYNC_DETAILS',
23
+ REPO_DETAILS = 'REPO_DETAILS',
24
+ RESOURCE_SYNC_DETAILS = 'RESOURCE_SYNC_DETAILS',
25
25
  ENROLLMENT_REQUESTS = 'ENROLLMENT_REQUESTS',
26
26
  ENROLLMENT_REQUEST_DETAILS = 'ENROLLMENT_REQUEST_DETAILS',
27
27
  }
@@ -0,0 +1,12 @@
1
+ import { EnrollmentRequestList } from '@flightctl/types';
2
+
3
+ import { useFetchPeriodically } from './useFetchPeriodically';
4
+ import { getApiListCount } from '../utils/api';
5
+
6
+ export const usePendingEnrollmentRequestsCount = (): [number, boolean, unknown] => {
7
+ const [erList, loading, error] = useFetchPeriodically<EnrollmentRequestList>({
8
+ endpoint: 'enrollmentrequests?fieldSelector=!status.approval.approved&limit=1',
9
+ });
10
+
11
+ return [getApiListCount(erList) || 0, loading, error];
12
+ };
@@ -0,0 +1,29 @@
1
+ import { FlightCtlLabel } from '../types/extraTypes';
2
+
3
+ const addQueryConditions = (fieldSelectors: string[], fieldSelector: string, values?: string[]) => {
4
+ if (values?.length === 1) {
5
+ fieldSelectors.push(`${fieldSelector}=${values[0]}`);
6
+ } else if (values?.length) {
7
+ fieldSelectors.push(`${fieldSelector} in (${values.join(',')})`);
8
+ }
9
+ };
10
+
11
+ const addTextContainsCondition = (fieldSelectors: string[], fieldSelector: string, value: string) => {
12
+ fieldSelectors.push(`${fieldSelector} contains ${value}`); // contains operator
13
+ };
14
+
15
+ const setLabelParams = (params: URLSearchParams, labels?: FlightCtlLabel[]) => {
16
+ if (labels?.length) {
17
+ const labelSelector = labels.reduce((acc, curr) => {
18
+ if (!acc) {
19
+ acc = `${curr.key}=${curr.value || ''}`;
20
+ } else {
21
+ acc += `,${curr.key}=${curr.value || ''}`;
22
+ }
23
+ return acc;
24
+ }, '');
25
+ params.append('labelSelector', labelSelector);
26
+ }
27
+ };
28
+
29
+ export { addQueryConditions, addTextContainsCondition, setLabelParams };