mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
add TypedSender and TypedInvoker
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
4cfdb60ce0
commit
04c36b4b5b
@ -1,62 +0,0 @@
|
|||||||
import { handleRequest } from "./ipc";
|
|
||||||
import { ClusterId, clusterStore } from "./cluster-store";
|
|
||||||
import { appEventBus } from "./event-bus";
|
|
||||||
import { ResourceApplier } from "../main/resource-applier";
|
|
||||||
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
|
||||||
import { clusterFrameMap } from "./cluster-frames";
|
|
||||||
|
|
||||||
export const clusterActivateHandler = "cluster:activate";
|
|
||||||
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
|
||||||
export const clusterRefreshHandler = "cluster:refresh";
|
|
||||||
export const clusterDisconnectHandler = "cluster:disconnect";
|
|
||||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
|
||||||
|
|
||||||
|
|
||||||
if (ipcMain) {
|
|
||||||
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
|
||||||
const cluster = clusterStore.getById(clusterId);
|
|
||||||
|
|
||||||
if (cluster) {
|
|
||||||
return cluster.activate(force);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
handleRequest(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
|
|
||||||
const cluster = clusterStore.getById(clusterId);
|
|
||||||
|
|
||||||
if (cluster) {
|
|
||||||
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
|
|
||||||
|
|
||||||
return cluster.pushState();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
|
||||||
const cluster = clusterStore.getById(clusterId);
|
|
||||||
|
|
||||||
if (cluster) return cluster.refresh({ refreshMetadata: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
|
||||||
appEventBus.emit({name: "cluster", action: "stop"});
|
|
||||||
const cluster = clusterStore.getById(clusterId);
|
|
||||||
|
|
||||||
if (cluster) {
|
|
||||||
cluster.disconnect();
|
|
||||||
clusterFrameMap.delete(cluster.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
|
|
||||||
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
|
|
||||||
const cluster = clusterStore.getById(clusterId);
|
|
||||||
|
|
||||||
if (cluster) {
|
|
||||||
const applier = new ResourceApplier(cluster);
|
|
||||||
|
|
||||||
applier.kubectlApplyAll(resources);
|
|
||||||
} else {
|
|
||||||
throw `${clusterId} is not a valid cluster id`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,11 +1,92 @@
|
|||||||
|
import { IpcMainInvokeEvent } from "electron";
|
||||||
|
import { ResourceApplier } from "../../main/resource-applier";
|
||||||
|
import { clusterFrameMap } from "../cluster-frames";
|
||||||
|
import { ClusterId, clusterStore } from "../cluster-store";
|
||||||
|
import { appEventBus } from "../event-bus";
|
||||||
|
import { hasOptionalProperty, hasTypedProperty, isString, isBoolean, bindPredicate, isTypedArray } from "../utils/type-narrowing";
|
||||||
|
import { createTypedInvoker, createTypedSender } from "./type-enforced-ipc";
|
||||||
|
|
||||||
|
export type ClusterIdArgList = [clusterId: ClusterId];
|
||||||
|
|
||||||
|
function isClusterIdArgList(args: unknown[]): args is ClusterIdArgList {
|
||||||
|
return hasTypedProperty(args, 0, isString)
|
||||||
|
&& args.length === 1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This channel is broadcast on whenever the cluster fails to list namespaces
|
* This channel is broadcast on whenever the cluster fails to list namespaces
|
||||||
* during a refresh and no `accessibleNamespaces` have been set.
|
* during a refresh and no `accessibleNamespaces` have been set.
|
||||||
*/
|
*/
|
||||||
export const ClusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden";
|
export const clusterListNamespacesForbidden = createTypedSender({
|
||||||
|
channel: "cluster:list-namespace-forbidden",
|
||||||
|
verifier: isClusterIdArgList,
|
||||||
|
});
|
||||||
|
|
||||||
export type ListNamespaceForbiddenArgs = [clusterId: string];
|
export const clusterActivate = createTypedInvoker({
|
||||||
|
channel: "cluster:activate",
|
||||||
|
handler(event, clusterId: ClusterId, force?: boolean) {
|
||||||
|
return clusterStore.getById(clusterId)?.activate(force ?? false);
|
||||||
|
},
|
||||||
|
verifier(args: unknown[]): args is [clusterId: ClusterId, force?: boolean] {
|
||||||
|
return hasTypedProperty(args, 0, isString)
|
||||||
|
&& hasOptionalProperty(args, 1, isBoolean)
|
||||||
|
&& args.length <= 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export function argArgsListNamespaceFordiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs {
|
export const clusterSetFrameId = createTypedInvoker({
|
||||||
return args.length === 1 && typeof args[0] === "string";
|
channel: "cluster:set-frame-id",
|
||||||
}
|
handler({ frameId, processId }: IpcMainInvokeEvent, clusterId: ClusterId) {
|
||||||
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
|
if (cluster) {
|
||||||
|
clusterFrameMap.set(cluster.id, { frameId, processId });
|
||||||
|
|
||||||
|
return cluster.pushState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifier: isClusterIdArgList,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clusterRefresh = createTypedInvoker({
|
||||||
|
channel: "cluster:refresh",
|
||||||
|
handler(event, clusterId: ClusterId) {
|
||||||
|
return clusterStore.getById(clusterId)?.refresh({ refreshMetadata: true });
|
||||||
|
},
|
||||||
|
verifier: isClusterIdArgList,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clusterDisconnect = createTypedInvoker({
|
||||||
|
channel: "cluster:disconnect",
|
||||||
|
handler(event, clusterId: ClusterId) {
|
||||||
|
appEventBus.emit({ name: "cluster", action: "stop" });
|
||||||
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
|
if (cluster) {
|
||||||
|
cluster.disconnect();
|
||||||
|
clusterFrameMap.delete(cluster.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifier: isClusterIdArgList,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clusterKubectlApplyAll = createTypedInvoker({
|
||||||
|
channel: "cluster:kubectl-apply-all",
|
||||||
|
handler(event, clusterId: ClusterId, resources: string[]) {
|
||||||
|
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
|
||||||
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
|
if (cluster) {
|
||||||
|
const applier = new ResourceApplier(cluster);
|
||||||
|
|
||||||
|
applier.kubectlApplyAll(resources);
|
||||||
|
} else {
|
||||||
|
throw new Error(`${clusterId} is not a valid cluster id`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifier(args: unknown[]): args is [clusterId: ClusterId, resources: string[]] {
|
||||||
|
return hasTypedProperty(args, 0, isString)
|
||||||
|
&& hasTypedProperty(args, 1, bindPredicate(isTypedArray, isString))
|
||||||
|
&& args.length === 2;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
import { ipcMain, ipcRenderer } from "electron";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
|
import { broadcastMessage } from "./ipc";
|
||||||
|
|
||||||
export type HandlerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
|
export type HandlerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
|
||||||
export type ListVerifier<T extends any[]> = (args: unknown[]) => args is T;
|
export type ListVerifier<T extends any[]> = (args: unknown[]) => args is T;
|
||||||
export type Rest<T> = T extends [any, ...infer R] ? R : [];
|
export type Rest<T> = T extends [any, ...infer R] ? R : [];
|
||||||
|
export type IpcListener<E extends Event, Args extends any[]> = (e: E, ...args: Args) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a listener to `source` that waits for the first IPC message with the correct
|
* Adds a listener to `source` that waits for the first IPC message with the correct
|
||||||
@ -14,7 +17,7 @@ export type Rest<T> = T extends [any, ...infer R] ? R : [];
|
|||||||
*/
|
*/
|
||||||
export function onceCorrect<
|
export function onceCorrect<
|
||||||
EM extends EventEmitter,
|
EM extends EventEmitter,
|
||||||
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
|
Listener extends IpcListener<Event, any[]>,
|
||||||
>({
|
>({
|
||||||
source,
|
source,
|
||||||
channel,
|
channel,
|
||||||
@ -23,8 +26,8 @@ export function onceCorrect<
|
|||||||
}: {
|
}: {
|
||||||
source: EM,
|
source: EM,
|
||||||
channel: string | symbol,
|
channel: string | symbol,
|
||||||
listener: L,
|
listener: Listener,
|
||||||
verifier: ListVerifier<Rest<Parameters<L>>>,
|
verifier: ListVerifier<Rest<Parameters<Listener>>>,
|
||||||
}): void {
|
}): void {
|
||||||
function handler(event: HandlerEvent<EM>, ...args: unknown[]): void {
|
function handler(event: HandlerEvent<EM>, ...args: unknown[]): void {
|
||||||
if (verifier(args)) {
|
if (verifier(args)) {
|
||||||
@ -48,7 +51,7 @@ export function onceCorrect<
|
|||||||
*/
|
*/
|
||||||
export function onCorrect<
|
export function onCorrect<
|
||||||
EM extends EventEmitter,
|
EM extends EventEmitter,
|
||||||
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
|
Listener extends IpcListener<Event, any[]>,
|
||||||
>({
|
>({
|
||||||
source,
|
source,
|
||||||
channel,
|
channel,
|
||||||
@ -57,8 +60,8 @@ export function onCorrect<
|
|||||||
}: {
|
}: {
|
||||||
source: EM,
|
source: EM,
|
||||||
channel: string | symbol,
|
channel: string | symbol,
|
||||||
listener: L,
|
listener: Listener,
|
||||||
verifier: ListVerifier<Rest<Parameters<L>>>,
|
verifier: ListVerifier<Rest<Parameters<Listener>>>,
|
||||||
}): void {
|
}): void {
|
||||||
source.on(channel, (event, ...args: unknown[]) => {
|
source.on(channel, (event, ...args: unknown[]) => {
|
||||||
if (verifier(args)) {
|
if (verifier(args)) {
|
||||||
@ -69,3 +72,154 @@ export function onCorrect<
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IPCEncodedError {
|
||||||
|
name: string,
|
||||||
|
message: string,
|
||||||
|
extra: Record<string, any>,
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeError(e: Error): IPCEncodedError {
|
||||||
|
delete e.stack;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: e.name,
|
||||||
|
message: e.message,
|
||||||
|
extra: { ...e },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeError({ extra, message, name }: IPCEncodedError): Error {
|
||||||
|
const e = new Error(message);
|
||||||
|
|
||||||
|
e.name = name;
|
||||||
|
|
||||||
|
Object.assign(e, extra);
|
||||||
|
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleCorrect<
|
||||||
|
Handler extends (event: Event, ...args: any[]) => any
|
||||||
|
>({
|
||||||
|
channel,
|
||||||
|
handler,
|
||||||
|
verifier,
|
||||||
|
}: {
|
||||||
|
channel: string,
|
||||||
|
handler: Handler,
|
||||||
|
verifier: ListVerifier<Rest<Parameters<Handler>>>,
|
||||||
|
}): void {
|
||||||
|
ipcMain.handle(channel, async (event, ...args: unknown[]) => {
|
||||||
|
try {
|
||||||
|
if (verifier(args)) {
|
||||||
|
const result = await Promise.resolve(handler(event, ...args));
|
||||||
|
|
||||||
|
return { result };
|
||||||
|
} else {
|
||||||
|
throw new TypeError("Arguments are wrong type");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { error: encodeError(error) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invokeWithDecode<
|
||||||
|
L extends any[],
|
||||||
|
>({
|
||||||
|
channel,
|
||||||
|
args
|
||||||
|
}: {
|
||||||
|
channel: string,
|
||||||
|
args: L
|
||||||
|
}): Promise<any> {
|
||||||
|
const { error, result } = await ipcRenderer.invoke(channel, ...args);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw decodeError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypedInvoker<
|
||||||
|
Handler extends (event: Event, ...args: any[]) => any
|
||||||
|
> {
|
||||||
|
invoke: (...args: Rest<Parameters<Handler>>) => Promise<ReturnType<Handler>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTypedInvoker<
|
||||||
|
Handler extends (event: Event, ...args: any[]) => any
|
||||||
|
>({
|
||||||
|
channel,
|
||||||
|
handler,
|
||||||
|
verifier,
|
||||||
|
}: {
|
||||||
|
channel: string,
|
||||||
|
handler: Handler,
|
||||||
|
verifier: ListVerifier<Rest<Parameters<Handler>>>,
|
||||||
|
}): TypedInvoker<Handler> {
|
||||||
|
if (ipcMain) {
|
||||||
|
handleCorrect({
|
||||||
|
channel,
|
||||||
|
handler,
|
||||||
|
verifier,
|
||||||
|
});
|
||||||
|
} else if (ipcRenderer) {
|
||||||
|
return {
|
||||||
|
invoke(...args) {
|
||||||
|
return invokeWithDecode({
|
||||||
|
channel,
|
||||||
|
args
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
invoke() {
|
||||||
|
throw new TypeError("invoke called in main");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypedSender<
|
||||||
|
Args extends any[]
|
||||||
|
> {
|
||||||
|
broadcast: (...args: Args) => void,
|
||||||
|
on: (listener: IpcListener<Event, Args>) => void,
|
||||||
|
once: (listener: IpcListener<Event, Args>) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTypedSender<
|
||||||
|
Args extends any[]
|
||||||
|
>({
|
||||||
|
channel,
|
||||||
|
verifier,
|
||||||
|
}: {
|
||||||
|
channel: string,
|
||||||
|
verifier: ListVerifier<Args>,
|
||||||
|
}): TypedSender<Args> {
|
||||||
|
return {
|
||||||
|
broadcast(...args) {
|
||||||
|
broadcastMessage(channel, ...args);
|
||||||
|
},
|
||||||
|
on(listener) {
|
||||||
|
onCorrect({
|
||||||
|
source: ipcMain ?? ipcRenderer,
|
||||||
|
channel,
|
||||||
|
listener,
|
||||||
|
verifier: verifier as any, // safety: this verifier is correct, TS just doesn't equate Rest<..> correctly
|
||||||
|
});
|
||||||
|
},
|
||||||
|
once(listener) {
|
||||||
|
onceCorrect({
|
||||||
|
source: ipcMain ?? ipcRenderer,
|
||||||
|
channel,
|
||||||
|
listener,
|
||||||
|
verifier: verifier as any, // safety: this verifier is correct, TS just doesn't equate Rest<..> correctly
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -1,25 +1,45 @@
|
|||||||
import { UpdateInfo } from "electron-updater";
|
import { UpdateInfo } from "electron-updater";
|
||||||
|
import { UpdateFileInfo, ReleaseNoteInfo } from "builder-util-runtime";
|
||||||
|
import { bindPredicate, bindPredicateOr, hasOptionalProperty, hasTypedProperty, isNull, isObject, isString, isTypedArray, isNumber, isBoolean } from "../utils/type-narrowing";
|
||||||
|
import { createTypedSender } from "./type-enforced-ipc";
|
||||||
|
|
||||||
export const UpdateAvailableChannel = "update-available";
|
|
||||||
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
|
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
|
||||||
|
export const updateAvailale = createTypedSender({
|
||||||
|
channel: "update-available",
|
||||||
|
verifier: isUpdateAvailableArgs,
|
||||||
|
});
|
||||||
|
|
||||||
export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo];
|
export type UpdateAvailableArgs = [backChannel: string, updateInfo: UpdateInfo];
|
||||||
|
|
||||||
export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain {
|
function isUpdateFileInfo(src: unknown): src is UpdateFileInfo {
|
||||||
if (args.length !== 2) {
|
return isObject(src)
|
||||||
return false;
|
&& hasTypedProperty(src, "sha512", isString)
|
||||||
}
|
&& hasTypedProperty(src, "url", isString)
|
||||||
|
&& hasOptionalProperty(src, "size", isNumber)
|
||||||
|
&& hasOptionalProperty(src, "blockMapSize", isNumber)
|
||||||
|
&& hasOptionalProperty(src, "isAdminRightsRequired", isBoolean);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof args[0] !== "string") {
|
function isReleaseNoteInfo(src: unknown): src is ReleaseNoteInfo {
|
||||||
return false;
|
return isObject(src)
|
||||||
}
|
&& hasTypedProperty(src, "version", isString)
|
||||||
|
&& hasTypedProperty(src, "note", bindPredicateOr(isString, isNull));
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof args[1] !== "object" || args[1] === null) {
|
function isUpdateInfo(src: unknown): src is UpdateInfo {
|
||||||
// TODO: improve this checking
|
return isObject(src)
|
||||||
return false;
|
&& hasTypedProperty(src, "version", isString)
|
||||||
}
|
&& hasTypedProperty(src, "releaseDate", isString)
|
||||||
|
&& hasTypedProperty(src, "files", bindPredicate(isTypedArray, isUpdateFileInfo))
|
||||||
|
&& hasOptionalProperty(src, "releaseName", bindPredicateOr(isString, isNull))
|
||||||
|
&& hasOptionalProperty(src, "stagingPercentage", isNumber)
|
||||||
|
&& hasOptionalProperty(src, "releaseNotes", bindPredicateOr(isString, isReleaseNoteInfo, isNull));
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
export function isUpdateAvailableArgs(args: unknown[]): args is UpdateAvailableArgs {
|
||||||
|
return hasTypedProperty(args, 0, isString)
|
||||||
|
&& hasTypedProperty(args, 1, isUpdateInfo)
|
||||||
|
&& args.length === 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BackchannelArg = {
|
export type BackchannelArg = {
|
||||||
@ -31,15 +51,19 @@ export type BackchannelArg = {
|
|||||||
|
|
||||||
export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg];
|
export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg];
|
||||||
|
|
||||||
export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel {
|
function isBackChannelArg(src: unknown): src is BackchannelArg {
|
||||||
if (args.length !== 1) {
|
if (!(
|
||||||
|
isObject(src)
|
||||||
|
&& hasTypedProperty(src, "doUpdate", isBoolean)
|
||||||
|
)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof args[0] !== "object" || args[0] === null) {
|
return !src.doUpdate
|
||||||
// TODO: improve this checking
|
|| hasTypedProperty(src, "now", isBoolean);
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel {
|
||||||
return true;
|
return hasTypedProperty(args, 0, isBackChannelArg)
|
||||||
|
&& args.length === 1;
|
||||||
}
|
}
|
||||||
|
|||||||
132
src/common/utils/type-narrowing.ts
Normal file
132
src/common/utils/type-narrowing.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* 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 (must be a literal for tsc to do any type meaningful)
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 type meaningful)
|
||||||
|
*/
|
||||||
|
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 type meaningful)
|
||||||
|
* @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 type meaningful)
|
||||||
|
* @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 boolean
|
||||||
|
* @param val the value to be checked
|
||||||
|
*/
|
||||||
|
export function isBoolean(val: unknown): val is boolean {
|
||||||
|
return typeof val === "boolean";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if val is of type number
|
||||||
|
* @param val the value to be checked
|
||||||
|
*/
|
||||||
|
export function isNumber(val: unknown): val is number {
|
||||||
|
return typeof val === "number";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if val is null
|
||||||
|
* @param val the value to be checked
|
||||||
|
*/
|
||||||
|
export function isNull(val: unknown): val is null {
|
||||||
|
return 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Predicate<T> = (arg: unknown) => arg is T;
|
||||||
|
|
||||||
|
export function bindPredicateOr<T1>(p1: Predicate<T1>): Predicate<T1>;
|
||||||
|
export function bindPredicateOr<T1, T2>(p1: Predicate<T1>, p2: Predicate<T2>): Predicate<T1 | T2>;
|
||||||
|
export function bindPredicateOr<T1, T2, T3>(p1: Predicate<T1>, p2: Predicate<T2>, p3: Predicate<T3>): Predicate<T1 | T2 | T3>;
|
||||||
|
export function bindPredicateOr<T1, T2, T3, T4>(p1: Predicate<T1>, p2: Predicate<T2>, p3: Predicate<T3>, p4: Predicate<T4>): Predicate<T1 | T2 | T3 | T4>;
|
||||||
|
|
||||||
|
export function bindPredicateOr<T extends any[]>(...predicates: T): Predicate<T> {
|
||||||
|
return (arg: unknown): arg is any => {
|
||||||
|
for (const predicate of predicates) {
|
||||||
|
if (predicate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -6,8 +6,7 @@ import { ResourceApplier } from "../main/resource-applier";
|
|||||||
import { Cluster } from "../main/cluster";
|
import { Cluster } from "../main/cluster";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { requestMain } from "../common/ipc";
|
import { clusterKubectlApplyAll } from "../common/ipc";
|
||||||
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
|
||||||
|
|
||||||
export interface ClusterFeatureStatus {
|
export interface ClusterFeatureStatus {
|
||||||
/** feature's current version, as set by the implementation */
|
/** feature's current version, as set by the implementation */
|
||||||
@ -94,7 +93,7 @@ export abstract class ClusterFeature {
|
|||||||
if (app) {
|
if (app) {
|
||||||
await new ResourceApplier(cluster).kubectlApplyAll(resources);
|
await new ResourceApplier(cluster).kubectlApplyAll(resources);
|
||||||
} else {
|
} else {
|
||||||
await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources);
|
await clusterKubectlApplyAll.invoke(cluster.id, resources);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,9 +37,9 @@ export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
|
|||||||
autoUpdater.autoInstallOnAppQuit = false;
|
autoUpdater.autoInstallOnAppQuit = false;
|
||||||
|
|
||||||
autoUpdater
|
autoUpdater
|
||||||
.on("update-available", (args: UpdateInfo) => {
|
.on("update-available", (updateInfo: UpdateInfo) => {
|
||||||
try {
|
try {
|
||||||
const backchannel = `auto-update:${args.version}`;
|
const backchannel = `auto-update:${updateInfo.version}`;
|
||||||
|
|
||||||
ipcMain.removeAllListeners(backchannel); // only one handler should be present
|
ipcMain.removeAllListeners(backchannel); // only one handler should be present
|
||||||
|
|
||||||
@ -50,8 +50,8 @@ export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
|
|||||||
listener: handleAutoUpdateBackChannel,
|
listener: handleAutoUpdateBackChannel,
|
||||||
verifier: areArgsUpdateAvailableToBackchannel,
|
verifier: areArgsUpdateAvailableToBackchannel,
|
||||||
});
|
});
|
||||||
logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: args.version });
|
logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: updateInfo.version });
|
||||||
broadcastMessage(UpdateAvailableChannel, backchannel, args);
|
broadcastMessage(UpdateAvailableChannel, backchannel, updateInfo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
|
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
|||||||
import type { WorkspaceId } from "../common/workspace-store";
|
import type { WorkspaceId } from "../common/workspace-store";
|
||||||
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
import { broadcastMessage, clusterListNamespacesForbidden } from "../common/ipc";
|
||||||
import { ContextHandler } from "./context-handler";
|
import { ContextHandler } from "./context-handler";
|
||||||
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||||
import { Kubectl } from "./kubectl";
|
import { Kubectl } from "./kubectl";
|
||||||
@ -685,7 +685,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
||||||
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id });
|
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id });
|
||||||
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
|
clusterListNamespacesForbidden.broadcast(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return namespaceList;
|
return namespaceList;
|
||||||
|
|||||||
@ -13,8 +13,7 @@ import { ClusterIcon } from "../cluster-icon";
|
|||||||
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { PageLayout } from "../layout/page-layout";
|
import { PageLayout } from "../layout/page-layout";
|
||||||
import { requestMain } from "../../../common/ipc";
|
import { clusterActivate, clusterRefresh } from "../../../common/ipc";
|
||||||
import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc";
|
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
|
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
|
||||||
@ -47,8 +46,8 @@ export class ClusterSettings extends React.Component<Props> {
|
|||||||
|
|
||||||
refreshCluster = async () => {
|
refreshCluster = async () => {
|
||||||
if (this.cluster) {
|
if (this.cluster) {
|
||||||
await requestMain(clusterActivateHandler, this.cluster.id);
|
await clusterActivate.invoke(this.cluster.id);
|
||||||
await requestMain(clusterRefreshHandler, this.cluster.id);
|
await clusterRefresh.invoke(this.cluster.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -36,9 +36,8 @@ import { webFrame } from "electron";
|
|||||||
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
||||||
import { extensionLoader } from "../../extensions/extension-loader";
|
import { extensionLoader } from "../../extensions/extension-loader";
|
||||||
import { appEventBus } from "../../common/event-bus";
|
import { appEventBus } from "../../common/event-bus";
|
||||||
import { broadcastMessage, requestMain } from "../../common/ipc";
|
import { broadcastMessage, clusterSetFrameId } from "../../common/ipc";
|
||||||
import whatInput from "what-input";
|
import whatInput from "what-input";
|
||||||
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
|
||||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
||||||
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
||||||
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
||||||
@ -60,7 +59,7 @@ export class App extends React.Component {
|
|||||||
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`);
|
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`);
|
||||||
await Terminal.preloadFonts();
|
await Terminal.preloadFonts();
|
||||||
|
|
||||||
await requestMain(clusterSetFrameIdHandler, clusterId);
|
await clusterSetFrameId.invoke(clusterId);
|
||||||
await getHostedCluster().whenReady; // cluster.activate() is done at this point
|
await getHostedCluster().whenReady; // cluster.activate() is done at this point
|
||||||
extensionLoader.loadOnClusterRenderer();
|
extensionLoader.loadOnClusterRenderer();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@ -5,14 +5,13 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { computed, observable } from "mobx";
|
import { computed, observable } from "mobx";
|
||||||
import { requestMain, subscribeToBroadcast } from "../../../common/ipc";
|
import { clusterActivate, subscribeToBroadcast } from "../../../common/ipc";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
||||||
import { CubeSpinner } from "../spinner";
|
import { CubeSpinner } from "../spinner";
|
||||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
@ -50,7 +49,7 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activateCluster = async (force = false) => {
|
activateCluster = async (force = false) => {
|
||||||
await requestMain(clusterActivateHandler, this.props.clusterId, force);
|
await clusterActivate.invoke(this.props.clusterId, force);
|
||||||
};
|
};
|
||||||
|
|
||||||
reconnect = async () => {
|
reconnect = async () => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import "./clusters-menu.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { remote } from "electron";
|
import { remote } from "electron";
|
||||||
import { requestMain } from "../../../common/ipc";
|
import { clusterDisconnect } from "../../../common/ipc";
|
||||||
import type { Cluster } from "../../../main/cluster";
|
import type { Cluster } from "../../../main/cluster";
|
||||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -21,7 +21,6 @@ import { Tooltip } from "../tooltip";
|
|||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import { clusterViewURL } from "./cluster-view.route";
|
import { clusterViewURL } from "./cluster-view.route";
|
||||||
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
|
||||||
import { clusterDisconnectHandler } from "../../../common/cluster-ipc";
|
|
||||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
import { CommandOverlay } from "../command-palette/command-container";
|
import { CommandOverlay } from "../command-palette/command-container";
|
||||||
import { computed } from "mobx";
|
import { computed } from "mobx";
|
||||||
@ -64,7 +63,7 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
navigate(landingURL());
|
navigate(landingURL());
|
||||||
clusterStore.setActive(null);
|
clusterStore.setActive(null);
|
||||||
}
|
}
|
||||||
await requestMain(clusterDisconnectHandler, cluster.id);
|
await clusterDisconnect.invoke(cluster.id);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ipcRenderer, IpcRendererEvent } from "electron";
|
import { ipcRenderer, IpcRendererEvent } from "electron";
|
||||||
import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg, ClusterListNamespaceForbiddenChannel, argArgsListNamespaceFordiddenArgs, ListNamespaceForbiddenArgs } from "../../common/ipc";
|
import { UpdateAvailableArgs, BackchannelArg, ClusterIdArgList, updateAvailale, clusterListNamespacesForbidden } from "../../common/ipc";
|
||||||
import { Notifications, notificationsStore } from "../components/notifications";
|
import { Notifications, notificationsStore } from "../components/notifications";
|
||||||
import { Button } from "../components/button";
|
import { Button } from "../components/button";
|
||||||
import { isMac } from "../../common/vars";
|
import { isMac } from "../../common/vars";
|
||||||
@ -32,7 +32,7 @@ function RenderYesButtons(props: { backchannel: string, notificationId: string }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, updateInfo]: UpdateAvailableFromMain): void {
|
function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, updateInfo]: UpdateAvailableArgs): void {
|
||||||
const notificationId = uuid.v4();
|
const notificationId = uuid.v4();
|
||||||
|
|
||||||
Notifications.info(
|
Notifications.info(
|
||||||
@ -58,7 +58,7 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update
|
|||||||
const listNamespacesForbiddenHandlerDisplayedAt = new Map<string, number>();
|
const listNamespacesForbiddenHandlerDisplayedAt = new Map<string, number>();
|
||||||
const intervalBetweenNotifications = 1000 * 60; // 60s
|
const intervalBetweenNotifications = 1000 * 60; // 60s
|
||||||
|
|
||||||
function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ListNamespaceForbiddenArgs): void {
|
function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ClusterIdArgList): void {
|
||||||
const lastDisplayedAt = listNamespacesForbiddenHandlerDisplayedAt.get(clusterId);
|
const lastDisplayedAt = listNamespacesForbiddenHandlerDisplayedAt.get(clusterId);
|
||||||
const wasDisplayed = Boolean(lastDisplayedAt);
|
const wasDisplayed = Boolean(lastDisplayedAt);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@ -92,16 +92,6 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
onCorrect({
|
updateAvailale.on(UpdateAvailableHandler);
|
||||||
source: ipcRenderer,
|
clusterListNamespacesForbidden.on(ListNamespacesForbiddenHandler);
|
||||||
channel: UpdateAvailableChannel,
|
|
||||||
listener: UpdateAvailableHandler,
|
|
||||||
verifier: areArgsUpdateAvailableFromMain,
|
|
||||||
});
|
|
||||||
onCorrect({
|
|
||||||
source: ipcRenderer,
|
|
||||||
channel: ClusterListNamespaceForbiddenChannel,
|
|
||||||
listener: ListNamespacesForbiddenHandler,
|
|
||||||
verifier: argArgsListNamespaceFordiddenArgs,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user