cleanup Lens repo with tighter linting

Signed-off-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
Sebastian Malton committed 2020-07-09 17:00:23 -04:00
1 parent e468105143
commit b1ff34879a
470 files changed
+8447 -7443

No files matched your search

@@ -1,21 +1,25 @@
import React, { useContext } from "react";
import { t } from "@lingui/macro";
import { IPodMetrics } from "../../api/endpoints";
import { PodMetricsData } from "../../api/endpoints";
import { BarChart, cpuOptions, memoryOptions } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { ResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { _i18n } from "../../i18n";
import { themeStore } from "../../theme.store";
type IContext = IResourceMetricsValue<any, { metrics: IPodMetrics }>;
type IContext = ResourceMetricsValue<any, { metrics: PodMetricsData }>;
export const ContainerCharts = () => {
export const ContainerCharts = (): JSX.Element => {
const { params: { metrics }, tabId } = useContext<IContext>(ResourceMetricsContext);
const { chartCapacityColor } = themeStore.activeTheme.colors;
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
if (!metrics) {
return null;
}
if (isMetricsEmpty(metrics)) {
return <NoMetrics/>;
}
const values = Object.values(metrics)
.map(normalizeMetrics)
@@ -100,4 +104,4 @@ export const ContainerCharts = () => {
data={{ datasets: datasets[tabId] }}
/>
);
}
};
@@ -1,2 +1,2 @@
export * from "./pods"
export * from "./pod-details"
export * from "./pods";
export * from "./pod-details";
@@ -1,31 +1,35 @@
import React, { useContext } from "react";
import { t, Trans } from "@lingui/macro";
import { observer } from "mobx-react";
import { IPodMetrics } from "../../api/endpoints";
import { PodMetricsData } from "../../api/endpoints";
import { BarChart, cpuOptions, memoryOptions } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { ResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { _i18n } from "../../i18n";
import { WorkloadKubeObject } from "../../api/workload-kube-object";
import { themeStore } from "../../theme.store";
export const podMetricTabs = [
<Trans>CPU</Trans>,
<Trans>Memory</Trans>,
<Trans>Network</Trans>,
<Trans>Filesystem</Trans>,
<Trans key="cpu">CPU</Trans>,
<Trans key="memory">Memory</Trans>,
<Trans key="netowork">Network</Trans>,
<Trans key="filesystem">Filesystem</Trans>,
];
type IContext = IResourceMetricsValue<WorkloadKubeObject, { metrics: IPodMetrics }>;
type IContext = ResourceMetricsValue<WorkloadKubeObject, { metrics: PodMetricsData }>;
export const PodCharts = observer(() => {
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
const { chartCapacityColor } = themeStore.activeTheme.colors;
const id = object.getId();
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
if (!metrics) {
return null;
}
if (isMetricsEmpty(metrics)) {
return <NoMetrics/>;
}
const options = tabId == 0 ? cpuOptions : memoryOptions;
const values = Object.values(metrics)
@@ -128,4 +132,4 @@ export const PodCharts = observer(() => {
data={{ datasets: datasets[tabId] }}
/>
);
})
});
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react";
import flatten from "lodash/flatten";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { IPodContainer, Secret } from "../../api/endpoints";
import { PodContainer, Secret } from "../../api/endpoints";
import { DrawerItem } from "../drawer";
import { autorun } from "mobx";
import { secretsStore } from "../+config-secrets/secrets.store";
@@ -13,45 +13,45 @@ import { Icon } from "../icon";
import { base64, cssNames } from "../../utils";
interface Props {
container: IPodContainer;
container: PodContainer;
namespace: string;
}
export const ContainerEnvironment = observer((props: Props) => {
const { container: { env, envFrom }, namespace } = props
const { container: { env, envFrom }, namespace } = props;
useEffect(
() =>
autorun(() => {
env && env.forEach(variable => {
const { valueFrom } = variable
const { valueFrom } = variable;
if (valueFrom && valueFrom.configMapKeyRef) {
configMapsStore.load({ name: valueFrom.configMapKeyRef.name, namespace })
configMapsStore.load({ name: valueFrom.configMapKeyRef.name, namespace });
}
})
});
envFrom && envFrom.forEach(item => {
const { configMapRef } = item
const { configMapRef } = item;
if (configMapRef && configMapRef.name) {
configMapsStore.load({ name: configMapRef.name, namespace })
configMapsStore.load({ name: configMapRef.name, namespace });
}
})
});
}),
[]
)
);
const renderEnv = () => {
const renderEnv = (): JSX.Element[] => {
return env.map(variable => {
const { name, value, valueFrom } = variable
let secretValue = null
const { name, value, valueFrom } = variable;
let secretValue = null;
if (value) {
secretValue = value
secretValue = value;
}
if (valueFrom) {
const { fieldRef, secretKeyRef, configMapKeyRef } = valueFrom
const { fieldRef, secretKeyRef, configMapKeyRef } = valueFrom;
if (fieldRef) {
const { apiVersion, fieldPath } = fieldRef
secretValue = `fieldRef(${apiVersion}:${fieldPath})`
const { apiVersion, fieldPath } = fieldRef;
secretValue = `fieldRef(${apiVersion}:${fieldPath})`;
}
if (secretKeyRef) {
secretValue = (
@@ -59,14 +59,14 @@ export const ContainerEnvironment = observer((props: Props) => {
reference={secretKeyRef}
namespace={namespace}
/>
)
);
}
if (configMapKeyRef) {
const { name, key } = configMapKeyRef
const configMap = configMapsStore.getByName(name, namespace)
const { name, key } = configMapKeyRef;
const configMap = configMapsStore.getByName(name, namespace);
secretValue = configMap ?
configMap.data[key] :
`configMapKeyRef(${name}${key})`
`configMapKeyRef(${name}${key})`;
}
}
@@ -74,31 +74,35 @@ export const ContainerEnvironment = observer((props: Props) => {
<div className="variable" key={name}>
<span className="var-name">{name}</span>: {secretValue}
</div>
)
})
}
);
});
};
const renderEnvFrom = () => {
const renderEnvFrom = (): JSX.Element[] => {
const envVars = envFrom.map(vars => {
if (!vars.configMapRef || !vars.configMapRef.name) return
const configMap = configMapsStore.getByName(vars.configMapRef.name, namespace)
if (!configMap) return
if (!vars.configMapRef || !vars.configMapRef.name) {
return;
}
const configMap = configMapsStore.getByName(vars.configMapRef.name, namespace);
if (!configMap) {
return;
}
return Object.entries(configMap.data).map(([name, value]) => (
<div className="variable" key={name}>
<span className="var-name">{name}</span>: {value}
</div>
))
})
return flatten(envVars)
}
));
});
return flatten(envVars);
};
return (
<DrawerItem name={<Trans>Environment</Trans>} className="ContainerEnvironment">
{env && renderEnv()}
{envFrom && renderEnvFrom()}
</DrawerItem>
)
})
);
});
interface SecretKeyProps {
reference: {
@@ -108,20 +112,20 @@ interface SecretKeyProps {
namespace: string;
}
const SecretKey = (props: SecretKeyProps) => {
const { reference: { name, key }, namespace } = props
const [loading, setLoading] = useState(false)
const [secret, setSecret] = useState<Secret>()
const SecretKey = (props: SecretKeyProps): JSX.Element => {
const { reference: { name, key }, namespace } = props;
const [loading, setLoading] = useState(false);
const [secret, setSecret] = useState<Secret>();
const showKey = async () => {
setLoading(true)
const showKey = async (): Promise<void> => {
setLoading(true);
const secret = await secretsStore.load({ name, namespace });
setLoading(false)
setSecret(secret)
}
setLoading(false);
setSecret(secret);
};
if (secret?.data?.[key]) {
return <>{base64.decode(secret.data[key])}</>
return <>{base64.decode(secret.data[key])}</>;
}
return (
@@ -134,5 +138,5 @@ const SecretKey = (props: SecretKeyProps) => {
onClick={showKey}
/>
</>
)
}
);
};
@@ -11,11 +11,13 @@ interface Props {
}
export class PodDetailsAffinities extends React.Component<Props> {
render() {
const { workload } = this.props
const affinitiesNum = workload.getAffinityNumber()
const affinities = workload.getAffinity()
if (!affinitiesNum) return null
render(): JSX.Element {
const { workload } = this.props;
const affinitiesNum = workload.getAffinityNumber();
const affinities = workload.getAffinity();
if (!affinitiesNum) {
return null;
}
return (
<DrawerItem name={<Trans>Affinities</Trans>} className="PodDetailsAffinities">
<DrawerParamToggler label={affinitiesNum}>
@@ -29,6 +31,6 @@ export class PodDetailsAffinities extends React.Component<Props> {
</div>
</DrawerParamToggler>
</DrawerItem>
)
);
}
}
@@ -1,39 +1,41 @@
import "./pod-details-container.scss"
import "./pod-details-container.scss";
import * as React from "react";
import { t, Trans } from "@lingui/macro";
import { IPodContainer, Pod } from "../../api/endpoints";
import { PodContainer, Pod } from "../../api/endpoints";
import { DrawerItem } from "../drawer";
import { cssNames } from "../../utils";
import { StatusBrick } from "../status-brick";
import { Badge } from "../badge";
import { ContainerEnvironment } from "./pod-container-env";
import { ResourceMetrics } from "../resource-metrics";
import { IMetrics } from "../../api/endpoints/metrics.api";
import { Metrics } from "../../api/endpoints/metrics.api";
import { ContainerCharts } from "./container-charts";
import { _i18n } from "../../i18n";
interface Props {
pod: Pod;
container: IPodContainer;
metrics?: { [key: string]: IMetrics };
container: PodContainer;
metrics?: { [key: string]: Metrics };
}
export class PodDetailsContainer extends React.Component<Props> {
render() {
const { pod, container, metrics } = this.props
if (!pod || !container) return null
const { name, image, imagePullPolicy, ports, volumeMounts, command, args } = container
const status = pod.getContainerStatuses().find(status => status.name === container.name)
const state = status ? Object.keys(status.state)[0] : ""
const ready = status ? status.ready : ""
const liveness = pod.getLivenessProbe(container)
const readiness = pod.getReadinessProbe(container)
const isInitContainer = !!pod.getInitContainers().find(c => c.name == name);
render(): JSX.Element {
const { pod, container, metrics } = this.props;
if (!pod || !container) {
return null;
}
const { name, image, imagePullPolicy, ports, volumeMounts, command, args } = container;
const status = pod.getContainerStatuses().find(status => status.name === container.name);
const state = status ? Object.keys(status.state)[0] : "";
const ready = status ? status.ready : "";
const liveness = pod.getLivenessProbe(container);
const readiness = pod.getReadinessProbe(container);
const isInitContainer = !!pod.spec.initContainers.find(c => c.name == name);
const metricTabs = [
<Trans>CPU</Trans>,
<Trans>Memory</Trans>,
<Trans>Filesystem</Trans>,
<Trans key="cpu">CPU</Trans>,
<Trans key="memory">Memory</Trans>,
<Trans key="filesystem">Filesystem</Trans>,
];
return (
<div className="PodDetailsContainer">
@@ -66,12 +68,12 @@ export class PodDetailsContainer extends React.Component<Props> {
{
ports.map(port => {
const { name, containerPort, protocol } = port;
const key = `${container.name}-port-${containerPort}-${protocol}`
const key = `${container.name}-port-${containerPort}-${protocol}`;
return (
<div key={key}>
{name ? name + ': ' : ''}{containerPort}/{protocol}
</div>
)
);
})
}
</DrawerItem>
@@ -87,7 +89,7 @@ export class PodDetailsContainer extends React.Component<Props> {
<span className="mount-path">{mountPath}</span>
<span className="mount-from">from {name} ({readOnly ? 'ro' : 'rw'})</span>
</React.Fragment>
)
);
})
}
</DrawerItem>
@@ -122,6 +124,6 @@ export class PodDetailsContainer extends React.Component<Props> {
</DrawerItem>
}
</div>
)
);
}
}
@@ -6,7 +6,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { podsStore } from "./pods.store";
import { Pod } from "../../api/endpoints";
import { autobind, bytesToUnits, cssNames, interval, prevDefault } from "../../utils";
import { autobind, bytesToUnits, cssNames, IntervalManager, prevDefault } from "../../utils";
import { KubeEventIcon } from "../+events/kube-event-icon";
import { LineProgress } from "../line-progress";
import { KubeObject } from "../../api/kube-object";
@@ -40,36 +40,38 @@ export class PodDetailsList extends React.Component<Props> {
showTitle: true
}
private metricsWatcher = interval(120, () => {
private metricsWatcher = new IntervalManager(120, () => {
podsStore.loadKubeMetrics(this.props.owner.getNs());
});
private sortingCallbacks = {
[sortBy.name]: (pod: Pod) => pod.getName(),
[sortBy.namespace]: (pod: Pod) => pod.getNs(),
[sortBy.cpu]: (pod: Pod) => podsStore.getPodKubeMetrics(pod).cpu,
[sortBy.memory]: (pod: Pod) => podsStore.getPodKubeMetrics(pod).memory,
[sortBy.name]: (pod: Pod): string => pod.getName(),
[sortBy.namespace]: (pod: Pod): string => pod.getNs(),
[sortBy.cpu]: (pod: Pod): number => podsStore.getPodKubeMetrics(pod).cpu,
[sortBy.memory]: (pod: Pod): number => podsStore.getPodKubeMetrics(pod).memory,
}
componentDidMount() {
componentDidMount(): void {
this.metricsWatcher.start(true);
disposeOnUnmount(this, [
reaction(() => this.props.owner, () => this.metricsWatcher.restart(true))
])
]);
}
componentWillUnmount() {
componentWillUnmount(): void {
this.metricsWatcher.stop();
}
renderCpuUsage(id: string, usage: number) {
renderCpuUsage(id: string, usage: number): (JSX.Element | string | number) {
const { maxCpu } = this.props;
const value = usage.toFixed(3);
const tooltip = (
<p><Trans>CPU</Trans>: {Math.ceil(usage * 100) / maxCpu}%<br/>{usage.toFixed(3)}</p>
);
if (!maxCpu) {
if (parseFloat(value) === 0) return 0;
if (parseFloat(value) === 0) {
return 0;
}
return value;
}
return (
@@ -80,12 +82,14 @@ export class PodDetailsList extends React.Component<Props> {
);
}
renderMemoryUsage(id: string, usage: number) {
renderMemoryUsage(id: string, usage: number): (JSX.Element | number | string) {
const { maxMemory } = this.props;
const tooltip = (
<p><Trans>Memory</Trans>: {Math.ceil(usage * 100 / maxMemory)}%<br/>{bytesToUnits(usage, 3)}</p>
);
if (!maxMemory) return usage ? bytesToUnits(usage) : 0;
if (!maxMemory) {
return usage ? bytesToUnits(usage) : 0;
}
return (
<LineProgress
max={maxMemory} value={usage}
@@ -95,7 +99,7 @@ export class PodDetailsList extends React.Component<Props> {
}
@autobind()
getTableRow(uid: string) {
getTableRow(uid: string): JSX.Element {
const { pods } = this.props;
const pod = pods.find(pod => pod.getId() == uid);
const metrics = podsStore.getPodKubeMetrics(pod);
@@ -116,13 +120,17 @@ export class PodDetailsList extends React.Component<Props> {
);
}
render() {
render(): JSX.Element {
const { pods, showTitle } = this.props;
const virtual = pods.length > 100;
if (!pods.length && !podsStore.isLoaded) return (
<div className="PodDetailsList flex justify-center"><Spinner/></div>
);
if (!pods.length) return null;
if (!pods.length && !podsStore.isLoaded) {
return (
<div className="PodDetailsList flex justify-center"><Spinner/></div>
);
}
if (!pods.length) {
return null;
}
return (
<div className="PodDetailsList flex column">
{showTitle && <DrawerTitle title={<Trans>Pods</Trans>}/>}
@@ -26,7 +26,7 @@ export class PodDetailsSecrets extends Component<Props> {
);
});
render() {
render(): JSX.Element {
return (
<div className="PodDetailsSecrets">
{
@@ -9,10 +9,12 @@ interface Props {
}
export class PodDetailsStatuses extends React.Component<Props> {
render() {
const { pods } = this.props
if (!pods.length) return null
const statuses = countBy(pods.map(pod => pod.getStatus()))
render(): JSX.Element {
const { pods } = this.props;
if (!pods.length) {
return null;
}
const statuses = countBy(pods.map(pod => pod.getStatus()));
return (
<div className="PodDetailsStatuses">
{
@@ -23,6 +25,6 @@ export class PodDetailsStatuses extends React.Component<Props> {
))
}
</div>
)
);
}
}
@@ -9,16 +9,18 @@ interface Props {
}
export class PodDetailsTolerations extends React.Component<Props> {
render() {
const { workload } = this.props
const tolerations = workload.getTolerations()
if (!tolerations.length) return null
render(): JSX.Element {
const { workload } = this.props;
const tolerations = workload.getTolerations();
if (!tolerations.length) {
return null;
}
return (
<DrawerItem name={<Trans>Tolerations</Trans>} className="PodDetailsTolerations">
<DrawerParamToggler label={tolerations.length}>
{
tolerations.map((toleration, index) => {
const { key, operator, effect, tolerationSeconds } = toleration
const { key, operator, effect, tolerationSeconds } = toleration;
return (
<div className="toleration" key={index}>
<DrawerItem name={<Trans>Key</Trans>}>{key}</DrawerItem>
@@ -26,11 +28,11 @@ export class PodDetailsTolerations extends React.Component<Props> {
{effect && <DrawerItem name={<Trans>Effect</Trans>}>{effect}</DrawerItem>}
{!!tolerationSeconds && <DrawerItem name={<Trans>Effect</Trans>}>{tolerationSeconds}</DrawerItem>}
</div>
)
);
})
}
</DrawerParamToggler>
</DrawerItem>
)
);
}
}
@@ -1,4 +1,4 @@
import "./pod-details.scss"
import "./pod-details.scss";
import * as React from "react";
import kebabCase from "lodash/kebabCase";
@@ -6,7 +6,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { Link } from "react-router-dom";
import { autorun, observable, reaction, toJS } from "mobx";
import { Trans } from "@lingui/macro";
import { IPodMetrics, nodesApi, Pod, podsApi, pvcApi, configMapApi } from "../../api/endpoints";
import { PodMetricsData, nodesApi, Pod, podsApi, pvcApi, configMapApi } from "../../api/endpoints";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
import { autobind, cssNames, interval } from "../../utils";
@@ -22,7 +22,6 @@ import { getDetailsUrl } from "../../navigation";
import { KubeObjectDetailsProps } from "../kube-object";
import { getItemMetrics } from "../../api/endpoints/metrics.api";
import { PodCharts, podMetricTabs } from "./pod-charts";
import { lookupApiLink } from "../../api/kube-api";
import { apiManager } from "../../api/api-manager";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
@@ -31,11 +30,11 @@ interface Props extends KubeObjectDetailsProps<Pod> {
@observer
export class PodDetails extends React.Component<Props> {
@observable containerMetrics: IPodMetrics;
@observable containerMetrics: PodMetricsData;
private watcher = interval(60, () => this.loadMetrics());
componentDidMount() {
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(() => {
this.containerMetrics = null;
@@ -48,30 +47,31 @@ export class PodDetails extends React.Component<Props> {
this.watcher.start();
}
componentWillUnmount() {
componentWillUnmount(): void {
podsStore.reset();
}
@autobind()
async loadMetrics() {
async loadMetrics(): Promise<void> {
const { object: pod } = this.props;
this.containerMetrics = await podsStore.loadContainerMetrics(pod);
}
render() {
render(): JSX.Element {
const { object: pod } = this.props;
if (!pod) return null;
if (!pod) {
return null;
}
const { status, spec } = pod;
const { conditions, podIP } = status;
const { nodeName } = spec;
const nodeSelector = pod.getNodeSelectors();
const volumes = pod.getVolumes();
const labels = pod.getLabels();
const volumes = pod.spec.volumes;
const metrics = podsStore.metrics;
return (
<div className="PodDetails">
<ResourceMetrics
loader={() => podsStore.loadMetrics(pod)}
loader={(): Promise<void> => podsStore.loadMetrics(pod)}
tabs={podMetricTabs} object={pod} params={{ metrics }}
>
<PodCharts/>
@@ -91,10 +91,10 @@ export class PodDetails extends React.Component<Props> {
{podIP}
</DrawerItem>
<DrawerItem name={<Trans>Priority Class</Trans>}>
{pod.getPriorityClassName()}
{pod.spec.priorityClassName}
</DrawerItem>
<DrawerItem name={<Trans>QoS Class</Trans>}>
{pod.getQosClass()}
{pod.status.qosClass}
</DrawerItem>
{conditions &&
<DrawerItem name={<Trans>Conditions</Trans>} className="conditions" labelsOnly>
@@ -108,7 +108,7 @@ export class PodDetails extends React.Component<Props> {
className={cssNames({ disabled: status === "False" })}
tooltip={<Trans>Last transition time: {lastTransitionTime}</Trans>}
/>
)
);
})
}
</DrawerItem>
@@ -131,17 +131,17 @@ export class PodDetails extends React.Component<Props> {
</DrawerItem>
)}
{pod.getInitContainers() && pod.getInitContainers().length > 0 &&
{pod.spec.initContainers?.length > 0 &&
<DrawerTitle title={<Trans>Init Containers</Trans>}/>
}
{
pod.getInitContainers() && pod.getInitContainers().map(container => {
return <PodDetailsContainer key={container.name} pod={pod} container={container}/>
pod.spec.initContainers?.map((container): JSX.Element => {
return <PodDetailsContainer key={container.name} pod={pod} container={container}/>;
})
}
<DrawerTitle title={<Trans>Containers</Trans>}/>
{
pod.getContainers().map(container => {
pod.spec.containers.map((container): JSX.Element => {
const { name } = container;
const metrics = getItemMetrics(toJS(this.containerMetrics), name);
return (
@@ -151,7 +151,7 @@ export class PodDetails extends React.Component<Props> {
container={container}
metrics={metrics}
/>
)
);
})
}
@@ -161,7 +161,7 @@ export class PodDetails extends React.Component<Props> {
{volumes.map(volume => {
const claimName = volume.persistentVolumeClaim ? volume.persistentVolumeClaim.claimName : null;
const configMap = volume.configMap ? volume.configMap.name : null;
const type = Object.keys(volume)[1]
const type = Object.keys(volume)[1];
return (
<div key={volume.name} className="volume">
@@ -174,28 +174,28 @@ export class PodDetails extends React.Component<Props> {
</DrawerItem>
{ type == "configMap" && (
<div>
{configMap && (
<DrawerItem name={<Trans>Name</Trans>}>
<Link
to={getDetailsUrl(configMapApi.getUrl({
name: configMap,
namespace: pod.getNs(),
}))}>{configMap}
</Link>
</DrawerItem>
)}
{configMap && (
<DrawerItem name={<Trans>Name</Trans>}>
<Link
to={getDetailsUrl(configMapApi.getUrl({
name: configMap,
namespace: pod.getNs(),
}))}>{configMap}
</Link>
</DrawerItem>
)}
</div>
)}
{ type === "emptyDir" && (
<div>
{ volume.emptyDir.medium && (
<DrawerItem name={<Trans>Medium</Trans>}>
{volume.emptyDir.medium}
</DrawerItem>
<DrawerItem name={<Trans>Medium</Trans>}>
{volume.emptyDir.medium}
</DrawerItem>
)}
{ volume.emptyDir.sizeLimit && (
<DrawerItem name={<Trans>Size Limit</Trans>}>
{volume.emptyDir.sizeLimit}
{volume.emptyDir.sizeLimit}
</DrawerItem>
)}
</div>
@@ -213,16 +213,16 @@ export class PodDetails extends React.Component<Props> {
</DrawerItem>
)}
</div>
)
);
})}
</>
)}
<KubeEventDetails object={pod}/>
</div>
)
);
}
}
apiManager.registerViews(podsApi, {
Details: PodDetails
})
});
@@ -7,17 +7,17 @@ import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { IPodContainer, Pod, podsApi } from "../../api/endpoints";
import { PodContainer, Pod, podsApi } from "../../api/endpoints";
import { Icon } from "../icon";
import { Select, SelectOption } from "../select";
import { Spinner } from "../spinner";
import { cssNames, downloadFile, interval } from "../../utils";
import { cssNames, downloadFile, IntervalManager } from "../../utils";
import { default as AnsiUp } from "ansi_up";
import DOMPurify from "dompurify"
import DOMPurify from "dompurify";
interface IPodLogsDialogData {
interface PodLogsDialogData {
pod: Pod;
container?: IPodContainer;
container?: PodContainer;
}
interface Props extends Partial<DialogProps> {
@@ -26,32 +26,32 @@ interface Props extends Partial<DialogProps> {
@observer
export class PodLogsDialog extends React.Component<Props> {
@observable static isOpen = false;
@observable static data: IPodLogsDialogData = null;
@observable static data: PodLogsDialogData = null;
static open(pod: Pod, container?: IPodContainer) {
static open(pod: Pod, container?: PodContainer): void {
PodLogsDialog.isOpen = true;
PodLogsDialog.data = { pod, container };
}
static close() {
static close(): void {
PodLogsDialog.isOpen = false;
}
get data() {
get data(): PodLogsDialogData {
return PodLogsDialog.data;
}
private logsArea: HTMLDivElement;
private refresher = interval(5, () => this.load());
private containers: IPodContainer[] = []
private initContainers: IPodContainer[] = []
private refresher = new IntervalManager(5, () => this.load());
private containers: PodContainer[] = []
private initContainers: PodContainer[] = []
private lastLineIsShown = true; // used for proper auto-scroll content after refresh
private colorConverter = new AnsiUp();
@observable logs = ""; // latest downloaded logs for pod
@observable newLogs = ""; // new logs since dialog is open
@observable logsReady = false;
@observable selectedContainer: IPodContainer;
@observable selectedContainer: PodContainer;
@observable showTimestamps = true;
@observable tailLines = 1000;
@@ -62,31 +62,33 @@ export class PodLogsDialog extends React.Component<Props> {
{ label: 100000, value: 100000 },
]
onOpen = async () => {
onOpen = async (): Promise<void> => {
const { pod, container } = this.data;
this.containers = pod.getContainers();
this.initContainers = pod.getInitContainers();
this.containers = pod.spec.containers;
this.initContainers = pod.spec.initContainers;
this.selectedContainer = container || this.containers[0];
await this.load();
this.refresher.start();
}
onClose = () => {
onClose = (): void => {
this.resetLogs();
this.refresher.stop();
}
close = () => {
close = (): void => {
PodLogsDialog.close();
}
load = async () => {
if (!this.data) return;
load = async (): Promise<void> => {
if (!this.data) {
return;
}
const { pod } = this.data;
try {
// if logs already loaded, check the latest timestamp for getting updates only from this point
const logsTimestamps = this.getTimestamps(this.newLogs || this.logs);
let lastLogDate = new Date(0)
let lastLogDate = new Date(0);
if (logsTimestamps) {
lastLogDate = new Date(logsTimestamps.slice(-1)[0]);
lastLogDate.setSeconds(lastLogDate.getSeconds() + 1); // avoid duplicates from last second
@@ -101,27 +103,26 @@ export class PodLogsDialog extends React.Component<Props> {
});
if (!this.logs) {
this.logs = logs;
}
else if (logs) {
} else if (logs) {
this.newLogs = `${this.newLogs}\n${logs}`.trim();
}
} catch (error) {
this.logs = [
_i18n._(t`Failed to load logs: ${error.message}`),
_i18n._(t`Reason: ${error.reason} (${error.code})`),
].join("\n")
].join("\n");
}
this.logsReady = true;
}
reload = async () => {
reload = async (): Promise<void> => {
this.resetLogs();
this.refresher.stop();
await this.load();
this.refresher.start();
}
componentDidUpdate() {
componentDidUpdate(): void {
// scroll logs only when it's already in the end,
// otherwise it can interrupt reading by jumping after loading new logs update
if (this.logsArea && this.lastLineIsShown) {
@@ -129,83 +130,85 @@ export class PodLogsDialog extends React.Component<Props> {
}
}
onScroll = (evt: React.UIEvent<HTMLDivElement>) => {
onScroll = (evt: React.UIEvent<HTMLDivElement>): void => {
const logsArea = evt.currentTarget;
const { scrollHeight, clientHeight, scrollTop } = logsArea;
this.lastLineIsShown = clientHeight + scrollTop === scrollHeight;
};
getLogs() {
getLogs(): { logs: string; newLogs: string } {
const { logs, newLogs, showTimestamps } = this;
return {
logs: showTimestamps ? logs : this.removeTimestamps(logs),
newLogs: showTimestamps ? newLogs : this.removeTimestamps(newLogs),
}
};
}
getTimestamps(logs: string) {
getTimestamps(logs: string): RegExpMatchArray {
return logs.match(/^\d+\S+/gm);
}
removeTimestamps(logs: string) {
removeTimestamps(logs: string): string {
return logs.replace(/^\d+.*?\s/gm, "");
}
resetLogs() {
resetLogs(): void {
this.logs = "";
this.newLogs = "";
this.lastLineIsShown = true;
this.logsReady = false;
}
onContainerChange = (option: SelectOption) => {
onContainerChange = (option: SelectOption): void => {
this.selectedContainer = this.containers
.concat(this.initContainers)
.find(container => container.name === option.value);
this.reload();
}
onTailLineChange = (option: SelectOption) => {
onTailLineChange = (option: SelectOption): void => {
this.tailLines = option.value;
this.reload();
}
formatOptionLabel = (option: SelectOption) => {
formatOptionLabel = (option: SelectOption): JSX.Element | {} => {
const { value, label } = option;
return label || <><Icon small material="view_carousel"/> {value}</>;
}
toggleTimestamps = () => {
toggleTimestamps = (): void => {
this.showTimestamps = !this.showTimestamps;
}
downloadLogs = () => {
downloadLogs = (): void => {
const { logs, newLogs } = this.getLogs();
const fileName = this.selectedContainer.name + ".log";
const fileContents = logs + newLogs;
downloadFile(fileName, fileContents, "text/plain");
}
get containerSelectOptions() {
get containerSelectOptions(): React.ReactNode[] {
return [
{
label: _i18n._(t`Containers`),
options: this.containers.map(container => {
return { value: container.name }
return { value: container.name };
}),
},
{
label: _i18n._(t`Init Containers`),
options: this.initContainers.map(container => {
return { value: container.name }
return { value: container.name };
}),
}
];
}
renderControlsPanel() {
renderControlsPanel(): JSX.Element {
const { logsReady, showTimestamps } = this;
if (!logsReady) return;
if (!logsReady) {
return;
}
const timestamps = this.getTimestamps(this.logs + this.newLogs);
let from = "";
let to = "";
@@ -232,16 +235,16 @@ export class PodLogsDialog extends React.Component<Props> {
/>
</div>
</div>
)
);
}
renderLogs() {
renderLogs(): JSX.Element {
if (!this.logsReady) {
return <Spinner center/>
return <Spinner center/>;
}
const { logs, newLogs } = this.getLogs();
if (!logs && !newLogs) {
return <p className="no-logs"><Trans>There are no logs available for container.</Trans></p>
return <p className="no-logs"><Trans>There are no logs available for container.</Trans></p>;
}
return (
<>
@@ -256,7 +259,7 @@ export class PodLogsDialog extends React.Component<Props> {
);
}
render() {
render(): JSX.Element {
const { ...dialogProps } = this.props;
const { selectedContainer, tailLines } = this;
const podName = this.data ? this.data.pod.getName() : "";
@@ -296,12 +299,14 @@ export class PodLogsDialog extends React.Component<Props> {
</div>
{this.renderControlsPanel()}
</div>
<div className="logs-area" onScroll={this.onScroll} ref={e => this.logsArea = e}>
<div className="logs-area" onScroll={this.onScroll} ref={(e): void => {
this.logsArea = e;
}}>
{this.renderLogs()}
</div>
</WizardStep>
</Wizard>
</Dialog>
)
);
}
}
@@ -3,7 +3,7 @@ import "./pod-menu.scss";
import * as React from "react";
import { t, Trans } from "@lingui/macro";
import { MenuItem, SubMenu } from "../menu";
import { IPodContainer, Pod, nodesApi } from "../../api/endpoints";
import { PodContainer, Pod } from "../../api/endpoints";
import { Icon } from "../icon";
import { StatusBrick } from "../status-brick";
import { PodLogsDialog } from "./pod-logs-dialog";
@@ -17,18 +17,18 @@ interface Props extends KubeObjectMenuProps<Pod> {
}
export class PodMenu extends React.Component<Props> {
async execShell(container?: string) {
async execShell(container?: string): Promise<void> {
hideDetails();
const { object: pod } = this.props
const containerParam = container ? `-c ${container}` : ""
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`
const { object: pod } = this.props;
const containerParam = container ? `-c ${container}` : "";
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`;
if (window.navigator.platform !== "Win32") {
command = `exec ${command}`
command = `exec ${command}`;
}
if (pod.getSelectedNodeOs() === "windows") {
command = `${command} powershell`
command = `${command} powershell`;
} else {
command = `${command} sh -c "clear; (bash || ash || sh)"`
command = `${command} sh -c "clear; (bash || ash || sh)"`;
}
const shell = createTerminalTab({
@@ -41,14 +41,16 @@ export class PodMenu extends React.Component<Props> {
});
}
showLogs(container: IPodContainer) {
showLogs(container: PodContainer): void {
PodLogsDialog.open(this.props.object, container);
}
renderShellMenu() {
const { object: pod, toolbar } = this.props
renderShellMenu(): JSX.Element {
const { object: pod, toolbar } = this.props;
const containers = pod.getRunningContainers();
if (!containers.length) return;
if (!containers.length) {
return;
}
return (
<MenuItem onClick={prevDefault(() => this.execShell(containers[0].name))}>
<Icon svg="ssh" interactive={toolbar} title={_i18n._(t`Pod shell`)}/>
@@ -65,21 +67,23 @@ export class PodMenu extends React.Component<Props> {
<StatusBrick/>
{name}
</MenuItem>
)
);
})
}
</SubMenu>
</>
)}
</MenuItem>
)
);
}
renderLogsMenu() {
const { object: pod, toolbar } = this.props
renderLogsMenu(): JSX.Element {
const { object: pod, toolbar } = this.props;
const containers = pod.getAllContainers();
const statuses = pod.getContainerStatuses();
if (!containers.length) return;
if (!containers.length) {
return;
}
return (
<MenuItem onClick={prevDefault(() => this.showLogs(containers[0]))}>
<Icon material="subject" title={_i18n._(t`Logs`)} interactive={toolbar}/>
@@ -90,35 +94,35 @@ export class PodMenu extends React.Component<Props> {
<SubMenu>
{
containers.map(container => {
const { name } = container
const { name } = container;
const status = statuses.find(status => status.name === name);
const brick = status ? (
<StatusBrick
className={cssNames(Object.keys(status.state)[0], { ready: status.ready })}
/>
) : null
) : null;
return (
<MenuItem key={name} onClick={prevDefault(() => this.showLogs(container))} className="flex align-center">
{brick}
{name}
</MenuItem>
)
);
})
}
</SubMenu>
</>
)}
</MenuItem>
)
);
}
render() {
render(): JSX.Element {
const { ...menuProps } = this.props;
return (
<KubeObjectMenu {...menuProps} className="PodMenu">
{this.renderShellMenu()}
{this.renderLogsMenu()}
</KubeObjectMenu>
)
);
}
}
@@ -2,75 +2,86 @@ import countBy from "lodash/countBy";
import { action, observable } from "mobx";
import { KubeObjectStore } from "../../kube-object.store";
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
import { IPodMetrics, Pod, PodMetrics, podMetricsApi, podsApi } from "../../api/endpoints";
import { PodMetricsData, Pod, podMetricsApi, podsApi } from "../../api/endpoints";
import { PodMetrics } from "../../api/endpoints/pod-metrics.api";
import { WorkloadKubeObject } from "../../api/workload-kube-object";
import { apiManager } from "../../api/api-manager";
import { Dictionary } from "lodash";
import { Metrics } from "client/api/endpoints/metrics.api";
export interface PodKubeMetrics {
cpu: number;
memory: number;
}
@autobind()
export class PodsStore extends KubeObjectStore<Pod> {
api = podsApi;
@observable metrics: IPodMetrics = null;
@observable metrics: PodMetricsData<Metrics> = null;
@observable kubeMetrics = observable.array<PodMetrics>([]);
@action
async loadMetrics(pod: Pod) {
this.metrics = await podsApi.getMetrics([pod], pod.getNs());
async loadMetrics(pod: Pod): Promise<void> {
this.metrics = await this.api.getMetrics([pod], pod.getNs());
}
loadContainerMetrics(pod: Pod) {
return podsApi.getMetrics([pod], pod.getNs(), "container, namespace");
loadContainerMetrics(pod: Pod): Promise<PodMetricsData<Metrics>> {
return this.api.getMetrics([pod], pod.getNs(), "container, namespace");
}
async loadKubeMetrics(namespace?: string) {
async loadKubeMetrics(namespace?: string): Promise<void> {
const metrics = await podMetricsApi.list({ namespace });
this.kubeMetrics.replace(metrics);
}
getPodsByOwner(workload: WorkloadKubeObject): Pod[] {
if (!workload) return [];
if (!workload) {
return [];
}
return this.items.filter(pod => {
const owners = pod.getOwnerRefs()
if (!owners.length) return
return owners.find(owner => owner.uid === workload.getId())
})
}
getPodsByNode(node: string) {
if (!this.isLoaded) return []
return this.items.filter(pod => pod.spec.nodeName === node)
}
getStatuses(pods: Pod[]) {
return countBy(pods.map(pod => pod.getStatus()))
}
getPodKubeMetrics(pod: Pod) {
const containers = pod.getContainers();
const empty = { cpu: 0, memory: 0 };
const metrics = this.kubeMetrics.find(metric => {
return [
metric.getName() === pod.getName(),
metric.getNs() === pod.getNs()
].every(v => v);
const owners = pod.getOwnerRefs();
if (!owners.length) {
return;
}
return owners.find(owner => owner.uid === workload.getId());
});
if (!metrics) return empty;
}
getPodsByNode(node: string): Pod[] {
if (!this.isLoaded) {
return [];
}
return this.items.filter(pod => pod.spec.nodeName === node);
}
getStatuses(pods: Pod[]): Dictionary<number> {
return countBy(pods.map(pod => pod.getStatus()));
}
getPodKubeMetrics(pod: Pod): PodKubeMetrics {
const containers = pod.spec.containers;
const empty = { cpu: 0, memory: 0 };
const metrics = this.kubeMetrics.find(metric => metric.getName() === pod.getName() && metric.getNs() === pod.getNs());
if (!metrics) {
return empty;
}
return containers.reduce((total, container) => {
const metric = metrics.containers.find(item => item.name == container.name);
let cpu = "0"
let memory = "0"
let cpu = "0";
let memory = "0";
if (metric && metric.usage) {
cpu = metric.usage.cpu || "0"
memory = metric.usage.memory || "0"
cpu = metric.usage.cpu || "0";
memory = metric.usage.memory || "0";
}
return {
cpu: total.cpu + cpuUnitsToNumber(cpu),
memory: total.memory + unitsToBytes(memory)
}
};
}, empty);
}
reset() {
reset(): void {
this.metrics = null;
}
}
@@ -1,4 +1,4 @@
import "./pods.scss"
import "./pods.scss";
import React, { Fragment } from "react";
import { observer } from "mobx-react";
@@ -7,7 +7,7 @@ import { Trans } from "@lingui/macro";
import { podsStore } from "./pods.store";
import { RouteComponentProps } from "react-router";
import { volumeClaimStore } from "../+storage-volume-claims/volume-claim.store";
import { IPodsRouteParams } from "../+workloads";
import { PodsRouteParams } from "../+workloads";
import { eventStore } from "../+events/event.store";
import { KubeObjectListLayout } from "../kube-object";
import { Pod, podsApi } from "../../api/endpoints";
@@ -34,57 +34,57 @@ enum sortBy {
status = "status",
}
interface Props extends RouteComponentProps<IPodsRouteParams> {
interface Props extends RouteComponentProps<PodsRouteParams> {
}
@observer
export class Pods extends React.Component<Props> {
renderContainersStatus(pod: Pod) {
return pod.getContainerStatuses().map(containerStatus => {
const { name, state, ready } = containerStatus;
const tooltip = (
<TooltipContent tableView>
{Object.keys(state).map(status => (
<Fragment key={status}>
<div className="title">
{name} <span className="text-secondary">({status}{ready ? ", ready" : ""})</span>
</div>
{toPairs(state[status]).map(([name, value]) => (
<div key={name} className="flex gaps align-center">
<div className="name">{startCase(name)}</div>
<div className="value">{value}</div>
renderContainersStatus(pod: Pod): JSX.Element[] {
return pod.getContainerStatuses()
.map(({ name, state, ready }) => {
const tooltip = (
<TooltipContent tableView>
{Object.keys(state).map(status => (
<Fragment key={status}>
<div className="title">
{name} <span className="text-secondary">({status}{ready ? ", ready" : ""})</span>
</div>
))}
</Fragment>
))}
</TooltipContent>
);
return (
<Fragment key={name}>
<StatusBrick className={cssNames(state, { ready })} tooltip={tooltip}/>
</Fragment>
)
});
{toPairs(state[status]).map(([name, value]) => (
<div key={name} className="flex gaps align-center">
<div className="name">{startCase(name)}</div>
<div className="value">{value}</div>
</div>
))}
</Fragment>
))}
</TooltipContent>
);
return (
<Fragment key={name}>
<StatusBrick className={cssNames(state, { ready })} tooltip={tooltip}/>
</Fragment>
);
});
}
render() {
render(): JSX.Element {
return (
<KubeObjectListLayout
className="Pods" store={podsStore}
dependentStores={[volumeClaimStore, eventStore]}
sortingCallbacks={{
[sortBy.name]: (pod: Pod) => pod.getName(),
[sortBy.namespace]: (pod: Pod) => pod.getNs(),
[sortBy.containers]: (pod: Pod) => pod.getContainers().length,
[sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(),
[sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
[sortBy.qos]: (pod: Pod) => pod.getQosClass(),
[sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp,
[sortBy.status]: (pod: Pod) => pod.getStatusMessage(),
[sortBy.name]: (pod: Pod): string => pod.getName(),
[sortBy.namespace]: (pod: Pod): string => pod.getNs(),
[sortBy.containers]: (pod: Pod): number => pod.spec.containers.length,
[sortBy.restarts]: (pod: Pod): number => pod.getRestartsCount(),
[sortBy.owners]: (pod: Pod): string[] => pod.getOwnerRefs().map(ref => ref.kind),
[sortBy.qos]: (pod: Pod): string => pod.status.qosClass,
[sortBy.age]: (pod: Pod): string => pod.metadata.creationTimestamp,
[sortBy.status]: (pod: Pod): string => pod.getStatusMessage(),
}}
searchFilters={[
(pod: Pod) => pod.getSearchFields(),
(pod: Pod) => pod.getStatusMessage(),
(pod: Pod): string[] => pod.getSearchFields(),
(pod: Pod): string => pod.getStatusMessage(),
]}
renderHeaderTitle={<Trans>Pods</Trans>}
renderTableHeader={[
@@ -98,7 +98,7 @@ export class Pods extends React.Component<Props> {
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
]}
renderTableContents={(pod: Pod) => [
renderTableContents={(pod: Pod): (string | number | JSX.Element[] | JSX.Element | React.ReactNode)[] => [
pod.getName(),
pod.hasIssues() && <KubeEventIcon object={pod}/>,
pod.getNs(),
@@ -111,20 +111,20 @@ export class Pods extends React.Component<Props> {
<Link key={name} to={detailsLink} className="owner" onClick={stopPropagation}>
{kind}
</Link>
)
);
}),
pod.getQosClass(),
pod.status.qosClass,
pod.getAge(),
{ title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) }
]}
renderItemMenu={(item: Pod) => {
return <PodMenu object={item}/>
renderItemMenu={(item: Pod): JSX.Element => {
return <PodMenu object={item}/>;
}}
/>
)
);
}
}
apiManager.registerViews(podsApi, {
Menu: PodMenu,
})
});