mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Bring back support for custom cluster icons (#3066)
* revive custom cluster icons Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix borders Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
42bb2a620a
commit
3abb3bdcce
@ -42,6 +42,7 @@ export type KubernetesClusterPrometheusMetrics = {
|
|||||||
export type KubernetesClusterSpec = {
|
export type KubernetesClusterSpec = {
|
||||||
kubeconfigPath: string;
|
kubeconfigPath: string;
|
||||||
kubeconfigContext: string;
|
kubeconfigContext: string;
|
||||||
|
iconData?: string;
|
||||||
metrics?: {
|
metrics?: {
|
||||||
source: string;
|
source: string;
|
||||||
prometheus?: KubernetesClusterPrometheusMetrics;
|
prometheus?: KubernetesClusterPrometheusMetrics;
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import "../common/cluster-ipc";
|
import "../common/cluster-ipc";
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { action, autorun, makeObservable, reaction } from "mobx";
|
import { action, autorun, makeObservable, reaction, toJS } from "mobx";
|
||||||
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||||
import type { Cluster } from "./cluster";
|
import type { Cluster } from "./cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
@ -45,7 +45,14 @@ export class ClusterManager extends Singleton {
|
|||||||
reaction(
|
reaction(
|
||||||
() => this.store.clustersList.map(c => c.getState()),
|
() => this.store.clustersList.map(c => c.getState()),
|
||||||
() => this.updateCatalog(this.store.clustersList),
|
() => this.updateCatalog(this.store.clustersList),
|
||||||
{ fireImmediately: true, }
|
{ fireImmediately: false, }
|
||||||
|
);
|
||||||
|
|
||||||
|
// reacting to every cluster's preferences change and total amount of items
|
||||||
|
reaction(
|
||||||
|
() => this.store.clustersList.map(c => toJS(c.preferences)),
|
||||||
|
() => this.updateCatalog(this.store.clustersList),
|
||||||
|
{ fireImmediately: false, }
|
||||||
);
|
);
|
||||||
|
|
||||||
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
|
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
|
||||||
@ -74,32 +81,40 @@ export class ClusterManager extends Singleton {
|
|||||||
@action
|
@action
|
||||||
protected updateCatalog(clusters: Cluster[]) {
|
protected updateCatalog(clusters: Cluster[]) {
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
|
this.updateEntityFromCluster(cluster);
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
|
|
||||||
|
|
||||||
this.updateEntityStatus(entity, cluster);
|
|
||||||
|
|
||||||
if (cluster.preferences?.clusterName) {
|
|
||||||
entity.metadata.name = cluster.preferences.clusterName;
|
|
||||||
}
|
|
||||||
|
|
||||||
entity.spec.metrics ||= { source: "local" };
|
|
||||||
|
|
||||||
if (entity.spec.metrics.source === "local") {
|
|
||||||
const prometheus: KubernetesClusterPrometheusMetrics = entity.spec?.metrics?.prometheus || {};
|
|
||||||
|
|
||||||
prometheus.type = cluster.preferences.prometheusProvider?.type;
|
|
||||||
prometheus.address = cluster.preferences.prometheus;
|
|
||||||
entity.spec.metrics.prometheus = prometheus;
|
|
||||||
}
|
|
||||||
|
|
||||||
catalogEntityRegistry.items.splice(index, 1, entity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updateEntityFromCluster(cluster: Cluster) {
|
||||||
|
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
|
||||||
|
|
||||||
|
this.updateEntityStatus(entity, cluster);
|
||||||
|
|
||||||
|
if (cluster.preferences?.clusterName) {
|
||||||
|
entity.metadata.name = cluster.preferences.clusterName;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.spec.metrics ||= { source: "local" };
|
||||||
|
|
||||||
|
if (entity.spec.metrics.source === "local") {
|
||||||
|
const prometheus: KubernetesClusterPrometheusMetrics = entity.spec?.metrics?.prometheus || {};
|
||||||
|
|
||||||
|
prometheus.type = cluster.preferences.prometheusProvider?.type;
|
||||||
|
prometheus.address = cluster.preferences.prometheus;
|
||||||
|
entity.spec.metrics.prometheus = prometheus;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.spec.iconData = cluster.preferences.icon;
|
||||||
|
|
||||||
|
catalogEntityRegistry.items.splice(index, 1, entity);
|
||||||
|
}
|
||||||
|
|
||||||
protected updateEntityStatus(entity: KubernetesCluster, cluster: Cluster) {
|
protected updateEntityStatus(entity: KubernetesCluster, cluster: Cluster) {
|
||||||
entity.status.phase = cluster.accessible ? "connected" : "disconnected";
|
entity.status.phase = cluster.accessible ? "connected" : "disconnected";
|
||||||
}
|
}
|
||||||
@ -121,7 +136,7 @@ export class ClusterManager extends Singleton {
|
|||||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||||
cluster.contextName = entity.spec.kubeconfigContext;
|
cluster.contextName = entity.spec.kubeconfigContext;
|
||||||
|
|
||||||
this.updateEntityStatus(entity, cluster);
|
this.updateEntityFromCluster(cluster);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,6 +79,7 @@ export class CatalogEntityDetails extends Component<Props> {
|
|||||||
uid={entity.metadata.uid}
|
uid={entity.metadata.uid}
|
||||||
title={entity.metadata.name}
|
title={entity.metadata.name}
|
||||||
source={entity.metadata.source}
|
source={entity.metadata.source}
|
||||||
|
icon={entity.spec.iconData}
|
||||||
onClick={() => this.openEntity()}
|
onClick={() => this.openEntity()}
|
||||||
size={128} />
|
size={128} />
|
||||||
<div className="IconHint">
|
<div className="IconHint">
|
||||||
|
|||||||
@ -37,12 +37,12 @@ import { catalogCategoryRegistry } from "../../../common/catalog";
|
|||||||
import { CatalogAddButton } from "./catalog-add-button";
|
import { CatalogAddButton } from "./catalog-add-button";
|
||||||
import type { RouteComponentProps } from "react-router";
|
import type { RouteComponentProps } from "react-router";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Avatar } from "../avatar/avatar";
|
|
||||||
import { MainLayout } from "../layout/main-layout";
|
import { MainLayout } from "../layout/main-layout";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { makeCss } from "../../../common/utils/makeCss";
|
import { makeCss } from "../../../common/utils/makeCss";
|
||||||
import { CatalogEntityDetails } from "./catalog-entity-details";
|
import { CatalogEntityDetails } from "./catalog-entity-details";
|
||||||
import type { CatalogViewRouteParam } from "../../../common/routes";
|
import type { CatalogViewRouteParam } from "../../../common/routes";
|
||||||
|
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
@ -192,13 +192,13 @@ export class Catalog extends React.Component<Props> {
|
|||||||
|
|
||||||
renderIcon(item: CatalogEntityItem) {
|
renderIcon(item: CatalogEntityItem) {
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<HotbarIcon
|
||||||
title={item.name}
|
uid={item.getId()}
|
||||||
colorHash={`${item.name}-${item.source}`}
|
title={item.getName()}
|
||||||
width={24}
|
source={item.source}
|
||||||
height={24}
|
icon={item.entity.spec.iconData}
|
||||||
className={css.catalogIcon}
|
onClick={() => this.onDetails(item)}
|
||||||
/>
|
size={24} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import type { KubernetesCluster } from "../../../common/catalog-entities";
|
||||||
import { ClusterStore } from "../../../common/cluster-store";
|
import { ClusterStore } from "../../../common/cluster-store";
|
||||||
import type { EntitySettingViewProps } from "../../../extensions/registries";
|
import type { EntitySettingViewProps } from "../../../extensions/registries";
|
||||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||||
@ -41,6 +42,9 @@ export function GeneralSettings({ entity }: EntitySettingViewProps) {
|
|||||||
<section>
|
<section>
|
||||||
<components.ClusterNameSetting cluster={cluster} />
|
<components.ClusterNameSetting cluster={cluster} />
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<components.ClusterIconSetting cluster={cluster} entity={entity as KubernetesCluster} />
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<components.ClusterKubeconfig cluster={cluster} />
|
<components.ClusterKubeconfig cluster={cluster} />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import type { Cluster } from "../../../../main/cluster";
|
||||||
|
//import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
|
||||||
|
import { boundMethod } from "../../../utils";
|
||||||
|
import { Button } from "../../button";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { SubTitle } from "../../layout/sub-title";
|
||||||
|
import { HotbarIcon } from "../../hotbar/hotbar-icon";
|
||||||
|
import type { KubernetesCluster } from "../../../../common/catalog-entities";
|
||||||
|
import { FilePicker, OverSizeLimitStyle } from "../../file-picker";
|
||||||
|
|
||||||
|
enum GeneralInputStatus {
|
||||||
|
CLEAN = "clean",
|
||||||
|
ERROR = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cluster: Cluster;
|
||||||
|
entity: KubernetesCluster
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class ClusterIconSetting extends React.Component<Props> {
|
||||||
|
@observable status = GeneralInputStatus.CLEAN;
|
||||||
|
@observable errorText?: string;
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
async onIconPick([file]: File[]) {
|
||||||
|
const { cluster } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (file) {
|
||||||
|
const buf = Buffer.from(await file.arrayBuffer());
|
||||||
|
|
||||||
|
cluster.preferences.icon = `data:${file.type};base64,${buf.toString("base64")}`;
|
||||||
|
} else {
|
||||||
|
// this has to be done as a seperate branch (and not always) because `cluster`
|
||||||
|
// is observable and triggers an update loop.
|
||||||
|
cluster.preferences.icon = undefined;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.errorText = e.toString();
|
||||||
|
this.status = GeneralInputStatus.ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClearButton() {
|
||||||
|
if (this.props.cluster.preferences.icon) {
|
||||||
|
return <Button
|
||||||
|
label="Clear"
|
||||||
|
tooltip="Revert back to default icon"
|
||||||
|
onClick={() => this.onIconPick([])}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { entity } = this.props;
|
||||||
|
const label = (
|
||||||
|
<>
|
||||||
|
<HotbarIcon
|
||||||
|
uid={entity.metadata.uid}
|
||||||
|
title={entity.metadata.name}
|
||||||
|
source={entity.metadata.source}
|
||||||
|
icon={entity.spec.iconData}
|
||||||
|
/>
|
||||||
|
<span style={{marginRight: "var(--unit)"}}>Browse for new icon...</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SubTitle title="Cluster Icon" />
|
||||||
|
<div className="file-loader">
|
||||||
|
<FilePicker
|
||||||
|
accept="image/*"
|
||||||
|
label={label}
|
||||||
|
onOverSizeLimit={OverSizeLimitStyle.FILTER}
|
||||||
|
handler={this.onIconPick}
|
||||||
|
/>
|
||||||
|
{this.getClearButton()}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,3 +27,4 @@ export * from "./cluster-name-setting";
|
|||||||
export * from "./cluster-prometheus-setting";
|
export * from "./cluster-prometheus-setting";
|
||||||
export * from "./cluster-proxy-setting";
|
export * from "./cluster-proxy-setting";
|
||||||
export * from "./cluster-show-metrics";
|
export * from "./cluster-show-metrics";
|
||||||
|
export * from "./cluster-icon-settings";
|
||||||
|
|||||||
@ -129,6 +129,7 @@ export class HotbarEntityIcon extends React.Component<Props> {
|
|||||||
uid={entity.metadata.uid}
|
uid={entity.metadata.uid}
|
||||||
title={entity.metadata.name}
|
title={entity.metadata.name}
|
||||||
source={entity.metadata.source}
|
source={entity.metadata.source}
|
||||||
|
icon={entity.spec.iconData}
|
||||||
className={className}
|
className={className}
|
||||||
active={isActive}
|
active={isActive}
|
||||||
onMenuOpen={onOpen}
|
onMenuOpen={onOpen}
|
||||||
|
|||||||
@ -113,7 +113,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: var(--size);
|
padding: 3px;
|
||||||
height: var(--size);
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&:not(.active) {
|
||||||
|
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff50;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export interface HotbarIconProps extends DOMAttributes<HTMLElement> {
|
|||||||
uid: string;
|
uid: string;
|
||||||
title: string;
|
title: string;
|
||||||
source: string;
|
source: string;
|
||||||
|
icon?: string;
|
||||||
onMenuOpen?: () => void;
|
onMenuOpen?: () => void;
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
@ -61,7 +62,7 @@ function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: HotbarIconProps) => {
|
export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: HotbarIconProps) => {
|
||||||
const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
|
const { uid, title, icon, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
|
||||||
const id = `hotbarIcon-${uid}`;
|
const id = `hotbarIcon-${uid}`;
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
@ -69,18 +70,31 @@ export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: Hotba
|
|||||||
setMenuOpen(!menuOpen);
|
setMenuOpen(!menuOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderIcon = () => {
|
||||||
|
if (icon) {
|
||||||
|
return <img
|
||||||
|
{...rest}
|
||||||
|
src={icon}
|
||||||
|
className={active ? "active" : "default"}
|
||||||
|
width={size}
|
||||||
|
height={size} />;
|
||||||
|
} else {
|
||||||
|
return <Avatar
|
||||||
|
{...rest}
|
||||||
|
title={title}
|
||||||
|
colorHash={`${title}-${source}`}
|
||||||
|
className={active ? "active" : "default"}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("HotbarIcon flex inline", className, { disabled })}>
|
<div className={cssNames("HotbarIcon flex inline", className, { disabled })}>
|
||||||
<MaterialTooltip title={`${title || "unknown"} (${source || "unknown"})`} placement="right">
|
<MaterialTooltip title={`${title || "unknown"} (${source || "unknown"})`} placement="right">
|
||||||
<div id={id}>
|
<div id={id}>
|
||||||
<Avatar
|
{renderIcon()}
|
||||||
{...rest}
|
|
||||||
title={title}
|
|
||||||
colorHash={`${title}-${source}`}
|
|
||||||
className={active ? "active" : "default"}
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
/>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</MaterialTooltip>
|
</MaterialTooltip>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user