mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
show lens-metrics on cluster settings
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
7b1b8e6321
commit
8b0b7fcbb2
@ -1,55 +1,27 @@
|
||||
import { LensRendererExtension, Interface, Component, Catalog} from "@k8slens/extensions";
|
||||
import { MetricsFeature } from "./src/metrics-feature";
|
||||
import React from "react";
|
||||
import { LensRendererExtension, Catalog } from "@k8slens/extensions";
|
||||
import { MetricsSettings } from "./src/metrics-settings";
|
||||
|
||||
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
|
||||
onActivate() {
|
||||
const category = Catalog.catalogCategories.getForGroupKind<Catalog.KubernetesClusterCategory>("entity.k8slens.dev", "KubernetesCluster");
|
||||
entitySettings = [
|
||||
{
|
||||
apiVersions: ["entity.k8slens.dev/v1alpha1"],
|
||||
kind: "KubernetesCluster",
|
||||
title: "Lens Metrics",
|
||||
priority: 5,
|
||||
components: {
|
||||
View: (props: {entity: Catalog.KubernetesCluster}) => { // eslint-disable-line
|
||||
const cluster = props.entity; // eslint-disable-line
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
category.on("contextMenuOpen", this.clusterContextMenuOpen.bind(this));
|
||||
}
|
||||
|
||||
async clusterContextMenuOpen(cluster: Catalog.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);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<section>
|
||||
<section>
|
||||
<MetricsSettings cluster={cluster} />
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
{{#if prometheus.enabled}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
@ -14,3 +15,4 @@ spec:
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: 9090
|
||||
{{/if}}
|
||||
@ -1,3 +1,4 @@
|
||||
{{#if prometheus.enabled}}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
@ -53,7 +54,7 @@ spec:
|
||||
mountPath: /var/lib/prometheus
|
||||
containers:
|
||||
- name: prometheus
|
||||
image: quay.io/prometheus/prometheus:v2.19.3
|
||||
image: quay.io/prometheus/prometheus:v2.20.1
|
||||
args:
|
||||
- --web.listen-address=0.0.0.0:9090
|
||||
- --config.file=/etc/prometheus/prometheus.yaml
|
||||
@ -114,3 +115,4 @@ spec:
|
||||
requests:
|
||||
storage: {{persistence.size}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@ -4,6 +4,9 @@ import * as path from "path";
|
||||
|
||||
export interface MetricsConfiguration {
|
||||
// Placeholder for Metrics config structure
|
||||
prometheus: {
|
||||
enabled: boolean;
|
||||
};
|
||||
persistence: {
|
||||
enabled: boolean;
|
||||
storageClass: string;
|
||||
@ -29,20 +32,23 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
||||
latestVersion = "v2.19.3-lens1";
|
||||
|
||||
templateContext: MetricsConfiguration = {
|
||||
prometheus: {
|
||||
enabled: false
|
||||
},
|
||||
persistence: {
|
||||
enabled: false,
|
||||
storageClass: null,
|
||||
size: "20G",
|
||||
},
|
||||
nodeExporter: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
},
|
||||
retention: {
|
||||
time: "2d",
|
||||
size: "5GB",
|
||||
},
|
||||
kubeStateMetrics: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
},
|
||||
alertManagers: null,
|
||||
replicas: 1,
|
||||
@ -59,7 +65,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
||||
sc.metadata?.annotations?.["storageclass.beta.kubernetes.io/is-default-class"] === "true"
|
||||
));
|
||||
|
||||
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
||||
await super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
||||
}
|
||||
|
||||
async upgrade(cluster: Catalog.KubernetesCluster): Promise<void> {
|
||||
@ -75,6 +81,13 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
||||
this.status.installed = true;
|
||||
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
|
||||
this.status.canUpgrade = semver.lt(this.status.currentVersion, this.latestVersion, true);
|
||||
|
||||
const pvcTemplate = prometheus.spec.volumeClaimTemplates[0];
|
||||
|
||||
if (pvcTemplate) {
|
||||
this.templateContext.persistence.enabled = true;
|
||||
this.templateContext.persistence.size = pvcTemplate.spec.resources.requests.storage;
|
||||
}
|
||||
} else {
|
||||
this.status.installed = false;
|
||||
}
|
||||
|
||||
213
extensions/metrics-cluster-feature/src/metrics-settings.tsx
Normal file
213
extensions/metrics-cluster-feature/src/metrics-settings.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
import React from "react";
|
||||
import { Component, Catalog, K8sApi } from "@k8slens/extensions";
|
||||
import { observer } from "mobx-react";
|
||||
import { observable } from "mobx";
|
||||
import { MetricsFeature } from "./metrics-feature";
|
||||
interface Props {
|
||||
cluster: Catalog.KubernetesCluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class MetricsSettings extends React.Component<Props> {
|
||||
@observable featureStates = {
|
||||
prometheus: false,
|
||||
kubeStateMetrics: false,
|
||||
nodeExporter: false
|
||||
};
|
||||
@observable canUpgrade = false;
|
||||
@observable upgrading = false;
|
||||
|
||||
pvcOptions = [
|
||||
{ value: "", label: "Use EmptyDir (no PVC)" },
|
||||
{ value: "5GB", label: "5GB" },
|
||||
{ value: "10GB", label: "10GB" },
|
||||
{ value: "20GB", label: "20GB" },
|
||||
{ value: "40GB", label: "40GB" },
|
||||
{ value: "80GB", label: "80GB" },
|
||||
];
|
||||
|
||||
featureSet: MetricsFeature;
|
||||
|
||||
async componentDidMount() {
|
||||
this.featureSet = new MetricsFeature();
|
||||
|
||||
await this.updateFeatureStates();
|
||||
}
|
||||
|
||||
async updateFeatureStates() {
|
||||
await this.featureSet.updateStatus(this.props.cluster);
|
||||
this.canUpgrade = this.featureSet.status.canUpgrade;
|
||||
|
||||
this.featureStates.prometheus = this.featureSet.status.installed;
|
||||
|
||||
const deployment = K8sApi.forCluster(this.props.cluster, K8sApi.Deployment);
|
||||
|
||||
try {
|
||||
await deployment.get({name: "kube-state-metrics", namespace: "lens-metrics"});
|
||||
this.featureStates.kubeStateMetrics = true;
|
||||
} catch(e) {
|
||||
if (e?.error?.code === 404) {
|
||||
this.featureStates.kubeStateMetrics = false;
|
||||
} else {
|
||||
this.featureStates.kubeStateMetrics = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const daemonSet = K8sApi.forCluster(this.props.cluster, K8sApi.DaemonSet);
|
||||
|
||||
try {
|
||||
await daemonSet.get({name: "node-exporter", namespace: "lens-metrics"});
|
||||
this.featureStates.nodeExporter = true;
|
||||
} catch(e) {
|
||||
if (e?.error?.code === 404) {
|
||||
this.featureStates.nodeExporter = false;
|
||||
} else {
|
||||
this.featureStates.nodeExporter = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
const ctx = this.featureSet.templateContext;
|
||||
|
||||
ctx.prometheus.enabled = !!this.featureStates.prometheus;
|
||||
ctx.kubeStateMetrics.enabled = !!this.featureStates.kubeStateMetrics;
|
||||
ctx.nodeExporter.enabled = !!this.featureStates.nodeExporter;
|
||||
|
||||
if (!ctx.prometheus.enabled && !ctx.kubeStateMetrics.enabled && !ctx.nodeExporter.enabled) {
|
||||
await this.featureSet.uninstall(this.props.cluster);
|
||||
} else {
|
||||
await this.featureSet.install(this.props.cluster);
|
||||
}
|
||||
}
|
||||
|
||||
async togglePrometheus(enabled: boolean) {
|
||||
this.featureStates.prometheus = enabled;
|
||||
|
||||
this.featureSet.templateContext.prometheus.enabled = this.featureStates.prometheus;
|
||||
|
||||
try {
|
||||
await this.save();
|
||||
} catch(error) {
|
||||
this.featureStates.prometheus = !enabled;
|
||||
Component.Notifications.error(`Failed to ${enabled ? "enable" : "disable"} Prometheus: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleKubeStateMetrics(enabled: boolean) {
|
||||
this.featureStates.kubeStateMetrics = enabled;
|
||||
|
||||
this.featureSet.templateContext.kubeStateMetrics.enabled = this.featureStates.kubeStateMetrics;
|
||||
|
||||
try {
|
||||
await this.save();
|
||||
} catch(error) {
|
||||
this.featureStates.kubeStateMetrics = !enabled;
|
||||
Component.Notifications.error(`Failed to ${enabled ? "enable" : "disable"} kube-state-metrics: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleNodeExporter(enabled: boolean) {
|
||||
this.featureStates.nodeExporter = enabled;
|
||||
|
||||
this.featureSet.templateContext.nodeExporter.enabled = this.featureStates.nodeExporter;
|
||||
|
||||
try {
|
||||
await this.save();
|
||||
} catch(error) {
|
||||
this.featureStates.nodeExporter = !enabled;
|
||||
Component.Notifications.error(`Failed to ${enabled ? "enable" : "disable"} node-exporter: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async updateStack() {
|
||||
this.upgrading = true;
|
||||
|
||||
await this.save();
|
||||
setTimeout(() => {
|
||||
Component.Notifications.info("Lens Metrics stack updated!", {timeout: 5_000});
|
||||
this.upgrading = false;
|
||||
this.canUpgrade = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{ !this.props.cluster.status.active && (
|
||||
<section>
|
||||
<p style={ {color: "var(--colorError)"} }>
|
||||
Lens Metrics settings requires established connection to the cluster.
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
{ this.canUpgrade && (
|
||||
<section>
|
||||
<Component.SubTitle title="Software Update" />
|
||||
|
||||
<Component.Button label={this.upgrading ? "Updating ..." : "Update Now"} primary onClick={() => this.updateStack() } waiting={this.upgrading} />
|
||||
|
||||
<small className="hint">
|
||||
An update is available for installed metrics components.
|
||||
</small>
|
||||
</section>
|
||||
)}
|
||||
<section>
|
||||
<Component.SubTitle title="Prometheus" />
|
||||
<Component.FormSwitch
|
||||
control={
|
||||
<Component.Switcher
|
||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.props.cluster.status.active}
|
||||
checked={!!this.featureStates.prometheus && this.props.cluster.status.active}
|
||||
onChange={v => this.togglePrometheus(v.target.checked)}
|
||||
name="prometheus"
|
||||
/>
|
||||
}
|
||||
label="Install built-in Prometheus metrics stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
Enable timeseries data visualization (Prometheus stack) for your cluster.
|
||||
</small>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Component.SubTitle title="Kube State Metrics" />
|
||||
<Component.FormSwitch
|
||||
control={
|
||||
<Component.Switcher
|
||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.props.cluster.status.active}
|
||||
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.active}
|
||||
onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
|
||||
name="node-exporter"
|
||||
/>
|
||||
}
|
||||
label="Install built-in kube-state-metrics stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
Enable Kubernetes API object metrics for your cluster.
|
||||
Install this only if you don't have existing kube-state-metrics stack installed.
|
||||
</small>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Component.SubTitle title="Node Exporter" />
|
||||
<Component.FormSwitch
|
||||
control={
|
||||
<Component.Switcher
|
||||
disabled={this.featureStates.nodeExporter === undefined || !this.props.cluster.status.active}
|
||||
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.active}
|
||||
onChange={v => this.toggleNodeExporter(v.target.checked)}
|
||||
name="node-exporter"
|
||||
/>
|
||||
}
|
||||
label="Install built-in node-exporter stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
Enable node level metrics for your cluster.
|
||||
Install this only if you don't have existing node-exporter stack installed.
|
||||
</small>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -16,8 +16,8 @@
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"./*.ts",
|
||||
"./*.tsx"
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
@ -34,7 +34,6 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
||||
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
context.menuItems = [
|
||||
{
|
||||
icon: "settings",
|
||||
title: "Settings",
|
||||
onlyVisibleForSource: "local",
|
||||
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
||||
@ -43,7 +42,6 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
||||
|
||||
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
|
||||
context.menuItems.push({
|
||||
icon: "delete",
|
||||
title: "Delete",
|
||||
onlyVisibleForSource: "local",
|
||||
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
|
||||
@ -54,14 +52,20 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
||||
}
|
||||
|
||||
if (this.status.phase == "connected") {
|
||||
context.menuItems.unshift({
|
||||
icon: "link_off",
|
||||
context.menuItems.push({
|
||||
title: "Disconnect",
|
||||
onClick: async () => {
|
||||
ClusterStore.getInstance().deactivate(this.metadata.uid);
|
||||
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
context.menuItems.push({
|
||||
title: "Connect",
|
||||
onClick: async () => {
|
||||
context.navigate(`/cluster/${this.metadata.uid}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);
|
||||
|
||||
@ -62,7 +62,6 @@ export interface CatalogEntityActionContext {
|
||||
}
|
||||
|
||||
export interface CatalogEntityContextMenu {
|
||||
icon: string;
|
||||
title: string;
|
||||
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
|
||||
onClick: () => void | Promise<void>;
|
||||
@ -71,6 +70,10 @@ export interface CatalogEntityContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface CatalogEntitySettingsMenu {
|
||||
group?: string;
|
||||
title: string;
|
||||
@ -90,7 +93,7 @@ export interface CatalogEntitySettingsContext {
|
||||
|
||||
export interface CatalogEntityAddMenuContext {
|
||||
navigate: (url: string) => void;
|
||||
menuItems: CatalogEntityContextMenu[];
|
||||
menuItems: CatalogEntityAddMenu[];
|
||||
}
|
||||
|
||||
export type CatalogEntitySpec = Record<string, any>;
|
||||
|
||||
@ -46,14 +46,20 @@ if (ipcMain) {
|
||||
}
|
||||
});
|
||||
|
||||
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
|
||||
handleRequest(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[]) => {
|
||||
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
const applier = new ResourceApplier(cluster);
|
||||
|
||||
applier.kubectlApplyAll(resources);
|
||||
try {
|
||||
const stdout = await applier.kubectlApplyAll(resources);
|
||||
|
||||
return { stdout };
|
||||
} catch (error: any) {
|
||||
return { stderr: error };
|
||||
}
|
||||
} else {
|
||||
throw `${clusterId} is not a valid cluster id`;
|
||||
}
|
||||
|
||||
@ -99,9 +99,9 @@ export abstract class ClusterFeature {
|
||||
}
|
||||
|
||||
if (app) {
|
||||
await new ResourceApplier(clusterModel).kubectlApplyAll(resources);
|
||||
return await new ResourceApplier(clusterModel).kubectlApplyAll(resources);
|
||||
} else {
|
||||
await requestMain(clusterKubectlApplyAllHandler, cluster.metadata.uid, resources);
|
||||
return await requestMain(clusterKubectlApplyAllHandler, cluster.metadata.uid, resources);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ export * from "../../renderer/components/checkbox";
|
||||
export * from "../../renderer/components/radio";
|
||||
export * from "../../renderer/components/select";
|
||||
export * from "../../renderer/components/slider";
|
||||
export * from "../../renderer/components/switch";
|
||||
export * from "../../renderer/components/input/input";
|
||||
|
||||
// command-overlay
|
||||
|
||||
@ -67,16 +67,19 @@ export class ResourceApplier {
|
||||
const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${tmpDir}"`;
|
||||
|
||||
console.log("shooting manifests with:", cmd);
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
exec(cmd, (error, stdout) => {
|
||||
if (error) {
|
||||
reject(`Error applying manifests:${error}`);
|
||||
}
|
||||
const splittedError = error.toString().split(`.yaml": `);
|
||||
|
||||
if (stderr != "") {
|
||||
reject(stderr);
|
||||
if (splittedError[1]) {
|
||||
reject(`Error applying manifests: ${splittedError[1]}`);
|
||||
} else {
|
||||
reject(`Error applying manifests:${error}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,6 +9,7 @@ export {
|
||||
CatalogEntityKindData,
|
||||
CatalogEntityActionContext,
|
||||
CatalogEntityAddMenuContext,
|
||||
CatalogEntityAddMenu,
|
||||
CatalogEntityContextMenu,
|
||||
CatalogEntityContextMenuContext
|
||||
} from "../../common/catalog";
|
||||
|
||||
@ -5,7 +5,7 @@ import { Icon } from "../icon";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { autobind } from "../../../common/utils";
|
||||
import { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityContextMenu } from "../../api/catalog-entity";
|
||||
import { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityAddMenu } from "../../api/catalog-entity";
|
||||
import { EventEmitter } from "events";
|
||||
import { navigate } from "../../navigation";
|
||||
|
||||
@ -16,7 +16,7 @@ export type CatalogAddButtonProps = {
|
||||
@observer
|
||||
export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
|
||||
@observable protected isOpen = false;
|
||||
protected menuItems = observable.array<CatalogEntityContextMenu>([]);
|
||||
protected menuItems = observable.array<CatalogEntityAddMenu>([]);
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
|
||||
@ -8,7 +8,6 @@ import { navigate } from "../../navigation";
|
||||
import { kebabCase } from "lodash";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { MenuItem, MenuActions } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
|
||||
import { Badge } from "../badge";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
@ -115,16 +114,16 @@ export class Catalog extends React.Component {
|
||||
|
||||
return (
|
||||
<MenuActions onOpen={() => item.onContextMenuOpen(this.contextMenu)}>
|
||||
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(item) }>
|
||||
<Icon material="add" small interactive={true} title="Add to hotbar"/> Add to Hotbar
|
||||
</MenuItem>
|
||||
{
|
||||
menuItems.map((menuItem, index) => (
|
||||
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||
<Icon material={menuItem.icon} small interactive={true} title={menuItem.title} /> {menuItem.title}
|
||||
{menuItem.title}
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(item) }>
|
||||
Add to Hotbar
|
||||
</MenuItem>
|
||||
</MenuActions>
|
||||
);
|
||||
}
|
||||
|
||||
@ -36,9 +36,15 @@ export class EntitySettings extends React.Component<Props> {
|
||||
async componentDidMount() {
|
||||
const { hash } = navigation.location;
|
||||
|
||||
this.ensureActiveTab();
|
||||
if (hash) {
|
||||
const item = this.menuItems.find((item) => item.title === hash.slice(1));
|
||||
|
||||
document.getElementById(hash.slice(1))?.scrollIntoView();
|
||||
if (item) {
|
||||
this.activeTab = item.id;
|
||||
}
|
||||
}
|
||||
|
||||
this.ensureActiveTab();
|
||||
}
|
||||
|
||||
onTabChange = (tabId: string) => {
|
||||
|
||||
@ -159,16 +159,16 @@ export class HotbarIcon extends React.Component<Props> {
|
||||
position={{right: true, bottom: true }} // FIXME: position does not work
|
||||
open={() => onOpen()}
|
||||
close={() => this.toggleMenu()}>
|
||||
<MenuItem key="remove-from-hotbar" onClick={() => this.remove(entity) }>
|
||||
<Icon material="clear" small interactive={true} title="Remove from hotbar"/> Remove from Hotbar
|
||||
</MenuItem>
|
||||
{ this.contextMenu && menuItems.map((menuItem) => {
|
||||
return (
|
||||
<MenuItem key={menuItem.title} onClick={() => this.onMenuItemClick(menuItem) }>
|
||||
<Icon material={menuItem.icon} small interactive={true} title={menuItem.title}/> {menuItem.title}
|
||||
{menuItem.title}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
<MenuItem key="remove-from-hotbar" onClick={() => this.remove(entity) }>
|
||||
Remove from Hotbar
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.Menu {
|
||||
--bgc: #{$contentColor};
|
||||
--bgc: #{$layoutBackground};
|
||||
|
||||
position: absolute;
|
||||
display: flex;
|
||||
@ -8,6 +8,8 @@
|
||||
list-style: none;
|
||||
border: 1px solid $borderColor;
|
||||
z-index: 101;
|
||||
box-shadow: rgba(0,0,0,0.24) 0px 8px 16px 0px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.portal {
|
||||
left: -1000px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user