From 4c975d8a7ebc7646c89bb70ae9da2c90c95c861d Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 12 Mar 2021 16:34:57 -0500 Subject: [PATCH] Add auto cluster icon color picking - Fix not handling grapheme clusters on cluster names Signed-off-by: Sebastian Malton --- package.json | 1 + src/common/__tests__/cluster-store.test.ts | 6 +- src/common/cluster-store.ts | 6 +- src/main/cluster.ts | 13 ++++ src/migrations/cluster-store/3.6.0-beta.1.ts | 2 +- .../+cluster-settings/cluster-settings.scss | 6 +- .../components/cluster-icon-setting.tsx | 34 +++++++++- .../components/cluster-icon/cluster-icon.scss | 18 +++-- .../components/cluster-icon/cluster-icon.tsx | 66 ++++++++++++++----- .../components/file-picker/file-picker.tsx | 20 +++--- src/renderer/components/input/input.scss | 2 +- src/renderer/components/input/input.tsx | 11 ++-- yarn.lock | 2 +- 13 files changed, 139 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 92853dcbd0..d1fc5e53b9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index fcbd5ebd6b..4cc9bcf41c 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -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); }); }); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index d8bd28f1e8..0df5e8bdd4 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -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; } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 13c74a285e..e381d0aacf 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -247,6 +247,19 @@ export class Cluster implements ClusterModel, ClusterState { }); } + @computed get iconPreference(): Required { + 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 */ diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts index ca2d0ccbed..ac28a481d0 100644 --- a/src/migrations/cluster-store/3.6.0-beta.1.ts +++ b/src/migrations/cluster-store/3.6.0-beta.1.ts @@ -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)); diff --git a/src/renderer/components/+cluster-settings/cluster-settings.scss b/src/renderer/components/+cluster-settings/cluster-settings.scss index dd1cd37f60..3ce2b75871 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.scss +++ b/src/renderer/components/+cluster-settings/cluster-settings.scss @@ -48,4 +48,8 @@ .Input, .Select { margin-top: $padding; } -} \ No newline at end of file + + .icon-background-color { + max-width: 100px; + } +} diff --git a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx index 466afc14da..a0e0f0987e 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx @@ -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 { } } + 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 { showErrors={false} showTooltip={false} /> - {"Browse for new icon..."} + Browse for new icon... ); @@ -73,6 +92,19 @@ export class ClusterIconSetting extends React.Component { /> {this.getClearButton()} +

Or change the colour of the generated icon.

+
+ + + This action clears any previously set icon. + +
); } diff --git a/src/renderer/components/cluster-icon/cluster-icon.scss b/src/renderer/components/cluster-icon/cluster-icon.scss index f246a8d9ee..eec4258baf 100644 --- a/src/renderer/components/cluster-icon/cluster-icon.scss +++ b/src/renderer/components/cluster-icon/cluster-icon.scss @@ -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 { diff --git a/src/renderer/components/cluster-icon/cluster-icon.tsx b/src/renderer/components/cluster-icon/cluster-icon.tsx index c7806e3ce3..db555753c8 100644 --- a/src/renderer/components/cluster-icon/cluster-icon.tsx +++ b/src/renderer/components/cluster-icon/cluster-icon.tsx @@ -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 { cluster: Cluster; @@ -25,6 +26,22 @@ const defaultProps: Partial = { 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 { static defaultProps = defaultProps as object; @@ -46,46 +63,59 @@ export class ClusterIcon extends React.Component { } 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 {name}; } + + return ( + + {this.iconString} + + ); } 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 ( -
+
{showTooltip && ( {name} )} - {icon && {name}/} - {!icon && {this.iconString}} + {this.renderIcon()} {showErrors && eventCount > 0 && !isActive && online && ( { 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 { 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 { const { accept, label, multiple } = this.props; return
- - {label}{" "}{this.getIconRight()} + { @autobind() onChange(evt: React.ChangeEvent) { - 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 { ); } + const labelClassNames = cssNames("input-area flex gaps align-center", { + notColor: inputProps.type !== "color", + }); + return (
{tooltipError} -