mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix editing of kube resource (#5906)
This commit is contained in:
1 parent
b94672b5a8
commit
e682c7de45
59 files changed
+14123
-298
No files matched your search
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getGlobalOverride } from "../../../../../../common/test-utils/get-global-override";
|
||||
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.",
|
||||
);
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 "../../../../../../common/utils/async-result";
|
||||
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
|
||||
) => Promise<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;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getGlobalOverride } from "../../../../../../common/test-utils/get-global-override";
|
||||
import callForResourceInjectable from "./call-for-resource.injectable";
|
||||
|
||||
export default getGlobalOverride(callForResourceInjectable, () => () => {
|
||||
throw new Error(
|
||||
"Tried to call for kube resource without explicit override.",
|
||||
);
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 { KubeObject } from "../../../../../../common/k8s-api/kube-object";
|
||||
import { parseKubeApi } from "../../../../../../common/k8s-api/kube-api-parse";
|
||||
import type { AsyncResult } from "../../../../../../common/utils/async-result";
|
||||
import { getErrorMessage } from "../../../../../../common/utils/get-error-message";
|
||||
import apiManagerInjectable from "../../../../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import { waitUntilDefined } from "../../../../../../common/utils";
|
||||
|
||||
export type CallForResource = (
|
||||
selfLink: string
|
||||
) => Promise<AsyncResult<KubeObject | undefined>>;
|
||||
|
||||
const callForResourceInjectable = getInjectable({
|
||||
id: "call-for-resource",
|
||||
|
||||
instantiate: (di): CallForResource => {
|
||||
const apiManager = di.inject(apiManagerInjectable);
|
||||
|
||||
return async (apiPath: string) => {
|
||||
const api = await waitUntilDefined(() => apiManager.getApi(apiPath));
|
||||
|
||||
const parsed = parseKubeApi(apiPath);
|
||||
|
||||
if (!api || !parsed.name) {
|
||||
return { callWasSuccessful: false, error: "Invalid API path" };
|
||||
}
|
||||
|
||||
let resource: KubeObject | null;
|
||||
|
||||
try {
|
||||
resource = await api.get({
|
||||
name: parsed.name,
|
||||
namespace: parsed.namespace,
|
||||
});
|
||||
} catch (e) {
|
||||
return { callWasSuccessful: false, error: getErrorMessage(e) };
|
||||
}
|
||||
|
||||
return { callWasSuccessful: true, response: resource || undefined };
|
||||
};
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForResourceInjectable;
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* 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 { waitUntilDefined } from "../../../../../common/utils";
|
||||
import editResourceTabStoreInjectable from "../store.injectable";
|
||||
import type { EditingResource, EditResourceTabStore } from "../store";
|
||||
import { action, computed, observable, runInAction } from "mobx";
|
||||
import type { KubeObject } 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 { createPatch } from "rfc6902";
|
||||
import type { ShowNotification } from "../../../notifications";
|
||||
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
|
||||
import React from "react";
|
||||
import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
|
||||
|
||||
const editResourceModelInjectable = getInjectable({
|
||||
id: "edit-resource-model",
|
||||
|
||||
instantiate: async (di, tabId: string) => {
|
||||
const store = di.inject(editResourceTabStoreInjectable);
|
||||
|
||||
const model = new EditResourceModel({
|
||||
callForResource: di.inject(callForResourceInjectable),
|
||||
callForPatchResource: di.inject(callForPatchResourceInjectable),
|
||||
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
|
||||
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
||||
store,
|
||||
tabId,
|
||||
|
||||
waitForEditingResource: () =>
|
||||
waitUntilDefined(() => store.getData(tabId)),
|
||||
});
|
||||
|
||||
await model.load();
|
||||
|
||||
return model;
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, tabId: string) => tabId,
|
||||
}),
|
||||
});
|
||||
|
||||
export default editResourceModelInjectable;
|
||||
|
||||
interface Dependencies {
|
||||
callForResource: CallForResource;
|
||||
callForPatchResource: CallForPatchResource;
|
||||
waitForEditingResource: () => Promise<EditingResource>;
|
||||
showSuccessNotification: ShowNotification;
|
||||
showErrorNotification: ShowNotification;
|
||||
store: EditResourceTabStore;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export class EditResourceModel {
|
||||
constructor(private dependencies: Dependencies) {
|
||||
}
|
||||
|
||||
readonly configuration = {
|
||||
value: computed(() => this.editingResource.draft || this.editingResource.firstDraft || ""),
|
||||
|
||||
onChange: action((value: string) => {
|
||||
this.editingResource.draft = value;
|
||||
this.configuration.error.value.set("");
|
||||
}),
|
||||
|
||||
error: {
|
||||
value: observable.box(""),
|
||||
|
||||
onChange: action((error: string) => {
|
||||
this.configuration.error.value.set(error);
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@observable private _resource: KubeObject | undefined;
|
||||
|
||||
@computed get shouldShowErrorAboutNoResource() {
|
||||
return !this._resource;
|
||||
}
|
||||
|
||||
@computed get resource() {
|
||||
assert(this._resource, "Resource does not have data");
|
||||
|
||||
return this._resource;
|
||||
}
|
||||
|
||||
@computed get editingResource() {
|
||||
const resource = this.dependencies.store.getData(this.dependencies.tabId);
|
||||
|
||||
assert(resource, "Resource is not present in the store");
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
@computed private get selfLink() {
|
||||
return this.editingResource.resource;
|
||||
}
|
||||
|
||||
load = async () => {
|
||||
await this.dependencies.waitForEditingResource();
|
||||
|
||||
const result = await this.dependencies.callForResource(this.selfLink);
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
this.dependencies.showErrorNotification(
|
||||
`Loading resource failed: ${result.error}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const resource = result.response;
|
||||
|
||||
runInAction(() => {
|
||||
this._resource = resource;
|
||||
});
|
||||
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.editingResource.firstDraft = yaml.dump(resource.toPlainObject());
|
||||
});
|
||||
};
|
||||
|
||||
get namespace() {
|
||||
return this.resource.metadata.namespace || "default";
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.resource.metadata.name;
|
||||
}
|
||||
|
||||
get kind() {
|
||||
return this.resource.kind;
|
||||
}
|
||||
|
||||
save = async () => {
|
||||
const currentValue = this.configuration.value.get();
|
||||
const currentVersion = yaml.load(currentValue);
|
||||
const firstVersion = yaml.load(this.editingResource.firstDraft ?? currentValue);
|
||||
const patches = createPatch(firstVersion, currentVersion);
|
||||
|
||||
const result = await this.dependencies.callForPatchResource(this.resource, patches);
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
this.dependencies.showErrorNotification(
|
||||
<p>
|
||||
Failed to save resource:
|
||||
{" "}
|
||||
{result.error}
|
||||
</p>,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { kind, name } = result.response;
|
||||
|
||||
this.dependencies.showSuccessNotification(
|
||||
<p>
|
||||
{kind}
|
||||
{" "}
|
||||
<b>{name}</b>
|
||||
{" updated."}
|
||||
</p>,
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.editingResource.firstDraft = currentValue;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -6,52 +6,52 @@ import { getInjectable } from "@ogre-tools/injectable";
|
||||
import editResourceTabStoreInjectable from "./store.injectable";
|
||||
import dockStoreInjectable from "../dock/store.injectable";
|
||||
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
|
||||
import type { DockStore, DockTabCreateSpecific, TabId } from "../dock/store";
|
||||
import type { DockTabCreateSpecific, TabId } from "../dock/store";
|
||||
import { TabKind } from "../dock/store";
|
||||
import type { EditResourceTabStore } from "./store";
|
||||
import { runInAction } from "mobx";
|
||||
|
||||
interface Dependencies {
|
||||
dockStore: DockStore;
|
||||
editResourceStore: EditResourceTabStore;
|
||||
}
|
||||
|
||||
const createEditResourceTab = ({ dockStore, editResourceStore }: Dependencies) => (object: KubeObject, tabParams: DockTabCreateSpecific = {}): TabId => {
|
||||
// use existing tab if already opened
|
||||
const tabId = editResourceStore.getTabIdByResource(object);
|
||||
|
||||
if (tabId) {
|
||||
dockStore.open();
|
||||
dockStore.selectTab(tabId);
|
||||
|
||||
return tabId;
|
||||
}
|
||||
|
||||
return runInAction(() => {
|
||||
const tab = dockStore.createTab(
|
||||
{
|
||||
title: `${object.kind}: ${object.getName()}`,
|
||||
...tabParams,
|
||||
kind: TabKind.EDIT_RESOURCE,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
editResourceStore.setData(tab.id, {
|
||||
resource: object.selfLink,
|
||||
});
|
||||
|
||||
return tab.id;
|
||||
});
|
||||
};
|
||||
import getRandomIdForEditResourceTabInjectable from "./get-random-id-for-edit-resource-tab.injectable";
|
||||
|
||||
const createEditResourceTabInjectable = getInjectable({
|
||||
id: "create-edit-resource-tab",
|
||||
|
||||
instantiate: (di) => createEditResourceTab({
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
editResourceStore: di.inject(editResourceTabStoreInjectable),
|
||||
}),
|
||||
instantiate: (di) => {
|
||||
const dockStore = di.inject(dockStoreInjectable);
|
||||
const editResourceStore = di.inject(editResourceTabStoreInjectable);
|
||||
const getRandomId = di.inject(getRandomIdForEditResourceTabInjectable);
|
||||
|
||||
return (
|
||||
object: KubeObject,
|
||||
tabParams: DockTabCreateSpecific = {},
|
||||
): TabId => {
|
||||
// use existing tab if already opened
|
||||
const tabId = editResourceStore.getTabIdByResource(object);
|
||||
|
||||
if (tabId) {
|
||||
dockStore.open();
|
||||
dockStore.selectTab(tabId);
|
||||
|
||||
return tabId;
|
||||
}
|
||||
|
||||
return runInAction(() => {
|
||||
const tab = dockStore.createTab(
|
||||
{
|
||||
id: getRandomId(),
|
||||
title: `${object.kind}: ${object.getName()}`,
|
||||
...tabParams,
|
||||
kind: TabKind.EDIT_RESOURCE,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
editResourceStore.setData(tab.id, {
|
||||
resource: object.selfLink,
|
||||
});
|
||||
|
||||
return tab.id;
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createEditResourceTabInjectable;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 getRandomIdInjectable from "../../../../common/utils/get-random-id.injectable";
|
||||
|
||||
const getRandomIdForEditResourceTabInjectable = getInjectable({
|
||||
id: "get-random-id-for-edit-resource-tab",
|
||||
instantiate: (di) => di.inject(getRandomIdInjectable),
|
||||
});
|
||||
|
||||
export default getRandomIdForEditResourceTabInjectable;
|
||||
@@ -5,14 +5,12 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { EditResourceTabStore } from "./store";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
|
||||
|
||||
const editResourceTabStoreInjectable = getInjectable({
|
||||
id: "edit-resource-tab-store",
|
||||
|
||||
instantiate: (di) => new EditResourceTabStore({
|
||||
createStorage: di.inject(createStorageInjectable),
|
||||
apiManager: di.inject(apiManagerInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
|
||||
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
|
||||
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
|
||||
import type { TabId } from "../dock/store";
|
||||
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
|
||||
import type { ApiManager } from "../../../../common/k8s-api/api-manager";
|
||||
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
|
||||
|
||||
export interface EditingResource {
|
||||
resource: string; // resource path, e.g. /api/v1/namespaces/default
|
||||
@@ -16,50 +13,14 @@ export interface EditingResource {
|
||||
firstDraft?: string;
|
||||
}
|
||||
|
||||
export interface EditResourceTabStoreDependencies extends DockTabStoreDependencies {
|
||||
readonly apiManager: ApiManager;
|
||||
}
|
||||
|
||||
export class EditResourceTabStore extends DockTabStore<EditingResource> {
|
||||
constructor(protected readonly dependencies: EditResourceTabStoreDependencies) {
|
||||
constructor(protected readonly dependencies: DockTabStoreDependencies) {
|
||||
super(dependencies, {
|
||||
storageKey: "edit_resource_store",
|
||||
});
|
||||
}
|
||||
|
||||
protected finalizeDataForSave({ draft, ...data }: EditingResource): EditingResource {
|
||||
return data; // skip saving draft to local-storage
|
||||
}
|
||||
|
||||
isReady(tabId: TabId) {
|
||||
return super.isReady(tabId) && Boolean(this.getResource(tabId)); // ready to edit resource
|
||||
}
|
||||
|
||||
getStore(tabId: TabId): KubeObjectStore | undefined {
|
||||
const apiPath = this.getResourcePath(tabId);
|
||||
|
||||
return apiPath
|
||||
? this.dependencies.apiManager.getStore(apiPath)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
getResource(tabId: TabId): KubeObject | undefined {
|
||||
const apiPath = this.getResourcePath(tabId);
|
||||
|
||||
return apiPath
|
||||
? this.dependencies.apiManager.getStore(apiPath)?.getByPath(apiPath)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
getResourcePath(tabId: TabId): string | undefined {
|
||||
return this.getData(tabId)?.resource;
|
||||
}
|
||||
|
||||
getTabIdByResource(object: KubeObject): string | undefined {
|
||||
return this.findTabIdFromData(({ resource }) => object.selfLink === resource);
|
||||
}
|
||||
|
||||
clearInitialDraft(tabId: TabId): void {
|
||||
delete this.getData(tabId)?.firstDraft;
|
||||
}
|
||||
}
|
||||
@@ -4,169 +4,82 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { autorun, makeObservable, observable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import yaml from "js-yaml";
|
||||
import type { DockTab, TabId } from "../dock/store";
|
||||
import type { EditingResource, EditResourceTabStore } from "./store";
|
||||
import { observer } from "mobx-react";
|
||||
import type { DockTab } from "../dock/store";
|
||||
import { Spinner } from "../../spinner";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { EditResourceModel } from "./edit-resource-model/edit-resource-model.injectable";
|
||||
import editResourceModelInjectable from "./edit-resource-model/edit-resource-model.injectable";
|
||||
import { EditorPanel } from "../editor-panel";
|
||||
import { InfoPanel } from "../info-panel";
|
||||
import { Badge } from "../../badge";
|
||||
import { EditorPanel } from "../editor-panel";
|
||||
import { Spinner } from "../../spinner";
|
||||
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
|
||||
import { createPatch } from "rfc6902";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import editResourceTabStoreInjectable from "./store.injectable";
|
||||
import { noop, onceDefined } from "../../../utils";
|
||||
import closeDockTabInjectable from "../dock/close-dock-tab.injectable";
|
||||
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
|
||||
import { Notice } from "../../+extensions/notice";
|
||||
|
||||
export interface EditResourceProps {
|
||||
tab: DockTab;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
editResourceStore: EditResourceTabStore;
|
||||
closeTab: (tabId: TabId) => void;
|
||||
model: EditResourceModel;
|
||||
}
|
||||
|
||||
interface SaveDraftArgs {
|
||||
tabData: EditingResource;
|
||||
resource: KubeObject;
|
||||
store: KubeObjectStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedEditResource extends React.Component<EditResourceProps & Dependencies> {
|
||||
@observable error = "";
|
||||
@observable draft = "";
|
||||
|
||||
constructor(props: EditResourceProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
disposeOnUnmount(this, [
|
||||
onceDefined(
|
||||
() => {
|
||||
const tabData = this.tabData;
|
||||
const resource = this.resource;
|
||||
|
||||
if (tabData && resource) {
|
||||
return { tabData, resource };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
({ tabData, resource }) => {
|
||||
if (typeof tabData.draft === "string") {
|
||||
this.draft = tabData.draft;
|
||||
} else {
|
||||
this.draft = tabData.firstDraft = yaml.dump(resource.toPlainObject());
|
||||
}
|
||||
},
|
||||
),
|
||||
autorun(() => {
|
||||
const store = this.store;
|
||||
const tabData = this.tabData;
|
||||
const resource = this.resource;
|
||||
|
||||
if (!resource && store && tabData) {
|
||||
if (store.isLoaded) {
|
||||
// auto-close tab when resource removed from store
|
||||
this.props.closeTab(this.props.tab.id);
|
||||
} else if (!store.isLoading) {
|
||||
// preload resource for editing
|
||||
store.loadFromPath(tabData.resource).catch(noop);
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
}
|
||||
|
||||
get store() {
|
||||
return this.props.editResourceStore.getStore(this.props.tab.id);
|
||||
}
|
||||
|
||||
get resource() {
|
||||
return this.props.editResourceStore.getResource(this.tabId);
|
||||
}
|
||||
|
||||
get tabData() {
|
||||
return this.props.editResourceStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
async save({ resource, store, tabData }: SaveDraftArgs) {
|
||||
if (this.error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentVersion = yaml.load(this.draft);
|
||||
const firstVersion = yaml.load(tabData.firstDraft ?? this.draft);
|
||||
const patches = createPatch(firstVersion, currentVersion);
|
||||
const updatedResource = await store.patch(resource, patches);
|
||||
|
||||
this.props.editResourceStore.clearInitialDraft(this.tabId);
|
||||
|
||||
return (
|
||||
<p>
|
||||
{updatedResource.kind}
|
||||
{" "}
|
||||
<b>{updatedResource.getName()}</b>
|
||||
{" updated."}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tabId, error, draft, tabData, resource, store } = this;
|
||||
|
||||
if (!tabData || !resource || !store) {
|
||||
return <Spinner center />;
|
||||
}
|
||||
|
||||
const NonInjectedEditResource = observer(
|
||||
({ model, tab: { id: tabId }}: EditResourceProps & Dependencies) => {
|
||||
return (
|
||||
<div className="EditResource flex column">
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
error={error}
|
||||
submit={() => this.save({ resource, store, tabData })}
|
||||
submitLabel="Save"
|
||||
submittingMessage="Applying.."
|
||||
controls={(
|
||||
<div className="resource-info flex gaps align-center">
|
||||
<span>Kind:</span>
|
||||
<Badge label={resource.kind} />
|
||||
<span>Name:</span>
|
||||
<Badge label={resource.getName()} />
|
||||
<span>Namespace:</span>
|
||||
<Badge label={resource.getNs() || "global"} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={draft}
|
||||
onChange={draft => {
|
||||
this.error = "";
|
||||
this.draft = tabData.draft = draft;
|
||||
}}
|
||||
onError={error => this.error = String(error)}
|
||||
/>
|
||||
{model.shouldShowErrorAboutNoResource && (
|
||||
<Notice>
|
||||
Resource not found
|
||||
</Notice>
|
||||
)}
|
||||
|
||||
{!model.shouldShowErrorAboutNoResource && (
|
||||
<>
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
error={model.configuration.error.value.get()}
|
||||
submit={model.save}
|
||||
showNotifications={false}
|
||||
submitLabel="Save"
|
||||
submittingMessage="Applying..."
|
||||
submitTestId={`save-edit-resource-from-tab-for-${tabId}`}
|
||||
submitAndCloseTestId={`save-and-close-edit-resource-from-tab-for-${tabId}`}
|
||||
cancelTestId={`cancel-edit-resource-from-tab-for-${tabId}`}
|
||||
submittingTestId={`saving-edit-resource-from-tab-for-${tabId}`}
|
||||
controls={(
|
||||
<div className="resource-info flex gaps align-center">
|
||||
<span>Kind:</span>
|
||||
<Badge label={model.kind} />
|
||||
<span>Name:</span>
|
||||
<Badge label={model.name} />
|
||||
<span>Namespace:</span>
|
||||
<Badge label={model.namespace} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={model.configuration.value.get()}
|
||||
onChange={model.configuration.onChange}
|
||||
onError={model.configuration.error.onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const EditResource = withInjectables<Dependencies, EditResourceProps>(NonInjectedEditResource, {
|
||||
getProps: (di, props) => ({
|
||||
editResourceStore: di.inject(editResourceTabStoreInjectable),
|
||||
closeTab: di.inject(closeDockTabInjectable),
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
export const EditResource = withInjectables<Dependencies, EditResourceProps>(
|
||||
NonInjectedEditResource,
|
||||
{
|
||||
getPlaceholder: () => (
|
||||
<Spinner center data-testid="edit-resource-tab-spinner" />
|
||||
),
|
||||
|
||||
getProps: async (di, props) => ({
|
||||
model: await di.inject(editResourceModelInjectable, props.tab.id),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -39,6 +39,7 @@ export interface OptionalProps {
|
||||
showNotifications?: boolean;
|
||||
showStatusPanel?: boolean;
|
||||
submitTestId?: string;
|
||||
submitAndCloseTestId?: string;
|
||||
cancelTestId?: string;
|
||||
submittingTestId?: string;
|
||||
}
|
||||
@@ -167,6 +168,7 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
|
||||
label={`${submitLabel} & Close`}
|
||||
onClick={submitAndClose}
|
||||
disabled={isDisabled}
|
||||
data-testid={this.props.submitAndCloseTestId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -12,7 +12,7 @@ class FakeMonacoEditor extends React.Component<MonacoEditorProps> {
|
||||
const { id, value, onChange, onError, language = "yaml" } = this.props;
|
||||
|
||||
return (
|
||||
<input
|
||||
<textarea
|
||||
data-testid={`monaco-editor-for-${id}`}
|
||||
|
||||
onChange={(event) => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
|
||||
import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable";
|
||||
import type { IObservableArray, ObservableSet } from "mobx";
|
||||
import type { ObservableSet } from "mobx";
|
||||
import { computed, observable, runInAction } from "mobx";
|
||||
import React from "react";
|
||||
import { Router } from "react-router";
|
||||
@@ -36,7 +36,6 @@ import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-f
|
||||
import startMainApplicationInjectable from "../../../main/start-main-application/start-main-application.injectable";
|
||||
import startFrameInjectable from "../../start-frame/start-frame.injectable";
|
||||
import type { NamespaceStore } from "../+namespaces/store";
|
||||
import namespaceStoreInjectable from "../+namespaces/store.injectable";
|
||||
import historyInjectable from "../../navigation/history.injectable";
|
||||
import type { MinimalTrayMenuItem } from "../../../main/tray/electron-tray/electron-tray.injectable";
|
||||
import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable";
|
||||
@@ -61,6 +60,8 @@ import { ClusterFrame } from "../../frames/cluster-frame/cluster-frame";
|
||||
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
|
||||
import activeKubernetesClusterInjectable from "../../cluster-frame-context/active-kubernetes-cluster.injectable";
|
||||
import { catalogEntityFromCluster } from "../../../main/cluster-manager";
|
||||
import namespaceStoreInjectable from "../+namespaces/store.injectable";
|
||||
import { isAllowedResource } from "../../../common/cluster/is-allowed-resource";
|
||||
|
||||
type Callback = (dis: DiContainers) => void | Promise<void>;
|
||||
|
||||
@@ -208,7 +209,7 @@ export const getApplicationBuilder = () => {
|
||||
},
|
||||
}));
|
||||
|
||||
let allowedResourcesState: IObservableArray<KubeResource>;
|
||||
let allowedResourcesState: KubeResource[];
|
||||
let rendered: RenderResult;
|
||||
|
||||
const enableExtensionsFor = <T extends ObservableSet>(
|
||||
@@ -398,16 +399,27 @@ export const getApplicationBuilder = () => {
|
||||
|
||||
const clusterStub = {
|
||||
accessibleNamespaces: [],
|
||||
isAllowedResource: isAllowedResource(allowedResourcesState),
|
||||
} as unknown as Cluster;
|
||||
|
||||
rendererDi.override(activeKubernetesClusterInjectable, () =>
|
||||
computed(() => catalogEntityFromCluster(clusterStub)),
|
||||
);
|
||||
|
||||
// TODO: Figure out a way to remove this stub.
|
||||
const namespaceStoreStub = {
|
||||
isLoaded: true,
|
||||
contextNamespaces: [],
|
||||
contextItems: [],
|
||||
api: {
|
||||
kind: "Namespace",
|
||||
},
|
||||
items: [],
|
||||
selectNamespaces: () => {},
|
||||
getByPath: () => undefined,
|
||||
pickOnlySelected: () => [],
|
||||
isSelectedAll: () => false,
|
||||
getTotalCount: () => 0,
|
||||
} as unknown as NamespaceStore;
|
||||
|
||||
const clusterFrameContextFake = new ClusterFrameContext(
|
||||
|
||||
@@ -22,5 +22,7 @@ export const controlWhenStoragesAreReady = (di: DiContainer) => {
|
||||
// TODO: Remove when typing is added to the library
|
||||
(di as any).decorateFunction(createStorageInjectable, decorated);
|
||||
|
||||
return async () => void await Promise.all(storagesAreReady);
|
||||
return async () => {
|
||||
await Promise.all(storagesAreReady);
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user