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",
"filenamify": "^4.1.0",
"fs-extra": "^9.0.1",
"grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.6",
"http-proxy": "^1.18.1",
"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.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 () => {
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
}
export interface IconColourPallet {
background: string;
}
export interface ClusterPreferences extends ClusterPrometheusPreferences{
terminalCWD?: string;
clusterName?: string;
iconOrder?: number;
icon?: string;
icon?: string | IconColourPallet;
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
*/

View File

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

View File

@ -48,4 +48,8 @@
.Input, .Select {
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 { SubTitle } from "../../layout/sub-title";
import { ClusterIcon } from "../../cluster-icon";
import { Input } from "../../input";
import { debounce } from "lodash";
enum GeneralInputStatus {
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() {
const label = (
<>
@ -56,7 +75,7 @@ export class ClusterIconSetting extends React.Component<Props> {
showErrors={false}
showTooltip={false}
/>
{"Browse for new icon..."}
Browse for new icon...
</>
);
@ -73,6 +92,19 @@ export class ClusterIconSetting extends React.Component<Props> {
/>
{this.getClearButton()}
</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;
cursor: pointer;
&.interactive {
img {
opacity: .55;
}
}
div.MuiAvatar-colorDefault {
font-weight:500;
font-weight: 900;
text-transform: uppercase;
-webkit-text-stroke: 0.2px black;
text-shadow: 1px 1px black;
}
div.active {
background-color: var(--primary);
}
div.default {
background-color: var(--halfGray);
img {
width: var(--size);
height: var(--size);
}
&.active, &.interactive:hover {

View File

@ -9,6 +9,7 @@ import { Tooltip } from "../tooltip";
import { subscribeToBroadcast } from "../../../common/ipc";
import { observable } from "mobx";
import { Avatar } from "@material-ui/core";
import GraphemeSplitter from "grapheme-splitter";
interface Props extends DOMAttributes<HTMLElement> {
cluster: Cluster;
@ -25,6 +26,22 @@ const defaultProps: Partial<Props> = {
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
export class ClusterIcon extends React.Component<Props> {
static defaultProps = defaultProps as object;
@ -46,46 +63,59 @@ export class ClusterIcon extends React.Component<Props> {
}
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) {
splittedName = splittedName[0].split("-");
for (const grapheme of first) {
res += grapheme;
break;
}
if (splittedName.length === 1) {
splittedName = splittedName[0].split("@");
for (const grapheme of second) {
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() {
const {
cluster, showErrors, showTooltip, errorClass, interactive, isActive,
children, ...elemProps
children, className, ...elemProps
} = this.props;
const { name, preferences, id: clusterId, online } = cluster;
const { name, id: clusterId, online } = cluster;
const eventCount = this.eventCount;
const { icon } = preferences;
const clusterIconId = `cluster-icon-${clusterId}`;
const className = cssNames("ClusterIcon flex inline", this.props.className, {
interactive: interactive !== undefined ? interactive : !!this.props.onClick,
const classNames = cssNames("ClusterIcon flex inline", className, {
interactive: interactive ?? Boolean(this.props.onClick),
active: isActive,
});
return (
<div {...elemProps} className={className} id={showTooltip ? clusterIconId : null}>
<div {...elemProps} className={classNames} id={showTooltip ? clusterIconId : null}>
{showTooltip && (
<Tooltip targetId={clusterIconId}>{name}</Tooltip>
)}
{icon && <img src={icon} alt={name}/>}
{!icon && <Avatar variant="square" className={isActive ? "active" : "default"}>{this.iconString}</Avatar>}
{this.renderIcon()}
{showErrors && eventCount > 0 && !isActive && online && (
<Badge
className={cssNames("events-count", errorClass)}

View File

@ -50,15 +50,15 @@ export interface BaseProps {
// the larger number is upper limit, the lower is lower limit
// the lower limit is capped at 0 and the upper limit is capped at Infinity
limit?: [number, number];
// default is "Reject"
onOverLimit?: OverLimitStyle;
// individual files are checked before the total size.
maxSize?: number;
// default is "Reject"
onOverSizeLimit?: OverSizeLimitStyle;
maxTotalSize?: number;
// default is "Reject"
onOverTotalSizeLimit?: OverTotalSizeLimitStyle;
@ -132,7 +132,7 @@ export class FilePicker extends React.Component<Props> {
files = _.orderBy(files, ["size"]);
case OverTotalSizeLimitStyle.FILTER_LAST:
let newTotalSize = totalSize;
for (;files.length > 0;) {
newTotalSize -= files.pop().size;
@ -154,12 +154,12 @@ export class FilePicker extends React.Component<Props> {
const numberLimitedFiles = this.handleFileCount(files);
const sizeLimitedFiles = this.handleIndiviualFileSizes(numberLimitedFiles);
const totalSizeLimitedFiles = this.handleTotalFileSizes(sizeLimitedFiles);
if ("uploadDir" in this.props) {
const { uploadDir } = this.props;
this.status = FileInputStatus.PROCESSING;
const paths: string[] = [];
const promises = totalSizeLimitedFiles.map(async file => {
const destinationPath = path.join(uploadDir, file.name);
@ -187,10 +187,10 @@ export class FilePicker extends React.Component<Props> {
const { accept, label, multiple } = this.props;
return <div className="FilePicker">
<label className="flex gaps align-center" htmlFor="file-upload">{label} {this.getIconRight()}</label>
<input
id="file-upload"
name="FilePicker"
<label className="flex gaps align-center" htmlFor="file-upload">{label}{" "}{this.getIconRight()}</label>
<input
id="file-upload"
name="FilePicker"
type="file"
accept={accept}
multiple={multiple}

View File

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

View File

@ -215,10 +215,7 @@ export class Input extends React.Component<InputProps, State> {
@autobind()
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.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 (
<div id={componentId} className={className}>
{tooltipError}
<label className="input-area flex gaps align-center" id="">
<label className={labelClassNames} id="">
{isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft}
{multiLine ? <textarea {...inputProps as any} /> : <input {...inputProps as any} />}
{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"
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"
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==