Cherry-pick from 4.2.3 (#2628)

* Fix: logs data disapearing causing crashes (#2566)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Refactor helm-chart.api and improve kube validation and error handling (#2265)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix: HPA's not sortable by age (#2565)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Conditionally render status icon for kube meta (#2298)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix custom resource loading spinner appears above extensions' cluster menus (#2344)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Lens should point to the release docs (#2268)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Refactor the Extensions settings page (#2221)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* try and get jest to not core dump

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-04-27 08:11:50 +03:00
1 parent 7fde8125eb
commit 1f854d0a0f
42 files changed
+1777 -1067

No files matched your search

+5 -1
View File
@@ -43,13 +43,17 @@ jest.mock("electron", () => {
},
ipcMain: {
handle: jest.fn(),
on: jest.fn()
on: jest.fn(),
removeAllListeners: jest.fn(),
off: jest.fn(),
send: jest.fn(),
}
};
});
describe("empty config", () => {
beforeEach(async () => {
ClusterStore.getInstance(false)?.unregisterIpcListener();
ClusterStore.resetInstance();
const mockOpts = {
"tmp": {
+3 -3
View File
@@ -124,8 +124,8 @@ export abstract class BaseStore<T = any> extends Singleton {
}
unregisterIpcListener() {
ipcRenderer.removeAllListeners(this.syncMainChannel);
ipcRenderer.removeAllListeners(this.syncRendererChannel);
ipcRenderer?.removeAllListeners(this.syncMainChannel);
ipcRenderer?.removeAllListeners(this.syncRendererChannel);
}
disableSync() {
@@ -167,7 +167,7 @@ export abstract class BaseStore<T = any> extends Singleton {
/**
* toJSON is called when syncing the store to the filesystem. It should
* produce a JSON serializable object representaion of the current state.
* produce a JSON serializable object representation of the current state.
*
* It is recommended that a round trip is valid. Namely, calling
* `this.fromStore(this.toJSON())` shouldn't change the state.
+16 -11
View File
@@ -1,5 +1,5 @@
import path from "path";
import { app, ipcRenderer, remote, webFrame } from "electron";
import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron";
import { unlink } from "fs-extra";
import { action, comparer, computed, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store";
@@ -12,6 +12,7 @@ import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node";
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
import { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting";
import { disposer, noop } from "./utils";
export interface ClusterIconUpload {
clusterId: string;
@@ -111,6 +112,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@observable clusters = observable.map<ClusterId, Cluster>();
private static stateRequestChannel = "cluster:states";
protected disposer = disposer();
constructor() {
super({
@@ -143,7 +145,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
cluster.setState(clusterState.state);
}
});
} else {
} else if (ipcMain) {
handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
const states: clusterStateSync[] = [];
@@ -160,13 +162,16 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
protected pushStateToViewsAutomatically() {
if (!ipcRenderer) {
reaction(() => this.enabledClustersList, () => {
this.pushState();
});
reaction(() => this.connectedClustersList, () => {
this.pushState();
});
if (ipcMain) {
this.disposer.push(
reaction(() => this.enabledClustersList, () => {
this.pushState();
}),
reaction(() => this.connectedClustersList, () => {
this.pushState();
}),
() => unsubscribeAllFromBroadcast("cluster:state"),
);
}
}
@@ -180,7 +185,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
unregisterIpcListener() {
super.unregisterIpcListener();
unsubscribeAllFromBroadcast("cluster:state");
this.disposer();
}
pushState() {
@@ -288,7 +293,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
// remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
unlink(cluster.kubeConfigPath).catch(() => null);
await unlink(cluster.kubeConfigPath).catch(noop);
}
}
}
+4 -4
View File
@@ -28,7 +28,7 @@ export async function broadcastMessage(channel: string, ...args: any[]) {
if (ipcRenderer) {
ipcRenderer.send(channel, ...args);
} else {
} else if (ipcMain) {
ipcMain.emit(channel, ...args);
}
@@ -55,7 +55,7 @@ export async function broadcastMessage(channel: string, ...args: any[]) {
export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) {
if (ipcRenderer) {
ipcRenderer.on(channel, listener);
} else {
} else if (ipcMain) {
ipcMain.on(channel, listener);
}
@@ -65,7 +65,7 @@ export function subscribeToBroadcast(channel: string, listener: (...args: any[])
export function unsubscribeFromBroadcast(channel: string, listener: (...args: any[]) => any) {
if (ipcRenderer) {
ipcRenderer.off(channel, listener);
} else {
} else if (ipcMain) {
ipcMain.off(channel, listener);
}
}
@@ -73,7 +73,7 @@ export function unsubscribeFromBroadcast(channel: string, listener: (...args: an
export function unsubscribeAllFromBroadcast(channel: string) {
if (ipcRenderer) {
ipcRenderer.removeAllListeners(channel);
} else {
} else if (ipcMain) {
ipcMain.removeAllListeners(channel);
}
}
+3 -3
View File
@@ -23,8 +23,8 @@ export const ProtocolHandlerExtension= `${ProtocolHandlerIpcPrefix}:extension`;
* Though under the current (2021/01/18) implementation, these are never matched
* against in the final matching so their names are less of a concern.
*/
const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
export const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
export const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
export abstract class LensProtocolRouter extends Singleton {
// Map between path schemas and the handlers
@@ -32,7 +32,7 @@ export abstract class LensProtocolRouter extends Singleton {
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
protected static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
/**
*
+20
View File
@@ -0,0 +1,20 @@
export type Disposer = () => void;
interface Extendable<T> {
push(...vals: T[]): void;
}
export type ExtendableDisposer = Disposer & Extendable<Disposer>;
export function disposer(...args: Disposer[]): ExtendableDisposer {
const res = () => {
args.forEach(dispose => dispose?.());
args.length = 0;
};
res.push = (...vals: Disposer[]) => {
args.push(...vals);
};
return res;
}
+12 -3
View File
@@ -6,13 +6,13 @@ export interface DownloadFileOptions {
timeout?: number;
}
export interface DownloadFileTicket {
export interface DownloadFileTicket<T> {
url: string;
promise: Promise<Buffer>;
promise: Promise<T>;
cancel(): void;
}
export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket {
export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket<Buffer> {
const fileChunks: Buffer[] = [];
const req = request(url, { gzip, timeout });
const promise: Promise<Buffer> = new Promise((resolve, reject) => {
@@ -35,3 +35,12 @@ export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions)
}
};
}
export function downloadJson(args: DownloadFileOptions): DownloadFileTicket<any> {
const { promise, ...rest } = downloadFile(args);
return {
promise: promise.then(res => JSON.parse(res.toString())),
...rest
};
}
+2
View File
@@ -19,6 +19,8 @@ export * from "./downloadFile";
export * from "./escapeRegExp";
export * from "./tar";
export * from "./type-narrowing";
export * from "./disposer";
import * as iter from "./iter";
export { iter };
+79 -3
View File
@@ -1,13 +1,89 @@
/**
* Narrows `val` to include the property `key` (if true is returned)
* @param val The object to be tested
* @param key The key to test if it is present on the object
* @param key The key to test if it is present on the object (must be a literal for tsc to do any meaningful typing)
*/
export function hasOwnProperty<V extends object, K extends PropertyKey>(val: V, key: K): val is (V & { [key in K]: unknown }) {
export function hasOwnProperty<S extends object, K extends PropertyKey>(val: S, key: K): val is (S & { [key in K]: unknown }) {
// this call syntax is for when `val` was created by `Object.create(null)`
return Object.prototype.hasOwnProperty.call(val, key);
}
export function hasOwnProperties<V extends object, K extends PropertyKey>(val: V, ...keys: K[]): val is (V & { [key in K]: unknown}) {
/**
* Narrows `val` to a static type that includes fields of names in `keys`
* @param val the value that we are trying to type narrow
* @param keys the key names (must be literals for tsc to do any meaningful typing)
*/
export function hasOwnProperties<S extends object, K extends PropertyKey>(val: S, ...keys: K[]): val is (S & { [key in K]: unknown }) {
return keys.every(key => hasOwnProperty(val, key));
}
/**
* Narrows `val` to include the property `key` with type `V`
* @param val the value that we are trying to type narrow
* @param key The key to test if it is present on the object (must be a literal for tsc to do any meaningful typing)
* @param isValid a function to check if the field is valid
*/
export function hasTypedProperty<S extends object, K extends PropertyKey, V>(val: S, key: K, isValid: (value: unknown) => value is V): val is (S & { [key in K]: V }) {
return hasOwnProperty(val, key) && isValid(val[key]);
}
/**
* Narrows `val` to include the property `key` with type `V | undefined` or doesn't contain it
* @param val the value that we are trying to type narrow
* @param key The key to test if it is present on the object (must be a literal for tsc to do any meaningful typing)
* @param isValid a function to check if the field (when present) is valid
*/
export function hasOptionalProperty<S extends object, K extends PropertyKey, V>(val: S, key: K, isValid: (value: unknown) => value is V): val is (S & { [key in K]?: V }) {
if (hasOwnProperty(val, key)) {
return typeof val[key] === "undefined" || isValid(val[key]);
}
return true;
}
/**
* isRecord checks if `val` matches the signature `Record<T, V>` or `{ [label in T]: V }`
* @param val The value to be checked
* @param isKey a function for checking if the key is of the correct type
* @param isValue a function for checking if a value is of the correct type
*/
export function isRecord<T extends PropertyKey, V>(val: unknown, isKey: (key: unknown) => key is T, isValue: (value: unknown) => value is V): val is Record<T, V> {
return isObject(val) && Object.entries(val).every(([key, value]) => isKey(key) && isValue(value));
}
/**
* isTypedArray checks if `val` is an array and all of its entries are of type `T`
* @param val The value to be checked
* @param isEntry a function for checking if an entry is the correct type
*/
export function isTypedArray<T>(val: unknown, isEntry: (entry: unknown) => entry is T): val is T[] {
return Array.isArray(val) && val.every(isEntry);
}
/**
* checks if val is of type string
* @param val the value to be checked
*/
export function isString(val: unknown): val is string {
return typeof val === "string";
}
/**
* checks if val is of type object and isn't null
* @param val the value to be checked
*/
export function isObject(val: unknown): val is object {
return typeof val === "object" && val !== null;
}
/**
* Creates a new predicate function (with the same predicate) from `fn`. Such
* that it can be called with just the value to be tested.
*
* This is useful for when using `hasOptionalProperty` and `hasTypedProperty`
* @param fn A typescript user predicate function to be bound
* @param boundArgs the set of arguments to be passed to `fn` in the new function
*/
export function bindPredicate<FnArgs extends any[], T>(fn: (arg1: unknown, ...args: FnArgs) => arg1 is T, ...boundArgs: FnArgs): (arg1: unknown) => arg1 is T {
return (arg1: unknown): arg1 is T => fn(arg1, ...boundArgs);
}
+8 -1
View File
@@ -1,5 +1,6 @@
// App's common configuration for any process (main, renderer, build pipeline, etc.)
import path from "path";
import { SemVer } from "semver";
import packageInfo from "../../package.json";
import { defineGlobal } from "./utils/defineGlobal";
@@ -44,5 +45,11 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis
// Links
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues";
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI";
export const docsUrl = "https://docs.k8slens.dev/";
export const supportUrl = "https://docs.k8slens.dev/latest/support/";
// This explicitly ignores the prerelease info on the package version
const { major, minor, patch } = new SemVer(packageInfo.version);
const mmpVersion = [major, minor, patch].join(".");
const docsVersion = isProduction ? `v${mmpVersion}` : "latest";
export const docsUrl = `https://docs.k8slens.dev/${docsVersion}`;