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

Add auto cluster icon color picking

- Fix not handling grapheme clusters on cluster names

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-03-12 16:34:57 -05:00
parent 523eff4c97
commit 4c975d8a7e
13 changed files with 139 additions and 48 deletions

View File

@ -200,6 +200,7 @@
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"filenamify": "^4.1.0", "filenamify": "^4.1.0",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"js-yaml": "^3.14.0", "js-yaml": "^3.14.0",

View File

@ -356,7 +356,8 @@ describe("pre 2.6.0 config with a cluster icon", () => {
expect(storedClusterData.hasOwnProperty("icon")).toBe(false); expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true); expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true); expect(typeof storedClusterData.preferences.icon).toBe("string");
expect((storedClusterData.preferences.icon as string).startsWith("data:;base64,")).toBe(true);
}); });
}); });
@ -443,6 +444,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
it("migrates to modern format with icon not in file", async () => { it("migrates to modern format with icon not in file", async () => {
const { icon } = clusterStore.clustersList[0].preferences; const { icon } = clusterStore.clustersList[0].preferences;
expect(icon.startsWith("data:;base64,")).toBe(true); expect(typeof icon).toBe("string");
expect((icon as string).startsWith("data:;base64,")).toBe(true);
}); });
}); });

View File

@ -70,11 +70,15 @@ export interface ClusterModel {
kubeConfig?: string; // yaml kubeConfig?: string; // yaml
} }
export interface IconColourPallet {
background: string;
}
export interface ClusterPreferences extends ClusterPrometheusPreferences{ export interface ClusterPreferences extends ClusterPrometheusPreferences{
terminalCWD?: string; terminalCWD?: string;
clusterName?: string; clusterName?: string;
iconOrder?: number; iconOrder?: number;
icon?: string; icon?: string | IconColourPallet;
httpsProxy?: string; httpsProxy?: string;
} }

View File

@ -247,6 +247,19 @@ export class Cluster implements ClusterModel, ClusterState {
}); });
} }
@computed get iconPreference(): Required<ClusterPreferences["icon"]> {
const { icon } = this.preferences;
if (typeof icon === "string") {
return icon;
}
const defaultBackground = getComputedStyle(document.documentElement).getPropertyValue("--halfGray").trim().slice(0, 7);
const { background = defaultBackground } = icon ?? {};
return { background };
}
/** /**
* Kubernetes version * Kubernetes version
*/ */

View File

