1
0
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:
Jari Kolehmainen 2021-06-16 07:12:10 +03:00 committed by GitHub
parent 42bb2a620a
commit 3abb3bdcce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 202 additions and 45 deletions

View File

@ -42,6 +42,7 @@ export type KubernetesClusterPrometheusMetrics = {
export type KubernetesClusterSpec = {
kubeconfigPath: string;
kubeconfigContext: string;
iconData?: string;
metrics?: {
source: string;
prometheus?: KubernetesClusterPrometheusMetrics;

View File

@ -21,7 +21,7 @@
import "../common/cluster-ipc";
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 type { Cluster } from "./cluster";
import logger from "./logger";
@ -45,7 +45,14 @@ export class ClusterManager extends Singleton {
reaction(
() => this.store.clustersList.map(c => c.getState()),
() => 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) => {
@ -74,32 +81,40 @@ export class ClusterManager extends Singleton {
@action
protected updateCatalog(clusters: Cluster[]) {
for (const cluster of clusters) {
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
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);
}
this.updateEntityFromCluster(cluster);
}
}
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) {
entity.status.phase = cluster.accessible ? "connected" : "disconnected";
}
@ -121,7 +136,7 @@ export class ClusterManager extends Singleton {
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
cluster.contextName = entity.spec.kubeconfigContext;
this.updateEntityStatus(entity, cluster);
this.updateEntityFromCluster(cluster);
}
}
}

View File

@ -79,6 +79,7 @@ export class CatalogEntityDetails extends Component<Props> {
uid={entity.metadata.uid}
title={entity.metadata.name}
source={entity.metadata.source}
icon={entity.spec.iconData}
onClick={() => this.openEntity()}
size={128} />
<div className="IconHint">

View File

@ -37,12 +37,12 @@ import { catalogCategoryRegistry } from "../../../common/catalog";
import { CatalogAddButton } from "./catalog-add-button";
import type { RouteComponentProps } from "react-router";
import { Notifications } from "../notifications";
import { Avatar } from "../avatar/avatar";
import { MainLayout } from "../layout/main-layout";
import { cssNames } from "../../utils";
import { makeCss } from "../../../common/utils/makeCss";
import { CatalogEntityDetails } from "./catalog-entity-details";
import type { CatalogViewRouteParam } from "../../../common/routes";
import { HotbarIcon } from "../hotbar/hotbar-icon";
enum sortBy {
name = "name",
@ -192,13 +192,13 @@ export class Catalog extends React.Component<Props> {
renderIcon(item: CatalogEntityItem) {
return (
<Avatar
title={item.name}
colorHash={`${item.name}-${item.source}`}
width={24}
height={24}
className={css.catalogIcon}
/>
<HotbarIcon
uid={item.getId()}
title={item.getName()}
source={item.source}
icon={item.entity.spec.iconData}
onClick={() => this.onDetails(item)}
size={24} />
);
}

View File

@ -20,6 +20,7 @@
*/
import React from "react";
import type { KubernetesCluster } from "../../../common/catalog-entities";
import { ClusterStore } from "../../../common/cluster-store";
import type { EntitySettingViewProps } from "../../../extensions/registries";
import type { CatalogEntity } from "../../api/catalog-entity";
@ -41,6 +42,9 @@ export function GeneralSettings({ entity }: EntitySettingViewProps) {
<section>
<components.ClusterNameSetting cluster={cluster} />
</section>
<section>
<components.ClusterIconSetting cluster={cluster} entity={entity as KubernetesCluster} />
</section>
<section>
<components.ClusterKubeconfig cluster={cluster} />
</section>

View File

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

View File

@ -27,3 +27,4 @@ export * from "./cluster-name-setting";
export * from "./cluster-prometheus-setting";
export * from "./cluster-proxy-setting";
export * from "./cluster-show-metrics";
export * from "./cluster-icon-settings";

View File

@ -129,6 +129,7 @@ export class HotbarEntityIcon extends React.Component<Props> {
uid={entity.metadata.uid}
title={entity.metadata.name}
source={entity.metadata.source}
icon={entity.spec.iconData}
className={className}
active={isActive}
onMenuOpen={onOpen}

View File

@ -113,7 +113,17 @@
}
img {
width: var(--size);
height: var(--size);
padding: 3px;
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;
}
}
}
}

View File

@ -35,6 +35,7 @@ export interface HotbarIconProps extends DOMAttributes<HTMLElement> {
uid: string;
title: string;
source: string;
icon?: string;
onMenuOpen?: () => void;
className?: IClassName;
active?: boolean;
@ -61,7 +62,7 @@ function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
}
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 [menuOpen, setMenuOpen] = useState(false);
@ -69,18 +70,31 @@ export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: Hotba
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 (
<div className={cssNames("HotbarIcon flex inline", className, { disabled })}>
<MaterialTooltip title={`${title || "unknown"} (${source || "unknown"})`} placement="right">
<div id={id}>
<Avatar
{...rest}
title={title}
colorHash={`${title}-${source}`}
className={active ? "active" : "default"}
width={size}
height={size}
/>
{renderIcon()}
{children}
</div>
</MaterialTooltip>