+
+ 32000m
+
+
+ 128Gi
+
|
+
+ 4000m
+
|
+
+ 15.6Gi
+
|
{
dispatchHeadlampEvent({
- resources: pods,
+ resources: pods ?? [],
resourceKind: 'Pod',
error: error || undefined,
});
diff --git a/frontend/src/components/pod/PodDetails.stories.tsx b/frontend/src/components/pod/PodDetails.stories.tsx
index e5fb2dc58a1..bd408c37591 100644
--- a/frontend/src/components/pod/PodDetails.stories.tsx
+++ b/frontend/src/components/pod/PodDetails.stories.tsx
@@ -6,17 +6,15 @@ import { TestContext } from '../../test';
import PodDetails from './Details';
import { podList } from './storyHelper';
-const usePhonyGet: KubeObjectClass['useGet'] = (name, namespace) => {
- return [
- new Pod(
+const usePhonyQuery: KubeObjectClass['useQuery'] = ({ name, namespace }: any): any => {
+ return {
+ data: new Pod(
podList.find(
pod => pod.metadata.name === name && pod.metadata.namespace === namespace
) as KubePod
),
- null,
- () => {},
- () => {},
- ] as any;
+ error: null,
+ };
};
export default {
@@ -31,21 +29,18 @@ export default {
} as Meta;
interface MockerStory {
- useGet?: KubeObjectClass['useGet'];
- useList?: KubeObjectClass['useList'];
+ usePhonyQuery?: KubeObjectClass['usePhonyQuery'];
objectEventsFunc?: typeof Event.objectEvents;
podName: string;
}
const Template: Story = args => {
- const { useGet, useList, podName, objectEventsFunc } = args;
+ const { usePhonyQuery, podName, objectEventsFunc } = args;
- if (!!useGet) {
- Pod.useGet = args.useGet!;
- }
- if (!!useList) {
- Pod.useList = args.useList!;
+ if (!!usePhonyQuery) {
+ Pod.useQuery = args.usePhonyQuery!;
}
+
if (!!objectEventsFunc) {
Event.objectEvents = objectEventsFunc!;
}
@@ -62,19 +57,19 @@ const Template: Story = args => {
export const PullBackOff = Template.bind({});
PullBackOff.args = {
- useGet: usePhonyGet,
+ usePhonyQuery: usePhonyQuery,
podName: 'imagepullbackoff',
};
export const Running = Template.bind({});
Running.args = {
- useGet: usePhonyGet,
+ usePhonyQuery: usePhonyQuery,
podName: 'running',
};
export const Error = Template.bind({});
Error.args = {
- useGet: usePhonyGet,
+ usePhonyQuery: usePhonyQuery,
podName: 'terminated',
objectEventsFunc: () =>
Promise.resolve([
@@ -114,18 +109,18 @@ Error.args = {
export const LivenessFailed = Template.bind({});
LivenessFailed.args = {
- useGet: usePhonyGet,
+ usePhonyQuery: usePhonyQuery,
podName: 'liveness-http',
};
export const Initializing = Template.bind({});
Initializing.args = {
- useGet: usePhonyGet,
+ usePhonyQuery: usePhonyQuery,
podName: 'initializing',
};
export const Successful = Template.bind({});
Successful.args = {
- useGet: usePhonyGet,
+ usePhonyQuery: usePhonyQuery,
podName: 'successful',
};
diff --git a/frontend/src/components/pod/PodList.stories.tsx b/frontend/src/components/pod/PodList.stories.tsx
index 39723dd93e4..7d0595f93eb 100644
--- a/frontend/src/components/pod/PodList.stories.tsx
+++ b/frontend/src/components/pod/PodList.stories.tsx
@@ -4,11 +4,10 @@ import { TestContext } from '../../test';
import PodList from './List';
import { podList } from './storyHelper';
-Pod.useList = () => {
+Pod.useListQuery = (() => {
const objList = podList.map(data => new Pod(data));
-
- return [objList, null, () => {}, () => {}] as any;
-};
+ return { items: objList, error: null };
+}) as any;
export default {
title: 'Pod/PodListView',
diff --git a/frontend/src/components/pod/PodLogs.stories.tsx b/frontend/src/components/pod/PodLogs.stories.tsx
index 5218c3983a4..8c7340cc6f7 100644
--- a/frontend/src/components/pod/PodLogs.stories.tsx
+++ b/frontend/src/components/pod/PodLogs.stories.tsx
@@ -6,30 +6,26 @@ import { TestContext } from '../../test';
import PodDetails, { PodDetailsProps } from './Details';
import { podList } from './storyHelper';
-const usePhonyGet: KubeObjectClass['useGet'] = (name, namespace) => {
- return [
- new Pod(
+const usePhonyQuery: KubeObjectClass['useQuery'] = ({ name, namespace }: any): any => {
+ return {
+ data: new Pod(
podList.find(
pod => pod.metadata.name === name && pod.metadata.namespace === namespace
) as KubePod
),
- null,
- () => {},
- () => {},
- ] as any;
+ error: null,
+ };
};
-const phonyGetAuthorization: KubeObjectClass['getAuthorization'] = async (
+const phonyGetAuthorization: KubeObjectClass['getAuthorization'] = (
verb: string,
resourceAttrs?: AuthRequestResourceAttrs
) => {
- return new Promise(exec => {
- exec({
- status: {
- allowed: resourceAttrs?.subresource === 'log',
- },
- });
- });
+ return {
+ status: {
+ allowed: resourceAttrs?.subresource === 'log',
+ },
+ };
};
// @todo: There is no "PodLogs" component. Stories should be named after the component.
@@ -102,7 +98,7 @@ function getLogs(container: string, onLogs: StreamResultsCb, logsOptions: LogOpt
export const Logs = Template.bind({});
Logs.args = {
- useGet: usePhonyGet,
+ useQuery: usePhonyQuery,
'prototype.getAuthorization': phonyGetAuthorization,
'prototype.getLogs': getLogs,
podName: 'running',
diff --git a/frontend/src/components/pod/__snapshots__/PodLogs.Logs.stories.storyshot b/frontend/src/components/pod/__snapshots__/PodLogs.Logs.stories.storyshot
index cca6d125393..b2f73be5b86 100644
--- a/frontend/src/components/pod/__snapshots__/PodLogs.Logs.stories.storyshot
+++ b/frontend/src/components/pod/__snapshots__/PodLogs.Logs.stories.storyshot
@@ -61,19 +61,7 @@
>
-
-
+ />
@@ -88,19 +76,7 @@
/>
-
-
+ />
diff --git a/frontend/src/components/podDisruptionBudget/pdbDetails.stories.tsx b/frontend/src/components/podDisruptionBudget/pdbDetails.stories.tsx
index 2a97dd9b781..c6637504670 100644
--- a/frontend/src/components/podDisruptionBudget/pdbDetails.stories.tsx
+++ b/frontend/src/components/podDisruptionBudget/pdbDetails.stories.tsx
@@ -1,4 +1,5 @@
import { Meta, Story } from '@storybook/react';
+import { useMockQuery } from '../../helpers/testHelpers';
import { KubeObjectClass } from '../../lib/k8s/cluster';
import { KubeObject } from '../../lib/k8s/cluster';
import Event from '../../lib/k8s/event';
@@ -6,54 +7,52 @@ import PDB, { KubePDB } from '../../lib/k8s/podDisruptionBudget';
import { TestContext } from '../../test';
import HPADetails from './Details';
-const usePhonyGet: KubeObjectClass['useGet'] = () => {
- return [
- new PDB({
- kind: 'PodDisruptionBudget',
- metadata: {
- annotations: {
- 'kubectl.kubernetes.io/last-applied-configuration':
- '{"apiVersion":"policy/v1beta1","kind":"PodDisruptionBudget","metadata":{"annotations":{},"labels":{"addonmanager.kubernetes.io/mode":"Reconcile"},"name":"coredns-pdb","namespace":"kube-system"},"spec":{"minAvailable":1,"selector":{"matchLabels":{"k8s-app":"kube-dns"}}}}\n',
- },
- selfLink: '',
- creationTimestamp: '2022-10-06T05:17:14Z',
- generation: 1,
- labels: {
- 'addonmanager.kubernetes.io/mode': 'Reconcile',
- },
- name: 'coredns-pdb',
- namespace: 'kube-system',
- resourceVersion: '3679611',
- uid: '80728de7-5d4f-42a2-bc4a-a8eb2a0ddabd',
+const usePhonyQuery = useMockQuery.data(
+ new PDB({
+ kind: 'PodDisruptionBudget',
+ metadata: {
+ annotations: {
+ 'kubectl.kubernetes.io/last-applied-configuration':
+ '{"apiVersion":"policy/v1beta1","kind":"PodDisruptionBudget","metadata":{"annotations":{},"labels":{"addonmanager.kubernetes.io/mode":"Reconcile"},"name":"coredns-pdb","namespace":"kube-system"},"spec":{"minAvailable":1,"selector":{"matchLabels":{"k8s-app":"kube-dns"}}}}\n',
},
- spec: {
- minAvailable: 1,
- selector: {
- matchLabels: {
- 'k8s-app': 'kube-dns',
- },
- },
+ selfLink: '',
+ creationTimestamp: '2022-10-06T05:17:14Z',
+ generation: 1,
+ labels: {
+ 'addonmanager.kubernetes.io/mode': 'Reconcile',
},
- status: {
- conditions: [
- {
- lastTransitionTime: '2022-10-17T21:56:52Z',
- message: '',
- observedGeneration: 1,
- reason: 'SufficientPods',
- status: 'True',
- type: 'DisruptionAllowed',
- },
- ],
- currentHealthy: 2,
- desiredHealthy: 1,
- disruptionsAllowed: 1,
- expectedPods: 2,
- observedGeneration: 1,
+ name: 'coredns-pdb',
+ namespace: 'kube-system',
+ resourceVersion: '3679611',
+ uid: '80728de7-5d4f-42a2-bc4a-a8eb2a0ddabd',
+ },
+ spec: {
+ minAvailable: 1,
+ selector: {
+ matchLabels: {
+ 'k8s-app': 'kube-dns',
+ },
},
- } as KubePDB),
- ] as any;
-};
+ },
+ status: {
+ conditions: [
+ {
+ lastTransitionTime: '2022-10-17T21:56:52Z',
+ message: '',
+ observedGeneration: 1,
+ reason: 'SufficientPods',
+ status: 'True',
+ type: 'DisruptionAllowed',
+ },
+ ],
+ currentHealthy: 2,
+ desiredHealthy: 1,
+ disruptionsAllowed: 1,
+ expectedPods: 2,
+ observedGeneration: 1,
+ },
+ } as KubePDB)
+);
export default {
title: 'pdb/PDBDetailsView',
@@ -71,27 +70,22 @@ export default {
} as Meta;
interface MockerStory {
- useGet?: KubeObjectClass['useGet'];
- useList?: KubeObjectClass['useList'];
+ usePhonyQuery?: KubeObjectClass['usePhonyQuery'];
allowEdit?: boolean;
}
const Template: Story = (args: MockerStory) => {
- if (!!args.useGet) {
- PDB.useGet = args.useGet;
+ if (!!args.usePhonyQuery) {
+ PDB.useQuery = args.usePhonyQuery;
Event.objectEvents = async (obj: KubeObject) => {
console.log('object:', obj);
return [];
};
}
- if (!!args.useList) {
- PDB.useList = args.useList;
- }
+
if (!!args.allowEdit) {
- PDB.getAuthorization = (): Promise<{ status: any }> => {
- return new Promise(resolve => {
- resolve({ status: { allowed: true, reason: '', code: 200 } });
- });
+ PDB.getAuthorization = () => {
+ return { status: { allowed: true, reason: '', code: 200 } };
};
}
@@ -100,16 +94,16 @@ const Template: Story = (args: MockerStory) => {
export const Default = Template.bind({});
Default.args = {
- useGet: usePhonyGet,
+ usePhonyQuery: usePhonyQuery,
allowEdit: true,
};
export const NoItemYet = Template.bind({});
NoItemYet.args = {
- useGet: () => [null, null, () => {}, () => {}] as any,
+ usePhonyQuery: useMockQuery.noData,
};
export const Error = Template.bind({});
Error.args = {
- useGet: () => [null, 'Phony error is phony!', () => {}, () => {}] as any,
+ usePhonyQuery: useMockQuery.error,
};
diff --git a/frontend/src/components/podDisruptionBudget/pdbList.stories.tsx b/frontend/src/components/podDisruptionBudget/pdbList.stories.tsx
index bd01c3b1948..cc9f0002e15 100644
--- a/frontend/src/components/podDisruptionBudget/pdbList.stories.tsx
+++ b/frontend/src/components/podDisruptionBudget/pdbList.stories.tsx
@@ -1,11 +1,12 @@
import { Meta, Story } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import PDB, { KubePDB } from '../../lib/k8s/podDisruptionBudget';
import { TestContext } from '../../test';
import { generateK8sResourceList } from '../../test/mocker';
import PDBList from './List';
-PDB.useList = () => {
- const objList = generateK8sResourceList(
+PDB.useListQuery = useMockListQuery.data(
+ generateK8sResourceList(
{
kind: 'PodDisruptionBudget',
metadata: {
@@ -50,9 +51,8 @@ PDB.useList = () => {
},
},
{ instantiateAs: PDB }
- );
- return [objList, null, () => {}, () => {}] as any;
-};
+ )
+);
export default {
title: 'PDB/PDBListView',
diff --git a/frontend/src/components/priorityClass/priorityClassDetails.stories.tsx b/frontend/src/components/priorityClass/priorityClassDetails.stories.tsx
index 7a6e11a0330..c68171c9e5e 100644
--- a/frontend/src/components/priorityClass/priorityClassDetails.stories.tsx
+++ b/frontend/src/components/priorityClass/priorityClassDetails.stories.tsx
@@ -1,32 +1,31 @@
import { Meta, Story } from '@storybook/react';
+import { useMockQuery } from '../../helpers/testHelpers';
import { KubeObject, KubeObjectClass } from '../../lib/k8s/cluster';
import Event from '../../lib/k8s/event';
import PriorityClass, { KubePriorityClass } from '../../lib/k8s/priorityClass';
import { TestContext } from '../../test';
import HPADetails from './Details';
-const usePhonyGet: KubeObjectClass['useGet'] = () => {
- return [
- new PriorityClass({
- description: 'Mission Critical apps.',
- kind: 'PriorityClass',
- metadata: {
- annotations: {
- 'kubectl.kubernetes.io/last-applied-configuration':
- '{"apiVersion":"scheduling.k8s.io/v1","description":"Mission Critical apps.","globalDefault":false,"kind":"PriorityClass","metadata":{"annotations":{},"name":"high-priority-apps"},"preemptionPolicy":"PreemptLowerPriority","value":1000000}\n',
- },
- selfLink: '',
- creationTimestamp: '2022-10-26T13:46:17Z',
- generation: 1,
- name: 'high-priority-apps',
- resourceVersion: '6474045',
- uid: '4cfbe956-a997-4b58-8ea3-18655d0ba8a9',
+const usePhonyGet = useMockQuery.data(
+ new PriorityClass({
+ description: 'Mission Critical apps.',
+ kind: 'PriorityClass',
+ metadata: {
+ annotations: {
+ 'kubectl.kubernetes.io/last-applied-configuration':
+ '{"apiVersion":"scheduling.k8s.io/v1","description":"Mission Critical apps.","globalDefault":false,"kind":"PriorityClass","metadata":{"annotations":{},"name":"high-priority-apps"},"preemptionPolicy":"PreemptLowerPriority","value":1000000}\n',
},
- preemptionPolicy: 'PreemptLowerPriority',
- value: 1000000,
- } as KubePriorityClass),
- ] as any;
-};
+ selfLink: '',
+ creationTimestamp: '2022-10-26T13:46:17Z',
+ generation: 1,
+ name: 'high-priority-apps',
+ resourceVersion: '6474045',
+ uid: '4cfbe956-a997-4b58-8ea3-18655d0ba8a9',
+ },
+ preemptionPolicy: 'PreemptLowerPriority',
+ value: 1000000,
+ } as KubePriorityClass)
+);
export default {
title: 'PriorityClass/PriorityClassDetailsView',
@@ -44,27 +43,21 @@ export default {
} as Meta;
interface MockerStory {
- useGet?: KubeObjectClass['useGet'];
- useList?: KubeObjectClass['useList'];
+ useQuery?: KubeObjectClass['useQuery'];
allowEdit?: boolean;
}
const Template: Story = (args: MockerStory) => {
- if (!!args.useGet) {
- PriorityClass.useGet = args.useGet;
+ if (!!args.useQuery) {
+ PriorityClass.useQuery = args.useQuery;
Event.objectEvents = async (obj: KubeObject) => {
console.log('object:', obj);
return [];
};
}
- if (!!args.useList) {
- PriorityClass.useList = args.useList;
- }
if (!!args.allowEdit) {
- PriorityClass.getAuthorization = (): Promise<{ status: any }> => {
- return new Promise(resolve => {
- resolve({ status: { allowed: true, reason: '', code: 200 } });
- });
+ PriorityClass.getAuthorization = () => {
+ return { status: { allowed: true, reason: '', code: 200 } };
};
}
@@ -73,16 +66,16 @@ const Template: Story = (args: MockerStory) => {
export const Default = Template.bind({});
Default.args = {
- useGet: usePhonyGet,
+ useQuery: usePhonyGet,
allowEdit: true,
};
export const NoItemYet = Template.bind({});
NoItemYet.args = {
- useGet: () => [null, null, () => {}, () => {}] as any,
+ useQuery: useMockQuery.noData,
};
export const Error = Template.bind({});
Error.args = {
- useGet: () => [null, 'Phony error is phony!', () => {}, () => {}] as any,
+ useQuery: useMockQuery.error,
};
diff --git a/frontend/src/components/priorityClass/priorityClassList.stories.tsx b/frontend/src/components/priorityClass/priorityClassList.stories.tsx
index 18ac92d1858..a01095d8c21 100644
--- a/frontend/src/components/priorityClass/priorityClassList.stories.tsx
+++ b/frontend/src/components/priorityClass/priorityClassList.stories.tsx
@@ -1,11 +1,12 @@
import { Meta, Story } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import PriorityClass, { KubePriorityClass } from '../../lib/k8s/priorityClass';
import { TestContext } from '../../test';
import { generateK8sResourceList } from '../../test/mocker';
import PriorityClassList from './List';
-PriorityClass.useList = () => {
- const objList = generateK8sResourceList(
+PriorityClass.useListQuery = useMockListQuery.data(
+ generateK8sResourceList(
{
description: 'Mission Critical apps.',
kind: 'PriorityClass',
@@ -24,9 +25,8 @@ PriorityClass.useList = () => {
value: 1000000,
},
{ instantiateAs: PriorityClass }
- );
- return [objList, null, () => {}, () => {}] as any;
-};
+ )
+);
export default {
title: 'PriorityClass/PriorityClassListView',
diff --git a/frontend/src/components/replicaset/List.stories.tsx b/frontend/src/components/replicaset/List.stories.tsx
index 6541a23a30c..8dd33b13fa6 100644
--- a/frontend/src/components/replicaset/List.stories.tsx
+++ b/frontend/src/components/replicaset/List.stories.tsx
@@ -1,11 +1,12 @@
import Container from '@mui/material/Container';
import { Meta, Story } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import ReplicaSet from '../../lib/k8s/replicaSet';
import { TestContext } from '../../test';
import List from './List';
-ReplicaSet.useList = () => {
- const objList = [
+ReplicaSet.useListQuery = useMockListQuery.data(
+ [
{
apiVersion: 'apps/v1',
kind: 'ReplicaSet',
@@ -217,9 +218,8 @@ ReplicaSet.useList = () => {
},
},
},
- ].map((data: any) => new ReplicaSet(data));
- return [objList, null, () => {}, () => {}] as any;
-};
+ ].map((data: any) => new ReplicaSet(data))
+);
export default {
title: 'ReplicaSet/List',
diff --git a/frontend/src/components/resourceQuota/List.tsx b/frontend/src/components/resourceQuota/List.tsx
index 50a5bd88a24..4f504280deb 100644
--- a/frontend/src/components/resourceQuota/List.tsx
+++ b/frontend/src/components/resourceQuota/List.tsx
@@ -83,7 +83,7 @@ export function ResourceQuotaRenderer(props: ResourceQuotaProps) {
}
export default function ResourceQuotaList() {
- const [resourceQuotas, error] = ResourceQuota.useList();
+ const { items, error } = ResourceQuota.useListQuery();
- return ;
+ return ;
}
diff --git a/frontend/src/components/resourceQuota/resourceQuotaDetails.stories.tsx b/frontend/src/components/resourceQuota/resourceQuotaDetails.stories.tsx
index 1377682f3a0..5e2a0c98e94 100644
--- a/frontend/src/components/resourceQuota/resourceQuotaDetails.stories.tsx
+++ b/frontend/src/components/resourceQuota/resourceQuotaDetails.stories.tsx
@@ -1,45 +1,44 @@
import { Meta, Story } from '@storybook/react';
+import { useMockQuery } from '../../helpers/testHelpers';
import { KubeObjectClass } from '../../lib/k8s/cluster';
import ResourceQuota, { KubeResourceQuota } from '../../lib/k8s/resourceQuota';
import { TestContext } from '../../test';
import ResourceQuotaDetails from './Details';
-const usePhonyGet: KubeObjectClass['useGet'] = () => {
- return [
- new ResourceQuota({
- apiVersion: 'v1',
- kind: 'ResourceQuota',
- metadata: {
- annotations: {
- 'kubectl.kubernetes.io/last-applied-configuration':
- '{"apiVersion":"v1","kind":"ResourceQuota","metadata":{"annotations":{},"name":"test-cpu-quota","namespace":"test"},"spec":{"hard":{"limits.cpu":"300m","requests.cpu":"200m"}}}\n',
- },
- selfLink: '',
- creationTimestamp: '2022-10-25T11:48:48Z',
- name: 'test-cpu-quota',
- namespace: 'test',
- resourceVersion: '6480949',
- uid: 'ebee95aa-f0a2-43d7-bd27-c7e756d0b163',
+const usePhonyQuery = useMockQuery.data(
+ new ResourceQuota({
+ apiVersion: 'v1',
+ kind: 'ResourceQuota',
+ metadata: {
+ annotations: {
+ 'kubectl.kubernetes.io/last-applied-configuration':
+ '{"apiVersion":"v1","kind":"ResourceQuota","metadata":{"annotations":{},"name":"test-cpu-quota","namespace":"test"},"spec":{"hard":{"limits.cpu":"300m","requests.cpu":"200m"}}}\n',
},
- spec: {
- hard: {
- 'limits.cpu': '300m',
- 'requests.cpu': '200m',
- },
+ selfLink: '',
+ creationTimestamp: '2022-10-25T11:48:48Z',
+ name: 'test-cpu-quota',
+ namespace: 'test',
+ resourceVersion: '6480949',
+ uid: 'ebee95aa-f0a2-43d7-bd27-c7e756d0b163',
+ },
+ spec: {
+ hard: {
+ 'limits.cpu': '300m',
+ 'requests.cpu': '200m',
},
- status: {
- hard: {
- 'limits.cpu': '300m',
- 'requests.cpu': '200m',
- },
- used: {
- 'limits.cpu': '0',
- 'requests.cpu': '500m',
- },
+ },
+ status: {
+ hard: {
+ 'limits.cpu': '300m',
+ 'requests.cpu': '200m',
},
- } as KubeResourceQuota),
- ] as any;
-};
+ used: {
+ 'limits.cpu': '0',
+ 'requests.cpu': '500m',
+ },
+ },
+ } as KubeResourceQuota)
+);
export default {
title: 'ResourceQuota/ResourceQuotaDetailsView',
@@ -57,24 +56,18 @@ export default {
} as Meta;
interface MockerStory {
- useGet?: KubeObjectClass['useGet'];
- useList?: KubeObjectClass['useList'];
+ useQuery?: KubeObjectClass['useQuery'];
allowEdit?: boolean;
}
const Template: Story = (args: MockerStory) => {
- if (!!args.useGet) {
- ResourceQuota.useGet = args.useGet;
- }
- if (!!args.useList) {
- ResourceQuota.useList = args.useList;
+ if (!!args.useQuery) {
+ ResourceQuota.useQuery = args.useQuery;
}
if (!!args.allowEdit) {
- ResourceQuota.getAuthorization = (): Promise<{ status: any }> => {
- return new Promise(resolve => {
- resolve({ status: { allowed: true, reason: '', code: 200 } });
- });
- };
+ ResourceQuota.getAuthorization = () => ({
+ status: { allowed: true, reason: '', code: 200 },
+ });
}
return ;
@@ -82,16 +75,16 @@ const Template: Story = (args: MockerStory) => {
export const Default = Template.bind({});
Default.args = {
- useGet: usePhonyGet,
+ useQuery: usePhonyQuery,
allowEdit: true,
};
export const NoItemYet = Template.bind({});
NoItemYet.args = {
- useGet: () => [null, null, () => {}, () => {}] as any,
+ useQuery: useMockQuery.noData,
};
export const Error = Template.bind({});
Error.args = {
- useGet: () => [null, 'Phony error is phony!', () => {}, () => {}] as any,
+ useQuery: useMockQuery.error,
};
diff --git a/frontend/src/components/resourceQuota/resourceQuotaList.stories.tsx b/frontend/src/components/resourceQuota/resourceQuotaList.stories.tsx
index 067996a6e36..78206839ca0 100644
--- a/frontend/src/components/resourceQuota/resourceQuotaList.stories.tsx
+++ b/frontend/src/components/resourceQuota/resourceQuotaList.stories.tsx
@@ -1,11 +1,12 @@
import { Meta, Story } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import ResourceQuota, { KubeResourceQuota } from '../../lib/k8s/resourceQuota';
import { TestContext } from '../../test';
import { generateK8sResourceList } from '../../test/mocker';
import ResourceQuotaList from './List';
-ResourceQuota.useList = () => {
- const objList = generateK8sResourceList(
+ResourceQuota.useListQuery = useMockListQuery.data(
+ generateK8sResourceList(
{
apiVersion: 'v1',
kind: 'ResourceQuota',
@@ -38,9 +39,8 @@ ResourceQuota.useList = () => {
},
},
{ instantiateAs: ResourceQuota }
- );
- return [objList, null, () => {}, () => {}] as any;
-};
+ )
+);
export default {
title: 'ResourceQuota/ResourceQuotaListView',
diff --git a/frontend/src/components/runtimeClass/Details.stories.tsx b/frontend/src/components/runtimeClass/Details.stories.tsx
index 408a3befa52..f31bb84cc8f 100644
--- a/frontend/src/components/runtimeClass/Details.stories.tsx
+++ b/frontend/src/components/runtimeClass/Details.stories.tsx
@@ -1,4 +1,5 @@
import { Meta, StoryFn } from '@storybook/react';
+import { useMockQuery } from '../../helpers/testHelpers';
import { KubeRuntimeClass, RuntimeClass } from '../../lib/k8s/runtime';
import { TestContext } from '../../test';
import { RuntimeClassDetails as Details } from './Details';
@@ -26,7 +27,7 @@ interface MockerStory {
const Template: StoryFn = (args: MockerStory) => {
const { json } = args;
if (!!json) {
- RuntimeClass.useGet = () => [new RuntimeClass(json), null, () => {}, () => {}] as any;
+ RuntimeClass.useQuery = useMockQuery.data(new RuntimeClass(json));
}
return ;
};
diff --git a/frontend/src/components/runtimeClass/List.stories.tsx b/frontend/src/components/runtimeClass/List.stories.tsx
index c3bdba0be13..aa82b91a0df 100644
--- a/frontend/src/components/runtimeClass/List.stories.tsx
+++ b/frontend/src/components/runtimeClass/List.stories.tsx
@@ -1,15 +1,14 @@
import { Meta, StoryFn } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import { KubeObject } from '../../lib/k8s/cluster';
import { RuntimeClass } from '../../lib/k8s/runtime';
import { TestContext } from '../../test';
import { RuntimeClassList } from './List';
import { RUNTIME_CLASS_DUMMY_DATA } from './storyHelper';
-RuntimeClass.useList = () => {
- const objList = RUNTIME_CLASS_DUMMY_DATA.map((data: KubeObject) => new RuntimeClass(data));
-
- return [objList, null, () => {}, () => {}] as any;
-};
+RuntimeClass.useListQuery = useMockListQuery.data(
+ RUNTIME_CLASS_DUMMY_DATA.map((data: KubeObject) => new RuntimeClass(data))
+);
export default {
title: 'RuntimeClass/ListView',
diff --git a/frontend/src/components/secret/Details.stories.tsx b/frontend/src/components/secret/Details.stories.tsx
index 05f65a457bc..00332308eca 100644
--- a/frontend/src/components/secret/Details.stories.tsx
+++ b/frontend/src/components/secret/Details.stories.tsx
@@ -1,4 +1,5 @@
import { Meta, StoryFn } from '@storybook/react';
+import { useMockQuery } from '../../helpers/testHelpers';
import Secret, { KubeSecret } from '../../lib/k8s/secret';
import { TestContext } from '../../test';
import SecretDetails from './Details';
@@ -26,7 +27,7 @@ interface MockerStory {
const Template: StoryFn = (args: MockerStory) => {
const { json } = args;
if (!!json) {
- Secret.useGet = () => [new Secret(json), null, () => {}, () => {}] as any;
+ Secret.useQuery = useMockQuery.data(new Secret(json));
}
return ;
};
diff --git a/frontend/src/components/secret/List.stories.tsx b/frontend/src/components/secret/List.stories.tsx
index c93ae63a3fe..b1eb346c985 100644
--- a/frontend/src/components/secret/List.stories.tsx
+++ b/frontend/src/components/secret/List.stories.tsx
@@ -1,15 +1,14 @@
import { Meta, StoryFn } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import { KubeObject } from '../../lib/k8s/cluster';
import Secret from '../../lib/k8s/secret';
import { TestContext } from '../../test';
import ListView from './List';
import { BASE_EMPTY_SECRET, BASE_SECRET } from './storyHelper';
-Secret.useList = () => {
- const objList = [BASE_EMPTY_SECRET, BASE_SECRET].map((data: KubeObject) => new Secret(data));
-
- return [objList, null, () => {}, () => {}] as any;
-};
+Secret.useListQuery = useMockListQuery.data(
+ [BASE_EMPTY_SECRET, BASE_SECRET].map((data: KubeObject) => new Secret(data))
+);
export default {
title: 'Secret/ListView',
diff --git a/frontend/src/components/service/Details.tsx b/frontend/src/components/service/Details.tsx
index 2c260aa8ad1..16fc5b9f004 100644
--- a/frontend/src/components/service/Details.tsx
+++ b/frontend/src/components/service/Details.tsx
@@ -18,7 +18,7 @@ export default function ServiceDetails() {
const { namespace, name } = useParams<{ namespace: string; name: string }>();
const { t } = useTranslation(['glossary', 'translation']);
- const [endpoints, endpointsError] = Endpoints.useList({ namespace });
+ const { items: endpoints, error: endpointsError } = Endpoints.useListQuery({ namespace });
function getOwnedEndpoints(item: Service) {
return item ? endpoints?.filter(endpoint => endpoint.getName() === item.getName()) : null;
@@ -93,7 +93,7 @@ export default function ServiceDetails() {
{endpointsError}
) : (
{
const { json } = args;
if (!!json) {
- PersistentVolumeClaim.useGet = () =>
- [new PersistentVolumeClaim(json), null, () => {}, () => {}] as any;
+ PersistentVolumeClaim.useQuery = useMockQuery.data(new PersistentVolumeClaim(json));
}
return ;
};
diff --git a/frontend/src/components/storage/ClaimList.stories.tsx b/frontend/src/components/storage/ClaimList.stories.tsx
index 61a004040d9..fd72b2d5a7e 100644
--- a/frontend/src/components/storage/ClaimList.stories.tsx
+++ b/frontend/src/components/storage/ClaimList.stories.tsx
@@ -1,5 +1,6 @@
import { Meta, Story } from '@storybook/react';
import _ from 'lodash';
+import { useMockListQuery } from '../../helpers/testHelpers';
import PersistentVolumeClaim, {
KubePersistentVolumeClaim,
} from '../../lib/k8s/persistentVolumeClaim';
@@ -7,29 +8,27 @@ import { TestContext } from '../../test';
import ListView from './ClaimList';
import { BASE_PVC } from './storyHelper';
-PersistentVolumeClaim.useList = () => {
- const noStorageClassNamePVC = _.cloneDeep(BASE_PVC);
- noStorageClassNamePVC.metadata.name = 'no-storage-class-name-pvc';
- noStorageClassNamePVC.spec!.storageClassName = '';
+const noStorageClassNamePVC = _.cloneDeep(BASE_PVC);
+noStorageClassNamePVC.metadata.name = 'no-storage-class-name-pvc';
+noStorageClassNamePVC.spec!.storageClassName = '';
- const noVolumeNamePVC = _.cloneDeep(BASE_PVC);
- noVolumeNamePVC.metadata.name = 'no-volume-name-pvc';
- noVolumeNamePVC.spec = {
- accessModes: ['ReadWriteOnce'],
- volumeMode: 'Block',
- resources: {
- requests: {
- storage: '10Gi',
- },
+const noVolumeNamePVC = _.cloneDeep(BASE_PVC);
+noVolumeNamePVC.metadata.name = 'no-volume-name-pvc';
+noVolumeNamePVC.spec = {
+ accessModes: ['ReadWriteOnce'],
+ volumeMode: 'Block',
+ resources: {
+ requests: {
+ storage: '10Gi',
},
- };
-
- const objList = [BASE_PVC, noStorageClassNamePVC, noVolumeNamePVC].map(
- pvc => new PersistentVolumeClaim(pvc as KubePersistentVolumeClaim)
- );
- return [objList, null, () => {}, () => {}] as any;
+ },
};
+const objList = [BASE_PVC, noStorageClassNamePVC, noVolumeNamePVC].map(
+ pvc => new PersistentVolumeClaim(pvc as KubePersistentVolumeClaim)
+);
+PersistentVolumeClaim.useListQuery = useMockListQuery.data(objList);
+
export default {
title: 'PersistentVolumeClaim/ListView',
component: ListView,
diff --git a/frontend/src/components/storage/ClassDetails.stories.tsx b/frontend/src/components/storage/ClassDetails.stories.tsx
index 1e5633219f0..02d69004c14 100644
--- a/frontend/src/components/storage/ClassDetails.stories.tsx
+++ b/frontend/src/components/storage/ClassDetails.stories.tsx
@@ -1,4 +1,5 @@
import { Meta, Story } from '@storybook/react';
+import { useMockQuery } from '../../helpers/testHelpers';
import StorageClass, { KubeStorageClass } from '../../lib/k8s/storageClass';
import { TestContext } from '../../test';
import Details from './ClaimDetails';
@@ -26,7 +27,7 @@ interface MockerStory {
const Template: Story = (args: MockerStory) => {
const { json } = args;
if (!!json) {
- StorageClass.useGet = () => [new StorageClass(json), null, () => {}, () => {}] as any;
+ StorageClass.useQuery = useMockQuery.data(new StorageClass(json));
}
return ;
};
diff --git a/frontend/src/components/storage/ClassList.stories.tsx b/frontend/src/components/storage/ClassList.stories.tsx
index f084870815a..c8d148b377d 100644
--- a/frontend/src/components/storage/ClassList.stories.tsx
+++ b/frontend/src/components/storage/ClassList.stories.tsx
@@ -1,15 +1,14 @@
import { Meta, Story } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import { KubeObject } from '../../lib/k8s/cluster';
import StorageClass from '../../lib/k8s/storageClass';
import { TestContext } from '../../test';
import ListView from './ClassList';
import { BASE_SC } from './storyHelper';
-StorageClass.useList = () => {
- const objList = [BASE_SC].map((data: KubeObject) => new StorageClass(data));
-
- return [objList, null, () => {}, () => {}] as any;
-};
+StorageClass.useListQuery = useMockListQuery.data(
+ [BASE_SC].map((data: KubeObject) => new StorageClass(data))
+);
export default {
title: 'StorageClass/ListView',
diff --git a/frontend/src/components/storage/VolumeDetails.stories.tsx b/frontend/src/components/storage/VolumeDetails.stories.tsx
index 0a0377d275c..d666117b9a4 100644
--- a/frontend/src/components/storage/VolumeDetails.stories.tsx
+++ b/frontend/src/components/storage/VolumeDetails.stories.tsx
@@ -1,4 +1,5 @@
import { Meta, Story } from '@storybook/react';
+import { useMockQuery } from '../../helpers/testHelpers';
import PersistentVolume, { KubePersistentVolume } from '../../lib/k8s/persistentVolume';
import { TestContext } from '../../test';
import Details from './ClaimDetails';
@@ -26,7 +27,7 @@ interface MockerStory {
const Template: Story = (args: MockerStory) => {
const { json } = args;
if (!!json) {
- PersistentVolume.useGet = () => [new PersistentVolume(json), null, () => {}, () => {}] as any;
+ PersistentVolume.useQuery = useMockQuery.data(new PersistentVolume(json));
}
return ;
};
diff --git a/frontend/src/components/storage/VolumeList.stories.tsx b/frontend/src/components/storage/VolumeList.stories.tsx
index 58b15a53a1b..1962b0e4cba 100644
--- a/frontend/src/components/storage/VolumeList.stories.tsx
+++ b/frontend/src/components/storage/VolumeList.stories.tsx
@@ -1,15 +1,14 @@
import { Meta, Story } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import { KubeObject } from '../../lib/k8s/cluster';
import PersistentVolume from '../../lib/k8s/persistentVolume';
import { TestContext } from '../../test';
import ListView from './ClassList';
import { BASE_PV } from './storyHelper';
-PersistentVolume.useList = () => {
- const objList = [BASE_PV].map((data: KubeObject) => new PersistentVolume(data));
-
- return [objList, null, () => {}, () => {}] as any;
-};
+PersistentVolume.useListQuery = useMockListQuery.data(
+ [BASE_PV].map((data: KubeObject) => new PersistentVolume(data))
+);
export default {
title: 'PersistentVolume/ListView',
diff --git a/frontend/src/components/verticalPodAutoscaler/VPADetails.stories.tsx b/frontend/src/components/verticalPodAutoscaler/VPADetails.stories.tsx
index 25033263db2..011296add4a 100644
--- a/frontend/src/components/verticalPodAutoscaler/VPADetails.stories.tsx
+++ b/frontend/src/components/verticalPodAutoscaler/VPADetails.stories.tsx
@@ -1,112 +1,111 @@
import { Meta, StoryFn } from '@storybook/react';
+import { useMockQuery } from '../../helpers/testHelpers';
import { KubeObject, KubeObjectClass } from '../../lib/k8s/cluster';
import Event from '../../lib/k8s/event';
import VPA from '../../lib/k8s/vpa';
import { TestContext } from '../../test';
import VpaDetails from './Details';
-const usePhonyGet: KubeObjectClass['useGet'] = () => {
- return [
- new VPA({
- apiVersion: 'autoscaling.k8s.io/v1',
- kind: 'VerticalPodAutoscaler',
- metadata: {
- annotations: {
- 'kubectl.kubernetes.io/last-applied-configuration':
- '{"apiVersion":"autoscaling.k8s.io/v1","kind":"VerticalPodAutoscaler","metadata":{"annotations":{},"name":"multi-container-vpa","namespace":"default"},"spec":{"resourcePolicy":{"containerPolicies":[{"containerName":"web-container","controlledResources":["cpu","memory"],"controlledValues":"RequestsAndLimits","minAllowed":{"cpu":"80m","memory":"512Mi"}},{"containerName":"db-container","controlledResources":["cpu","memory"],"controlledValues":"RequestsAndLimits","minAllowed":{"cpu":"1000m","memory":"2Gi"}}]},"targetRef":{"apiVersion":"apps/v1","kind":"Deployment","name":"multi-container-deployment"},"updatePolicy":{"updateMode":"Auto"}}}\n',
- },
- creationTimestamp: '2023-11-23T07:18:45Z',
- name: 'multi-container-vpa',
- namespace: 'default',
- resourceVersion: '111487',
- uid: '79cf71ba-81f4-4e7b-957d-8625c3afb0c1',
+const usePhonyQuery = useMockQuery.data(
+ new VPA({
+ apiVersion: 'autoscaling.k8s.io/v1',
+ kind: 'VerticalPodAutoscaler',
+ metadata: {
+ annotations: {
+ 'kubectl.kubernetes.io/last-applied-configuration':
+ '{"apiVersion":"autoscaling.k8s.io/v1","kind":"VerticalPodAutoscaler","metadata":{"annotations":{},"name":"multi-container-vpa","namespace":"default"},"spec":{"resourcePolicy":{"containerPolicies":[{"containerName":"web-container","controlledResources":["cpu","memory"],"controlledValues":"RequestsAndLimits","minAllowed":{"cpu":"80m","memory":"512Mi"}},{"containerName":"db-container","controlledResources":["cpu","memory"],"controlledValues":"RequestsAndLimits","minAllowed":{"cpu":"1000m","memory":"2Gi"}}]},"targetRef":{"apiVersion":"apps/v1","kind":"Deployment","name":"multi-container-deployment"},"updatePolicy":{"updateMode":"Auto"}}}\n',
},
- spec: {
- resourcePolicy: {
- containerPolicies: [
- {
- containerName: 'web-container',
- controlledResources: ['cpu', 'memory'],
- controlledValues: 'RequestsAndLimits',
- minAllowed: {
- cpu: '80m',
- memory: '512Mi',
- },
+ creationTimestamp: '2023-11-23T07:18:45Z',
+ name: 'multi-container-vpa',
+ namespace: 'default',
+ resourceVersion: '111487',
+ uid: '79cf71ba-81f4-4e7b-957d-8625c3afb0c1',
+ },
+ spec: {
+ resourcePolicy: {
+ containerPolicies: [
+ {
+ containerName: 'web-container',
+ controlledResources: ['cpu', 'memory'],
+ controlledValues: 'RequestsAndLimits',
+ minAllowed: {
+ cpu: '80m',
+ memory: '512Mi',
},
- {
- containerName: 'db-container',
- controlledResources: ['cpu', 'memory'],
- controlledValues: 'RequestsAndLimits',
- minAllowed: {
- cpu: '1000m',
- memory: '2Gi',
- },
+ },
+ {
+ containerName: 'db-container',
+ controlledResources: ['cpu', 'memory'],
+ controlledValues: 'RequestsAndLimits',
+ minAllowed: {
+ cpu: '1000m',
+ memory: '2Gi',
},
- ],
- },
- targetRef: {
- apiVersion: 'apps/v1',
- kind: 'Deployment',
- name: 'multi-container-deployment',
- },
- updatePolicy: {
- updateMode: 'Auto',
- },
+ },
+ ],
+ },
+ targetRef: {
+ apiVersion: 'apps/v1',
+ kind: 'Deployment',
+ name: 'multi-container-deployment',
},
- status: {
- conditions: [
+ updatePolicy: {
+ updateMode: 'Auto',
+ },
+ },
+ status: {
+ conditions: [
+ {
+ lastTransitionTime: '2023-11-23T07:18:48Z',
+ status: 'True',
+ type: 'RecommendationProvided',
+ },
+ ],
+ recommendation: {
+ containerRecommendations: [
{
- lastTransitionTime: '2023-11-23T07:18:48Z',
- status: 'True',
- type: 'RecommendationProvided',
+ containerName: 'db-container',
+ lowerBound: {
+ cpu: '1',
+ memory: '2Gi',
+ },
+ target: {
+ cpu: '1',
+ memory: '2Gi',
+ },
+ uncappedTarget: {
+ cpu: '12m',
+ memory: '131072k',
+ },
+ upperBound: {
+ cpu: '1',
+ memory: '2Gi',
+ },
},
- ],
- recommendation: {
- containerRecommendations: [
- {
- containerName: 'db-container',
- lowerBound: {
- cpu: '1',
- memory: '2Gi',
- },
- target: {
- cpu: '1',
- memory: '2Gi',
- },
- uncappedTarget: {
- cpu: '12m',
- memory: '131072k',
- },
- upperBound: {
- cpu: '1',
- memory: '2Gi',
- },
+ {
+ containerName: 'web-container',
+ lowerBound: {
+ cpu: '80m',
+ memory: '512Mi',
},
- {
- containerName: 'web-container',
- lowerBound: {
- cpu: '80m',
- memory: '512Mi',
- },
- target: {
- cpu: '80m',
- memory: '512Mi',
- },
- uncappedTarget: {
- cpu: '12m',
- memory: '131072k',
- },
- upperBound: {
- cpu: '80m',
- memory: '512Mi',
- },
+ target: {
+ cpu: '80m',
+ memory: '512Mi',
},
- ],
- },
+ uncappedTarget: {
+ cpu: '12m',
+ memory: '131072k',
+ },
+ upperBound: {
+ cpu: '80m',
+ memory: '512Mi',
+ },
+ },
+ ],
},
- }),
- ] as any;
-};
+ },
+ })
+);
export default {
title: 'VPA/VPADetailsView',
@@ -124,13 +123,13 @@ export default {
} as Meta;
interface MockerStory {
- useGet?: KubeObjectClass['useGet'];
+ useQuery?: KubeObjectClass['useQuery'];
useList?: KubeObjectClass['useList'];
}
const Template: StoryFn = (args: MockerStory) => {
- if (!!args.useGet) {
- VPA.useGet = args.useGet;
+ if (!!args.useQuery) {
+ VPA.useQuery = args.useQuery;
Event.objectEvents = async (obj: KubeObject) => {
console.log('objectEvents', obj);
return [];
@@ -141,15 +140,15 @@ const Template: StoryFn = (args: MockerStory) => {
export const Default = Template.bind({});
Default.args = {
- useGet: usePhonyGet,
+ useQuery: usePhonyQuery,
};
export const NoItemYet = Template.bind({});
NoItemYet.args = {
- useGet: () => [null, null, () => {}, () => {}] as any,
+ useQuery: useMockQuery.noData,
};
export const Error = Template.bind({});
Error.args = {
- useGet: () => [null, 'Phony error is phony!', () => {}, () => {}] as any,
+ useQuery: useMockQuery.error,
};
diff --git a/frontend/src/components/verticalPodAutoscaler/VPAList.stories.tsx b/frontend/src/components/verticalPodAutoscaler/VPAList.stories.tsx
index 93b27bcc249..cc27668387d 100644
--- a/frontend/src/components/verticalPodAutoscaler/VPAList.stories.tsx
+++ b/frontend/src/components/verticalPodAutoscaler/VPAList.stories.tsx
@@ -1,12 +1,13 @@
import { Meta } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import VPA, { KubeVPA } from '../../lib/k8s/vpa';
import { TestContext } from '../../test';
import { generateK8sResourceList } from '../../test/mocker';
import VpaList from './List';
VPA.isEnabled = () => Promise.resolve(true);
-VPA.useList = () => {
- const objList = generateK8sResourceList(
+VPA.useListQuery = useMockListQuery.data(
+ generateK8sResourceList(
{
apiVersion: 'autoscaling.k8s.io/v1',
kind: 'VerticalPodAutoscaler',
@@ -106,9 +107,8 @@ VPA.useList = () => {
},
},
{ instantiateAs: VPA }
- );
- return [objList, null, () => {}, () => {}] as any;
-};
+ )
+);
export default {
title: 'VPA/VPAListView',
diff --git a/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx b/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx
index e618b9fab92..47f06eb6050 100644
--- a/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx
+++ b/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx
@@ -1,11 +1,12 @@
import { Meta, Story } from '@storybook/react';
-import MWC, { KubeMutatingWebhookConfiguration } from '../../lib/k8s/mutatingWebhookConfiguration';
+import { useMockQuery } from '../../helpers/testHelpers';
+import MWC from '../../lib/k8s/mutatingWebhookConfiguration';
import { TestContext } from '../../test';
import MutatingWebhookConfigDetails from './MutatingWebhookConfigDetails';
import { createMWC } from './storyHelper';
-const usePhonyGet: KubeMutatingWebhookConfiguration['useGet'] = (withService: boolean) => {
- return [new MWC(createMWC(withService)), null, () => {}, () => {}] as any;
+const usePhonyQuery = (withService: boolean) => {
+ return useMockQuery.data(new MWC(createMWC(withService)));
};
export default {
@@ -26,7 +27,7 @@ interface MockerStory {
const Template: Story = args => {
const { withService } = args;
- MWC.useGet = () => usePhonyGet(withService);
+ MWC.useQuery = usePhonyQuery(withService);
return (
diff --git a/frontend/src/components/webhookconfiguration/MutatingWebhookConfigList.stories.tsx b/frontend/src/components/webhookconfiguration/MutatingWebhookConfigList.stories.tsx
index e6690df6ac7..f1324b60b27 100644
--- a/frontend/src/components/webhookconfiguration/MutatingWebhookConfigList.stories.tsx
+++ b/frontend/src/components/webhookconfiguration/MutatingWebhookConfigList.stories.tsx
@@ -1,5 +1,6 @@
import Container from '@mui/material/Container';
import { Meta, Story } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import MutatingWebhookConfiguration from '../../lib/k8s/mutatingWebhookConfiguration';
import { TestContext } from '../../test';
import { generateK8sResourceList } from '../../test/mocker';
@@ -12,8 +13,8 @@ MWCTemplate.metadata.name = 'mwc-test-{{i}}';
const MWCWithServiceTemplate = createMWC(true);
MWCWithServiceTemplate.metadata.name = 'mwc-test-with-service-{{i}}';
-MutatingWebhookConfiguration.useList = () => {
- const objList = generateK8sResourceList(MWCTemplate, {
+MutatingWebhookConfiguration.useListQuery = useMockListQuery.data(
+ generateK8sResourceList(MWCTemplate, {
numResults: 3,
instantiateAs: MutatingWebhookConfiguration,
}).concat(
@@ -21,9 +22,8 @@ MutatingWebhookConfiguration.useList = () => {
numResults: 3,
instantiateAs: MutatingWebhookConfiguration,
})
- );
- return [objList, null, () => {}, () => {}] as any;
-};
+ )
+);
export default {
title: 'WebhookConfiguration/MutatingWebhookConfig/List',
diff --git a/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx b/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx
index da6a6bbba5b..f57a8b831a1 100644
--- a/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx
+++ b/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx
@@ -1,13 +1,12 @@
import { Meta, Story } from '@storybook/react';
-import VWC, {
- KubeValidatingWebhookConfiguration,
-} from '../../lib/k8s/validatingWebhookConfiguration';
+import { useMockQuery } from '../../helpers/testHelpers';
+import VWC from '../../lib/k8s/validatingWebhookConfiguration';
import { TestContext } from '../../test';
import { createVWC } from './storyHelper';
import ValidatingWebhookConfigDetails from './ValidatingWebhookConfigDetails';
-const usePhonyGet: KubeValidatingWebhookConfiguration['useGet'] = (withService: boolean) => {
- return [new VWC(createVWC(withService)), null, () => {}, () => {}] as any;
+const usePhonyQuery = (withService: boolean) => {
+ return useMockQuery.data(new VWC(createVWC(withService)));
};
export default {
@@ -28,7 +27,7 @@ interface MockerStory {
const Template: Story = args => {
const { withService } = args;
- VWC.useGet = () => usePhonyGet(withService);
+ VWC.useQuery = usePhonyQuery(withService);
return (
diff --git a/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigList.stories.tsx b/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigList.stories.tsx
index d3272e3e5dc..cca0fe871c7 100644
--- a/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigList.stories.tsx
+++ b/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigList.stories.tsx
@@ -1,5 +1,6 @@
import Container from '@mui/material/Container';
import { Meta, Story } from '@storybook/react';
+import { useMockListQuery } from '../../helpers/testHelpers';
import ValidatingWebhookConfiguration from '../../lib/k8s/validatingWebhookConfiguration';
import { TestContext } from '../../test';
import { generateK8sResourceList } from '../../test/mocker';
@@ -12,8 +13,8 @@ VWCTemplate.metadata.name = 'vwc-test-{{i}}';
const VWCWithServiceTemplate = createVWC(true);
VWCWithServiceTemplate.metadata.name = 'vwc-test-with-service-{{i}}';
-ValidatingWebhookConfiguration.useList = () => {
- const objList = generateK8sResourceList(VWCTemplate, {
+ValidatingWebhookConfiguration.useListQuery = useMockListQuery.data(
+ generateK8sResourceList(VWCTemplate, {
numResults: 3,
instantiateAs: ValidatingWebhookConfiguration,
}).concat(
@@ -21,9 +22,8 @@ ValidatingWebhookConfiguration.useList = () => {
numResults: 3,
instantiateAs: ValidatingWebhookConfiguration,
})
- );
- return [objList, null, () => {}, () => {}] as any;
-};
+ )
+);
export default {
title: 'WebhookConfiguration/ValidatingWebhookConfig/List',
diff --git a/frontend/src/components/workload/Overview.tsx b/frontend/src/components/workload/Overview.tsx
index 377066d589b..a8e7cf27fe7 100644
--- a/frontend/src/components/workload/Overview.tsx
+++ b/frontend/src/components/workload/Overview.tsx
@@ -2,8 +2,6 @@ import Grid from '@mui/material/Grid';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
-import { useCluster } from '../../lib/k8s';
-import { ApiError } from '../../lib/k8s/apiProxy';
import { KubeObject, Workload } from '../../lib/k8s/cluster';
import CronJob from '../../lib/k8s/cronJob';
import DaemonSet from '../../lib/k8s/daemonSet';
@@ -13,7 +11,7 @@ import Pod from '../../lib/k8s/pod';
import ReplicaSet from '../../lib/k8s/replicaSet';
import StatefulSet from '../../lib/k8s/statefulSet';
import { getReadyReplicas, getTotalReplicas } from '../../lib/util';
-import Link from '../common/Link';
+import { Link } from '../common';
import { PageGrid, ResourceLink } from '../common/Resource';
import ResourceListView from '../common/Resource/ResourceListView';
import { SectionBox } from '../common/SectionBox';
@@ -24,21 +22,26 @@ interface WorkloadDict {
}
export default function Overview() {
- const [workloadsData, setWorkloadsData] = React.useState({});
- const location = useLocation();
- const { t } = useTranslation('glossary');
- const cluster = useCluster();
+ const { items: pods } = Pod.useListQuery();
+ const { items: deployments } = Deployment.useListQuery();
+ const { items: statefulSets } = StatefulSet.useListQuery();
+ const { items: daemonSets } = DaemonSet.useListQuery();
+ const { items: replicaSets } = ReplicaSet.useListQuery();
+ const { items: jobs } = Job.useListQuery();
+ const { items: cronJobs } = CronJob.useListQuery();
- React.useEffect(() => {
- setWorkloadsData({});
- }, [cluster]);
+ const workloadsData: WorkloadDict = {
+ Pod: pods ?? [],
+ Deployment: deployments ?? [],
+ StatefulSet: statefulSets ?? [],
+ DaemonSet: daemonSets ?? [],
+ ReplicaSet: replicaSets ?? [],
+ Job: jobs ?? [],
+ CronJob: cronJobs ?? [],
+ };
- function setWorkloads(newWorkloads: WorkloadDict) {
- setWorkloadsData(workloads => ({
- ...workloads,
- ...newWorkloads,
- }));
- }
+ const location = useLocation();
+ const { t } = useTranslation('glossary');
function getPods(item: Workload) {
return `${getReadyReplicas(item)}/${getTotalReplicas(item)}`;
@@ -57,11 +60,6 @@ export default function Overview() {
const jointItems = React.useMemo(() => {
let joint: Workload[] = [];
- // Return null if no items are yet loaded, so we show the spinner in the table.
- if (Object.keys(workloadsData).length === 0) {
- return null;
- }
-
// Get all items except the pods since those shouldn't be shown in the table (only the chart).
for (const [key, items] of Object.entries(workloadsData)) {
if (key === 'Pod') {
@@ -69,6 +67,14 @@ export default function Overview() {
}
joint = joint.concat(items);
}
+
+ joint = joint.filter(Boolean);
+
+ // Return null if no items are yet loaded, so we show the spinner in the table.
+ if (joint.length === 0) {
+ return null;
+ }
+
return joint;
}, [workloadsData]);
@@ -82,18 +88,6 @@ export default function Overview() {
CronJob,
];
- workloads.forEach((workloadClass: KubeObject) => {
- workloadClass.useApiList(
- (items: InstanceType[]) => {
- setWorkloads({ [workloadClass.className]: items });
- },
- (err: ApiError) => {
- console.error(`Workloads list: Failed to get list for ${workloadClass.className}: ${err}`);
- setWorkloads({ [workloadClass.className]: [] });
- }
- );
- });
-
function ChartLink(workload: KubeObject) {
const linkName = workload.pluralName;
return {linkName};
@@ -108,7 +102,7 @@ export default function Overview() {
w.className === name))}
partialLabel={t('translation|Failed')}
totalLabel={t('translation|Running')}
/>
diff --git a/frontend/src/helpers/testHelpers.ts b/frontend/src/helpers/testHelpers.ts
new file mode 100644
index 00000000000..028a68a079e
--- /dev/null
+++ b/frontend/src/helpers/testHelpers.ts
@@ -0,0 +1,32 @@
+import { useKubeObject, useKubeObjectList } from '../lib/k8s/api/v2/hooks';
+
+export function mockListQuery({ items }: { items: any[] }) {
+ return (() => ({ items, error: null })) as any as typeof useKubeObjectList;
+}
+
+export const useMockQuery = {
+ noData: () => ({ data: null, error: null } as any as typeof useKubeObject),
+ error: () => ({ data: null, error: 'Phony error is phony!' } as any as typeof useKubeObject),
+ data: (data: any) => (() => ({ data: data, error: null })) as any as typeof useKubeObject,
+};
+
+export const useMockListQuery = {
+ noData: () =>
+ ({
+ data: null,
+ items: null,
+ error: null,
+ } as any as typeof useKubeObjectList),
+ error: () =>
+ ({
+ data: null,
+ items: null,
+ error: 'Phony error is phony!',
+ } as any as typeof useKubeObjectList),
+ data: (items: any[]) =>
+ (() => ({
+ data: { kind: 'List', items },
+ items,
+ error: null,
+ })) as any as typeof useKubeObjectList,
+};
diff --git a/frontend/src/lib/k8s/apiProxy/apiProxy.test.ts b/frontend/src/lib/k8s/api/v1/apiProxy.test.ts
similarity index 99%
rename from frontend/src/lib/k8s/apiProxy/apiProxy.test.ts
rename to frontend/src/lib/k8s/api/v1/apiProxy.test.ts
index def31cd3ba3..66204996762 100644
--- a/frontend/src/lib/k8s/apiProxy/apiProxy.test.ts
+++ b/frontend/src/lib/k8s/api/v1/apiProxy.test.ts
@@ -6,10 +6,10 @@
import nock from 'nock';
import { Mock, MockedFunction } from 'vitest';
import WS from 'vitest-websocket-mock';
-import exportFunctions from '../../../helpers';
-import * as auth from '../../auth';
-import * as cluster from '../../cluster';
-import * as apiProxy from '../apiProxy';
+import exportFunctions from '../../../../helpers';
+import * as auth from '../../../auth';
+import * as cluster from '../../../cluster';
+import * as apiProxy from '../../apiProxy';
const baseApiUrl = exportFunctions.getAppUrl();
const wsUrl = baseApiUrl.replace('http', 'ws');
diff --git a/frontend/src/lib/k8s/apiProxy/apiTypes.ts b/frontend/src/lib/k8s/api/v1/apiTypes.ts
similarity index 96%
rename from frontend/src/lib/k8s/apiProxy/apiTypes.ts
rename to frontend/src/lib/k8s/api/v1/apiTypes.ts
index e6396ec3533..b04eadde87b 100644
--- a/frontend/src/lib/k8s/apiProxy/apiTypes.ts
+++ b/frontend/src/lib/k8s/api/v1/apiTypes.ts
@@ -1,19 +1,23 @@
import { OpPatch } from 'json-patch';
-import { KubeMetadata, KubeObjectInterface } from '../cluster';
+import { KubeMetadata, KubeObjectInterface } from '../../cluster';
type RecursivePartial = {
- [P in keyof T]?: RecursivePartial;
+ [P in keyof T]?: T[P] extends (infer U)[]
+ ? RecursivePartial[]
+ : T[P] extends object | undefined
+ ? RecursivePartial
+ : T[P];
};
-export type StreamUpdate = {
+export type StreamUpdate = {
type: 'ADDED' | 'MODIFIED' | 'DELETED' | 'ERROR';
object: T;
};
export type CancelFunction = () => void;
-export type StreamResultsCb = (data: T[]) => void;
-export type StreamUpdatesCb = (data: StreamUpdate) => void;
+export type StreamResultsCb = (data: T) => void;
+export type StreamUpdatesCb = (data: T | StreamUpdate) => void;
export type StreamErrCb = (err: Error & { status?: number }, cancelStreamFunc?: () => void) => void;
diff --git a/frontend/src/lib/k8s/apiProxy/apiUtils.ts b/frontend/src/lib/k8s/api/v1/apiUtils.ts
similarity index 93%
rename from frontend/src/lib/k8s/apiProxy/apiUtils.ts
rename to frontend/src/lib/k8s/api/v1/apiUtils.ts
index 4aef9692b02..5d7875c7be6 100644
--- a/frontend/src/lib/k8s/apiProxy/apiUtils.ts
+++ b/frontend/src/lib/k8s/api/v1/apiUtils.ts
@@ -1,9 +1,9 @@
import { omit } from 'lodash';
-import store from '../../../redux/stores/store';
+import store from '../../../../redux/stores/store';
import { QueryParameters } from './apiTypes';
export function buildUrl(urlOrParts: string | string[], queryParams?: QueryParameters): string {
- const url = Array.isArray(urlOrParts) ? urlOrParts.join('/') : urlOrParts;
+ const url = Array.isArray(urlOrParts) ? urlOrParts.filter(Boolean).join('/') : urlOrParts;
return url + asQuery(queryParams);
}
diff --git a/frontend/src/lib/k8s/apiProxy/apply.ts b/frontend/src/lib/k8s/api/v1/apply.ts
similarity index 92%
rename from frontend/src/lib/k8s/apiProxy/apply.ts
rename to frontend/src/lib/k8s/api/v1/apply.ts
index 291f9166710..6e9c6325f2d 100644
--- a/frontend/src/lib/k8s/apiProxy/apply.ts
+++ b/frontend/src/lib/k8s/api/v1/apply.ts
@@ -1,6 +1,6 @@
import _ from 'lodash';
-import { getCluster } from '../../cluster';
-import { KubeObjectInterface } from '../cluster';
+import { getCluster } from '../../../cluster';
+import { KubeObjectInterface } from '../../cluster';
import { ApiError } from './apiTypes';
import { getClusterDefaultNamespace } from './clusterApi';
import { resourceDefToApiFactory } from './factories';
@@ -58,6 +58,6 @@ export async function apply(
// Preserve the resourceVersion if its an update request
bodyToApply.metadata.resourceVersion = resourceVersion;
// We had a conflict. Try a PUT
- return apiEndpoint.put(bodyToApply, {}, cluster!);
+ return apiEndpoint.put(bodyToApply, {}, cluster!) as Promise;
}
}
diff --git a/frontend/src/lib/k8s/apiProxy/clusterApi.ts b/frontend/src/lib/k8s/api/v1/clusterApi.ts
similarity index 67%
rename from frontend/src/lib/k8s/apiProxy/clusterApi.ts
rename to frontend/src/lib/k8s/api/v1/clusterApi.ts
index 0568dd80602..40b98ef4aef 100644
--- a/frontend/src/lib/k8s/apiProxy/clusterApi.ts
+++ b/frontend/src/lib/k8s/api/v1/clusterApi.ts
@@ -1,12 +1,12 @@
-import helpers, { getHeadlampAPIHeaders } from '../../../helpers';
-import { ConfigState } from '../../../redux/configSlice';
-import store from '../../../redux/stores/store';
+import helpers, { getHeadlampAPIHeaders } from '../../../../helpers';
+import { ConfigState } from '../../../../redux/configSlice';
+import store from '../../../../redux/stores/store';
import {
deleteClusterKubeconfig,
findKubeconfigByClusterName,
storeStatelessClusterKubeconfig,
-} from '../../../stateless';
-import { getCluster } from '../../util';
+} from '../../../../stateless';
+import { getCluster } from '../../../util';
import { ClusterRequest } from './apiTypes';
import { clusterRequest, post, request } from './clusterRequests';
import { JSON_HEADERS } from './constants';
@@ -15,8 +15,8 @@ import { JSON_HEADERS } from './constants';
* Test authentication for the given cluster.
* Will throw an error if the user is not authenticated.
*/
-export async function testAuth(cluster = '') {
- const spec = { namespace: 'default' };
+export async function testAuth(cluster = '', namespace = 'default') {
+ const spec = { namespace };
const clusterName = cluster || getCluster();
return post('/apis/authorization.k8s.io/v1/selfsubjectrulesreviews', { spec }, false, {
@@ -123,3 +123,56 @@ export function getClusterDefaultNamespace(cluster: string, checkSettings?: bool
return defaultNamespace;
}
+
+/**
+ * renameCluster sends call to backend to update a field in kubeconfig which
+ * is the custom name of the cluster used by the user.
+ * @param cluster
+ */
+export async function renameCluster(cluster: string, newClusterName: string, source: string) {
+ let stateless = false;
+ if (cluster) {
+ const kubeconfig = await findKubeconfigByClusterName(cluster);
+ if (kubeconfig !== null) {
+ stateless = true;
+ }
+ }
+
+ return request(
+ `/cluster/${cluster}`,
+ {
+ method: 'PUT',
+ headers: { ...getHeadlampAPIHeaders() },
+ body: JSON.stringify({ newClusterName, source, stateless }),
+ },
+ false,
+ false
+ );
+}
+
+/**
+ * parseKubeConfig sends call to backend to parse kubeconfig and send back
+ * the parsed clusters and contexts.
+ * @param clusterReq - The cluster request object.
+ */
+export async function parseKubeConfig(clusterReq: ClusterRequest) {
+ const kubeconfig = clusterReq.kubeconfig;
+
+ if (kubeconfig) {
+ return request(
+ '/parseKubeConfig',
+ {
+ method: 'POST',
+ body: JSON.stringify(clusterReq),
+ headers: {
+ ...JSON_HEADERS,
+ ...getHeadlampAPIHeaders(),
+ },
+ },
+ false,
+ false
+ );
+ }
+
+ return null;
+}
diff --git a/frontend/src/lib/k8s/apiProxy/clusterRequests.ts b/frontend/src/lib/k8s/api/v1/clusterRequests.ts
similarity index 96%
rename from frontend/src/lib/k8s/apiProxy/clusterRequests.ts
rename to frontend/src/lib/k8s/api/v1/clusterRequests.ts
index 1dfd59adf9c..dfeb2046f3c 100644
--- a/frontend/src/lib/k8s/apiProxy/clusterRequests.ts
+++ b/frontend/src/lib/k8s/api/v1/clusterRequests.ts
@@ -1,10 +1,10 @@
// @todo: Params is a confusing name for options, because params are also query params.
-import { isDebugVerbose } from '../../../helpers';
-import { findKubeconfigByClusterName, getUserIdFromLocalStorage } from '../../../stateless';
-import { getToken, logout, setToken } from '../../auth';
-import { getCluster } from '../../cluster';
-import { KubeObjectInterface } from '../cluster';
+import { isDebugVerbose } from '../../../../helpers';
+import { findKubeconfigByClusterName, getUserIdFromLocalStorage } from '../../../../stateless';
+import { getToken, logout, setToken } from '../../../auth';
+import { getCluster } from '../../../cluster';
+import { KubeObjectInterface } from '../../cluster';
import { ApiError, ClusterRequestParams, QueryParameters, RequestParams } from './apiTypes';
import { asQuery, combinePath, getClusterAuthType } from './apiUtils';
import { BASE_HTTP_URL, CLUSTERS_PREFIX, DEFAULT_TIMEOUT, JSON_HEADERS } from './constants';
diff --git a/frontend/src/lib/k8s/apiProxy/constants.ts b/frontend/src/lib/k8s/api/v1/constants.ts
similarity index 87%
rename from frontend/src/lib/k8s/apiProxy/constants.ts
rename to frontend/src/lib/k8s/api/v1/constants.ts
index 7d9ab6047f1..001bdcece52 100644
--- a/frontend/src/lib/k8s/apiProxy/constants.ts
+++ b/frontend/src/lib/k8s/api/v1/constants.ts
@@ -1,4 +1,4 @@
-import helpers from '../../../helpers';
+import helpers from '../../../../helpers';
export const BASE_HTTP_URL = helpers.getAppUrl();
export const CLUSTERS_PREFIX = 'clusters';
diff --git a/frontend/src/lib/k8s/apiProxy/drainNode.ts b/frontend/src/lib/k8s/api/v1/drainNode.ts
similarity index 96%
rename from frontend/src/lib/k8s/apiProxy/drainNode.ts
rename to frontend/src/lib/k8s/api/v1/drainNode.ts
index be782498a5f..fa920cf1598 100644
--- a/frontend/src/lib/k8s/apiProxy/drainNode.ts
+++ b/frontend/src/lib/k8s/api/v1/drainNode.ts
@@ -1,5 +1,5 @@
-import helpers from '../../../helpers';
-import { getToken } from '../../auth';
+import helpers from '../../../../helpers';
+import { getToken } from '../../../auth';
import { JSON_HEADERS } from './constants';
/**
diff --git a/frontend/src/lib/k8s/apiProxy/factories.ts b/frontend/src/lib/k8s/api/v1/factories.ts
similarity index 98%
rename from frontend/src/lib/k8s/apiProxy/factories.ts
rename to frontend/src/lib/k8s/api/v1/factories.ts
index 94e63297246..086ac81d6b9 100644
--- a/frontend/src/lib/k8s/apiProxy/factories.ts
+++ b/frontend/src/lib/k8s/api/v1/factories.ts
@@ -1,9 +1,9 @@
// @todo: repeatStreamFunc could be improved for performance by remembering when a URL
// is 404 and not trying it again... and again.
-import { isDebugVerbose } from '../../../helpers';
-import { getCluster } from '../../cluster';
-import { KubeObjectInterface } from '../cluster';
+import { isDebugVerbose } from '../../../../helpers';
+import { getCluster } from '../../../cluster';
+import { KubeObjectInterface } from '../../cluster';
import {
ApiClient,
ApiError,
diff --git a/frontend/src/lib/k8s/apiProxy/metricsApi.ts b/frontend/src/lib/k8s/api/v1/metricsApi.ts
similarity index 89%
rename from frontend/src/lib/k8s/apiProxy/metricsApi.ts
rename to frontend/src/lib/k8s/api/v1/metricsApi.ts
index 947e63885e4..ceabd91ac25 100644
--- a/frontend/src/lib/k8s/apiProxy/metricsApi.ts
+++ b/frontend/src/lib/k8s/api/v1/metricsApi.ts
@@ -1,6 +1,6 @@
-import { isDebugVerbose } from '../../../helpers';
-import { getCluster } from '../../cluster';
-import { KubeMetrics } from '../cluster';
+import { isDebugVerbose } from '../../../../helpers';
+import { getCluster } from '../../../cluster';
+import { KubeMetrics } from '../../cluster';
import { ApiError } from './apiTypes';
import { clusterRequest } from './clusterRequests';
diff --git a/frontend/src/lib/k8s/apiProxy/pluginsApi.ts b/frontend/src/lib/k8s/api/v1/pluginsApi.ts
similarity index 95%
rename from frontend/src/lib/k8s/apiProxy/pluginsApi.ts
rename to frontend/src/lib/k8s/api/v1/pluginsApi.ts
index 08b6bc23a6f..8e99554a85d 100644
--- a/frontend/src/lib/k8s/apiProxy/pluginsApi.ts
+++ b/frontend/src/lib/k8s/api/v1/pluginsApi.ts
@@ -1,4 +1,4 @@
-import { getHeadlampAPIHeaders } from '../../../helpers';
+import { getHeadlampAPIHeaders } from '../../../../helpers';
import { request } from './clusterRequests';
//@todo: what is DELETE /plugins/name response type? It's not used by headlamp in PLuginSettingsDetail.
diff --git a/frontend/src/lib/k8s/apiProxy/portForward.ts b/frontend/src/lib/k8s/api/v1/portForward.ts
similarity index 97%
rename from frontend/src/lib/k8s/apiProxy/portForward.ts
rename to frontend/src/lib/k8s/api/v1/portForward.ts
index 2082cb1a2e7..f8a295b4b4c 100644
--- a/frontend/src/lib/k8s/apiProxy/portForward.ts
+++ b/frontend/src/lib/k8s/api/v1/portForward.ts
@@ -1,5 +1,5 @@
-import helpers from '../../../helpers';
-import { getToken } from '../../auth';
+import helpers from '../../../../helpers';
+import { getToken } from '../../../auth';
import { JSON_HEADERS } from './constants';
// @todo: the return type is missing for the following functions.
diff --git a/frontend/src/lib/k8s/apiProxy/scaleApi.ts b/frontend/src/lib/k8s/api/v1/scaleApi.ts
similarity index 92%
rename from frontend/src/lib/k8s/apiProxy/scaleApi.ts
rename to frontend/src/lib/k8s/api/v1/scaleApi.ts
index bee462095b7..be3597dc30a 100644
--- a/frontend/src/lib/k8s/apiProxy/scaleApi.ts
+++ b/frontend/src/lib/k8s/api/v1/scaleApi.ts
@@ -1,5 +1,5 @@
-import { getCluster } from '../../cluster';
-import { KubeMetadata } from '../cluster';
+import { getCluster } from '../../../cluster';
+import { KubeMetadata } from '../../cluster';
import { ScaleApi } from './apiTypes';
import { clusterRequest, patch, put } from './clusterRequests';
diff --git a/frontend/src/lib/k8s/apiProxy/streamingApi.ts b/frontend/src/lib/k8s/api/v1/streamingApi.ts
similarity index 93%
rename from frontend/src/lib/k8s/apiProxy/streamingApi.ts
rename to frontend/src/lib/k8s/api/v1/streamingApi.ts
index 047d61701a9..c8e12577e58 100644
--- a/frontend/src/lib/k8s/apiProxy/streamingApi.ts
+++ b/frontend/src/lib/k8s/api/v1/streamingApi.ts
@@ -1,8 +1,8 @@
-import { isDebugVerbose } from '../../../helpers';
-import { findKubeconfigByClusterName, getUserIdFromLocalStorage } from '../../../stateless';
-import { getToken } from '../../auth';
-import { getCluster } from '../../cluster';
-import { KubeObjectInterface } from '../cluster';
+import { isDebugVerbose } from '../../../../helpers';
+import { findKubeconfigByClusterName, getUserIdFromLocalStorage } from '../../../../stateless';
+import { getToken } from '../../../auth';
+import { getCluster } from '../../../cluster';
+import { KubeObjectInterface } from '../../cluster';
import {
ApiError,
CancelFunction,
@@ -10,7 +10,6 @@ import {
StreamErrCb,
StreamResultsCb,
StreamUpdate,
- StreamUpdatesCb,
} from './apiTypes';
import { asQuery, combinePath } from './apiUtils';
import { clusterRequest } from './clusterRequests';
@@ -106,23 +105,23 @@ export function streamResults(
// @todo: this interface needs documenting.
-export interface StreamResultsParams {
- cb: StreamResultsCb;
+export interface StreamResultsParams {
+ cb: StreamResultsCb;
errCb: StreamErrCb;
cluster?: string;
}
// @todo: needs documenting
-export function streamResultsForCluster(
+export function streamResultsForCluster(
url: string,
- params: StreamResultsParams,
+ params: StreamResultsParams,
queryParams?: QueryParameters
): Promise {
const { cb, errCb, cluster = '' } = params;
const clusterName = cluster || getCluster() || '';
- const results: Record = {};
+ const results: Record = {};
let isCancelled = false;
let socket: ReturnType;
@@ -169,7 +168,7 @@ export function streamResultsForCluster(
if (socket) socket.cancel();
}
- function add(items: T[], kind: string) {
+ function add(items: any[], kind: string) {
const fixedKind = kind.slice(0, -4); // Trim off the word "List" from the end of the string
for (const item of items) {
item.kind = fixedKind;
@@ -179,7 +178,7 @@ export function streamResultsForCluster(
push();
}
- function update({ type, object }: StreamUpdate) {
+ function update({ type, object }: StreamUpdate) {
(object as KubeObjectInterface).actionType = type; // eslint-disable-line no-param-reassign
switch (type) {
@@ -272,11 +271,7 @@ export interface StreamArgs {
* @returns An object with two functions: `cancel`, which can be called to cancel
* the stream, and `getSocket`, which returns the WebSocket object.
*/
-export function stream(
- url: string,
- cb: StreamUpdatesCb,
- args: StreamArgs
-) {
+export function stream(url: string, cb: StreamResultsCb, args: StreamArgs) {
let connection: { close: () => void; socket: WebSocket | null } | null = null;
let isCancelled = false;
const { failCb, cluster = '' } = args;
@@ -347,9 +342,9 @@ export function stream(
*
* @returns An object with a `close` function and a `socket` property.
*/
-export async function connectStream(
+export async function connectStream(
path: string,
- cb: StreamUpdatesCb,
+ cb: StreamResultsCb,
onFail: () => void,
isJson: boolean,
additionalProtocols: string[] = [],
@@ -387,9 +382,9 @@ interface StreamParams {
*
* @returns A promise that resolves to an object with a `close` function and a `socket` property.
*/
-export async function connectStreamWithParams(
+export async function connectStreamWithParams(
path: string,
- cb: StreamUpdatesCb,
+ cb: StreamResultsCb,
onFail: () => void,
params?: StreamParams
): Promise<{
diff --git a/frontend/src/lib/k8s/apiProxy/tokenApi.ts b/frontend/src/lib/k8s/api/v1/tokenApi.ts
similarity index 93%
rename from frontend/src/lib/k8s/apiProxy/tokenApi.ts
rename to frontend/src/lib/k8s/api/v1/tokenApi.ts
index 204bd729f1d..f93a5d56ea6 100644
--- a/frontend/src/lib/k8s/apiProxy/tokenApi.ts
+++ b/frontend/src/lib/k8s/api/v1/tokenApi.ts
@@ -1,8 +1,8 @@
import { decodeToken } from 'react-jwt';
-import { isDebugVerbose } from '../../../helpers';
-import { setToken } from '../../auth';
-import { getCluster } from '../../cluster';
-import { KubeToken } from '../token';
+import { isDebugVerbose } from '../../../../helpers';
+import { setToken } from '../../../auth';
+import { getCluster } from '../../../cluster';
+import { KubeToken } from '../../token';
import { combinePath } from './apiUtils';
import {
BASE_HTTP_URL,
diff --git a/frontend/src/lib/k8s/api/v2/KubeList.ts b/frontend/src/lib/k8s/api/v2/KubeList.ts
new file mode 100644
index 00000000000..13fb12164e9
--- /dev/null
+++ b/frontend/src/lib/k8s/api/v2/KubeList.ts
@@ -0,0 +1,55 @@
+import { KubeObjectClass, KubeObjectInterface } from '../../cluster';
+
+export interface KubeList {
+ kind: string;
+ apiVersion: string;
+ items: T[];
+ metadata: {
+ resourceVersion: string;
+ };
+}
+
+export interface KubeListUpdateEvent {
+ type: 'ADDED' | 'MODIFIED' | 'DELETED' | 'ERROR';
+ object: T;
+}
+
+export const KubeList = {
+ applyUpdate(
+ list: KubeList,
+ update: KubeListUpdateEvent,
+ itemClass: KubeObjectClass
+ ): KubeList {
+ const newItems = [...list.items];
+ const index = newItems.findIndex(item => item.metadata.uid === update.object.metadata.uid);
+
+ switch (update.type) {
+ case 'ADDED':
+ case 'MODIFIED':
+ if (index !== -1) {
+ newItems[index] = new itemClass(update.object) as T;
+ } else {
+ newItems.push(new itemClass(update.object) as T);
+ }
+ break;
+ case 'DELETED':
+ if (index !== -1) {
+ newItems.splice(index, 1);
+ }
+ break;
+ case 'ERROR':
+ console.error('Error in update', update);
+ break;
+ default:
+ console.error('Unknown update type', update);
+ }
+
+ return {
+ ...list,
+ metadata: {
+ resourceVersion: update.object.metadata.resourceVersion!,
+ },
+ items: newItems,
+ };
+ },
+};
diff --git a/frontend/src/lib/k8s/api/v2/KubeObjectEndpoint.ts b/frontend/src/lib/k8s/api/v2/KubeObjectEndpoint.ts
new file mode 100644
index 00000000000..fee158478fb
--- /dev/null
+++ b/frontend/src/lib/k8s/api/v2/KubeObjectEndpoint.ts
@@ -0,0 +1,25 @@
+export interface KubeObjectEndpoint {
+ group?: string;
+ version: string;
+ resource: string;
+}
+
+export const KubeObjectEndpoint = {
+ toUrl: ({ group, version, resource }: KubeObjectEndpoint, namespace?: string) => {
+ const parts = [];
+ if (group) {
+ parts.push('apis', group);
+ } else {
+ parts.push('api');
+ }
+ parts.push(version);
+
+ if (namespace) {
+ parts.push('namespaces', namespace);
+ }
+
+ parts.push(resource);
+
+ return parts.join('/');
+ },
+};
diff --git a/frontend/src/lib/k8s/api/v2/fetch.ts b/frontend/src/lib/k8s/api/v2/fetch.ts
new file mode 100644
index 00000000000..3a1a5332373
--- /dev/null
+++ b/frontend/src/lib/k8s/api/v2/fetch.ts
@@ -0,0 +1,55 @@
+import helpers from '../../../../helpers';
+import { findKubeconfigByClusterName, getUserIdFromLocalStorage } from '../../../../stateless';
+import { getToken, setToken } from '../../../auth';
+import { getClusterAuthType } from '../v1/apiUtils';
+import { refreshToken } from '../v1/tokenApi';
+import { makeUrl } from './utils';
+
+export const BASE_HTTP_URL = helpers.getAppUrl();
+
+async function backendFetch(url: string | URL, init: RequestInit) {
+ const response = await fetch(makeUrl([BASE_HTTP_URL, url]), init);
+
+ // The backend signals through this header that it wants a reload.
+ // See plugins.go
+ const headerVal = response.headers.get('X-Reload');
+ if (headerVal && headerVal.indexOf('reload') !== -1) {
+ window.location.reload();
+ }
+
+ return response;
+}
+
+export async function clusterFetch(url: string | URL, init: RequestInit & { cluster: string }) {
+ const token = getToken(init.cluster);
+
+ init.headers = new Headers(init.headers);
+
+ // Set stateless kubeconfig if exists
+ const kubeconfig = await findKubeconfigByClusterName(init.cluster);
+ if (kubeconfig !== null) {
+ const userID = getUserIdFromLocalStorage();
+ init.headers.set('KUBECONFIG', kubeconfig);
+ init.headers.set('X-HEADLAMP-USER-ID', userID);
+ }
+
+ // Refresh service account token only if the cluster auth type is not OIDC
+ if (getClusterAuthType(init.cluster) !== 'oidc') {
+ await refreshToken(token);
+ }
+
+ if (token) {
+ init.headers.set('Authorization', `Bearer ${token}`);
+ }
+
+ const response = await backendFetch(makeUrl(['clusters', init.cluster, url]), init);
+
+ // In case of OIDC auth if the token is about to expire the backend
+ // sends a refreshed token in the response header.
+ const newToken = response.headers.get('X-Authorization');
+ if (newToken && init.cluster) {
+ setToken(init.cluster, newToken);
+ }
+
+ return response;
+}
diff --git a/frontend/src/lib/k8s/api/v2/hooks.ts b/frontend/src/lib/k8s/api/v2/hooks.ts
new file mode 100644
index 00000000000..61b5db35358
--- /dev/null
+++ b/frontend/src/lib/k8s/api/v2/hooks.ts
@@ -0,0 +1,189 @@
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { useEffect, useMemo, useRef } from 'react';
+import { getCluster } from '../../../cluster';
+import { ApiError, QueryParameters } from '../../apiProxy';
+import { KubeObjectClass, KubeObjectInterface } from '../../cluster';
+import { clusterFetch } from './fetch';
+import { KubeList } from './KubeList';
+import { KubeObjectEndpoint } from './KubeObjectEndpoint';
+import { makeUrl } from './utils';
+import { watchList, watchObject } from './watching';
+
+/**
+ * Returns a single KubeObject.
+ */
+export function useKubeObject({
+ kubeObjectClass,
+ namespace,
+ name,
+ cluster: maybeCluster,
+}: {
+ kubeObjectClass: T;
+ namespace?: string;
+ name: string;
+ cluster?: string;
+}) {
+ type Instance = InstanceType;
+ const endpoint = useEndpoints(kubeObjectClass.apiEndpoint.apiInfo);
+ const cluster = maybeCluster ?? getCluster() ?? 'default';
+
+ const queryKey = useMemo(
+ () => ['object', cluster, endpoint, namespace, name],
+ [endpoint, namespace, name]
+ );
+
+ const socketRef = useRef | undefined>();
+ function startWatching() {
+ if (socketRef.current || !endpoint) return;
+
+ socketRef.current = watchObject({
+ endpoint,
+ name,
+ onObject: obj => client.setQueryData(queryKey, obj),
+ itemClass: kubeObjectClass,
+ });
+ }
+
+ useEffect(() => {
+ if (!endpoint) return;
+ const maybeData = client.getQueryData>(queryKey);
+ if (maybeData) {
+ startWatching();
+ }
+
+ return () => {
+ socketRef.current?.then(socket => socket.close());
+ };
+ }, [endpoint]);
+
+ const client = useQueryClient();
+ const query = useQuery({
+ enabled: !!endpoint,
+ placeholderData: null,
+ staleTime: 5000,
+ queryKey,
+ queryFn: async () => {
+ if (!endpoint) return;
+ const url = makeUrl([KubeObjectEndpoint.toUrl(endpoint, namespace), name]);
+ const obj: KubeObjectInterface = await clusterFetch(url, {
+ cluster,
+ }).then(it => it.json());
+ startWatching();
+ return new kubeObjectClass(obj);
+ },
+ });
+
+ return query;
+}
+
+/**
+ * Test different endpoints to see which one is working.
+ */
+const getWorkingEndpoint = async (endpoints: KubeObjectEndpoint[]) => {
+ const promises = endpoints.map(endpoint => {
+ return clusterFetch(KubeObjectEndpoint.toUrl(endpoint), {
+ method: 'GET',
+ cluster: getCluster() ?? '',
+ }).then(it => {
+ if (!it.ok) {
+ // reject
+ throw new Error('error');
+ }
+ return endpoint;
+ });
+ });
+ return Promise.any(promises);
+};
+
+const useEndpoints = (endpoints: KubeObjectEndpoint[]) => {
+ const { data: endpoint } = useQuery({
+ enabled: endpoints.length > 1,
+ queryKey: ['endpoints', endpoints],
+ queryFn: () => getWorkingEndpoint(endpoints),
+ });
+
+ if (endpoints.length === 1) return endpoints[0];
+
+ return endpoint;
+};
+
+export function useKubeObjectList({
+ kubeObjectClass,
+ namespace,
+ cluster: maybeCluster,
+ queryParams,
+}: {
+ kubeObjectClass: T;
+ namespace?: string;
+ cluster?: string;
+ queryParams: QueryParameters;
+}) {
+ const endpoint = useEndpoints(kubeObjectClass.apiEndpoint.apiInfo);
+
+ const cleanedUpQueryParams = Object.fromEntries(
+ Object.entries(queryParams ?? {}).filter(([, value]) => value !== undefined && value !== '')
+ );
+
+ const cluster = maybeCluster ?? getCluster() ?? 'default';
+
+ const queryKey = useMemo(
+ () => ['list', cluster, endpoint, namespace, cleanedUpQueryParams],
+ [endpoint, namespace, cleanedUpQueryParams]
+ );
+
+ const socketRef = useRef | undefined>();
+ function startWatching(resourceVersion: string) {
+ if (socketRef.current || !endpoint) return;
+
+ socketRef.current = watchList({
+ endpoint,
+ resourceVersion,
+ cluster,
+ queryParams: cleanedUpQueryParams,
+ itemClass: kubeObjectClass,
+ onListUpdate: updater => {
+ client.setQueryData(queryKey, (oldList: any) => {
+ const newList = updater(oldList);
+ return newList;
+ });
+ },
+ });
+ }
+
+ const client = useQueryClient();
+ const query = useQuery | null | undefined, ApiError>({
+ enabled: !!endpoint,
+ placeholderData: null,
+ queryKey,
+ queryFn: async () => {
+ if (!endpoint) return;
+ const list: KubeList = await clusterFetch(
+ makeUrl([KubeObjectEndpoint.toUrl(endpoint!, namespace)], cleanedUpQueryParams),
+ {
+ cluster,
+ }
+ ).then(it => it.json());
+ list.items = list.items.map(
+ item => new kubeObjectClass({ ...item, kind: list.kind.replace('List', '') })
+ );
+
+ // start watching if we haven't yet
+ startWatching(list.metadata.resourceVersion);
+ return list;
+ },
+ });
+
+ useEffect(() => {
+ if (!endpoint) return;
+ const maybeData = client.getQueryData>(queryKey);
+ if (maybeData) {
+ startWatching(maybeData.metadata.resourceVersion);
+ }
+
+ return () => {
+ socketRef.current?.then(socket => socket.close());
+ };
+ }, [endpoint]);
+
+ return { items: query.data?.items ?? null, ...query };
+}
diff --git a/frontend/src/lib/k8s/api/v2/utils.ts b/frontend/src/lib/k8s/api/v2/utils.ts
new file mode 100644
index 00000000000..2fae198163e
--- /dev/null
+++ b/frontend/src/lib/k8s/api/v2/utils.ts
@@ -0,0 +1,27 @@
+import { KubeObjectEndpoint } from './KubeObjectEndpoint';
+
+export function makeUrl(urlParts: any[], query: Record = {}) {
+ const url = urlParts
+ .map(it => (typeof it === 'string' ? it : String(it)))
+ .filter(Boolean)
+ .join('/');
+ const queryString = new URLSearchParams(query).toString();
+ const fullUrl = queryString ? `${url}?${queryString}` : url;
+
+ // replace multiple slashes with a single one
+ // unless it is part of the protocol
+ return fullUrl.replace(/([^:]\/)\/+/g, '$1');
+}
+
+/**
+ * Represents a KubeObject with an endpoint.
+ */
+export interface ObjectWithEndpoint {
+ apiEndpoint: {
+ apiInfo: KubeObjectEndpoint[];
+ };
+
+ new (data: any): ObjectWithEndpoint;
+
+ [prop: string]: any;
+}
diff --git a/frontend/src/lib/k8s/api/v2/watching.ts b/frontend/src/lib/k8s/api/v2/watching.ts
new file mode 100644
index 00000000000..266bd6850d0
--- /dev/null
+++ b/frontend/src/lib/k8s/api/v2/watching.ts
@@ -0,0 +1,128 @@
+import { findKubeconfigByClusterName, getUserIdFromLocalStorage } from '../../../../stateless';
+import { getToken } from '../../../auth';
+import { getCluster } from '../../../cluster';
+import { KubeObjectClass, KubeObjectInterface } from '../../cluster';
+import { BASE_HTTP_URL } from './fetch';
+import { KubeList, KubeListUpdateEvent } from './KubeList';
+import { KubeObjectEndpoint } from './KubeObjectEndpoint';
+import { makeUrl } from './utils';
+
+const BASE_WS_URL = BASE_HTTP_URL.replace('http', 'ws');
+
+export async function openWebSocket(
+ url: string,
+ {
+ protocols: moreProtocols = [],
+ type = 'binary',
+ cluster = getCluster() ?? '',
+ onMessage,
+ }: {
+ protocols?: string | string[];
+ type: 'json' | 'binary';
+ cluster?: string;
+ onMessage: (data: T) => void;
+ }
+) {
+ const path = [url];
+ const protocols = ['base64.binary.k8s.io', ...(moreProtocols ?? [])];
+
+ const token = getToken(cluster);
+ if (token) {
+ const encodedToken = btoa(token).replace(/=/g, '');
+ protocols.push(`base64url.bearer.authorization.k8s.io.${encodedToken}`);
+ }
+
+ if (cluster) {
+ path.unshift('clusters', cluster);
+
+ try {
+ const kubeconfig = await findKubeconfigByClusterName(cluster);
+
+ if (kubeconfig !== null) {
+ const userID = getUserIdFromLocalStorage();
+ protocols.push(`base64url.headlamp.authorization.k8s.io.${userID}`);
+ }
+ } catch (error) {
+ console.error('Error while finding kubeconfig:', error);
+ }
+ }
+
+ const socket = new WebSocket(makeUrl([BASE_WS_URL, ...path], {}), protocols);
+ socket.binaryType = 'arraybuffer';
+ socket.addEventListener('message', (body: MessageEvent) => {
+ const data = type === 'json' ? JSON.parse(body.data) : body.data;
+ onMessage(data);
+ });
+ socket.addEventListener('error', error => {
+ console.error('WebSocket error:', error);
+ });
+
+ return socket;
+}
+
+export async function watchList({
+ endpoint,
+ resourceVersion,
+ cluster = getCluster() ?? '',
+ queryParams,
+ onListUpdate,
+ itemClass,
+}: {
+ endpoint: KubeObjectEndpoint;
+ resourceVersion: string;
+ cluster?: string;
+ queryParams?: any;
+ onListUpdate: (update: (oldList: KubeList) => KubeList) => void;
+ itemClass: KubeObjectClass;
+}) {
+ const watchUrl = makeUrl([KubeObjectEndpoint.toUrl(endpoint)], {
+ ...queryParams,
+ watch: '1',
+ resourceVersion,
+ });
+
+ function update(event: KubeListUpdateEvent) {
+ onListUpdate(list => KubeList.applyUpdate(list, event, itemClass));
+ }
+
+ const socket = await openWebSocket>(watchUrl, {
+ type: 'json',
+ cluster,
+ onMessage: update,
+ });
+
+ return socket;
+}
+
+export async function watchObject({
+ endpoint,
+ name,
+ cluster = getCluster() ?? '',
+ onObject,
+ itemClass,
+}: {
+ endpoint: KubeObjectEndpoint;
+ name: string;
+ cluster?: string;
+ onObject: (newObject: T) => void;
+ itemClass: KubeObjectClass;
+}) {
+ const watchUrl = makeUrl([KubeObjectEndpoint.toUrl(endpoint)], {
+ watch: '1',
+ fieldSelector: `metadata.name=${name}`,
+ });
+
+ function update(event: KubeListUpdateEvent) {
+ if (event.object) {
+ onObject(new itemClass(event.object) as T);
+ }
+ }
+
+ const socket = await openWebSocket>(watchUrl, {
+ type: 'json',
+ cluster,
+ onMessage: update,
+ });
+
+ return socket;
+}
diff --git a/frontend/src/lib/k8s/apiProxy/index.ts b/frontend/src/lib/k8s/apiProxy/index.ts
index d45a153f40c..338b29f7381 100644
--- a/frontend/src/lib/k8s/apiProxy/index.ts
+++ b/frontend/src/lib/k8s/apiProxy/index.ts
@@ -30,10 +30,10 @@ export type {
RequestParams,
StreamErrCb,
StreamResultsCb,
-} from './apiTypes';
+} from '../api/v1/apiTypes';
// Basic cluster API functions
-export { clusterRequest, patch, post, put, remove, request } from './clusterRequests';
+export { clusterRequest, patch, post, put, remove, request } from '../api/v1/clusterRequests';
// Streaming API functions
export {
@@ -43,18 +43,25 @@ export {
streamResultsForCluster,
type StreamArgs,
type StreamResultsParams,
-} from './streamingApi';
+} from '../api/v1/streamingApi';
// API factory functions
-export { apiFactory, apiFactoryWithNamespace } from './factories';
+export { apiFactory, apiFactoryWithNamespace } from '../api/v1/factories';
// Port forward functions
-export { listPortForward, startPortForward, stopOrDeletePortForward } from './portForward';
+export { listPortForward, startPortForward, stopOrDeletePortForward } from '../api/v1/portForward';
-export { deleteCluster, setCluster, testAuth, testClusterHealth } from './clusterApi';
-export { metrics } from './metricsApi';
-export { deletePlugin } from './pluginsApi';
+export {
+ deleteCluster,
+ setCluster,
+ testAuth,
+ testClusterHealth,
+ parseKubeConfig,
+ renameCluster,
+} from '../api/v1/clusterApi';
+export { metrics } from '../api/v1/metricsApi';
+export { deletePlugin } from '../api/v1/pluginsApi';
-export { drainNodeStatus, drainNode } from './drainNode';
+export { drainNodeStatus, drainNode } from '../api/v1/drainNode';
-export { apply } from './apply';
+export { apply } from '../api/v1/apply';
diff --git a/frontend/src/lib/k8s/cluster.ts b/frontend/src/lib/k8s/cluster.ts
index 5c776a92004..fa10b1d4f73 100644
--- a/frontend/src/lib/k8s/cluster.ts
+++ b/frontend/src/lib/k8s/cluster.ts
@@ -6,6 +6,7 @@ import helpers from '../../helpers';
import { createRouteURL } from '../router';
import { getCluster, timeAgo, useErrorState } from '../util';
import { useCluster, useConnectApi } from '.';
+import { useKubeObject, useKubeObjectList } from './api/v2/hooks';
import { ApiError, apiFactory, apiFactoryWithNamespace, post, QueryParameters } from './apiProxy';
import CronJob from './cronJob';
import DaemonSet from './daemonSet';
@@ -319,6 +320,16 @@ export interface KubeObjectIface {
className: string;
[prop: string]: any;
getAuthorization?: (arg: string, resourceAttrs?: AuthRequestResourceAttrs) => any;
+ useListQuery(options?: {
+ cluster?: string;
+ namespace?: string;
+ queryParams?: QueryParameters;
+ }): ReturnType;
+ useQuery(options?: {
+ name: string;
+ namespace?: string;
+ cluster?: string;
+ }): ReturnType;
}
export interface AuthRequestResourceAttrs {
@@ -567,6 +578,30 @@ export function makeKubeObject(
useConnectApi(...listCalls);
}
+ static useListQuery(
+ this: U,
+ options: { cluster?: string; queryParams?: QueryParameters; namespace?: string } = {}
+ ) {
+ return useKubeObjectList({
+ kubeObjectClass: this,
+ cluster: options.cluster,
+ queryParams: options.queryParams as any,
+ namespace: options.namespace,
+ });
+ }
+
+ static useQuery(
+ this: U,
+ options: { name: string; namespace: string; cluster?: string }
+ ) {
+ return useKubeObject({
+ kubeObjectClass: this,
+ name: options.name,
+ namespace: options.namespace,
+ cluster: options.cluster,
+ });
+ }
+
static useList(
opts?: ApiListOptions
): [U[] | null, ApiError | null, (items: U[]) => void, (err: ApiError | null) => void] {
@@ -690,6 +725,7 @@ export function makeKubeObject(
args.unshift(this.getNamespace()!);
}
+ // @ts-expect-error
return this._class().apiEndpoint.delete(...args, {}, this._clusterName);
}
@@ -732,13 +768,14 @@ export function makeKubeObject(
patch(body: OpPatch[]) {
const patchMethod = this._class().apiEndpoint.patch;
- const args: Parameters = [body];
+ const args: Partial> = [body];
if (this.isNamespaced) {
args.push(this.getNamespace());
}
args.push(this.getName());
+ // @ts-expect-error
return this._class().apiEndpoint.patch(...args, {}, this._clusterName);
}
diff --git a/frontend/src/lib/k8s/clusterRole.ts b/frontend/src/lib/k8s/clusterRole.ts
index 72adcbea956..62d8a68be54 100644
--- a/frontend/src/lib/k8s/clusterRole.ts
+++ b/frontend/src/lib/k8s/clusterRole.ts
@@ -1,8 +1,14 @@
+import { ApiClient } from './api/v1/apiTypes';
import { apiFactory } from './apiProxy';
-import Role from './role';
+import { makeKubeObject } from './cluster';
+import { KubeRole } from './role';
-class ClusterRole extends Role {
- static apiEndpoint = apiFactory('rbac.authorization.k8s.io', 'v1', 'clusterroles');
+class ClusterRole extends makeKubeObject('role') {
+ static apiEndpoint: ApiClient = apiFactory(
+ 'rbac.authorization.k8s.io',
+ 'v1',
+ 'clusterroles'
+ );
static get className() {
return 'ClusterRole';
@@ -11,6 +17,10 @@ class ClusterRole extends Role {
get detailsRoute() {
return 'clusterRole';
}
+
+ get rules() {
+ return this.jsonData!.rules;
+ }
}
export default ClusterRole;
diff --git a/frontend/src/lib/k8s/clusterRoleBinding.ts b/frontend/src/lib/k8s/clusterRoleBinding.ts
index cb723e7270f..354c08fa3ce 100644
--- a/frontend/src/lib/k8s/clusterRoleBinding.ts
+++ b/frontend/src/lib/k8s/clusterRoleBinding.ts
@@ -1,7 +1,8 @@
import { apiFactory } from './apiProxy';
-import RoleBinding from './roleBinding';
+import { makeKubeObject } from './cluster';
+import { KubeRoleBinding } from './roleBinding';
-class ClusterRoleBinding extends RoleBinding {
+class ClusterRoleBinding extends makeKubeObject('roleBinding') {
static apiEndpoint = apiFactory('rbac.authorization.k8s.io', 'v1', 'clusterrolebindings');
static get className(): string {
@@ -11,6 +12,14 @@ class ClusterRoleBinding extends RoleBinding {
get detailsRoute() {
return 'clusterRoleBinding';
}
+
+ get roleRef() {
+ return this.jsonData!.roleRef;
+ }
+
+ get subjects(): KubeRoleBinding['subjects'] {
+ return this.jsonData!.subjects;
+ }
}
export default ClusterRoleBinding;
diff --git a/frontend/src/lib/k8s/crd.ts b/frontend/src/lib/k8s/crd.ts
index 91f7763e4ee..65ca1ddbee3 100644
--- a/frontend/src/lib/k8s/crd.ts
+++ b/frontend/src/lib/k8s/crd.ts
@@ -145,7 +145,7 @@ export function makeCustomResourceClass(
}
// Used for tests
- if (import.meta.env.UNDER_TEST === 'true') {
+ if (import.meta.env.UNDER_TEST || import.meta.env.STORYBOOK) {
const knownClass = ResourceClasses[apiInfoArgs[0][2]];
if (!!knownClass) {
return knownClass;
diff --git a/frontend/src/storybook.test.tsx b/frontend/src/storybook.test.tsx
index b519bb6b5cc..2cf294fd771 100644
--- a/frontend/src/storybook.test.tsx
+++ b/frontend/src/storybook.test.tsx
@@ -3,6 +3,7 @@ import { StyledEngineProvider, ThemeProvider } from '@mui/material';
import { StylesProvider } from '@mui/styles';
import type { Meta, StoryFn } from '@storybook/react';
import { composeStories } from '@storybook/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, render } from '@testing-library/react';
import path from 'path';
import themesConf from './lib/themes';
@@ -12,16 +13,27 @@ type StoryFile = {
[name: string]: StoryFn | Meta;
};
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 3 * 60_000,
+ refetchOnWindowFocus: false,
+ },
+ },
+});
+
const withThemeProvider = (Story: any) => {
const lightTheme = themesConf['light'];
const theme = lightTheme;
const ourThemeProvider = (
-
-
-
-
-
+
+
+
+
+
+
+
);
const generateClassName = (rule: any, styleSheet: any) =>
@@ -94,9 +106,10 @@ getAllStoryFiles().forEach(({ storyFile, componentName, storyDir }) => {
test(name, async () => {
const jsx = withThemeProvider(story);
+ queryClient.clear();
await act(async () => {
const { unmount, asFragment, rerender } = render(jsx);
- rerender(jsx);
+ await act(async () => rerender(jsx));
rerender(jsx);
await act(() => new Promise(resolve => setTimeout(resolve)));
diff --git a/plugins/headlamp-plugin/package-lock.json b/plugins/headlamp-plugin/package-lock.json
index bc4e052903e..4adfb95d292 100644
--- a/plugins/headlamp-plugin/package-lock.json
+++ b/plugins/headlamp-plugin/package-lock.json
@@ -45,6 +45,8 @@
"@storybook/react-webpack5": "^7.6.7",
"@storybook/test": "^8.1.10",
"@svgr/webpack": "^6.2.1",
+ "@tanstack/react-query": "^4.36.1",
+ "@tanstack/react-query-devtools": "^4.36.1",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^14.5.1",
@@ -9844,6 +9846,63 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "4.36.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz",
+ "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "4.36.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz",
+ "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "4.36.1",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-native": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "4.36.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.36.1.tgz",
+ "integrity": "sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/match-sorter-utils": "^8.7.0",
+ "superjson": "^1.10.0",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^4.36.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@tanstack/react-table": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.16.0.tgz",
@@ -13871,6 +13930,21 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
+ "node_modules/copy-anything": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
+ "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/core-js-compat": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz",
@@ -19893,6 +19967,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-what": {
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@@ -33507,6 +33593,18 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
+ "node_modules/superjson": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz",
+ "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==",
+ "license": "MIT",
+ "dependencies": {
+ "copy-anything": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -34894,6 +34992,15 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
+ "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -42779,6 +42886,30 @@
"remove-accents": "0.5.0"
}
},
+ "@tanstack/query-core": {
+ "version": "4.36.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz",
+ "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA=="
+ },
+ "@tanstack/react-query": {
+ "version": "4.36.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz",
+ "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==",
+ "requires": {
+ "@tanstack/query-core": "4.36.1",
+ "use-sync-external-store": "^1.2.0"
+ }
+ },
+ "@tanstack/react-query-devtools": {
+ "version": "4.36.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.36.1.tgz",
+ "integrity": "sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==",
+ "requires": {
+ "@tanstack/match-sorter-utils": "^8.7.0",
+ "superjson": "^1.10.0",
+ "use-sync-external-store": "^1.2.0"
+ }
+ },
"@tanstack/react-table": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.16.0.tgz",
@@ -45905,6 +46036,14 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
+ "copy-anything": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
+ "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
+ "requires": {
+ "is-what": "^4.1.8"
+ }
+ },
"core-js-compat": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz",
@@ -50311,6 +50450,11 @@
"get-intrinsic": "^1.1.1"
}
},
+ "is-what": {
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="
+ },
"is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@@ -60121,6 +60265,14 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
+ "superjson": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz",
+ "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==",
+ "requires": {
+ "copy-anything": "^3.0.2"
+ }
+ },
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -61157,6 +61309,12 @@
}
}
},
+ "use-sync-external-store": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
+ "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
+ "requires": {}
+ },
"util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
diff --git a/plugins/headlamp-plugin/package.json b/plugins/headlamp-plugin/package.json
index 64c270597be..4dca4f4e76b 100644
--- a/plugins/headlamp-plugin/package.json
+++ b/plugins/headlamp-plugin/package.json
@@ -49,6 +49,8 @@
"@storybook/react-webpack5": "^7.6.7",
"@storybook/test": "^8.1.10",
"@svgr/webpack": "^6.2.1",
+ "@tanstack/react-query": "^4.36.1",
+ "@tanstack/react-query-devtools": "^4.36.1",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^14.5.1",
|