chore: Turn on @typescript-eslint/strict-boolean-expressions

- Fix some bugs about bad conditions

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton committed 2023-06-01 09:27:44 -04:00
1 parent d3f45bfe49
commit 1675c56e59
165 files changed
+853 -1141

No files matched your search

+6
View File
@@ -125,6 +125,7 @@ module.exports = {
"no-constant-condition": ["error", {
"checkLoops": false,
}],
"no-extra-boolean-cast": "off",
"header/header": [2, "../../license-header"],
"react/prop-types": "off",
"no-invalid-this": "off",
@@ -136,6 +137,11 @@ module.exports = {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/strict-boolean-expressions": ["error", {
"allowNullableString": true,
"allowNullableBoolean": true,
"allowNullableNumber": true,
}],
"no-restricted-imports": ["error", {
"paths": [
{
@@ -3,27 +3,24 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import type { AppPaths } from "./app-path-injection-token";
const appPathsStateInjectable = getInjectable({
id: "app-paths-state",
instantiate: () => {
let state: AppPaths;
let state: AppPaths | undefined;
return {
get: () =>{
if (!state) {
throw new Error("Tried to get app paths before state is setupped.");
}
assert(state, "Tried to get app paths before initialization");
return state;
},
set: (newState: AppPaths) => {
if (state) {
throw new Error("Tried to overwrite existing state of app paths.");
}
assert(!state, "Tried to overwrite existing state of app paths");
state = newState;
},
@@ -8,7 +8,7 @@ import type TypedEmitter from "typed-emitter";
import { observable, makeObservable } from "mobx";
import { once } from "lodash";
import type { Disposer, StrictReactNode } from "@k8slens/utilities";
import { iter } from "@k8slens/utilities";
import { isObject, iter } from "@k8slens/utilities";
import type { CategoryColumnRegistration, TitleCellProps } from "../../renderer/components/catalog/custom-category-columns";
import type { Literal, Metadata } from "../cluster-types";
@@ -364,15 +364,15 @@ export abstract class CatalogEntity<
constructor({ metadata, status, spec }: CatalogEntityData<Metadata, Status, Spec>) {
makeObservable(this);
if (!metadata || typeof metadata !== "object") {
if (!isObject(metadata)) {
throw new TypeError("CatalogEntity's metadata must be a defined object");
}
if (!status || typeof status !== "object") {
if (!isObject(status)) {
throw new TypeError("CatalogEntity's status must be a defined object");
}
if (!spec || typeof spec !== "object") {
if (!isObject(spec)) {
throw new TypeError("CatalogEntity's spec must be a defined object");
}
@@ -3,24 +3,21 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import type { SelfSignedCert } from "selfsigned";
const lensProxyCertificateInjectable = getInjectable({
id: "lens-proxy-certificate",
instantiate: () => {
let certState: SelfSignedCert;
let certState: SelfSignedCert | undefined;
const cert = {
get: () => {
if (!certState) {
throw "certificate has not been set";
}
assert(certState, "certificate has not been set");
return certState;
},
set: (certificate: SelfSignedCert) => {
if (certState) {
throw "certificate has already been set";
}
assert(!certState, "certificate has already been set");
certState = certificate;
},
+4 -2
View File
@@ -30,12 +30,14 @@ export function ipcMainHandle(channel: string, listener: (event: Electron.IpcMai
}
export async function broadcastMessage(channel: string, ...args: unknown[]): Promise<void> {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (ipcRenderer) {
await ipcRenderer.invoke(broadcastMainChannel, channel, ...args.map(toJS));
return;
}
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!webContents) {
return;
}
@@ -56,9 +58,9 @@ export async function broadcastMessage(channel: string, ...args: unknown[]): Pro
);
});
const views = webContents.getAllWebContents();
const views = webContents.getAllWebContents() ?? [];
if (!views || !isArray(views) || views.length === 0) return;
if (!isArray(views) || views.length === 0) return;
args = args.map(toJS);
@@ -10,7 +10,7 @@ import { autorun, action, observable } from "mobx";
import type { KubeApi } from "@k8slens/kube-api";
import type { KubeObject, ObjectReference } from "@k8slens/kube-object";
import { parseKubeApi, createKubeApiURL } from "@k8slens/kube-api";
import { getOrInsertWith, iter } from "@k8slens/utilities";
import { getOrInsertWith, isDefined, iter } from "@k8slens/utilities";
import type { CreateCustomResourceStore } from "./create-custom-resource-store.injectable";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -142,7 +142,7 @@ export class ApiManager {
*/
getStore<Store extends KubeObjectStore>(api: string | KubeApi): Store | undefined ;
getStore(apiOrBase: string | KubeApi | undefined): KubeObjectStore | undefined {
if (!apiOrBase) {
if (!isDefined(apiOrBase) || apiOrBase === "") {
return undefined;
}
@@ -152,7 +152,7 @@ export class ApiManager {
const api = apiBase && this.getApi(apiBase);
if (!api) {
if (!isDefined(api) || api === "") {
return undefined;
}
@@ -12,6 +12,7 @@ import createKubeJsonApiInjectable from "./create-kube-json-api.injectable";
import type { KubeApiOptions } from "@k8slens/kube-api";
import { KubeApi } from "@k8slens/kube-api";
import type { KubeJsonApiDataFor, KubeObject, KubeObjectConstructor } from "@k8slens/kube-object";
import { isDefined } from "@k8slens/utilities";
export interface CreateKubeApiForRemoteClusterConfig {
cluster: {
@@ -92,7 +93,7 @@ const createKubeApiForRemoteClusterInjectable = getInjectable({
serverAddress: config.cluster.server,
apiBase: "",
debug: isDevelopment,
...(token ? {
...(isDefined(token) ? {
getRequestOptions: async () => ({
headers: {
"Authorization": `Bearer ${typeof token === "function" ? await token() : token}`,
@@ -49,10 +49,10 @@ export function normalizeMetrics(metrics: MetricData | undefined | null, frames
if (frames > 0) {
// fill the gaps
result.forEach(res => {
if (!res.values || !res.values.length) return;
if (!res.values.length) return;
let now = moment().startOf("minute").subtract(1, "minute").unix();
let timestamp = res.values[0][0];
let timestamp = res.values[0]?.[0] ?? 0;
while (timestamp <= now) {
timestamp = moment.unix(timestamp).add(1, "minute").unix();
@@ -63,7 +63,7 @@ export function normalizeMetrics(metrics: MetricData | undefined | null, frames
}
while (res.values.length < frames) {
const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix();
const timestamp = moment.unix(res.values[0]?.[0] ?? 0).subtract(1, "minute").unix();
if (!res.values.find((value) => value[0] === timestamp)) {
res.values.unshift([timestamp, "0"]);
@@ -101,9 +101,11 @@ export function getItemMetrics<Keys extends string>(metrics: Partial<Record<Keys
}
const results = metrics[metric]?.data.result;
const result = results?.find(res => Object.values(res.metric)[0] == itemName);
const item = itemMetrics[metric];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
itemMetrics[metric]!.data.result = result ? [result] : [];
if (item) {
item.data.result = result ? [result] : [];
}
}
return itemMetrics;
@@ -114,7 +116,9 @@ export function getMetricLastPoints<Keys extends string>(metrics: Partial<Record
object.entries(metrics)
.map(([metricName, metric]) => {
try {
return [metricName, +metric.data.result[0].values.slice(-1)[0][1]] as const;
const [value = [0, "0"]] = metric.data.result[0]?.values.slice(-1) ?? [];
return [metricName, +value[1]] as const;
} catch {
return undefined;
}
@@ -48,14 +48,8 @@ const requestMetricsInjectable = getInjectable({
function requestMetrics<Keys extends string>(query: Record<Keys, Partial<Record<string, string>>>, params?: RequestMetricsParams): Promise<Record<Keys, MetricData>>;
async function requestMetrics(metricsQuery: string | string[] | Partial<Record<string, Partial<Record<string, string>>>>, params: RequestMetricsParams = {}): Promise<MetricData | MetricData[] | Partial<Record<string, MetricData>>> {
const { range = 3600, step = 60, namespace } = params;
let { start, end } = params;
if (!start && !end) {
const now = getSecondsFromUnixEpoch();
start = now - range;
end = now;
}
const now = getSecondsFromUnixEpoch();
const { start = now - range, end = now } = params;
return apiBase.post("/metrics", {
data: metricsQuery,
@@ -306,7 +306,7 @@ export class KubeObjectStore<
}
protected resetOnError(error: unknown) {
if (error) this.reset();
if (Boolean(error)) this.reset();
}
protected async loadItem(params: { name: string; namespace?: string }): Promise<K | null> {
@@ -122,7 +122,7 @@ export abstract class LensProtocolRouter {
return -1;
}
return countBy(b.path)["/"] - countBy(a.path)["/"];
return (countBy(b.path)["/"] ?? 0) - (countBy(a.path)["/"] ?? 0);
})[0] ?? null;
}
@@ -204,6 +204,7 @@ export abstract class LensProtocolRouter {
const extension = extensionLoader.getInstanceByName(name) as LensExtension | undefined;
if (!extension) {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
return name;
+2 -8
View File
@@ -3,13 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type KubeResource =
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" | "leases" |
"secrets" | "configmaps" | "ingresses" | "ingressclasses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" |
"pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "replicationcontrollers" | "jobs" | "cronjobs" |
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "verticalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets" |
"priorityclasses" | "runtimeclasses" |
"roles" | "clusterroles" | "rolebindings" | "clusterrolebindings" | "serviceaccounts" | "mutatingwebhookconfigurations" | "validatingwebhookconfigurations";
export type KubeResource = keyof typeof apiResourceRecord;
export interface KubeApiResource {
kind: string;
@@ -35,7 +29,7 @@ export interface KubeApiResourceData {
namespaced: boolean;
}
export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
export const apiResourceRecord = {
clusterroles: {
kind: "ClusterRole",
group: "rbac.authorization.k8s.io",
@@ -20,15 +20,16 @@ export function reactiveNow(interval?: number | "frame") {
// Note: This is the kludge until https://github.com/mobxjs/mobx-utils/issues/306 is fixed
const synchronizationIsEnabled = !process.env.JEST_WORKER_ID;
let ticker = tickers[interval];
if (!tickers[interval] || !synchronizationIsEnabled) {
if (!ticker || !synchronizationIsEnabled) {
if (typeof interval === "number")
tickers[interval] = createIntervalTicker(interval);
ticker = tickers[interval] = createIntervalTicker(interval);
else
tickers[interval] = createAnimationFrameTicker();
ticker = tickers[interval] = createAnimationFrameTicker();
}
return tickers[interval].current();
return ticker.current();
}
function createIntervalTicker(interval: number) {
@@ -109,6 +109,7 @@ export class ExtensionDiscovery {
* Initializes the class and setups the file watcher for added/removed local extensions.
*/
async init() {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (ipcRenderer) {
await this.initRenderer();
} else {
@@ -101,6 +101,7 @@ export class ExtensionLoader {
));
async init() {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (ipcMain) {
await this.initMain();
} else {
@@ -211,6 +212,7 @@ export class ExtensionLoader {
}
broadcastExtensions() {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
const channel = ipcRenderer
? extensionLoaderFromRendererChannel
: extensionLoaderFromMainChannel;
@@ -145,7 +145,9 @@ export class LensRendererExtension extends LensExtension {
*/
// eslint-disable-next-line @typescript-eslint/ban-types
async isEnabledForCluster(cluster: KubernetesCluster): Promise<Boolean> {
return Promise.resolve((void cluster) || true);
void cluster;
return Promise.resolve(true);
}
/**
@@ -146,10 +146,13 @@ export const logTabStore = Object.assign(
createWorkloadTab: asLegacyGlobalFunctionForExtensionApi(createWorkloadLogsTabInjectable),
renameTab: (tabId: string): void => {
const { selectedPodId } = logTabStore.getData(tabId) ?? {};
const pod = selectedPodId && podStore.getById(selectedPodId);
if (pod) {
renameTab(tabId, `Pod ${pod.getName()}`);
if (selectedPodId) {
const pod = podStore.getById(selectedPodId);
if (pod) {
renameTab(tabId, `Pod ${pod.getName()}`);
}
}
},
tabs: undefined,
@@ -38,12 +38,13 @@ import roleBindingApiInjectable from "../../common/k8s-api/endpoints/role-bindin
import customResourceDefinitionApiInjectable from "../../common/k8s-api/endpoints/custom-resource-definition.api.injectable";
import { shouldShowResourceInjectionToken } from "../../features/cluster/showing-kube-resources/common/allowed-resources-injection-token";
import requestMetricsInjectable from "../../common/k8s-api/endpoints/metrics.api/request-metrics.injectable";
import { maybeGet } from "@k8slens/utilities";
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
const di = getLegacyGlobalDiForExtensionApi();
return [resources].flat().every((resourceName) => {
const resource = apiResourceRecord[resourceName];
return [resources].flat().every((resourceName: string) => {
const resource = maybeGet(apiResourceRecord, resourceName);
if (!resource) {
return true;
@@ -81,11 +81,12 @@ const toRecursedInjectablesFor = (logError: (errorMessage: string) => void) => {
injectionToken: applicationMenuItemInjectionToken,
}),
...((registration.submenu as MenuRegistration[])
? (registration.submenu as MenuRegistration[]).flatMap(
toRecursedInjectables(currentIdPath),
)
: []),
...(
registration.submenu
? (registration.submenu as MenuRegistration[])
.flatMap(toRecursedInjectables(currentIdPath))
: []
),
];
};
@@ -40,10 +40,11 @@ const NonInjectedUpdateButton = observer(({ warningLevel, update, id }: UpdateBu
data-testid="update-button"
data-warning-level={level}
id={buttonId}
className={cssNames(styles.updateButton, {
[styles.warningHigh]: level === "high",
[styles.warningMedium]: level === "medium",
})}
className={cssNames(
styles.updateButton,
level === "high" && styles.warningHigh,
level === "medium" && styles.warningMedium,
)}
>
Update
<Icon material="arrow_drop_down" className={styles.icon}/>
@@ -26,7 +26,7 @@ const selectedUpdateChannelInjectable = getInjectable({
setValue: action((channelId) => {
const targetUpdateChannel =
channelId && updateChannels[channelId]
channelId && (channelId in updateChannels)
? updateChannels[channelId]
: defaultUpdateChannel;
@@ -3,6 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isTruthy } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import { globalAgent } from "https";
import { requestSystemCAsInjectionToken } from "./request-system-cas-token";
@@ -34,7 +35,7 @@ const injectSystemCAsInjectable = getInjectable({
return globalAgent.options.ca;
}
if (globalAgent.options.ca) {
if (isTruthy(globalAgent.options.ca)) {
return [globalAgent.options.ca];
}
@@ -73,13 +73,17 @@ const v500Beta10HotbarStoreMigrationInjectable = getInjectable({
{
// grab the default named hotbar or the first.
const defaultHotbarIndex = Math.max(0, hotbars.findIndex(hotbar => hotbar.name === "default"));
const [{ name, id, items }] = hotbars.splice(defaultHotbarIndex, 1);
const [hotbar] = hotbars.splice(defaultHotbarIndex, 1);
workspaceHotbars.set("default", {
name,
id,
items: items.filter(isDefined),
});
if (hotbar) {
const { name, id, items } = hotbar;
workspaceHotbars.set("default", {
name,
id,
items: items.filter(isDefined),
});
}
}
for (const cluster of clusters) {
@@ -130,7 +134,8 @@ const v500Beta10HotbarStoreMigrationInjectable = getInjectable({
const defaultHotbarIndex = hotbars.findIndex(hotbar => hotbar.name === "default");
if (defaultHotbarIndex >= 0) {
const defaultHotbar = createHotbar(hotbars[defaultHotbarIndex]);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const defaultHotbar = createHotbar(hotbars[defaultHotbarIndex]!);
if (defaultHotbar.isFull()) {
// making a new hotbar is less destructive if the first hotbar
@@ -113,11 +113,10 @@ const registratorForPreferenceItemsInjectable = getInjectable({
const items = extension.appPreferences.map((registration, i) => {
const itemId = `${commonId}-item-${registration.id ?? i}`;
const itemIsInSpecialTab =
registration.showInPreferencesTab &&
["telemetry", "application"].includes(
registration.showInPreferencesTab,
);
const itemIsInSpecialTab = (
registration.showInPreferencesTab === "telemetry"
|| registration.showInPreferencesTab === "application"
);
return getInjectable({
id: itemId,
@@ -9,6 +9,7 @@ import type { LensRendererExtension } from "../../../../extensions/lens-renderer
import { pipeline } from "@ogre-tools/fp";
import { topBarItemOnRightSideInjectionToken } from "../../../../renderer/components/layout/top-bar/top-bar-items/top-bar-item-injection-token";
import { computed } from "mobx";
import type { PartialDeep } from "type-fest";
const legacyExtensionApiRegistratorForTopBarItemsInjectable = getInjectable({
id: "legacy-extension-api-registrator-for-top-bar-items",
@@ -19,7 +20,7 @@ const legacyExtensionApiRegistratorForTopBarItemsInjectable = getInjectable({
return pipeline(
extension.topBarItems,
reject((registration) => !registration?.components?.Item),
reject((registration) => !(registration as PartialDeep<typeof registration>)?.components?.Item),
(validTopBarRegistrations) =>
validTopBarRegistrations.map((registration, index) => {
+1 -1
View File
@@ -64,7 +64,7 @@ jest.mock("@k8slens/tooltip", () => ({
void tooltipOverrideDisabled;
const ResolvedTarget = Target as React.FunctionComponent<TargetProps>;
if (tooltip) {
if (Boolean(tooltip)) {
const testId = props["data-testid"] as string | undefined;
return (
@@ -111,9 +111,7 @@ export const createClusterPrometheusHandler = (...args: [Dependencies, Cluster])
break;
case "fulfilled":
if (res.value) {
return res.value;
}
return res.value;
}
}
@@ -130,6 +128,10 @@ export const createClusterPrometheusHandler = (...args: [Dependencies, Cluster])
const prometheusPath = ensurePrometheusPath(service);
const provider = ensurePrometheusProvider(service);
if (!provider) {
return undefined;
}
return { prometheusPath, provider };
};
@@ -29,7 +29,7 @@ const clusterStoreSnapMigrationInjectable = getInjectable({
}
logger.info("Migrating embedded kubeconfig paths");
const storedClusters = (store.get("clusters") || []) as ClusterModel[];
const storedClusters = (store.get("clusters") as (ClusterModel[] | undefined) ?? []);
if (!storedClusters.length) return;
@@ -4,7 +4,7 @@
*/
import * as yaml from "js-yaml";
import { iter, put, sortBySemverVersion } from "@k8slens/utilities";
import { isObject, iter, put, sortBySemverVersion } from "@k8slens/utilities";
import type { HelmRepo } from "../../common/helm/helm-repo";
import type { HelmChartManagerCache } from "./helm-chart-manager-cache.injectable";
import type { Logger } from "@k8slens/logger";
@@ -71,11 +71,11 @@ export class HelmChartManager {
const cacheFileStats = await this.dependencies.stat(this.repo.cacheFilePath);
const data = yaml.load(cacheFile) as string | number | HelmCacheFile;
if (!data || typeof data !== "object" || typeof data.entries !== "object") {
if (!isObject(data) || !isObject(data.entries)) {
throw Object.assign(new TypeError("Helm Cache file does not parse correctly"), { file: this.repo.cacheFilePath, data });
}
const normalized = normalizeHelmCharts(this.repo.name, data.entries);
const normalized = normalizeHelmCharts(this.repo.name, data.entries as RepoHelmChartList);
return put(
this.dependencies.cache,
@@ -21,14 +21,17 @@ const getClusterForRequestInjectable = getInjectable({
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.url && req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
const cluster = getClusterById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
if (clusterId) {
const cluster = getClusterById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
}
return cluster;
}
return cluster;
}
const clusterId = getClusterIdFromHost(req.headers.host);
@@ -200,7 +200,7 @@ export class LensProxy {
this.dependencies.logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${String(error)}`, { url: req.url });
if (target) {
if (target !== undefined) {
this.dependencies.logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
@@ -245,12 +245,10 @@ export class LensProxy {
const kubeAuthProxyServer = this.dependencies.getKubeAuthProxyServer(cluster);
const proxyTarget = await kubeAuthProxyServer.getApiTarget(isLongRunningRequest(req.url));
if (proxyTarget) {
return this.dependencies.proxy.web(req, res, proxyTarget);
}
this.dependencies.proxy.web(req, res, proxyTarget);
} else {
res.setHeader("Content-Security-Policy", this.dependencies.contentSecurityPolicy);
await this.dependencies.router.route(cluster, req, res);
}
res.setHeader("Content-Security-Policy", this.dependencies.contentSecurityPolicy);
await this.dependencies.router.route(cluster, req, res);
}
}
@@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { chunk } from "lodash";
import type { ConnectionOptions } from "tls";
import { connect } from "tls";
import url from "url";
@@ -13,6 +12,7 @@ import type { LensProxyApiRequest } from "../lens-proxy";
import kubeAuthProxyServerInjectable from "../../cluster/kube-auth-proxy-server.injectable";
import kubeAuthProxyCertificateInjectable from "../../kube-auth-proxy/kube-auth-proxy-certificate.injectable";
import clusterApiUrlInjectable from "../../../features/cluster/connections/main/api-url.injectable";
import { iter } from "@k8slens/utilities";
const skipRawHeaders = new Set(["Host", "Authorization"]);
@@ -35,7 +35,7 @@ const kubeApiUpgradeRequestInjectable = getInjectable({
proxySocket.write(`${req.method} ${pUrl.path ?? ""} HTTP/1.1\r\n`);
proxySocket.write(`Host: ${clusterApiUrl.host}\r\n`);
for (const [key, value] of chunk(req.rawHeaders, 2)) {
for (const [key, value] of iter.chunks(req.rawHeaders, 2)) {
if (skipRawHeaders.has(key)) {
continue;
}
@@ -9,6 +9,7 @@ import type { InspectOptions } from "util";
import { inspect } from "util";
import { omit } from "lodash";
import type { Format, TransformableInfo } from "logform";
import { isObject, isTruthy } from "@k8slens/utilities";
// The following license was copied from https://github.com/duccio/winston-console-format/blob/master/LICENSE
// This was modified to support formatting causes
@@ -116,9 +117,15 @@ export class ConsoleFormat implements Format {
private _cause(source: unknown): string[] {
const messages: string[] = [];
if (source instanceof Error && source.cause) {
if (source instanceof Error && isTruthy(source.cause)) {
messages.push("Cause:");
messages.push(...this.getLines(omit(source.cause, "response")).map(l => ` ${l}`));
if (isObject(source.cause)) {
messages.push(...this.getLines(omit(source.cause, "response")).map(l => ` ${l}`));
} else {
messages.push(...this.getLines(source.cause).map(l => ` ${l}`));
}
messages.push(...this._cause(source.cause));
}
@@ -7,22 +7,14 @@ import { matches } from "lodash/fp";
import type { PrometheusProvider } from "./provider";
import prometheusProvidersInjectable from "./providers.injectable";
export type GetPrometheusProviderByKind = (kind: string) => PrometheusProvider;
export type GetPrometheusProviderByKind = (kind: string) => PrometheusProvider | undefined;
const getPrometheusProviderByKindInjectable = getInjectable({
id: "get-prometheus-provider-by-kind",
instantiate: (di): GetPrometheusProviderByKind => {
const providers = di.inject(prometheusProvidersInjectable);
return (kind) => {
const provider = providers.get().find(matches({ kind }));
if (!provider) {
throw new Error(`Provider of kind "${kind}" does not exist`);
}
return provider;
};
return (kind) => providers.get().find(matches({ kind }));
},
});
@@ -6,11 +6,14 @@
import type { PrometheusProvider } from "./provider";
import { createPrometheusProvider, bytesSent, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
(opts, queryName) => {
switch(opts.category) {
case "cluster":
assert(opts.nodes, 'Missing "nodes" option when "category" option is "cluster"');
switch (queryName) {
case "memoryUsage":
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (component)`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
@@ -69,6 +72,10 @@ export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
}
break;
case "pods":
assert(opts.pods, 'Missing "pods" option when "category" option is "pods"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "pods"');
assert(opts.selector, 'Missing "selector" option when "category" option is "pods"');
switch (queryName) {
case "cpuUsage":
return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`;
@@ -95,6 +102,9 @@ export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
}
break;
case "pvc":
assert(opts.pvc, 'Missing "pvc" option when "category" option is "pvc"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "pvc"');
switch (queryName) {
case "diskUsage":
return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
@@ -103,6 +113,9 @@ export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
}
break;
case "ingress":
assert(opts.ingress, 'Missing "ingress" option when "category" option is "ingress"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "ingress"');
switch (queryName) {
case "bytesSentSuccess":
return bytesSent({
@@ -126,7 +139,7 @@ export const getHelmLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
break;
}
throw new Error(`Unknown queryName="${queryName}" for category="${opts.category}"`);
throw new Error(`Unknown queryName="${queryName}" for category="${String(opts.category)}"`);
}
);
@@ -6,11 +6,14 @@
import { bytesSent, prometheusProviderInjectionToken, findNamespacedService, createPrometheusProvider } from "./provider";
import type { PrometheusProvider } from "./provider";
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
(opts, queryName) => {
switch(opts.category) {
case "cluster":
assert(opts.nodes, 'Missing "nodes" option when "category" option is "cluster"');
switch (queryName) {
case "memoryUsage":
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`);
@@ -69,6 +72,10 @@ export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
}
break;
case "pods":
assert(opts.pods, 'Missing "pods" option when "category" option is "pods"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "pods"');
assert(opts.selector, 'Missing "selector" option when "category" option is "pods"');
switch (queryName) {
case "cpuUsage":
return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`;
@@ -95,6 +102,9 @@ export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
}
break;
case "pvc":
assert(opts.pvc, 'Missing "pvc" option when "category" option is "pvc"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "pvc"');
switch (queryName) {
case "diskUsage":
return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
@@ -103,6 +113,9 @@ export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
}
break;
case "ingress":
assert(opts.ingress, 'Missing "ingress" option when "category" option is "ingress"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "ingress"');
switch (queryName) {
case "bytesSentSuccess":
return bytesSent({
@@ -126,7 +139,7 @@ export const getLensLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }):
break;
}
throw new Error(`Unknown queryName="${queryName}" for category="${opts.category}"`);
throw new Error(`Unknown queryName="${queryName}" for category="${String(opts.category)}"`);
}
);
@@ -6,11 +6,14 @@
import type { PrometheusProvider } from "./provider";
import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
(opts, queryName) => {
switch(opts.category) {
case "cluster":
assert(opts.nodes, 'Missing "nodes" option when "category" option is "cluster"');
switch (queryName) {
case "memoryUsage":
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
@@ -69,6 +72,10 @@ export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string
}
break;
case "pods":
assert(opts.pods, 'Missing "pods" option when "category" option is "pods"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "pods"');
assert(opts.selector, 'Missing "selector" option when "category" option is "pods"');
switch (queryName) {
case "cpuUsage":
return `sum(rate(container_cpu_usage_seconds_total{pod=~"${opts.pods}", namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`;
@@ -95,6 +102,9 @@ export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string
}
break;
case "pvc":
assert(opts.pvc, 'Missing "pvc" option when "category" option is "pvc"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "pvc"');
switch (queryName) {
case "diskUsage":
return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}", namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
@@ -103,6 +113,9 @@ export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string
}
break;
case "ingress":
assert(opts.ingress, 'Missing "ingress" option when "category" option is "ingress"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "ingress"');
switch (queryName) {
case "bytesSentSuccess":
return bytesSent({
@@ -126,7 +139,7 @@ export const getOperatorLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string
break;
}
throw new Error(`Unknown queryName="${queryName}" for category="${opts.category}"`);
throw new Error(`Unknown queryName="${queryName}" for category="${String(opts.category)}"`);
}
);
@@ -6,11 +6,14 @@
import type { PrometheusProvider } from "./provider";
import { bytesSent, createPrometheusProvider, findFirstNamespacedService, prometheusProviderInjectionToken } from "./provider";
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: string }): PrometheusProvider["getQuery"] => (
(opts, queryName) => {
switch(opts.category) {
case "cluster":
assert(opts.nodes, 'Missing "nodes" option when "category" option is "cluster"');
switch (queryName) {
case "memoryUsage":
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
@@ -69,6 +72,10 @@ export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: stri
}
break;
case "pods":
assert(opts.pods, 'Missing "pods" option when "category" option is "pods"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "pods"');
assert(opts.selector, 'Missing "selector" option when "category" option is "pods"');
switch (queryName) {
case "cpuUsage":
return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${rateAccuracy}])) by (${opts.selector})`;
@@ -95,6 +102,9 @@ export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: stri
}
break;
case "pvc":
assert(opts.pvc, 'Missing "pvc" option when "category" option is "pvc"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "pvc"');
switch (queryName) {
case "diskUsage":
return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
@@ -103,6 +113,9 @@ export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: stri
}
break;
case "ingress":
assert(opts.ingress, 'Missing "ingress" option when "category" option is "ingress"');
assert(opts.namespace, 'Missing "namespace" option when "category" option is "ingress"');
switch (queryName) {
case "bytesSentSuccess":
return bytesSent({
@@ -126,7 +139,7 @@ export const getStacklightLikeQueryFor = ({ rateAccuracy }: { rateAccuracy: stri
break;
}
throw new Error(`Unknown queryName="${queryName}" for category="${opts.category}"`);
throw new Error(`Unknown queryName="${queryName}" for category="${String(opts.category)}"`);
}
);
@@ -9,7 +9,7 @@ import type { LensExtension } from "../../../extensions/lens-extension";
import { observable, when } from "mobx";
import type { LensProtocolRouterDependencies, RouteAttempt } from "../../../common/protocol-handler";
import { ProtocolHandlerInvalid } from "../../../common/protocol-handler";
import { disposer, noop } from "@k8slens/utilities";
import { disposer, isTruthy, noop } from "@k8slens/utilities";
import type { BroadcastMessage } from "../../../common/ipc/broadcast-message.injectable";
export interface FallbackHandler {
@@ -79,7 +79,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
await this._routeToExtension(url);
}
} catch (error) {
void this.dependencies.broadcastMessage(ProtocolHandlerInvalid, error ? String(error) : "unknown error", rawUrl);
void this.dependencies.broadcastMessage(ProtocolHandlerInvalid, isTruthy(error) ? String(error) : "unknown error", rawUrl);
if (error instanceof proto.RoutingError) {
this.dependencies.logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${String(error)}`, { url: error.url });
@@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import type { ServerResponse } from "http";
import { loggerInjectionToken } from "@k8slens/logger";
import { object } from "@k8slens/utilities";
import { isDefined, object } from "@k8slens/utilities";
import type { LensApiRequest, Route } from "./route";
import { contentTypes } from "./router-content-types";
@@ -34,7 +34,7 @@ const writeServerResponseFor = (serverResponse: ServerResponse) => ({
if (content instanceof Buffer) {
serverResponse.write(content);
serverResponse.end();
} else if (content) {
} else if (isDefined(content)) {
serverResponse.end(content);
} else {
serverResponse.end();
@@ -67,7 +67,7 @@ const createHandlerForRouteInjectable = getInjectable({
} catch(error) {
const mappedResult = contentTypes.txt.resultMapper({
statusCode: 500,
error: error ? String(error) : "unknown error",
error: isDefined(error) ? String(error) : "unknown error",
});
logger.error(`[ROUTER]: route ${route.path}, called with ${request.path}, threw an error`, error);
+1 -1
View File
@@ -54,7 +54,7 @@ export interface ClusterLensApiRequest<Path extends string> extends LensApiReque
export interface LensApiResult<Response> {
statusCode?: number;
response?: Response;
error?: unknown;
error?: string | Error;
contentType?: LensApiResultContentType;
headers?: Partial<Record<string, string>>;
proxy?: httpProxy;
@@ -2,6 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isDefined } from "@k8slens/utilities";
import type { LensApiResult } from "./route";
export interface LensApiResultContentType {
@@ -14,19 +15,15 @@ export interface LensApiResultContentType {
const resultMapperFor =
(contentType: string): LensApiResultContentType["resultMapper"] =>
({ response, error, statusCode= error ? 400 : 200, headers = {}}) => ({
({ response, error, statusCode = isDefined(error) ? 200 : 400, headers = {}}) => ({
statusCode,
content: error || response,
content: isDefined(error) ? error : response,
headers: { ...headers, "Content-Type": contentType },
});
export type SupportedFileExtension = "json" | "txt" | "html" | "css" | "gif" | "jpg" | "png" | "svg" | "js" | "woff2" | "ttf";
export interface ContentTypes extends Record<SupportedFileExtension, LensApiResultContentType> {
[key: string]: LensApiResultContentType | undefined;
}
export const contentTypes: ContentTypes = {
export const contentTypes: Record<SupportedFileExtension, LensApiResultContentType> = {
json: {
resultMapper: (result: LensApiResult<unknown>) => {
const resultMapper = resultMapperFor("application/json");
@@ -10,6 +10,7 @@ import type { LensApiRequest } from "../../router/route";
import path from "path";
import { contentTypes } from "../../router/router-content-types";
import { prefixedLoggerInjectable } from "@k8slens/logger";
import { maybeGet } from "@k8slens/utilities";
const prodStaticFileRouteHandlerInjectable = getInjectable({
id: "prod-static-file-route-handler",
@@ -33,7 +34,7 @@ const prodStaticFileRouteHandlerInjectable = getInjectable({
}
const fileExtension = path.extname(assetFilePath).slice(1);
const contentType = contentTypes[fileExtension] || contentTypes.txt;
const contentType = maybeGet(contentTypes, fileExtension) || contentTypes.txt;
try {
return { response: await readFileBuffer(assetFilePath), contentType };
@@ -16,6 +16,7 @@ import type { CreateKubeApi } from "../../../common/k8s-api/create-kube-api.inje
import { initialNodeShellImage } from "../../../common/cluster-types";
import type { LoadProxyKubeconfig } from "../../cluster/load-proxy-kubeconfig.injectable";
import type { Pod } from "@k8slens/kube-object";
import { isDefined } from "@k8slens/utilities";
export interface NodeShellSessionArgs extends ShellSessionArgs {
nodeName: string;
@@ -67,7 +68,7 @@ export class NodeShellSession extends ShellSession {
this.send({
type: TerminalChannels.STDOUT,
data: `Error occurred: ${get(error, "response.body.message", error ? String(error) : "unknown error")}`,
data: `Error occurred: ${get(error, "response.body.message", isDefined(error) ? String(error) : "unknown error")}`,
});
throw new ShellOpenError(
@@ -271,7 +271,7 @@ export abstract class ShellSession {
}
})
.once("close", code => {
this.dependencies.logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${WebSocketCloseEvent[code]}(${code})`, { cluster: this.cluster.getMeta() });
this.dependencies.logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${String(WebSocketCloseEvent[code])}(${code})`, { cluster: this.cluster.getMeta() });
const stopShellSession = this.running
&& (
@@ -55,15 +55,11 @@ const toItemInjectablesFor = (extension: LensMainExtension, withErrorLoggingFor:
separator: registration.type === "separator",
label: computed(() => {
if (!registration.label) {
return "";
}
if (isString(registration.label)) {
return registration.label;
}
return registration.label.get();
return registration.label?.get() ?? "";
}),
tooltip: registration.toolTip,
@@ -7,7 +7,7 @@ import { computed, observable, makeObservable, action, runInAction } from "mobx"
import { ipcRendererOn } from "../../../../common/ipc";
import type { CatalogCategory, CatalogEntity, CatalogEntityData, CatalogCategoryRegistry, CatalogEntityKindData } from "../../../../common/catalog";
import type { Disposer } from "@k8slens/utilities";
import { iter } from "@k8slens/utilities";
import { isDefined, isString, iter } from "@k8slens/utilities";
import { once } from "lodash";
import { CatalogRunEvent } from "../../../../common/catalog/catalog-run-event";
import { ipcRenderer } from "electron";
@@ -60,7 +60,7 @@ export class CatalogEntityRegistry {
// If the entity was not found but there are rawEntities to be processed,
// try to process them and return the entity.
// This might happen if an extension registered a new Catalog category.
if (this.activeEntityId && !entity && this.rawEntities.length > 0) {
if (this.activeEntityId.get() && !entity && this.rawEntities.length > 0) {
this.processRawEntities();
return this.getActiveEntityById();
@@ -70,8 +70,8 @@ export class CatalogEntityRegistry {
}
set activeEntity(raw: CatalogEntity | string | undefined) {
if (raw) {
const id = typeof raw === "string"
if (isDefined(raw)) {
const id = isString(raw)
? raw
: raw.getId();
@@ -9,7 +9,7 @@ import type { ImgHTMLAttributes, MouseEventHandler } from "react";
import React from "react";
import randomColor from "randomcolor";
import type { StrictReactNode } from "@k8slens/utilities";
import { cssNames } from "@k8slens/utilities";
import { isTruthy, cssNames } from "@k8slens/utilities";
import { computeDefaultShortName } from "../../../common/catalog/helpers";
export interface AvatarProps {
@@ -44,11 +44,13 @@ export const Avatar = ({
"data-testid": dataTestId,
}: AvatarProps) => (
<div
className={cssNames(styles.Avatar, {
[styles.circle]: variant == "circle",
[styles.rounded]: variant == "rounded",
[styles.disabled]: disabled,
}, className)}
className={cssNames(
styles.Avatar,
variant == "circle" && styles.circle,
variant == "rounded" && styles.rounded,
disabled && styles.disabled,
className,
)}
style={{
width: `${size}px`,
height: `${size}px`,
@@ -62,14 +64,18 @@ export const Avatar = ({
onClick={onClick}
data-testid={dataTestId}
>
{src
? (
<img
src={src}
{...imgProps}
alt={title}
/>
)
: children || computeDefaultShortName(title)}
{
src
? (
<img
src={src}
{...imgProps}
alt={title}
/>
)
: isTruthy(children)
? children
: computeDefaultShortName(title)
}
</div>
);
@@ -63,15 +63,17 @@ export const Badge = withTooltip(observer(({
return (
<div
{...elemProps}
className={cssNames(styles.badge, className, {
[styles.small]: small,
[styles.flat]: flat,
[styles.clickable]: Boolean(elemProps.onClick) || isExpandable,
[styles.interactive]: isExpandable,
[styles.isExpanded]: isExpanded,
[styles.disabled]: disabled,
[styles.scrollable]: scrollable,
})}
className={cssNames(
styles.badge,
className,
small && styles.small,
flat && styles.flat,
(Boolean(elemProps.onClick) || isExpandable) && styles.clickable,
isExpandable && styles.interactive,
isExpanded && styles.isExpanded,
disabled && styles.disabled,
scrollable && styles.scrollable,
)}
onMouseUp={onMouseUp}
ref={elem}
>
@@ -11,9 +11,7 @@ import { observer } from "mobx-react";
import { observable, action } from "mobx";
import type { CatalogCategory, CatalogEntityAddMenu } from "../../api/catalog-entity";
import { EventEmitter } from "events";
import type { CatalogCategoryRegistry } from "../../../common/catalog";
import { withInjectables } from "@ogre-tools/injectable-react";
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
import type { Navigate } from "../../navigation/navigate.injectable";
import navigateInjectable from "../../navigation/navigate.injectable";
@@ -24,7 +22,6 @@ export interface CatalogAddButtonProps {
type CategoryId = string;
interface Dependencies {
catalogCategoryRegistry: CatalogCategoryRegistry;
navigate: Navigate;
}
@@ -43,19 +40,9 @@ class NonInjectedCatalogAddButton extends React.Component<CatalogAddButtonProps
}
}
get categories() {
return this.props.catalogCategoryRegistry.filteredItems;
}
updateMenuItems = action(() => {
this.menuItems.clear();
if (this.props.category) {
this.updateCategoryItems(this.props.category);
} else {
// Show menu items from all categories
this.categories.forEach(this.updateCategoryItems);
}
this.updateCategoryItems(this.props.category);
});
updateCategoryItems = action((category: CatalogCategory) => {
@@ -84,16 +71,13 @@ class NonInjectedCatalogAddButton extends React.Component<CatalogAddButtonProps
onButtonClick = () => {
const defaultAction = this.items.find(item => item.defaultAction)?.onClick;
const clickAction = defaultAction || (this.items.length === 1 ? this.items[0].onClick : null);
const clickAction = defaultAction ?? this.items[0]?.onClick;
void clickAction?.();
};
get items() {
const { category } = this.props;
return category ? this.getCategoryFilteredItems(category) :
this.categories.map(this.getCategoryFilteredItems).flat();
return this.getCategoryFilteredItems(this.props.category);
}
render() {
@@ -136,7 +120,6 @@ class NonInjectedCatalogAddButton extends React.Component<CatalogAddButtonProps
export const CatalogAddButton = withInjectables<Dependencies, CatalogAddButtonProps>(NonInjectedCatalogAddButton, {
getProps: (di, props) => ({
...props,
catalogCategoryRegistry: di.inject(catalogCategoryRegistryInjectable),
navigate: di.inject(navigateInjectable),
}),
});
@@ -2,6 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isTruthy } from "@k8slens/utilities";
import React from "react";
import type { CatalogCategory } from "../../../common/catalog/catalog-entity";
@@ -18,7 +19,7 @@ export const CatalogCategoryLabel = ({ category }: CatalogCategoryLabelProps) =>
return (
<div className="flex">
<div>{category.metadata.name}</div>
{badge ? (<div className="flex items-center">{badge}</div>) : null}
{isTruthy(badge) ? (<div className="flex items-center">{badge}</div>) : null}
</div>
);
};
@@ -43,10 +43,6 @@ class NonInjectedCatalogEntityDrawerMenu<T extends CatalogEntity> extends React.
}
getMenuItems(entity: T): React.ReactChild[] {
if (!entity) {
return [];
}
const items = this.menuItems
.map(this.props.normalizeMenuItem)
.filter(hasDefiniteField("icon"))
@@ -15,22 +15,26 @@ const kubernetesClusterDetailsItemInjectable = getInjectable({
kind: KubernetesCluster.kind,
orderNumber: 40,
components: {
Details: ({ entity }) => (
<>
<DrawerTitle>Kubernetes Information</DrawerTitle>
<div className="box grow EntityMetadata">
<DrawerItem
name="Distribution"
data-testid={`kubernetes-distro-for-${entity.getId()}`}
>
{entity.metadata.distro || "unknown"}
</DrawerItem>
<DrawerItem name="Kubelet Version">
{entity.metadata.kubeVersion || "unknown"}
</DrawerItem>
</div>
</>
),
Details: ({ entity }) => {
const kube = entity as KubernetesCluster;
return (
<>
<DrawerTitle>Kubernetes Information</DrawerTitle>
<div className="box grow EntityMetadata">
<DrawerItem
name="Distribution"
data-testid={`kubernetes-distro-for-${entity.getId()}`}
>
{kube.metadata.distro || "unknown"}
</DrawerItem>
<DrawerItem name="Kubelet Version">
{kube.metadata.kubeVersion || "unknown"}
</DrawerItem>
</div>
</>
);
},
},
}),
injectionToken: catalogEntityDetailItemInjectionToken,
@@ -137,7 +137,7 @@ const NonInjectedBarChart = observer(({
typeof datasetIndex === "number"
? {
borderColor: "darkgray",
backgroundColor: datasets[datasetIndex].borderColor as string,
backgroundColor: datasets[datasetIndex]?.borderColor as string,
}
: {
borderColor: "darkgray",
@@ -181,7 +181,7 @@ const tooltipCallbackWith = (precision: number): ChartTooltipCallback["label"] =
return "";
}
const { label, data } = datasets[datasetIndex];
const { label, data } = datasets[datasetIndex] ?? {};
if (!label || !data) {
return "<unknown>";
@@ -36,7 +36,7 @@ const memoryLikeOptions: ChartOptions = {
return "";
}
const { label = "", data } = datasets[datasetIndex];
const { label = "", data } = datasets[datasetIndex] ?? {};
if (!data) {
return label;
@@ -78,7 +78,7 @@ export const metricTabOptions: Record<MetricsTab, ChartOptions> = {
return "";
}
const { label = "", data } = datasets[datasetIndex];
const { label = "", data } = datasets[datasetIndex] ?? {};
if (!data) {
return label;
@@ -106,7 +106,7 @@ export const metricTabOptions: Record<MetricsTab, ChartOptions> = {
return "";
}
const { label = "", data } = datasets[datasetIndex];
const { label = "", data } = datasets[datasetIndex] ?? {};
if (!data) {
return label;
@@ -134,7 +134,7 @@ export const metricTabOptions: Record<MetricsTab, ChartOptions> = {
return "";
}
const { label = "", data } = datasets[datasetIndex];
const { label = "", data } = datasets[datasetIndex] ?? {};
if (!data) {
return label;
@@ -80,7 +80,7 @@ const NonInjectedPieChart = observer(({
return false;
}
const { data = [] } = datasets[datasetIndex];
const { data = [] } = datasets[datasetIndex] ?? {};
if (datasets.length === 1) return true;
@@ -6,7 +6,7 @@
import "./checkbox.scss";
import React from "react";
import type { StrictReactNode } from "@k8slens/utilities";
import { cssNames, noop } from "@k8slens/utilities";
import { isTruthy, cssNames, noop } from "@k8slens/utilities";
export interface CheckboxProps {
className?: string;
@@ -35,7 +35,7 @@ export function Checkbox({ label, inline, className, value, children, onChange =
onChange={event => onChange(event.target.checked, event)}
/>
<i className="box flex align-center"/>
{label ? <span className="label">{label}</span> : null}
{isTruthy(label) ? <span className="label">{label}</span> : null}
{children}
</label>
);
@@ -16,7 +16,7 @@ import { TopBar } from "../layout/top-bar/top-bar";
import type { IComputedValue } from "mobx";
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
import welcomeRouteInjectable from "../../../common/front-end-routing/routes/welcome/welcome-route.injectable";
import { buildURL } from "@k8slens/utilities";
import { buildURL, isDefined } from "@k8slens/utilities";
import type { WatchForGeneralEntityNavigation } from "../../api/helpers/watch-for-general-entity-navigation.injectable";
import watchForGeneralEntityNavigationInjectable from "../../api/helpers/watch-for-general-entity-navigation.injectable";
import currentPathInjectable from "../../routes/current-path.injectable";
@@ -39,7 +39,7 @@ class NonInjectedClusterManager extends React.Component<Dependencies> {
renderMainComponent() {
const Component = this.props.currentRouteComponent.get();
if (Component) {
if (isDefined(Component)) {
return <Component />;
}
@@ -59,7 +59,7 @@ class NonInjectedClusterPrometheusSetting extends React.Component<ClusterPrometh
}
@computed get canEditPrometheusPath(): boolean {
if (!this.selectedOption || this.selectedOption === autoDetectPrometheus) {
if (this.selectedOption === autoDetectPrometheus) {
return false;
}
@@ -96,7 +96,7 @@ class NonInjectedClusterPrometheusSetting extends React.Component<ClusterPrometh
}
parsePrometheusPath = () => {
if (!this.selectedOption || !this.path) {
if (!this.path) {
return undefined;
}
const parsed = this.path.split(/\/|:/, 3);
@@ -14,7 +14,7 @@ import navigateToEntitySettingsInjectable from "../../../common/front-end-routin
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
export interface ClusterNoMetricsProps {
className: string;
className?: string;
}
interface Dependencies {
@@ -212,9 +212,7 @@ function getInternalCommands(dependencies: Dependencies): CommandRegistration[]
id: "entity.viewSettings",
title: ({ entity }) => `${entity.kind}/${entity.getName()}: View Settings`,
action: ({ entity }) => dependencies.navigateToEntitySettings(entity.getId()),
isActive: ({ entity }) => (
entity && dependencies.hasCatalogEntitySettingItems(entity)
),
isActive: ({ entity }) => dependencies.hasCatalogEntitySettingItems(entity),
},
{
id: "cluster.openTerminal",
@@ -54,7 +54,7 @@ class NonInjectedHorizontalPodAutoscalerDetails extends React.Component<KubeObje
}
renderMetrics(hpa: HorizontalPodAutoscaler) {
const renderName = (metric: HorizontalPodAutoscalerMetricSpec) => {
const renderName = (metric: HorizontalPodAutoscalerMetricSpec | undefined) => {
const metricName = getMetricName(metric);
switch (metric?.type) {
@@ -95,7 +95,7 @@ class NonInjectedHorizontalPodAutoscalerDetails extends React.Component<KubeObje
this.props.getMetrics(hpa)
.map((metrics, index) => (
<TableRow key={index}>
<TableCell className="name">{renderName(hpa.getMetrics()[index])}</TableCell>
<TableCell className="name">{renderName(hpa.getMetrics()?.[index])}</TableCell>
<TableCell className="metrics">{metrics}</TableCell>
</TableRow>
))
@@ -107,10 +107,6 @@ class NonInjectedHorizontalPodAutoscalerDetails extends React.Component<KubeObje
render() {
const { object: hpa, apiManager, getDetailsUrl, logger } = this.props;
if (!hpa) {
return null;
}
if (!(hpa instanceof HorizontalPodAutoscaler)) {
logger.error("[HpaDetails]: passed object that is not an instanceof HorizontalPodAutoscaler", hpa);
@@ -59,10 +59,6 @@ class NonInjectedLimitRangeDetails extends React.Component<KubeObjectDetailsProp
render() {
const { object: limitRange, logger } = this.props;
if (!limitRange) {
return null;
}
if (!(limitRange instanceof LimitRange)) {
logger.error("[LimitRangeDetails]: passed object that is not an instanceof LimitRange", limitRange);
@@ -44,9 +44,7 @@ class NonInjectedConfigMapDetails extends React.Component<KubeObjectDetailsProps
autorun(() => {
const configMap = this.props.object as ConfigMap;
if (configMap) {
this.data.replace(configMap.data); // refresh
}
this.data.replace(configMap.data); // refresh
}),
]);
}
@@ -79,10 +77,6 @@ class NonInjectedConfigMapDetails extends React.Component<KubeObjectDetailsProps
render() {
const { object: configMap, logger } = this.props;
if (!configMap) {
return null;
}
if (!(configMap instanceof ConfigMap)) {
logger.error("[ConfigMapDetails]: passed object that is not an instanceof ConfigMap", configMap);
@@ -144,13 +144,11 @@ export const WebhookConfig: React.FC<WebhookProps> = ({ webhook }) => {
{" "}
{rule.operations.join(", ")}
</div>
{rule.resources && (
<div>
Resources:
{" "}
{rule.resources.join(", ")}
</div>
)}
<div>
Resources:
{" "}
{rule.resources?.join(", ")}
</div>
{rule.scope && (
<div>
Scope:
@@ -83,10 +83,6 @@ class NonInjectedResourceQuotaDetails extends React.Component<KubeObjectDetailsP
render() {
const { object: quota } = this.props;
if (!quota) {
return null;
}
if (!(quota instanceof ResourceQuota)) {
this.props.logger.error("[ResourceQuotaDetails]: passed object that is not an instanceof ResourceQuota", quota);
@@ -48,10 +48,8 @@ class NonInjectedSecretDetails extends React.Component<KubeObjectDetailsProps &
autorun(() => {
const secret = this.props.object as Secret;
if (secret) {
this.data = secret.data;
this.revealSecret.clear();
}
this.data = secret.data;
this.revealSecret.clear();
}),
]);
}
@@ -145,10 +143,6 @@ class NonInjectedSecretDetails extends React.Component<KubeObjectDetailsProps &
render() {
const { object: secret, logger } = this.props;
if (!secret) {
return null;
}
if (!(secret instanceof Secret)) {
logger.error("[SecretDetails]: passed object that is not an instanceof Secret", secret);
@@ -161,10 +161,6 @@ class NonInjectedVpaDetails extends React.Component<KubeObjectDetailsProps & Dep
render() {
const { object: vpa, apiManager, getDetailsUrl, logger } = this.props;
if (!vpa) {
return null;
}
if (!(vpa instanceof VerticalPodAutoscaler)) {
logger.error("[VpaDetails]: passed object that is not an instanceof VerticalPodAutoscaler", vpa);
@@ -186,10 +182,7 @@ class NonInjectedVpaDetails extends React.Component<KubeObjectDetailsProps & Dep
</DrawerItem>
<DrawerItem name="Recommender">
{
/* according to the spec there can be 0 or 1 recommenders, only */
recommenders?.length ? recommenders[0].name : "default"
}
{recommenders?.[0]?.name ?? "default"}
</DrawerItem>
{vpa.status && this.renderStatus(vpa, vpa.status)}
@@ -28,10 +28,6 @@ class NonInjectedCustomResourceDefinitionDetails extends React.Component<KubeObj
render() {
const { object: crd } = this.props;
if (!crd) {
return null;
}
if (!(crd instanceof CustomResourceDefinition)) {
this.props.logger.error("[CustomResourceDefinitionDetails]: passed object that is not an instanceof CustomResourceDefinition", crd);
@@ -110,9 +110,9 @@ class NonInjectedCustomResourceDetails extends React.Component<CustomResourceDet
}
render() {
const { props: { object, crd, logger }} = this;
const { object, crd, logger } = this.props;
if (!object || !crd) {
if (!crd) {
return null;
}
@@ -9,6 +9,7 @@ import type { CreateStorage } from "../../../utils/create-storage/create-storage
import type { TabId } from "../dock/store";
import autoBind from "auto-bind";
import { toJS } from "../../../../common/utils";
import { isTruthy } from "@k8slens/utilities";
export interface DockTabStoreOptions {
autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true
@@ -59,7 +60,7 @@ export class DockTabStore<T> {
findTabIdFromData(inspector: (val: T) => unknown): TabId | undefined {
for (const [tabId, data] of this.data) {
if (inspector(data)) {
if (isTruthy(inspector(data))) {
return tabId;
}
}
@@ -78,10 +78,6 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
render() {
const { className, moreActions, dockStore, active, isMac, ...tabProps } = this.props;
if (!tabProps.value) {
return;
}
void dockStore;
void active;
void isMac;
@@ -94,9 +90,11 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
<Tab
{...tabProps}
id={`tab-${id}`}
className={cssNames(styles.DockTab, className, {
[styles.pinned]: pinned,
})}
className={cssNames(
styles.DockTab,
className,
pinned && styles.pinned,
)}
onContextMenu={() => this.menuVisible.set(true)}
label={(
<div className="flex align-center" onAuxClick={isMiddleClick(close)}>
@@ -93,11 +93,11 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
const currentIndex = tabs.indexOf(selectedTab);
const nextIndex = currentIndex + direction;
// check if moving to the next or previous tab is possible.
if (nextIndex >= tabs.length || nextIndex < 0) return;
const nextElement = tabs[nextIndex];
// check if moving to the next or previous tab is possible.
if (!nextElement) return;
this.onChangeTab(nextElement);
};
@@ -9,7 +9,7 @@ import React, { Component } from "react";
import { computed, observable, reaction, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import type { StrictReactNode } from "@k8slens/utilities";
import { cssNames } from "@k8slens/utilities";
import { cssNames, isTruthy } from "@k8slens/utilities";
import { Button } from "@k8slens/button";
import { Icon } from "@k8slens/icon";
import { Spinner } from "../spinner";
@@ -90,7 +90,7 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
try {
const result = await this.props.submit?.();
if (showNotifications && result) {
if (showNotifications && isTruthy(result)) {
this.props.showSuccessNotification(result);
}
@@ -109,7 +109,7 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
submitAndClose = async () => {
const result = await this.submit();
if (result) {
if (isTruthy(result)) {
this.close();
}
};
@@ -32,7 +32,7 @@ export const LogSearch = observer((props: PodLogSearchProps) => {
const logs = tabData.showTimestamps
? model.logs.get()
: model.logsWithoutTimestamps.get();
const jumpDisabled = !searchQuery || !occurrences.length;
const jumpDisabled = !searchQuery.get() || !occurrences.length;
const setSearch = (query: string) => {
searchStore.onSearch(logs, query);
@@ -4,7 +4,6 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { when } from "mobx";
import { loggerInjectionToken } from "@k8slens/logger";
import { TerminalChannels } from "../../../../common/terminal/channels";
import { waitUntilDefined, noop } from "@k8slens/utilities";
import showSuccessNotificationInjectable from "../../notifications/show-success-notification.injectable";
@@ -40,7 +39,6 @@ const sendCommandInjectable = getInjectable({
const selectTab = di.inject(selectDockTabInjectable);
const getTerminalApi = di.inject(getTerminalApiInjectable);
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
const logger = di.inject(loggerInjectionToken);
return async (command: string, options: SendCommandOptions = {}): Promise<void> => {
let tabId: string | undefined = options.tabId;
@@ -70,21 +68,14 @@ const sendCommandInjectable = getInjectable({
await shellIsReady.catch(noop);
clearTimeout(notifyVeryLong);
if (terminalApi) {
if (options.enter) {
command += "\r";
}
terminalApi.sendMessage({
type: TerminalChannels.STDIN,
data: command,
});
} else {
logger.warn(
"The selected tab is does not have a connection. Cannot send command.",
{ tabId, command },
);
if (options.enter) {
command += "\r";
}
terminalApi.sendMessage({
type: TerminalChannels.STDIN,
data: command,
});
};
},
});
@@ -44,13 +44,11 @@ export class Terminal {
protected readonly api: TerminalApi;
private get elem() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.xterm.element!;
return this.xterm.element;
}
private get viewport() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.elem.querySelector(".xterm-viewport")!;
return this.elem?.querySelector(".xterm-viewport");
}
attachTo(parentElem: HTMLElement) {
@@ -99,8 +97,10 @@ export class Terminal {
const onDataHandler = this.xterm.onData(this.onData);
const clearOnce = once(this.onClear);
this.viewport.addEventListener("scroll", this.onScroll);
this.elem.addEventListener("contextmenu", this.onContextMenu);
const { viewport, elem } = this;
viewport?.addEventListener("scroll", this.onScroll);
elem?.addEventListener("contextmenu", this.onContextMenu);
this.api.once("ready", clearOnce);
this.api.once("connected", clearOnce);
this.api.on("data", this.onApiData);
@@ -128,7 +128,8 @@ export class Terminal {
() => this.fitAddon.dispose(),
() => this.api.removeAllListeners(),
() => window.removeEventListener("resize", this.onResize),
() => this.elem.removeEventListener("contextmenu", this.onContextMenu),
() => elem?.removeEventListener("contextmenu", this.onContextMenu),
() => viewport?.removeEventListener("scroll", this.onScroll),
this.xterm.onResize(({ cols, rows }) => {
this.api.sendTerminalSize(cols, rows);
}),
@@ -161,7 +162,7 @@ export class Terminal {
};
onScroll = () => {
this.scrollPos = this.viewport.scrollTop;
this.scrollPos = this.viewport?.scrollTop ?? 0;
};
onClear = () => {
@@ -176,7 +177,12 @@ export class Terminal {
onActivate = () => {
this.fit();
setTimeout(() => this.focus(), 250); // delay used to prevent focus on active tab
this.viewport.scrollTop = this.scrollPos; // restore last scroll position
const { viewport } = this;
if (viewport) {
viewport.scrollTop = this.scrollPos; // restore last scroll position
}
};
onContextMenu = () => {
@@ -10,7 +10,7 @@ import { Badge } from "../badge";
import { KubeObject } from "@k8slens/kube-object";
export interface DrawerItemLabelsProps extends DrawerItemProps {
labels: string[] | Partial<Record<string, string>>;
labels?: string[] | Partial<Record<string, string>>;
}
export function DrawerItemLabels(props: DrawerItemLabelsProps) {
@@ -28,10 +28,12 @@ export interface DrawerTitleProps {
export function DrawerTitle({ className, children, size = "title" }: DrawerTitleProps) {
return (
<div
className={cssNames(styles.DrawerTitle, className, {
[styles.title]: size === "title",
[styles["sub-title"]]: size === "sub-title",
})}
className={cssNames(
styles.DrawerTitle,
className,
size === "title" && styles.title,
size === "sub-title" && styles["sub-title"],
)}
>
{children}
</div>
@@ -36,10 +36,6 @@ const NonInjectedEventDetails = observer(({
className,
logger,
}: Dependencies & KubeObjectDetailsProps) => {
if (!event) {
return null;
}
if (!(event instanceof KubeEvent)) {
logger.error("[EventDetails]: passed object that is not an instanceof KubeEvent", event);
@@ -43,10 +43,6 @@ class NonInjectedKubeEventDetails extends React.Component<KubeEventDetailsProps
render() {
const { object, eventStore } = this.props;
if (!object) {
return null;
}
if (!(object instanceof KubeObject)) {
this.props.logger.error("[KubeEventDetails]: passed object that is not an instanceof KubeObject", object);
@@ -64,7 +60,7 @@ class NonInjectedKubeEventDetails extends React.Component<KubeEventDetailsProps
<div className={styles.KubeEventDetails}>
{events.map(event => (
<div className={styles.event} key={event.getId()}>
<div className={cssNames(styles.title, { [styles.warning]: event.isWarning() })}>
<div className={cssNames(styles.title, event.isWarning() && styles.warning)}>
{event.message}
</div>
<DrawerItem name="Source">
@@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isObject } from "@k8slens/utilities";
import { isObject, isTruthy } from "@k8slens/utilities";
import React from "react";
import { SemVer } from "semver";
import URLParse from "url-parse";
@@ -76,8 +76,8 @@ const attemptInstallByInfoInjectable = getInjectable({
return disposer();
}
if (result.value.error || !isObject(result.value.versions)) {
const message = result.value.error ? `: ${String(result.value.error)}` : "";
if (isTruthy(result.value.error) || !isObject(result.value.versions)) {
const message = isTruthy(result.value.error) ? `: ${String(result.value.error)}` : "";
showErrorNotification(`Failed to get registry information for extension${message}`);
@@ -3,10 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { hasTypedProperty, isDefined } from "@k8slens/utilities";
import { hasTypedProperty, isDefined, isObject } from "@k8slens/utilities";
export function getMessageFromError(error: unknown): string {
if (!error || typeof error !== "object") {
if (!isObject(error)) {
return "an error has occurred";
}
@@ -51,7 +51,7 @@ const NonInjectedInstalledExtensions = observer(({
enableExtension,
disableExtension,
}: Dependencies) => {
if (!extensionDiscovery.isLoaded) {
if (!extensionDiscovery.isLoaded.get()) {
return <div><Spinner center /></div>;
}
@@ -85,8 +85,8 @@ const NonInjectedInstalledExtensions = observer(({
accessor: "extension",
width: 200,
sortType: (rowA, rowB) => { // Custom sorting for extension name
const nameA = extensions[rowA.index].manifest.name;
const nameB = extensions[rowB.index].manifest.name;
const nameA = extensions[rowA.index]?.manifest.name ?? "";
const nameB = extensions[rowB.index]?.manifest.name ?? "";
if (nameA > nameB) return -1;
if (nameB > nameA) return 1;
@@ -126,7 +126,12 @@ const NonInjectedInstalledExtensions = observer(({
),
version,
status: (
<div className={cssNames({ [styles.enabled]: isEnabled, [styles.invalid]: !isCompatible })}>
<div
className={cssNames(
isEnabled && styles.enabled,
!isCompatible && styles.invalid,
)}
>
{getStatus(extension)}
</div>
),
@@ -25,7 +25,6 @@ import versionsOfSelectedHelmChartInjectable from "./details/versions-of-selecte
import type { HelmChartDetailsVersionSelection } from "./details/versions/helm-chart-details-version-selection.injectable";
import helmChartDetailsVersionSelectionInjectable from "./details/versions/helm-chart-details-version-selection.injectable";
import assert from "assert";
import autoBindReact from "auto-bind/react";
export interface HelmChartDetailsProps {
hideDetails(): void;
@@ -47,15 +46,6 @@ interface Dependencies {
@observer
class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Dependencies> {
constructor(props: HelmChartDetailsProps & Dependencies) {
super(props);
autoBindReact(this);
}
get chart() {
return this.props.chart;
}
install = () => {
const chart = this.props.versionSelection.value.get();
@@ -144,13 +134,13 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
const readmeIsLoading = this.props.readme.pending.get();
const versionsAreLoading = this.props.versions.pending.get();
if (!this.chart || versionsAreLoading) {
if (versionsAreLoading) {
return <Spinner center data-testid="spinner-for-chart-details" />;
}
return (
<div className="box grow">
{this.renderIntroduction(this.chart)}
{this.renderIntroduction(this.props.chart)}
{readmeIsLoading ? (
<Spinner center data-testid="spinner-for-chart-readme" />
@@ -162,12 +152,13 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
}
render() {
return (
<Drawer
className="HelmChartDetails"
usePortal={true}
open={!!this.chart}
title={this.chart ? `Chart: ${this.chart.getFullName()}` : ""}
usePortal
open
title={`Chart: ${this.props.chart.getFullName()}`}
onClose={this.props.hideDetails}
>
{this.renderContent()}
@@ -31,7 +31,7 @@ export const HelmChartIcon = ({
return (
<div
className={cssNames(styles.chartIcon, className, { [styles.imageNotLoaded]: !isImageLoaded })}
className={cssNames(styles.chartIcon, className, !isImageLoaded && styles.imageNotLoaded)}
data-testid="image-container"
style={{
backgroundImage: isImageLoaded ? backgroundImage : `url(${placeholderImageUrl})`,
@@ -46,12 +46,9 @@ class NonInjectedHelmReleaseMenu extends React.Component<HelmReleaseMenuProps &
renderContent() {
const { release, toolbar } = this.props;
if (!release) return null;
const hasRollback = release && release.getRevision() > 1;
return (
<>
{hasRollback && (
{(release.getRevision() > 1) && (
<MenuItem onClick={this.rollback}>
<Icon
material="history"
@@ -69,10 +69,10 @@ class NonInjectedHotbarEntityIcon extends React.Component<HotbarEntityIconProps
return (
<div
className={cssNames(styles.led, {
// TODO: make it more generic
[styles.online]: this.props.entity.status.phase === LensKubernetesClusterStatus.CONNECTED,
})}
className={cssNames(
styles.led,
(this.props.entity.status.phase === LensKubernetesClusterStatus.CONNECTED) && styles.online,
)}
/>
);
}
@@ -107,7 +107,6 @@ class NonInjectedHotbarEntityIcon extends React.Component<HotbarEntityIconProps
className={className}
active={this.isActive(entity)}
onMenuOpen={() => this.onMenuOpen()}
disabled={!entity}
menuItems={this.menuItems}
tooltip={(
entity.metadata.source
@@ -50,7 +50,7 @@ const NonInjectedHotbarIcon = observer(({
};
return (
<div className={cssNames(styles.HotbarIcon, className, { [styles.contextMenuAvailable]: menuItems.length > 0 })}>
<div className={cssNames(styles.HotbarIcon, className, (menuItems.length > 0 && styles.contextMenuAvailable))}>
{tooltip && (
<Tooltip targetId={id}>
{tooltip}
@@ -61,7 +61,11 @@ const NonInjectedHotbarIcon = observer(({
id={id}
title={title}
colorHash={source ? `${title}-${source}` : title}
className={cssNames(styles.avatar, { [styles.active]: active, [styles.hasImage]: !!src })}
className={cssNames(
styles.avatar,
active && styles.active,
!!src && styles.hasImag,
)}
disabled={disabled}
size={size}
src={src}
@@ -8,7 +8,7 @@ import "./input.scss";
import type { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
import React from "react";
import type { StrictReactNode, SingleOrMany } from "@k8slens/utilities";
import { isReactNode, isString, isFunction, debouncePromise, isPromiseSettledFulfilled, cssNames } from "@k8slens/utilities";
import { isReactNode, isFalsy, isTruthy, isString, isFunction, debouncePromise, isPromiseSettledFulfilled, cssNames } from "@k8slens/utilities";
import { Icon } from "@k8slens/icon";
import type { TooltipProps } from "@k8slens/tooltip";
import { Tooltip } from "@k8slens/tooltip";
@@ -195,7 +195,13 @@ export class Input extends React.Component<InputProps, State> {
return undefined;
} catch (error) {
return this.getValidatorError(value, validator) || (error instanceof Error ? error.message : String(error));
const validationError = this.getValidatorError(value, validator);
return isTruthy(validationError)
? validationError
: error instanceof Error
? error.message
: String(error);
}
})());
}
@@ -327,7 +333,7 @@ export class Input extends React.Component<InputProps, State> {
get showMaxLenIndicator() {
const { maxLength, multiLine } = this.props;
return maxLength && multiLine;
return Boolean(maxLength && multiLine);
}
get isUncontrolled() {
@@ -440,12 +446,12 @@ export class Input extends React.Component<InputProps, State> {
{errors.map((error, i) => <p key={i}>{error}</p>)}
</div>
);
const componentId = id || showErrorsAsTooltip
const componentId = id || isTruthy(showErrorsAsTooltip)
? `input_tooltip_id_${uuid.v4()}`
: undefined;
let tooltipError: StrictReactNode;
if (showErrorsAsTooltip && showErrors) {
if (isTruthy(showErrorsAsTooltip) && showErrors) {
const tooltipProps = typeof showErrorsAsTooltip === "object" ? showErrorsAsTooltip : {};
tooltipProps.className = cssNames("InputTooltipError", tooltipProps.className);
@@ -473,7 +479,7 @@ export class Input extends React.Component<InputProps, State> {
{contentRight}
</label>
<div className="input-info flex gaps">
{!showErrorsAsTooltip && showErrors && errorsInfo}
{(isFalsy(showErrorsAsTooltip) && showErrors) && errorsInfo}
{this.showMaxLenIndicator && (
<div className="maxLengthIndicator box right">
{this.getValue().length}
@@ -59,6 +59,10 @@ export function isAsyncValidator(validator: InputValidator): validator is AsyncI
return typeof validator.debounce === "number";
}
export function isSyncValidator(validator: InputValidator): validator is SyncInputValidator {
return typeof validator.debounce !== "number";
}
/**
* A helper function to create an {@link AsyncInputValidator}
*/
@@ -106,7 +110,11 @@ export function unionInputValidatorsAsync(
debounce: longestDebounce,
validate: async (value, props) => {
for (const validator of validators) {
if (isAsyncValidator(validator)) {
if (isSyncValidator(validator)) {
if (validator.validate(value, props)) {
return;
}
} else {
try {
await validator.validate(value, props);
@@ -114,10 +122,6 @@ export function unionInputValidatorsAsync(
} catch {
// Do nothing
}
} else {
if (validator.validate(value, props)) {
return;
}
}
}
@@ -7,7 +7,7 @@ import "./search-input.scss";
import React, { createRef } from "react";
import { observer } from "mobx-react";
import { addWindowEventListener, cssNames, disposer } from "@k8slens/utilities";
import { addWindowEventListener, cssNames, disposer, isTruthy } from "@k8slens/utilities";
import { Icon } from "@k8slens/icon";
import type { InputProps } from "./input";
import { Input } from "./input";
@@ -86,7 +86,7 @@ class NonInjectedSearchInput extends React.Component<SearchInputProps & Dependen
void bindGlobalFocusHotkey;
void isMac;
if (showClearIcon && value) {
if (showClearIcon && isTruthy(value)) {
rightIcon = (
<Icon
small
@@ -210,7 +210,7 @@ class NonInjectedItemListLayoutContent<
const onConfirm = isFunction(removeItems)
? () => removeItems.apply(store, [selectedItems])
: isFunction(removeSelectedItems)
? removeSelectedItems.bind(store)
? () => removeSelectedItems.apply(store)
: noop;
openConfirmDialog({
@@ -328,7 +328,7 @@ class NonInjectedItemListLayoutContent<
{() => (
<AddRemoveButtons
onRemove={
(store.removeItems || store.removeSelectedItems) && selectedItems.length > 0
(Boolean(store.removeItems) || Boolean(store.removeSelectedItems)) && selectedItems.length > 0
? () => this.removeItemsDialog(selectedItems)
: undefined
}
@@ -8,7 +8,7 @@ import "./item-list-layout.scss";
import React from "react";
import { observer } from "mobx-react";
import type { IClassName, StrictReactNode } from "@k8slens/utilities";
import { isFunction, cssNames, isDefined } from "@k8slens/utilities";
import { isTruthy, isFunction, cssNames, isDefined } from "@k8slens/utilities";
import type { ItemObject } from "@k8slens/list-layout";
import type { Filter } from "./page-filters/store";
import type { HeaderCustomizer, HeaderPlaceholders, ItemListStore, SearchFilter } from "./list-layout";
@@ -91,7 +91,7 @@ export class ItemListLayoutHeader<I extends ItemObject, PreLoadStores extends bo
<div className={cssNames("header flex gaps align-center", headerClassName)}>
{title}
{
info && (
isTruthy(info) && (
<div className="info-panel box grow">
{info}
</div>
@@ -346,7 +346,7 @@ export const ItemListLayout = withInjectables<Dependencies, ItemListLayoutProps<
}) as <I extends ItemObject, PreLoadStores extends boolean = true>(props: ItemListLayoutProps<I, PreLoadStores>) => React.ReactElement;
function applyFilters<I extends ItemObject>(filters: ItemsFilter<I>[], items: I[]): I[] {
if (!filters || !filters.length) {
if (!filters.length) {
return items;
}
@@ -76,7 +76,7 @@ const matchesApiFor = (api: SubscribableStore["api"]) => (column: GeneralKubeObj
const getLoadErrorMessage = (error: unknown): string => {
if (error instanceof Error) {
if (error.cause) {
if (Boolean(error.cause)) {
return `${error.message}: ${getLoadErrorMessage(error.cause)}`;
}
@@ -166,7 +166,19 @@ class NonInjectedKubeObjectListLayout<
sortingCallbacks = {},
...layoutProps
} = this.props;
const resourceName = this.props.resourceName || ResourceNames[ResourceKindMap[store.api.kind]] || store.api.kind;
const resourceName = (() => {
if (this.props.resourceName) {
return this.props.resourceName;
}
const kubeResource = ResourceKindMap[store.api.kind];
if (kubeResource) {
return ResourceNames[kubeResource];
}
return store.api.kind;
})();
const targetColumns = [
...(columns ?? []),
...generalColumns.filter(matchesApiFor(store.api)),
@@ -51,7 +51,7 @@ class NonInjectedKubeObjectMenu<Kube extends KubeObject> extends React.Component
private menuItems = observable.array<KubeObjectContextMenuItem>();
componentDidUpdate(prevProps: Readonly<KubeObjectMenuProps<Kube> & Dependencies>): void {
if (prevProps.object !== this.props.object && this.props.object) {
if (prevProps.object !== this.props.object) {
this.emitOnContextMenuOpen(this.props.object);
}
}
@@ -199,11 +199,11 @@ class NonInjectedKubeObjectMenu<Kube extends KubeObject> extends React.Component
id={`menu-actions-for-kube-object-menu-for-${object?.getId()}`}
data-testid={`menu-actions-for-kube-object-menu-for-${object?.getId()}`}
className={cssNames("KubeObjectMenu", className)}
onOpen={object ? () => this.emitOnContextMenuOpen(object) : undefined}
onOpen={() => this.emitOnContextMenuOpen(object)}
{...menuProps}
>
{this.renderMenuItems()}
{object && this.renderContextMenuItems(object)}
{this.renderContextMenuItems(object)}
</MenuActions>
);
}
@@ -48,10 +48,6 @@ const NonInjectedKubeObjectMeta = observer((props : Dependencies & KubeObjectMet
namespaceApi,
} = props;
if (!object) {
return null;
}
if (!(object instanceof KubeObject)) {
logger.error("[KubeObjectMeta]: passed object that is not an instanceof KubeObject", object);
@@ -8,7 +8,7 @@ import React from "react";
import type { IObservableValue } from "mobx";
import { observer } from "mobx-react";
import type { StrictReactNode } from "@k8slens/utilities";
import { cssNames } from "@k8slens/utilities";
import { cssNames, isTruthy } from "@k8slens/utilities";
import { Button } from "@k8slens/button";
import type { DialogProps } from "../dialog";
import { Dialog } from "../dialog";
@@ -54,7 +54,7 @@ class NonInjectedKubeConfigDialog extends React.Component<KubeConfigDialogProps
};
renderContents = (data: KubeconfigDialogData) => (
<Wizard header={<h5>{ data.title || "Kubeconfig File" }</h5>}>
<Wizard header={<h5>{isTruthy(data.title) ? data.title : "Kubeconfig File" }</h5>}>
<WizardStep
customButtons={ (
<div className="actions flex gaps">
@@ -51,7 +51,7 @@ const extensionSidebarItemRegistratorInjectable = getInjectable({
parentId,
isVisible: computed(() => extensionShouldBeEnabledForClusterFrame.value.get() && (visible?.get() ?? true)),
title,
getIcon: () => (components.Icon && <components.Icon />),
getIcon: () => <components.Icon />,
onClick: () => {
const route = targetRoute.get();
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff. Show more