From 6a702ad19c6804260bdb9629d687c81732fe9f12 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Tue, 27 Apr 2021 11:25:06 +0300 Subject: [PATCH] Hotbar visual improvements (#2638) * Adding hotbar cells Signed-off-by: Alex Andreev * Add/remove empty cells Signed-off-by: Alex Andreev * Increase cell corner radius Signed-off-by: Alex Andreev * Styling hotbar selector Signed-off-by: Alex Andreev * Generating 12 cells by default Signed-off-by: Alex Andreev * Adding custom scrollbar on hover Signed-off-by: Alex Andreev * Reset active cluster when leaving dashboard Signed-off-by: Alex Andreev * Moving kind icon top the top left corner Signed-off-by: Alex Andreev * Highlighting kind icon Signed-off-by: Alex Andreev * Add hotbar cell animations Signed-off-by: Alex Andreev * Adding small hover effect Signed-off-by: Alex Andreev --- .../catalog-entities/kubernetes-cluster.ts | 2 +- src/common/hotbar-store.ts | 59 +++++- src/renderer/components/+catalog/catalog.tsx | 10 +- .../cluster-manager/cluster-manager.tsx | 1 + .../components/hotbar/hotbar-icon.scss | 43 ++-- .../components/hotbar/hotbar-icon.tsx | 20 +- .../components/hotbar/hotbar-menu.scss | 188 ++++++++++++++++-- .../components/hotbar/hotbar-menu.tsx | 104 ++++++++-- 8 files changed, 344 insertions(+), 83 deletions(-) diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 747e042efd..c2dae01b74 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -50,7 +50,7 @@ export class KubernetesCluster extends CatalogEntity { @observable hotbars: Hotbar[] = []; @observable private _activeHotbarId: string; @@ -58,12 +63,16 @@ export class HotbarStore extends BaseStore { return this.hotbars.findIndex((hotbar) => hotbar.id === this.activeHotbarId); } + get initialItems() { + return [...Array.from(Array(defaultHotbarCells).fill(null))]; + } + @action protected async fromStore(data: Partial = {}) { if (data.hotbars?.length === 0) { this.hotbars = [{ id: uuid.v4(), name: "Default", - items: [] + items: this.initialItems, }]; } else { this.hotbars = data.hotbars; @@ -95,7 +104,7 @@ export class HotbarStore extends BaseStore { add(data: HotbarCreateOptions) { const { id = uuid.v4(), - items = [], + items = this.initialItems, name, } = data; @@ -115,6 +124,52 @@ export class HotbarStore extends BaseStore { } } + addToHotbar(item: CatalogEntityItem, cellIndex = -1) { + const hotbar = this.getActive(); + const newItem = { entity: { uid: item.id }}; + + if (hotbar.items.find(i => i?.entity.uid === item.id)) { + return; + } + + if (cellIndex == -1) { + // Add item to empty cell + const emptyCellIndex = hotbar.items.findIndex(isNull); + + if (emptyCellIndex != -1) { + hotbar.items[emptyCellIndex] = newItem; + } else { + // Add new item to the end of list + hotbar.items.push(newItem); + } + } else { + hotbar.items[cellIndex] = newItem; + } + } + + removeFromHotbar(item: CatalogEntity) { + const hotbar = this.getActive(); + const index = hotbar.items.findIndex((i) => i?.entity.uid === item.getId()); + + if (index == -1) { + return; + } + + hotbar.items[index] = null; + } + + addEmptyCell() { + const hotbar = this.getActive(); + + hotbar.items.push(null); + } + + removeEmptyCell(index: number) { + const hotbar = this.getActive(); + + hotbar.items.splice(index, 1); + } + switchToPrevious() { const hotbarStore = HotbarStore.getInstance(); let index = hotbarStore.activeHotbarIndex - 1; diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index b04a83e9c6..e748f27b64 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -46,14 +46,8 @@ export class Catalog extends React.Component { ]); } - addToHotbar(item: CatalogEntityItem) { - const hotbar = HotbarStore.getInstance().getActive(); - - if (!hotbar) { - return; - } - - hotbar.items.push({ entity: { uid: item.id }}); + addToHotbar(item: CatalogEntityItem): void { + HotbarStore.getInstance().addToHotbar(item); } onDetails(item: CatalogEntityItem) { diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 2aa923c402..c09b4d7057 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -28,6 +28,7 @@ export class ClusterManager extends React.Component { reaction(getMatchedClusterId, initView, { fireImmediately: true }), + reaction(() => !getMatchedClusterId(), () => ClusterStore.getInstance().setActive(null)), reaction(() => [ getMatchedClusterId(), // refresh when active cluster-view changed hasLoadedView(getMatchedClusterId()), // refresh when cluster's webview loaded diff --git a/src/renderer/components/hotbar/hotbar-icon.scss b/src/renderer/components/hotbar/hotbar-icon.scss index 913fbc6cb7..6ac4580877 100644 --- a/src/renderer/components/hotbar/hotbar-icon.scss +++ b/src/renderer/components/hotbar/hotbar-icon.scss @@ -2,53 +2,46 @@ .HotbarIcon { --size: 37px; - position: relative; - border-radius: 8px; - padding: 2px; + border-radius: 6px; user-select: none; cursor: pointer; + transition: none; div.MuiAvatar-colorDefault { font-weight:500; text-transform: uppercase; - border-radius: 4px; - } - - div.active { - background-color: var(--primary); - } - - &.interactive { - margin-left: -3px; - border: 3px solid var(--clusterMenuBackground); + border-radius: 6px; } &.active { - border: 3px solid #fff; + box-shadow: 0 0 0px 3px #ffffff; + transition: all 0s 0.8s; } &.active, &.interactive:hover { - - div { - background-color: var(--primary); - } - img { opacity: 1; } } .badge { - color: $textColorAccent; position: absolute; - right: 0; - bottom: 0; - margin: -$padding; - font-size: $font-size-small; - background: $clusterMenuBackground; + right: -2px; + bottom: -3px; + margin: -8px; + font-size: var(--font-size-small); + background: var(--clusterMenuBackground); color: white; padding: 0px; border-radius: 50%; + border: 3px solid var(--clusterMenuBackground); + width: 15px; + height: 15px; + + &.online { + background-color: #44b700; + } + svg { width: 13px; } diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index 0a6b6f56bb..db2bf6a0dd 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -66,8 +66,8 @@ export class HotbarIcon extends React.Component { ].filter(Boolean).join(""); } - get badgeIcon() { - const className = "badge"; + get kindIcon() { + const className = cssNames("badge", { online: this.props.entity.status.phase == "connected"}); const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity); if (!category) { @@ -85,14 +85,10 @@ export class HotbarIcon extends React.Component { this.menuOpen = !this.menuOpen; } - removeFromHotbar(item: CatalogEntity) { - const hotbar = HotbarStore.getInstance().getActive(); + remove(item: CatalogEntity) { + const hotbar = HotbarStore.getInstance(); - if (!hotbar) { - return; - } - - hotbar.items = hotbar.items.filter((i) => i.entity.uid !== item.metadata.uid); + hotbar.removeFromHotbar(item); } onMenuItemClick(menuItem: CatalogEntityContextMenu) { @@ -146,9 +142,9 @@ export class HotbarIcon extends React.Component { > {this.iconString} - { this.badgeIcon } + { this.kindIcon } { position={{right: true, bottom: true }} // FIXME: position does not work open={() => onOpen()} close={() => this.toggleMenu()}> - this.removeFromHotbar(entity) }> + this.remove(entity) }> Remove from Hotbar { this.contextMenu && menuItems.map((menuItem) => { diff --git a/src/renderer/components/hotbar/hotbar-menu.scss b/src/renderer/components/hotbar/hotbar-menu.scss index e8cf6efc1a..af41e1b95b 100644 --- a/src/renderer/components/hotbar/hotbar-menu.scss +++ b/src/renderer/components/hotbar/hotbar-menu.scss @@ -4,32 +4,194 @@ position: relative; text-align: center; background: $clusterMenuBackground; - border-right: 1px solid $clusterMenuBorderColor; - padding: $spacing 0; - min-width: 75px; + padding-top: 28px; + width: 75px; .is-mac &:before { content: ""; - height: 20px; // extra spacing for mac-os "traffic-light" buttons + height: 4px; // extra spacing for mac-os "traffic-light" buttons } - .items { - padding: 0 $spacing; // extra spacing for cluster-icon's badge - margin-bottom: $margin; - overflow: visible; + &:hover { + .AddCellButton { + opacity: 1; + } + } - &:empty { - display: none; + .HotbarItems { + --cellWidth: 40px; + --cellHeight: 40px; + + box-sizing: content-box; + margin: 0 auto; + height: 100%; + overflow: hidden; + padding-bottom: 8px; + + &:hover { + overflow: overlay; + + &::-webkit-scrollbar { + width: 0.4em; + background: transparent; + z-index: 1; + } + + &::-webkit-scrollbar-thumb { + background: var(--borderFaintColor); + } + } + + .HotbarCell { + width: var(--cellWidth); + height: var(--cellHeight); + min-height: var(--cellHeight); + margin: 12px; + background: var(--layoutBackground); + border-radius: 6px; + position: relative; + transform: translateZ(0); // Remove flickering artifacts + + &:hover { + .cellDeleteButton { + opacity: 1; + transition: opacity 0.1s 0.2s; + } + + &:not(.empty) { + box-shadow: 0 0 0px 3px #ffffff1a; + } + } + + &.animating { + &.empty { + animation: shake .6s cubic-bezier(.36,.07,.19,.97) both; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; + } + + &:not(.empty) { + animation: outline 0.8s cubic-bezier(0.19, 1, 0.22, 1); + } + } + + .cellDeleteButton { + width: 2rem; + height: 2rem; + border-radius: 50%; + background-color: var(--textColorDimmed); + position: absolute; + top: -7px; + right: -7px; + color: var(--secondaryBackground); + opacity: 0; + border: 3px solid var(--clusterMenuBackground); + box-sizing: border-box; + + &:hover { + background-color: white; + transition: all 0.2s; + } + + .Icon { + --smallest-size: 12px; + font-weight: bold; + position: relative; + top: -2px; + left: .5px; + } + } } } .HotbarSelector { - position: absolute; - bottom: 0; - width: 100%; + height: 26px; + background-color: var(--layoutBackground); + position: relative; + + &:before { + content: " "; + position: absolute; + width: 100%; + height: 20px; + background: linear-gradient(0deg, var(--clusterMenuBackground), transparent); + top: -20px; + } .Badge { cursor: pointer; + background: var(--secondaryBackground); + width: 100%; + color: var(--settingsColor); + padding-top: 3px; + } + + .Icon { + --size: 16px; + padding: 0 4px; + + &:hover { + box-shadow: none; + background-color: transparent; + } + + &.previous { + transform: rotateY(180deg); + } + } + } + + .AddCellButton { + width: 40px; + height: 40px; + min-height: 40px; + margin: 12px auto 8px; + background-color: transparent; + color: var(--textColorDimmed); + border-radius: 6px; + transition: all 0.2s; + cursor: pointer; + z-index: 1; + opacity: 0; + transition: all 0.2s; + + &:hover { + background-color: var(--sidebarBackground); + } + + .Icon { + --size: 24px; + margin-left: 2px; } } } + +@keyframes shake { + 10%, 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, 50%, 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, 60% { + transform: translate3d(4px, 0, 0); + } +} + +// TODO: Use theme-aware colors +@keyframes outline { + 0% { + box-shadow: 0 0 0px 11px $clusterMenuBackground, 0 0 0px 15px #ffffff00; + } + + 100% { + box-shadow: 0 0 0px 0px $clusterMenuBackground, 0 0 0px 3px #ffffff; + } +} \ No newline at end of file diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index ac99a45226..931ebc02e8 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -1,17 +1,18 @@ import "./hotbar-menu.scss"; import "./hotbar.commands"; -import React from "react"; +import React, { ReactNode, useState } from "react"; import { observer } from "mobx-react"; import { HotbarIcon } from "./hotbar-icon"; import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { HotbarStore } from "../../../common/hotbar-store"; -import { catalogEntityRunContext } from "../../api/catalog-entity"; +import { defaultHotbarCells, HotbarItem, HotbarStore } from "../../../common/hotbar-store"; +import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity"; import { Icon } from "../icon"; import { Badge } from "../badge"; import { CommandOverlay } from "../command-palette"; import { HotbarSwitchCommand } from "./hotbar-switch-command"; +import { ClusterStore } from "../../../common/cluster-store"; import { Tooltip, TooltipPosition } from "../tooltip"; interface Props { @@ -20,14 +21,22 @@ interface Props { @observer export class HotbarMenu extends React.Component { - get hotbarItems() { + get hotbar() { + return HotbarStore.getInstance().getActive(); + } + + isActive(item: CatalogEntity) { + return ClusterStore.getInstance().activeClusterId == item.getId(); + } + + getEntity(item: HotbarItem) { const hotbar = HotbarStore.getInstance().getActive(); if (!hotbar) { - return []; + return null; } - return hotbar.items.map((item) => catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid)).filter(Boolean); + return item ? catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid) : null; } previous() { @@ -42,6 +51,36 @@ export class HotbarMenu extends React.Component { CommandOverlay.open(); } + renderGrid() { + if (!this.hotbar.items.length) return; + + return this.hotbar.items.map((item, index) => { + const entity = this.getEntity(item); + + return ( + + {entity && ( + entity.onRun(catalogEntityRunContext)} + /> + )} + + ); + }); + } + + renderAddCellButton() { + return ( + + ); + } + render() { const { className } = this.props; const hotbarStore = HotbarStore.getInstance(); @@ -50,22 +89,13 @@ export class HotbarMenu extends React.Component { return (
-
- {this.hotbarItems.map((entity, index) => { - return ( - entity.onRun(catalogEntityRunContext)} - /> - ); - })} +
+ {this.renderGrid()} + {this.hotbar.items.length != defaultHotbarCells && this.renderAddCellButton()}
-
- this.previous()} /> -
+
+ this.previous()} /> +
this.openSelector()} /> { {hotbar.name}
- this.next()} /> + this.next()} />
); } } + +interface HotbarCellProps { + children?: ReactNode; + index: number; +} + +function HotbarCell(props: HotbarCellProps) { + const [animating, setAnimating] = useState(false); + const onAnimationEnd = () => { setAnimating(false); }; + const onClick = () => { setAnimating(true); }; + const onDeleteClick = (evt: Event | React.SyntheticEvent) => { + evt.stopPropagation(); + HotbarStore.getInstance().removeEmptyCell(props.index); + }; + + return ( +
+ {props.children} + {!props.children && ( +
+ +
+ )} +
+ ); +}