@ -40,7 +40,7 @@ export default migration({
* migrate cluster icon * migrate cluster icon
*/ */
try { try {
if (cluster.preferences?.icon) { if (typeof cluster.preferences?.icon === "string") {
printLog(`migrating ${cluster.preferences.icon} for ${cluster.preferences.clusterName}`); printLog(`migrating ${cluster.preferences.icon} for ${cluster.preferences.clusterName}`);
const iconPath = cluster.preferences.icon.replace("store://", ""); const iconPath = cluster.preferences.icon.replace("store://", "");
const fileData = fse.readFileSync(path.join(userDataPath, iconPath)); const fileData = fse.readFileSync(path.join(userDataPath, iconPath));

View File

@ -48,4 +48,8 @@
.Input, .Select { .Input, .Select {
margin-top: $padding; margin-top: $padding;
} }
.icon-background-color {
max-width: 100px;
}
} }

View File

@ -7,6 +7,8 @@ import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { SubTitle } from "../../layout/sub-title"; import { SubTitle } from "../../layout/sub-title";
import { ClusterIcon } from "../../cluster-icon"; import { ClusterIcon } from "../../cluster-icon";
import { Input } from "../../input";
import { debounce } from "lodash";
enum GeneralInputStatus { enum GeneralInputStatus {
CLEAN = "clean", CLEAN = "clean",
@ -48,6 +50,23 @@ export class ClusterIconSetting extends React.Component<Props> {
} }
} }
getIconBackgroundColorValue(): string {
const { iconPreference } = this.props.cluster;
if (typeof iconPreference === "string") {
return getComputedStyle(document.documentElement).getPropertyValue("--halfGray").trim().slice(0, 7);
}
return iconPreference.background;
}
onColorChange = debounce(background => {
this.props.cluster.preferences.icon = { background };
}, 100, {
leading: true,
trailing: true,
});
render() { render() {
const label = ( const label = (
<> <>
@ -56,7 +75,7 @@ export class ClusterIconSetting extends React.Component<Props> {
showErrors={false} showErrors={false}
showTooltip={false} showTooltip={false}
/> />
{"Browse for new icon..."} Browse for new icon...
</> </>
); );
@ -73,6 +92,19 @@ export class ClusterIconSetting extends React.Component<Props> {
/> />
{this.getClearButton()} {this.getClearButton()}
</div> </div>
<p>Or change the colour of the generated icon.</p>
<div>
<Input
className="icon-background-color"
type="color"
value={this.getIconBackgroundColorValue()}
title="Choose auto generated icon's background color"
onChange={this.onColorChange}
/>
<small className="hint">
This action clears any previously set icon.
</small>
</div>
</> </>
); );
} }

View File

@ -7,18 +7,22 @@
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
&.interactive {
img {
opacity: .55;
}
}
div.MuiAvatar-colorDefault { div.MuiAvatar-colorDefault {
font-weight:500; font-weight: 900;
text-transform: uppercase; text-transform: uppercase;
-webkit-text-stroke: 0.2px black;
text-shadow: 1px 1px black;
} }
div.active { img {
background-color: var(--primary); width: var(--size);
} height: var(--size);
div.default {
background-color: var(--halfGray);
} }
&.active, &.interactive:hover { &.active, &.interactive:hover {

View File

@ -9,6 +9,7 @@ import { Tooltip } from "../tooltip";
import { subscribeToBroadcast } from "../../../common/ipc"; import { subscribeToBroadcast } from "../../../common/ipc";
import { observable } from "mobx"; import { observable } from "mobx";
import { Avatar } from "@material-ui/core"; import { Avatar } from "@material-ui/core";
import GraphemeSplitter from "grapheme-splitter";
interface Props extends DOMAttributes<HTMLElement> { interface Props extends DOMAttributes<HTMLElement> {
cluster: Cluster; cluster: Cluster;
@ -25,6 +26,22 @@ const defaultProps: Partial<Props> = {
showTooltip: true, showTooltip: true,
}; };
function getNameParts(name: string): string[] {
const byWhitespace = name.split(/\s+/);
if (byWhitespace.length > 1) {
return byWhitespace;
}
const byDashes = name.split(/[-_]+/);
if (byDashes.length > 1) {
return byDashes;
}
return name.split(/@+/);
}
@observer @observer
export class ClusterIcon extends React.Component<Props> { export class ClusterIcon extends React.Component<Props> {
static defaultProps = defaultProps as object; static defaultProps = defaultProps as object;
@ -46,46 +63,59 @@ export class ClusterIcon extends React.Component<Props> {
} }
get iconString() { get iconString() {
let splittedName = this.props.cluster.name.split(" "); const [rawfirst, rawSecond] = getNameParts(this.props.cluster.name);
const splitter = new GraphemeSplitter();
const first = splitter.iterateGraphemes(rawfirst);
const second = rawSecond ? splitter.iterateGraphemes(rawSecond) : first;
let res = "";
if (splittedName.length === 1) { for (const grapheme of first) {
splittedName = splittedName[0].split("-"); res += grapheme;
break;
} }
if (splittedName.length === 1) { for (const grapheme of second) {
splittedName = splittedName[0].split("@"); res += grapheme;
break;
} }
splittedName = splittedName.map((part) => part.replace(/\W/g, "")); return res;
if (splittedName.length === 1) {
return splittedName[0].substring(0, 2);
} else {
return splittedName[0].substring(0, 1) + splittedName[1].substring(0, 1);
} }
renderIcon() {
const { cluster } = this.props;
const { name, iconPreference } = cluster;
if (typeof iconPreference === "string") {
return <img src={iconPreference} alt={name} />;
}
return (
<Avatar variant="rounded" style={{backgroundColor: iconPreference.background}}>
{this.iconString}
</Avatar>
);
} }
render() { render() {
const { const {
cluster, showErrors, showTooltip, errorClass, interactive, isActive, cluster, showErrors, showTooltip, errorClass, interactive, isActive,
children, ...elemProps children, className, ...elemProps
} = this.props; } = this.props;
const { name, preferences, id: clusterId, online } = cluster; const { name, id: clusterId, online } = cluster;
const eventCount = this.eventCount; const eventCount = this.eventCount;
const { icon } = preferences;
const clusterIconId = `cluster-icon-${clusterId}`; const clusterIconId = `cluster-icon-${clusterId}`;
const className = cssNames("ClusterIcon flex inline", this.props.className, { const classNames = cssNames("ClusterIcon flex inline", className, {
interactive: interactive !== undefined ? interactive : !!this.props.onClick, interactive: interactive ?? Boolean(this.props.onClick),
active: isActive, active: isActive,
}); });
return ( return (
<div {...elemProps} className={className} id={showTooltip ? clusterIconId : null}> <div {...elemProps} className={classNames} id={showTooltip ? clusterIconId : null}>
{showTooltip && ( {showTooltip && (
<Tooltip targetId={clusterIconId}>{name}</Tooltip> <Tooltip targetId={clusterIconId}>{name}</Tooltip>
)} )}
{icon && <img src={icon} alt={name}/>} {this.renderIcon()}
{!icon && <Avatar variant="square" className={isActive ? "active" : "default"}>{this.iconString}</Avatar>}
{showErrors && eventCount > 0 && !isActive && online && ( {showErrors && eventCount > 0 && !isActive && online && (
<Badge <Badge
className={cssNames("events-count", errorClass)} className={cssNames("events-count", errorClass)}

View File

@ -187,7 +187,7 @@ export class FilePicker extends React.Component<Props> {
const { accept, label, multiple } = this.props; const { accept, label, multiple } = this.props;
return <div className="FilePicker"> return <div className="FilePicker">
<label className="flex gaps align-center" htmlFor="file-upload">{label} {this.getIconRight()}</label> <label className="flex gaps align-center" htmlFor="file-upload">{label}{" "}{this.getIconRight()}</label>
<input <input
id="file-upload" id="file-upload"
name="FilePicker" name="FilePicker"

View File

@ -23,7 +23,7 @@
} }
} }
label { label.notColor {
--flex-gap: #{$padding / 1.5}; --flex-gap: #{$padding / 1.5};
position: relative; position: relative;

View File

@ -215,10 +215,7 @@ export class Input extends React.Component<InputProps, State> {
@autobind() @autobind()
onChange(evt: React.ChangeEvent<any>) { onChange(evt: React.ChangeEvent<any>) {
if (this.props.onChange) { this.props.onChange?.(evt.currentTarget.value, evt);
this.props.onChange(evt.currentTarget.value, evt);
}
this.validate(); this.validate();
this.autoFitHeight(); this.autoFitHeight();
@ -339,10 +336,14 @@ export class Input extends React.Component<InputProps, State> {
); );
} }
const labelClassNames = cssNames("input-area flex gaps align-center", {
notColor: inputProps.type !== "color",
});
return ( return (
<div id={componentId} className={className}> <div id={componentId} className={className}>
{tooltipError} {tooltipError}
<label className="input-area flex gaps align-center" id=""> <label className={labelClassNames} id="">
{isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft} {isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft}
{multiLine ? <textarea {...inputProps as any} /> : <input {...inputProps as any} />} {multiLine ? <textarea {...inputProps as any} /> : <input {...inputProps as any} />}
{isString(iconRight) ? <Icon material={iconRight}/> : iconRight} {isString(iconRight) ? <Icon material={iconRight}/> : iconRight}

View File

@ -6330,7 +6330,7 @@ graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
grapheme-splitter@^1.0.2: grapheme-splitter@^1.0.2, grapheme-splitter@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==