1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'ui-components/resizing-anchor-error-boundary' of https://github.com/lensapp/lens into ui-components/resizing-anchor-error-boundary

This commit is contained in:
Gabriel 2023-04-24 17:26:07 +02:00
commit f4432a4659
27 changed files with 1190 additions and 964 deletions

View File

@ -51,7 +51,7 @@ jobs:
command: npm ci
- name: Build library parts
run: run npm build -- --ignore open-lens
run: npm run build -- --ignore open-lens
- run: npm run test:unit
name: Run tests

View File

@ -114,7 +114,7 @@ const tests: KubeApiParseTestData[] = [
}],
];
const throwtests = [
const invalidTests = [
undefined,
"",
"ajklsmh",
@ -125,7 +125,7 @@ describe("parseApi unit tests", () => {
expect(parseKubeApi(url)).toStrictEqual(expected);
});
it.each(throwtests)("testing %j should throw", (url) => {
expect(() => parseKubeApi(url as never)).toThrowError("invalid apiPath");
it.each(invalidTests)("testing %j should throw", (url) => {
expect(parseKubeApi(url as never)).toBe(undefined);
});
});

View File

@ -74,9 +74,13 @@ export class ApiManager {
return iter.find(this.apis.values(), pathOrCallback);
}
const { apiBase } = parseKubeApi(pathOrCallback);
const parsedApi = parseKubeApi(pathOrCallback);
return this.apis.get(apiBase);
if (!parsedApi) {
return undefined;
}
return this.apis.get(parsedApi.apiBase);
}
getApiByKind(kind: string, apiVersion: string) {
@ -141,9 +145,10 @@ export class ApiManager {
}
const { apiBase } = typeof apiOrBase === "string"
? parseKubeApi(apiOrBase)
? parseKubeApi(apiOrBase) ?? {}
: apiOrBase;
const api = this.getApi(apiBase);
const api = apiBase && this.getApi(apiBase);
if (!api) {
return undefined;

View File

@ -26,6 +26,30 @@ export interface JsonApiError {
errors?: { id: string; title: string; status?: number }[];
}
export interface KubeJsonApiErrorCause {
reason: string;
message: string;
field: string;
}
export interface KubeJsonApiErrorDetails {
name: string;
group: string;
kind: string;
causes: KubeJsonApiErrorCause[];
}
export interface KubeJsonApiError {
kind: "Status";
apiVersion: "v1";
metadata: object;
status: string;
message: string;
reason: string;
details: KubeJsonApiErrorDetails;
code: number;
}
export interface JsonApiParams<D> {
data?: PartialDeep<D>; // request body
}
@ -246,7 +270,7 @@ export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = Js
export class JsonApiErrorParsed {
isUsedForNotification = false;
constructor(private error: JsonApiError | DOMException, private messages: string[]) {
constructor(private error: JsonApiError | DOMException | KubeJsonApiError, private messages: string[]) {
}
get isAborted() {

View File

@ -22,16 +22,16 @@ export interface IKubeApiParsed extends IKubeApiLinkRef {
apiVersionWithGroup: string;
}
export function parseKubeApi(path: string): IKubeApiParsed {
export function parseKubeApi(path: string): IKubeApiParsed | undefined {
const apiPath = new URL(path, "https://localhost").pathname;
const [, prefix, ...parts] = apiPath.split("/");
const apiPrefix = `/${prefix}`;
const [left, right, namespaced] = array.split(parts, "namespaces");
let apiGroup!: string;
let apiVersion!: string;
let namespace!: string;
let resource!: string;
let name!: string;
let apiGroup: string;
let apiVersion: string | undefined;
let namespace: string | undefined;
let resource: string;
let name: string | undefined;
if (namespaced) {
switch (right.length) {
@ -46,26 +46,21 @@ export function parseKubeApi(path: string): IKubeApiParsed {
break;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
apiVersion = left.pop()!;
apiGroup = left.join("/");
let rest: string[];
[apiVersion, ...rest] = left;
apiGroup = rest.join("/");
} else {
switch (left.length) {
case 0:
throw new Error(`invalid apiPath: ${apiPath}`);
case 4:
[apiGroup, apiVersion, resource, name] = left;
break;
case 2:
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
resource = left.pop()!;
// fallthrough
case 1:
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
apiVersion = left.pop()!;
apiGroup = "";
break;
default:
if (left.length === 0) {
return undefined;
}
if (left.length === 1 || left.length === 2) {
[apiVersion, resource] = left;
apiGroup = "";
} else if (left.length === 4) {
[apiGroup, apiVersion, resource, name] = left;
} else {
/**
* Given that
* - `apiVersion` is `GROUP/VERSION` and
@ -82,15 +77,14 @@ export function parseKubeApi(path: string): IKubeApiParsed {
* 3. otherwise assume apiVersion <- left[0]
* 4. always resource, name <- left[(0 or 1)+1..]
*/
if (left[0].includes(".") || left[1].match(/^v[0-9]/)) {
[apiGroup, apiVersion] = left;
resource = left.slice(2).join("/");
} else {
apiGroup = "";
apiVersion = left[0];
[resource, name] = left.slice(1);
}
break;
if (left[0].includes(".") || left[1].match(/^v[0-9]/)) {
[apiGroup, apiVersion] = left;
resource = left.slice(2).join("/");
} else {
apiGroup = "";
apiVersion = left[0];
[resource, name] = left.slice(1);
}
}
}
@ -98,7 +92,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
if (!apiBase) {
throw new Error(`invalid apiPath: ${apiPath}`);
return undefined;
}
return {
@ -110,7 +104,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
}
function isIKubeApiParsed(refOrParsed: IKubeApiLinkRef | IKubeApiParsed): refOrParsed is IKubeApiParsed {
return "apiGroup" in refOrParsed;
return "apiGroup" in refOrParsed && !!refOrParsed.apiGroup;
}
export function createKubeApiURL(linkRef: IKubeApiLinkRef): string;

View File

@ -264,12 +264,16 @@ export class KubeApi<
allowedUsableVersions,
} = opts;
assert(fullApiPathname, "apiBase MUST be provied either via KubeApiOptions.apiBase or KubeApiOptions.objectConstructor.apiBase");
assert(fullApiPathname, "apiBase MUST be provided either via KubeApiOptions.apiBase or KubeApiOptions.objectConstructor.apiBase");
assert(request, "request MUST be provided if not in a cluster page frame context");
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(fullApiPathname);
const parsedApi = parseKubeApi(fullApiPathname);
assert(kind, "kind MUST be provied either via KubeApiOptions.kind or KubeApiOptions.objectConstructor.kind");
assert(parsedApi, "apiBase MUST be a valid kube api pathname");
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parsedApi;
assert(kind, "kind MUST be provided either via KubeApiOptions.kind or KubeApiOptions.objectConstructor.kind");
assert(apiPrefix, "apiBase MUST be parsable as a kubeApi selfLink style string");
this.doCheckPreferredVersion = doCheckPreferredVersion;
@ -308,8 +312,14 @@ export class KubeApi<
const apiBases = new Set(rawApiBases);
for (const apiUrl of apiBases) {
const parsedApi = parseKubeApi(apiUrl);
if (!parsedApi) {
continue;
}
try {
const { apiPrefix, apiGroup, resource } = parseKubeApi(apiUrl);
const { apiPrefix, apiGroup, resource } = parsedApi;
const list = await this.request.get(`${apiPrefix}/${apiGroup}`) as KubeApiResourceVersionList;
const resourceVersions = getOrderedVersions(list, this.allowedUsableVersions?.[apiGroup]);
@ -324,8 +334,8 @@ export class KubeApi<
};
}
}
} catch (error) {
// Exception is ignored as we can try the next url
} catch {
// ignore exception to try next url
}
}

View File

@ -7,18 +7,18 @@ import { parseKubeApi } from "../kube-api-parse";
import { kubeApiInjectionToken } from "./kube-api-injection-token";
import type { KubeApi } from "../kube-api";
export type GetKubeApiFromPath = (apiPath: string) => KubeApi | undefined;
const getKubeApiFromPathInjectable = getInjectable({
id: "get-kube-api-from-path",
instantiate: (di) => {
instantiate: (di): GetKubeApiFromPath => {
const kubeApis = di.injectMany(kubeApiInjectionToken);
return (apiPath: string) => {
const parsed = parseKubeApi(apiPath);
const kubeApi = kubeApis.find((api) => api.apiBase === parsed.apiBase);
return (kubeApi as KubeApi) || undefined;
return kubeApis.find((api) => api.apiBase === parsed?.apiBase);
};
},
});

View File

@ -324,7 +324,11 @@ export class KubeObjectStore<
@action
async loadFromPath(resourcePath: string) {
const { namespace, name } = parseKubeApi(resourcePath);
const parsedApi = parseKubeApi(resourcePath);
assert(parsedApi, "resourcePath must be a valid kube api");
const { namespace, name } = parsedApi;
assert(name, "name must be part of resourcePath");

View File

@ -26,15 +26,11 @@ import type { ItemObject } from "@k8slens/list-layout";
import type { Patch } from "rfc6902";
import assert from "assert";
import type { JsonObject } from "type-fest";
import requestKubeObjectPatchInjectable
from "./endpoints/resource-applier.api/request-patch.injectable";
import requestKubeObjectPatchInjectable from "./endpoints/resource-applier.api/request-patch.injectable";
import { apiKubeInjectionToken } from "./api-kube";
import requestKubeObjectCreationInjectable
from "./endpoints/resource-applier.api/request-update.injectable";
import requestKubeObjectCreationInjectable from "./endpoints/resource-applier.api/request-update.injectable";
import { dump } from "js-yaml";
import {
getLegacyGlobalDiForExtensionApi,
} from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import autoBind from "auto-bind";
export type KubeJsonApiDataFor<K> = K extends KubeObject<infer Metadata, infer Status, infer Spec>
@ -552,14 +548,30 @@ export class KubeObject<
}
constructor(data: KubeJsonApiData<Metadata, Status, Spec>) {
if (typeof data !== "object") {
if (!isObject(data)) {
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
}
if (!data.metadata || typeof data.metadata !== "object") {
if (!isObject(data.metadata)) {
throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata`, data);
}
if (!isString(data.metadata.name)) {
throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata.name being a string`, data);
}
if (!isString(data.metadata.uid)) {
throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata.uid being a string`, data);
}
if (!isString(data.metadata.resourceVersion)) {
throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata.resourceVersion being a string`, data);
}
if (!isString(data.metadata.selfLink)) {
throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata.selfLink being a string`, data);
}
Object.assign(this, data);
autoBind(this);
}

View File

@ -2,6 +2,9 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { JsonApiErrorParsed } from "../k8s-api/json-api";
export const getErrorMessage = (error: unknown): string => {
if (typeof error === "string") {
return error;
@ -11,5 +14,9 @@ export const getErrorMessage = (error: unknown): string => {
return error.message;
}
if (error instanceof JsonApiErrorParsed) {
return error.toString();
}
return JSON.stringify(error);
};

View File

@ -12,8 +12,6 @@ import createEditResourceTabInjectable from "../../../renderer/components/dock/e
import getRandomIdForEditResourceTabInjectable from "../../../renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { CallForPatchResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
import callForPatchResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints";
import showSuccessNotificationInjectable from "../../../renderer/components/notifications/show-success-notification.injectable";
@ -21,15 +19,22 @@ import showErrorNotificationInjectable from "../../../renderer/components/notifi
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
import type { ApiKubePatch } from "../../../renderer/k8s/api-kube-patch.injectable";
import type { ApiKubeGet } from "../../../renderer/k8s/api-kube-get.injectable";
import apiKubePatchInjectable from "../../../renderer/k8s/api-kube-patch.injectable";
import apiKubeGetInjectable from "../../../renderer/k8s/api-kube-get.injectable";
import type { KubeJsonApiData } from "../../../common/k8s-api/kube-json-api";
import type { BaseKubeJsonApiObjectMetadata, KubeObjectScope } from "../../../common/k8s-api/kube-object";
import { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
import type { ShowNotification } from "../../../renderer/components/notifications";
import React from "react";
describe("cluster/namespaces - edit namespace from new tab", () => {
let builder: ApplicationBuilder;
let callForResourceMock: AsyncFnMock<CallForResource>;
let callForPatchResourceMock: AsyncFnMock<CallForPatchResource>;
let showSuccessNotificationMock: jest.Mock;
let showErrorNotificationMock: jest.Mock;
let apiKubePatchMock: AsyncFnMock<ApiKubePatch>;
let apiKubeGetMock: AsyncFnMock<ApiKubeGet>;
let showSuccessNotificationMock: jest.MockedFunction<ShowNotification>;
let showErrorNotificationMock: jest.MockedFunction<ShowNotification>;
beforeEach(() => {
builder = getApplicationBuilder();
@ -57,11 +62,11 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
.mockReturnValueOnce("some-second-tab-id"),
);
callForResourceMock = asyncFn();
windowDi.override(callForResourceInjectable, () => callForResourceMock);
apiKubePatchMock = asyncFn();
windowDi.override(apiKubePatchInjectable, () => apiKubePatchMock);
callForPatchResourceMock = asyncFn();
windowDi.override(callForPatchResourceInjectable, () => callForPatchResourceMock);
apiKubeGetMock = asyncFn();
windowDi.override(apiKubeGetInjectable, () => apiKubeGetMock);
});
builder.afterWindowStart(() => {
@ -156,16 +161,16 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
});
it("calls for namespace", () => {
expect(callForResourceMock).toHaveBeenCalledWith(
expect(apiKubeGetMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid",
);
});
describe("when call for namespace resolves with namespace", () => {
let someNamespace: Namespace;
let someNamespaceData: KubeJsonApiData<BaseKubeJsonApiObjectMetadata<KubeObjectScope.Cluster>, unknown, unknown>;
beforeEach(async () => {
someNamespace = new Namespace({
someNamespaceData = ({
apiVersion: "some-api-version",
kind: "Namespace",
@ -173,16 +178,12 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
selfLink: "/apis/some-api-version/namespaces/some-uid",
somePropertyToBeRemoved: "some-value",
somePropertyToBeChanged: "some-old-value",
},
});
await callForResourceMock.resolve({
callWasSuccessful: true,
response: someNamespace,
});
await apiKubeGetMock.resolve(someNamespaceData);
});
it("renders", () => {
@ -206,9 +207,9 @@ metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value
selfLink: /apis/some-api-version/namespaces/some-uid
`);
});
@ -226,15 +227,22 @@ metadata:
});
it("calls for save with just the adding version label", () => {
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someNamespace,
[{
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
expect(apiKubePatchMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid",
{
data: [{
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
},
}],
},
{
headers: {
"content-type": "application/json-patch+json",
},
}],
},
);
});
@ -262,9 +270,11 @@ metadata:
describe("when saving resolves with success", () => {
beforeEach(async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: true,
response: { name: "some-name", kind: "Namespace" },
await apiKubePatchMock.resolve({
kind: "Namespace",
metadata: {
name: "some-name",
},
});
});
@ -309,12 +319,9 @@ metadata:
});
});
describe("when saving resolves with failure", () => {
describe("when saving fails", () => {
beforeEach(async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: false,
error: "some-error",
});
await apiKubePatchMock.reject(new Error("some-error"));
});
it("renders", () => {
@ -380,9 +387,11 @@ metadata:
describe("when saving resolves with success", () => {
beforeEach(async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: true,
response: { name: "some-name", kind: "Namespace" },
await apiKubePatchMock.resolve({
kind: "Namespace",
metadata: {
name: "some-name",
},
});
});
@ -397,12 +406,9 @@ metadata:
});
});
describe("when saving resolves with failure", () => {
describe("when saving failings", () => {
beforeEach(async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: false,
error: "Some error",
});
await apiKubePatchMock.reject(new Error("some-error"));
});
it("renders", () => {
@ -415,6 +421,57 @@ metadata:
).toBeInTheDocument();
});
});
describe("when saving failings with a JsonApiError", () => {
beforeEach(async () => {
await apiKubePatchMock.reject(new JsonApiErrorParsed(
{
kind: "Status",
apiVersion: "v1",
metadata: {},
status: "Failure",
message: "PodDisruptionBudget.policy \"frontend-pdb\" is invalid: spec.minAvailable: Invalid value: -10: must be greater than or equal to 0",
reason: "Invalid",
details: {
name: "frontend-pdb",
group: "policy",
kind: "PodDisruptionBudget",
causes: [
{
reason: "FieldValueInvalid",
message: "Invalid value: -10: must be greater than or equal to 0",
field: "spec.minAvailable",
},
],
},
code: 422,
},
[
"PodDisruptionBudget.policy \"frontend-pdb\" is invalid: spec.minAvailable: Invalid value: -10: must be greater than or equal to 0",
],
));
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not close the dock tab", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("shows an error notification with a condensed message", () => {
expect(showErrorNotificationMock).toBeCalledWith(
<p>
{"Failed to save resource:"}
{" "}
{'PodDisruptionBudget.policy "frontend-pdb" is invalid: spec.minAvailable: Invalid value: -10: must be greater than or equal to 0'}
</p>,
);
});
});
});
describe("when selecting to cancel", () => {
@ -451,9 +508,9 @@ metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value
selfLink: /apis/some-api-version/namespaces/some-uid
`,
},
});
@ -474,9 +531,9 @@ metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value
selfLink: /apis/some-api-version/namespaces/some-uid
`);
});
@ -499,9 +556,9 @@ metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value
selfLink: /apis/some-api-version/namespaces/some-uid
`,
draft: `apiVersion: some-api-version
kind: Namespace
@ -509,9 +566,9 @@ metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value
selfLink: /apis/some-api-version/namespaces/some-uid
`,
});
});
@ -526,41 +583,46 @@ metadata:
});
it("calls for save with changed configuration", () => {
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someNamespace,
[
{
op: "remove",
path: "/metadata/somePropertyToBeRemoved",
},
{
op: "add",
path: "/metadata/someAddedProperty",
value: "some-new-value",
},
{
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
expect(apiKubePatchMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid",
{
data: [
{
op: "remove",
path: "/metadata/somePropertyToBeRemoved",
},
{
op: "add",
path: "/metadata/someAddedProperty",
value: "some-new-value",
},
{
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
},
},
{
op: "replace",
path: "/metadata/somePropertyToBeChanged",
value: "some-changed-value",
},
],
},
{
headers: {
"content-type": "application/json-patch+json",
},
{
op: "replace",
path: "/metadata/somePropertyToBeChanged",
value: "some-changed-value",
},
],
},
);
});
it("given save resolves and another change in configuration, when saving, calls for save with changed configuration", async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: true,
response: {
await apiKubePatchMock.resolve({
kind: "Namespace",
metadata: {
name: "some-name",
kind: "Namespace",
},
});
@ -585,7 +647,7 @@ metadata:
});
callForPatchResourceMock.mockClear();
apiKubePatchMock.mockClear();
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
@ -593,15 +655,22 @@ metadata:
fireEvent.click(saveButton);
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someNamespace,
[
{
op: "add",
path: "/metadata/someOtherAddedProperty",
value: "some-other-new-value",
expect(apiKubePatchMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid",
{
data: [
{
op: "add",
path: "/metadata/someOtherAddedProperty",
value: "some-other-new-value",
},
],
},
{
headers: {
"content-type": "application/json-patch+json",
},
],
},
);
});
});
@ -692,7 +761,7 @@ metadata:
describe("given clicking the context menu for second namespace, when clicking to edit namespace", () => {
beforeEach(() => {
callForResourceMock.mockClear();
apiKubeGetMock.mockClear();
// TODO: Make implementation match the description
const namespaceStub = new Namespace(someOtherNamespaceDataStub);
@ -725,7 +794,7 @@ metadata:
});
it("calls for second namespace", () => {
expect(callForResourceMock).toHaveBeenCalledWith(
expect(apiKubeGetMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-other-uid",
);
});
@ -747,10 +816,7 @@ metadata:
},
});
await callForResourceMock.resolve({
callWasSuccessful: true,
response: someOtherNamespace,
});
await apiKubeGetMock.resolve(someOtherNamespace);
});
it("renders", () => {
@ -773,7 +839,7 @@ metadata:
});
it("when selecting to save, calls for save of second namespace with just the add edit version label", () => {
callForPatchResourceMock.mockClear();
apiKubePatchMock.mockClear();
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-second-tab-id",
@ -781,21 +847,28 @@ metadata:
fireEvent.click(saveButton);
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someOtherNamespace,
[{
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
expect(apiKubePatchMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-other-uid",
{
data: [{
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
},
}],
},
{
headers: {
"content-type": "application/json-patch+json",
},
}],
},
);
});
describe("when clicking dock tab for the first namespace", () => {
beforeEach(() => {
callForResourceMock.mockClear();
apiKubeGetMock.mockClear();
const tab = rendered.getByTestId("dock-tab-for-some-first-tab-id");
@ -825,7 +898,7 @@ metadata:
});
it("does not call for namespace", () => {
expect(callForResourceMock).not.toHaveBeenCalledWith("/apis/some-api-version/namespaces/some-uid");
expect(apiKubeGetMock).not.toHaveBeenCalledWith("/apis/some-api-version/namespaces/some-uid");
});
it("has configuration in the editor", () => {
@ -839,14 +912,14 @@ metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value
selfLink: /apis/some-api-version/namespaces/some-uid
`);
});
it("when selecting to save, calls for save of first namespace with just the new edit version label", () => {
callForPatchResourceMock.mockClear();
apiKubePatchMock.mockClear();
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
@ -854,15 +927,22 @@ metadata:
fireEvent.click(saveButton);
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someNamespace,
[ {
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
expect(apiKubePatchMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid",
{
data: [{
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
},
}],
},
{
headers: {
"content-type": "application/json-patch+json",
},
}],
},
);
});
});
@ -870,41 +950,9 @@ metadata:
});
});
describe("when call for namespace resolves without namespace", () => {
beforeEach(async () => {
await callForResourceMock.resolve({
callWasSuccessful: true,
response: undefined,
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("still shows the dock tab for editing namespace", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("shows error message", () => {
expect(
rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
).toHaveTextContent("Resource not found");
});
it("does not show error notification", () => {
expect(showErrorNotificationMock).not.toHaveBeenCalled();
});
});
describe("when call for namespace resolves with failure", () => {
beforeEach(async () => {
await callForResourceMock.resolve({
callWasSuccessful: false,
error: "some-error",
});
await apiKubeGetMock.reject(new Error("some-error-missing-namespace"));
});
it("renders", () => {

View File

@ -8,8 +8,8 @@ import type { ApplicationBuilder } from "../../../renderer/components/test-utils
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
import type { RequestKubeResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/request-kube-resource.injectable";
import requestKubeResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/request-kube-resource.injectable";
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
import { TabKind } from "../../../renderer/components/dock/dock/store";
@ -17,14 +17,14 @@ import { Namespace } from "../../../common/k8s-api/endpoints";
describe("cluster/namespaces - edit namespaces from previously opened tab", () => {
let builder: ApplicationBuilder;
let callForNamespaceMock: AsyncFnMock<CallForResource>;
let requestKubeResourceMock: AsyncFnMock<RequestKubeResource>;
beforeEach(() => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
callForNamespaceMock = asyncFn();
requestKubeResourceMock = asyncFn();
builder.beforeWindowStart(({ windowDi }) => {
windowDi.override(
@ -32,7 +32,7 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
() => "/some-directory-for-lens-local-storage",
);
windowDi.override(callForResourceInjectable, () => callForNamespaceMock);
windowDi.override(requestKubeResourceInjectable, () => requestKubeResourceMock);
});
builder.afterWindowStart(() => {
@ -97,7 +97,7 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
});
it("calls for namespace", () => {
expect(callForNamespaceMock).toHaveBeenCalledWith(
expect(requestKubeResourceMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid",
);
});
@ -122,7 +122,7 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
// TODO: Figure out why act is needed here. In CI it works without it.
await act(async () => {
await callForNamespaceMock.resolve({
await requestKubeResourceMock.resolve({
callWasSuccessful: true,
response: someNamespace,
});

View File

@ -4,6 +4,7 @@
*/
import { getInjectable, createInstantiationTargetDecorator, instantiationDecoratorToken } from "@ogre-tools/injectable";
import { pick } from "lodash";
import { inspect } from "util";
import { parseKubeApi } from "../../../common/k8s-api/kube-api-parse";
import showDetailsInjectable from "../../../renderer/components/kube-detail-params/show-details.injectable";
import emitTelemetryInjectable from "./emit-telemetry.injectable";
@ -21,13 +22,15 @@ const telemetryDecoratorForShowDetailsInjectable = getInjectable({
? {
action: "open",
...(() => {
try {
return {
resource: pick(parseKubeApi(args[0]), "apiPrefix", "apiVersion", "apiGroup", "namespace", "resource", "name"),
};
} catch (error) {
return { error: `${error}` };
const parsedApi = parseKubeApi(args[0]);
if (!parsedApi) {
return { error: `invalid apiPath: ${inspect(args[0])}` };
}
return {
resource: pick(parsedApi, "apiPrefix", "apiVersion", "apiGroup", "namespace", "resource", "name"),
};
})(),
}
: {

View File

@ -21,7 +21,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
showDetails = di.inject(showDetailsInjectable);
});
it("when showDetails is called with no selflink (ie closing) should emit telemetry with param indicating closing the drawer", () => {
it("when showDetails is called with no selfLink (ie closing) should emit telemetry with param indicating closing the drawer", () => {
showDetails(undefined);
expect(emitAppEventMock).toBeCalledWith({
@ -34,7 +34,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
});
});
it("when showDetails is called with empty selflink (ie closing) should emit telemetry with param indicating closing the drawer", () => {
it("when showDetails is called with empty selfLink (ie closing) should emit telemetry with param indicating closing the drawer", () => {
showDetails("");
expect(emitAppEventMock).toBeCalledWith({
@ -47,7 +47,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
});
});
it("when showDetails is called with valid selflink should emit telemetry with param indicating opening the drawer with that resource", () => {
it("when showDetails is called with valid selfLink should emit telemetry with param indicating opening the drawer with that resource", () => {
showDetails("/api/v1/namespaces/default/pods/some-name");
expect(emitAppEventMock).toBeCalledWith({
@ -68,7 +68,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
});
});
it("when showDetails is called with invalid selflink should emit telemetry with param indicating opening the drawer but also show error", () => {
it("when showDetails is called with invalid selfLink should emit telemetry with param indicating opening the drawer but also show error", () => {
showDetails("some-non-self-link-value");
expect(emitAppEventMock).toBeCalledWith({
@ -77,7 +77,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
name: "show-details",
params: {
action: "open",
error: "Error: invalid apiPath: /some-non-self-link-value",
error: "invalid apiPath: 'some-non-self-link-value'",
},
});
});

View File

@ -22,8 +22,16 @@ describe("NetworkPolicyDetails", () => {
it("should render w/o errors", () => {
const policy = new NetworkPolicy({
metadata: {} as never,
spec: {} as never,
metadata: {
name: "some-network-policy-name",
namespace: "some-namespace",
resourceVersion: "1",
selfLink: "/apis/networking.k8s.io/v1/namespace/some-namespace/some-network-policy-name",
uid: "1",
},
spec: {
podSelector: {},
},
apiVersion: "networking.k8s.io/v1",
kind: "NetworkPolicy",
});
@ -34,7 +42,13 @@ describe("NetworkPolicyDetails", () => {
it("should render egress nodeSelector", async () => {
const policy = new NetworkPolicy({
metadata: {} as never,
metadata: {
name: "some-network-policy-name",
namespace: "some-namespace",
resourceVersion: "1",
selfLink: "/apis/networking.k8s.io/v1/namespace/some-namespace/some-network-policy-name",
uid: "1",
},
spec: {
egress: [{
to: [{
@ -58,7 +72,13 @@ describe("NetworkPolicyDetails", () => {
it("should not crash if egress nodeSelector doesn't have matchLabels", async () => {
const policy = new NetworkPolicy({
metadata: {} as never,
metadata: {
name: "some-network-policy-name",
namespace: "some-namespace",
resourceVersion: "1",
selfLink: "/apis/networking.k8s.io/v1/namespace/some-namespace/some-network-policy-name",
uid: "1",
},
spec: {
egress: [{
to: [{

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import callForPatchResourceInjectable from "./call-for-patch-resource.injectable";
export default getGlobalOverride(callForPatchResourceInjectable, () => () => {
throw new Error(
"Tried to call patching of kube resource without explicit override.",
);
});

View File

@ -1,49 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { AsyncResult } from "@k8slens/utilities";
import apiManagerInjectable from "../../../../../../common/k8s-api/api-manager/manager.injectable";
import type { JsonPatch } from "../../../../../../common/k8s-api/kube-object.store";
import type { KubeObject } from "../../../../../../common/k8s-api/kube-object";
import assert from "assert";
import { getErrorMessage } from "../../../../../../common/utils/get-error-message";
export type CallForPatchResource = (
item: KubeObject,
patch: JsonPatch
) => AsyncResult<{ name: string; kind: string }>;
const callForPatchResourceInjectable = getInjectable({
id: "call-for-patch-resource",
instantiate: (di): CallForPatchResource => {
const apiManager = di.inject(apiManagerInjectable);
return async (item, patch) => {
const store = apiManager.getStore(item.selfLink);
assert(store);
let kubeObject: KubeObject;
try {
kubeObject = await store.patch(item, patch);
} catch (e: any) {
return {
callWasSuccessful: false,
error: getErrorMessage(e),
};
}
return {
callWasSuccessful: true,
response: { name: kubeObject.getName(), kind: kubeObject.kind },
};
};
},
causesSideEffects: true,
});
export default callForPatchResourceInjectable;

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import callForResourceInjectable from "./call-for-resource.injectable";
export default getGlobalOverride(callForResourceInjectable, () => () => {
throw new Error(
"Tried to call for kube resource without explicit override.",
);
});

View File

@ -1,41 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { KubeObject } from "../../../../../../common/k8s-api/kube-object";
import { parseKubeApi } from "../../../../../../common/k8s-api/kube-api-parse";
import type { AsyncResult } from "@k8slens/utilities";
import { getErrorMessage } from "../../../../../../common/utils/get-error-message";
import apiKubeInjectable from "../../../../../k8s/api-kube.injectable";
export type CallForResource = (selfLink: string) => AsyncResult<KubeObject | undefined>;
const callForResourceInjectable = getInjectable({
id: "call-for-resource",
instantiate: (di): CallForResource => {
const apiKube = di.inject(apiKubeInjectable);
return async (apiPath: string) => {
const parsed = parseKubeApi(apiPath);
if (!parsed.name) {
return { callWasSuccessful: false, error: "Invalid API path" };
}
try {
return {
callWasSuccessful: true,
response: new KubeObject(await apiKube.get(apiPath)),
};
} catch (e) {
return { callWasSuccessful: false, error: getErrorMessage(e) };
}
};
},
causesSideEffects: true,
});
export default callForResourceInjectable;

View File

@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { CallForResource } from "./call-for-resource/call-for-resource.injectable";
import callForResourceInjectable from "./call-for-resource/call-for-resource.injectable";
import type { RequestKubeResource } from "./request-kube-resource.injectable";
import requestKubeResourceInjectable from "./request-kube-resource.injectable";
import { waitUntilDefined } from "@k8slens/utilities";
import editResourceTabStoreInjectable from "../store.injectable";
import type { EditingResource, EditResourceTabStore } from "../store";
@ -12,8 +12,8 @@ import { action, computed, observable, runInAction } from "mobx";
import type { KubeObject, RawKubeObject } from "../../../../../common/k8s-api/kube-object";
import yaml from "js-yaml";
import assert from "assert";
import type { CallForPatchResource } from "./call-for-patch-resource/call-for-patch-resource.injectable";
import callForPatchResourceInjectable from "./call-for-patch-resource/call-for-patch-resource.injectable";
import type { RequestPatchKubeResource } from "./request-patch-kube-resource.injectable";
import requestPatchKubeResourceInjectable from "./request-patch-kube-resource.injectable";
import { createPatch } from "rfc6902";
import type { ShowNotification } from "../../../notifications";
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
@ -28,8 +28,8 @@ const editResourceModelInjectable = getInjectable({
const store = di.inject(editResourceTabStoreInjectable);
const model = new EditResourceModel({
callForResource: di.inject(callForResourceInjectable),
callForPatchResource: di.inject(callForPatchResourceInjectable),
requestKubeResource: di.inject(requestKubeResourceInjectable),
requestPatchKubeResource: di.inject(requestPatchKubeResourceInjectable),
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
showErrorNotification: di.inject(showErrorNotificationInjectable),
store,
@ -50,8 +50,8 @@ const editResourceModelInjectable = getInjectable({
export default editResourceModelInjectable;
interface Dependencies {
callForResource: CallForResource;
callForPatchResource: CallForPatchResource;
requestKubeResource: RequestKubeResource;
requestPatchKubeResource: RequestPatchKubeResource;
waitForEditingResource: () => Promise<EditingResource>;
showSuccessNotification: ShowNotification;
showErrorNotification: ShowNotification;
@ -59,17 +59,21 @@ interface Dependencies {
readonly tabId: string;
}
function getEditSelfLinkFor(object: RawKubeObject): string {
function getEditSelfLinkFor(object: RawKubeObject): string | undefined {
const lensVersionLabel = object.metadata.labels?.[EditResourceLabelName];
if (lensVersionLabel) {
const { apiVersionWithGroup, ...parsedApi } = parseKubeApi(object.metadata.selfLink);
const parsedKubeApi = parseKubeApi(object.metadata.selfLink);
parsedApi.apiVersion = lensVersionLabel;
if (!parsedKubeApi) {
return undefined;
}
const { apiVersionWithGroup, ...parsedApi } = parsedKubeApi;
return createKubeApiURL({
...parsedApi,
apiVersion: `${parsedApi.apiGroup}/${parsedApi.apiVersion}`,
apiVersion: lensVersionLabel,
});
}
@ -130,7 +134,7 @@ export class EditResourceModel {
load = async (): Promise<void> => {
await this.dependencies.waitForEditingResource();
let result = await this.dependencies.callForResource(this.selfLink);
let result = await this.dependencies.requestKubeResource(this.selfLink);
if (!result.callWasSuccessful) {
return void this.dependencies.showErrorNotification(`Loading resource failed: ${result.error}`);
@ -139,9 +143,13 @@ export class EditResourceModel {
if (result?.response?.metadata.labels?.[EditResourceLabelName]) {
const parsed = parseKubeApi(this.selfLink);
if (!parsed) {
return void this.dependencies.showErrorNotification(`Object's selfLink is invalid: "${this.selfLink}"`);
}
parsed.apiVersion = result.response.metadata.labels[EditResourceLabelName];
result = await this.dependencies.callForResource(createKubeApiURL(parsed));
result = await this.dependencies.requestKubeResource(createKubeApiURL(parsed));
}
if (!result.callWasSuccessful) {
@ -186,7 +194,18 @@ export class EditResourceModel {
const patches = createPatch(firstVersion, currentVersion);
const selfLink = getEditSelfLinkFor(currentVersion);
const result = await this.dependencies.callForPatchResource(this.resource, patches);
if (!selfLink) {
this.dependencies.showErrorNotification((
<p>
{`Cannot save resource, unknown selfLink: "${currentVersion.metadata.selfLink}"`}
</p>
));
return null;
}
const result = await this.dependencies.requestPatchKubeResource(selfLink, patches);
if (!result.callWasSuccessful) {
this.dependencies.showErrorNotification((

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { KubeObjectMetadata, KubeObjectScope } from "../../../../../common/k8s-api/kube-object";
import { KubeObject } from "../../../../../common/k8s-api/kube-object";
import type { AsyncResult } from "@k8slens/utilities";
import { getErrorMessage } from "../../../../../common/utils/get-error-message";
import type { Writable } from "type-fest";
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api";
import { parseKubeApi } from "../../../../../common/k8s-api/kube-api-parse";
import apiKubeGetInjectable from "../../../../k8s/api-kube-get.injectable";
export type RequestKubeResource = (selfLink: string) => AsyncResult<KubeObject | undefined>;
const requestKubeResourceInjectable = getInjectable({
id: "request-kube-resource",
instantiate: (di): RequestKubeResource => {
const apiKubeGet = di.inject(apiKubeGetInjectable);
return async (selfLink) => {
const parsed = parseKubeApi(selfLink);
if (!parsed?.name) {
return { callWasSuccessful: false, error: "Invalid API path" };
}
try {
const rawData = await apiKubeGet(selfLink) as KubeJsonApiData<KubeObjectMetadata<KubeObjectScope>, unknown, unknown>;
(rawData.metadata as Writable<typeof rawData.metadata>).selfLink = selfLink;
return {
callWasSuccessful: true,
response: new KubeObject(rawData),
};
} catch (e) {
return { callWasSuccessful: false, error: getErrorMessage(e) };
}
};
},
});
export default requestKubeResourceInjectable;

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { AsyncResult } from "@k8slens/utilities";
import type { JsonPatch } from "../../../../../common/k8s-api/kube-object.store";
import { getErrorMessage } from "../../../../../common/utils/get-error-message";
import { patchTypeHeaders } from "../../../../../common/k8s-api/kube-api";
import apiKubePatchInjectable from "../../../../k8s/api-kube-patch.injectable";
export type RequestPatchKubeResource = (selfLink: string, patch: JsonPatch) => AsyncResult<{ name: string; kind: string }>;
const requestPatchKubeResourceInjectable = getInjectable({
id: "request-patch-kube-resource",
instantiate: (di): RequestPatchKubeResource => {
const apiKubePatch = di.inject(apiKubePatchInjectable);
return async (selfLink, patch) => {
try {
const { metadata, kind } = await apiKubePatch(selfLink, { data: patch }, {
headers: {
"content-type": patchTypeHeaders.json,
},
});
return {
callWasSuccessful: true,
response: { name: metadata.name, kind },
};
} catch (e) {
return {
callWasSuccessful: false,
error: getErrorMessage(e),
};
}
};
},
});
export default requestPatchKubeResourceInjectable;

View File

@ -16,7 +16,6 @@ const showErrorNotificationInjectable = getInjectable({
return (message, customOpts = {}) =>
notificationsStore.add({
status: NotificationStatus.ERROR,
timeout: 5000,
message,
...customOpts,
});

View File

@ -16,7 +16,6 @@ const showInfoNotificationInjectable = getInjectable({
return (message, customOpts = {}) =>
notificationsStore.add({
status: NotificationStatus.INFO,
timeout: 5000,
message,
...customOpts,
});

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { KubeJsonApi } from "../../common/k8s-api/kube-json-api";
import apiKubeInjectable from "./api-kube.injectable";
export type ApiKubeGet = KubeJsonApi["get"];
const apiKubeGetInjectable = getInjectable({
id: "api-kube-get",
instantiate: (di): ApiKubeGet => {
const apiKube = di.inject(apiKubeInjectable);
return (...params) => apiKube.get(...params);
},
});
export default apiKubeGetInjectable;

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { KubeJsonApi } from "../../common/k8s-api/kube-json-api";
import apiKubeInjectable from "./api-kube.injectable";
export type ApiKubePatch = KubeJsonApi["patch"];
const apiKubePatchInjectable = getInjectable({
id: "api-kube-patch",
instantiate: (di): ApiKubePatch => {
const apiKube = di.inject(apiKubeInjectable);
return (...params) => apiKube.patch(...params);
},
});
export default apiKubePatchInjectable;