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

Hotbar visual improvements (#2638)

* Adding hotbar cells

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add/remove empty cells

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Increase cell corner radius

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Styling hotbar selector

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Generating 12 cells by default

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding custom scrollbar on hover

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Reset active cluster when leaving dashboard

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Moving kind icon top the top left corner

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Highlighting kind icon

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add hotbar cell animations

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding small hover effect

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2021-04-27 11:25:06 +03:00 committed by GitHub
parent b1274cbb33
commit 6a702ad19c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 344 additions and 83 deletions

View File

@ -50,7 +50,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
},
];
if (this.status.active) {
if (this.status.phase == "connected") {
context.menuItems.unshift({
icon: "link_off",
title: "Disconnect",

View File

@ -2,6 +2,9 @@ import { action, comparer, observable, toJS } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/hotbar-store";
import * as uuid from "uuid";
import { CatalogEntityItem } from "../renderer/components/+catalog/catalog-entity.store";
import isNull from "lodash/isNull";
import { CatalogEntity } from "./catalog/catalog-entity";
export interface HotbarItem {
entity: {
@ -29,6 +32,8 @@ export interface HotbarStoreModel {
activeHotbarId: string;
}
export const defaultHotbarCells = 12; // Number is choosen to easy hit any item with keyboard
export class HotbarStore extends BaseStore<HotbarStoreModel> {
@observable hotbars: Hotbar[] = [];
@observable private _activeHotbarId: string;
@ -58,12 +63,16 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
return this.hotbars.findIndex((hotbar) => hotbar.id === this.activeHotbarId);
}
get initialItems() {
return [...Array.from(Array(defaultHotbarCells).fill(null))];
}
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
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<HotbarStoreModel> {
add(data: HotbarCreateOptions) {
const {
id = uuid.v4(),
items = [],
items = this.initialItems,
name,
} = data;
@ -115,6 +124,52 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
}
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;

View File

@ -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) {

View File

@ -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

View File

@ -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;
}

View File

@ -66,8 +66,8 @@ export class HotbarIcon extends React.Component<Props> {
].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<Props> {
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<Props> {
>
{this.iconString}
</Avatar>
{ this.badgeIcon }
{ this.kindIcon }
<Menu
usePortal={false}
usePortal
htmlFor={entityIconId}
className="HotbarIconMenu"
isOpen={this.menuOpen}
@ -156,7 +152,7 @@ export class HotbarIcon extends React.Component<Props> {
position={{right: true, bottom: true }} // FIXME: position does not work
open={() => onOpen()}
close={() => this.toggleMenu()}>
<MenuItem key="remove-from-hotbar" onClick={() => this.removeFromHotbar(entity) }>
<MenuItem key="remove-from-hotbar" onClick={() => this.remove(entity) }>
<Icon material="clear" small interactive={true} title="Remove from hotbar"/> Remove from Hotbar
</MenuItem>
{ this.contextMenu && menuItems.map((menuItem) => {

View File

@ -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;
}
}

View File

@ -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<Props> {
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<Props> {
CommandOverlay.open(<HotbarSwitchCommand />);
}
renderGrid() {
if (!this.hotbar.items.length) return;
return this.hotbar.items.map((item, index) => {
const entity = this.getEntity(item);
return (
<HotbarCell key={index} index={index}>
{entity && (
<HotbarIcon
key={index}
index={index}
entity={entity}
isActive={this.isActive(entity)}
onClick={() => entity.onRun(catalogEntityRunContext)}
/>
)}
</HotbarCell>
);
});
}
renderAddCellButton() {
return (
<button className="AddCellButton" onClick={() => HotbarStore.getInstance().addEmptyCell()}>
<Icon material="add"/>
</button>
);
}
render() {
const { className } = this.props;
const hotbarStore = HotbarStore.getInstance();
@ -50,22 +89,13 @@ export class HotbarMenu extends React.Component<Props> {
return (
<div className={cssNames("HotbarMenu flex column", className)}>
<div className="items flex column gaps">
{this.hotbarItems.map((entity, index) => {
return (
<HotbarIcon
key={index}
index={index}
entity={entity}
isActive={entity.status.active}
onClick={() => entity.onRun(catalogEntityRunContext)}
/>
);
})}
<div className="HotbarItems flex column gaps">
{this.renderGrid()}
{this.hotbar.items.length != defaultHotbarCells && this.renderAddCellButton()}
</div>
<div className="HotbarSelector flex gaps auto">
<Icon material="chevron_left" className="previous box" onClick={() => this.previous()} />
<div className="box">
<div className="HotbarSelector flex align-center">
<Icon material="play_arrow" className="previous box" onClick={() => this.previous()} />
<div className="box grow flex align-center">
<Badge id="hotbarIndex" small label={activeIndexDisplay} onClick={() => this.openSelector()} />
<Tooltip
targetId="hotbarIndex"
@ -74,9 +104,39 @@ export class HotbarMenu extends React.Component<Props> {
{hotbar.name}
</Tooltip>
</div>
<Icon material="chevron_right" className="next box" onClick={() => this.next()} />
<Icon material="play_arrow" className="next box" onClick={() => this.next()} />
</div>
</div>
);
}
}
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 (
<div
className={cssNames("HotbarCell", { animating, empty: !props.children })}
onAnimationEnd={onAnimationEnd}
onClick={onClick}
>
{props.children}
{!props.children && (
<div className="cellDeleteButton" onClick={onDeleteClick}>
<Icon material="close" smallest/>
</div>
)}
</div>
);
}