1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

chore: Fixup remaining type errors from core

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-05-04 16:32:43 -04:00
parent 894553cdea
commit 271f8860e2
72 changed files with 345 additions and 278 deletions

View File

@ -239,8 +239,8 @@ describe("kube helpers", () => {
assert(result.isOk === true);
expect(result.value.getCurrentContext()).toBe("minikube");
expect(result.value.contexts.length).toBe(2);
expect(result.value.contexts[0].name).toBe("minikube");
expect(result.value.contexts[1].name).toBe("cluster-3");
expect(result.value.contexts[0]?.name).toBe("minikube");
expect(result.value.contexts[1]?.name).toBe("cluster-3");
});
});
});

View File

@ -79,17 +79,23 @@ export class CatalogCategoryRegistry {
}
hasCategoryForEntity({ kind, apiVersion }: CatalogEntityData & CatalogEntityKindData): boolean {
const splitApiVersion = apiVersion.split("/");
const group = splitApiVersion[0];
const [group] = apiVersion.split("/");
if (!group) {
return false;
}
return this.groupKinds.get(group)?.has(kind) ?? false;
}
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData): T | undefined {
const splitApiVersion = data.apiVersion.split("/");
const group = splitApiVersion[0];
getCategoryForEntity<T extends CatalogCategory>({ apiVersion, kind }: CatalogEntityData & CatalogEntityKindData): T | undefined {
const [group] = apiVersion.split("/");
return this.getForGroupKind(group, data.kind);
if (!group) {
return undefined;
}
return this.getForGroupKind(group, kind);
}
getByName(name: string) {

View File

@ -5,22 +5,22 @@
import type { CatalogEntity } from "./catalog-entity";
import GraphemeSplitter from "grapheme-splitter";
import { hasOwnProperty, hasTypedProperty, isObject, isString, iter } from "@k8slens/utilities";
import { hasLengthAtLeast, hasOwnProperty, hasTypedProperty, isObject, isString, iter } from "@k8slens/utilities";
function getNameParts(name: string): string[] {
function getNameParts(name: string): [string, ...string[]] {
const byWhitespace = name.split(/\s+/);
if (byWhitespace.length > 1) {
if (hasLengthAtLeast(byWhitespace, 1)) {
return byWhitespace;
}
const byDashes = name.split(/[-_]+/);
if (byDashes.length > 1) {
if (hasLengthAtLeast(byDashes, 1)) {
return byDashes;
}
return name.split(/@+/);
return name.split(/@+/) as [string, ...string[]];
}
export function limitGraphemeLengthOf(src: string, count: number): string {

View File

@ -9,7 +9,7 @@ import type { Stats } from "fs-extra";
import { lowerFirst } from "lodash/fp";
import statInjectable from "./stat.injectable";
export type ValidateDirectory = (path: string) => AsyncResult<undefined, string>;
export type ValidateDirectory = (path: string) => AsyncResult<void, string>;
function getUserReadableFileType(stats: Stats): string {
if (stats.isFile()) {
@ -46,7 +46,7 @@ const validateDirectoryInjectable = getInjectable({
const stats = await stat(path);
if (stats.isDirectory()) {
return { isOk: true, value: undefined };
return { isOk: true };
}
return { isOk: false, error: `the provided path is ${getUserReadableFileType(stats)} and not a directory.` };
@ -63,9 +63,7 @@ const validateDirectoryInjectable = getInjectable({
ENOTDIR: "a prefix of the provided path is not a directory.",
};
const humanReadableError = error.code
? humanReadableErrors[error.code]
: lowerFirst(String(error));
const humanReadableError = humanReadableErrors[String(error.code)] ?? lowerFirst(String(error));
return { isOk: false, error: humanReadableError };
}

View File

@ -83,7 +83,7 @@ describe("computeRuleDeclarations", () => {
},
});
expect(result[0].url).toBe("http://foo.bar/");
expect(result[0]?.url).toBe("http://foo.bar/");
});
it("given no tls entries, should format links as http://", () => {
@ -120,7 +120,7 @@ describe("computeRuleDeclarations", () => {
},
});
expect(result[0].url).toBe("http://foo.bar/");
expect(result[0]?.url).toBe("http://foo.bar/");
});
it("given some tls entries, should format links as https://", () => {
@ -159,6 +159,6 @@ describe("computeRuleDeclarations", () => {
},
});
expect(result[0].url).toBe("https://foo.bar/");
expect(result[0]?.url).toBe("https://foo.bar/");
});
});

View File

@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import type { RawHelmChart } from "../helm-charts.api";
import { HelmChart } from "../helm-charts.api";
import { isDefined } from "@k8slens/utilities";
import { hasLengthAtLeast, isDefined } from "@k8slens/utilities";
import apiBaseInjectable from "../../api-base.injectable";
export type RequestHelmCharts = () => Promise<HelmChart[]>;
@ -25,6 +25,7 @@ const requestHelmChartsInjectable = getInjectable({
return Object
.values(data)
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), new Array<RawHelmChart[]>())
.filter((charts): charts is [HelmChart, ...HelmChart[]] => hasLengthAtLeast(charts, 1))
.map(([chart]) => HelmChart.create(chart, { onError: "log" }))
.filter(isDefined);
};

View File

@ -19,7 +19,8 @@ const toSeparatedTupleUsing =
return [leftItem];
}
const rightItem = arr[index + 1];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const rightItem = arr[index + 1]!;
const separator = getSeparator(leftItem, rightItem);
return [leftItem, separator];

View File

@ -12,7 +12,7 @@ import type { ClusterId } from "../cluster-types";
*/
export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split(".");
const subDomains = host.split(":")[0]?.split(".") ?? [];
return subDomains.slice(-3, -2)[0]; // ClusterId or undefined
}

View File

@ -8,7 +8,7 @@ const _findComposite = <T>(currentLeftIds: string[], currentId: string, currentR
const [nextId, ...nextRightIds] = currentRightIds;
const nextLeftIds = [...currentLeftIds, currentId];
if (currentRightIds.length === 0 && composite.id === currentId) {
if (!nextId || composite.id === currentId) {
return composite;
}
@ -26,11 +26,11 @@ Node '${[...currentLeftIds, composite.id].join(" -> ")}' had only following chil
${composite.children.map((child) => child.id).join("\n")}`);
};
export const findComposite =
(...path: string[]) =>
export const findComposite = (...path: [string, ...string[]]) => (
<T>(composite: Composite<T>): Composite<T> => {
const [currentId, ...rightIds] = path;
const leftIds: string[] = [];
return _findComposite(leftIds, currentId, rightIds, composite);
};
}
);

View File

@ -3,6 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { hasLengthAtLeast } from "@k8slens/utilities";
import { pipeline } from "@ogre-tools/fp";
import {
countBy,
@ -119,11 +120,11 @@ export const getCompositeFor = <T>({
const roots = source.filter(isRootId);
if (roots.length > 1) {
if (!hasLengthAtLeast(roots, 1)) {
const ids = roots.map(getId).join('", "');
throw new Error(
`Tried to get a composite, but multiple roots where encountered: "${roots
.map(getId)
.join('", "')}"`,
`Tried to get a composite, but multiple roots where encountered: "${ids}"`,
);
}
@ -139,9 +140,12 @@ const throwMissingParentIds = ({
missingParentIds,
availableParentIds,
}: ParentIdsForHandling) => {
throw new Error(
`Tried to get a composite but encountered missing parent ids: "${missingParentIds.join(
'", "',
)}".\n\nAvailable parent ids are:\n"${availableParentIds.join('",\n"')}"`,
);
const missingIds = missingParentIds.join('", "');
const availableIds = availableParentIds.join('", "');
throw new Error([
`Tried to get a composite but encountered missing parent ids: "${missingIds}".`,
"",
`Available parent ids are: "${availableIds}"`,
].join("\n"));
};

View File

