diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 0ee8c7353f..82582d2bc5 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -42,6 +42,7 @@ export type KubernetesClusterPrometheusMetrics = { export type KubernetesClusterSpec = { kubeconfigPath: string; kubeconfigContext: string; + iconData?: string; metrics?: { source: string; prometheus?: KubernetesClusterPrometheusMetrics; diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index f3859e5fb5..4075c81e36 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -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("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); } } } diff --git a/src/renderer/components/+catalog/catalog-entity-details.tsx b/src/renderer/components/+catalog/catalog-entity-details.tsx index 73a15c9f48..639d7c4843 100644 --- a/src/renderer/components/+catalog/catalog-entity-details.tsx +++ b/src/renderer/components/+catalog/catalog-entity-details.tsx @@ -79,6 +79,7 @@ export class CatalogEntityDetails extends Component { uid={entity.metadata.uid} title={entity.metadata.name} source={entity.metadata.source} + icon={entity.spec.iconData} onClick={() => this.openEntity()} size={128} />
diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 9d9ac6fd2e..7a34d44c04 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -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 { renderIcon(item: CatalogEntityItem) { return ( - + this.onDetails(item)} + size={24} /> ); } diff --git a/src/renderer/components/cluster-settings/cluster-settings.tsx b/src/renderer/components/cluster-settings/cluster-settings.tsx index 32190df9bc..ed01ce4fb7 100644 --- a/src/renderer/components/cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/cluster-settings/cluster-settings.tsx @@ -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) {
+
+ +
diff --git a/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx new file mode 100644 index 0000000000..2800586a11 --- /dev/null +++ b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx @@ -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 { + @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