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:
parent
b1274cbb33
commit
6a702ad19c
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user