@ -2,6 +2,8 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { hasLengthAtLeast } from "@k8slens/utilities";
export const findExactlyOne = <T>(predicate: (item: T) => boolean) => (collection: T[]): T => {
const itemsFound = collection.filter(predicate);
@ -11,7 +13,7 @@ export const findExactlyOne = <T>(predicate: (item: T) => boolean) => (collectio
);
}
if (itemsFound.length > 1) {
if (!hasLengthAtLeast(itemsFound, 1)) {
throw new Error(
"Tried to find exactly one, but found many",
);

View File

@ -10,7 +10,7 @@ import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getPromiseStatus } from "@k8slens/test-utils";
import logErrorInjectable from "../../log-error.injectable";
import type { Logger } from "../../logger";
import type { Logger } from "@k8slens/logger";
describe("with-error-logging", () => {
describe("given decorated sync function", () => {

View File

@ -125,7 +125,7 @@ export class LensRendererExtension extends LensExtension {
object.entries(params),
map(([key, value]) => [
key,
normalizedParams[key].stringify(value),
normalizedParams[key]?.stringify(value),
]),
fromPairs,
);

View File

@ -225,7 +225,7 @@ describe("installing update using tray", () => {
});
it("when download progresses with decimals, percentage increases as integers", () => {
downloadPlatformUpdateMock.mock.calls[0][0]({ percentage: 42.424242 });
downloadPlatformUpdateMock.mock.calls[0]?.[0]({ percentage: 42.424242 });
expect(
builder.tray.get("check-for-updates")?.label,

View File

@ -140,13 +140,11 @@ describe("entity running technical tests", () => {
});
it("calls on before run event", () => {
const target = onBeforeRunMock.mock.calls[0][0].target;
const actual = { id: target.getId(), name: target.getName() };
expect(actual).toEqual({
expect(onBeforeRunMock).toHaveBeenCalled();
expect(onBeforeRunMock).toHaveBeenCalledWith(expect.objectContaining({
id: "a_catalogEntity_uid",
name: "a catalog entity",
});
}));
});
it("does not call onRun yet", () => {

View File

@ -546,7 +546,7 @@ metadata:
) as Record<string, Record<string, unknown>>;
expect(
actual.edit_resource_store["some-first-tab-id"],
actual.edit_resource_store?.["some-first-tab-id"],
).toEqual({
resource: "/api/some-api-version/namespaces/some-uid",
firstDraft: `apiVersion: some-api-version

View File

@ -220,11 +220,11 @@ describe("cluster storage technical tests", () => {
const storedClusters = clusters.get();
expect(storedClusters.length).toBe(3);
expect(storedClusters[0].id).toBe("cluster1");
expect(storedClusters[0].preferences.terminalCWD).toBe("/foo");
expect(storedClusters[1].id).toBe("cluster2");
expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2");
expect(storedClusters[2].id).toBe("cluster3");
expect(storedClusters[0]?.id).toBe("cluster1");
expect(storedClusters[0]?.preferences.terminalCWD).toBe("/foo");
expect(storedClusters[1]?.id).toBe("cluster2");
expect(storedClusters[1]?.preferences.terminalCWD).toBe("/foo2");
expect(storedClusters[2]?.id).toBe("cluster3");
});
});
@ -259,13 +259,13 @@ describe("cluster storage technical tests", () => {
});
it("migrates to modern format with kubeconfig in a file", () => {
const configPath = clusters.get()[0].kubeConfigPath.get();
const configPath = clusters.get()[0]?.kubeConfigPath.get() ?? "";
expect(readFileSync(configPath)).toBe(minimalValidKubeConfig);
});
it("migrates to modern format with icon not in file", () => {
expect(clusters.get()[0].preferences.icon).toMatch(/data:;base64,/);
expect(clusters.get()[0]?.preferences.icon).toMatch(/data:;base64,/);
});
});
});

View File

@ -49,9 +49,9 @@ const NonInjectedHelmFileInput = ({
name: placeholder,
extensions: fileExtensions,
},
onPick: (filePaths) => {
if (filePaths.length) {
setValue(filePaths[0]);
onPick: ([filePath]) => {
if (filePath) {
setValue(filePath);
}
},
})}

View File

@ -70,7 +70,8 @@ export class Hotbar {
from >= this.items.length ||
to >= this.items.length ||
isNaN(from) ||
isNaN(to)
isNaN(to) ||
!source
) {
throw new Error("Invalid 'from' or 'to' arguments");
}

View File

@ -359,15 +359,15 @@ describe("Hotbars technical tests", () => {
});
it("clears cells without entity", () => {
const items = hotbars.get()[0].items;
const items = hotbars.get()[0]?.items;
expect(items[2]).toBeNull();
expect(items?.[2]).toBeNull();
});
it("adds extra data to cells with according entity", () => {
const items = hotbars.get()[0].items;
const items = hotbars.get()[0]?.items;
expect(items[0]).toEqual({
expect(items?.[0]).toEqual({
entity: {
name: "my-aws-cluster",
source: "local",

View File

@ -67,7 +67,8 @@ describe("download logs options in logs dock tab", () => {
windowDi.override(reloadLogsInjectable, () => jest.fn());
windowDi.override(getLogTabDataInjectable, () => () => ({
selectedPodId: selectedPod.getId(),
selectedContainer: selectedPod.getContainers()[0].name,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedContainer: selectedPod.getContainers()[0]!.name,
namespace: "default",
showPrevious: true,
showTimestamps: false,

View File

@ -97,8 +97,8 @@ describe("kubeconfig-sync.source tests", () => {
const models = configToModels(config, "/bar");
expect(models.length).toBe(1);
expect(models[0].contextName).toBe("context-name");
expect(models[0].kubeConfigPath).toBe("/bar");
expect(models[0]?.contextName).toBe("context-name");
expect(models[0]?.kubeConfigPath).toBe("/bar");
});
});

View File

@ -74,7 +74,7 @@ const installHelmChartInjectable = getInjectable({
}
const output = result.value;
const releaseName = output.split("\n")[0].split(" ")[1].trim();
const releaseName = output.split("\n")[0]?.split(" ")[1]?.trim() ?? "";
return {
log: output,

View File

@ -5,7 +5,7 @@
import type { CoreV1Api } from "@kubernetes/client-node";
import { getInjectionToken } from "@ogre-tools/injectable";
import { isRequestError } from "@k8slens/utilities";
import { hasLengthAtLeast, isRequestError } from "@k8slens/utilities";
export interface PrometheusService extends PrometheusServiceInfo {
kind: string;
@ -54,7 +54,7 @@ export async function findFirstNamespacedService(client: CoreV1Api, ...selectors
for (const selector of selectors) {
const { body: { items: [service] }} = await client.listServiceForAllNamespaces(undefined, undefined, undefined, selector);
if (service?.metadata?.namespace && service.metadata.name && service.spec?.ports) {
if (service?.metadata?.namespace && service.metadata.name && service.spec?.ports && hasLengthAtLeast(service.spec.ports, 1)) {
return {
namespace: service.metadata.namespace,
service: service.metadata.name,
@ -73,7 +73,7 @@ export async function findNamespacedService(client: CoreV1Api, name: string, nam
try {
const { body: service } = await client.readNamespacedService(name, namespace);
if (!service.metadata?.namespace || !service.metadata.name || !service.spec?.ports) {
if (!service.metadata?.namespace || !service.metadata.name || !service.spec?.ports || !hasLengthAtLeast(service.spec.ports, 1)) {
throw new Error(`Service found in namespace="${namespace}" did not have required information`);
}

View File

@ -66,7 +66,7 @@ const getServiceAccountRouteInjectable = getRouteInjectable({
{
name: params.account,
user: {
token: Buffer.from(token, "base64").toString("utf8"),
token: token ? Buffer.from(token, "base64").toString("utf8") : undefined,
},
},
],

View File

@ -67,11 +67,7 @@ const startPortForwardRouteInjectable = getRouteInjectable({
});
return {
error: {
message: `Failed to forward port ${port} to ${
thePort ? forwardPort : "random port"
}`,
},
error: `Failed to forward port ${port} to ${thePort ? forwardPort : "random port"}`,
};
}
}
@ -84,9 +80,7 @@ const startPortForwardRouteInjectable = getRouteInjectable({
);
return {
error: {
message: `Failed to forward port ${port}`,
},
error: `Failed to forward port ${port}`,
};
}
});

View File

@ -34,9 +34,7 @@ const stopCurrentPortForwardRouteInjectable = getRouteInjectable({
logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName });
return {
error: {
message: `error stopping a forward port ${port}`,
},
error: `error stopping a forward port ${port}`,
};
}
});

View File

@ -143,13 +143,14 @@ describe("CatalogEntityRegistry", () => {
entityRegistry.updateItems(items);
expect(entityRegistry.items.get().length).toEqual(1);
expect(entityRegistry.items.get()[0].status.phase).toEqual("disconnected");
expect(entityRegistry.items.get()[0]?.status.phase).toEqual("disconnected");
items[0].status.phase = "connected";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
items[0]!.status.phase = "connected";
entityRegistry.updateItems(items);
expect(entityRegistry.items.get().length).toEqual(1);
expect(entityRegistry.items.get()[0].status.phase).toEqual("connected");
expect(entityRegistry.items.get()[0]?.status.phase).toEqual("connected");
});
it("updates activeEntity", () => {
@ -170,11 +171,12 @@ describe("CatalogEntityRegistry", () => {
entityRegistry.updateItems(items);
entityRegistry.activeEntity = entityRegistry.items.get()[0];
expect(entityRegistry.activeEntity.status.phase).toEqual("disconnected");
expect(entityRegistry.activeEntity?.status.phase).toEqual("disconnected");
items[0].status.phase = "connected";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
items[0]!.status.phase = "connected";
entityRegistry.updateItems(items);
expect(entityRegistry.activeEntity.status.phase).toEqual("connected");
expect(entityRegistry.activeEntity?.status.phase).toEqual("connected");
});
it("removes deleted items", () => {
@ -213,7 +215,7 @@ describe("CatalogEntityRegistry", () => {
items.splice(0, 1);
entityRegistry.updateItems(items);
expect(entityRegistry.items.get().length).toEqual(1);
expect(entityRegistry.items.get()[0].metadata.uid).toEqual("456");
expect(entityRegistry.items.get()[0]?.metadata.uid).toEqual("456");
});
});

View File

@ -53,8 +53,8 @@ describe("Custom Category Views", () => {
const customCategoryViews = di.inject(customCategoryViewsInjectable);
const { after = [] } = customCategoryViews.get().get("foo")?.get("bar") ?? {};
expect(after[0].View).toBe(component2);
expect(after[1].View).toBe(component1);
expect(after[0]?.View).toBe(component2);
expect(after[1]?.View).toBe(component1);
});
it("should put put priority < 50 items in before", () => {
@ -91,6 +91,6 @@ describe("Custom Category Views", () => {
const customCategoryViews = di.inject(customCategoryViewsInjectable);
const { before = [] } = customCategoryViews.get().get("foo")?.get("bar") ?? {};
expect(before[0].View).toBe(component1);
expect(before[0]?.View).toBe(component1);
});
});

View File

@ -126,10 +126,10 @@ export class Chart extends React.Component<ChartProps> {
// Mutating inner chart datasets to enable seamless transitions
nextDatasets.forEach((next, datasetIndex) => {
const index = datasets.findIndex(set => set.id === next.id);
const dataset = datasets.find(set => set.id === next.id);
if (index !== -1) {
const data = datasets[index].data = (datasets[index].data ?? []).slice(); // "Clean" mobx observables data to use in ChartJS
if (dataset) {
const data = dataset.data = (dataset.data ?? []).slice(); // "Clean" mobx observables data to use in ChartJS
const nextData = next.data ??= [];
data.splice(next.data.length);
@ -143,10 +143,7 @@ export class Chart extends React.Component<ChartProps> {
void _data;
datasets[index] = {
...datasets[index],
...props,
};
Object.assign(dataset, props);
} else {
datasets[datasetIndex] = next;
}

View File

@ -93,7 +93,11 @@ const NonInjectedClusterLocalTerminalSetting = observer((props: Dependencies & C
message: "Choose Working Directory",
buttonLabel: "Pick",
properties: ["openDirectory", "showHiddenFiles"],
onPick: ([directory]) => setAndCommitDirectory(directory),
onPick: ([directory]) => {
if (directory) {
setAndCommitDirectory(directory);
}
},
});
};

View File

@ -7,9 +7,9 @@ import styles from "./cluster-metrics.module.scss";
import React, { useState } from "react";
import { observer } from "mobx-react";
import type { ChartOptions, ChartPoint } from "chart.js";
import type { ChartOptions } from "chart.js";
import { BarChart } from "../chart";
import { bytesToUnits, cssNames } from "@k8slens/utilities";
import { bytesToUnits, cssNames, isArray, isObject } from "@k8slens/utilities";
import { Spinner } from "../spinner";
import { ZebraStripesPlugin } from "../chart/zebra-stripes.plugin";
import { ClusterNoMetrics } from "./cluster-no-metrics";
@ -71,7 +71,11 @@ const NonInjectedClusterMetrics = observer((props: Dependencies) => {
return "<unknown>";
}
const value = data.datasets?.[0].data?.[index] as ChartPoint;
const value = data.datasets?.[0]?.data?.[index];
if (!isObject(value) || isArray(value)) {
return "<unknown>";
}
return value.y?.toString() ?? "<unknown>";
},
@ -94,7 +98,11 @@ const NonInjectedClusterMetrics = observer((props: Dependencies) => {
return "<unknown>";
}
const value = data.datasets?.[0].data?.[index] as ChartPoint;
const value = data.datasets?.[0]?.data?.[index];
if (!isObject(value) || isArray(value)) {
return "<unknown>";
}
return bytesToUnits(parseInt(value.y as string), { precision: 3 });
},

View File

@ -29,18 +29,18 @@ const selectedMetricsTypeInjectable = getInjectable({
switch (type) {
case "cpu":
return normalizeMetrics(rawValue.cpuUsage).data.result[0].values;
return normalizeMetrics(rawValue.cpuUsage).data.result[0]?.values ?? [];
case "memory":
return normalizeMetrics(rawValue.memoryUsage).data.result[0].values;
return normalizeMetrics(rawValue.memoryUsage).data.result[0]?.values ?? [];
default:
return [];
}
});
const hasCPUMetrics = computed(() => (
normalizeMetrics(overviewMetrics.value.get()?.cpuUsage).data.result[0].values.length > 0
(normalizeMetrics(overviewMetrics.value.get()?.cpuUsage).data.result[0]?.values.length ?? 0) > 0
));
const hasMemoryMetrics = computed(() => (
normalizeMetrics(overviewMetrics.value.get()?.memoryUsage).data.result[0].values.length > 0
(normalizeMetrics(overviewMetrics.value.get()?.memoryUsage).data.result[0]?.values.length ?? 0) > 0
));
return {

View File

@ -32,7 +32,8 @@ describe("DockStore", () => {
it("closes tab and selects one from right", () => {
dockStore.tabs = initialTabs;
dockStore.closeTab(dockStore.tabs[0].id);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
dockStore.closeTab(dockStore.tabs[0]!.id);
expect(dockStore.selectedTabId).toBe("create");

View File

@ -169,10 +169,6 @@ export class DockStore implements DockStorageState {
this.dependencies.storage.merge({ selectedTabId: tabId });
}
@computed get tabsNumber() : number {
return this.tabs.length;
}
@computed get selectedTab() {
return this.tabs.find(tab => tab.id === this.selectedTabId);
}
@ -325,7 +321,11 @@ export class DockStore implements DockStorageState {
if (this.selectedTabId === tab.id) {
if (this.tabs.length) {
const newTab = tabIndex < this.tabsNumber ? this.tabs[tabIndex] : this.tabs[tabIndex - 1];
const newTab = (
tabIndex < this.tabs.length
? this.tabs[tabIndex]
: this.tabs[this.tabs.length - 1]
) as Required<DockTabCreate>;
this.selectTab(newTab.id);
} else {

View File

@ -48,7 +48,8 @@ function getOnePodViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependenc
return mockLogTabViewModel(tabId, {
getLogTabData: () => ({
selectedPodId: selectedPod.getId(),
selectedContainer: selectedPod.getContainers()[0].name,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedContainer: selectedPod.getContainers()[0]!.name,
namespace: selectedPod.getNs(),
showPrevious: false,
showTimestamps: false,
@ -76,7 +77,8 @@ const getFewPodsTabData = (tabId: TabId, deps: Partial<LogTabViewModelDependenci
name: "super-deployment",
},
selectedPodId: selectedPod.getId(),
selectedContainer: selectedPod.getContainers()[0].name,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedContainer: selectedPod.getContainers()[0]!.name,
namespace: selectedPod.getNs(),
showPrevious: false,
showTimestamps: false,

View File

@ -43,7 +43,8 @@ const getOnePodViewModel = (tabId: TabId, deps: Partial<LogTabViewModelDependenc
return mockLogTabViewModel(tabId, {
getLogTabData: () => ({
selectedPodId: selectedPod.getId(),
selectedContainer: selectedPod.getContainers()[0].name,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedContainer: selectedPod.getContainers()[0]!.name,
namespace: selectedPod.getNs(),
showPrevious: false,
showTimestamps: false,

View File

@ -26,7 +26,7 @@ export const LogControls = observer(({ model }: LogControlsProps) => {
const logs = model.timestampSplitLogs.get();
const { showTimestamps, showPrevious: previous } = tabData;
const since = logs.length ? logs[0][0] : null;
const since = logs[0]?.[0] ?? null;
const toggleTimestamps = () => {
model.updateLogTabData({ showTimestamps: !showTimestamps });

View File

@ -9,6 +9,7 @@ import type { DaemonSet, Deployment, Job, ReplicaSet, StatefulSet } from "@k8sle
import type { TabId } from "../dock/store";
import type { CreateLogsTabData } from "./create-logs-tab.injectable";
import createLogsTabInjectable from "./create-logs-tab.injectable";
import { hasLengthAtLeast } from "@k8slens/utilities";
export interface WorkloadLogsTabData {
workload: StatefulSet | Job | Deployment | DaemonSet | ReplicaSet;
@ -24,15 +25,20 @@ const createWorkloadLogsTab = ({
getPodsByOwnerId,
}: Dependencies) => ({ workload }: WorkloadLogsTabData): TabId | undefined => {
const pods = getPodsByOwnerId(workload.getId());
const selectedPod = pods[0];
if (pods.length === 0) {
if (!selectedPod) {
return undefined;
}
const selectedPod = pods[0];
const containers = selectedPod.getAllContainers();
if (!hasLengthAtLeast(containers, 1)) {
return undefined;
}
return createLogsTab(`${workload.kind} ${selectedPod.getName()}`, {
selectedContainer: selectedPod.getAllContainers()[0].name,
selectedContainer: containers[0].name,
selectedPodId: selectedPod.getId(),
namespace: selectedPod.getNs(),
owner: {

View File

@ -199,7 +199,7 @@ class NonForwardedLogList extends React.Component<Dependencies & LogListProps &
getLogRow = (rowIndex: number) => {
const { isActiveOverlay } = this.props.model.searchStore;
const searchQuery = this.props.model.searchStore.searchQuery.get();
const item = this.logs[rowIndex];
const item = this.logs[rowIndex] ?? "";
// If search is enabled, replace keyword with a background <span>
const contents = searchQuery

View File

@ -179,7 +179,7 @@ export class LogStore {
*/
getLastSinceTime(tabId: TabId): string {
const logs = this.podLogs.get(tabId) ?? [];
const [timestamp] = getTimestamps(logs[logs.length - 1]) ?? [];
const [timestamp] = getTimestamps(logs[logs.length - 1] ?? "") ?? [];
const stamp = timestamp ? new Date(timestamp) : new Date();
stamp.setSeconds(stamp.getSeconds() + 1); // avoid duplicates from last second
@ -201,11 +201,7 @@ export class LogStore {
const removeTimestamps = (logs: string) => logs.replace(/^\d+.*?\s/gm, "");
const getTimestamps = (logs: string) => logs.match(/^\d+\S+/gm);
const splitOutTimestamp = (logs: string): [string, string] => {
const extraction = /^(\d+\S+)(.*)/m.exec(logs);
const [,timestamp = "", log = logs]= [.../^(\d+\S+)(.*)/m.exec(logs) ?? []];
if (!extraction || extraction.length < 3) {
return ["", logs];
}
return [extraction[1], extraction[2]];
return [timestamp, log];
};

View File

@ -30,16 +30,14 @@ class NonInjectedKubeEventIcon extends React.Component<KubeEventIconProps & Depe
}
render() {
const { object, filterEvents, eventStore } = this.props;
const { object, filterEvents = (events) => events, eventStore } = this.props;
const events = eventStore.getEventsByObject(object);
let warnings = events.filter(evt => evt.isWarning());
const warnings = filterEvents(events.filter(event => event.isWarning()));
if (filterEvents) warnings = filterEvents(warnings);
const [event] = [...warnings, ...events]; // get latest event
if (!events.length || (this.showWarningsOnly && !warnings.length)) {
return null;
}
const event = [...warnings, ...events][0]; // get latest event
if (!event) return null;
if (this.showWarningsOnly && !event.isWarning()) return null;
return (
<Icon

View File

@ -54,9 +54,9 @@ export class EventStore extends KubeObjectStore<KubeEvent, KubeEventApi> {
const groupsByInvolvedObject = groupBy(warnings, warning => warning.involvedObject.uid);
const eventsWithError = Object.values(groupsByInvolvedObject).map(events => {
const recent = events[0];
const { kind, uid } = recent.involvedObject;
const { kind, uid } = recent?.involvedObject ?? {};
if (kind == Pod.kind) { // Wipe out running pods
if (kind == Pod.kind && uid) { // Wipe out running pods
const pod = this.dependencies.getPodById(uid);
if (!pod || (!pod.hasIssues() && (pod.spec?.priority ?? 0) < 500000)) {

View File

@ -7,7 +7,7 @@ import extensionLoaderInjectable from "../../../../extensions/extension-loader/e
import getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable";
import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import type { Disposer } from "@k8slens/utilities";
import { noop } from "@k8slens/utilities";
import { hasLengthAtLeast, noop } from "@k8slens/utilities";
import { extensionDisplayName } from "../../../../extensions/lens-extension";
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
import path from "path";
@ -63,7 +63,7 @@ const unpackExtensionInjectable = getInjectable({
const unpackedFiles = await fse.readdir(unpackingTempFolder);
let unpackedRootFolder = unpackingTempFolder;
if (unpackedFiles.length === 1) {
if (hasLengthAtLeast(unpackedFiles, 1)) {
// check if %extension.tgz was packed with single top folder,
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);

View File

@ -17,7 +17,7 @@ export async function validatePackage(filePath: string): Promise<LensExtensionMa
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
}
const rootFolder = path.normalize(firstFile).split(path.sep)[0];
const rootFolder = path.normalize(firstFile).split(path.sep)[0] ?? "";
const packedInRootFolder = tarFiles.every(entry => entry.startsWith(rootFolder));
const manifestLocation = packedInRootFolder
? path.join(rootFolder, manifestFilename)

View File

@ -37,7 +37,11 @@ export class PageFiltersStore {
protected syncWithGlobalSearch() {
const disposers = [
reaction(() => this.getValues(FilterType.SEARCH)[0], search => this.dependencies.searchUrlParam.set(search)),
reaction(() => this.getValues(FilterType.SEARCH)[0], search => {
if (search) {
this.dependencies.searchUrlParam.set(search);
}
}),
reaction(() => this.dependencies.searchUrlParam.get(), search => {
const filter = this.getByType(FilterType.SEARCH);

View File

@ -17,7 +17,7 @@ import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import type { KubeObjectStatusText } from "./kube-object-status-text-injection-token";
function statusClassName(level: KubeObjectStatusLevel): string {
function statusClassName(level: KubeObjectStatusLevel | undefined): string | undefined {
switch (level) {
case KubeObjectStatusLevel.INFO:
return "info";
@ -26,6 +26,8 @@ function statusClassName(level: KubeObjectStatusLevel): string {
case KubeObjectStatusLevel.CRITICAL:
return "error";
}
return undefined;
}
function statusTitle(level: KubeObjectStatusLevel): string {
@ -46,7 +48,7 @@ function getAge(timestamp: string | undefined) {
}
interface SplitStatusesByLevel {
maxLevel: string;
maxLevel: string | undefined;
criticals: KubeObjectStatus[];
warnings: KubeObjectStatus[];
infos: KubeObjectStatus[];
@ -69,7 +71,7 @@ function splitByLevel(statuses: KubeObjectStatus[]): SplitStatusesByLevel {
const criticals = parts.get(KubeObjectStatusLevel.CRITICAL) ?? [];
const warnings = parts.get(KubeObjectStatusLevel.WARNING) ?? [];
const infos = parts.get(KubeObjectStatusLevel.INFO) ?? [];
const maxLevel = statusClassName(criticals[0]?.level ?? warnings[0]?.level ?? infos[0].level);
const maxLevel = statusClassName(criticals[0]?.level ?? warnings[0]?.level ?? infos[0]?.level);
return { maxLevel, criticals, warnings, infos };
}

View File

@ -24,7 +24,8 @@ export function List<I, T extends object>({ columns, data, title, items, filters
const query = search.toLowerCase();
const filteredData = data.filter((item, index) => (
filters.some(getText => String(getText(items[index])).toLowerCase().includes(query))
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
filters.some(getText => String(getText(items[index]!)).toLowerCase().includes(query))
));
return (

View File

@ -21,7 +21,7 @@ export const IngressCharts = observer(() => {
const id = object.getId();
const values = Object.values(metrics)
.map(normalizeMetrics)
.map(({ data }) => data.result[0].values);
.map(({ data }) => data.result[0]?.values);
const [
bytesSentSuccess,
bytesSentFailure,
@ -36,14 +36,14 @@ export const IngressCharts = observer(() => {
label: `Bytes sent, status 2xx`,
tooltip: `Bytes sent by Ingress controller with successful status`,
borderColor: "#46cd9e",
data: bytesSentSuccess.map(([x, y]) => ({ x, y })),
data: bytesSentSuccess?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-bytesSentFailure`,
label: `Bytes sent, status 5xx`,
tooltip: `Bytes sent by Ingress controller with error status`,
borderColor: "#cd465a",
data: bytesSentFailure.map(([x, y]) => ({ x, y })),
data: bytesSentFailure?.map(([x, y]) => ({ x, y })),
},
],
Duration: [
@ -52,14 +52,14 @@ export const IngressCharts = observer(() => {
label: `Request`,
tooltip: `Request duration in seconds`,
borderColor: "#48b18d",
data: requestDurationSeconds.map(([x, y]) => ({ x, y })),
data: requestDurationSeconds?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-responseDurationSeconds`,
label: `Response`,
tooltip: `Response duration in seconds`,
borderColor: "#73ba3c",
data: responseDurationSeconds.map(([x, y]) => ({ x, y })),
data: responseDurationSeconds?.map(([x, y]) => ({ x, y })),
},
],
};

View File

@ -44,7 +44,7 @@ const NonInjectedNodeCharts = observer((props: Dependencies) => {
podCapacity,
fsSize,
fsUsage,
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values);
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0]?.values);
const datasets: Partial<Record<MetricsTab, ChartDataSets[]>> = {
CPU: [
@ -53,28 +53,28 @@ const NonInjectedNodeCharts = observer((props: Dependencies) => {
label: `Usage`,
tooltip: `CPU cores usage`,
borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y })),
data: cpuUsage?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-cpuRequests`,
label: `Requests`,
tooltip: `CPU requests`,
borderColor: "#30b24d",
data: cpuRequests.map(([x, y]) => ({ x, y })),
data: cpuRequests?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-cpuAllocatableCapacity`,
label: `Allocatable Capacity`,
tooltip: `CPU allocatable capacity`,
borderColor: "#032b4d",
data: cpuAllocatableCapacity.map(([x, y]) => ({ x, y })),
data: cpuAllocatableCapacity?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-cpuCapacity`,
label: `Capacity`,
tooltip: `CPU capacity`,
borderColor: chartCapacityColor,
data: cpuCapacity.map(([x, y]) => ({ x, y })),
data: cpuCapacity?.map(([x, y]) => ({ x, y })),
},
],
Memory: [
@ -83,35 +83,35 @@ const NonInjectedNodeCharts = observer((props: Dependencies) => {
label: `Usage`,
tooltip: `Memory usage`,
borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y })),
data: memoryUsage?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-workloadMemoryUsage`,
label: `Workload Memory Usage`,
tooltip: `Workload memory usage`,
borderColor: "#9cd3ce",
data: workloadMemoryUsage.map(([x, y]) => ({ x, y })),
data: workloadMemoryUsage?.map(([x, y]) => ({ x, y })),
},
{
id: "memoryRequests",
label: `Requests`,
tooltip: `Memory requests`,
borderColor: "#30b24d",
data: memoryRequests.map(([x, y]) => ({ x, y })),
data: memoryRequests?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-memoryAllocatableCapacity`,
label: `Allocatable Capacity`,
tooltip: `Memory allocatable capacity`,
borderColor: "#032b4d",
data: memoryAllocatableCapacity.map(([x, y]) => ({ x, y })),
data: memoryAllocatableCapacity?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-memoryCapacity`,
label: `Capacity`,
tooltip: `Memory capacity`,
borderColor: chartCapacityColor,
data: memoryCapacity.map(([x, y]) => ({ x, y })),
data: memoryCapacity?.map(([x, y]) => ({ x, y })),
},
],
Disk: [
@ -120,14 +120,14 @@ const NonInjectedNodeCharts = observer((props: Dependencies) => {
label: `Usage`,
tooltip: `Node filesystem usage in bytes`,
borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y })),
data: fsUsage?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-fsSize`,
label: `Size`,
tooltip: `Node filesystem size in bytes`,
borderColor: chartCapacityColor,
data: fsSize.map(([x, y]) => ({ x, y })),
data: fsSize?.map(([x, y]) => ({ x, y })),
},
],
Pods: [
@ -136,14 +136,14 @@ const NonInjectedNodeCharts = observer((props: Dependencies) => {
label: `Usage`,
tooltip: `Number of running Pods`,
borderColor: "#30b24d",
data: podUsage.map(([x, y]) => ({ x, y })),
data: podUsage?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-podCapacity`,
label: `Capacity`,
tooltip: `Node Pods capacity`,
borderColor: chartCapacityColor,
data: podCapacity.map(([x, y]) => ({ x, y })),
data: podCapacity?.map(([x, y]) => ({ x, y })),
},
],
};

View File

@ -6,7 +6,7 @@
import "./nodes.scss";
import React from "react";
import { observer } from "mobx-react";
import { bytesToUnits, cssNames, hasLengthGreaterThan, interval } from "@k8slens/utilities";
import { bytesToUnits, cssNames, hasLengthAtLeast, interval } from "@k8slens/utilities";
import { TabLayout } from "../layout/tab-layout-2";
import { KubeObjectListLayout } from "../kube-object-list-layout";
import type { Node } from "@k8slens/kube-object";
@ -104,7 +104,7 @@ class NonInjectedNodesRoute extends React.Component<Dependencies> {
private renderUsage({ node, title, metricNames, formatters }: UsageArgs) {
const metrics = this.getLastMetricValues(node, metricNames);
if (!hasLengthGreaterThan(metrics, 2)) {
if (!hasLengthAtLeast(metrics, 2)) {
return <LineProgress value={0}/>;
}

View File

@ -38,7 +38,7 @@ export const ScrollSpy = observer(({ render, htmlFor, rootMargin = "0px 0px -100
};
const getSectionsParentElement = () => {
return sections.current?.[0].parentElement;
return sections.current?.[0]?.parentElement;
};
const updateNavigation = () => {
@ -76,9 +76,9 @@ export const ScrollSpy = observer(({ render, htmlFor, rootMargin = "0px 0px -100
};
const handleIntersect = ([entry]: IntersectionObserverEntry[]) => {
const closest = entry.target.closest("section[id]");
const closest = entry?.target.closest("section[id]");
if (entry.isIntersecting && closest) {
if (entry?.isIntersecting && closest) {
setActiveElementId(closest.id);
}
};

View File

@ -67,14 +67,14 @@ describe("<Select />", () => {
const { container } = render((
<Select
value={options[0].value}
value={options[0]?.value}
onChange={onChange}
options={options}
/>
));
const selectedValueContainer = container.querySelector(".Select__single-value");
expect(selectedValueContainer?.textContent).toBe(options[0].label);
expect(selectedValueContainer?.textContent).toBe(options[0]?.label);
});
it("should reflect to change value", () => {
@ -93,24 +93,24 @@ describe("<Select />", () => {
const { container, rerender } = render((
<Select
value={options[0].value}
value={options[0]?.value}
onChange={onChange}
options={options}
/>
));
const selectedValueContainer = container.querySelector(".Select__single-value");
expect(selectedValueContainer?.textContent).toBe(options[0].label);
expect(selectedValueContainer?.textContent).toBe(options[0]?.label);
rerender((
<Select
value={options[1].value}
value={options[1]?.value}
onChange={onChange}
options={options}
/>
));
expect(container.querySelector(".Select__single-value")?.textContent).toBe(options[1].label);
expect(container.querySelector(".Select__single-value")?.textContent).toBe(options[1]?.label);
});
it("should unselect value if null is passed as a value", () => {
@ -129,14 +129,14 @@ describe("<Select />", () => {
const { container, rerender } = render((
<Select
value={options[0].value}
value={options[0]?.value}
onChange={onChange}
options={options}
/>
));
const selectedValueContainer = container.querySelector(".Select__single-value");
expect(selectedValueContainer?.textContent).toBe(options[0].label);
expect(selectedValueContainer?.textContent).toBe(options[0]?.label);
rerender((
<Select<string, SelectOption<string>>
@ -165,14 +165,14 @@ describe("<Select />", () => {
const { container, rerender } = render((
<Select
value={options[0].value}
value={options[0]?.value}
onChange={onChange}
options={options}
/>
));
const selectedValueContainer = container.querySelector(".Select__single-value");
expect(selectedValueContainer?.textContent).toBe(options[0].label);
expect(selectedValueContainer?.textContent).toBe(options[0]?.label);
rerender((
<Select<string, SelectOption<string>>

View File

@ -29,8 +29,8 @@ const NonInjectedVolumeClaimDiskChart = observer((props: Dependencies) => {
const id = object.getId();
const { chartCapacityColor } = activeTheme.get().colors;
const { diskUsage, diskCapacity } = metrics;
const usage = normalizeMetrics(diskUsage).data.result[0].values;
const capacity = normalizeMetrics(diskCapacity).data.result[0].values;
const usage = normalizeMetrics(diskUsage).data.result[0]?.values;
const capacity = normalizeMetrics(diskCapacity).data.result[0]?.values;
const datasets: ChartDataSets[] = [
{
@ -38,14 +38,14 @@ const NonInjectedVolumeClaimDiskChart = observer((props: Dependencies) => {
label: `Usage`,
tooltip: `Volume disk usage`,
borderColor: "#ffc63d",
data: usage.map(([x, y]) => ({ x, y })),
data: usage?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-diskCapacity`,
label: `Capacity`,
tooltip: `Volume disk capacity`,
borderColor: chartCapacityColor,
data: capacity.map(([x, y]) => ({ x, y })),
data: capacity?.map(([x, y]) => ({ x, y })),
},
];

View File

@ -49,7 +49,7 @@ export function ReactTable<Data extends object>({ columns, data, headless }: Rea
<div
{...cell.getCellProps()}
key={cell.getCellProps().key}
className={cssNames(styles.td, columns[index].accessor)}
className={cssNames(styles.td, columns[index]?.accessor)}
>
{cell.render("Cell")}
</div>

View File

@ -43,7 +43,11 @@ export function getSorted<T>(rawItems: T[], sortingCallback: TableSortCallback<T
const res = [];
for (const { index } of sortData) {
res.push(rawItems[index]);
const item = rawItems[index];
if (item !== undefined) {
res.push(item);
}
}
return res;

View File

@ -18,8 +18,7 @@ interface Dependencies {
export class TableModel {
constructor(private dependencies: Dependencies) {}
getSortParams = (tableId: string): Partial<TableSortParams> =>
this.dependencies.storage.get().sortParams[tableId];
getSortParams = (tableId: string): Partial<TableSortParams> => this.dependencies.storage.get().sortParams[tableId] ?? {};
setSortParams = (
tableId: string,

View File

@ -429,7 +429,7 @@ export const getApplicationBuilder = () => {
return getCompositePaths(composite);
},
click: (...path: string[]) => {
click: (...path: [string, ...string[]]) => {
const composite = mainDi.inject(
applicationMenuItemCompositeInjectable,
).get();

View File

@ -73,7 +73,8 @@ function VirtualListInner<T extends { getId(): string } | string>({
listRef.current?.scrollToItem(index, "smart");
}
}, [selectedItemId, [items]]);
const getItemSize = (index: number) => rowHeights[index];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const getItemSize = (index: number) => rowHeights[index]!;
useImperativeHandle(forwardedRef, () => ({
scrollToItem: (index, align) => listRef.current?.scrollToItem(index, align),
@ -147,11 +148,16 @@ const Row = observer(<T extends { getId(): string } | string>(props: RowProps<T>
const { index, style, data } = props;
const { items, getRow } = data;
const item = items[index];
if (item == null) {
return null;
}
const row = getRow?.((
typeof item == "string"
? index
: item.getId()
) as never);
) as T extends string ? number : string);
if (!row) return null;

View File

@ -12,6 +12,7 @@ import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import assert from "assert";
const tolerations: Toleration[] =[
{
@ -49,6 +50,9 @@ describe("<PodTolerations />", () => {
const { container } = render(<PodTolerations tolerations={tolerations} />);
const rows = container.querySelectorAll(".TableRow");
assert(rows[0]);
assert(rows[1]);
expect(rows[0].querySelector(".key")?.textContent).toBe("CriticalAddonsOnly");
expect(rows[0].querySelector(".operator")?.textContent).toBe("Exist");
expect(rows[0].querySelector(".effect")?.textContent).toBe("NoExecute");
@ -69,6 +73,6 @@ describe("<PodTolerations />", () => {
const rows = container.querySelectorAll(".TableRow");
expect(rows[0].querySelector(".key")?.textContent).toBe("node.kubernetes.io/not-ready");
expect(rows[0]?.querySelector(".key")?.textContent).toBe("node.kubernetes.io/not-ready");
});
});

View File

@ -39,7 +39,7 @@ const NonInjectedContainerCharts = observer((props: Dependencies) => {
fsUsage,
fsWrites,
fsReads,
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values);
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0]?.values);
const datasets: Partial<Record<MetricsTab, ChartDataSets[]>> = {
CPU: [
@ -48,21 +48,21 @@ const NonInjectedContainerCharts = observer((props: Dependencies) => {
label: `Usage`,
tooltip: `CPU cores usage`,
borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y })),
data: cpuUsage?.map(([x, y]) => ({ x, y })),
},
{
id: "cpuRequests",
label: `Requests`,
tooltip: `CPU requests`,
borderColor: "#30b24d",
data: cpuRequests.map(([x, y]) => ({ x, y })),
data: cpuRequests?.map(([x, y]) => ({ x, y })),
},
{
id: "cpuLimits",
label: `Limits`,
tooltip: `CPU limits`,
borderColor: chartCapacityColor,
data: cpuLimits.map(([x, y]) => ({ x, y })),
data: cpuLimits?.map(([x, y]) => ({ x, y })),
},
],
Memory: [
@ -71,21 +71,21 @@ const NonInjectedContainerCharts = observer((props: Dependencies) => {
label: `Usage`,
tooltip: `Memory usage`,
borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y })),
data: memoryUsage?.map(([x, y]) => ({ x, y })),
},
{
id: "memoryRequests",
label: `Requests`,
tooltip: `Memory requests`,
borderColor: "#30b24d",
data: memoryRequests.map(([x, y]) => ({ x, y })),
data: memoryRequests?.map(([x, y]) => ({ x, y })),
},
{
id: "memoryLimits",
label: `Limits`,
tooltip: `Memory limits`,
borderColor: chartCapacityColor,
data: memoryLimits.map(([x, y]) => ({ x, y })),
data: memoryLimits?.map(([x, y]) => ({ x, y })),
},
],
Filesystem: [
@ -94,21 +94,21 @@ const NonInjectedContainerCharts = observer((props: Dependencies) => {
label: `Usage`,
tooltip: `Bytes consumed on this filesystem`,
borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y })),
data: fsUsage?.map(([x, y]) => ({ x, y })),
},
{
id: "fsWrites",
label: `Writes`,
tooltip: `Bytes written on this filesystem`,
borderColor: "#ff963d",
data: fsWrites.map(([x, y]) => ({ x, y })),
data: fsWrites?.map(([x, y]) => ({ x, y })),
},
{
id: "fsReads",
label: `Reads`,
tooltip: `Bytes read on this filesystem`,
borderColor: "#fff73d",
data: fsReads.map(([x, y]) => ({ x, y })),
data: fsReads?.map(([x, y]) => ({ x, y })),
},
],
};

View File

@ -37,7 +37,7 @@ export const PodCharts = observer(() => {
fsReads,
networkReceive,
networkTransmit,
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values);
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0]?.values);
const datasets: Partial<Record<MetricsTab, ChartDataSets[]>> = {
CPU: [
@ -46,7 +46,7 @@ export const PodCharts = observer(() => {
label: `Usage`,
tooltip: `Container CPU cores usage`,
borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y })),
data: cpuUsage?.map(([x, y]) => ({ x, y })),
},
],
Memory: [
@ -55,7 +55,7 @@ export const PodCharts = observer(() => {
label: `Usage`,
tooltip: `Container memory usage`,
borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y })),
data: memoryUsage?.map(([x, y]) => ({ x, y })),
},
],
Network: [
@ -64,14 +64,14 @@ export const PodCharts = observer(() => {
label: `Receive`,
tooltip: `Bytes received by all containers`,
borderColor: "#64c5d6",
data: networkReceive.map(([x, y]) => ({ x, y })),
data: networkReceive?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-networkTransmit`,
label: `Transmit`,
tooltip: `Bytes transmitted from all containers`,
borderColor: "#46cd9e",
data: networkTransmit.map(([x, y]) => ({ x, y })),
data: networkTransmit?.map(([x, y]) => ({ x, y })),
},
],
Filesystem: [
@ -80,21 +80,21 @@ export const PodCharts = observer(() => {
label: `Usage`,
tooltip: `Bytes consumed on this filesystem`,
borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y })),
data: fsUsage?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-fsWrites`,
label: `Writes`,
tooltip: `Bytes written on this filesystem`,
borderColor: "#ff963d",
data: fsWrites.map(([x, y]) => ({ x, y })),
data: fsWrites?.map(([x, y]) => ({ x, y })),
},
{
id: `${id}-fsReads`,
label: `Reads`,
tooltip: `Bytes read on this filesystem`,
borderColor: "#fff73d",
data: fsReads.map(([x, y]) => ({ x, y })),
data: fsReads?.map(([x, y]) => ({ x, y })),
},
],
};

View File

@ -122,7 +122,7 @@ export class PageParam<Value> {
this.dependencies.history.searchParams.append(this.name, value);
});
} else {
this.dependencies.history.searchParams.set(this.name, values[0]);
this.dependencies.history.searchParams.set(this.name, values[0] ?? "");
}
}
@ -132,7 +132,7 @@ export class PageParam<Value> {
getRaw(): string | string[] {
const values: string[] = this.dependencies.history.searchParams.getAll(this.name);
return this.isMulti ? values : values[0];
return this.isMulti ? values : values[0] ?? "";
}
@action

View File

@ -16,14 +16,33 @@ export const routeSpecificComponentInjectionToken = getInjectionToken<{
export interface GetRouteSpecificComponentOptions<Path extends string> {
id: string;
routeInjectable: Injectable<Route<Path>, Route<string>, void>;
Component: React.ComponentType<{ params?: InferParamFromPath<Path> }>;
Component: React.ComponentType<{ params: InferParamFromPath<Path> }>;
}
export const getRouteSpecificComponentInjectable = <Path extends string>(options: GetRouteSpecificComponentOptions<Path>) => getInjectable({
export interface GetRouteSpecificComponentInjectable {
<Path extends string>(options: {
id: string;
routeInjectable: Injectable<Route<Path>, Route<string>, void>;
Component: React.ComponentType<{ params: InferParamFromPath<Path> }>;
}): Injectable<{
route: Route<Path>;
Component: React.ComponentType<{ params: InferParamFromPath<Path> }>;
}, unknown, void>;
<Path extends string>(options: {
id: string;
routeInjectable: Injectable<Route<Path>, Route<string>, void>;
Component: React.ComponentType<{ params?: InferParamFromPath<Path> }>;
}): Injectable<{
route: Route<Path>;
Component: React.ComponentType<{ params?: InferParamFromPath<Path> }>;
}, unknown, void>;
}
export const getRouteSpecificComponentInjectable = ((options) => getInjectable({
id: options.id,
instantiate: (di) => ({
route: di.inject(options.routeInjectable),
Component: options.Component as React.ComponentType<{ params: InferParamFromPath<string> }>,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
})) as GetRouteSpecificComponentInjectable;

View File

@ -5,6 +5,7 @@
import assert from "assert";
import { iter } from "./iter";
import { TypedRegEx } from "typed-regex";
// Helper to convert memory from units Ki, Mi, Gi, Ti, Pi to bytes and vise versa
@ -18,25 +19,25 @@ const magnitudes = new Map([
["TiB", baseMagnitude ** 4] as const,
maxMagnitude,
]);
const unitRegex = /(?<value>[0-9]+(\.[0-9]*)?)(?<suffix>(B|[KMGTP]iB?))?/;
const unitRegex = TypedRegEx("^(?<value>\\d+(\\.\\d+)?)\\s*(?<suffix>B|KiB|MiB|GiB|TiB|PiB)?$");
type BinaryUnit = typeof magnitudes extends Map<infer Key, any> ? Key : never;
export function unitsToBytes(value: string): number {
const unitsMatch = value.match(unitRegex);
const unitsMatch = unitRegex.captures(value.trim());
if (!unitsMatch?.groups) {
if (!unitsMatch?.value) {
return NaN;
}
const parsedValue = parseFloat(unitsMatch.groups.value);
const parsedValue = parseFloat(unitsMatch.value);
if (!unitsMatch.groups?.suffix) {
if (!unitsMatch.suffix) {
return parsedValue;
}
const magnitude = magnitudes.get(unitsMatch.groups.suffix as BinaryUnit)
?? magnitudes.get(`${unitsMatch.groups.suffix}B` as BinaryUnit);
const magnitude = magnitudes.get(unitsMatch.suffix as BinaryUnit)
?? magnitudes.get(`${unitsMatch.suffix}B` as BinaryUnit);
assert(magnitude, "UnitRegex is wrong some how");

View File

@ -84,7 +84,7 @@ export function formatDuration(timeValue: number, compact = true) {
function getMeaningfulValues(values: number[], suffixes: string[], separator = " ") {
return values
.map((a, i): [number, string] => [a, suffixes[i]])
.map((a, i) => [a, suffixes[i]] as [number, string])
.filter(([dur]) => dur > 0)
.map(([dur, suf]) => dur + suf)
.join(separator);

View File

@ -5,6 +5,7 @@
import type { IInterceptable, IInterceptor, IListenable, ISetWillChange, ObservableMap } from "mobx";
import { observable, ObservableSet, runInAction } from "mobx";
import { iter } from "./iter";
export function makeIterableIterator<T>(iterator: Iterator<T>): IterableIterator<T> {
(iterator as IterableIterator<T>)[Symbol.iterator] = () => iterator as IterableIterator<T>;
@ -73,20 +74,8 @@ export class HashSet<T> implements Set<T> {
return this.#hashmap.size;
}
entries(): IterableIterator<[T, T]> {
let nextIndex = 0;
const keys = Array.from(this.keys());
const values = Array.from(this.values());
return makeIterableIterator<[T, T]>({
next() {
const index = nextIndex++;
return index < values.length
? { value: [keys[index], values[index]], done: false }
: { done: true, value: undefined };
},
});
entries() {
return iter.zip(this.keys(), this.values());
}
keys(): IterableIterator<T> {
@ -94,16 +83,7 @@ export class HashSet<T> implements Set<T> {
}
values(): IterableIterator<T> {
let nextIndex = 0;
const observableValues = Array.from(this.#hashmap.values());
return makeIterableIterator<T>({
next: () => {
return nextIndex < observableValues.length
? { value: observableValues[nextIndex++], done: false }
: { done: true, value: undefined };
},
});
return this.#hashmap.values();
}
[Symbol.iterator](): IterableIterator<T> {
@ -197,19 +177,7 @@ export class ObservableHashSet<T> implements Set<T>, IInterceptable<ISetWillChan
}
entries(): IterableIterator<[T, T]> {
let nextIndex = 0;
const keys = Array.from(this.keys());
const values = Array.from(this.values());
return makeIterableIterator<[T, T]>({
next() {
const index = nextIndex++;
return index < values.length
? { value: [keys[index], values[index]], done: false }
: { done: true, value: undefined };
},
});
return iter.zip(this.keys(), this.values());
}
keys(): IterableIterator<T> {
@ -217,16 +185,7 @@ export class ObservableHashSet<T> implements Set<T>, IInterceptable<ISetWillChan
}
values(): IterableIterator<T> {
let nextIndex = 0;
const observableValues = Array.from(this.#hashmap.values());
return makeIterableIterator<T>({
next: () => {
return nextIndex < observableValues.length
? { value: observableValues[nextIndex++], done: false }
: { done: true, value: undefined };
},
});
return this.#hashmap.values();
}
[Symbol.iterator](): IterableIterator<T> {

View File

@ -4,7 +4,7 @@
*/
import { getOrInsert } from "./collection-functions";
import type { Tuple } from "./tuple";
import type { ReadonlyTuple, Tuple } from "./tuple";
export type Falsy = false | 0 | "" | null | undefined;
@ -295,8 +295,13 @@ function nFircate<T>(from: Iterable<T>, field: keyof T, parts: T[typeof field][]
return res;
}
function chunks<T, ChunkSize extends number>(src: Iterable<T>, size: ChunkSize): Tuple<T, ChunkSize>[] {
const res: Tuple<T, ChunkSize>[] = [];
/**
*
* @param src The source iterable to chunk over
* @param size The size of each chunk
* @returns An iterator over the chunks of `src`
*/
function* chunks<T, ChunkSize extends number>(src: Iterable<T>, size: ChunkSize): IterableIterator<Tuple<T, ChunkSize>> {
const iterating = src[Symbol.iterator]();
top: for (;;) {
@ -312,10 +317,33 @@ function chunks<T, ChunkSize extends number>(src: Iterable<T>, size: ChunkSize):
item.push(result.value);
}
res.push(item as Tuple<T, ChunkSize>);
yield item as Tuple<T, ChunkSize>;
}
}
return res;
function zip<T, Count extends 0>(...sources: readonly []): IterableIterator<[]>;
function zip<T, Count extends 1>(...sources: readonly [Iterable<T>]): IterableIterator<[T]>;
function zip<T, Count extends 2>(...sources: readonly [Iterable<T>, Iterable<T>]): IterableIterator<[T, T]>;
function zip<T, Count extends 3>(...sources: readonly [Iterable<T>, Iterable<T>, Iterable<T>]): IterableIterator<[T, T, T]>;
function* zip<T, Count extends number>(...sources: ReadonlyTuple<Iterable<T>, Count>): IterableIterator<Tuple<T, Count>> {
const iterating = sources.map((iter) => iter[Symbol.iterator]());
for (;;) {
const item: T[] = [];
for (const iter of iterating) {
const result = iter.next();
if (result.done === true) {
return;
}
item.push(result.value);
}
yield item as Tuple<T, Count>;
}
}
export const iter = {
@ -337,4 +365,5 @@ export const iter = {
nth,
reduce,
take,
zip,
};

View File

@ -40,7 +40,7 @@ export function convertKubectlJsonPathToNodeJsonPath(jsonPath: string) {
let { pathExpression } = captures;
if (pathExpression.match(slashDashSearch)) {
const [first, ...rest] = pathExpression.split(pathByBareDots);
const [first, ...rest] = pathExpression.split(pathByBareDots) as [string, ...string[]];
pathExpression = `${convertToIndexNotation(first, true)}${rest.map(value => convertToIndexNotation(value)).join("")}`;
}

View File

@ -32,6 +32,10 @@ function parseKeyDownDescriptor(descriptor: string): (event: KeyboardEvent) => b
throw new Error("only single key combinations are currently supported");
}
if (!key) {
throw new Error("no key specified");
}
return (event) => {
return event.altKey === hasAlt
&& event.shiftKey === hasShift

View File

@ -17,6 +17,18 @@ type TupleOfImpl<T, N extends number, R extends unknown[]> = R["length"] extends
? R
: TupleOfImpl<T, N, [T, ...R]>;
/**
* A strict N-tuple of type T
*/
export type ReadonlyTuple<T, N extends number> = N extends N
? number extends N
? T[]
: ReadonlyTupleOfImpl<T, N, []>
: never;
type ReadonlyTupleOfImpl<T, N extends number, R extends readonly unknown[]> = R["length"] extends N
? R
: ReadonlyTupleOfImpl<T, N, readonly [T, ...R]>;
/**
* Iterates over `sources` yielding full tuples until one of the tuple arrays
* is empty. Then it returns a tuple with the rest of each of tuples

View File

@ -79,15 +79,15 @@ export function isTypedArray<T>(val: unknown, isEntry: (entry: unknown) => entry
return Array.isArray(val) && val.every(isEntry);
}
export function hasLengthGreaterThan<T, Len extends number>(x: T[], len: Len): x is Tuple<T, Len> & T[] {
export function hasLengthAtLeast<T, Len extends number>(x: T[], len: Len): x is Tuple<T, Len> & T[] {
return x.length > len;
}
export function isTruthy<T>(x: T): x is Exclude<T, Falsy> {
export function isTruthy<T>(x: T): x is Exclude<T, Falsy | void> {
return !!(x as unknown);
}
export function isFalsy<T>(x: T): x is Extract<T, Falsy> {
export function isFalsy<T>(x: T): x is Extract<T, Falsy | void> {
return !(x as unknown);
}
@ -122,6 +122,10 @@ export function isBoolean(val: unknown): val is boolean {
return typeof val === "boolean";
}
export function extractObject<T>(val: T): val is Extract<T, object> {
return typeof val === "object" && val !== null;
}
/**
* checks if val is of type object and isn't null
* @param val the value to be checked