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:
parent
894553cdea
commit
271f8860e2
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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/");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@ -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"));
|
||||
};
|
||||
|
||||
@ -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",
|
||||
);
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -49,9 +49,9 @@ const NonInjectedHelmFileInput = ({
|
||||
name: placeholder,
|
||||
extensions: fileExtensions,
|
||||
},
|
||||
onPick: (filePaths) => {
|
||||
if (filePaths.length) {
|
||||
setValue(filePaths[0]);
|
||||
onPick: ([filePath]) => {
|
||||
if (filePath) {
|
||||
setValue(filePath);
|
||||
}
|
||||
},
|
||||
})}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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`);
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -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}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@ -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}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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 });
|
||||
},
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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];
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 })),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -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 })),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -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}/>;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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>>
|
||||
|
||||
@ -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 })),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -429,7 +429,7 @@ export const getApplicationBuilder = () => {
|
||||
return getCompositePaths(composite);
|
||||
},
|
||||
|
||||
click: (...path: string[]) => {
|
||||
click: (...path: [string, ...string[]]) => {
|
||||
const composite = mainDi.inject(
|
||||
applicationMenuItemCompositeInjectable,
|
||||
).get();
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 })),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -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 })),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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("")}`;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user