diff --git a/src/common/k8s-api/endpoints/custom-resource-definition.api.ts b/src/common/k8s-api/endpoints/custom-resource-definition.api.ts index 03d0903f71..90485a5ab8 100644 --- a/src/common/k8s-api/endpoints/custom-resource-definition.api.ts +++ b/src/common/k8s-api/endpoints/custom-resource-definition.api.ts @@ -10,12 +10,14 @@ import type { BaseKubeObjectCondition, KubeObjectScope } from "../kube-object"; import { KubeObject } from "../kube-object"; import type { DerivedKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; +import type { JSONSchemaProps } from "./types/json-schema-props"; interface AdditionalPrinterColumnsCommon { name: string; type: "integer" | "number" | "string" | "boolean" | "date"; - priority: number; - description: string; + priority?: number; + format?: "int32" | "int64" | "float" | "double" | "byte" | "binary" | "date" | "date-time" | "password"; + description?: string; } export type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & { @@ -26,11 +28,15 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & { JSONPath: string; }; +export interface CustomResourceValidation { + openAPIV3Schema?: JSONSchemaProps; +} + export interface CustomResourceDefinitionVersion { name: string; served: boolean; storage: boolean; - schema?: object; // required in v1 but not present in v1beta + schema?: CustomResourceValidation; // required in v1 but not present in v1beta additionalPrinterColumns?: AdditionalPrinterColumnsV1[]; } diff --git a/src/common/k8s-api/endpoints/types/external-documentation.ts b/src/common/k8s-api/endpoints/types/external-documentation.ts new file mode 100644 index 0000000000..b433785323 --- /dev/null +++ b/src/common/k8s-api/endpoints/types/external-documentation.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export interface ExternalDocumentation { + description?: string; + url?: string; +} diff --git a/src/common/k8s-api/endpoints/types/json-schema-props.ts b/src/common/k8s-api/endpoints/types/json-schema-props.ts new file mode 100644 index 0000000000..1c0f18a7d2 --- /dev/null +++ b/src/common/k8s-api/endpoints/types/json-schema-props.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { JsonValue } from "type-fest"; +import type { ExternalDocumentation } from "./external-documentation"; + +export interface JSONSchemaProps { + $ref?: string; + $schema?: string; + additionalItems?: JSONSchemaProps | boolean; + additionalProperties?: JSONSchemaProps | boolean; + allOf?: JSONSchemaProps[]; + anyOf?: JSONSchemaProps[]; + + /** + * default is a default value for undefined object fields. + * Defaulting is a beta feature under the CustomResourceDefaulting feature gate. + * Defaulting requires spec.preserveUnknownFields to be false. + */ + _default?: object; + + definitions?: Partial>; + dependencies?: Partial>; + description?: string; + _enum?: object[]; + example?: JsonValue; + + exclusiveMaximum?: boolean; + exclusiveMinimum?: boolean; + externalDocs?: ExternalDocumentation; + + /** + * format is an OpenAPI v3 format string. + * Unknown formats are ignored. + * + * The following formats are validated: + * - bsonobjectid: a bson object ID, i.e. a 24 characters hex string + * - uri: an URI as parsed by Golang net/url.ParseRequestURI + * - email: an email address as parsed by Golang net/mail.ParseAddress + * - hostname: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. + * - ipv4: an IPv4 IP as parsed by Golang net.ParseIP + * - ipv6: an IPv6 IP as parsed by Golang net.ParseIP + * - cidr: a CIDR as parsed by Golang net.ParseCIDR + * - mac: a MAC address as parsed by Golang net.ParseMAC + * - uuid: an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + * - uuid3: an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + * - uuid4: an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + * - uuid5: an UUID5 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + * - isbn: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041" + * - isbn10: an ISBN10 number string like "0321751043" + * - isbn13: an ISBN13 number string like "978-0321751041" + * - creditcard: a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in + * - ssn: a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ + * - hexcolor: an hexadecimal color code like "#FFFFFF: following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + * - rgbcolor: an RGB color code like rgb like "rgb(255,255,2559" + * - byte: base64 encoded binary data + * - password: any kind of string + * - date: a date string like "2006-01-02" as defined by full-date in RFC3339 + * - duration: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format + * - datetime: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339. + */ + format?: string; + + id?: string; + items?: JSONSchemaProps | JSONSchemaProps[]; + maxItems?: number; + maxLength?: number; + maxProperties?: number; + maximum?: number; + minItems?: number; + minLength?: number; + minProperties?: number; + minimum?: number; + multipleOf?: number; + not?: JSONSchemaProps; + nullable?: boolean; + oneOf?: JSONSchemaProps[]; + pattern?: string; + patternProperties?: Partial>; + properties?: Partial>; + required?: Array; + title?: string; + type?: string; + uniqueItems?: boolean; + x_kubernetes_embedded_resource?: boolean; + x_kubernetes_int_or_string?: boolean; + x_kubernetes_list_map_keys?: string[]; + x_kubernetes_list_type?: string; + x_kubernetes_map_type?: string; + x_kubernetes_preserve_unknown_fields?: boolean; +} diff --git a/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap b/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap new file mode 100644 index 0000000000..d3e0cd1d5a --- /dev/null +++ b/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap @@ -0,0 +1,225 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` with a CRD with a boolean field should display false in an additionalPrinterColumn as 'false' 1`] = ` +
+
+
+ + Created + + + <unknown> + ago + +
+
+ + Name + + + first-crd + +
+
+ + MyField + + +
    +
  • + false +
  • +
+
+
+
+
+`; + +exports[` with a CRD with a boolean field should display true in an additionalPrinterColumn as 'true' 1`] = ` +
+
+
+ + Created + + + <unknown> + ago + +
+
+ + Name + + + first-crd + +
+
+ + MyField + + +
    +
  • + true +
  • +
+
+
+
+
+`; + +exports[` with a CRD with a number field should display 0 in an additionalPrinterColumn as '0' 1`] = ` +
+
+
+ + Created + + + <unknown> + ago + +
+
+ + Name + + + first-crd + +
+
+ + MyField + + +
    +
  • + 0 +
  • +
+
+
+
+
+`; + +exports[` with a CRD with a number field should display 1234 in an additionalPrinterColumn as '1234' 1`] = ` +
+
+
+ + Created + + + <unknown> + ago + +
+
+ + Name + + + first-crd + +
+
+ + MyField + + +
    +
  • + 1234 +
  • +
+
+
+
+
+`; diff --git a/src/renderer/components/+custom-resources/__tests__/custom-resource-details.test.tsx b/src/renderer/components/+custom-resources/__tests__/custom-resource-details.test.tsx new file mode 100644 index 0000000000..2b8e8b2b1e --- /dev/null +++ b/src/renderer/components/+custom-resources/__tests__/custom-resource-details.test.tsx @@ -0,0 +1,206 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import React from "react"; +import { CustomResourceDefinition } from "../../../../common/k8s-api/endpoints"; +import { KubeObject } from "../../../../common/k8s-api/kube-object"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; +import { CustomResourceDetails } from "../crd-resource-details"; + +describe("", () => { + let render: DiRender; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + render = renderFor(di); + }); + + describe("with a CRD with a boolean field", () => { + let crd: CustomResourceDefinition; + + beforeEach(() => { + crd = new CustomResourceDefinition({ + apiVersion: "apiextensions.k8s.io/v1", + kind: "CustomResourceDefinition", + metadata: { + name: "my-crd", + resourceVersion: "1", + selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/my-crd", + uid: "1", + }, + spec: { + versions: [{ + name: "v1", + served: true, + storage: true, + schema: { + openAPIV3Schema: { + type: "object", + properties: { + spec: { + type: "object", + properties: { + "my-field": { + type: "boolean", + }, + }, + }, + }, + }, + }, + additionalPrinterColumns: [ + { + name: "MyField", + jsonPath: ".spec.my-field", + type: "boolean", + }, + ], + }], + group: "stable.lens.dev", + names: { + kind: "MyCrd", + plural: "my-crds", + }, + scope: "Cluster", + }, + }); + }); + + it("should display false in an additionalPrinterColumn as 'false'", () => { + const cr = new KubeObject({ + apiVersion: "stable.lens.dev/v1", + kind: "MyCrd", + metadata: { + name: "first-crd", + resourceVersion: "1", + selfLink: "stable.lens.dev/v1/first-crd", + uid: "2", + }, + spec: { + "my-field": false, + }, + }); + const result = render(); + + expect(result.container).toMatchSnapshot(); + expect(result.getByText("false")).toBeTruthy(); + }); + + it("should display true in an additionalPrinterColumn as 'true'", () => { + const cr = new KubeObject({ + apiVersion: "stable.lens.dev/v1", + kind: "MyCrd", + metadata: { + name: "first-crd", + resourceVersion: "1", + selfLink: "stable.lens.dev/v1/first-crd", + uid: "2", + }, + spec: { + "my-field": true, + }, + }); + const result = render(); + + expect(result.container).toMatchSnapshot(); + expect(result.getByText("true")).toBeTruthy(); + }); + }); + + describe("with a CRD with a number field", () => { + let crd: CustomResourceDefinition; + + beforeEach(() => { + crd = new CustomResourceDefinition({ + apiVersion: "apiextensions.k8s.io/v1", + kind: "CustomResourceDefinition", + metadata: { + name: "my-crd", + resourceVersion: "1", + selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/my-crd", + uid: "1", + }, + spec: { + versions: [{ + name: "v1", + served: true, + storage: true, + schema: { + openAPIV3Schema: { + type: "object", + properties: { + spec: { + type: "object", + properties: { + "my-field": { + type: "number", + }, + }, + }, + }, + }, + }, + additionalPrinterColumns: [ + { + name: "MyField", + jsonPath: ".spec.my-field", + type: "number", + }, + ], + }], + group: "stable.lens.dev", + names: { + kind: "MyCrd", + plural: "my-crds", + }, + scope: "Cluster", + }, + }); + }); + + it("should display 0 in an additionalPrinterColumn as '0'", () => { + const cr = new KubeObject({ + apiVersion: "stable.lens.dev/v1", + kind: "MyCrd", + metadata: { + name: "first-crd", + resourceVersion: "1", + selfLink: "stable.lens.dev/v1/first-crd", + uid: "2", + }, + spec: { + "my-field": 0, + }, + }); + const result = render(); + + expect(result.container).toMatchSnapshot(); + expect(result.getByText("0")).toBeTruthy(); + }); + + it("should display 1234 in an additionalPrinterColumn as '1234'", () => { + const cr = new KubeObject({ + apiVersion: "stable.lens.dev/v1", + kind: "MyCrd", + metadata: { + name: "first-crd", + resourceVersion: "1", + selfLink: "stable.lens.dev/v1/first-crd", + uid: "2", + }, + spec: { + "my-field": 1234, + }, + }); + const result = render(); + + expect(result.container).toMatchSnapshot(); + expect(result.getByText("1234")).toBeTruthy(); + }); + }); +}); diff --git a/src/renderer/components/+custom-resources/crd-resource-details.tsx b/src/renderer/components/+custom-resources/crd-resource-details.tsx index 36c06de0e3..48049682e4 100644 --- a/src/renderer/components/+custom-resources/crd-resource-details.tsx +++ b/src/renderer/components/+custom-resources/crd-resource-details.tsx @@ -25,7 +25,7 @@ export interface CustomResourceDetailsProps extends KubeObjectDetailsProps @@ -50,18 +50,22 @@ function convertSpecValue(value: any): any { ); } - return value; + if ( + typeof value === "boolean" + || typeof value === "string" + || typeof value === "number" + ) { + return value.toString(); + } + + return null; } @observer export class CustomResourceDetails extends React.Component { renderAdditionalColumns(resource: KubeObject, columns: AdditionalPrinterColumnsV1[]) { return columns.map(({ name, jsonPath }) => ( - + {convertSpecValue(JSONPath.query(resource, convertKubectlJsonPathToNodeJsonPath(jsonPath)))} )); diff --git a/src/renderer/components/drawer/drawer-item.tsx b/src/renderer/components/drawer/drawer-item.tsx index 737749f783..72a311794c 100644 --- a/src/renderer/components/drawer/drawer-item.tsx +++ b/src/renderer/components/drawer/drawer-item.tsx @@ -5,14 +5,20 @@ import "./drawer-item.scss"; import React from "react"; -import { cssNames, displayBooleans } from "../../utils"; +import { cssNames } from "../../utils"; export interface DrawerItemProps extends React.HTMLAttributes { name: React.ReactNode; title?: string; labelsOnly?: boolean; hidden?: boolean; - renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean" + + /** + * @deprecated This prop is no longer used, you should stringify the booleans yourself. + * + * This was only meant to be an internal prop anyway. + */ + renderBooleans?: boolean; } export function DrawerItem({ @@ -22,7 +28,6 @@ export function DrawerItem({ children, hidden = false, className, - renderBoolean, ...elemProps }: DrawerItemProps) { if (hidden) { @@ -36,7 +41,7 @@ export function DrawerItem({ title={title} > {name} - {displayBooleans(renderBoolean, children)} + {children} ); } diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx index a002923e40..758c5cd951 100644 --- a/src/renderer/components/table/table-cell.tsx +++ b/src/renderer/components/table/table-cell.tsx @@ -8,7 +8,7 @@ import type { TableSortBy, TableSortParams } from "./table"; import type { ReactNode } from "react"; import React from "react"; -import { autoBind, cssNames, displayBooleans } from "../../utils"; +import { autoBind, cssNames } from "../../utils"; import { Icon } from "../icon"; import { Checkbox } from "../checkbox"; @@ -45,11 +45,6 @@ export interface TableCellProps extends React.DOMAttributes { */ isChecked?: boolean; - /** - * show "true" or "false" for all of the children elements are "typeof boolean" - */ - renderBoolean?: boolean; - /** * column name, must be same as key in sortable object */ @@ -136,7 +131,6 @@ export class TableCell extends React.Component { _nowrap, children, title, - renderBoolean: displayBoolean = false, showWithColumn, ...cellProps } = this.props; @@ -147,7 +141,7 @@ export class TableCell extends React.Component { nowrap: _nowrap, sorting: _sort && typeof sortBy === "string", }); - const content = displayBooleans(displayBoolean, title || children); + const content = title || children; return (
{ - it("should not do anything to div's if shouldShow is false", () => { - expect(displayBooleans(false,
)).toStrictEqual(
); - }); - - it("should not do anything to booleans's if shouldShow is false", () => { - expect(displayBooleans(false, true)).toStrictEqual(true); - expect(displayBooleans(false, false)).toStrictEqual(false); - }); - - it("should stringify booleans when shouldShow is true", () => { - expect(displayBooleans(true, true)).toStrictEqual("true"); - expect(displayBooleans(true, false)).toStrictEqual("false"); - }); -}); diff --git a/src/renderer/utils/display-booleans.ts b/src/renderer/utils/display-booleans.ts deleted file mode 100644 index fc92004295..0000000000 --- a/src/renderer/utils/display-booleans.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type React from "react"; - -export function displayBooleans(shouldShow: boolean | undefined, from: React.ReactNode): React.ReactNode { - if (shouldShow) { - if (typeof from === "boolean") { - return from.toString(); - } - - if (Array.isArray(from)) { - return from.map(node => displayBooleans(shouldShow, node)); - } - } - - return from; -} diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index fee7dfc56d..b4e315be17 100755 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -9,7 +9,6 @@ export * from "../../common/event-emitter"; export * from "./cssNames"; export * from "./cssVar"; -export * from "./display-booleans"; export * from "./display-mode"; export * from "./interval"; export * from "./isMiddleClick";