Skip to content

Commit

Permalink
frontend: Add non-editable fields to KubeObjects and exclude them in …
Browse files Browse the repository at this point in the history
…YAML editor

This PR adds a new var to KubeObject classes to declare non-editable fields. It also sets a new getEditableObject method that uses these fields and removes them from an object clone and ensure YAML editor uses getEditableObject to exclude non-editable fields.

Fixes: #2032
Signed-off-by: guilhane <[email protected]>
  • Loading branch information
Guilamb committed Jul 25, 2024
1 parent 41d7722 commit c1afcbe
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 22 deletions.
4 changes: 2 additions & 2 deletions frontend/src/components/common/Resource/EditButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default function EditButton(props: EditButtonProps) {
cancelUrl,
errorUrl: cancelUrl,
...options,
})
}),
);

dispatchHeadlampEditEvent({
Expand Down Expand Up @@ -108,7 +108,7 @@ export default function EditButton(props: EditButtonProps) {
/>
{openDialog && (
<EditorDialog
item={item.jsonData}
item={item.getEditableObject()}
open={openDialog}
onClose={() => setOpenDialog(false)}
onSave={handleSave}
Expand Down
59 changes: 43 additions & 16 deletions frontend/src/lib/k8s/cluster.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OpPatch } from 'json-patch';
import { JSONPath } from 'jsonpath-plus';
import { cloneDeep, unset } from 'lodash';
import React from 'react';
import helpers from '../../helpers';
Expand Down Expand Up @@ -293,25 +294,25 @@ export interface KubeObjectIface<T extends KubeObjectInterface | KubeEvent> {
apiList: (
onList: (arg: InstanceType<KubeObjectIface<T>>[]) => void,
onError?: (err: ApiError) => void,
opts?: ApiListSingleNamespaceOptions
opts?: ApiListSingleNamespaceOptions,
) => any;
useApiList: (
onList: (arg: InstanceType<KubeObjectIface<T>>[]) => void,
onError?: (err: ApiError) => void,
opts?: ApiListOptions
opts?: ApiListOptions,
) => any;
useApiGet: (
onGet: (...args: any) => void,
name: string,
namespace?: string,
onError?: (err: ApiError) => void
onError?: (err: ApiError) => void,
) => void;
useList: (
opts?: ApiListOptions
opts?: ApiListOptions,
) => [any[], ApiError | null, (items: any[]) => void, (err: ApiError | null) => void];
useGet: (
name: string,
namespace?: string
namespace?: string,
) => [any, ApiError | null, (item: any) => void, (err: ApiError | null) => void];
getErrorMessage: (err?: ApiError | null) => string | null;
new (json: T): any;
Expand All @@ -329,6 +330,11 @@ export interface AuthRequestResourceAttrs {
group?: string;
verb?: string;
}
type JsonPath<T> = T extends object
? {
[K in keyof T]: K extends string ? `${K}` | `${K}.${JsonPath<T[K]>}` : never;
}[keyof T]
: never;

// @todo: uses of makeKubeObject somehow end up in an 'any' type.

Expand All @@ -338,11 +344,12 @@ export interface AuthRequestResourceAttrs {
* @param objectName The name of the object to create a KubeObject implementation for.
*/
export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
objectName: string
objectName: string,
): KubeObjectIface<T> {
class KubeObject {
static apiEndpoint: ReturnType<typeof apiFactoryWithNamespace | typeof apiFactory>;
jsonData: T | null = null;
public static readOnlyFields: JsonPath<T>[];
private readonly _clusterName: string;

constructor(json: T) {
Expand Down Expand Up @@ -430,6 +437,26 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
return this.apiEndpoint.isNamespaced;
}

getEditableObject() {
const fieldsToRemove = this._class().readOnlyFields;
const code = this.jsonData ? cloneDeep(this.jsonData) : {};

fieldsToRemove?.forEach((path: JsonPath<T>) => {
JSONPath({
path,
json: code,
callback: (result, type, fullPayload) => {
if (fullPayload.parent && fullPayload.parentProperty) {
delete fullPayload.parent[fullPayload.parentProperty];
}
},
resultType: 'all',
});
});

return code;
}

// @todo: apiList has 'any' return type.
/**
* Returns the API endpoint for this object.
Expand All @@ -443,7 +470,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
static apiList<U extends KubeObject>(
onList: (arg: U[]) => void,
onError?: (err: ApiError) => void,
opts?: ApiListSingleNamespaceOptions
opts?: ApiListSingleNamespaceOptions,
) {
const createInstance = (item: T) => this.create(item) as U;

Expand Down Expand Up @@ -475,7 +502,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
static useApiList<U extends KubeObject>(
onList: (...arg: any[]) => any,
onError?: (err: ApiError) => void,
opts?: ApiListOptions
opts?: ApiListOptions,
) {
const [objs, setObjs] = React.useState<{ [key: string]: U[] }>({});
const listCallback = onList as (arg: U[]) => void;
Expand Down Expand Up @@ -528,7 +555,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
namespace,
queryParams,
cluster,
})
}),
);
}
} else {
Expand All @@ -541,7 +568,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
}

static useList<U extends KubeObject>(
opts?: ApiListOptions
opts?: ApiListOptions,
): [U[] | null, ApiError | null, (items: U[]) => void, (err: ApiError | null) => void] {
const [objList, setObjList] = React.useState<U[] | null>(null);
const [error, setError] = useErrorState(setObjList);
Expand Down Expand Up @@ -580,7 +607,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
opts?: {
queryParams?: QueryParameters;
cluster?: string;
}
},
) {
const createInstance = (item: T) => this.create(item) as U;
const args: any[] = [name, (obj: T) => onGet(createInstance(obj))];
Expand All @@ -604,7 +631,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
opts?: {
queryParams?: QueryParameters;
cluster?: string;
}
},
) {
// We do the type conversion here because we want to be able to use hooks that may not have
// the exact signature as get callbacks.
Expand All @@ -618,7 +645,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
opts?: {
queryParams?: QueryParameters;
cluster?: string;
}
},
): [U | null, ApiError | null, (items: U) => void, (err: ApiError | null) => void] {
const [obj, setObj] = React.useState<U | null>(null);
const [error, setError] = useErrorState(setObj);
Expand Down Expand Up @@ -689,7 +716,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
patch: (
body: { spec: { replicas: number } },
metadata: KubeMetadata,
clusterName?: string
clusterName?: string,
) => Promise<any>;
};
};
Expand All @@ -699,7 +726,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
spec,
},
this.metadata,
this._clusterName
this._clusterName,
);
}

Expand Down Expand Up @@ -735,7 +762,7 @@ export function makeKubeObject<T extends KubeObjectInterface | KubeEvent>(
resourceAttributes: reqResourseAttrs,
},
},
false
false,
);
} catch (err) {
// If this is the last attempt or the error is not 404, let it throw.
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/lib/k8s/crd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ export interface KubeCRD extends KubeObjectInterface {
class CustomResourceDefinition extends makeKubeObject<KubeCRD>('crd') {
static apiEndpoint = apiFactory(
['apiextensions.k8s.io', 'v1', 'customresourcedefinitions'],
['apiextensions.k8s.io', 'v1beta1', 'customresourcedefinitions']
['apiextensions.k8s.io', 'v1beta1', 'customresourcedefinitions'],
);
static readOnlyFields = ['metadata.managedFields'];

static get className(): string {
return 'CustomResourceDefinition';
Expand Down Expand Up @@ -99,7 +100,7 @@ class CustomResourceDefinition extends makeKubeObject<KubeCRD>('crd') {

makeCRClass(): KubeObjectClass {
const apiInfo: CRClassArgs['apiInfo'] = (this.jsonData as KubeCRD).spec.versions.map(
versionInfo => ({ group: this.spec.group, version: versionInfo.name })
versionInfo => ({ group: this.spec.group, version: versionInfo.name }),
);

return makeCustomResourceClass({
Expand Down Expand Up @@ -128,12 +129,12 @@ export interface CRClassArgs {
/** @deprecated Use the version of the function that receives an object as its argument. */
export function makeCustomResourceClass(
args: [group: string, version: string, pluralName: string][],
isNamespaced: boolean
isNamespaced: boolean,
): ReturnType<typeof makeKubeObject>;
export function makeCustomResourceClass(args: CRClassArgs): ReturnType<typeof makeKubeObject>;
export function makeCustomResourceClass(
args: [group: string, version: string, pluralName: string][] | CRClassArgs,
isNamespaced?: boolean
isNamespaced?: boolean,
): ReturnType<typeof makeKubeObject> {
let apiInfoArgs: [group: string, version: string, pluralName: string][] = [];

Expand Down

0 comments on commit c1afcbe

Please sign in to comment.