mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Restructure to monorepo (#6907)
* wip: restructure to monorepo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor create-release-pr to a package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * github workflow fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix typo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * add webpack-env types to core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix github workflows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor/fix integration tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * lint fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * yarn run dev Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * eslint settings for vscode Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * move templates to right package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * open-lens build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * integration test fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix nx task dependencies Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use bash shell for unit tests in test workflow Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix test:unit for windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix build:app on windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ELECTRON_BUILDER_EXTRA_ARGS Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * sync src/ from master Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove Makefile from core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
0066ca4ea2
commit
2657df2293
3321 files changed
+28699
-11447
No files matched your search
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { CatalogCategory } from "../catalog-entity";
|
||||
import { KubernetesCluster, WebLink } from "../../../common/catalog-entities";
|
||||
import { observable } from "mobx";
|
||||
import type { CatalogCategoryRegistry } from "../../../common/catalog";
|
||||
import { categoryVersion } from "../../../common/catalog";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import type { CatalogEntityRegistry } from "../catalog/entity/registry";
|
||||
import catalogEntityRegistryInjectable from "../catalog/entity/registry.injectable";
|
||||
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
|
||||
|
||||
|
||||
class FooBarCategory extends CatalogCategory {
|
||||
public readonly apiVersion = "entityRegistry.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
name: "FooBars",
|
||||
icon: "broken",
|
||||
};
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
categoryVersion("v1alpha1", WebLink),
|
||||
],
|
||||
names: {
|
||||
kind: "FooBar",
|
||||
},
|
||||
};
|
||||
}
|
||||
const entity = new WebLink({
|
||||
metadata: {
|
||||
uid: "test",
|
||||
name: "test-link",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
spec: {
|
||||
url: "https://k8slens.dev",
|
||||
},
|
||||
status: {
|
||||
phase: "available",
|
||||
},
|
||||
});
|
||||
const entity2 = new WebLink({
|
||||
metadata: {
|
||||
uid: "test2",
|
||||
name: "test-link",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
spec: {
|
||||
url: "https://k8slens.dev",
|
||||
},
|
||||
status: {
|
||||
phase: "available",
|
||||
},
|
||||
});
|
||||
const entitykc = new KubernetesCluster({
|
||||
metadata: {
|
||||
uid: "test3",
|
||||
name: "test-link",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
spec: {
|
||||
kubeconfigPath: "",
|
||||
kubeconfigContext: "",
|
||||
},
|
||||
status: {
|
||||
phase: "connected",
|
||||
},
|
||||
});
|
||||
|
||||
describe("CatalogEntityRegistry", () => {
|
||||
let entityRegistry: CatalogEntityRegistry;
|
||||
let catalogCategoryRegistry: CatalogCategoryRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
entityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
catalogCategoryRegistry = di.inject(catalogCategoryRegistryInjectable);
|
||||
});
|
||||
|
||||
describe("updateItems", () => {
|
||||
it("adds new catalog item", () => {
|
||||
const items = [{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
metadata: {
|
||||
uid: "123",
|
||||
name: "foobar",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
},
|
||||
spec: {},
|
||||
}];
|
||||
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(1);
|
||||
|
||||
items.push({
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
metadata: {
|
||||
uid: "456",
|
||||
name: "barbaz",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
},
|
||||
spec: {},
|
||||
});
|
||||
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(2);
|
||||
});
|
||||
|
||||
it("updates existing items", () => {
|
||||
const items = [{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
metadata: {
|
||||
uid: "123",
|
||||
name: "foobar",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
},
|
||||
spec: {},
|
||||
}];
|
||||
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(1);
|
||||
expect(entityRegistry.items.get()[0].status.phase).toEqual("disconnected");
|
||||
|
||||
items[0].status.phase = "connected";
|
||||
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(1);
|
||||
expect(entityRegistry.items.get()[0].status.phase).toEqual("connected");
|
||||
});
|
||||
|
||||
it("updates activeEntity", () => {
|
||||
const items = [{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
metadata: {
|
||||
uid: "123",
|
||||
name: "foobar",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
},
|
||||
spec: {},
|
||||
}];
|
||||
|
||||
entityRegistry.updateItems(items);
|
||||
entityRegistry.activeEntity = entityRegistry.items.get()[0];
|
||||
expect(entityRegistry.activeEntity.status.phase).toEqual("disconnected");
|
||||
|
||||
items[0].status.phase = "connected";
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.activeEntity.status.phase).toEqual("connected");
|
||||
});
|
||||
|
||||
it("removes deleted items", () => {
|
||||
const items = [
|
||||
{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
metadata: {
|
||||
uid: "123",
|
||||
name: "foobar",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
metadata: {
|
||||
uid: "456",
|
||||
name: "barbaz",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
];
|
||||
|
||||
entityRegistry.updateItems(items);
|
||||
items.splice(0, 1);
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toEqual(1);
|
||||
expect(entityRegistry.items.get()[0].metadata.uid).toEqual("456");
|
||||
});
|
||||
});
|
||||
|
||||
describe("items", () => {
|
||||
it("does not return items without matching category", () => {
|
||||
const items = [
|
||||
{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "KubernetesCluster",
|
||||
metadata: {
|
||||
uid: "123",
|
||||
name: "foobar",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "FooBar",
|
||||
metadata: {
|
||||
uid: "456",
|
||||
name: "barbaz",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
];
|
||||
|
||||
entityRegistry.updateItems(items);
|
||||
expect(entityRegistry.items.get().length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("does return items after matching category is added", () => {
|
||||
const items = [
|
||||
{
|
||||
apiVersion: "entity.k8slens.dev/v1alpha1",
|
||||
kind: "FooBar",
|
||||
metadata: {
|
||||
uid: "456",
|
||||
name: "barbaz",
|
||||
source: "test",
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
phase: "disconnected",
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
];
|
||||
|
||||
entityRegistry.updateItems(items);
|
||||
catalogCategoryRegistry.add(new FooBarCategory());
|
||||
expect(entityRegistry.items.get().length).toBe(1);
|
||||
});
|
||||
|
||||
it("does not return items that are filtered out", () => {
|
||||
const source = observable.array([entity, entity2, entitykc]);
|
||||
|
||||
entityRegistry.updateItems(source);
|
||||
|
||||
expect(entityRegistry.items.get().length).toBe(3);
|
||||
expect(entityRegistry.filteredItems.length).toBe(3);
|
||||
|
||||
const d = entityRegistry.addCatalogFilter(entity => entity.kind === KubernetesCluster.kind);
|
||||
|
||||
expect(entityRegistry.items.get().length).toBe(3);
|
||||
expect(entityRegistry.filteredItems.length).toBe(1);
|
||||
|
||||
// Remove filter
|
||||
d();
|
||||
|
||||
expect(entityRegistry.items.get().length).toBe(3);
|
||||
expect(entityRegistry.filteredItems.length).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import defaultWebsocketApiParamsInjectable from "../default-websocket-api-params.injectable";
|
||||
import type { WebSocketEvents } from "../websocket-api";
|
||||
import { WebSocketApi } from "../websocket-api";
|
||||
|
||||
class TestWebSocketApi extends WebSocketApi<WebSocketEvents> {
|
||||
flush(): void {
|
||||
super.flush();
|
||||
}
|
||||
}
|
||||
|
||||
describe("WebsocketApi tests", () => {
|
||||
let api: TestWebSocketApi;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
api = new TestWebSocketApi({
|
||||
defaultParams: di.inject(defaultWebsocketApiParamsInjectable),
|
||||
}, {});
|
||||
});
|
||||
|
||||
describe("before connection", () => {
|
||||
it("does not hang when flush is called", () => {
|
||||
api.flush();
|
||||
});
|
||||
|
||||
describe("when a message has been sent", () => {
|
||||
beforeEach(() => {
|
||||
api.send("a command");
|
||||
});
|
||||
|
||||
it("does not hang when flush is called", () => {
|
||||
api.flush();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export { CatalogCategory, CatalogEntity } from "../../common/catalog";
|
||||
export type {
|
||||
CatalogEntityData,
|
||||
CatalogEntityKindData,
|
||||
CatalogEntityActionContext,
|
||||
CatalogEntityAddMenuContext,
|
||||
CatalogEntityAddMenu,
|
||||
CatalogEntityContextMenu,
|
||||
CatalogEntityContextMenuContext,
|
||||
} from "../../common/catalog";
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import catalogEntityRegistryInjectable from "./registry.injectable";
|
||||
|
||||
const activeEntityInjectable = getInjectable({
|
||||
id: "active-entity",
|
||||
instantiate: (di) => {
|
||||
const registry = di.inject(catalogEntityRegistryInjectable);
|
||||
|
||||
return computed(() => registry.activeEntity);
|
||||
},
|
||||
});
|
||||
|
||||
export default activeEntityInjectable;
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import catalogEntityRegistryInjectable from "./registry.injectable";
|
||||
|
||||
const catalogEnitiesInjectable = getInjectable({
|
||||
id: "catalog-enities",
|
||||
instantiate: (di) => di.inject(catalogEntityRegistryInjectable).items,
|
||||
});
|
||||
|
||||
export default catalogEnitiesInjectable;
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import catalogEntityRegistryInjectable from "./registry.injectable";
|
||||
|
||||
export type GetActiveClusterEntity = () => Cluster | undefined;
|
||||
|
||||
const getActiveClusterEntityInjectable = getInjectable({
|
||||
id: "get-active-cluster-entity",
|
||||
instantiate: (di): GetActiveClusterEntity => {
|
||||
const store = di.inject(clusterStoreInjectable);
|
||||
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
|
||||
return () => store.getById(entityRegistry.activeEntity?.getId());
|
||||
},
|
||||
});
|
||||
|
||||
export default getActiveClusterEntityInjectable;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import type { ClusterMetricsResourceType } from "../../../../common/cluster-types";
|
||||
import currentKubeObjectInDetailsInjectable from "../../../components/kube-object-details/current-kube-object-in-details.injectable";
|
||||
import enabledMetricsInjectable from "./metrics-enabled.injectable";
|
||||
|
||||
const metricsDetailsComponentEnabledInjectable = getInjectable({
|
||||
id: "metrics-details-component-enabled",
|
||||
instantiate: (di, kind) => {
|
||||
const metricsEnabled = di.inject(enabledMetricsInjectable, kind);
|
||||
const currentKubeObjectInDetails = di.inject(currentKubeObjectInDetailsInjectable);
|
||||
|
||||
return computed(() => {
|
||||
const current = currentKubeObjectInDetails.value.get();
|
||||
|
||||
if (!current?.object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return current.object.kind == kind && metricsEnabled.get();
|
||||
});
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, kind: ClusterMetricsResourceType) => kind,
|
||||
}),
|
||||
});
|
||||
|
||||
export default metricsDetailsComponentEnabledInjectable;
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import type { ClusterMetricsResourceType } from "../../../../common/cluster-types";
|
||||
import getActiveClusterEntityInjectable from "./get-active-cluster-entity.injectable";
|
||||
|
||||
const enabledMetricsInjectable = getInjectable({
|
||||
id: "enabled-metrics",
|
||||
instantiate: (di, kind) => {
|
||||
const getActiveClusterEntity = di.inject(getActiveClusterEntityInjectable);
|
||||
|
||||
return computed(() => !getActiveClusterEntity()?.isMetricHidden(kind));
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, kind: ClusterMetricsResourceType) => kind,
|
||||
}),
|
||||
});
|
||||
|
||||
export default enabledMetricsInjectable;
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import catalogCategoryRegistryInjectable from "../../../../common/catalog/category-registry.injectable";
|
||||
import navigateInjectable from "../../../navigation/navigate.injectable";
|
||||
import { CatalogEntityRegistry } from "./registry";
|
||||
import loggerInjectable from "../../../../common/logger.injectable";
|
||||
|
||||
const catalogEntityRegistryInjectable = getInjectable({
|
||||
id: "catalog-entity-registry",
|
||||
instantiate: (di) => new CatalogEntityRegistry({
|
||||
categoryRegistry: di.inject(catalogCategoryRegistryInjectable),
|
||||
navigate: di.inject(navigateInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default catalogEntityRegistryInjectable;
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { computed, observable, makeObservable, action } from "mobx";
|
||||
import { ipcRendererOn } from "../../../../common/ipc";
|
||||
import type { CatalogCategory, CatalogEntity, CatalogEntityData, CatalogCategoryRegistry, CatalogEntityKindData } from "../../../../common/catalog";
|
||||
import "../../../../common/catalog-entities";
|
||||
import { iter } from "../../../utils";
|
||||
import type { Disposer } from "../../../utils";
|
||||
import { once } from "lodash";
|
||||
import { CatalogRunEvent } from "../../../../common/catalog/catalog-run-event";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { catalogInitChannel, catalogItemsChannel, catalogEntityRunListener } from "../../../../common/ipc/catalog";
|
||||
import { isMainFrame } from "process";
|
||||
import type { Navigate } from "../../../navigation/navigate.injectable";
|
||||
import type { Logger } from "../../../../common/logger";
|
||||
|
||||
export type EntityFilter = (entity: CatalogEntity) => any;
|
||||
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
|
||||
|
||||
interface Dependencies {
|
||||
navigate: Navigate;
|
||||
readonly categoryRegistry: CatalogCategoryRegistry;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
protected readonly activeEntityId = observable.box<string | undefined>(undefined);
|
||||
protected readonly _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
||||
protected readonly filters = observable.set<EntityFilter>([], {
|
||||
deep: false,
|
||||
});
|
||||
protected readonly onBeforeRunHooks = observable.set<CatalogEntityOnBeforeRun>([], {
|
||||
deep: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Buffer for keeping entities that don't yet have CatalogCategory synced
|
||||
*/
|
||||
protected readonly rawEntities: (CatalogEntityData & CatalogEntityKindData)[] = [];
|
||||
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
protected getActiveEntityById() {
|
||||
const activeEntityId = this.activeEntityId.get();
|
||||
|
||||
if (!activeEntityId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._entities.get(activeEntityId);
|
||||
}
|
||||
|
||||
get activeEntity(): CatalogEntity | undefined {
|
||||
const entity = this.getActiveEntityById();
|
||||
|
||||
// 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) {
|
||||
this.processRawEntities();
|
||||
|
||||
return this.getActiveEntityById();
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
set activeEntity(raw: CatalogEntity | string | undefined) {
|
||||
if (raw) {
|
||||
const id = typeof raw === "string"
|
||||
? raw
|
||||
: raw.getId();
|
||||
|
||||
this.activeEntityId.set(id);
|
||||
} else {
|
||||
this.activeEntityId.set(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
ipcRendererOn(catalogItemsChannel, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
||||
this.updateItems(items);
|
||||
});
|
||||
|
||||
// Make sure that we get items ASAP and not the next time one of them changes
|
||||
ipcRenderer.send(catalogInitChannel);
|
||||
|
||||
if (isMainFrame) {
|
||||
ipcRendererOn(catalogEntityRunListener, (event, id: string) => {
|
||||
const entity = this.getById(id);
|
||||
|
||||
if (entity) {
|
||||
this.onRun(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
||||
this.rawEntities.length = 0;
|
||||
|
||||
const newIds = new Set(items.map((item) => item.metadata.uid));
|
||||
|
||||
for (const uid of this._entities.keys()) {
|
||||
if (!newIds.has(uid)) {
|
||||
this._entities.delete(uid);
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
this.updateItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
@action protected updateItem(item: (CatalogEntityData & CatalogEntityKindData)) {
|
||||
const existing = this._entities.get(item.metadata.uid);
|
||||
|
||||
if (!existing) {
|
||||
const entity = this.dependencies.categoryRegistry.getEntityForData(item);
|
||||
|
||||
if (entity) {
|
||||
this._entities.set(entity.getId(), entity);
|
||||
} else {
|
||||
this.rawEntities.push(item);
|
||||
}
|
||||
} else {
|
||||
existing.metadata = item.metadata;
|
||||
existing.spec = item.spec;
|
||||
existing.status = item.status;
|
||||
}
|
||||
}
|
||||
|
||||
protected processRawEntities() {
|
||||
const items = [...this.rawEntities];
|
||||
|
||||
this.rawEntities.length = 0;
|
||||
|
||||
for (const item of items) {
|
||||
this.updateItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
readonly items = computed(() => {
|
||||
this.processRawEntities();
|
||||
|
||||
return Array.from(this._entities.values());
|
||||
});
|
||||
|
||||
@computed get filteredItems() {
|
||||
return Array.from(
|
||||
iter.reduce(
|
||||
this.filters,
|
||||
iter.filter,
|
||||
this.items.get().values(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@computed get entities(): Map<string, CatalogEntity> {
|
||||
return new Map(
|
||||
this.items.get().map(entity => [entity.getId(), entity]),
|
||||
);
|
||||
}
|
||||
|
||||
@computed get filteredEntities(): Map<string, CatalogEntity> {
|
||||
return new Map(
|
||||
this.filteredItems.map(entity => [entity.getId(), entity]),
|
||||
);
|
||||
}
|
||||
|
||||
getById(id: string) {
|
||||
return this.entities.get(id);
|
||||
}
|
||||
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string, { filtered = false } = {}): T[] {
|
||||
const byApiKind = (item: CatalogEntity) => item.apiVersion === apiVersion && item.kind === kind;
|
||||
const entities = filtered ? this.filteredItems : this.items.get();
|
||||
|
||||
return entities.filter(byApiKind) as T[];
|
||||
}
|
||||
|
||||
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory, { filtered = false } = {}): T[] {
|
||||
const supportedVersions = new Set(category.spec.versions.map((v) => `${category.spec.group}/${v.name}`));
|
||||
const byApiVersionKind = (item: CatalogEntity) => supportedVersions.has(item.apiVersion) && item.kind === category.spec.names.kind;
|
||||
const entities = filtered ? this.filteredItems : this.items.get();
|
||||
|
||||
return entities.filter(byApiVersionKind) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new filter to the set of item filters
|
||||
* @param fn The function that should return a truthy value if that entity should be sent currently "active"
|
||||
* @returns A function to remove that filter
|
||||
*/
|
||||
@action addCatalogFilter(fn: EntityFilter): Disposer {
|
||||
this.filters.add(fn);
|
||||
|
||||
return once(() => void this.filters.delete(fn));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a onBeforeRun hook. If `onBeforeRun` was previously added then it will not be added again
|
||||
* @param onBeforeRun The function that should return a boolean if the onRun of catalog entity should be triggered.
|
||||
* @returns A function to remove that hook
|
||||
*/
|
||||
@action addOnBeforeRun(onBeforeRun: CatalogEntityOnBeforeRun): Disposer {
|
||||
this.onBeforeRunHooks.add(onBeforeRun);
|
||||
|
||||
return once(() => void this.onBeforeRunHooks.delete(onBeforeRun));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all the registered `onBeforeRun` hooks, short circuiting on the first event that's preventDefaulted
|
||||
* @param entity The entity to run the hooks on
|
||||
* @returns Whether the entities `onRun` method should be executed
|
||||
*/
|
||||
async onBeforeRun(entity: CatalogEntity): Promise<boolean> {
|
||||
this.dependencies.logger.debug(`[CATALOG-ENTITY-REGISTRY]: run onBeforeRun on ${entity.getId()}`);
|
||||
|
||||
const runEvent = new CatalogRunEvent({ target: entity });
|
||||
|
||||
for (const onBeforeRun of this.onBeforeRunHooks) {
|
||||
try {
|
||||
await onBeforeRun(runEvent);
|
||||
} catch (error) {
|
||||
this.dependencies.logger.warn(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onBeforeRun threw an error`, error);
|
||||
}
|
||||
|
||||
if (runEvent.defaultPrevented) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the onBeforeRun check and, if successful, then proceed to call `entity`'s onRun method
|
||||
* @param entity The instance to invoke the hooks and then execute the onRun
|
||||
*/
|
||||
onRun(entity: CatalogEntity): void {
|
||||
this.onBeforeRun(entity)
|
||||
.then(doOnRun => {
|
||||
if (doOnRun) {
|
||||
return entity.onRun?.({
|
||||
navigate: this.dependencies.navigate,
|
||||
setCommandPaletteContext: (entity) => {
|
||||
this.activeEntity = entity;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.dependencies.logger.debug(`onBeforeRun for ${entity.getId()} returned false`);
|
||||
}
|
||||
})
|
||||
.catch(error => this.dependencies.logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import hostedClusterIdInjectable from "../cluster-frame-context/hosted-cluster-id.injectable";
|
||||
import defaultWebsocketApiParamsInjectable from "./default-websocket-api-params.injectable";
|
||||
import type { TerminalApiDependencies, TerminalApiQuery } from "./terminal-api";
|
||||
import { TerminalApi } from "./terminal-api";
|
||||
|
||||
export type CreateTerminalApi = (query: TerminalApiQuery) => TerminalApi;
|
||||
|
||||
const createTerminalApiInjectable = getInjectable({
|
||||
id: "create-terminal-api",
|
||||
instantiate: (di): CreateTerminalApi => {
|
||||
const hostedClusterId = di.inject(hostedClusterIdInjectable);
|
||||
const deps: Omit<TerminalApiDependencies, "hostedClusterId"> = {
|
||||
logger: di.inject(loggerInjectable),
|
||||
defaultParams: di.inject(defaultWebsocketApiParamsInjectable),
|
||||
};
|
||||
|
||||
return (query) => {
|
||||
assert(hostedClusterId, "Can only create terminal APIs within a cluster frame");
|
||||
|
||||
return new TerminalApi({
|
||||
hostedClusterId,
|
||||
...deps,
|
||||
}, query);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createTerminalApiInjectable;
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { TerminalMessage } from "../../common/terminal/channels";
|
||||
import { TerminalChannels } from "../../common/terminal/channels";
|
||||
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
||||
|
||||
export type DefaultWebsocketApiParams = ReturnType<typeof defaultWebsocketApiParamsInjectable.instantiate>;
|
||||
|
||||
const defaultWebsocketApiParamsInjectable = getInjectable({
|
||||
id: "default-websocket-api-params",
|
||||
instantiate: (di) => ({
|
||||
logging: di.inject(isDevelopmentInjectable),
|
||||
reconnectDelay: 10,
|
||||
flushOnOpen: true,
|
||||
pingMessage: JSON.stringify({ type: TerminalChannels.PING } as TerminalMessage),
|
||||
}),
|
||||
});
|
||||
|
||||
export default defaultWebsocketApiParamsInjectable;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { reaction, when } from "mobx";
|
||||
import type { GeneralEntity } from "../../../common/catalog-entities";
|
||||
import generalCategoryInjectable from "../../../common/catalog/categories/general.injectable";
|
||||
import isActiveRouteInjectable from "../../navigation/is-route-active.injectable";
|
||||
import observableHistoryInjectable from "../../navigation/observable-history.injectable";
|
||||
import type { Disposer } from "../../utils";
|
||||
import { disposer } from "../../utils";
|
||||
import catalogEntityRegistryInjectable from "../catalog/entity/registry.injectable";
|
||||
|
||||
export type WatchForGeneralEntityNavigation = () => Disposer;
|
||||
|
||||
const watchForGeneralEntityNavigationInjectable = getInjectable({
|
||||
id: "watch-for-general-entity-navigation",
|
||||
instantiate: (di): WatchForGeneralEntityNavigation => {
|
||||
const observableHistory = di.inject(observableHistoryInjectable);
|
||||
const isActiveRoute = di.inject(isActiveRouteInjectable);
|
||||
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
const generalCategory = di.inject(generalCategoryInjectable);
|
||||
|
||||
return () => {
|
||||
const dispose = disposer();
|
||||
|
||||
dispose.push(when(
|
||||
() => entityRegistry.entities.size > 0,
|
||||
() => {
|
||||
dispose.push(reaction(
|
||||
() => observableHistory.location,
|
||||
() => {
|
||||
const entities = entityRegistry.getItemsForCategory(generalCategory) as GeneralEntity[];
|
||||
const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path));
|
||||
|
||||
if (activeEntity) {
|
||||
entityRegistry.activeEntity = activeEntity;
|
||||
}
|
||||
},
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
));
|
||||
},
|
||||
));
|
||||
|
||||
return dispose;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default watchForGeneralEntityNavigationInjectable;
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { WebSocketApiDependencies, WebSocketEvents } from "./websocket-api";
|
||||
import { WebSocketApi } from "./websocket-api";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import url from "url";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { ipcRenderer } from "electron";
|
||||
import type { Logger } from "../../common/logger";
|
||||
import { once } from "lodash";
|
||||
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
|
||||
|
||||
enum TerminalColor {
|
||||
RED = "\u001b[31m",
|
||||
GREEN = "\u001b[32m",
|
||||
YELLOW = "\u001b[33m",
|
||||
BLUE = "\u001b[34m",
|
||||
MAGENTA = "\u001b[35m",
|
||||
CYAN = "\u001b[36m",
|
||||
GRAY = "\u001b[90m",
|
||||
LIGHT_GRAY = "\u001b[37m",
|
||||
NO_COLOR = "\u001b[0m",
|
||||
}
|
||||
|
||||
export interface TerminalApiQuery extends Record<string, string | undefined> {
|
||||
id: string;
|
||||
node?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface TerminalEvents extends WebSocketEvents {
|
||||
ready: () => void;
|
||||
connected: () => void;
|
||||
}
|
||||
|
||||
export interface TerminalApiDependencies extends WebSocketApiDependencies {
|
||||
readonly hostedClusterId: string;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class TerminalApi extends WebSocketApi<TerminalEvents> {
|
||||
protected size?: { width: number; height: number };
|
||||
|
||||
@observable public isReady = false;
|
||||
|
||||
constructor(protected readonly dependencies: TerminalApiDependencies, protected readonly query: TerminalApiQuery) {
|
||||
super(dependencies, {
|
||||
flushOnOpen: false,
|
||||
pingInterval: 30,
|
||||
});
|
||||
makeObservable(this);
|
||||
|
||||
if (query.node) {
|
||||
query.type ||= "node";
|
||||
}
|
||||
}
|
||||
|
||||
async connect() {
|
||||
if (!this.socket) {
|
||||
/**
|
||||
* Only emit this message if we are not "reconnecting", so as to keep the
|
||||
* output display clean when the computer wakes from sleep
|
||||
*/
|
||||
this.emitStatus("Connecting ...");
|
||||
}
|
||||
|
||||
const authTokenArray = await ipcRenderer.invoke("cluster:shell-api", this.dependencies.hostedClusterId, this.query.id);
|
||||
|
||||
if (!(authTokenArray instanceof Uint8Array)) {
|
||||
throw new TypeError("ShellApi token is not a Uint8Array");
|
||||
}
|
||||
|
||||
const { hostname, protocol, port } = location;
|
||||
const socketUrl = url.format({
|
||||
protocol: protocol.includes("https") ? "wss" : "ws",
|
||||
hostname,
|
||||
port,
|
||||
pathname: "/api",
|
||||
query: {
|
||||
...this.query,
|
||||
shellToken: Buffer.from(authTokenArray).toString("base64"),
|
||||
},
|
||||
slashes: true,
|
||||
});
|
||||
|
||||
const onReady = once((data?: string) => {
|
||||
this.isReady = true;
|
||||
this.emit("ready");
|
||||
this.removeListener("data", onReady);
|
||||
this.removeListener("connected", onReady);
|
||||
this.flush();
|
||||
|
||||
// data is undefined if the event that was handled is "connected"
|
||||
if (data === undefined) {
|
||||
const lastData = window.localStorage.getItem(`${this.query.id}:last-data`);
|
||||
|
||||
if (lastData) {
|
||||
/**
|
||||
* Output the last line, the makes sure that the terminal isn't completely
|
||||
* empty when the user refreshes.
|
||||
*/
|
||||
this.emit("data", lastData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.prependListener("data", onReady);
|
||||
this.prependListener("connected", onReady);
|
||||
|
||||
super.connect(socketUrl);
|
||||
}
|
||||
|
||||
sendMessage(message: TerminalMessage) {
|
||||
return this.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
sendTerminalSize(cols: number, rows: number) {
|
||||
const newSize = { width: cols, height: rows };
|
||||
|
||||
if (!isEqual(this.size, newSize)) {
|
||||
this.sendMessage({
|
||||
type: TerminalChannels.RESIZE,
|
||||
data: newSize,
|
||||
});
|
||||
this.size = newSize;
|
||||
}
|
||||
}
|
||||
|
||||
protected _onMessage({ data, ...evt }: MessageEvent<string>): void {
|
||||
try {
|
||||
const message = JSON.parse(data) as TerminalMessage;
|
||||
|
||||
switch (message.type) {
|
||||
case TerminalChannels.STDOUT:
|
||||
/**
|
||||
* save the last data for reconnections. User localStorage because we
|
||||
* don't want this data to survive if the app is closed
|
||||
*/
|
||||
window.localStorage.setItem(`${this.query.id}:last-data`, message.data);
|
||||
super._onMessage({ data: message.data, ...evt });
|
||||
break;
|
||||
case TerminalChannels.CONNECTED:
|
||||
this.emit("connected");
|
||||
break;
|
||||
default:
|
||||
this.dependencies.logger.warn(`[TERMINAL-API]: unknown or unhandleable message type`, message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(`[TERMINAL-API]: failed to handle message`, error);
|
||||
}
|
||||
}
|
||||
|
||||
protected _onOpen(evt: Event) {
|
||||
// Client should send terminal size in special channel 4,
|
||||
// But this size will be changed by terminal.fit()
|
||||
this.sendTerminalSize(120, 80);
|
||||
super._onOpen(evt);
|
||||
}
|
||||
|
||||
protected _onClose(evt: CloseEvent) {
|
||||
super._onClose(evt);
|
||||
this.isReady = false;
|
||||
}
|
||||
|
||||
protected emitStatus(data: string, options: { color?: TerminalColor; showTime?: boolean } = {}) {
|
||||
const { color, showTime } = options;
|
||||
const time = showTime ? `${(new Date()).toLocaleString()} ` : "";
|
||||
|
||||
if (color) {
|
||||
data = `${color}${data}${TerminalColor.NO_COLOR}`;
|
||||
}
|
||||
|
||||
this.emit("data", `${time}${data}\r\n`);
|
||||
}
|
||||
|
||||
protected emitError(error: string) {
|
||||
this.emitStatus(error, {
|
||||
color: TerminalColor.RED,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import EventEmitter from "events";
|
||||
import type TypedEventEmitter from "typed-emitter";
|
||||
import type { Arguments } from "typed-emitter";
|
||||
import type { Defaulted } from "../utils";
|
||||
import type { DefaultWebsocketApiParams } from "./default-websocket-api-params.injectable";
|
||||
|
||||
interface WebsocketApiParams {
|
||||
/**
|
||||
* Flush pending commands on open socket
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
flushOnOpen?: boolean;
|
||||
|
||||
/**
|
||||
* In case of an error, wait this many seconds before reconnecting.
|
||||
*
|
||||
* If falsy, don't reconnect
|
||||
*
|
||||
* @default 10
|
||||
*/
|
||||
reconnectDelay?: number;
|
||||
|
||||
/**
|
||||
* The message for pinging the websocket
|
||||
*
|
||||
* @default "{type: \"ping\"}"
|
||||
*/
|
||||
pingMessage?: string;
|
||||
|
||||
/**
|
||||
* If set to a number > 0, then the API will ping the socket on that interval.
|
||||
*
|
||||
* @unit seconds
|
||||
*/
|
||||
pingInterval?: number;
|
||||
|
||||
/**
|
||||
* Whether to show logs in the console
|
||||
*
|
||||
* @default isDevelopment
|
||||
*/
|
||||
logging?: boolean;
|
||||
}
|
||||
|
||||
export enum WebSocketApiState {
|
||||
PENDING = "pending",
|
||||
OPEN = "open",
|
||||
CONNECTING = "connecting",
|
||||
RECONNECTING = "reconnecting",
|
||||
CLOSED = "closed",
|
||||
}
|
||||
|
||||
export interface WebSocketEvents {
|
||||
open: () => void;
|
||||
data: (message: string) => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export interface WebSocketApiDependencies {
|
||||
readonly defaultParams: DefaultWebsocketApiParams;
|
||||
}
|
||||
|
||||
export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter as { new<T>(): TypedEventEmitter<T> })<Events> {
|
||||
protected socket: WebSocket | null = null;
|
||||
protected pendingCommands: string[] = [];
|
||||
protected reconnectTimer?: number;
|
||||
protected pingTimer?: number;
|
||||
protected params: Defaulted<WebsocketApiParams, keyof DefaultWebsocketApiParams>;
|
||||
|
||||
@observable readyState = WebSocketApiState.PENDING;
|
||||
|
||||
constructor(protected readonly dependencies: WebSocketApiDependencies, params: WebsocketApiParams) {
|
||||
super();
|
||||
makeObservable(this);
|
||||
this.params = {
|
||||
...this.dependencies.defaultParams,
|
||||
...params,
|
||||
};
|
||||
const { pingInterval } = this.params;
|
||||
|
||||
if (pingInterval) {
|
||||
this.pingTimer = window.setInterval(() => this.ping(), pingInterval * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
isConnected(): this is (WebSocketApi<Events> & { socket: WebSocket }) {
|
||||
return this.socket?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
connect(url: string) {
|
||||
// close previous connection first
|
||||
this.socket?.close();
|
||||
|
||||
// start new connection
|
||||
this.socket = new WebSocket(url);
|
||||
this.socket.addEventListener("open", ev => this._onOpen(ev));
|
||||
this.socket.addEventListener("message", ev => this._onMessage(ev));
|
||||
this.socket.addEventListener("error", ev => this._onError(ev));
|
||||
this.socket.addEventListener("close", ev => this._onClose(ev));
|
||||
this.readyState = WebSocketApiState.CONNECTING;
|
||||
}
|
||||
|
||||
ping() {
|
||||
if (this.isConnected()) {
|
||||
this.send(this.params.pingMessage);
|
||||
}
|
||||
}
|
||||
|
||||
reconnect(): void {
|
||||
if (!this.socket) {
|
||||
return void console.error("[WEBSOCKET-API]: cannot reconnect to a socket that is not connected");
|
||||
}
|
||||
|
||||
this.connect(this.socket.url);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.socket) return;
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
this.pendingCommands = [];
|
||||
this.clearAllListeners();
|
||||
clearTimeout(this.reconnectTimer);
|
||||
clearInterval(this.pingTimer);
|
||||
this.readyState = WebSocketApiState.PENDING;
|
||||
}
|
||||
|
||||
clearAllListeners() {
|
||||
for (const name of this.eventNames()) {
|
||||
this.removeAllListeners(name as keyof Events);
|
||||
}
|
||||
}
|
||||
|
||||
send(command: string) {
|
||||
if (this.isConnected()) {
|
||||
this.socket.send(command);
|
||||
} else {
|
||||
this.pendingCommands.push(command);
|
||||
}
|
||||
}
|
||||
|
||||
protected flush() {
|
||||
const commands = this.pendingCommands;
|
||||
|
||||
this.pendingCommands = [];
|
||||
|
||||
for (const command of commands) {
|
||||
this.send(command);
|
||||
}
|
||||
}
|
||||
|
||||
protected _onOpen(evt: Event) {
|
||||
this.emit("open", ...[] as Arguments<Events["open"]>);
|
||||
if (this.params.flushOnOpen) this.flush();
|
||||
this.readyState = WebSocketApiState.OPEN;
|
||||
this.writeLog("%cOPEN", "color:green;font-weight:bold;", evt);
|
||||
}
|
||||
|
||||
protected _onMessage({ data }: MessageEvent): void {
|
||||
this.emit("data", ...[data] as Arguments<Events["data"]>);
|
||||
this.writeLog("%cMESSAGE", "color:black;font-weight:bold;", data);
|
||||
}
|
||||
|
||||
protected _onError(evt: Event) {
|
||||
this.writeLog("%cERROR", "color:red;font-weight:bold;", evt);
|
||||
}
|
||||
|
||||
protected _onClose(evt: CloseEvent) {
|
||||
const error = evt.code !== 1000 || !evt.wasClean;
|
||||
|
||||
if (error) {
|
||||
const { reconnectDelay } = this.params;
|
||||
|
||||
if (reconnectDelay && this.socket) {
|
||||
const url = this.socket.url;
|
||||
|
||||
this.writeLog("will reconnect in", `${reconnectDelay}s`);
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => this.connect(url), reconnectDelay * 1000);
|
||||
this.readyState = WebSocketApiState.RECONNECTING;
|
||||
}
|
||||
} else {
|
||||
this.readyState = WebSocketApiState.CLOSED;
|
||||
this.emit("close", ...[] as Arguments<Events["close"]>);
|
||||
}
|
||||
this.writeLog("%cCLOSE", `color:${error ? "red" : "black"};font-weight:bold;`, evt);
|
||||
}
|
||||
|
||||
protected writeLog(...data: any[]) {
|
||||
if (this.params.logging) {
|
||||
console.debug(...data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user