diff --git a/locales/en/messages.po b/locales/en/messages.po index cfa796b938..86024661b6 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -2407,8 +2407,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/cluster-manager/clusters-menu.tsx:84 msgid "This is the quick launch menu." diff --git a/locales/fi/messages.po b/locales/fi/messages.po index 0aa7bb5ca8..5aa3ed6695 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -2390,7 +2390,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/cluster-manager/clusters-menu.tsx:84 diff --git a/locales/ru/messages.po b/locales/ru/messages.po index c32d565891..d125111613 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -2408,7 +2408,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/cluster-manager/clusters-menu.tsx:84 diff --git a/src/main/router.ts b/src/main/router.ts index 7c5d48e1d6..1b50ec2c2e 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -121,7 +121,7 @@ export class Router { this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute)) // Port-forward API - this.router.add({ method: "post", path: `${apiPrefix}/services/{namespace}/{service}/port-forward/{port}` }, portForwardRoute.routeServicePortForward.bind(portForwardRoute)) + this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, portForwardRoute.routePortForward.bind(portForwardRoute)) // Helm API this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, helmRoute.listCharts.bind(helmRoute)) diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index a5fe683cc2..86cf4f0917 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.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.getProxyKubeconfigPath() }) const started = await portForward.start() diff --git a/src/renderer/api/kube-api-parse.ts b/src/renderer/api/kube-api-parse.ts index df75eea2c4..d5e61c2305 100644 --- a/src/renderer/api/kube-api-parse.ts +++ b/src/renderer/api/kube-api-parse.ts @@ -50,6 +50,9 @@ export function parseKubeApi(path: string): IKubeApiParsed { apiGroup = left.join("/"); } else { switch (left.length) { + case 4: + [apiGroup, apiVersion, resource, name] = left + break; case 2: resource = left.pop(); // fallthrough @@ -66,7 +69,7 @@ export function parseKubeApi(path: string): IKubeApiParsed { * - `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/api/kube-api-parse_test.ts b/src/renderer/api/kube-api-parse_test.ts index b33c833bfa..dee3bf031d 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: { diff --git a/src/renderer/browser-check.tsx b/src/renderer/browser-check.tsx deleted file mode 100644 index ce6c9ecad6..0000000000 --- a/src/renderer/browser-check.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import { Notifications } from "./components/notifications"; -import { Trans } from "@lingui/macro"; - -export function browserCheck() { - const ua = window.navigator.userAgent - const msie = ua.indexOf('MSIE ') // IE < 11 - const trident = ua.indexOf('Trident/') // IE 11 - const edge = ua.indexOf('Edge') // Edge - if (msie > 0 || trident > 0 || edge > 0) { - Notifications.info( -

- - Your browser does not support all Lens features. {" "} - Please consider using another browser. - -

- ) - } -} \ No newline at end of file diff --git a/src/renderer/components/+cluster-settings/cluster-settings.scss b/src/renderer/components/+cluster-settings/cluster-settings.scss index e02a4da5f8..acb59ea0ac 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.scss +++ b/src/renderer/components/+cluster-settings/cluster-settings.scss @@ -57,6 +57,10 @@ font-size: smaller; opacity: 0.8; } + + p + p, .hint + p { + padding-top: $padding; + } } .status-table { @@ -79,7 +83,13 @@ } } - .Input,.Select { + .Input, .Select { margin-top: 10px; } + + .Select { + &__control { + box-shadow: 0 0 0 1px $borderFaintColor; + } + } } \ No newline at end of file diff --git a/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx index e4f81bb18a..729090629d 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx @@ -1,10 +1,11 @@ import React from "react"; -import merge from "lodash/merge"; import { observer } from "mobx-react"; import { prometheusProviders } from "../../../../common/prometheus-providers"; import { Cluster } from "../../../../main/cluster"; import { SubTitle } from "../../layout/sub-title"; import { Select, SelectOption } from "../../select"; +import { Input } from "../../input"; +import { observable, computed } from "mobx"; const options: SelectOption[] = [ { value: "", label: "Auto detect" }, @@ -17,6 +18,52 @@ interface Props { @observer export class ClusterPrometheusSetting extends React.Component { + @observable path = ""; + @observable provider = ""; + + @computed get canEditPrometheusPath() { + if (this.provider === "" || this.provider === "lens") return false; + return true; + } + + componentDidMount() { + const { prometheus, prometheusProvider } = this.props.cluster.preferences; + if (prometheus) { + const prefix = prometheus.prefix || ""; + this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`; + } + if (prometheusProvider) { + this.provider = prometheusProvider.type; + } + } + + parsePrometheusPath = () => { + if (!this.provider || !this.path) { + return null; + } + const parsed = this.path.split(/\/|:/, 3); + const apiPrefix = this.path.substring(parsed.join("/").length); + if (!parsed[0] || !parsed[1] || !parsed[2]) { + return null; + } + return { + namespace: parsed[0], + service: parsed[1], + port: parseInt(parsed[2]), + prefix: apiPrefix + } + } + + onSaveProvider = () => { + this.props.cluster.preferences.prometheusProvider = this.provider ? + { type: this.provider } : + null; + } + + onSavePath = () => { + this.props.cluster.preferences.prometheus = this.parsePrometheusPath(); + }; + render() { return ( <> @@ -26,18 +73,32 @@ export class ClusterPrometheusSetting extends React.Component { guide{" "} for possible configuration changes.

+

Prometheus installation method.

this.path = value} + onBlur={this.onSavePath} + placeholder="/:" + /> + + An address to an existing Prometheus installation{" "} + ({'/:'}). Lens tries to auto-detect address if left empty. + + + )} ); } 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/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-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index 32fb1ec10c..5a3031787a 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -98,7 +98,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) { return ( DeploymentScaleDialog.open(object)}> - + Scale 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( + ) }) } diff --git a/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx b/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx index e6e9708002..d7ff3863cc 100644 --- a/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx +++ b/src/renderer/components/+workloads-pods/pod-logs-dialog.tsx @@ -226,7 +226,7 @@ export class PodLogsDialog extends React.Component { tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)} /> diff --git a/src/renderer/components/app.scss b/src/renderer/components/app.scss index 3fcbce9613..59d9b632d0 100755 --- a/src/renderer/components/app.scss +++ b/src/renderer/components/app.scss @@ -82,7 +82,7 @@ hr { h1 { color: white; font-size: 28px; - font-weight: 300; + font-weight: normal; letter-spacing: -.010em; margin: 0; } @@ -99,13 +99,13 @@ h3 { h4 { @extend h3; - font-size: 16px; + font-size: 18px; } h5 { @extend h4; padding: $padding / 2 0; - font-size: 14px; + font-size: 16px; } h6 { diff --git a/src/renderer/components/cluster-manager/cluster-manager.scss b/src/renderer/components/cluster-manager/cluster-manager.scss index 1f55219b50..991edbb009 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.scss +++ b/src/renderer/components/cluster-manager/cluster-manager.scss @@ -14,6 +14,7 @@ #lens-views { grid-area: main; display: flex; + overflow: hidden; &.active { z-index: 1; 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 diff --git a/src/renderer/components/select/select.scss b/src/renderer/components/select/select.scss index 20078215ba..1b5a081362 100644 --- a/src/renderer/components/select/select.scss +++ b/src/renderer/components/select/select.scss @@ -31,7 +31,7 @@ html { border-radius: $radius; background: transparent; min-height: 0; - box-shadow: 0 0 0 1px $borderFaintColor; + box-shadow: 0 0 0 1px $halfGray; &--is-focused { box-shadow: 0 0 0 2px $primary; 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