mirror of
https://github.com/portainer/portainer.git
synced 2025-07-18 21:09:40 +02:00
fix(k8s-services): avoid rerendering services table [r8s-387] (#832)
This commit is contained in:
parent
8d29b5ae71
commit
303047656e
9 changed files with 216 additions and 99 deletions
|
@ -21,7 +21,7 @@ export function useClusterRoles(
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...withGlobalError('Unable to get cluster roles'),
|
...withGlobalError('Unable to get cluster roles'),
|
||||||
...options,
|
refetchInterval: options?.autoRefreshRate,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,13 @@ import { PortainerNamespace } from '../types';
|
||||||
|
|
||||||
import { queryKeys } from './queryKeys';
|
import { queryKeys } from './queryKeys';
|
||||||
|
|
||||||
export function useNamespacesQuery(
|
export function useNamespacesQuery<T = PortainerNamespace[]>(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
options?: { autoRefreshRate?: number; withResourceQuota?: boolean }
|
options?: {
|
||||||
|
autoRefreshRate?: number;
|
||||||
|
withResourceQuota?: boolean;
|
||||||
|
select?: (namespaces: PortainerNamespace[]) => T;
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery(
|
||||||
queryKeys.list(environmentId, {
|
queryKeys.list(environmentId, {
|
||||||
|
@ -22,6 +26,7 @@ export function useNamespacesQuery(
|
||||||
refetchInterval() {
|
refetchInterval() {
|
||||||
return options?.autoRefreshRate ?? false;
|
return options?.autoRefreshRate ?? false;
|
||||||
},
|
},
|
||||||
|
select: options?.select,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { HttpResponse } from 'msw';
|
||||||
|
|
||||||
|
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||||
|
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||||
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||||
|
import { createMockUsers } from '@/react-tools/test-mocks';
|
||||||
|
import { server, http } from '@/setup-tests/server';
|
||||||
|
|
||||||
|
import { ServicesDatatable } from './ServicesDatatable';
|
||||||
|
|
||||||
|
vi.mock('@/react/hooks/useEnvironmentId', () => ({
|
||||||
|
useEnvironmentId: () => 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/portainer/services/notifications', () => ({
|
||||||
|
notifyError: vi.fn(),
|
||||||
|
notifySuccess: vi.fn(),
|
||||||
|
}));
|
||||||
|
function createMockServices(count: number) {
|
||||||
|
return Array.from({ length: count }, (_, i) => {
|
||||||
|
let namespace = 'default';
|
||||||
|
if (i % 3 === 0) {
|
||||||
|
namespace = 'kube-system';
|
||||||
|
} else if (i % 2 !== 0) {
|
||||||
|
namespace = 'my-namespace';
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = 'ClusterIP';
|
||||||
|
if (i % 4 === 1) {
|
||||||
|
type = 'NodePort';
|
||||||
|
} else if (i % 4 === 2) {
|
||||||
|
type = 'LoadBalancer';
|
||||||
|
} else if (i % 4 === 3) {
|
||||||
|
type = 'ExternalName';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
UID: `service-${i}`,
|
||||||
|
Name: `service-${i}`,
|
||||||
|
Namespace: namespace,
|
||||||
|
Type: type,
|
||||||
|
Ports: [{ Port: 80 + i, TargetPort: 8080 + i, Protocol: 'TCP' }],
|
||||||
|
Selector: { app: `app-${i}` },
|
||||||
|
CreationTimestamp: new Date(Date.now() - i * 1000 * 60).toISOString(),
|
||||||
|
ApplicationOwner: '',
|
||||||
|
Applications: [{ Name: `app-${i}` }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockServices = createMockServices(4);
|
||||||
|
|
||||||
|
const mockNamespaces = [
|
||||||
|
{
|
||||||
|
Name: 'default',
|
||||||
|
IsSystem: false,
|
||||||
|
Status: 'Active',
|
||||||
|
CreationTimestamp: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'kube-system',
|
||||||
|
IsSystem: true,
|
||||||
|
Status: 'Active',
|
||||||
|
CreationTimestamp: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'my-namespace',
|
||||||
|
IsSystem: false,
|
||||||
|
Status: 'Active',
|
||||||
|
CreationTimestamp: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
server.use(
|
||||||
|
http.get('/api/kubernetes/1/services', () =>
|
||||||
|
HttpResponse.json(mockServices)
|
||||||
|
),
|
||||||
|
http.get('/api/kubernetes/1/namespaces', () =>
|
||||||
|
HttpResponse.json(mockNamespaces)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const mockUser = {
|
||||||
|
...createMockUsers(1)[0],
|
||||||
|
PortainerAuthorizations: {
|
||||||
|
K8sAccessSystemNamespaces: true,
|
||||||
|
K8sServiceW: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createTestComponent() {
|
||||||
|
return withTestRouter(
|
||||||
|
withUserProvider(withTestQueryProvider(ServicesDatatable), mockUser),
|
||||||
|
{
|
||||||
|
route: '/kubernetes/services',
|
||||||
|
stateConfig: [
|
||||||
|
{
|
||||||
|
name: 'kubernetes.services',
|
||||||
|
url: '/kubernetes/services',
|
||||||
|
params: { endpointId: '1' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ServicesDatatable', () => {
|
||||||
|
it('renders services data correctly', async () => {
|
||||||
|
const TestComponent = createTestComponent();
|
||||||
|
render(<TestComponent />);
|
||||||
|
|
||||||
|
expect(await screen.findByText('service-1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('service-2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter system resources correctly when toggled', async () => {
|
||||||
|
const TestComponent = createTestComponent();
|
||||||
|
render(<TestComponent />);
|
||||||
|
|
||||||
|
const settingsButton = screen.getByRole('button', { name: /settings/i });
|
||||||
|
await userEvent.click(settingsButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('service-0')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const systemToggle = await screen.findByTestId('show-system-resources');
|
||||||
|
await userEvent.click(systemToggle);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('service-0')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('service-3')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('service-1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('service-2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show loading state when data is loading', async () => {
|
||||||
|
const TestComponent = createTestComponent();
|
||||||
|
render(<TestComponent />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
|
@ -16,6 +16,7 @@ import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultD
|
||||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
||||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||||
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
|
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
|
||||||
|
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||||
|
|
||||||
import { Datatable, Table, TableSettingsMenu } from '@@/datatables';
|
import { Datatable, Table, TableSettingsMenu } from '@@/datatables';
|
||||||
import { useTableState } from '@@/datatables/useTableState';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
@ -25,7 +26,6 @@ import { useMutationDeleteServices, useClusterServices } from '../../service';
|
||||||
import { Service } from '../../types';
|
import { Service } from '../../types';
|
||||||
|
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
import { createStore } from './datatable-store';
|
|
||||||
import { ServiceRowData } from './types';
|
import { ServiceRowData } from './types';
|
||||||
|
|
||||||
const storageKey = 'k8sServicesDatatable';
|
const storageKey = 'k8sServicesDatatable';
|
||||||
|
@ -34,52 +34,53 @@ const settingsStore = createStore(storageKey);
|
||||||
export function ServicesDatatable() {
|
export function ServicesDatatable() {
|
||||||
const tableState = useTableState(settingsStore, storageKey);
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
const { data: namespacesArray, ...namespacesQuery } =
|
const { data: namespaces, ...namespacesQuery } = useNamespacesQuery(
|
||||||
useNamespacesQuery(environmentId);
|
environmentId,
|
||||||
|
{
|
||||||
|
select: (namespacesArray) =>
|
||||||
|
namespacesArray?.reduce<Record<string, PortainerNamespace>>(
|
||||||
|
(acc, namespace) => {
|
||||||
|
acc[namespace.Name] = namespace;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { authorized: canWrite } = useAuthorizations(['K8sServiceW']);
|
||||||
|
const { authorized: canAccessSystemResources } = useAuthorizations(
|
||||||
|
'K8sAccessSystemNamespaces'
|
||||||
|
);
|
||||||
const { data: services, ...servicesQuery } = useClusterServices(
|
const { data: services, ...servicesQuery } = useClusterServices(
|
||||||
environmentId,
|
environmentId,
|
||||||
{
|
{
|
||||||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||||
withApplications: true,
|
withApplications: true,
|
||||||
|
select: (services) =>
|
||||||
|
services?.filter(
|
||||||
|
(service) =>
|
||||||
|
(canAccessSystemResources && tableState.showSystemResources) ||
|
||||||
|
!namespaces?.[service.Namespace]?.IsSystem
|
||||||
|
),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const namespaces: Record<string, PortainerNamespace> = {};
|
const servicesWithIsSystem = useServicesRowData(services || [], namespaces);
|
||||||
if (Array.isArray(namespacesArray)) {
|
|
||||||
for (let i = 0; i < namespacesArray.length; i++) {
|
|
||||||
const namespace = namespacesArray[i];
|
|
||||||
namespaces[namespace.Name] = namespace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { authorized: canWrite } = useAuthorizations(['K8sServiceW']);
|
|
||||||
const readOnly = !canWrite;
|
|
||||||
const { authorized: canAccessSystemResources } = useAuthorizations(
|
|
||||||
'K8sAccessSystemNamespaces'
|
|
||||||
);
|
|
||||||
const filteredServices = services?.filter(
|
|
||||||
(service) =>
|
|
||||||
(canAccessSystemResources && tableState.showSystemResources) ||
|
|
||||||
!namespaces?.[service.Namespace]?.IsSystem
|
|
||||||
);
|
|
||||||
|
|
||||||
const servicesWithIsSystem = useServicesRowData(
|
|
||||||
filteredServices || [],
|
|
||||||
namespaces
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Datatable
|
<Datatable
|
||||||
dataset={servicesWithIsSystem || []}
|
dataset={servicesWithIsSystem || []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
settingsManager={tableState}
|
settingsManager={tableState}
|
||||||
isLoading={servicesQuery.isLoading || namespacesQuery.isLoading}
|
isLoading={
|
||||||
|
servicesQuery.isInitialLoading || namespacesQuery.isInitialLoading
|
||||||
|
}
|
||||||
emptyContentLabel="No services found"
|
emptyContentLabel="No services found"
|
||||||
title="Services"
|
title="Services"
|
||||||
titleIcon={Shuffle}
|
titleIcon={Shuffle}
|
||||||
getRowId={(row) => row.UID}
|
getRowId={(row) => row.UID}
|
||||||
isRowSelectable={(row) => !namespaces?.[row.original.Namespace]?.IsSystem}
|
isRowSelectable={(row) => !namespaces?.[row.original.Namespace]?.IsSystem}
|
||||||
disableSelect={readOnly}
|
disableSelect={!canWrite}
|
||||||
renderTableActions={(selectedRows) => (
|
renderTableActions={(selectedRows) => (
|
||||||
<TableActions selectedItems={selectedRows} />
|
<TableActions selectedItems={selectedRows} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -23,18 +23,21 @@ export const queryKeys = {
|
||||||
*
|
*
|
||||||
* @returns The result of the query.
|
* @returns The result of the query.
|
||||||
*/
|
*/
|
||||||
export function useClusterServices(
|
export function useClusterServices<T = Service[]>(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
options?: { autoRefreshRate?: number; withApplications?: boolean }
|
options?: {
|
||||||
|
autoRefreshRate?: number;
|
||||||
|
withApplications?: boolean;
|
||||||
|
select?: (services: Service[]) => T;
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery(
|
||||||
queryKeys.clusterServices(environmentId),
|
queryKeys.clusterServices(environmentId),
|
||||||
async () => getClusterServices(environmentId, options?.withApplications),
|
async () => getClusterServices(environmentId, options?.withApplications),
|
||||||
{
|
{
|
||||||
...withGlobalError('Unable to get services.'),
|
...withGlobalError('Unable to get services.'),
|
||||||
refetchInterval() {
|
refetchInterval: options?.autoRefreshRate,
|
||||||
return options?.autoRefreshRate ?? false;
|
select: options?.select,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,17 @@ import {
|
||||||
} from '../../datatables/DefaultDatatableSettings';
|
} from '../../datatables/DefaultDatatableSettings';
|
||||||
import { SystemResourceDescription } from '../../datatables/SystemResourceDescription';
|
import { SystemResourceDescription } from '../../datatables/SystemResourceDescription';
|
||||||
import { useNamespacesQuery } from '../../namespaces/queries/useNamespacesQuery';
|
import { useNamespacesQuery } from '../../namespaces/queries/useNamespacesQuery';
|
||||||
import { useAllVolumesQuery } from '../queries/useVolumesQuery';
|
import {
|
||||||
|
convertToVolumeViewModels,
|
||||||
|
useAllVolumesQuery,
|
||||||
|
} from '../queries/useVolumesQuery';
|
||||||
import { isSystemNamespace } from '../../namespaces/queries/useIsSystemNamespace';
|
import { isSystemNamespace } from '../../namespaces/queries/useIsSystemNamespace';
|
||||||
import { useDeleteVolumes } from '../queries/useDeleteVolumes';
|
import { useDeleteVolumes } from '../queries/useDeleteVolumes';
|
||||||
import { isVolumeUsed } from '../utils';
|
import { isVolumeUsed } from '../utils';
|
||||||
|
import { K8sVolumeInfo } from '../types';
|
||||||
|
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
|
import { VolumeViewModel } from './types';
|
||||||
|
|
||||||
export function VolumesDatatable() {
|
export function VolumesDatatable() {
|
||||||
const tableState = useTableStateWithStorage<TableSettings>(
|
const tableState = useTableStateWithStorage<TableSettings>(
|
||||||
|
@ -45,21 +50,15 @@ export function VolumesDatatable() {
|
||||||
const namespaces = namespaceListQuery.data ?? [];
|
const namespaces = namespaceListQuery.data ?? [];
|
||||||
const volumesQuery = useAllVolumesQuery(envId, {
|
const volumesQuery = useAllVolumesQuery(envId, {
|
||||||
refetchInterval: tableState.autoRefreshRate * 1000,
|
refetchInterval: tableState.autoRefreshRate * 1000,
|
||||||
|
select: transformAndFilterVolumes,
|
||||||
});
|
});
|
||||||
const volumes = volumesQuery.data ?? [];
|
const volumes = volumesQuery.data ?? [];
|
||||||
|
|
||||||
const filteredVolumes = tableState.showSystemResources
|
|
||||||
? volumes
|
|
||||||
: volumes.filter(
|
|
||||||
(volume) =>
|
|
||||||
!isSystemNamespace(volume.ResourcePool.Namespace.Name, namespaces)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Datatable
|
<Datatable
|
||||||
data-cy="k8s-volumes-datatable"
|
data-cy="k8s-volumes-datatable"
|
||||||
isLoading={volumesQuery.isLoading || namespaceListQuery.isLoading}
|
isLoading={volumesQuery.isLoading || namespaceListQuery.isLoading}
|
||||||
dataset={filteredVolumes}
|
dataset={volumes}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
settingsManager={tableState}
|
settingsManager={tableState}
|
||||||
title="Volumes"
|
title="Volumes"
|
||||||
|
@ -96,4 +95,15 @@ export function VolumesDatatable() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function transformAndFilterVolumes(
|
||||||
|
volumes: K8sVolumeInfo[]
|
||||||
|
): VolumeViewModel[] {
|
||||||
|
const transformedVolumes = convertToVolumeViewModels(volumes);
|
||||||
|
return transformedVolumes.filter(
|
||||||
|
(volume) =>
|
||||||
|
tableState.showSystemResources ||
|
||||||
|
!isSystemNamespace(volume.ResourcePool.Namespace.Name, namespaces)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ export function useDeleteVolumes(environmentId: EnvironmentId) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
queryClient.invalidateQueries(queryKeys.storages(environmentId));
|
queryClient.invalidateQueries(queryKeys.storages(environmentId));
|
||||||
queryClient.invalidateQueries(queryKeys.volumes(environmentId));
|
return queryClient.invalidateQueries(queryKeys.volumes(environmentId));
|
||||||
},
|
},
|
||||||
...withGlobalError('Unable to remove volumes'),
|
...withGlobalError('Unable to remove volumes'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
|
||||||
import { withGlobalError } from '@/react-tools/react-query';
|
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|
||||||
|
|
||||||
import { K8sVolumeInfo } from '../types';
|
|
||||||
|
|
||||||
import { queryKeys } from './query-keys';
|
|
||||||
import { convertToVolumeViewModels } from './useVolumesQuery';
|
|
||||||
|
|
||||||
// useQuery to get a list of all volumes in a cluster
|
|
||||||
export function useNamespaceVolumes(
|
|
||||||
environmentId: EnvironmentId,
|
|
||||||
namespace: string,
|
|
||||||
queryOptions?: {
|
|
||||||
refetchInterval?: number;
|
|
||||||
withApplications?: boolean;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return useQuery(
|
|
||||||
queryKeys.volumes(environmentId),
|
|
||||||
() =>
|
|
||||||
getNamespaceVolumes(environmentId, namespace, {
|
|
||||||
withApplications: queryOptions?.withApplications ?? false,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
enabled: !!namespace,
|
|
||||||
refetchInterval: queryOptions?.refetchInterval,
|
|
||||||
select: convertToVolumeViewModels,
|
|
||||||
...withGlobalError('Unable to retrieve volumes'),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all volumes in a cluster
|
|
||||||
async function getNamespaceVolumes(
|
|
||||||
environmentId: EnvironmentId,
|
|
||||||
namespace: string,
|
|
||||||
params?: { withApplications: boolean }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get<K8sVolumeInfo[]>(
|
|
||||||
`/kubernetes/${environmentId}/namespaces/${namespace}/volumes`,
|
|
||||||
{ params }
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
throw parseAxiosError(e, 'Unable to retrieve volumes');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,10 +14,11 @@ import { appOwnerLabel } from '../../applications/constants';
|
||||||
import { queryKeys } from './query-keys';
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
// useQuery to get a list of all volumes in a cluster
|
// useQuery to get a list of all volumes in a cluster
|
||||||
export function useAllVolumesQuery(
|
export function useAllVolumesQuery<T = K8sVolumeInfo>(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
queryOptions?: {
|
queryOptions?: {
|
||||||
refetchInterval?: number;
|
refetchInterval?: number;
|
||||||
|
select?: (volumes: K8sVolumeInfo[]) => T[];
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery(
|
||||||
|
@ -25,7 +26,7 @@ export function useAllVolumesQuery(
|
||||||
() => getAllVolumes(environmentId, { withApplications: true }),
|
() => getAllVolumes(environmentId, { withApplications: true }),
|
||||||
{
|
{
|
||||||
refetchInterval: queryOptions?.refetchInterval,
|
refetchInterval: queryOptions?.refetchInterval,
|
||||||
select: convertToVolumeViewModels,
|
select: queryOptions?.select,
|
||||||
...withGlobalError('Unable to retrieve volumes'),
|
...withGlobalError('Unable to retrieve volumes'),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue