1
0
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:
Jari Kolehmainen 2021-04-01 10:00:42 +03:00
parent cc684d62c3
commit 10fee4a451
21 changed files with 16791 additions and 261 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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&apos;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 });
}
});
}
}
} }

View File

@ -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"});
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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,

View File

@ -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();

View File

@ -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 = {

View File

@ -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);
} }
} }

View File

@ -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();

View File

@ -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";

View File

@ -0,0 +1 @@
export * from "../../common/catalog-entity";

View File

@ -1 +1,2 @@
export * from "./registrations"; export * from "./registrations";
export * from "./catalog";

View File

@ -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>
); );
} }

View File

@ -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>
</>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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);
} }
} }

View File

@ -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>

View File

@ -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
} }