1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Make CatalogEntity labels clickable (#3075)

- Transition workspace's to be labels

- Fix migrations to support multiple migrations for the same version

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-06-18 08:44:28 -04:00 committed by GitHub
parent d812e28639
commit 7f51e3addd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 268 additions and 120 deletions

View File

@ -3,7 +3,7 @@
"productName": "OpenLens", "productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes", "description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens", "homepage": "https://github.com/lensapp/lens",
"version": "5.0.0-beta.9", "version": "5.0.0-beta.10",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors", "copyright": "© 2021 OpenLens Authors",
"license": "MIT", "license": "MIT",

View File

@ -80,6 +80,11 @@ export interface ClusterModel {
/** Metadata */ /** Metadata */
metadata?: ClusterMetadata; metadata?: ClusterMetadata;
/**
* Labels for the catalog entity
*/
labels?: Record<string, string>;
/** List of accessible namespaces */ /** List of accessible namespaces */
accessibleNamespaces?: string[]; accessibleNamespaces?: string[];
} }

View File

@ -103,6 +103,8 @@ export class ClusterManager extends Singleton {
this.updateEntityStatus(entity, cluster); this.updateEntityStatus(entity, cluster);
entity.metadata.labels = Object.assign({}, cluster.labels, entity.metadata.labels);
if (cluster.preferences?.clusterName) { if (cluster.preferences?.clusterName) {
entity.metadata.name = cluster.preferences.clusterName; entity.metadata.name = cluster.preferences.clusterName;
} }

View File

@ -207,6 +207,11 @@ export class Cluster implements ClusterModel, ClusterState {
*/ */
@observable accessibleNamespaces: string[] = []; @observable accessibleNamespaces: string[] = [];
/**
* Labels for the catalog entity
*/
@observable labels: Record<string, string> = {};
/** /**
* Is cluster available * Is cluster available
* *
@ -304,6 +309,10 @@ export class Cluster implements ClusterModel, ClusterState {
if (model.accessibleNamespaces) { if (model.accessibleNamespaces) {
this.accessibleNamespaces = model.accessibleNamespaces; this.accessibleNamespaces = model.accessibleNamespaces;
} }
if (model.labels) {
this.labels = model.labels;
}
} }
/** /**
@ -583,6 +592,7 @@ export class Cluster implements ClusterModel, ClusterState {
preferences: this.preferences, preferences: this.preferences,
metadata: this.metadata, metadata: this.metadata,
accessibleNamespaces: this.accessibleNamespaces, accessibleNamespaces: this.accessibleNamespaces,
labels: this.labels,
}; };
return toJS(model); return toJS(model);

View File

@ -19,12 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
/* Early store format had the kubeconfig directly under context name, this moves import type { MigrationDeclaration } from "../helpers";
it under the kubeConfig key */
import { migration } from "../migration-wrapper"; /**
* Early store format had the kubeconfig directly under context name, this moves
* it under the kubeConfig key
*/
export default migration({ export default {
version: "2.0.0-beta.2", version: "2.0.0-beta.2",
run(store) { run(store) {
for (const value of store) { for (const value of store) {
@ -35,4 +37,4 @@ export default migration({
store.set(contextName, { kubeConfig: value[1] }); store.set(contextName, { kubeConfig: value[1] });
} }
} }
}); } as MigrationDeclaration;

View File

@ -19,10 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
// Cleans up a store that had the state related data stored import type { MigrationDeclaration } from "../helpers";
import { migration } from "../migration-wrapper";
export default migration({ // Cleans up a store that had the state related data stored
export default {
version: "2.4.1", version: "2.4.1",
run(store) { run(store) {
for (const value of store) { for (const value of store) {
@ -34,4 +35,4 @@ export default migration({
store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {} }); store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {} });
} }
} }
}); } as MigrationDeclaration;

View File

@ -20,9 +20,9 @@
*/ */
// Move cluster icon from root to preferences // Move cluster icon from root to preferences
import { migration } from "../migration-wrapper"; import type { MigrationDeclaration } from "../helpers";
export default migration({ export default {
version: "2.6.0-beta.2", version: "2.6.0-beta.2",
run(store) { run(store) {
for (const value of store) { for (const value of store) {
@ -40,4 +40,4 @@ export default migration({
store.set(clusterKey, { contextName: clusterKey, kubeConfig: value[1].kubeConfig, preferences: value[1].preferences }); store.set(clusterKey, { contextName: clusterKey, kubeConfig: value[1].kubeConfig, preferences: value[1].preferences });
} }
} }
}); } as MigrationDeclaration;

View File

@ -19,12 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { migration } from "../migration-wrapper";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { MigrationDeclaration, migrationLog } from "../helpers";
export default migration({ export default {
version: "2.6.0-beta.3", version: "2.6.0-beta.3",
run(store, log) { run(store) {
for (const value of store) { for (const value of store) {
const clusterKey = value[0]; const clusterKey = value[0];
@ -50,7 +50,7 @@ export default migration({
if (authConfig.expiry) { if (authConfig.expiry) {
authConfig.expiry = `${authConfig.expiry}`; authConfig.expiry = `${authConfig.expiry}`;
} }
log(authConfig); migrationLog(authConfig);
user["auth-provider"].config = authConfig; user["auth-provider"].config = authConfig;
kubeConfig.users = [{ kubeConfig.users = [{
name: userObj.name, name: userObj.name,
@ -62,4 +62,4 @@ export default migration({
} }
} }
} }
}); } as MigrationDeclaration;

View File

@ -20,9 +20,9 @@
*/ */
// Add existing clusters to "default" workspace // Add existing clusters to "default" workspace
import { migration } from "../migration-wrapper"; import type { MigrationDeclaration } from "../helpers";
export default migration({ export default {
version: "2.7.0-beta.0", version: "2.7.0-beta.0",
run(store) { run(store) {
for (const value of store) { for (const value of store) {
@ -35,4 +35,4 @@ export default migration({
store.set(clusterKey, cluster); store.set(clusterKey, cluster);
} }
} }
}); } as MigrationDeclaration;

View File

@ -20,10 +20,10 @@
*/ */
// Add id for clusters and store them to array // Add id for clusters and store them to array
import { migration } from "../migration-wrapper";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import type { MigrationDeclaration } from "../helpers";
export default migration({ export default {
version: "2.7.0-beta.1", version: "2.7.0-beta.1",
run(store) { run(store) {
const clusters: any[] = []; const clusters: any[] = [];
@ -48,4 +48,4 @@ export default migration({
store.set("clusters", clusters); store.set("clusters", clusters);
} }
} }
}); } as MigrationDeclaration;

View File

@ -24,25 +24,25 @@
import path from "path"; import path from "path";
import { app } from "electron"; import { app } from "electron";
import { migration } from "../migration-wrapper";
import fse from "fs-extra"; import fse from "fs-extra";
import { ClusterModel, ClusterStore } from "../../common/cluster-store"; import { ClusterModel, ClusterStore } from "../../common/cluster-store";
import { loadConfigFromFileSync } from "../../common/kube-helpers"; import { loadConfigFromFileSync } from "../../common/kube-helpers";
import { MigrationDeclaration, migrationLog } from "../helpers";
interface Pre360ClusterModel extends ClusterModel { interface Pre360ClusterModel extends ClusterModel {
kubeConfig: string; kubeConfig: string;
} }
export default migration({ export default {
version: "3.6.0-beta.1", version: "3.6.0-beta.1",
run(store, printLog) { run(store) {
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const storedClusters: Pre360ClusterModel[] = store.get("clusters") ?? []; const storedClusters: Pre360ClusterModel[] = store.get("clusters") ?? [];
const migratedClusters: ClusterModel[] = []; const migratedClusters: ClusterModel[] = [];
fse.ensureDirSync(ClusterStore.storedKubeConfigFolder); fse.ensureDirSync(ClusterStore.storedKubeConfigFolder);
printLog("Number of clusters to migrate: ", storedClusters.length); migrationLog("Number of clusters to migrate: ", storedClusters.length);
for (const clusterModel of storedClusters) { for (const clusterModel of storedClusters) {
/** /**
@ -59,7 +59,7 @@ export default migration({
delete clusterModel.kubeConfig; delete clusterModel.kubeConfig;
} catch (error) { } catch (error) {
printLog(`Failed to migrate Kubeconfig for cluster "${clusterModel.id}", removing clusterModel...`, error); migrationLog(`Failed to migrate Kubeconfig for cluster "${clusterModel.id}", removing clusterModel...`, error);
continue; continue;
} }
@ -69,7 +69,7 @@ export default migration({
*/ */
try { try {
if (clusterModel.preferences?.icon) { if (clusterModel.preferences?.icon) {
printLog(`migrating ${clusterModel.preferences.icon} for ${clusterModel.preferences.clusterName}`); migrationLog(`migrating ${clusterModel.preferences.icon} for ${clusterModel.preferences.clusterName}`);
const iconPath = clusterModel.preferences.icon.replace("store://", ""); const iconPath = clusterModel.preferences.icon.replace("store://", "");
const fileData = fse.readFileSync(path.join(userDataPath, iconPath)); const fileData = fse.readFileSync(path.join(userDataPath, iconPath));
@ -78,7 +78,7 @@ export default migration({
delete clusterModel.preferences?.icon; delete clusterModel.preferences?.icon;
} }
} catch (error) { } catch (error) {
printLog(`Failed to migrate cluster icon for cluster "${clusterModel.id}"`, error); migrationLog(`Failed to migrate cluster icon for cluster "${clusterModel.id}"`, error);
delete clusterModel.preferences.icon; delete clusterModel.preferences.icon;
} }
@ -87,4 +87,4 @@ export default migration({
store.set("clusters", migratedClusters); store.set("clusters", migratedClusters);
} }
}); } as MigrationDeclaration;

View File

@ -0,0 +1,69 @@
/**
* 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 path from "path";
import { app } from "electron";
import fse from "fs-extra";
import type { ClusterModel } from "../../common/cluster-store";
import { MigrationDeclaration, migrationLog } from "../helpers";
interface Pre500WorkspaceStoreModel {
workspaces: {
id: string;
name: string;
}[];
}
export default {
version: "5.0.0-beta.10",
run(store) {
const userDataPath = app.getPath("userData");
try {
const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json"));
const workspaces = new Map<string, string>(); // mapping from WorkspaceId to name
for (const { id, name } of workspaceData.workspaces) {
workspaces.set(id, name);
}
migrationLog("workspaces", JSON.stringify([...workspaces.entries()]));
const clusters: ClusterModel[] = store.get("clusters");
for (const cluster of clusters) {
if (cluster.workspace && workspaces.has(cluster.workspace)) {
cluster.labels ??= {};
cluster.labels.workspace = workspaces.get(cluster.workspace);
}
}
store.set("clusters", clusters);
} catch (error) {
migrationLog("error", error.path);
if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {
// ignore lens-workspace-store.json being missing
throw error;
}
}
},
} as MigrationDeclaration;

View File

@ -21,6 +21,8 @@
// Cluster store migrations // Cluster store migrations
import { joinMigrations } from "../helpers";
import version200Beta2 from "./2.0.0-beta.2"; import version200Beta2 from "./2.0.0-beta.2";
import version241 from "./2.4.1"; import version241 from "./2.4.1";
import version260Beta2 from "./2.6.0-beta.2"; import version260Beta2 from "./2.6.0-beta.2";
@ -28,15 +30,17 @@ import version260Beta3 from "./2.6.0-beta.3";
import version270Beta0 from "./2.7.0-beta.0"; import version270Beta0 from "./2.7.0-beta.0";
import version270Beta1 from "./2.7.0-beta.1"; import version270Beta1 from "./2.7.0-beta.1";
import version360Beta1 from "./3.6.0-beta.1"; import version360Beta1 from "./3.6.0-beta.1";
import version500Beta10 from "./5.0.0-beta.10";
import snap from "./snap"; import snap from "./snap";
export default { export default joinMigrations(
...version200Beta2, version200Beta2,
...version241, version241,
...version260Beta2, version260Beta2,
...version260Beta3, version260Beta3,
...version270Beta0, version270Beta0,
...version270Beta1, version270Beta1,
...version360Beta1, version360Beta1,
...snap version500Beta10,
}; snap,
);

View File

@ -21,22 +21,22 @@
// Fix embedded kubeconfig paths under snap config // Fix embedded kubeconfig paths under snap config
import { migration } from "../migration-wrapper";
import type { ClusterModel } from "../../common/cluster-store"; import type { ClusterModel } from "../../common/cluster-store";
import { getAppVersion } from "../../common/utils/app-version"; import { getAppVersion } from "../../common/utils/app-version";
import fs from "fs"; import fs from "fs";
import { MigrationDeclaration, migrationLog } from "../helpers";
export default migration({ export default {
version: getAppVersion(), // Run always after upgrade version: getAppVersion(), // Run always after upgrade
run(store, printLog) { run(store) {
if (!process.env["SNAP"]) return; if (!process.env["SNAP"]) return;
printLog("Migrating embedded kubeconfig paths"); migrationLog("Migrating embedded kubeconfig paths");
const storedClusters: ClusterModel[] = store.get("clusters") || []; const storedClusters: ClusterModel[] = store.get("clusters") || [];
if (!storedClusters.length) return; if (!storedClusters.length) return;
printLog("Number of clusters to migrate: ", storedClusters.length); migrationLog("Number of clusters to migrate: ", storedClusters.length);
const migratedClusters = storedClusters const migratedClusters = storedClusters
.map(cluster => { .map(cluster => {
/** /**
@ -54,4 +54,4 @@ export default migration({
store.set("clusters", migratedClusters); store.set("clusters", migratedClusters);
} }
}); } as MigrationDeclaration;

View File

@ -19,24 +19,37 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type Config from "conf"; import type Conf from "conf";
import type { Migrations } from "conf/dist/source/types";
import { ExtendedMap, iter } from "../common/utils";
import { isTestEnv } from "../common/vars"; import { isTestEnv } from "../common/vars";
export interface MigrationOpts { export function migrationLog(...args: any[]) {
version: string; if (!isTestEnv) {
run(storeConfig: Config<any>, log: (...args: any[]) => void): void;
}
function infoLog(...args: any[]) {
if (isTestEnv) return;
console.log(...args); console.log(...args);
}
} }
export function migration<S = any>({ version, run }: MigrationOpts) { export interface MigrationDeclaration {
return { version: string,
[version]: (storeConfig: Config<S>) => { run(store: Conf<any>): void;
infoLog(`STORE MIGRATION (${storeConfig.path}): ${version}`,); }
run(storeConfig, infoLog);
} export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations<any> {
}; const migrations = new ExtendedMap<string, ((store: Conf<any>) => void)[]>();
for (const decl of declarations) {
migrations.getOrInsert(decl.version, () => []).push(decl.run);
}
return Object.fromEntries(
iter.map(
migrations,
([v, fns]) => [v, (store: Conf<any>) => {
for (const fn of fns) {
fn(store);
}
}]
)
);
} }

View File

@ -22,10 +22,10 @@
// Cleans up a store that had the state related data stored // Cleans up a store that had the state related data stored
import type { Hotbar } from "../../common/hotbar-store"; import type { Hotbar } from "../../common/hotbar-store";
import { ClusterStore } from "../../common/cluster-store"; import { ClusterStore } from "../../common/cluster-store";
import { migration } from "../migration-wrapper"; import * as uuid from "uuid";
import { v4 as uuid } from "uuid"; import type { MigrationDeclaration } from "../helpers";
export default migration({ export default {
version: "5.0.0-alpha.0", version: "5.0.0-alpha.0",
run(store) { run(store) {
const hotbars: Hotbar[] = []; const hotbars: Hotbar[] = [];
@ -38,7 +38,7 @@ export default migration({
let hotbar = hotbars.find((h) => h.name === name); let hotbar = hotbars.find((h) => h.name === name);
if (!hotbar) { if (!hotbar) {
hotbar = { id: uuid(), name, items: [] }; hotbar = { id: uuid.v4(), name, items: [] };
hotbars.push(hotbar); hotbars.push(hotbar);
} }
@ -50,4 +50,4 @@ export default migration({
store.set("hotbars", hotbars); store.set("hotbars", hotbars);
} }
}); } as MigrationDeclaration;

View File

@ -21,10 +21,10 @@
// Cleans up a store that had the state related data stored // Cleans up a store that had the state related data stored
import type { Hotbar } from "../../common/hotbar-store"; import type { Hotbar } from "../../common/hotbar-store";
import { migration } from "../migration-wrapper";
import * as uuid from "uuid"; import * as uuid from "uuid";
import type { MigrationDeclaration } from "../helpers";
export default migration({ export default {
version: "5.0.0-alpha.2", version: "5.0.0-alpha.2",
run(store) { run(store) {
const hotbars = (store.get("hotbars") || []) as Hotbar[]; const hotbars = (store.get("hotbars") || []) as Hotbar[];
@ -34,4 +34,4 @@ export default migration({
...hotbar ...hotbar
}))); })));
} }
}); } as MigrationDeclaration;

View File

@ -20,10 +20,10 @@
*/ */
import type { Hotbar } from "../../common/hotbar-store"; import type { Hotbar } from "../../common/hotbar-store";
import { migration } from "../migration-wrapper";
import { catalogEntityRegistry } from "../../renderer/api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../renderer/api/catalog-entity-registry";
import type { MigrationDeclaration } from "../helpers";
export default migration({ export default {
version: "5.0.0-beta.5", version: "5.0.0-beta.5",
run(store) { run(store) {
const hotbars: Hotbar[] = store.get("hotbars"); const hotbars: Hotbar[] = store.get("hotbars");
@ -48,4 +48,4 @@ export default migration({
store.set("hotbars", hotbars); store.set("hotbars", hotbars);
} }
}); } as MigrationDeclaration;

View File

@ -21,12 +21,14 @@
// Hotbar store migrations // Hotbar store migrations
import { joinMigrations } from "../helpers";
import version500alpha0 from "./5.0.0-alpha.0"; import version500alpha0 from "./5.0.0-alpha.0";
import version500alpha2 from "./5.0.0-alpha.2"; import version500alpha2 from "./5.0.0-alpha.2";
import version500beta5 from "./5.0.0-beta.5"; import version500beta5 from "./5.0.0-beta.5";
export default { export default joinMigrations(
...version500alpha0, version500alpha0,
...version500alpha2, version500alpha2,
...version500beta5, version500beta5,
}; );

View File

@ -20,11 +20,11 @@
*/ */
// Add / reset "lastSeenAppVersion" // Add / reset "lastSeenAppVersion"
import { migration } from "../migration-wrapper"; import type { MigrationDeclaration } from "../helpers";
export default migration({ export default {
version: "2.1.0-beta.4", version: "2.1.0-beta.4",
run(store) { run(store) {
store.set("lastSeenAppVersion", "0.0.0"); store.set("lastSeenAppVersion", "0.0.0");
} }
}); } as MigrationDeclaration;

View File

@ -20,9 +20,9 @@
*/ */
// Switch representation of hiddenTableColumns in store // Switch representation of hiddenTableColumns in store
import { migration } from "../migration-wrapper"; import type { MigrationDeclaration } from "../helpers";
export default migration({ export default {
version: "5.0.0-alpha.3", version: "5.0.0-alpha.3",
run(store) { run(store) {
const preferences = store.get("preferences"); const preferences = store.get("preferences");
@ -36,4 +36,4 @@ export default migration({
store.set("preferences", preferences); store.set("preferences", preferences);
} }
}); } as MigrationDeclaration;

View File

@ -21,6 +21,8 @@
// User store migrations // User store migrations
import { joinMigrations } from "../helpers";
import version210Beta4 from "./2.1.0-beta.4"; import version210Beta4 from "./2.1.0-beta.4";
import version500Alpha3 from "./5.0.0-alpha.3"; import version500Alpha3 from "./5.0.0-alpha.3";
import { fileNameMigration } from "./file-name-migration"; import { fileNameMigration } from "./file-name-migration";
@ -29,7 +31,7 @@ export {
fileNameMigration fileNameMigration
}; };
export default { export default joinMigrations(
...version210Beta4, version210Beta4,
...version500Alpha3, version500Alpha3,
}; );

View File

@ -22,9 +22,9 @@
import "./catalog-entity-details.scss"; import "./catalog-entity-details.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Drawer, DrawerItem, DrawerItemLabels } from "../drawer"; import { Drawer, DrawerItem } from "../drawer";
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity"; import { catalogEntityRunContext } from "../../api/catalog-entity";
import type { CatalogCategory } from "../../../common/catalog"; import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu"; import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
@ -86,10 +86,9 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
<DrawerItem name="Status"> <DrawerItem name="Status">
{item.phase} {item.phase}
</DrawerItem> </DrawerItem>
<DrawerItemLabels <DrawerItem name="Labels">
name="Labels" {...item.getLabelBadges(this.props.hideDetails)}
labels={item.labels} </DrawerItem>
/>
</div> </div>
</div> </div>
)} )}

View File

@ -19,12 +19,23 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./catalog.module.css";
import React from "react";
import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx"; import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity"; import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
import { ItemObject, ItemStore } from "../../item.store"; import { ItemObject, ItemStore } from "../../item.store";
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog"; import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
import { autoBind } from "../../../common/utils"; import { autoBind } from "../../../common/utils";
import { Badge } from "../badge";
import { navigation } from "../../navigation";
import { searchUrlParam } from "../input";
import { makeCss } from "../../../common/utils/makeCss";
import { KubeObject } from "../../api/kube-object";
const css = makeCss(styles);
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject { export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
constructor(public entity: T) {} constructor(public entity: T) {}
@ -61,15 +72,24 @@ export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
} }
get labels() { get labels() {
const labels: string[] = []; return KubeObject.stringifyLabels(this.entity.metadata.labels);
}
Object.keys(this.entity.metadata.labels).forEach((key) => { getLabelBadges(onClick?: React.MouseEventHandler<any>) {
const value = this.entity.metadata.labels[key]; return this.labels
.map(label => (
labels.push(`${key}=${value}`); <Badge
}); className={css.badge}
key={label}
return labels; label={label}
title={label}
onClick={(event) => {
navigation.searchParams.set(searchUrlParam.name, label);
onClick?.(event);
event.stopPropagation();
}}
/>
));
} }
get source() { get source() {
@ -82,7 +102,7 @@ export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
this.id, this.id,
this.phase, this.phase,
`source=${this.source}`, `source=${this.source}`,
...this.labels.map((value, key) => `${key}=${value}`) ...this.labels,
]; ];
} }

View File

@ -63,6 +63,10 @@
max-width: unset; max-width: unset;
} }
.badge:hover {
color: var(--textColorAccent);
}
.badge:not(:first-child) { .badge:not(:first-child) {
margin-left: 0.5em; margin-left: 0.5em;
} }

View File

@ -29,7 +29,6 @@ import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { MenuItem, MenuActions } from "../menu"; import { MenuItem, MenuActions } from "../menu";
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
import { Badge } from "../badge";
import { HotbarStore } from "../../../common/hotbar-store"; import { HotbarStore } from "../../../common/hotbar-store";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
@ -208,7 +207,7 @@ export class Catalog extends React.Component<Props> {
this.renderIcon(item), this.renderIcon(item),
item.name, item.name,
item.source, item.source,
item.labels.map((label) => <Badge className={css.badge} key={label} label={label} title={label} />), item.getLabelBadges(),
{ title: item.phase, className: cssNames(css[item.phase]) } { title: item.phase, className: cssNames(css[item.phase]) }
]} ]}
onDetails={this.onDetails} onDetails={this.onDetails}
@ -252,7 +251,7 @@ export class Catalog extends React.Component<Props> {
item.name, item.name,
item.kind, item.kind,
item.source, item.source,
item.labels.map((label) => <Badge className={css.badge} key={label} label={label} title={label} />), item.getLabelBadges(),
{ title: item.phase, className: cssNames(css[item.phase]) } { title: item.phase, className: cssNames(css[item.phase]) }
]} ]}
detailsItem={this.catalogEntityStore.selectedItem} detailsItem={this.catalogEntityStore.selectedItem}
@ -274,15 +273,13 @@ export class Catalog extends React.Component<Props> {
</div> </div>
{ {
this.catalogEntityStore.selectedItem this.catalogEntityStore.selectedItem
? ( ? <CatalogEntityDetails
<CatalogEntityDetails
item={this.catalogEntityStore.selectedItem} item={this.catalogEntityStore.selectedItem}
hideDetails={() => this.catalogEntityStore.selectedItemId = null} hideDetails={() => this.catalogEntityStore.selectedItemId = null}
/> />
) : <CatalogAddButton
: ( category={this.catalogEntityStore.activeCategory}
<CatalogAddButton category = {this.catalogEntityStore.activeCategory} /> />
)
} }
</MainLayout> </MainLayout>
); );

View File

@ -36,4 +36,8 @@
&.small { &.small {
font-size: $font-size-small; font-size: $font-size-small;
} }
&.clickable {
cursor: pointer;
}
} }

View File

@ -35,9 +35,10 @@ export interface BadgeProps extends React.HTMLAttributes<any>, TooltipDecoratorP
export class Badge extends React.Component<BadgeProps> { export class Badge extends React.Component<BadgeProps> {
render() { render() {
const { className, label, small, flat, children, ...elemProps } = this.props; const { className, label, small, flat, children, ...elemProps } = this.props;
const clickable = Boolean(this.props.onClick);
return <> return <>
<span className={cssNames("Badge", { small, flat }, className)} {...elemProps}> <span className={cssNames("Badge", { small, flat, clickable }, className)} {...elemProps}>
{label} {label}
{children} {children}
</span> </span>

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { computed, observable, reaction, makeObservable } from "mobx"; import { computed, observable, reaction, makeObservable, action } from "mobx";
import { autoBind } from "../../utils"; import { autoBind } from "../../utils";
import { searchUrlParam } from "../input/search-input-url"; import { searchUrlParam } from "../input/search-input-url";
@ -68,6 +68,7 @@ export class PageFiltersStore {
return () => disposers.forEach(dispose => dispose()); return () => disposers.forEach(dispose => dispose());
} }
@action
addFilter(filter: Filter, begin = false) { addFilter(filter: Filter, begin = false) {
if (begin) this.filters.unshift(filter); if (begin) this.filters.unshift(filter);
else { else {
@ -75,12 +76,17 @@ export class PageFiltersStore {
} }
} }
@action
removeFilter(filter: Filter) { removeFilter(filter: Filter) {
if (!this.filters.remove(filter)) { if (!this.filters.remove(filter)) {
const filterCopy = this.filters.find(f => f.type === filter.type && f.value === filter.value); const filterCopy = this.filters.find(f => f.type === filter.type && f.value === filter.value);
if (filterCopy) this.filters.remove(filterCopy); if (filterCopy) this.filters.remove(filterCopy);
} }
if (filter.type === FilterType.SEARCH) {
searchUrlParam.clear();
}
} }
getByType(type: FilterType, value?: any): Filter { getByType(type: FilterType, value?: any): Filter {
@ -99,19 +105,26 @@ export class PageFiltersStore {
return !this.isDisabled.get(type); return !this.isDisabled.get(type);
} }
@action
disable(type: FilterType | FilterType[]) { disable(type: FilterType | FilterType[]) {
[type].flat().forEach(type => this.isDisabled.set(type, true)); [type].flat().forEach(type => this.isDisabled.set(type, true));
return () => this.enable(type); return () => this.enable(type);
} }
@action
enable(type: FilterType | FilterType[]) { enable(type: FilterType | FilterType[]) {
[type].flat().forEach(type => this.isDisabled.delete(type)); [type].flat().forEach(type => this.isDisabled.delete(type));
return () => this.disable(type); return () => this.disable(type);
} }
@action
reset() { reset() {
if (this.isEnabled(FilterType.SEARCH)) {
searchUrlParam.clear();
}
this.filters.length = 0; this.filters.length = 0;
this.isDisabled.clear(); this.isDisabled.clear();
} }

View File

@ -12,7 +12,7 @@
"textColorPrimary": "#555555", "textColorPrimary": "#555555",
"textColorSecondary": "#51575d", "textColorSecondary": "#51575d",
"textColorTertiary": "#555555", "textColorTertiary": "#555555",
"textColorAccent": "#333333", "textColorAccent": "#222222",
"textColorDimmed": "#5557598c", "textColorDimmed": "#5557598c",
"borderColor": "#c9cfd3", "borderColor": "#c9cfd3",
"borderFaintColor": "#dfdfdf", "borderFaintColor": "#dfdfdf",