From 7f65f1ea0682cebb23a6cbdecb947ee90ffc2376 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Fri, 31 Jul 2020 16:50:57 +0300 Subject: [PATCH 1/4] Fix CRD api parsing (#622) Signed-off-by: Lauri Nevala --- src/renderer/api/kube-api-parse.ts | 3 +++ src/renderer/api/kube-api-parse_test.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/renderer/api/kube-api-parse.ts b/src/renderer/api/kube-api-parse.ts index 0745dc71eb..97a7875322 100644 --- a/src/renderer/api/kube-api-parse.ts +++ b/src/renderer/api/kube-api-parse.ts @@ -40,6 +40,9 @@ export function parseApi(path: string): IKubeApiLinkBase { apiGroup = left.join("/"); } else { switch (left.length) { + case 4: + [apiGroup, apiVersion, resource, name] = left + break; case 2: resource = left.pop(); // fallthrough diff --git a/src/renderer/api/kube-api-parse_test.ts b/src/renderer/api/kube-api-parse_test.ts index 03f53ae34d..1fda8c3e53 100644 --- a/src/renderer/api/kube-api-parse_test.ts +++ b/src/renderer/api/kube-api-parse_test.ts @@ -6,6 +6,19 @@ interface KubeApi_Parse_Test { } const tests: KubeApi_Parse_Test[] = [ + { + url: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", + expected: { + apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions", + apiPrefix: "/apis", + apiGroup: "apiextensions.k8s.io", + apiVersion: "v1beta1", + apiVersionWithGroup: "apiextensions.k8s.io/v1beta1", + namespace: undefined, + resource: "customresourcedefinitions", + name: "prometheuses.monitoring.coreos.com" + }, + }, { url: "/api/v1/namespaces/kube-system/pods/coredns-6955765f44-v8p27", expected: { From 0c3be9bbaea6d18eb8296b1e52949081180abed3 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 4 Aug 2020 13:14:02 -0400 Subject: [PATCH 2/4] Fix Resource Quota Rendering (#624) * add parsing of quota values which are non-numeric in the general case * add unit tests Signed-off-by: Sebastian Malton Co-authored-by: Sebastian Malton --- .../resource-quota-details.tsx | 48 +++++++++++-------- src/renderer/utils/convertCpu.ts | 13 +++-- src/renderer/utils/convertMemory.ts | 10 ++-- src/renderer/utils/index.ts | 1 + src/renderer/utils/metricUnitsToNumber.ts | 10 ++++ .../utils/metricUnitsToNumber_test.ts | 15 ++++++ 6 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 src/renderer/utils/metricUnitsToNumber.ts create mode 100644 src/renderer/utils/metricUnitsToNumber_test.ts diff --git a/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx b/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx index 6a2665e741..3da8c7112a 100644 --- a/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx +++ b/src/renderer/components/+config-resource-quotas/resource-quota-details.tsx @@ -4,7 +4,7 @@ import kebabCase from "lodash/kebabCase"; import { observer } from "mobx-react"; import { Trans } from "@lingui/macro"; import { DrawerItem, DrawerTitle } from "../drawer"; -import { cpuUnitsToNumber, cssNames, unitsToBytes } from "../../utils"; +import { cpuUnitsToNumber, cssNames, unitsToBytes, metricUnitsToNumber } from "../../utils"; import { KubeObjectDetailsProps } from "../kube-object"; import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api"; import { LineProgress } from "../line-progress"; @@ -15,24 +15,30 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta"; interface Props extends KubeObjectDetailsProps { } -@observer -export class ResourceQuotaDetails extends React.Component { - renderQuotas = (quota: ResourceQuota) => { - const { hard, used } = quota.status - if (!hard || !used) return null - const transformUnit = (name: string, value: string) => { - if (name.includes("memory") || name.includes("storage")) { - return unitsToBytes(value) - } - if (name.includes("cpu")) { - return cpuUnitsToNumber(value) - } - return parseInt(value) - } - return Object.entries(hard).map(([name, value]) => { - if (!used[name]) return null +const onlyNumbers = /$[0-9]*^/g; + +function transformUnit(name: string, value: string): number { + if (name.includes("memory") || name.includes("storage")) { + return unitsToBytes(value) + } + + if (name.includes("cpu")) { + return cpuUnitsToNumber(value) + } + + return metricUnitsToNumber(value); +} + +function renderQuotas(quota: ResourceQuota): JSX.Element[] { + const { hard = {}, used = {} } = quota.status + + return Object.entries(hard) + .filter(([name]) => used[name]) + .map(([name, value]) => { const current = transformUnit(name, used[name]) const max = transformUnit(name, value) + const usage = max === 0 ? 100 : Math.ceil(current / max * 100); // special case 0 max as always 100% usage + return (
{name} @@ -41,14 +47,16 @@ export class ResourceQuotaDetails extends React.Component { max={max} value={current} tooltip={ -

Set: {value}. Used: {Math.ceil(current / max * 100) + "%"}

+

Set: {value}. Usage: {usage + "%"}

} />
) }) - } +} +@observer +export class ResourceQuotaDetails extends React.Component { render() { const { object: quota } = this.props; if (!quota) return null; @@ -57,7 +65,7 @@ export class ResourceQuotaDetails extends React.Component { Quotas} className="quota-list"> - {this.renderQuotas(quota)} + {renderQuotas(quota)} {quota.getScopeSelector().length > 0 && ( diff --git a/src/renderer/utils/convertCpu.ts b/src/renderer/utils/convertCpu.ts index 2e7c6b85c3..7b81a30cc3 100644 --- a/src/renderer/utils/convertCpu.ts +++ b/src/renderer/utils/convertCpu.ts @@ -1,10 +1,13 @@ // Helper to convert CPU K8S units to numbers +const thousand = 1000; +const million = thousand * thousand; +const shortBillion = thousand * million; + export function cpuUnitsToNumber(cpu: string) { const cpuNum = parseInt(cpu) - const billion = 1000000 * 1000 - if (cpu.includes("m")) return cpuNum / 1000 - if (cpu.includes("u")) return cpuNum / 1000000 - if (cpu.includes("n")) return cpuNum / billion + if (cpu.includes("m")) return cpuNum / thousand + if (cpu.includes("u")) return cpuNum / million + if (cpu.includes("n")) return cpuNum / shortBillion return parseFloat(cpu) -} \ No newline at end of file +} diff --git a/src/renderer/utils/convertMemory.ts b/src/renderer/utils/convertMemory.ts index faa89dd990..d0d7e1fc52 100644 --- a/src/renderer/utils/convertMemory.ts +++ b/src/renderer/utils/convertMemory.ts @@ -7,9 +7,9 @@ export function unitsToBytes(value: string) { if (!suffixes.some(suffix => value.includes(suffix))) { return parseFloat(value) } - const index = suffixes.findIndex(suffix => - suffix == value.replace(/[0-9]|i|\./g, '') - ) + + const suffix = value.replace(/[0-9]|i|\./g, ''); + const index = suffixes.indexOf(suffix); return parseInt( (parseFloat(value) * Math.pow(base, index + 1)).toFixed(1) ) @@ -21,8 +21,10 @@ export function bytesToUnits(bytes: number, precision = 1) { if (!bytes) { return "N/A" } + if (index === 0) { return `${bytes}${sizes[index]}` } + return `${(bytes / (1024 ** index)).toFixed(precision)}${sizes[index]}i` -} \ No newline at end of file +} diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index 8a3a263077..578ec5c355 100755 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -20,3 +20,4 @@ export * from './formatDuration' export * from './isReactNode' export * from './convertMemory' export * from './convertCpu' +export * from './metricUnitsToNumber' diff --git a/src/renderer/utils/metricUnitsToNumber.ts b/src/renderer/utils/metricUnitsToNumber.ts new file mode 100644 index 0000000000..9390c35b24 --- /dev/null +++ b/src/renderer/utils/metricUnitsToNumber.ts @@ -0,0 +1,10 @@ +const base = 1000; +const suffixes = ["k", "m", "g", "t", "q"]; + +export function metricUnitsToNumber(value: string): number { + const suffix = value.toLowerCase().slice(-1); + const index = suffixes.indexOf(suffix); + return parseInt( + (parseFloat(value) * Math.pow(base, index + 1)).toFixed(1) + ) +} diff --git a/src/renderer/utils/metricUnitsToNumber_test.ts b/src/renderer/utils/metricUnitsToNumber_test.ts new file mode 100644 index 0000000000..cbb0669122 --- /dev/null +++ b/src/renderer/utils/metricUnitsToNumber_test.ts @@ -0,0 +1,15 @@ +import { metricUnitsToNumber } from "./metricUnitsToNumber"; + +describe("metricUnitsToNumber tests", () => { + test("plain number", () => { + expect(metricUnitsToNumber("124")).toStrictEqual(124); + }); + + test("with k suffix", () => { + expect(metricUnitsToNumber("124k")).toStrictEqual(124000); + }); + + test("with m suffix", () => { + expect(metricUnitsToNumber("124m")).toStrictEqual(124000000); + }); +}); \ No newline at end of file From 693017d2ec87a971d35a9dc1e628e507737f3e4d Mon Sep 17 00:00:00 2001 From: Jim Ehrismann <40840436+jim-docker@users.noreply.github.com> Date: Fri, 7 Aug 2020 11:30:37 -0400 Subject: [PATCH 3/4] adding port-forward for containers in pods (#528) * adding port-forward for containers in pods address review comments use more idiomatic approach for async code move some files in advance of merge conflict with Lens restructure work * Separate the port forward links in the UI (so they don't all spin when one link is clicked) * minor fixes * addressed review comments (replaced

with

, moved key attribute to proper element) * fix lint issue * removed extraneous
from pod container port details Signed-off-by: Jim Ehrismann --- src/main/router.ts | 2 +- src/main/routes/port-forward.ts | 21 ++++---- .../+network-services/service-details.tsx | 10 +++- .../service-port-component.scss | 22 ++++++++ .../service-port-component.tsx | 48 +++++++++++++++++ .../+network-services/service-ports.scss | 24 --------- .../+network-services/service-ports.tsx | 54 ------------------- .../+workloads-pods/pod-container-port.scss | 23 ++++++++ .../+workloads-pods/pod-container-port.tsx | 54 +++++++++++++++++++ .../+workloads-pods/pod-details-container.tsx | 12 ++--- 10 files changed, 172 insertions(+), 98 deletions(-) create mode 100644 src/renderer/components/+network-services/service-port-component.scss create mode 100644 src/renderer/components/+network-services/service-port-component.tsx delete mode 100644 src/renderer/components/+network-services/service-ports.scss delete mode 100644 src/renderer/components/+network-services/service-ports.tsx create mode 100644 src/renderer/components/+workloads-pods/pod-container-port.scss create mode 100644 src/renderer/components/+workloads-pods/pod-container-port.tsx diff --git a/src/main/router.ts b/src/main/router.ts index 9ee3a47c73..3381fa139d 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -123,7 +123,7 @@ export class Router { this.router.add({ method: "post", path: `${apiBase}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute)) // Port-forward API - this.router.add({ method: "post", path: `${apiBase}/services/{namespace}/{service}/port-forward/{port}` }, portForwardRoute.routeServicePortForward.bind(portForwardRoute)) + this.router.add({ method: "post", path: `${apiBase}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, portForwardRoute.routePortForward.bind(portForwardRoute)) // Helm API this.router.add({ method: "get", path: `${apiHelm}/v2/charts` }, helmApi.listCharts.bind(helmApi)) diff --git a/src/main/routes/port-forward.ts b/src/main/routes/port-forward.ts index 27b1158700..ca222596a1 100644 --- a/src/main/routes/port-forward.ts +++ b/src/main/routes/port-forward.ts @@ -14,7 +14,7 @@ class PortForward { return PortForward.portForwards.find((pf) => { return ( pf.clusterId == forward.clusterId && - pf.kind == "service" && + pf.kind == forward.kind && pf.name == forward.name && pf.namespace == forward.namespace && pf.port == forward.port @@ -42,7 +42,7 @@ class PortForward { "--kubeconfig", this.kubeConfig, "port-forward", "-n", this.namespace, - `service/${this.name}`, + `${this.kind}/${this.name}`, `${this.localPort}:${this.port}` ] @@ -72,21 +72,22 @@ class PortForward { class PortForwardRoute extends LensApi { - public async routeServicePortForward(request: LensApiRequest) { + public async routePortForward(request: LensApiRequest) { const { params, response, cluster} = request + const { namespace, port, resourceType, resourceName } = params let portForward = PortForward.getPortforward({ - clusterId: cluster.id, kind: "service", name: params.service, - namespace: params.namespace, port: params.port + clusterId: cluster.id, kind: resourceType, name: resourceName, + namespace: namespace, port: port }) if (!portForward) { - logger.info(`Creating a new port-forward ${params.namespace}/${params.service}:${params.port}`) + logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`) portForward = new PortForward({ clusterId: cluster.id, - kind: "service", - namespace: params.namespace, - name: params.service, - port: params.port, + kind: resourceType, + namespace: namespace, + name: resourceName, + port: port, kubeConfig: cluster.proxyKubeconfigPath() }) const started = await portForward.start() diff --git a/src/renderer/components/+network-services/service-details.tsx b/src/renderer/components/+network-services/service-details.tsx index 0c158c9e6b..df37d6238e 100644 --- a/src/renderer/components/+network-services/service-details.tsx +++ b/src/renderer/components/+network-services/service-details.tsx @@ -11,7 +11,7 @@ import { Service, serviceApi, endpointApi } from "../../api/endpoints"; import { _i18n } from "../../i18n"; import { apiManager } from "../../api/api-manager"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; -import { ServicePorts } from "./service-ports"; +import { ServicePortComponent } from "./service-port-component"; import { endpointStore } from "../+network-endpoints/endpoints.store"; import { ServiceDetailsEndpoint } from "./service-details-endpoint"; @@ -61,7 +61,13 @@ export class ServiceDetails extends React.Component { )} Ports}> - +
+ { + service.getPorts().map((port) => ( + + )) + } +
{spec.type === "LoadBalancer" && spec.loadBalancerIP && ( diff --git a/src/renderer/components/+network-services/service-port-component.scss b/src/renderer/components/+network-services/service-port-component.scss new file mode 100644 index 0000000000..0e9945631d --- /dev/null +++ b/src/renderer/components/+network-services/service-port-component.scss @@ -0,0 +1,22 @@ +.ServicePortComponent { + &.waiting { + opacity: 0.5; + pointer-events: none; + } + + &:not(:last-child) { + margin-bottom: $margin; + } + + span { + cursor: pointer; + color: $primary; + text-decoration: underline; + } + + .Spinner { + --spinner-size: #{$unit * 2}; + margin-left: $margin; + position: absolute; + } +} diff --git a/src/renderer/components/+network-services/service-port-component.tsx b/src/renderer/components/+network-services/service-port-component.tsx new file mode 100644 index 0000000000..252bf8eb16 --- /dev/null +++ b/src/renderer/components/+network-services/service-port-component.tsx @@ -0,0 +1,48 @@ +import "./service-port-component.scss" + +import React from "react"; +import { observer } from "mobx-react"; +import { t } from "@lingui/macro"; +import { Service, ServicePort } from "../../api/endpoints"; +import { _i18n } from "../../i18n"; +import { apiBase } from "../../api" +import { observable } from "mobx"; +import { cssNames } from "../../utils"; +import { Notifications } from "../notifications"; +import { Spinner } from "../spinner" + +interface Props { + service: Service; + port: ServicePort; +} + +@observer +export class ServicePortComponent extends React.Component { + @observable waiting = false; + + async portForward() { + const { service, port } = this.props; + this.waiting = true; + try { + await apiBase.post(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}`, {}) + } catch(error) { + Notifications.error(error); + } finally { + this.waiting = false; + } + } + + render() { + const { port } = this.props; + return ( +
+ this.portForward() }> + {port.toString()} + {this.waiting && ( + + )} + +
+ ); + } +} diff --git a/src/renderer/components/+network-services/service-ports.scss b/src/renderer/components/+network-services/service-ports.scss deleted file mode 100644 index 5a683af86c..0000000000 --- a/src/renderer/components/+network-services/service-ports.scss +++ /dev/null @@ -1,24 +0,0 @@ -.ServicePorts { - &.waiting { - opacity: 0.5; - pointer-events: none; - } - - p { - &:not(:last-child) { - margin-bottom: $margin; - } - - span { - cursor: pointer; - color: $primary; - text-decoration: underline; - } - } - - .Spinner { - --spinner-size: #{$unit * 2}; - margin-left: $margin; - position: absolute; - } -} diff --git a/src/renderer/components/+network-services/service-ports.tsx b/src/renderer/components/+network-services/service-ports.tsx deleted file mode 100644 index 3335be6907..0000000000 --- a/src/renderer/components/+network-services/service-ports.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import "./service-ports.scss" - -import React from "react"; -import { observer } from "mobx-react"; -import { t } from "@lingui/macro"; -import { Service, ServicePort } from "../../api/endpoints"; -import { _i18n } from "../../i18n"; -import { apiBase } from "../../api" -import { observable } from "mobx"; -import { cssNames } from "../../utils"; -import { Notifications } from "../notifications"; -import { Spinner } from "../spinner" - -interface Props { - service: Service; -} - -@observer -export class ServicePorts extends React.Component { - @observable waiting = false; - - async portForward(port: ServicePort) { - const { service } = this.props; - this.waiting = true; - apiBase.post(`/services/${service.getNs()}/${service.getName()}/port-forward/${port.port}`, {}) - .catch(error => { - Notifications.error(error); - }) - .finally(() => { - this.waiting = false; - }); - } - - render() { - const { service } = this.props; - return ( -
- { - service.getPorts().map((port) => { - return( -

- this.portForward(port) }> - {port.toString()} - {this.waiting && ( - - )} - -

- ); - })} -
- ); - } -} diff --git a/src/renderer/components/+workloads-pods/pod-container-port.scss b/src/renderer/components/+workloads-pods/pod-container-port.scss new file mode 100644 index 0000000000..081f0b1090 --- /dev/null +++ b/src/renderer/components/+workloads-pods/pod-container-port.scss @@ -0,0 +1,23 @@ +.PodContainerPort { + &.waiting { + opacity: 0.5; + pointer-events: none; + } + + &:not(:last-child) { + margin-bottom: $margin; + } + + span { + cursor: pointer; + color: $primary; + text-decoration: underline; + position: relative; + } + + .Spinner { + --spinner-size: #{$unit * 2}; + margin-left: $margin; + position: absolute; + } +} \ No newline at end of file diff --git a/src/renderer/components/+workloads-pods/pod-container-port.tsx b/src/renderer/components/+workloads-pods/pod-container-port.tsx new file mode 100644 index 0000000000..9ebaed4fd7 --- /dev/null +++ b/src/renderer/components/+workloads-pods/pod-container-port.tsx @@ -0,0 +1,54 @@ +import "./pod-container-port.scss" + +import React from "react"; +import { observer } from "mobx-react"; +import { t } from "@lingui/macro"; +import { Pod, IPodContainer } from "../../api/endpoints"; +import { _i18n } from "../../i18n"; +import { apiBase } from "../../api" +import { observable } from "mobx"; +import { cssNames } from "../../utils"; +import { Notifications } from "../notifications"; +import { Spinner } from "../spinner" + +interface Props { + pod: Pod; + port: { + name?: string; + containerPort: number; + protocol: string; + } +} + +@observer +export class PodContainerPort extends React.Component { + @observable waiting = false; + + async portForward() { + const { pod, port } = this.props; + this.waiting = true; + try { + await apiBase.post(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}`, {}) + } catch(error) { + Notifications.error(error); + } finally { + this.waiting = false; + } + } + + render() { + const { port } = this.props; + const { name, containerPort, protocol } = port; + const text = (name ? name + ': ' : '')+`${containerPort}/${protocol}` + return ( +
+ this.portForward() }> + {text} + {this.waiting && ( + + )} + +
+ ) + } +} diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index a5cdcc1fbd..79180b1130 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -8,6 +8,7 @@ import { cssNames } from "../../utils"; import { StatusBrick } from "../status-brick"; import { Badge } from "../badge"; import { ContainerEnvironment } from "./pod-container-env"; +import { PodContainerPort } from "./pod-container-port"; import { ResourceMetrics } from "../resource-metrics"; import { IMetrics } from "../../api/endpoints/metrics.api"; import { ContainerCharts } from "./container-charts"; @@ -64,13 +65,10 @@ export class PodDetailsContainer extends React.Component { {ports && ports.length > 0 && Ports}> { - ports.map(port => { - const { name, containerPort, protocol } = port; - const key = `${container.name}-port-${containerPort}-${protocol}` - return ( -
- {name ? name + ': ' : ''}{containerPort}/{protocol} -
+ ports.map((port) => { + const key = `${container.name}-port-${port.containerPort}-${port.protocol}` + return( + ) }) } From 858ab88940f3b208835c3ad70b7010ab700e38ee Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 7 Aug 2020 16:24:47 -0400 Subject: [PATCH 4/4] use the Kubernetes regex for matching system names (#659) * use the Kubernetes regex for matching system names Signed-off-by: Sebastian Malton Co-authored-by: Sebastian Malton --- locales/en/messages.po | 4 +- locales/fi/messages.po | 2 +- locales/ru/messages.po | 2 +- src/renderer/api/kube-api-parse.ts | 2 +- .../components/input/input.validators.ts | 5 +- .../components/input/input.validators_test.ts | 48 +++++++++++++++++++ 6 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 src/renderer/components/input/input.validators_test.ts diff --git a/locales/en/messages.po b/locales/en/messages.po index f2ca7973e8..6f2abc511f 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -2178,8 +2178,8 @@ msgid "This field is required" msgstr "This field is required" #: src/renderer/components/input/input.validators.ts:39 -msgid "This field must contain only lowercase latin characters, numbers and dash." -msgstr "This field must contain only lowercase latin characters, numbers and dash." +msgid "A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics." +msgstr "A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics." #: src/renderer/components/+network-policies/network-policy-details.tsx:59 msgid "To" diff --git a/locales/fi/messages.po b/locales/fi/messages.po index 4d81ae2a65..5cfa28d784 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -2161,7 +2161,7 @@ msgid "This field is required" msgstr "" #: src/renderer/components/input/input.validators.ts:39 -msgid "This field must contain only lowercase latin characters, numbers and dash." +msgid "A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics." msgstr "" #: src/renderer/components/+network-policies/network-policy-details.tsx:59 diff --git a/locales/ru/messages.po b/locales/ru/messages.po index 6e55ab9f73..d67fcd9ab1 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -2179,7 +2179,7 @@ msgid "This field is required" msgstr "Это обязательное поле" #: src/renderer/components/input/input.validators.ts:39 -msgid "This field must contain only lowercase latin characters, numbers and dash." +msgid "A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics." msgstr "Это поле может содержать только латинские буквы в нижнем регистре, номера и дефис." #: src/renderer/components/+network-policies/network-policy-details.tsx:59 diff --git a/src/renderer/api/kube-api-parse.ts b/src/renderer/api/kube-api-parse.ts index 97a7875322..1354f86b60 100644 --- a/src/renderer/api/kube-api-parse.ts +++ b/src/renderer/api/kube-api-parse.ts @@ -59,7 +59,7 @@ export function parseApi(path: string): IKubeApiLinkBase { * - `GROUP` is /^D(\.D)*$/ where D is `DNS_LABEL` and length <= 253 * * There is no well defined selection from an array of items that were - * seperated by '/' + * separated by '/' * * Solution is to create a huristic. Namely: * 1. if '.' in left[0] then apiGroup <- left[0] diff --git a/src/renderer/components/input/input.validators.ts b/src/renderer/components/input/input.validators.ts index b0b415ef9f..7db0934d4f 100644 --- a/src/renderer/components/input/input.validators.ts +++ b/src/renderer/components/input/input.validators.ts @@ -53,9 +53,10 @@ export const maxLength: Validator = { validate: (value, { maxLength }) => value.length <= maxLength, }; +const systemNameMatcher = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/; export const systemName: Validator = { - message: () => _i18n._(t`This field must contain only lowercase latin characters, numbers and dash.`), - validate: value => !!value.match(/^[a-z0-9-]+$/), + message: () => _i18n._(t`A System Name must be lowercase DNS labels separated by dots. DNS labels are alphanumerics and dashes enclosed by alphanumerics.`), + validate: value => !!value.match(systemNameMatcher), }; export const accountId: Validator = { diff --git a/src/renderer/components/input/input.validators_test.ts b/src/renderer/components/input/input.validators_test.ts new file mode 100644 index 0000000000..4477d63e93 --- /dev/null +++ b/src/renderer/components/input/input.validators_test.ts @@ -0,0 +1,48 @@ +import { isEmail, systemName } from "./input.validators"; + +describe("input validation tests", () => { + describe("isEmail tests", () => { + it("should be valid", () => { + expect(isEmail.validate("abc@news.com")).toBe(true); + expect(isEmail.validate("abc@news.co.uk")).toBe(true); + expect(isEmail.validate("abc1.3@news.co.uk")).toBe(true); + expect(isEmail.validate("abc1.3@news.name")).toBe(true); + }); + + it("should be invalid", () => { + expect(isEmail.validate("@news.com")).toBe(false); + expect(isEmail.validate("abcnews.co.uk")).toBe(false); + expect(isEmail.validate("abc1.3@news")).toBe(false); + expect(isEmail.validate("abc1.3@news.name.a.b.c.d.d")).toBe(false); + }); + }); + + describe("systemName tests", () => { + it("should be valid", () => { + expect(systemName.validate("a")).toBe(true); + expect(systemName.validate("ab")).toBe(true); + expect(systemName.validate("abc")).toBe(true); + expect(systemName.validate("1")).toBe(true); + expect(systemName.validate("12")).toBe(true); + expect(systemName.validate("123")).toBe(true); + expect(systemName.validate("1a2")).toBe(true); + expect(systemName.validate("1-2")).toBe(true); + expect(systemName.validate("1---------------2")).toBe(true); + expect(systemName.validate("1---------------2.a")).toBe(true); + expect(systemName.validate("1---------------2.a.1")).toBe(true); + expect(systemName.validate("1---------------2.9-a.1")).toBe(true); + }); + + it("should be invalid", () => { + expect(systemName.validate("")).toBe(false); + expect(systemName.validate("-")).toBe(false); + expect(systemName.validate(".")).toBe(false); + expect(systemName.validate("as.")).toBe(false); + expect(systemName.validate(".asd")).toBe(false); + expect(systemName.validate("a.-")).toBe(false); + expect(systemName.validate("a.1-")).toBe(false); + expect(systemName.validate("o.2-2.")).toBe(false); + expect(systemName.validate("o.2-2....")).toBe(false); + }); + }); +}); \ No newline at end of file