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:
parent
d812e28639
commit
7f51e3addd
@ -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",
|
||||||
|
|||||||
@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
69
src/migrations/cluster-store/5.0.0-beta.10.ts
Normal file
69
src/migrations/cluster-store/5.0.0-beta.10.ts
Normal 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;
|
||||||
@ -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,
|
||||||
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -36,4 +36,8 @@
|
|||||||
&.small {
|
&.small {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user