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:
parent
523eff4c97
commit
4c975d8a7e
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -48,4 +48,8 @@
|
|||||||
.Input, .Select {
|
.Input, .Select {
|
||||||
margin-top: $padding;
|
margin-top: $padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-background-color {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label.notColor {
|
||||||
--flex-gap: #{$padding / 1.5};
|
--flex-gap: #{$padding / 1.5};
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user