Persist apiVersion when editing resources in monaco (#4406)

* Persist apiVersion when editing resources in monaco

- Use a new custom k8slens prefixed label

- Means that users aren't surprised when they use lens to update a
  resource to a new apiVersionWithGroup

- Doesn't touch the versions in the stores

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix: Fix lint issues

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* chore: make lint not bail on failure

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* chore: Run lint:fix on all files

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2023-04-05 10:56:23 -04:00
1 parent 058494bc73
commit 807f98ed1b
57 files changed
+295 -400

No files matched your search

@@ -16,6 +16,7 @@
.TableCell {
word-break: break-word;
&:first-child {
margin-left: $margin * 2;
}
@@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import { HpaDetails } from "./hpa-details";
import { HorizontalPodAutoscalerDetails } from "./details";
jest.mock("react-router-dom", () => ({
Link: ({ children }: { children: React.ReactNode }) => children,
@@ -62,7 +62,7 @@ describe("<HpaDetails/>", () => {
const hpa = new HorizontalPodAutoscaler(hpaV2);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.baseElement).toMatchSnapshot();
@@ -72,7 +72,7 @@ describe("<HpaDetails/>", () => {
const hpa = new HorizontalPodAutoscaler(hpaV2);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.queryByTestId("hpa-metrics")).toBeNull();
@@ -101,7 +101,7 @@ describe("<HpaDetails/>", () => {
});
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.getByText("CPU Utilization percentage")).toBeInTheDocument();
@@ -131,7 +131,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.getByText("Resource cpu on Pods")).toBeInTheDocument();
@@ -160,7 +160,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.getByText("Resource cpu on Pods")).toBeInTheDocument();
@@ -191,7 +191,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.getByText("packets-per-second on Pods")).toBeInTheDocument();
@@ -216,7 +216,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.getByText("packets-per-second on Pods")).toBeInTheDocument();
@@ -252,7 +252,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.getByText(/requests-per-second/)).toHaveTextContent("requests-per-second onService/nginx");
@@ -277,7 +277,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.getByText("requests-per-second")).toBeInTheDocument();
@@ -311,7 +311,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.getByText("queue_messages_ready on {\"matchLabels\":{\"queue\":\"worker_tasks\"}}")).toBeInTheDocument();
@@ -339,7 +339,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.getByText("queue_messages_ready on {\"matchLabels\":{\"queue\":\"worker_tasks\"}}")).toBeInTheDocument();
@@ -368,7 +368,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.baseElement).toMatchSnapshot();
@@ -398,7 +398,7 @@ describe("<HpaDetails/>", () => {
);
result = render(
<HpaDetails object={hpa} />,
<HorizontalPodAutoscalerDetails object={hpa} />,
);
expect(result.baseElement).toMatchSnapshot();
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./hpa-details.scss";
import "./details.scss";
import React from "react";
import { observer } from "mobx-react";
@@ -22,8 +22,8 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import getHorizontalPodAutoscalerMetrics from "./get-hpa-metrics.injectable";
import { getMetricName } from "./get-hpa-metric-name";
import getHorizontalPodAutoscalerMetrics from "./get-metrics.injectable";
import { getMetricName } from "./get-metric-name";
export interface HpaDetailsProps extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
}
@@ -36,7 +36,7 @@ interface Dependencies {
}
@observer
class NonInjectedHpaDetails extends React.Component<HpaDetailsProps & Dependencies> {
class NonInjectedHorizontalPodAutoscalerDetails extends React.Component<HpaDetailsProps & Dependencies> {
private renderTargetLink(target: HorizontalPodAutoscalerMetricTarget | undefined) {
if (!target) {
return null;
@@ -177,7 +177,7 @@ class NonInjectedHpaDetails extends React.Component<HpaDetailsProps & Dependenci
}
}
export const HpaDetails = withInjectables<Dependencies, HpaDetailsProps>(NonInjectedHpaDetails, {
export const HorizontalPodAutoscalerDetails = withInjectables<Dependencies, HpaDetailsProps>(NonInjectedHorizontalPodAutoscalerDetails, {
getProps: (di, props) => ({
...props,
apiManager: di.inject(apiManagerInjectable),
@@ -5,9 +5,9 @@
import { getInjectable } from "@ogre-tools/injectable";
import type { HorizontalPodAutoscaler, HorizontalPodAutoscalerMetricSpec, HorizontalPodAutoscalerMetricStatus } from "../../../common/k8s-api/endpoints";
import { HpaMetricType } from "../../../common/k8s-api/endpoints";
import { getMetricName } from "./get-hpa-metric-name";
import { HorizontalPodAutoscalerV1MetricParser } from "./hpa-v1-metric-parser";
import { HorizontalPodAutoscalerV2MetricParser } from "./hpa-v2-metric-parser";
import { getMetricName } from "./get-metric-name";
import { HorizontalPodAutoscalerV1MetricParser } from "./metric-parser-v1";
import { HorizontalPodAutoscalerV2MetricParser } from "./metric-parser-v2";
type Parser = HorizontalPodAutoscalerV1MetricParser | HorizontalPodAutoscalerV2MetricParser;
@@ -3,5 +3,5 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./hpa";
export * from "./hpa-details";
export * from "./list-view";
export * from "./details";
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./hpa.scss";
import "./list-view.scss";
import React from "react";
import { observer } from "mobx-react";
@@ -17,7 +17,7 @@ import { KubeObjectAge } from "../kube-object/age";
import type { HorizontalPodAutoscalerStore } from "./store";
import { withInjectables } from "@ogre-tools/injectable-react";
import horizontalPodAutoscalerStoreInjectable from "./store.injectable";
import getHorizontalPodAutoscalerMetrics from "./get-hpa-metrics.injectable";
import getHorizontalPodAutoscalerMetrics from "./get-metrics.injectable";
import { NamespaceSelectBadge } from "../+namespaces/namespace-select-badge";
enum columnId {
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import getHorizontalPodAutoscalerMetrics from "./get-hpa-metrics.injectable";
import getHorizontalPodAutoscalerMetrics from "./get-metrics.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints";
@@ -658,10 +658,10 @@ describe("getHorizontalPodAutoscalerMetrics", () => {
],
},
});
expect(getMetrics(hpa)[0]).toEqual("10% / 50%");
});
it("should return correct resource metrics with current value", () => {
const hpa = new HorizontalPodAutoscaler({
...hpaV2Beta1,
@@ -691,7 +691,7 @@ describe("getHorizontalPodAutoscalerMetrics", () => {
],
},
});
expect(getMetrics(hpa)[0]).toEqual("500m / 100m");
});
@@ -787,7 +787,7 @@ describe("getHorizontalPodAutoscalerMetrics", () => {
type: HpaMetricType.Pods,
pods: {
metricName: "packets-per-second",
targetAverageValue: "1k",
},
},
@@ -1038,7 +1038,7 @@ describe("getHorizontalPodAutoscalerMetrics", () => {
],
},
});
expect(getMetrics(hpa)[0]).toEqual("unknown / 50%");
});
});
@@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import horizontalPodAutoscalersRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/horizontal-pod-autoscalers-route.injectable";
import { HorizontalPodAutoscalers } from "./hpa";
import { HorizontalPodAutoscalers } from "./list-view";
const horizontalPodAutoscalersRouteComponentInjectable = getInjectable({
id: "horizontal-pod-autoscalers-route-component",
@@ -3,44 +3,35 @@
* 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 { 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 apiManagerInjectable from "../../../../../../common/k8s-api/api-manager/manager.injectable";
import { waitUntilDefined } from "@k8slens/utilities";
import apiKubeInjectable from "../../../../../k8s/api-kube.injectable";
export type CallForResource = (
selfLink: string
) => AsyncResult<KubeObject | undefined>;
export type CallForResource = (selfLink: string) => AsyncResult<KubeObject | undefined>;
const callForResourceInjectable = getInjectable({
id: "call-for-resource",
instantiate: (di): CallForResource => {
const apiManager = di.inject(apiManagerInjectable);
const apiKube = di.inject(apiKubeInjectable);
return async (apiPath: string) => {
const api = await waitUntilDefined(() => apiManager.getApi(apiPath));
const parsed = parseKubeApi(apiPath);
if (!api || !parsed.name) {
if (!parsed.name) {
return { callWasSuccessful: false, error: "Invalid API path" };
}
let resource: KubeObject | null;
try {
resource = await api.get({
name: parsed.name,
namespace: parsed.namespace,
});
return {
callWasSuccessful: true,
response: new KubeObject(await apiKube.get(apiPath)),
};
} catch (e) {
return { callWasSuccessful: false, error: getErrorMessage(e) };
}
return { callWasSuccessful: true, response: resource || undefined };
};
},
@@ -7,9 +7,9 @@ import type { CallForResource } from "./call-for-resource/call-for-resource.inje
import callForResourceInjectable from "./call-for-resource/call-for-resource.injectable";
import { waitUntilDefined } from "@k8slens/utilities";
import editResourceTabStoreInjectable from "../store.injectable";
import type { EditResourceTabStore } from "../store";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import type { KubeObject } from "../../../../../common/k8s-api/kube-object";
import type { EditingResource, EditResourceTabStore } from "../store";
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";
@@ -19,18 +19,22 @@ 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";
import { createKubeApiURL, parseKubeApi } from "../../../../../common/k8s-api/kube-api-parse";
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: di.inject(editResourceTabStoreInjectable),
store,
tabId,
waitForEditingResource: () => waitUntilDefined(() => store.getData(tabId)),
});
await model.load();
@@ -48,19 +52,42 @@ export default editResourceModelInjectable;
interface Dependencies {
callForResource: CallForResource;
callForPatchResource: CallForPatchResource;
waitForEditingResource: () => Promise<EditingResource>;
showSuccessNotification: ShowNotification;
showErrorNotification: ShowNotification;
readonly store: EditResourceTabStore;
readonly tabId: string;
}
export class EditResourceModel {
constructor(private readonly dependencies: Dependencies) {
makeObservable(this);
function getEditSelfLinkFor(object: RawKubeObject): string {
const lensVersionLabel = object.metadata.labels?.[EditResourceLabelName];
if (lensVersionLabel) {
const { apiVersionWithGroup, ...parsedApi } = parseKubeApi(object.metadata.selfLink);
parsedApi.apiVersion = lensVersionLabel;
return createKubeApiURL({
...parsedApi,
apiVersion: `${parsedApi.apiGroup}/${parsedApi.apiVersion}`,
});
}
return object.metadata.selfLink;
}
/**
* The label name that Lens uses to receive the desired api version
*/
export const EditResourceLabelName = "k8slens-edit-resource-version";
export class EditResourceModel {
constructor(protected readonly dependencies: Dependencies) {}
readonly configuration = {
value: computed(() => this.editingResource.draft || this.editingResource.firstDraft || ""),
value: computed(
() => this.editingResource.draft || this.editingResource.firstDraft || "",
),
onChange: action((value: string) => {
this.editingResource.draft = value;
@@ -100,27 +127,39 @@ export class EditResourceModel {
return this.editingResource.resource;
}
load = async () => {
await waitUntilDefined(() => this.dependencies.store.getData(this.dependencies.tabId));
load = async (): Promise<void> => {
await this.dependencies.waitForEditingResource();
const result = await this.dependencies.callForResource(this.selfLink);
let result = await this.dependencies.callForResource(this.selfLink);
if (!result.callWasSuccessful) {
this.dependencies.showErrorNotification(
`Loading resource failed: ${result.error}`,
);
return void this.dependencies.showErrorNotification(`Loading resource failed: ${result.error}`);
}
if (result?.response?.metadata.labels?.[EditResourceLabelName]) {
const parsed = parseKubeApi(this.selfLink);
parsed.apiVersion = result.response.metadata.labels[EditResourceLabelName];
result = await this.dependencies.callForResource(createKubeApiURL(parsed));
}
if (!result.callWasSuccessful) {
return void this.dependencies.showErrorNotification(`Loading resource failed: ${result.error}`);
}
const resource = result.response;
runInAction(() => {
this._resource = resource;
});
if (!resource) {
return;
}
runInAction(() => {
this._resource = result.response;
if (this._resource) {
this.editingResource.firstDraft = yaml.dump(
this._resource.toPlainObject(),
);
}
this.editingResource.firstDraft = yaml.dump(resource.toPlainObject());
});
};
@@ -138,16 +177,16 @@ export class EditResourceModel {
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 currentVersion = yaml.load(currentValue) as RawKubeObject;
const firstVersion = yaml.load(this.editingResource.firstDraft ?? currentValue);
const result = await this.dependencies.callForPatchResource(
this.resource,
patches,
);
// Make sure we save this label so that we can use it in the future
currentVersion.metadata.labels ??= {};
currentVersion.metadata.labels[EditResourceLabelName] = currentVersion.apiVersion.split("/").pop();
const patches = createPatch(firstVersion, currentVersion);
const selfLink = getEditSelfLinkFor(currentVersion);
const result = await this.dependencies.callForPatchResource(this.resource, patches);
if (!result.callWasSuccessful) {
this.dependencies.showErrorNotification((
@@ -158,23 +197,26 @@ export class EditResourceModel {
</p>
));
return;
return null;
}
const { kind, name } = result.response;
this.dependencies.showSuccessNotification((
this.dependencies.showSuccessNotification(
<p>
{`${kind} `}
{kind}
{" "}
<b>{name}</b>
{" updated."}
</p>
));
</p>,
);
runInAction(() => {
this.editingResource.firstDraft = currentValue;
this.editingResource.firstDraft = yaml.dump(currentVersion);
this.editingResource.resource = selfLink;
});
return result.response.toString();
// NOTE: This is required for `saveAndClose` to work correctly
return [];
};
}
@@ -56,12 +56,14 @@ const NonInjectedEditResource = observer(({
<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} />
onError={model.configuration.error.onChange}
/>
</>
)
}
@@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { kubeObjectDetailItemInjectionToken } from "../kube-object-detail-item-injection-token";
import { HpaDetails } from "../../../+config-horizontal-pod-autoscalers";
import { HorizontalPodAutoscalerDetails } from "../../../+config-horizontal-pod-autoscalers";
import { computed } from "mobx";
import { kubeObjectMatchesToKindAndApiVersion } from "../kube-object-matches-to-kind-and-api-version";
import currentKubeObjectInDetailsInjectable from "../../current-kube-object-in-details.injectable";
@@ -16,7 +16,7 @@ const horizontalPodAutoscalerDetailItemInjectable = getInjectable({
const kubeObject = di.inject(currentKubeObjectInDetailsInjectable);
return {
Component: HpaDetails,
Component: HorizontalPodAutoscalerDetails,
enabled: computed(() => isHorizontalPodAutoscaler(kubeObject.value.get()?.object)),
orderNumber: 10,
};
@@ -246,12 +246,10 @@ class NonInjectedMonacoEditor extends React.Component<MonacoEditorProps & Depend
this.dispose.push(
reaction(() => this.model, this.onModelChange),
reaction(() => this.theme, theme => {
if (theme) {
editor.setTheme(theme);
}
reaction(() => this.theme, editor.setTheme),
reaction(() => this.props.value, value => this.setValue(value), {
fireImmediately: true,
}),
reaction(() => this.props.value, value => this.setValue(value)),
reaction(() => this.options, opts => this.editor.updateOptions(opts)),
() => onDidLayoutChangeDisposer.dispose(),