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); assert(result.isOk === true);
expect(result.value.getCurrentContext()).toBe("minikube"); expect(result.value.getCurrentContext()).toBe("minikube");
expect(result.value.contexts.length).toBe(2); expect(result.value.contexts.length).toBe(2);
expect(result.value.contexts[0].name).toBe("minikube"); expect(result.value.contexts[0]?.name).toBe("minikube");
expect(result.value.contexts[1].name).toBe("cluster-3"); expect(result.value.contexts[1]?.name).toBe("cluster-3");
}); });
}); });
}); });

View File

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

View File

@ -5,22 +5,22 @@
import type { CatalogEntity } from "./catalog-entity"; import type { CatalogEntity } from "./catalog-entity";
import GraphemeSplitter from "grapheme-splitter"; 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+/); const byWhitespace = name.split(/\s+/);
if (byWhitespace.length > 1) { if (hasLengthAtLeast(byWhitespace, 1)) {
return byWhitespace; return byWhitespace;
} }
const byDashes = name.split(/[-_]+/); const byDashes = name.split(/[-_]+/);
if (byDashes.length > 1) { if (hasLengthAtLeast(byDashes, 1)) {
return byDashes; return byDashes;
} }
return name.split(/@+/); return name.split(/@+/) as [string, ...string[]];
} }
export function limitGraphemeLengthOf(src: string, count: number): 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 { lowerFirst } from "lodash/fp";
import statInjectable from "./stat.injectable"; 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 { function getUserReadableFileType(stats: Stats): string {
if (stats.isFile()) { if (stats.isFile()) {
@ -46,7 +46,7 @@ const validateDirectoryInjectable = getInjectable({
const stats = await stat(path); const stats = await stat(path);
if (stats.isDirectory()) { 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.` }; 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.", ENOTDIR: "a prefix of the provided path is not a directory.",
}; };
const humanReadableError = error.code const humanReadableError = humanReadableErrors[String(error.code)] ?? lowerFirst(String(error));
? humanReadableErrors[error.code]
: lowerFirst(String(error));
return { isOk: false, error: humanReadableError }; 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://", () => { 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://", () => { 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 { getInjectable } from "@ogre-tools/injectable";
import type { RawHelmChart } from "../helm-charts.api"; import type { RawHelmChart } from "../helm-charts.api";
import { HelmChart } 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"; import apiBaseInjectable from "../../api-base.injectable";
export type RequestHelmCharts = () => Promise<HelmChart[]>; export type RequestHelmCharts = () => Promise<HelmChart[]>;
@ -25,6 +25,7 @@ const requestHelmChartsInjectable = getInjectable({
return Object return Object
.values(data) .values(data)
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), new Array<RawHelmChart[]>()) .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" })) .map(([chart]) => HelmChart.create(chart, { onError: "log" }))
.filter(isDefined); .filter(isDefined);
}; };

View File

@ -19,7 +19,8 @@ const toSeparatedTupleUsing =
return [leftItem]; 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); const separator = getSeparator(leftItem, rightItem);
return [leftItem, separator]; return [leftItem, separator];

View File

@ -12,7 +12,7 @@ import type { ClusterId } from "../cluster-types";
*/ */
export function getClusterIdFromHost(host: string): ClusterId | undefined { export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345" // 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 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 [nextId, ...nextRightIds] = currentRightIds;
const nextLeftIds = [...currentLeftIds, currentId]; const nextLeftIds = [...currentLeftIds, currentId];
if (currentRightIds.length === 0 && composite.id === currentId) { if (!nextId || composite.id === currentId) {
return composite; return composite;
} }
@ -26,11 +26,11 @@ Node '${[...currentLeftIds, composite.id].join(" -> ")}' had only following chil
${composite.children.map((child) => child.id).join("\n")}`); ${composite.children.map((child) => child.id).join("\n")}`);
}; };
export const findComposite = export const findComposite = (...path: [string, ...string[]]) => (
(...path: string[]) =>
<T>(composite: Composite<T>): Composite<T> => { <T>(composite: Composite<T>): Composite<T> => {
const [currentId, ...rightIds] = path; const [currentId, ...rightIds] = path;
const leftIds: string[] = []; const leftIds: string[] = [];
return _findComposite(leftIds, currentId, rightIds, composite); return _findComposite(leftIds, currentId, rightIds, composite);
}; }
);

View File

@ -3,6 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { hasLengthAtLeast } from "@k8slens/utilities";
import { pipeline } from "@ogre-tools/fp"; import { pipeline } from "@ogre-tools/fp";
import { import {
countBy, countBy,
@ -119,11 +120,11 @@ export const getCompositeFor = <T>({
const roots = source.filter(isRootId); const roots = source.filter(isRootId);
if (roots.length > 1) { if (!hasLengthAtLeast(roots, 1)) {
const ids = roots.map(getId).join('", "');
throw new Error( throw new Error(
`Tried to get a composite, but multiple roots where encountered: "${roots `Tried to get a composite, but multiple roots where encountered: "${ids}"`,
.map(getId)
.join('", "')}"`,
); );
} }
@ -139,9 +140,12 @@ const throwMissingParentIds = ({
missingParentIds, missingParentIds,
availableParentIds, availableParentIds,
}: ParentIdsForHandling) => { }: ParentIdsForHandling) => {
throw new Error( const missingIds = missingParentIds.join('", "');
`Tried to get a composite but encountered missing parent ids: "${missingParentIds.join( const availableIds = availableParentIds.join('", "');
'", "',
)}".\n\nAvailable parent ids are:\n"${availableParentIds.join('",\n"')}"`, 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. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 => { export const findExactlyOne = <T>(predicate: (item: T) => boolean) => (collection: T[]): T => {
const itemsFound = collection.filter(predicate); 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( throw new Error(
"Tried to find exactly one, but found many", "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 asyncFn from "@async-fn/jest";
import { getPromiseStatus } from "@k8slens/test-utils"; import { getPromiseStatus } from "@k8slens/test-utils";
import logErrorInjectable from "../../log-error.injectable"; import logErrorInjectable from "../../log-error.injectable";
import type { Logger } from "../../logger"; import type { Logger } from "@k8slens/logger";
describe("with-error-logging", () => { describe("with-error-logging", () => {
describe("given decorated sync function", () => { describe("given decorated sync function", () => {

View File

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

View File

@ -225,7 +225,7 @@ describe("installing update using tray", () => {
}); });
it("when download progresses with decimals, percentage increases as integers", () => { 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( expect(
builder.tray.get("check-for-updates")?.label, builder.tray.get("check-for-updates")?.label,

View File

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

View File

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

View File

@ -220,11 +220,11 @@ describe("cluster storage technical tests", () => {
const storedClusters = clusters.get(); const storedClusters = clusters.get();
expect(storedClusters.length).toBe(3); expect(storedClusters.length).toBe(3);
expect(storedClusters[0].id).toBe("cluster1"); expect(storedClusters[0]?.id).toBe("cluster1");
expect(storedClusters[0].preferences.terminalCWD).toBe("/foo"); expect(storedClusters[0]?.preferences.terminalCWD).toBe("/foo");
expect(storedClusters[1].id).toBe("cluster2"); expect(storedClusters[1]?.id).toBe("cluster2");
expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2"); expect(storedClusters[1]?.preferences.terminalCWD).toBe("/foo2");
expect(storedClusters[2].id).toBe("cluster3"); 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", () => { 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); expect(readFileSync(configPath)).toBe(minimalValidKubeConfig);
}); });
it("migrates to modern format with icon not in file", () => { 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, name: placeholder,
extensions: fileExtensions, extensions: fileExtensions,
}, },
onPick: (filePaths) => { onPick: ([filePath]) => {
if (filePaths.length) { if (filePath) {
setValue(filePaths[0]); setValue(filePath);
} }
}, },
})} })}

View File

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

View File

@ -359,15 +359,15 @@ describe("Hotbars technical tests", () => {
}); });
it("clears cells without entity", () => { 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", () => { 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: { entity: {
name: "my-aws-cluster", name: "my-aws-cluster",
source: "local", source: "local",

View File

@ -67,7 +67,8 @@ describe("download logs options in logs dock tab", () => {
windowDi.override(reloadLogsInjectable, () => jest.fn()); windowDi.override(reloadLogsInjectable, () => jest.fn());
windowDi.override(getLogTabDataInjectable, () => () => ({ windowDi.override(getLogTabDataInjectable, () => () => ({
selectedPodId: selectedPod.getId(), 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", namespace: "default",
showPrevious: true, showPrevious: true,
showTimestamps: false, showTimestamps: false,

View File

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

View File

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

View File

@ -5,7 +5,7 @@
import type { CoreV1Api } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import { getInjectionToken } from "@ogre-tools/injectable"; import { getInjectionToken } from "@ogre-tools/injectable";
import { isRequestError } from "@k8slens/utilities"; import { hasLengthAtLeast, isRequestError } from "@k8slens/utilities";
export interface PrometheusService extends PrometheusServiceInfo { export interface PrometheusService extends PrometheusServiceInfo {
kind: string; kind: string;
@ -54,7 +54,7 @@ export async function findFirstNamespacedService(client: CoreV1Api, ...selectors
for (const selector of selectors) { for (const selector of selectors) {
const { body: { items: [service] }} = await client.listServiceForAllNamespaces(undefined, undefined, undefined, selector); 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 { return {
namespace: service.metadata.namespace, namespace: service.metadata.namespace,
service: service.metadata.name, service: service.metadata.name,
@ -73,7 +73,7 @@ export async function findNamespacedService(client: CoreV1Api, name: string, nam
try { try {
const { body: service } = await client.readNamespacedService(name, namespace); 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`); 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, name: params.account,
user: { 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 { return {
error: { error: `Failed to forward port ${port} to ${thePort ? forwardPort : "random port"}`,
message: `Failed to forward port ${port} to ${
thePort ? forwardPort : "random port"
}`,
},
}; };
} }
} }
@ -84,9 +80,7 @@ const startPortForwardRouteInjectable = getRouteInjectable({
); );
return { return {
error: { error: `Failed to forward port ${port}`,
message: `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 }); logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName });
return { return {
error: { error: `error stopping a forward port ${port}`,
message: `error stopping a forward port ${port}`,
},
}; };
} }
}); });

View File

@ -143,13 +143,14 @@ describe("CatalogEntityRegistry", () => {
entityRegistry.updateItems(items); entityRegistry.updateItems(items);
expect(entityRegistry.items.get().length).toEqual(1); 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); entityRegistry.updateItems(items);
expect(entityRegistry.items.get().length).toEqual(1); 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", () => { it("updates activeEntity", () => {
@ -170,11 +171,12 @@ describe("CatalogEntityRegistry", () => {
entityRegistry.updateItems(items); entityRegistry.updateItems(items);
entityRegistry.activeEntity = entityRegistry.items.get()[0]; 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); entityRegistry.updateItems(items);
expect(entityRegistry.activeEntity.status.phase).toEqual("connected"); expect(entityRegistry.activeEntity?.status.phase).toEqual("connected");
}); });
it("removes deleted items", () => { it("removes deleted items", () => {
@ -213,7 +215,7 @@ describe("CatalogEntityRegistry", () => {
items.splice(0, 1); items.splice(0, 1);
entityRegistry.updateItems(items); entityRegistry.updateItems(items);
expect(entityRegistry.items.get().length).toEqual(1); 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 customCategoryViews = di.inject(customCategoryViewsInjectable);
const { after = [] } = customCategoryViews.get().get("foo")?.get("bar") ?? {}; const { after = [] } = customCategoryViews.get().get("foo")?.get("bar") ?? {};
expect(after[0].View).toBe(component2); expect(after[0]?.View).toBe(component2);
expect(after[1].View).toBe(component1); expect(after[1]?.View).toBe(component1);
}); });
it("should put put priority < 50 items in before", () => { it("should put put priority < 50 items in before", () => {
@ -91,6 +91,6 @@ describe("Custom Category Views", () => {
const customCategoryViews = di.inject(customCategoryViewsInjectable); const customCategoryViews = di.inject(customCategoryViewsInjectable);
const { before = [] } = customCategoryViews.get().get("foo")?.get("bar") ?? {}; 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 // Mutating inner chart datasets to enable seamless transitions
nextDatasets.forEach((next, datasetIndex) => { 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) { if (dataset) {
const data = datasets[index].data = (datasets[index].data ?? []).slice(); // "Clean" mobx observables data to use in ChartJS const data = dataset.data = (dataset.data ?? []).slice(); // "Clean" mobx observables data to use in ChartJS
const nextData = next.data ??= []; const nextData = next.data ??= [];
data.splice(next.data.length); data.splice(next.data.length);
@ -143,10 +143,7 @@ export class Chart extends React.Component<ChartProps> {
void _data; void _data;
datasets[index] = { Object.assign(dataset, props);
...datasets[index],
...props,
};
} else { } else {
datasets[datasetIndex] = next; datasets[datasetIndex] = next;
} }

View File

@ -93,7 +93,11 @@ const NonInjectedClusterLocalTerminalSetting = observer((props: Dependencies & C
message: "Choose Working Directory", message: "Choose Working Directory",
buttonLabel: "Pick", buttonLabel: "Pick",
properties: ["openDirectory", "showHiddenFiles"], 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 React, { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { ChartOptions, ChartPoint } from "chart.js"; import type { ChartOptions } from "chart.js";
import { BarChart } from "../chart"; import { BarChart } from "../chart";
import { bytesToUnits, cssNames } from "@k8slens/utilities"; import { bytesToUnits, cssNames, isArray, isObject } from "@k8slens/utilities";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { ZebraStripesPlugin } from "../chart/zebra-stripes.plugin"; import { ZebraStripesPlugin } from "../chart/zebra-stripes.plugin";
import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterNoMetrics } from "./cluster-no-metrics";
@ -71,7 +71,11 @@ const NonInjectedClusterMetrics = observer((props: Dependencies) => {
return "<unknown>"; 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>"; return value.y?.toString() ?? "<unknown>";
}, },
@ -94,7 +98,11 @@ const NonInjectedClusterMetrics = observer((props: Dependencies) => {
return "<unknown>"; 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 }); return bytesToUnits(parseInt(value.y as string), { precision: 3 });
}, },

View File

@ -29,18 +29,18 @@ const selectedMetricsTypeInjectable = getInjectable({
switch (type) { switch (type) {
case "cpu": case "cpu":
return normalizeMetrics(rawValue.cpuUsage).data.result[0].values; return normalizeMetrics(rawValue.cpuUsage).data.result[0]?.values ?? [];
case "memory": case "memory":
return normalizeMetrics(rawValue.memoryUsage).data.result[0].values; return normalizeMetrics(rawValue.memoryUsage).data.result[0]?.values ?? [];
default: default:
return []; return [];
} }
}); });
const hasCPUMetrics = computed(() => ( 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(() => ( 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 { return {

View File

@ -32,7 +32,8 @@ describe("DockStore", () => {
it("closes tab and selects one from right", () => { it("closes tab and selects one from right", () => {
dockStore.tabs = initialTabs; 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"); expect(dockStore.selectedTabId).toBe("create");

View File

@ -169,10 +169,6 @@ export class DockStore implements DockStorageState {
this.dependencies.storage.merge({ selectedTabId: tabId }); this.dependencies.storage.merge({ selectedTabId: tabId });
} }
@computed get tabsNumber() : number {
return this.tabs.length;
}
@computed get selectedTab() { @computed get selectedTab() {
return this.tabs.find(tab => tab.id === this.selectedTabId); 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.selectedTabId === tab.id) {
if (this.tabs.length) { 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); this.selectTab(newTab.id);
} else { } else {

View File

@ -48,7 +48,8 @@ function getOnePodViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependenc
return mockLogTabViewModel(tabId, { return mockLogTabViewModel(tabId, {
getLogTabData: () => ({ getLogTabData: () => ({
selectedPodId: selectedPod.getId(), 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(), namespace: selectedPod.getNs(),
showPrevious: false, showPrevious: false,
showTimestamps: false, showTimestamps: false,
@ -76,7 +77,8 @@ const getFewPodsTabData = (tabId: TabId, deps: Partial<LogTabViewModelDependenci
name: "super-deployment", name: "super-deployment",
}, },
selectedPodId: selectedPod.getId(), 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(), namespace: selectedPod.getNs(),
showPrevious: false, showPrevious: false,
showTimestamps: false, showTimestamps: false,

View File

@ -43,7 +43,8 @@ const getOnePodViewModel = (tabId: TabId, deps: Partial<LogTabViewModelDependenc
return mockLogTabViewModel(tabId, { return mockLogTabViewModel(tabId, {
getLogTabData: () => ({ getLogTabData: () => ({
selectedPodId: selectedPod.getId(), 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(), namespace: selectedPod.getNs(),
showPrevious: false, showPrevious: false,
showTimestamps: false, showTimestamps: false,

View File

@ -26,7 +26,7 @@ export const LogControls = observer(({ model }: LogControlsProps) => {
const logs = model.timestampSplitLogs.get(); const logs = model.timestampSplitLogs.get();
const { showTimestamps, showPrevious: previous } = tabData; const { showTimestamps, showPrevious: previous } = tabData;
const since = logs.length ? logs[0][0] : null; const since = logs[0]?.[0] ?? null;
const toggleTimestamps = () => { const toggleTimestamps = () => {
model.updateLogTabData({ showTimestamps: !showTimestamps }); 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 { TabId } from "../dock/store";
import type { CreateLogsTabData } from "./create-logs-tab.injectable"; import type { CreateLogsTabData } from "./create-logs-tab.injectable";
import createLogsTabInjectable from "./create-logs-tab.injectable"; import createLogsTabInjectable from "./create-logs-tab.injectable";
import { hasLengthAtLeast } from "@k8slens/utilities";
export interface WorkloadLogsTabData { export interface WorkloadLogsTabData {
workload: StatefulSet | Job | Deployment | DaemonSet | ReplicaSet; workload: StatefulSet | Job | Deployment | DaemonSet | ReplicaSet;
@ -24,15 +25,20 @@ const createWorkloadLogsTab = ({
getPodsByOwnerId, getPodsByOwnerId,
}: Dependencies) => ({ workload }: WorkloadLogsTabData): TabId | undefined => { }: Dependencies) => ({ workload }: WorkloadLogsTabData): TabId | undefined => {
const pods = getPodsByOwnerId(workload.getId()); const pods = getPodsByOwnerId(workload.getId());
const selectedPod = pods[0];
if (pods.length === 0) { if (!selectedPod) {
return undefined; return undefined;
} }
const selectedPod = pods[0]; const containers = selectedPod.getAllContainers();
if (!hasLengthAtLeast(containers, 1)) {
return undefined;
}
return createLogsTab(`${workload.kind} ${selectedPod.getName()}`, { return createLogsTab(`${workload.kind} ${selectedPod.getName()}`, {
selectedContainer: selectedPod.getAllContainers()[0].name, selectedContainer: containers[0].name,
selectedPodId: selectedPod.getId(), selectedPodId: selectedPod.getId(),
namespace: selectedPod.getNs(), namespace: selectedPod.getNs(),
owner: { owner: {

View File

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

View File

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

View File

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

View File

@ -54,9 +54,9 @@ export class EventStore extends KubeObjectStore<KubeEvent, KubeEventApi> {
const groupsByInvolvedObject = groupBy(warnings, warning => warning.involvedObject.uid); const groupsByInvolvedObject = groupBy(warnings, warning => warning.involvedObject.uid);
const eventsWithError = Object.values(groupsByInvolvedObject).map(events => { const eventsWithError = Object.values(groupsByInvolvedObject).map(events => {
const recent = events[0]; 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); const pod = this.dependencies.getPodById(uid);
if (!pod || (!pod.hasIssues() && (pod.spec?.priority ?? 0) < 500000)) { 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 getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable";
import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import type { Disposer } from "@k8slens/utilities"; import type { Disposer } from "@k8slens/utilities";
import { noop } from "@k8slens/utilities"; import { hasLengthAtLeast, noop } from "@k8slens/utilities";
import { extensionDisplayName } from "../../../../extensions/lens-extension"; import { extensionDisplayName } from "../../../../extensions/lens-extension";
import { getMessageFromError } from "../get-message-from-error/get-message-from-error"; import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
import path from "path"; import path from "path";
@ -63,7 +63,7 @@ const unpackExtensionInjectable = getInjectable({
const unpackedFiles = await fse.readdir(unpackingTempFolder); const unpackedFiles = await fse.readdir(unpackingTempFolder);
let unpackedRootFolder = unpackingTempFolder; let unpackedRootFolder = unpackingTempFolder;
if (unpackedFiles.length === 1) { if (hasLengthAtLeast(unpackedFiles, 1)) {
// check if %extension.tgz was packed with single top folder, // check if %extension.tgz was packed with single top folder,
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball // e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]); 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`); 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 packedInRootFolder = tarFiles.every(entry => entry.startsWith(rootFolder));
const manifestLocation = packedInRootFolder const manifestLocation = packedInRootFolder
? path.join(rootFolder, manifestFilename) ? path.join(rootFolder, manifestFilename)

View File

@ -37,7 +37,11 @@ export class PageFiltersStore {
protected syncWithGlobalSearch() { protected syncWithGlobalSearch() {
const disposers = [ 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 => { reaction(() => this.dependencies.searchUrlParam.get(), search => {
const filter = this.getByType(FilterType.SEARCH); const filter = this.getByType(FilterType.SEARCH);

View File

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

View File

@ -21,7 +21,7 @@ export const IngressCharts = observer(() => {
const id = object.getId(); const id = object.getId();
const values = Object.values(metrics) const values = Object.values(metrics)
.map(normalizeMetrics) .map(normalizeMetrics)
.map(({ data }) => data.result[0].values); .map(({ data }) => data.result[0]?.values);
const [ const [
bytesSentSuccess, bytesSentSuccess,
bytesSentFailure, bytesSentFailure,
@ -36,14 +36,14 @@ export const IngressCharts = observer(() => {
label: `Bytes sent, status 2xx`, label: `Bytes sent, status 2xx`,
tooltip: `Bytes sent by Ingress controller with successful status`, tooltip: `Bytes sent by Ingress controller with successful status`,
borderColor: "#46cd9e", borderColor: "#46cd9e",
data: bytesSentSuccess.map(([x, y]) => ({ x, y })), data: bytesSentSuccess?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-bytesSentFailure`, id: `${id}-bytesSentFailure`,
label: `Bytes sent, status 5xx`, label: `Bytes sent, status 5xx`,
tooltip: `Bytes sent by Ingress controller with error status`, tooltip: `Bytes sent by Ingress controller with error status`,
borderColor: "#cd465a", borderColor: "#cd465a",
data: bytesSentFailure.map(([x, y]) => ({ x, y })), data: bytesSentFailure?.map(([x, y]) => ({ x, y })),
}, },
], ],
Duration: [ Duration: [
@ -52,14 +52,14 @@ export const IngressCharts = observer(() => {
label: `Request`, label: `Request`,
tooltip: `Request duration in seconds`, tooltip: `Request duration in seconds`,
borderColor: "#48b18d", borderColor: "#48b18d",
data: requestDurationSeconds.map(([x, y]) => ({ x, y })), data: requestDurationSeconds?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-responseDurationSeconds`, id: `${id}-responseDurationSeconds`,
label: `Response`, label: `Response`,
tooltip: `Response duration in seconds`, tooltip: `Response duration in seconds`,
borderColor: "#73ba3c", 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, podCapacity,
fsSize, fsSize,
fsUsage, 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[]>> = { const datasets: Partial<Record<MetricsTab, ChartDataSets[]>> = {
CPU: [ CPU: [
@ -53,28 +53,28 @@ const NonInjectedNodeCharts = observer((props: Dependencies) => {
label: `Usage`, label: `Usage`,
tooltip: `CPU cores usage`, tooltip: `CPU cores usage`,
borderColor: "#3D90CE", borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y })), data: cpuUsage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-cpuRequests`, id: `${id}-cpuRequests`,
label: `Requests`, label: `Requests`,
tooltip: `CPU requests`, tooltip: `CPU requests`,
borderColor: "#30b24d", borderColor: "#30b24d",
data: cpuRequests.map(([x, y]) => ({ x, y })), data: cpuRequests?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-cpuAllocatableCapacity`, id: `${id}-cpuAllocatableCapacity`,
label: `Allocatable Capacity`, label: `Allocatable Capacity`,
tooltip: `CPU allocatable capacity`, tooltip: `CPU allocatable capacity`,
borderColor: "#032b4d", borderColor: "#032b4d",
data: cpuAllocatableCapacity.map(([x, y]) => ({ x, y })), data: cpuAllocatableCapacity?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-cpuCapacity`, id: `${id}-cpuCapacity`,
label: `Capacity`, label: `Capacity`,
tooltip: `CPU capacity`, tooltip: `CPU capacity`,
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: cpuCapacity.map(([x, y]) => ({ x, y })), data: cpuCapacity?.map(([x, y]) => ({ x, y })),
}, },
], ],
Memory: [ Memory: [
@ -83,35 +83,35 @@ const NonInjectedNodeCharts = observer((props: Dependencies) => {
label: `Usage`, label: `Usage`,
tooltip: `Memory usage`, tooltip: `Memory usage`,
borderColor: "#c93dce", borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y })), data: memoryUsage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-workloadMemoryUsage`, id: `${id}-workloadMemoryUsage`,
label: `Workload Memory Usage`, label: `Workload Memory Usage`,
tooltip: `Workload memory usage`, tooltip: `Workload memory usage`,
borderColor: "#9cd3ce", borderColor: "#9cd3ce",
data: workloadMemoryUsage.map(([x, y]) => ({ x, y })), data: workloadMemoryUsage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: "memoryRequests", id: "memoryRequests",
label: `Requests`, label: `Requests`,
tooltip: `Memory requests`, tooltip: `Memory requests`,
borderColor: "#30b24d", borderColor: "#30b24d",
data: memoryRequests.map(([x, y]) => ({ x, y })), data: memoryRequests?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-memoryAllocatableCapacity`, id: `${id}-memoryAllocatableCapacity`,
label: `Allocatable Capacity`, label: `Allocatable Capacity`,
tooltip: `Memory allocatable capacity`, tooltip: `Memory allocatable capacity`,
borderColor: "#032b4d", borderColor: "#032b4d",
data: memoryAllocatableCapacity.map(([x, y]) => ({ x, y })), data: memoryAllocatableCapacity?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-memoryCapacity`, id: `${id}-memoryCapacity`,
label: `Capacity`, label: `Capacity`,
tooltip: `Memory capacity`, tooltip: `Memory capacity`,
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: memoryCapacity.map(([x, y]) => ({ x, y })), data: memoryCapacity?.map(([x, y]) => ({ x, y })),
}, },
], ],
Disk: [ Disk: [
@ -120,14 +120,14 @@ const NonInjectedNodeCharts = observer((props: Dependencies) => {
label: `Usage`, label: `Usage`,
tooltip: `Node filesystem usage in bytes`, tooltip: `Node filesystem usage in bytes`,
borderColor: "#ffc63d", borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y })), data: fsUsage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-fsSize`, id: `${id}-fsSize`,
label: `Size`, label: `Size`,
tooltip: `Node filesystem size in bytes`, tooltip: `Node filesystem size in bytes`,
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: fsSize.map(([x, y]) => ({ x, y })), data: fsSize?.map(([x, y]) => ({ x, y })),
}, },
], ],
Pods: [ Pods: [
@ -136,14 +136,14 @@ const NonInjectedNodeCharts = observer((props: Dependencies) => {
label: `Usage`, label: `Usage`,
tooltip: `Number of running Pods`, tooltip: `Number of running Pods`,
borderColor: "#30b24d", borderColor: "#30b24d",
data: podUsage.map(([x, y]) => ({ x, y })), data: podUsage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-podCapacity`, id: `${id}-podCapacity`,
label: `Capacity`, label: `Capacity`,
tooltip: `Node Pods capacity`, tooltip: `Node Pods capacity`,
borderColor: chartCapacityColor, 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 "./nodes.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-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 { TabLayout } from "../layout/tab-layout-2";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import type { Node } from "@k8slens/kube-object"; import type { Node } from "@k8slens/kube-object";
@ -104,7 +104,7 @@ class NonInjectedNodesRoute extends React.Component<Dependencies> {
private renderUsage({ node, title, metricNames, formatters }: UsageArgs) { private renderUsage({ node, title, metricNames, formatters }: UsageArgs) {
const metrics = this.getLastMetricValues(node, metricNames); const metrics = this.getLastMetricValues(node, metricNames);
if (!hasLengthGreaterThan(metrics, 2)) { if (!hasLengthAtLeast(metrics, 2)) {
return <LineProgress value={0}/>; return <LineProgress value={0}/>;
} }

View File

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

View File

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

View File

@ -29,8 +29,8 @@ const NonInjectedVolumeClaimDiskChart = observer((props: Dependencies) => {
const id = object.getId(); const id = object.getId();
const { chartCapacityColor } = activeTheme.get().colors; const { chartCapacityColor } = activeTheme.get().colors;
const { diskUsage, diskCapacity } = metrics; const { diskUsage, diskCapacity } = metrics;
const usage = normalizeMetrics(diskUsage).data.result[0].values; const usage = normalizeMetrics(diskUsage).data.result[0]?.values;
const capacity = normalizeMetrics(diskCapacity).data.result[0].values; const capacity = normalizeMetrics(diskCapacity).data.result[0]?.values;
const datasets: ChartDataSets[] = [ const datasets: ChartDataSets[] = [
{ {
@ -38,14 +38,14 @@ const NonInjectedVolumeClaimDiskChart = observer((props: Dependencies) => {
label: `Usage`, label: `Usage`,
tooltip: `Volume disk usage`, tooltip: `Volume disk usage`,
borderColor: "#ffc63d", borderColor: "#ffc63d",
data: usage.map(([x, y]) => ({ x, y })), data: usage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-diskCapacity`, id: `${id}-diskCapacity`,
label: `Capacity`, label: `Capacity`,
tooltip: `Volume disk capacity`, tooltip: `Volume disk capacity`,
borderColor: chartCapacityColor, 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 <div
{...cell.getCellProps()} {...cell.getCellProps()}
key={cell.getCellProps().key} key={cell.getCellProps().key}
className={cssNames(styles.td, columns[index].accessor)} className={cssNames(styles.td, columns[index]?.accessor)}
> >
{cell.render("Cell")} {cell.render("Cell")}
</div> </div>

View File

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

View File

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

View File

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

View File

@ -73,7 +73,8 @@ function VirtualListInner<T extends { getId(): string } | string>({
listRef.current?.scrollToItem(index, "smart"); listRef.current?.scrollToItem(index, "smart");
} }
}, [selectedItemId, [items]]); }, [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, () => ({ useImperativeHandle(forwardedRef, () => ({
scrollToItem: (index, align) => listRef.current?.scrollToItem(index, align), 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 { index, style, data } = props;
const { items, getRow } = data; const { items, getRow } = data;
const item = items[index]; const item = items[index];
if (item == null) {
return null;
}
const row = getRow?.(( const row = getRow?.((
typeof item == "string" typeof item == "string"
? index ? index
: item.getId() : item.getId()
) as never); ) as T extends string ? number : string);
if (!row) return null; if (!row) return null;

View File

@ -12,6 +12,7 @@ import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor"; import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } 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 directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import assert from "assert";
const tolerations: Toleration[] =[ const tolerations: Toleration[] =[
{ {
@ -49,6 +50,9 @@ describe("<PodTolerations />", () => {
const { container } = render(<PodTolerations tolerations={tolerations} />); const { container } = render(<PodTolerations tolerations={tolerations} />);
const rows = container.querySelectorAll(".TableRow"); const rows = container.querySelectorAll(".TableRow");
assert(rows[0]);
assert(rows[1]);
expect(rows[0].querySelector(".key")?.textContent).toBe("CriticalAddonsOnly"); expect(rows[0].querySelector(".key")?.textContent).toBe("CriticalAddonsOnly");
expect(rows[0].querySelector(".operator")?.textContent).toBe("Exist"); expect(rows[0].querySelector(".operator")?.textContent).toBe("Exist");
expect(rows[0].querySelector(".effect")?.textContent).toBe("NoExecute"); expect(rows[0].querySelector(".effect")?.textContent).toBe("NoExecute");
@ -69,6 +73,6 @@ describe("<PodTolerations />", () => {
const rows = container.querySelectorAll(".TableRow"); 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, fsUsage,
fsWrites, fsWrites,
fsReads, 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[]>> = { const datasets: Partial<Record<MetricsTab, ChartDataSets[]>> = {
CPU: [ CPU: [
@ -48,21 +48,21 @@ const NonInjectedContainerCharts = observer((props: Dependencies) => {
label: `Usage`, label: `Usage`,
tooltip: `CPU cores usage`, tooltip: `CPU cores usage`,
borderColor: "#3D90CE", borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y })), data: cpuUsage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: "cpuRequests", id: "cpuRequests",
label: `Requests`, label: `Requests`,
tooltip: `CPU requests`, tooltip: `CPU requests`,
borderColor: "#30b24d", borderColor: "#30b24d",
data: cpuRequests.map(([x, y]) => ({ x, y })), data: cpuRequests?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: "cpuLimits", id: "cpuLimits",
label: `Limits`, label: `Limits`,
tooltip: `CPU limits`, tooltip: `CPU limits`,
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: cpuLimits.map(([x, y]) => ({ x, y })), data: cpuLimits?.map(([x, y]) => ({ x, y })),
}, },
], ],
Memory: [ Memory: [
@ -71,21 +71,21 @@ const NonInjectedContainerCharts = observer((props: Dependencies) => {
label: `Usage`, label: `Usage`,
tooltip: `Memory usage`, tooltip: `Memory usage`,
borderColor: "#c93dce", borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y })), data: memoryUsage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: "memoryRequests", id: "memoryRequests",
label: `Requests`, label: `Requests`,
tooltip: `Memory requests`, tooltip: `Memory requests`,
borderColor: "#30b24d", borderColor: "#30b24d",
data: memoryRequests.map(([x, y]) => ({ x, y })), data: memoryRequests?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: "memoryLimits", id: "memoryLimits",
label: `Limits`, label: `Limits`,
tooltip: `Memory limits`, tooltip: `Memory limits`,
borderColor: chartCapacityColor, borderColor: chartCapacityColor,
data: memoryLimits.map(([x, y]) => ({ x, y })), data: memoryLimits?.map(([x, y]) => ({ x, y })),
}, },
], ],
Filesystem: [ Filesystem: [
@ -94,21 +94,21 @@ const NonInjectedContainerCharts = observer((props: Dependencies) => {
label: `Usage`, label: `Usage`,
tooltip: `Bytes consumed on this filesystem`, tooltip: `Bytes consumed on this filesystem`,
borderColor: "#ffc63d", borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y })), data: fsUsage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: "fsWrites", id: "fsWrites",
label: `Writes`, label: `Writes`,
tooltip: `Bytes written on this filesystem`, tooltip: `Bytes written on this filesystem`,
borderColor: "#ff963d", borderColor: "#ff963d",
data: fsWrites.map(([x, y]) => ({ x, y })), data: fsWrites?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: "fsReads", id: "fsReads",
label: `Reads`, label: `Reads`,
tooltip: `Bytes read on this filesystem`, tooltip: `Bytes read on this filesystem`,
borderColor: "#fff73d", 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, fsReads,
networkReceive, networkReceive,
networkTransmit, 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[]>> = { const datasets: Partial<Record<MetricsTab, ChartDataSets[]>> = {
CPU: [ CPU: [
@ -46,7 +46,7 @@ export const PodCharts = observer(() => {
label: `Usage`, label: `Usage`,
tooltip: `Container CPU cores usage`, tooltip: `Container CPU cores usage`,
borderColor: "#3D90CE", borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y })), data: cpuUsage?.map(([x, y]) => ({ x, y })),
}, },
], ],
Memory: [ Memory: [
@ -55,7 +55,7 @@ export const PodCharts = observer(() => {
label: `Usage`, label: `Usage`,
tooltip: `Container memory usage`, tooltip: `Container memory usage`,
borderColor: "#c93dce", borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y })), data: memoryUsage?.map(([x, y]) => ({ x, y })),
}, },
], ],
Network: [ Network: [
@ -64,14 +64,14 @@ export const PodCharts = observer(() => {
label: `Receive`, label: `Receive`,
tooltip: `Bytes received by all containers`, tooltip: `Bytes received by all containers`,
borderColor: "#64c5d6", borderColor: "#64c5d6",
data: networkReceive.map(([x, y]) => ({ x, y })), data: networkReceive?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-networkTransmit`, id: `${id}-networkTransmit`,
label: `Transmit`, label: `Transmit`,
tooltip: `Bytes transmitted from all containers`, tooltip: `Bytes transmitted from all containers`,
borderColor: "#46cd9e", borderColor: "#46cd9e",
data: networkTransmit.map(([x, y]) => ({ x, y })), data: networkTransmit?.map(([x, y]) => ({ x, y })),
}, },
], ],
Filesystem: [ Filesystem: [
@ -80,21 +80,21 @@ export const PodCharts = observer(() => {
label: `Usage`, label: `Usage`,
tooltip: `Bytes consumed on this filesystem`, tooltip: `Bytes consumed on this filesystem`,
borderColor: "#ffc63d", borderColor: "#ffc63d",
data: fsUsage.map(([x, y]) => ({ x, y })), data: fsUsage?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-fsWrites`, id: `${id}-fsWrites`,
label: `Writes`, label: `Writes`,
tooltip: `Bytes written on this filesystem`, tooltip: `Bytes written on this filesystem`,
borderColor: "#ff963d", borderColor: "#ff963d",
data: fsWrites.map(([x, y]) => ({ x, y })), data: fsWrites?.map(([x, y]) => ({ x, y })),
}, },
{ {
id: `${id}-fsReads`, id: `${id}-fsReads`,
label: `Reads`, label: `Reads`,
tooltip: `Bytes read on this filesystem`, tooltip: `Bytes read on this filesystem`,
borderColor: "#fff73d", 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); this.dependencies.history.searchParams.append(this.name, value);
}); });
} else { } 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[] { getRaw(): string | string[] {
const values: string[] = this.dependencies.history.searchParams.getAll(this.name); const values: string[] = this.dependencies.history.searchParams.getAll(this.name);
return this.isMulti ? values : values[0]; return this.isMulti ? values : values[0] ?? "";
} }
@action @action

View File

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

View File

@ -5,6 +5,7 @@
import assert from "assert"; import assert from "assert";
import { iter } from "./iter"; 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 // 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, ["TiB", baseMagnitude ** 4] as const,
maxMagnitude, 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; type BinaryUnit = typeof magnitudes extends Map<infer Key, any> ? Key : never;
export function unitsToBytes(value: string): number { 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; return NaN;
} }
const parsedValue = parseFloat(unitsMatch.groups.value); const parsedValue = parseFloat(unitsMatch.value);
if (!unitsMatch.groups?.suffix) { if (!unitsMatch.suffix) {
return parsedValue; return parsedValue;
} }
const magnitude = magnitudes.get(unitsMatch.groups.suffix as BinaryUnit) const magnitude = magnitudes.get(unitsMatch.suffix as BinaryUnit)
?? magnitudes.get(`${unitsMatch.groups.suffix}B` as BinaryUnit); ?? magnitudes.get(`${unitsMatch.suffix}B` as BinaryUnit);
assert(magnitude, "UnitRegex is wrong some how"); 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 = " ") { function getMeaningfulValues(values: number[], suffixes: string[], separator = " ") {
return values return values
.map((a, i): [number, string] => [a, suffixes[i]]) .map((a, i) => [a, suffixes[i]] as [number, string])
.filter(([dur]) => dur > 0) .filter(([dur]) => dur > 0)
.map(([dur, suf]) => dur + suf) .map(([dur, suf]) => dur + suf)
.join(separator); .join(separator);

View File

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

View File

@ -4,7 +4,7 @@
*/ */
import { getOrInsert } from "./collection-functions"; import { getOrInsert } from "./collection-functions";
import type { Tuple } from "./tuple"; import type { ReadonlyTuple, Tuple } from "./tuple";
export type Falsy = false | 0 | "" | null | undefined; 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; 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](); const iterating = src[Symbol.iterator]();
top: for (;;) { top: for (;;) {
@ -312,10 +317,33 @@ function chunks<T, ChunkSize extends number>(src: Iterable<T>, size: ChunkSize):
item.push(result.value); 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 = { export const iter = {
@ -337,4 +365,5 @@ export const iter = {
nth, nth,
reduce, reduce,
take, take,
zip,
}; };

View File

@ -40,7 +40,7 @@ export function convertKubectlJsonPathToNodeJsonPath(jsonPath: string) {
let { pathExpression } = captures; let { pathExpression } = captures;
if (pathExpression.match(slashDashSearch)) { 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("")}`; 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"); throw new Error("only single key combinations are currently supported");
} }
if (!key) {
throw new Error("no key specified");
}
return (event) => { return (event) => {
return event.altKey === hasAlt return event.altKey === hasAlt
&& event.shiftKey === hasShift && event.shiftKey === hasShift

View File

@ -17,6 +17,18 @@ type TupleOfImpl<T, N extends number, R extends unknown[]> = R["length"] extends
? R ? R
: TupleOfImpl<T, N, [T, ...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 * 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 * 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); 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; 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); 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); return !(x as unknown);
} }
@ -122,6 +122,10 @@ export function isBoolean(val: unknown): val is boolean {
return typeof val === "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 * checks if val is of type object and isn't null
* @param val the value to be checked * @param val the value to be checked