mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into vue_react_migration
Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
commit
efcf02e0ad
@ -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."
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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<ResourceQuota> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ResourceQuotaDetails extends React.Component<Props> {
|
||||
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 (
|
||||
<div key={name} className={cssNames("param", kebabCase(name))}>
|
||||
<span className="title">{name}</span>
|
||||
@ -41,14 +47,16 @@ export class ResourceQuotaDetails extends React.Component<Props> {
|
||||
max={max}
|
||||
value={current}
|
||||
tooltip={
|
||||
<p><Trans>Set</Trans>: {value}. <Trans>Used</Trans>: {Math.ceil(current / max * 100) + "%"}</p>
|
||||
<p><Trans>Set</Trans>: {value}. <Trans>Usage</Trans>: {usage + "%"}</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ResourceQuotaDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: quota } = this.props;
|
||||
if (!quota) return null;
|
||||
@ -57,7 +65,7 @@ export class ResourceQuotaDetails extends React.Component<Props> {
|
||||
<KubeObjectMeta object={quota}/>
|
||||
|
||||
<DrawerItem name={<Trans>Quotas</Trans>} className="quota-list">
|
||||
{this.renderQuotas(quota)}
|
||||
{renderQuotas(quota)}
|
||||
</DrawerItem>
|
||||
|
||||
{quota.getScopeSelector().length > 0 && (
|
||||
|
||||
@ -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<Props> {
|
||||
)}
|
||||
|
||||
<DrawerItem name={<Trans>Ports</Trans>}>
|
||||
<ServicePorts service={service}/>
|
||||
<div>
|
||||
{
|
||||
service.getPorts().map((port) => (
|
||||
<ServicePortComponent service={service} port={port} key={port.toString()}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</DrawerItem>
|
||||
|
||||
{spec.type === "LoadBalancer" && spec.loadBalancerIP && (
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<Props> {
|
||||
@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 (
|
||||
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
||||
<span title={_i18n._(t`Open in a browser`)} onClick={() => this.portForward() }>
|
||||
{port.toString()}
|
||||
{this.waiting && (
|
||||
<Spinner />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<Props> {
|
||||
@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 (
|
||||
<div className={cssNames("ServicePorts", { waiting: this.waiting })}>
|
||||
{
|
||||
service.getPorts().map((port) => {
|
||||
return(
|
||||
<p key={port.toString()}>
|
||||
<span title={_i18n._(t`Open in a browser`)} onClick={() => this.portForward(port) }>
|
||||
{port.toString()}
|
||||
{this.waiting && (
|
||||
<Spinner />
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<Props> {
|
||||
@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 (
|
||||
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
||||
<span title={_i18n._(t`Open in a browser`)} onClick={() => this.portForward() }>
|
||||
{text}
|
||||
{this.waiting && (
|
||||
<Spinner />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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<Props> {
|
||||
{ports && ports.length > 0 &&
|
||||
<DrawerItem name={<Trans>Ports</Trans>}>
|
||||
{
|
||||
ports.map(port => {
|
||||
const { name, containerPort, protocol } = port;
|
||||
const key = `${container.name}-port-${containerPort}-${protocol}`
|
||||
return (
|
||||
<div key={key}>
|
||||
{name ? name + ': ' : ''}{containerPort}/{protocol}
|
||||
</div>
|
||||
ports.map((port) => {
|
||||
const key = `${container.name}-port-${port.containerPort}-${port.protocol}`
|
||||
return(
|
||||
<PodContainerPort pod={pod} port={port} key={key}/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
48
src/renderer/components/input/input.validators_test.ts
Normal file
48
src/renderer/components/input/input.validators_test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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`
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,3 +20,4 @@ export * from './formatDuration'
|
||||
export * from './isReactNode'
|
||||
export * from './convertMemory'
|
||||
export * from './convertCpu'
|
||||
export * from './metricUnitsToNumber'
|
||||
|
||||
10
src/renderer/utils/metricUnitsToNumber.ts
Normal file
10
src/renderer/utils/metricUnitsToNumber.ts
Normal file
@ -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)
|
||||
)
|
||||
}
|
||||
15
src/renderer/utils/metricUnitsToNumber_test.ts
Normal file
15
src/renderer/utils/metricUnitsToNumber_test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user