mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge remote-tracking branch 'origin/vue_react_migration' into views_management_refactoring
# Conflicts: # src/renderer/components/cluster-manager/cluster-manager.scss
This commit is contained in:
commit
312fdfa4d3
@ -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: {
|
||||
|
||||
@ -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(
|
||||
<p>
|
||||
<Trans>
|
||||
<b>Your browser does not support all Lens features. </b>{" "}
|
||||
Please consider using another browser.
|
||||
</Trans>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<string>[] = [
|
||||
{ value: "", label: "Auto detect" },
|
||||
@ -17,6 +18,52 @@ interface Props {
|
||||
|
||||
@observer
|
||||
export class ClusterPrometheusSetting extends React.Component<Props> {
|
||||
@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<Props> {
|
||||
<a href="https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md" target="_blank">guide</a>{" "}
|
||||
for possible configuration changes.
|
||||
</p>
|
||||
<p>Prometheus installation method.</p>
|
||||
<Select
|
||||
value={this.props.cluster.preferences.prometheusProvider?.type || ""}
|
||||
value={this.provider}
|
||||
onChange={({value}) => {
|
||||
const provider = {
|
||||
prometheusProvider: {
|
||||
type: value
|
||||
}
|
||||
}
|
||||
merge(this.props.cluster.preferences, provider);
|
||||
this.provider = value;
|
||||
this.onSaveProvider();
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
<span className="hint">What query format is used to fetch metrics from Prometheus</span>
|
||||
{this.canEditPrometheusPath && (
|
||||
<>
|
||||
<p>Prometheus service address.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={this.path}
|
||||
onChange={(value) => this.path = value}
|
||||
onBlur={this.onSavePath}
|
||||
placeholder="<namespace>/<service>:<port>"
|
||||
/>
|
||||
<span className="hint">
|
||||
An address to an existing Prometheus installation{" "}
|
||||
({'<namespace>/<service>:<port>'}). Lens tries to auto-detect address if left empty.
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -98,7 +98,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
|
||||
return (
|
||||
<KubeObjectMenu {...props}>
|
||||
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
|
||||
<Icon material="control_camera" title={_i18n._(t`Scale`)} interactive={toolbar}/>
|
||||
<Icon material="open_with" title={_i18n._(t`Scale`)} interactive={toolbar}/>
|
||||
<span className="title"><Trans>Scale</Trans></span>
|
||||
</MenuItem>
|
||||
</KubeObjectMenu>
|
||||
|
||||
@ -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}/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ export class PodLogsDialog extends React.Component<Props> {
|
||||
tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)}
|
||||
/>
|
||||
<Icon
|
||||
material="save_alt"
|
||||
material="get_app"
|
||||
onClick={this.downloadLogs}
|
||||
tooltip={_i18n._(t`Save`)}
|
||||
/>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
#lens-views {
|
||||
grid-area: main;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
&.active {
|
||||
z-index: 1;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
|
||||
@ -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