mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
fix in-tree extensions
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
cc684d62c3
commit
10fee4a451
8221
extensions/metrics-cluster-feature/package-lock.json
generated
8221
extensions/metrics-cluster-feature/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,6 @@
|
|||||||
"dev": "npm run build --watch",
|
"dev": "npm run build --watch",
|
||||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
|
|||||||
@ -1,21 +1,55 @@
|
|||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { LensRendererExtension, Store, Interface, Component } from "@k8slens/extensions";
|
||||||
import { MetricsFeature } from "./src/metrics-feature";
|
import { MetricsFeature } from "./src/metrics-feature";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
|
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
|
||||||
clusterFeatures = [
|
onActivate() {
|
||||||
{
|
const category = Store.catalogCategories.getForGroupKind<Store.KubernetesClusterCategory>("entity.k8slens.dev", "KubernetesCluster");
|
||||||
title: "Metrics Stack",
|
|
||||||
components: {
|
if (!category) {
|
||||||
Description: () => (
|
return;
|
||||||
<span>
|
|
||||||
Enable timeseries data visualization (Prometheus stack) for your cluster.
|
|
||||||
Install this only if you don't have existing Prometheus stack installed.
|
|
||||||
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/metrics-cluster-feature/resources" rel="noreferrer" target="_blank">here</a>.
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
feature: new MetricsFeature()
|
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
category.on("contextMenuOpen", this.clusterContextMenuOpen.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async clusterContextMenuOpen(cluster: Store.KubernetesCluster, ctx: Interface.CatalogEntityContextMenuContext) {
|
||||||
|
if (!cluster.status.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metricsFeature = new MetricsFeature();
|
||||||
|
|
||||||
|
await metricsFeature.updateStatus(cluster);
|
||||||
|
|
||||||
|
if (metricsFeature.status.installed) {
|
||||||
|
if (metricsFeature.status.canUpgrade) {
|
||||||
|
ctx.menuItems.unshift({
|
||||||
|
icon: "refresh",
|
||||||
|
title: "Upgrade Lens Metrics stack",
|
||||||
|
onClick: async () => {
|
||||||
|
metricsFeature.upgrade(cluster);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.menuItems.unshift({
|
||||||
|
icon: "toggle_off",
|
||||||
|
title: "Uninstall Lens Metrics stack",
|
||||||
|
onClick: async () => {
|
||||||
|
await metricsFeature.uninstall(cluster);
|
||||||
|
|
||||||
|
Component.Notifications.info(`Lens Metrics has been removed from ${cluster.metadata.name}`, { timeout: 10_000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ctx.menuItems.unshift({
|
||||||
|
icon: "toggle_on",
|
||||||
|
title: "Install Lens Metrics stack",
|
||||||
|
onClick: async () => {
|
||||||
|
metricsFeature.install(cluster);
|
||||||
|
|
||||||
|
Component.Notifications.info(`Lens Metrics is now installed to ${cluster.metadata.name}`, { timeout: 10_000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
storageClass: null,
|
storageClass: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
async install(cluster: Store.Cluster): Promise<void> {
|
async install(cluster: Store.KubernetesCluster): Promise<void> {
|
||||||
// Check if there are storageclasses
|
// Check if there are storageclasses
|
||||||
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
|
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
|
||||||
const scs = await storageClassApi.list();
|
const scs = await storageClassApi.list();
|
||||||
@ -62,11 +62,11 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async upgrade(cluster: Store.Cluster): Promise<void> {
|
async upgrade(cluster: Store.KubernetesCluster): Promise<void> {
|
||||||
return this.install(cluster);
|
return this.install(cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
|
async updateStatus(cluster: Store.KubernetesCluster): Promise<ClusterFeature.FeatureStatus> {
|
||||||
try {
|
try {
|
||||||
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
|
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
|
||||||
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
||||||
@ -87,12 +87,13 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
return this.status;
|
return this.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstall(cluster: Store.Cluster): Promise<void> {
|
async uninstall(cluster: Store.KubernetesCluster): Promise<void> {
|
||||||
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace);
|
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace);
|
||||||
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding);
|
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding);
|
||||||
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole);
|
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole);
|
||||||
|
|
||||||
await namespaceApi.delete({name: "lens-metrics"});
|
await namespaceApi.delete({name: "lens-metrics"});
|
||||||
await clusterRoleBindingApi.delete({name: "lens-prometheus"});
|
await clusterRoleBindingApi.delete({name: "lens-prometheus"});
|
||||||
await clusterRoleApi.delete({name: "lens-prometheus"}); }
|
await clusterRoleApi.delete({name: "lens-prometheus"});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8428
extensions/telemetry/package-lock.json
generated
8428
extensions/telemetry/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,6 @@
|
|||||||
"dev": "webpack --watch",
|
"dev": "webpack --watch",
|
||||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||||
"@types/analytics-node": "^3.1.3",
|
"@types/analytics-node": "^3.1.3",
|
||||||
|
|||||||
@ -102,13 +102,12 @@ export class Tracker extends Util.Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected reportData() {
|
protected reportData() {
|
||||||
const clustersList = Store.clusterStore.enabledClustersList;
|
const clustersList = Store.catalogEntities.getItemsForApiKind<Store.KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster");
|
||||||
|
|
||||||
this.event("generic-data", "report", {
|
this.event("generic-data", "report", {
|
||||||
appVersion: App.version,
|
appVersion: App.version,
|
||||||
os: this.os,
|
os: this.os,
|
||||||
clustersCount: clustersList.length,
|
clustersCount: clustersList.length,
|
||||||
workspacesCount: Store.workspaceStore.enabledWorkspacesList.length,
|
|
||||||
extensions: App.getEnabledExtensions()
|
extensions: App.getEnabledExtensions()
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -118,10 +117,10 @@ export class Tracker extends Util.Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected reportClusterData(cluster: Store.ClusterModel) {
|
protected reportClusterData(cluster: Store.KubernetesCluster) {
|
||||||
this.event("cluster-data", "report", {
|
this.event("cluster-data", "report", {
|
||||||
id: cluster.metadata.id,
|
id: cluster.metadata.id,
|
||||||
managed: !!cluster.ownerRef,
|
managed: cluster.metadata.source !== "local",
|
||||||
kubernetesVersion: cluster.metadata.version,
|
kubernetesVersion: cluster.metadata.version,
|
||||||
distribution: cluster.metadata.distribution,
|
distribution: cluster.metadata.distribution,
|
||||||
nodesCount: cluster.metadata.nodes,
|
nodesCount: cluster.metadata.nodes,
|
||||||
|
|||||||
@ -16,19 +16,20 @@ export class CatalogCategoryRegistry {
|
|||||||
return toJS(this.categories);
|
return toJS(this.categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntityForData(data: CatalogEntityData) {
|
getForGroupKind<T extends CatalogCategory>(group: string, kind: string) {
|
||||||
const splitApiVersion = data.apiVersion.split("/");
|
return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T;
|
||||||
const group = splitApiVersion[0];
|
}
|
||||||
const version = splitApiVersion[1];
|
|
||||||
|
|
||||||
const category = this.categories.find((category) => {
|
getEntityForData(data: CatalogEntityData) {
|
||||||
return category.spec.group === group && category.spec.names.kind === data.kind;
|
const category = this.getCategoryForEntity(data);
|
||||||
});
|
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const splitApiVersion = data.apiVersion.split("/");
|
||||||
|
const version = splitApiVersion[1];
|
||||||
|
|
||||||
const specVersion = category.spec.versions.find((v) => v.name === version);
|
const specVersion = category.spec.versions.find((v) => v.name === version);
|
||||||
|
|
||||||
if (!specVersion) {
|
if (!specVersion) {
|
||||||
@ -37,6 +38,19 @@ export class CatalogCategoryRegistry {
|
|||||||
|
|
||||||
return new specVersion.entityClass(data);
|
return new specVersion.entityClass(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData) {
|
||||||
|
const splitApiVersion = data.apiVersion.split("/");
|
||||||
|
const group = splitApiVersion[0];
|
||||||
|
|
||||||
|
const category = this.categories.find((category) => {
|
||||||
|
return category.spec.group === group && category.spec.names.kind === data.kind;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!category) return null;
|
||||||
|
|
||||||
|
return category as T;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
||||||
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
||||||
@ -67,10 +68,14 @@ export class KubernetesCluster implements CatalogEntity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);
|
||||||
|
|
||||||
|
if (category) category.emit("contextMenuOpen", this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubernetesClusterCategory implements CatalogCategory {
|
export class KubernetesClusterCategory extends EventEmitter implements CatalogCategory {
|
||||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||||
public readonly kind = "CatalogCategory";
|
public readonly kind = "CatalogCategory";
|
||||||
public metadata = {
|
public metadata = {
|
||||||
|
|||||||
@ -3,11 +3,12 @@ import path from "path";
|
|||||||
import hb from "handlebars";
|
import hb from "handlebars";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { ResourceApplier } from "../main/resource-applier";
|
import { ResourceApplier } from "../main/resource-applier";
|
||||||
import { Cluster } from "../main/cluster";
|
import { KubernetesCluster } from "./core-api/stores";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { requestMain } from "../common/ipc";
|
import { requestMain } from "../common/ipc";
|
||||||
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
||||||
|
import { clusterStore } from "../common/cluster-store";
|
||||||
|
|
||||||
export interface ClusterFeatureStatus {
|
export interface ClusterFeatureStatus {
|
||||||
/** feature's current version, as set by the implementation */
|
/** feature's current version, as set by the implementation */
|
||||||
@ -44,7 +45,7 @@ export abstract class ClusterFeature {
|
|||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature is to be installed on
|
* @param cluster the cluster that the feature is to be installed on
|
||||||
*/
|
*/
|
||||||
abstract async install(cluster: Cluster): Promise<void>;
|
abstract async install(cluster: KubernetesCluster): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation
|
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation
|
||||||
@ -52,7 +53,7 @@ export abstract class ClusterFeature {
|
|||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature is to be upgraded on
|
* @param cluster the cluster that the feature is to be upgraded on
|
||||||
*/
|
*/
|
||||||
abstract async upgrade(cluster: Cluster): Promise<void>;
|
abstract async upgrade(cluster: KubernetesCluster): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
|
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
|
||||||
@ -60,7 +61,7 @@ export abstract class ClusterFeature {
|
|||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature is to be uninstalled from
|
* @param cluster the cluster that the feature is to be uninstalled from
|
||||||
*/
|
*/
|
||||||
abstract async uninstall(cluster: Cluster): Promise<void>;
|
abstract async uninstall(cluster: KubernetesCluster): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
|
* to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
|
||||||
@ -72,7 +73,7 @@ export abstract class ClusterFeature {
|
|||||||
*
|
*
|
||||||
* @return a promise, resolved with the updated ClusterFeatureStatus
|
* @return a promise, resolved with the updated ClusterFeatureStatus
|
||||||
*/
|
*/
|
||||||
abstract async updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
abstract async updateStatus(cluster: KubernetesCluster): Promise<ClusterFeatureStatus>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this is a helper method that conveniently applies kubernetes resources to the cluster.
|
* this is a helper method that conveniently applies kubernetes resources to the cluster.
|
||||||
@ -82,9 +83,15 @@ export abstract class ClusterFeature {
|
|||||||
* files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the
|
* files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the
|
||||||
* cluster. As a string[] type resourceSpec is treated as an array of fully formed (not templated) kubernetes resources that are applied to the cluster
|
* cluster. As a string[] type resourceSpec is treated as an array of fully formed (not templated) kubernetes resources that are applied to the cluster
|
||||||
*/
|
*/
|
||||||
protected async applyResources(cluster: Cluster, resourceSpec: string | string[]) {
|
protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) {
|
||||||
let resources: string[];
|
let resources: string[];
|
||||||
|
|
||||||
|
const clusterModel = clusterStore.getById(cluster.metadata.uid);
|
||||||
|
|
||||||
|
if (!clusterModel) {
|
||||||
|
throw new Error(`cluster not found`);
|
||||||
|
}
|
||||||
|
|
||||||
if ( typeof resourceSpec === "string" ) {
|
if ( typeof resourceSpec === "string" ) {
|
||||||
resources = this.renderTemplates(resourceSpec);
|
resources = this.renderTemplates(resourceSpec);
|
||||||
} else {
|
} else {
|
||||||
@ -92,9 +99,9 @@ export abstract class ClusterFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (app) {
|
if (app) {
|
||||||
await new ResourceApplier(cluster).kubectlApplyAll(resources);
|
await new ResourceApplier(clusterModel).kubectlApplyAll(resources);
|
||||||
} else {
|
} else {
|
||||||
await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources);
|
await requestMain(clusterKubectlApplyAllHandler, cluster.metadata.uid, resources);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import { CatalogEntity } from "../../common/catalog-entity";
|
||||||
|
import { catalogEntityRegistry as registry } from "../../common/catalog-entity-registry";
|
||||||
|
|
||||||
|
export class CatalogEntityRegistry {
|
||||||
|
@computed getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||||
|
return registry.getItemsForApiKind<T>(apiVersion, kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogEntities = new CatalogEntityRegistry();
|
||||||
@ -1 +1,4 @@
|
|||||||
export { ExtensionStore } from "../extension-store";
|
export { ExtensionStore } from "../extension-store";
|
||||||
|
export { KubernetesCluster, KubernetesClusterCategory } from "../../common/catalog-entities/kubernetes-cluster";
|
||||||
|
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog-category-registry";
|
||||||
|
export { catalogEntities } from "./catalog";
|
||||||
|
|||||||
1
src/extensions/interfaces/catalog.ts
Normal file
1
src/extensions/interfaces/catalog.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "../../common/catalog-entity";
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from "./registrations";
|
export * from "./registrations";
|
||||||
|
export * from "./catalog";
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import React from "react";
|
|||||||
import { reaction } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import { RouteComponentProps } from "react-router";
|
import { RouteComponentProps } from "react-router";
|
||||||
import { observer, disposeOnUnmount } from "mobx-react";
|
import { observer, disposeOnUnmount } from "mobx-react";
|
||||||
import { Features } from "./features";
|
|
||||||
import { Removal } from "./removal";
|
|
||||||
import { Status } from "./status";
|
import { Status } from "./status";
|
||||||
import { General } from "./general";
|
import { General } from "./general";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
@ -65,8 +63,6 @@ export class ClusterSettings extends React.Component<Props> {
|
|||||||
<PageLayout className="ClusterSettings" header={header} showOnTop={true}>
|
<PageLayout className="ClusterSettings" header={header} showOnTop={true}>
|
||||||
<Status cluster={cluster}></Status>
|
<Status cluster={cluster}></Status>
|
||||||
<General cluster={cluster}></General>
|
<General cluster={cluster}></General>
|
||||||
<Features cluster={cluster}></Features>
|
|
||||||
<Removal cluster={cluster}></Removal>
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,110 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { observable, reaction, comparer } from "mobx";
|
|
||||||
import { observer, disposeOnUnmount } from "mobx-react";
|
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { Button } from "../../button";
|
|
||||||
import { Notifications } from "../../notifications";
|
|
||||||
import { Spinner } from "../../spinner";
|
|
||||||
import { ClusterFeature } from "../../../../extensions/cluster-feature";
|
|
||||||
import { interval } from "../../../utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster
|
|
||||||
feature: ClusterFeature
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
export class InstallFeature extends React.Component<Props> {
|
|
||||||
@observable loading = false;
|
|
||||||
@observable message = "";
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const feature = this.props.feature;
|
|
||||||
const cluster = this.props.cluster;
|
|
||||||
const statusUpdate = interval(20, () => {
|
|
||||||
feature.updateStatus(cluster);
|
|
||||||
});
|
|
||||||
|
|
||||||
statusUpdate.start(true);
|
|
||||||
|
|
||||||
disposeOnUnmount(this, () => {
|
|
||||||
statusUpdate.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
disposeOnUnmount(this,
|
|
||||||
reaction(() => feature.status.installed, () => {
|
|
||||||
this.loading = false;
|
|
||||||
this.message = "";
|
|
||||||
}, { equals: comparer.structural })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getActionButtons() {
|
|
||||||
const { cluster, feature } = this.props;
|
|
||||||
const disabled = !cluster.isAdmin || this.loading;
|
|
||||||
const loadingIcon = this.loading ? <Spinner/> : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gaps align-center">
|
|
||||||
{feature.status.canUpgrade &&
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={this.runAction(() =>
|
|
||||||
feature.upgrade(cluster)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Upgrade
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{feature.status.installed &&
|
|
||||||
<Button
|
|
||||||
accent
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={this.runAction(async () => {
|
|
||||||
this.message = "Uninstalling feature ...";
|
|
||||||
feature.uninstall(cluster);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Uninstall
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{!feature.status.installed && !feature.status.canUpgrade &&
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={this.runAction(async () =>{
|
|
||||||
this.message = "Installing feature ...";
|
|
||||||
feature.install(cluster);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Install
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{loadingIcon}
|
|
||||||
{!cluster.isAdmin && <span className='admin-note'>Actions can only be performed by admins.</span>}
|
|
||||||
{cluster.isAdmin && this.loading && this.message !== "" && <span className='admin-note'>{this.message}</span>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAction(action: () => Promise<any>): () => Promise<void> {
|
|
||||||
return async () => {
|
|
||||||
try {
|
|
||||||
this.loading = true;
|
|
||||||
await action();
|
|
||||||
} catch (err) {
|
|
||||||
Notifications.error(err.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.props.children}
|
|
||||||
<div className="button-area">{this.getActionButtons()}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
import { InstallFeature } from "./components/install-feature";
|
|
||||||
import { SubTitle } from "../layout/sub-title";
|
|
||||||
import { clusterFeatureRegistry } from "../../../extensions/registries/cluster-feature-registry";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Features extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Features</h2>
|
|
||||||
{
|
|
||||||
clusterFeatureRegistry
|
|
||||||
.getItems()
|
|
||||||
.map((f) => (
|
|
||||||
<InstallFeature key={f.title} cluster={cluster} feature={f.feature}>
|
|
||||||
<>
|
|
||||||
<SubTitle title={f.title} />
|
|
||||||
<p><f.components.Description /></p>
|
|
||||||
</>
|
|
||||||
</InstallFeature>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
import { RemoveClusterButton } from "./components/remove-cluster-button";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cluster: Cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Removal extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { cluster } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Removal</h2>
|
|
||||||
<RemoveClusterButton cluster={cluster} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { computed, reaction } from "mobx";
|
import { action, computed, reaction } from "mobx";
|
||||||
import { CatalogEntity, catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { CatalogEntity, catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { ItemObject, ItemStore } from "../../item.store";
|
import { ItemObject, ItemStore } from "../../item.store";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
@ -42,8 +42,9 @@ export class CatalogEntityItem implements ItemObject {
|
|||||||
this.entity.onRun(ctx);
|
this.entity.onRun(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
onContextMenuOpen(ctx: any) {
|
@action
|
||||||
this.entity.onContextMenuOpen(ctx);
|
async onContextMenuOpen(ctx: any) {
|
||||||
|
return this.entity.onContextMenuOpen(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import "./landing-page.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ItemListLayout } from "../item-object-list";
|
import { ItemListLayout } from "../item-object-list";
|
||||||
import { IReactionDisposer, observable } from "mobx";
|
import { IReactionDisposer, observable, reaction, toJS } from "mobx";
|
||||||
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { kebabCase } from "lodash";
|
import { kebabCase } from "lodash";
|
||||||
@ -12,6 +12,8 @@ import { Icon } from "../icon";
|
|||||||
import { CatalogEntityContextMenuContext } from "../../api/catalog-entity-registry";
|
import { CatalogEntityContextMenuContext } from "../../api/catalog-entity-registry";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { hotbarStore } from "../../../common/hotbar-store";
|
import { hotbarStore } from "../../../common/hotbar-store";
|
||||||
|
import { addClusterURL } from "../+add-cluster";
|
||||||
|
import { autobind } from "../../utils";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
@ -21,9 +23,14 @@ enum sortBy {
|
|||||||
@observer
|
@observer
|
||||||
export class LandingPage extends React.Component {
|
export class LandingPage extends React.Component {
|
||||||
@observable private catalogEntityStore?: CatalogEntityStore;
|
@observable private catalogEntityStore?: CatalogEntityStore;
|
||||||
|
@observable.deep private contextMenu: CatalogEntityContextMenuContext;
|
||||||
private disposers: IReactionDisposer[] = [];
|
private disposers: IReactionDisposer[] = [];
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.contextMenu = {
|
||||||
|
menuItems: [],
|
||||||
|
navigate: (url: string) => navigate(url)
|
||||||
|
};
|
||||||
this.catalogEntityStore = new CatalogEntityStore();
|
this.catalogEntityStore = new CatalogEntityStore();
|
||||||
this.disposers.push(this.catalogEntityStore.watch());
|
this.disposers.push(this.catalogEntityStore.watch());
|
||||||
}
|
}
|
||||||
@ -52,6 +59,31 @@ export class LandingPage extends React.Component {
|
|||||||
hotbar.items = hotbar.items.filter((i) => i.entity.uid !== item.id);
|
hotbar.items = hotbar.items.filter((i) => i.entity.uid !== item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
renderItemMenu(item: CatalogEntityItem) {
|
||||||
|
const onOpen = async () => {
|
||||||
|
await item.onContextMenuOpen(this.contextMenu);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuActions onOpen={() => onOpen()}>
|
||||||
|
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(item) }>
|
||||||
|
<Icon material="add" interactive={true} title="Add to hotbar"/> Add to Hotbar
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key="remove-from-hotbar" onClick={() => this.removeFromHotbar(item) }>
|
||||||
|
<Icon material="clear" interactive={true} title="Remove from hotbar"/> Remove from Hotbar
|
||||||
|
</MenuItem>
|
||||||
|
{ this.contextMenu.menuItems.map((menuItem) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={menuItem.title} onClick={() => menuItem.onClick()}>
|
||||||
|
<Icon material={menuItem.icon} interactive={true} title={menuItem.title}/> {menuItem.title}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MenuActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.catalogEntityStore) {
|
if (!this.catalogEntityStore) {
|
||||||
return null;
|
return null;
|
||||||
@ -81,31 +113,10 @@ export class LandingPage extends React.Component {
|
|||||||
{ title: item.phase, className: kebabCase(item.phase) }
|
{ title: item.phase, className: kebabCase(item.phase) }
|
||||||
]}
|
]}
|
||||||
onDetails={(item: CatalogEntityItem) => item.onRun({ navigate: (url: string) => navigate(url)})}
|
onDetails={(item: CatalogEntityItem) => item.onRun({ navigate: (url: string) => navigate(url)})}
|
||||||
renderItemMenu={(item: CatalogEntityItem) => {
|
renderItemMenu={this.renderItemMenu}
|
||||||
const menuOpenContext: CatalogEntityContextMenuContext = {
|
addRemoveButtons={{
|
||||||
menuItems: [],
|
addTooltip: "Add Kubernetes Cluster",
|
||||||
navigate: (url: string) => navigate(url)
|
onAdd: () => navigate(addClusterURL()),
|
||||||
};
|
|
||||||
|
|
||||||
item.onContextMenuOpen(menuOpenContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuActions>
|
|
||||||
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(item) }>
|
|
||||||
<Icon material="add" interactive={true} title="Add to hotbar"/> Add to Hotbar
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="remove-from-hotbar" onClick={() => this.removeFromHotbar(item) }>
|
|
||||||
<Icon material="clear" interactive={true} title="Remove from hotbar"/> Remove from Hotbar
|
|
||||||
</MenuItem>
|
|
||||||
{ menuOpenContext.menuItems.map((menuItem) => {
|
|
||||||
return (
|
|
||||||
<MenuItem key={menuItem.title} onClick={() => menuItem.onClick()}>
|
|
||||||
<Icon material={menuItem.icon} interactive={true} title={menuItem.title}/> {menuItem.title}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</MenuActions>
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export interface MenuActionsProps extends Partial<MenuProps> {
|
|||||||
removeConfirmationMessage?: React.ReactNode | (() => React.ReactNode);
|
removeConfirmationMessage?: React.ReactNode | (() => React.ReactNode);
|
||||||
updateAction?(): void;
|
updateAction?(): void;
|
||||||
removeAction?(): void;
|
removeAction?(): void;
|
||||||
|
onOpen?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -70,6 +71,10 @@ export class MenuActions extends React.Component<MenuActionsProps> {
|
|||||||
...(typeof triggerIcon === "object" ? triggerIcon : {}),
|
...(typeof triggerIcon === "object" ? triggerIcon : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.props.onOpen) {
|
||||||
|
iconProps.onClick = this.props.onOpen;
|
||||||
|
}
|
||||||
|
|
||||||
if (iconProps.tooltip && this.isOpen) {
|
if (iconProps.tooltip && this.isOpen) {
|
||||||
delete iconProps.tooltip; // don't show tooltip for icon when menu is open
|
delete iconProps.tooltip; // don't show tooltip for icon when menu is open
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user