Restructure to monorepo (#6907)

* wip: restructure to monorepo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor create-release-pr to a package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* github workflow fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix typo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* add webpack-env types to core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix github workflows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor/fix integration tests

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* lint fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* yarn run dev

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* eslint settings for vscode

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* move templates to right package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* open-lens build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* integration test fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix nx task dependencies

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* use bash shell for unit tests in test workflow

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix test:unit for windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix build:app on windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove ELECTRON_BUILDER_EXTRA_ARGS

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* sync src/ from master

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove Makefile from core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
1 parent 0066ca4ea2
commit 2657df2293
3321 files changed
+28699 -11447

No files matched your search

@@ -0,0 +1,79 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "@testing-library/jest-dom/extend-expect";
import { HotbarRemoveCommand } from "../hotbar-remove-command";
import { fireEvent } from "@testing-library/react";
import React from "react";
import type { DiContainer } from "@ogre-tools/injectable";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import { ConfirmDialog } from "../../confirm-dialog";
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { HotbarStore } from "../../../../common/hotbars/store";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
const mockHotbars: Partial<Record<string, any>> = {
"1": {
id: "1",
name: "Default",
items: [],
},
};
describe("<HotbarRemoveCommand />", () => {
let di: DiContainer;
let render: DiRender;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
render = renderFor(di);
});
it("renders w/o errors", () => {
di.override(hotbarStoreInjectable, () => ({
hotbars: [mockHotbars["1"]],
getById: (id: string) => mockHotbars[id],
remove: () => {
},
hotbarIndex: () => 0,
getDisplayLabel: () => "1: Default",
}) as unknown as HotbarStore);
const { container } = render(<HotbarRemoveCommand />);
expect(container).toBeInstanceOf(HTMLElement);
});
it("calls remove if you click on the entry", () => {
const removeMock = jest.fn();
di.override(hotbarStoreInjectable, () => ({
hotbars: [mockHotbars["1"]],
getById: (id: string) => mockHotbars[id],
remove: removeMock,
hotbarIndex: () => 0,
getDisplayLabel: () => "1: Default",
}) as unknown as HotbarStore);
const { getByText } = render(
<>
<HotbarRemoveCommand />
<ConfirmDialog />
</>,
);
fireEvent.click(getByText("1: Default"));
fireEvent.click(getByText("Remove Hotbar"));
expect(removeMock).toHaveBeenCalled();
});
});
@@ -0,0 +1,58 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { observer } from "mobx-react";
import type { InputValidator } from "../input";
import { Input } from "../input";
import type { CreateHotbarData, CreateHotbarOptions } from "../../../common/hotbars/types";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable";
import addHotbarInjectable from "../../../common/hotbars/add-hotbar.injectable";
interface Dependencies {
closeCommandOverlay: () => void;
addHotbar: (data: CreateHotbarData, opts: CreateHotbarOptions) => void;
uniqueHotbarName: InputValidator<boolean>;
}
const NonInjectedHotbarAddCommand = observer(({ closeCommandOverlay, addHotbar, uniqueHotbarName }: Dependencies) => {
const onSubmit = (name: string) => {
if (!name.trim()) {
return;
}
addHotbar({ name }, { setActive: true });
closeCommandOverlay();
};
return (
<>
<Input
placeholder="Hotbar name"
autoFocus={true}
theme="round-black"
data-test-id="command-palette-hotbar-add-name"
validators={uniqueHotbarName}
onSubmit={onSubmit}
dirty={true}
showValidationLine={true}
/>
<small className="hint">
Please provide a new hotbar name (Press &quot;Enter&quot; to confirm or &quot;Escape&quot; to cancel)
</small>
</>
);
});
export const HotbarAddCommand = withInjectables<Dependencies>(NonInjectedHotbarAddCommand, {
getProps: (di, props) => ({
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
addHotbar: di.inject(addHotbarInjectable),
uniqueHotbarName: di.inject(uniqueHotbarNameInjectable),
...props,
}),
});
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./hotbar-menu.scss";
import type { HTMLAttributes, ReactNode } from "react";
import React, { useState } from "react";
import { cssNames } from "../../utils";
export interface HotbarCellProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
index: number;
innerRef?: React.Ref<HTMLDivElement>;
}
export function HotbarCell({ innerRef, children, className, ...rest }: HotbarCellProps) {
const [animating, setAnimating] = useState(false);
const onAnimationEnd = () => { setAnimating(false); };
const onClick = () => {
setAnimating(!className?.includes("isDraggingOver"));
};
return (
<div
className={cssNames("HotbarCell", { animating }, className)}
onAnimationEnd={onAnimationEnd}
onClick={onClick}
ref={innerRef}
{...rest}
>
{children}
</div>
);
}
@@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.led {
position: absolute;
left: 3px;
top: 3px;
background-color: var(--layoutBackground);
border: 1px solid var(--clusterMenuBackground);
border-radius: 50%;
padding: 0px;
width: 8px;
height: 8px;
&.online {
background-color: var(--primary);
box-shadow: 0 0 5px var(--clusterMenuBackground), 0 0 5px var(--primary);
}
}
.badge {
position: absolute;
right: -2px;
bottom: -3px;
margin: -8px;
font-size: var(--font-size-small);
background: var(--clusterMenuBackground);
color: var(--textColorAccent);
padding: 0px;
border-radius: 50%;
border: 2px solid var(--clusterMenuBackground);
width: 15px;
height: 15px;
svg {
width: 13px;
}
}
@@ -0,0 +1,133 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./hotbar-entity-icon.module.scss";
import React from "react";
import type { IComputedValue } from "mobx";
import { observable } from "mobx";
import { observer } from "mobx-react";
import type { CatalogCategoryRegistry, CatalogEntity, CatalogEntityContextMenu } from "../../../common/catalog";
import type { IClassName } from "../../utils";
import { cssNames } from "../../utils";
import { Icon } from "../icon";
import { HotbarIcon } from "./hotbar-icon";
import { LensKubernetesClusterStatus } from "../../../common/catalog-entities/kubernetes-cluster";
import type { VisitEntityContextMenu } from "../../../common/catalog/visit-entity-context-menu.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
import visitEntityContextMenuInjectable from "../../../common/catalog/visit-entity-context-menu.injectable";
import activeEntityInjectable from "../../api/catalog/entity/active.injectable";
import type { Navigate } from "../../navigation/navigate.injectable";
import navigateInjectable from "../../navigation/navigate.injectable";
export interface HotbarEntityIconProps {
entity: CatalogEntity;
index: number;
errorClass?: IClassName;
add: (item: CatalogEntity, index: number) => void;
remove: (uid: string) => void;
size?: number;
onClick?: () => void;
className?: string;
}
interface Dependencies {
visitEntityContextMenu: VisitEntityContextMenu;
catalogCategoryRegistry: CatalogCategoryRegistry;
activeEntity: IComputedValue<CatalogEntity | undefined>;
navigate: Navigate;
}
@observer
class NonInjectedHotbarEntityIcon extends React.Component<HotbarEntityIconProps & Dependencies> {
private readonly menuItems = observable.array<CatalogEntityContextMenu>();
private renderKindIcon() {
const className = styles.badge;
const category = this.props.catalogCategoryRegistry.getCategoryForEntity(this.props.entity);
if (!category) {
return <Icon material="bug_report" className={className} />;
}
if (Icon.isSvg(category.metadata.icon)) {
return <Icon svg={category.metadata.icon} className={className} />;
}
return <Icon material={category.metadata.icon} className={className} />;
}
private renderLedIcon() {
if (this.props.entity.kind !== "KubernetesCluster") {
return null;
}
return (
<div
className={cssNames(styles.led, {
// TODO: make it more generic
[styles.online]: this.props.entity.status.phase === LensKubernetesClusterStatus.CONNECTED,
})}
/>
);
}
isActive(item: CatalogEntity) {
return this.props.activeEntity.get()?.metadata?.uid == item.getId();
}
onMenuOpen() {
this.menuItems.replace([{
title: "Remove from Hotbar",
onClick: () => this.props.remove(this.props.entity.getId()),
}]);
this.props.visitEntityContextMenu(this.props.entity, {
menuItems: this.menuItems,
navigate: this.props.navigate,
});
}
render() {
const { entity, className, onClick } = this.props;
return (
<HotbarIcon
uid={entity.getId()}
title={entity.getName()}
source={entity.metadata.source}
src={entity.spec.icon?.src}
material={entity.spec.icon?.material}
background={entity.spec.icon?.background}
className={className}
active={this.isActive(entity)}
onMenuOpen={() => this.onMenuOpen()}
disabled={!entity}
menuItems={this.menuItems}
tooltip={(
entity.metadata.source
? `${entity.getName()} (${entity.metadata.source})`
: entity.getName()
)}
onClick={onClick}
>
{this.renderLedIcon()}
{this.renderKindIcon()}
</HotbarIcon>
);
}
}
export const HotbarEntityIcon = withInjectables<Dependencies, HotbarEntityIconProps>(NonInjectedHotbarEntityIcon, {
getProps: (di, props) => ({
...props,
catalogCategoryRegistry: di.inject(catalogCategoryRegistryInjectable),
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
activeEntity: di.inject(activeEntityInjectable),
navigate: di.inject(navigateInjectable),
}),
});
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.HotbarIcon {
--corner: 0px 0px 0px 1px var(--clusterMenuBackground);
--iconActiveShadow: var(--corner), var(--corner), 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
--iconHoverShadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff50;
display: flex;
cursor: pointer;
position: relative;
border-radius: 6px;
transition: box-shadow 0.1s ease-in-out;
&:not(.active):hover {
box-shadow: var(--iconHoverShadow);
}
}
.contextMenuAvailable {
cursor: context-menu;
}
.avatar {
border-radius: 6px;
&.hasImage {
background-color: var(--clusterMenuCellBackground);
}
}
.active {
box-shadow: var(--iconActiveShadow);
}
@@ -0,0 +1,104 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./hotbar-icon.module.scss";
import React, { useState } from "react";
import type { CatalogEntityContextMenu } from "../../../common/catalog";
import { cssNames } from "../../utils";
import { Menu, MenuItem } from "../menu";
import { observer } from "mobx-react";
import type { AvatarProps } from "../avatar";
import { Avatar } from "../avatar";
import { Icon } from "../icon";
import { Tooltip } from "../tooltip";
import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable";
export interface HotbarIconProps extends AvatarProps {
uid: string;
source?: string;
material?: string;
onMenuOpen?: () => void;
active?: boolean;
menuItems?: CatalogEntityContextMenu[];
disabled?: boolean;
tooltip?: string;
}
interface Dependencies {
normalizeMenuItem: NormalizeCatalogEntityContextMenu;
}
const NonInjectedHotbarIcon = observer(({
menuItems = [],
size = 40,
tooltip,
normalizeMenuItem,
...props
}: HotbarIconProps & Dependencies) => {
const { uid, title, src, material, active, className, source, disabled, onMenuOpen, onClick, children, ...rest } = props;
const id = `hotbarIcon-${uid}`;
const [menuOpen, setMenuOpen] = useState(false);
const toggleMenu = () => {
setMenuOpen(!menuOpen);
};
return (
<div className={cssNames(styles.HotbarIcon, className, { [styles.contextMenuAvailable]: menuItems.length > 0 })}>
{tooltip && (
<Tooltip targetId={id}>
{tooltip}
</Tooltip>
)}
<Avatar
{...rest}
id={id}
title={title}
colorHash={source ? `${title}-${source}` : title}
className={cssNames(styles.avatar, { [styles.active]: active, [styles.hasImage]: !!src })}
disabled={disabled}
size={size}
src={src}
onClick={(event) => !disabled && onClick?.(event)}
>
{material && <Icon material={material} />}
</Avatar>
{children}
<Menu
usePortal
htmlFor={id}
isOpen={menuOpen}
toggleEvent="contextmenu"
position={{ right: true, bottom: true }} // FIXME: position does not work
open={() => {
onMenuOpen?.();
toggleMenu();
}}
close={() => toggleMenu()}
>
{
menuItems
.map(normalizeMenuItem)
.map((menuItem) => (
<MenuItem key={menuItem.title} onClick={menuItem.onClick}>
{menuItem.title}
</MenuItem>
))
}
</Menu>
</div>
);
});
export const HotbarIcon = withInjectables<Dependencies, HotbarIconProps>(NonInjectedHotbarIcon, {
getProps: (di, props) => ({
...props,
normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable),
}),
});
@@ -0,0 +1,127 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.HotbarMenu {
$spacing: $padding * 2;
position: relative;
text-align: center;
background: var(--clusterMenuBackground);
padding-top: 1px;
width: var(--hotbar-width);
overflow: hidden;
&.draggingOver::after {
content: " ";
position: fixed;
left: var(--hotbar-width);
width: 100%;
height: 100%;
}
.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(--clusterMenuCellBackground);
border-radius: 6px;
position: relative;
&.isDraggingOver {
background-color: #3e4148;
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30;
&:not(.isDraggingOwner) {
z-index: 50;
> div:not(:empty) {
border-radius: 6px;
box-shadow: 0 0 9px #00000042;
}
&.animateUp {
> div {
transform: translate(0px, -40px)!important;
}
}
&.animateDown {
> div {
transform: translate(0px, 40px);
}
}
}
}
&.animating {
&:empty {
animation: shake .6s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
&:not(:empty) {
> div {
animation: click .1s;
}
}
}
}
}
}
@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);
}
}
@keyframes click {
0% {
margin-top: 0;
}
100% {
margin-top: 2px;
}
}
@@ -0,0 +1,208 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./hotbar-menu.scss";
import React from "react";
import { observer } from "mobx-react";
import { HotbarEntityIcon } from "./hotbar-entity-icon";
import type { IClassName } from "../../utils";
import { cssNames } from "../../utils";
import type { CatalogEntityRegistry } from "../../api/catalog/entity/registry";
import type { CatalogEntity } from "../../api/catalog-entity";
import { DragDropContext, Draggable, Droppable, type DropResult } from "react-beautiful-dnd";
import { HotbarSelector } from "./hotbar-selector";
import { HotbarCell } from "./hotbar-cell";
import { HotbarIcon } from "./hotbar-icon";
import type { HotbarItem } from "../../../common/hotbars/types";
import { defaultHotbarCells } from "../../../common/hotbars/types";
import { action, makeObservable, observable } from "mobx";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { HotbarStore } from "../../../common/hotbars/store";
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
export interface HotbarMenuProps {
className?: IClassName;
}
interface Dependencies {
hotbarStore: HotbarStore;
entityRegistry: CatalogEntityRegistry;
}
@observer
class NonInjectedHotbarMenu extends React.Component<Dependencies & HotbarMenuProps> {
@observable draggingOver = false;
constructor(props: Dependencies & HotbarMenuProps) {
super(props);
makeObservable(this);
}
get hotbar() {
return this.props.hotbarStore.getActive();
}
getEntity(item: HotbarItem | null) {
const hotbar = this.props.hotbarStore.getActive();
if (!hotbar || !item) {
return null;
}
return this.props.entityRegistry.getById(item.entity.uid) ?? null;
}
@action
onDragStart() {
this.draggingOver = true;
}
@action
onDragEnd(result: DropResult) {
const { source, destination } = result;
this.draggingOver = false;
if (!destination) { // Dropped outside of the list
return;
}
const from = parseInt(source.droppableId);
const to = parseInt(destination.droppableId);
this.props.hotbarStore.restackItems(from, to);
}
removeItem = (uid: string) => {
const hotbar = this.props.hotbarStore;
hotbar.removeFromHotbar(uid);
};
addItem = (entity: CatalogEntity, index = -1) => {
const hotbar = this.props.hotbarStore;
hotbar.addToHotbar(entity, index);
};
getMoveAwayDirection(entityId: string | undefined | null, cellIndex: number) {
if (!entityId) {
return "animateDown";
}
const draggableItemIndex = this.hotbar.items.findIndex(item => item?.entity.uid == entityId);
return draggableItemIndex > cellIndex ? "animateDown" : "animateUp";
}
renderGrid() {
return this.hotbar.items.map((item, index) => {
const entity = this.getEntity(item);
return (
<Droppable droppableId={`${index}`} key={index}>
{(provided, snapshot) => (
<HotbarCell
index={index}
key={entity ? entity.getId() : `cell${index}`}
innerRef={provided.innerRef}
className={cssNames(
{
isDraggingOver: snapshot.isDraggingOver,
isDraggingOwner: snapshot.draggingOverWith == entity?.getId(),
},
this.getMoveAwayDirection(snapshot.draggingOverWith, index),
)}
{...provided.droppableProps}
>
{item && (
<Draggable
draggableId={item.entity.uid}
key={item.entity.uid}
index={0}
>
{(provided, snapshot) => {
const style = {
zIndex: defaultHotbarCells - index,
position: "absolute",
...provided.draggableProps.style,
} as React.CSSProperties;
return (
<div
key={item.entity.uid}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={style}
>
{entity ? (
<HotbarEntityIcon
key={index}
index={index}
entity={entity}
onClick={() => this.props.entityRegistry.onRun(entity)}
className={cssNames({ isDragging: snapshot.isDragging })}
remove={this.removeItem}
add={this.addItem}
size={40}
/>
) : (
<HotbarIcon
uid={`hotbar-icon-${item.entity.uid}`}
title={item.entity.name}
source={item.entity.source ?? "local"}
tooltip={`${item.entity.name} (${item.entity.source})`}
menuItems={[
{
title: "Remove from Hotbar",
onClick: () => this.removeItem(item.entity.uid),
},
]}
disabled
size={40}
/>
)}
</div>
);
}}
</Draggable>
)}
{provided.placeholder}
</HotbarCell>
)}
</Droppable>
);
});
}
render() {
const { className, hotbarStore } = this.props;
const hotbar = hotbarStore.getActive();
return (
<div className={cssNames("HotbarMenu flex column", { draggingOver: this.draggingOver }, className)}>
<div className="HotbarItems flex column gaps">
<DragDropContext
onDragStart={() => this.onDragStart()}
onDragEnd={(result) => this.onDragEnd(result)}>
{this.renderGrid()}
</DragDropContext>
</div>
<HotbarSelector hotbar={hotbar} />
</div>
);
}
}
export const HotbarMenu = withInjectables<Dependencies, HotbarMenuProps>(NonInjectedHotbarMenu, {
getProps: (di, props) => ({
...props,
hotbarStore: di.inject(hotbarStoreInjectable),
entityRegistry: di.inject(catalogEntityRegistryInjectable),
}),
});
@@ -0,0 +1,78 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { observer } from "mobx-react";
import { Select } from "../select";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import type { HotbarStore } from "../../../common/hotbars/store";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
interface Dependencies {
closeCommandOverlay: () => void;
openConfirmDialog: OpenConfirmDialog;
hotbarStore: HotbarStore;
}
const NonInjectedHotbarRemoveCommand = observer(({
closeCommandOverlay,
hotbarStore,
openConfirmDialog,
}: Dependencies) => (
<Select
menuPortalTarget={null}
onChange={option => {
if (!option) {
return;
}
closeCommandOverlay();
openConfirmDialog({
okButtonProps: {
label: "Remove Hotbar",
primary: false,
accent: true,
},
ok: () => hotbarStore.remove(option.value),
message: (
<div className="confirm flex column gaps">
<p>
Are you sure you want remove hotbar
{" "}
<b>
{option.value.name}
</b>
?
</p>
</div>
),
});
} }
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true}
options={(
hotbarStore.hotbars
.map(hotbar => ({
value: hotbar,
label: hotbarStore.getDisplayLabel(hotbar),
}))
)}
autoFocus={true}
escapeClearsValue={false}
placeholder="Remove hotbar"
/>
));
export const HotbarRemoveCommand = withInjectables<Dependencies>(NonInjectedHotbarRemoveCommand, {
getProps: (di, props) => ({
...props,
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
hotbarStore: di.inject(hotbarStoreInjectable),
openConfirmDialog: di.inject(openConfirmDialogInjectable),
}),
});
@@ -0,0 +1,94 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React, { useState } from "react";
import { observer } from "mobx-react";
import { Select } from "../select";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import type { InputValidator } from "../input";
import { Input } from "../input";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable";
import type { HotbarStore } from "../../../common/hotbars/store";
interface Dependencies {
closeCommandOverlay: () => void;
hotbarStore: HotbarStore;
uniqueHotbarName: InputValidator<false>;
}
const NonInjectedHotbarRenameCommand = observer(({
closeCommandOverlay,
hotbarStore,
uniqueHotbarName,
}: Dependencies) => {
const [hotbarId, setHotbarId] = useState("");
const [hotbarName, setHotbarName] = useState("");
const onSubmit = (name: string) => {
if (!name.trim()) {
return;
}
hotbarStore.setHotbarName(hotbarId, name);
closeCommandOverlay();
};
if (hotbarId) {
return (
<>
<Input
trim={true}
value={hotbarName}
onChange={setHotbarName}
placeholder="New hotbar name"
autoFocus={true}
theme="round-black"
validators={uniqueHotbarName}
onSubmit={onSubmit}
showValidationLine={true}
/>
<small className="hint">
Please provide a new hotbar name (Press &quot;Enter&quot; to confirm or &quot;Escape&quot; to cancel)
</small>
</>
);
}
return (
<Select
id="rename-hotbar-input"
menuPortalTarget={null}
onChange={(option) => {
if (option) {
setHotbarId(option.value.id);
setHotbarName(option.value.name);
}
}}
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true}
options={(
hotbarStore.hotbars
.map(hotbar => ({
value: hotbar,
label: hotbarStore.getDisplayLabel(hotbar),
}))
)}
autoFocus={true}
escapeClearsValue={false}
placeholder="Rename hotbar"
/>
);
});
export const HotbarRenameCommand = withInjectables<Dependencies>(NonInjectedHotbarRenameCommand, {
getProps: (di, props) => ({
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
hotbarStore: di.inject(hotbarStoreInjectable),
uniqueHotbarName: di.inject(uniqueHotbarNameInjectable),
...props,
}),
});
@@ -0,0 +1,46 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.HotbarSelector {
display: flex;
align-items: center;
height: 25px;
background-color: var(--layoutBackground);
position: relative;
justify-content: center;
&:before {
content: " ";
position: absolute;
width: 100%;
height: 20px;
background: linear-gradient(0deg, var(--clusterMenuBackground), transparent);
top: -20px;
}
.HotbarIndex {
display: flex;
width: 3ch;
}
.Badge {
cursor: pointer;
background: var(--secondaryBackground);
width: 100%;
color: var(--settingsColor);
padding: 0px calc(var(--padding) / 2);
}
.Icon {
&:hover {
box-shadow: none;
background-color: transparent;
}
&:focus-visible {
z-index: 1;
}
}
}
@@ -0,0 +1,97 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./hotbar-selector.module.scss";
import React, { useRef, useState } from "react";
import { Icon } from "../icon";
import { Badge } from "../badge";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import { HotbarSwitchCommand } from "./hotbar-switch-command";
import { Tooltip, TooltipPosition } from "../tooltip";
import { observer } from "mobx-react";
import type { Hotbar } from "../../../common/hotbars/types";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import { cssNames } from "../../utils";
import type { HotbarStore } from "../../../common/hotbars/store";
interface Dependencies {
hotbarStore: HotbarStore;
openCommandOverlay: (component: React.ReactElement) => void;
}
export interface HotbarSelectorProps extends Partial<Dependencies> {
hotbar: Hotbar;
}
const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOverlay }: HotbarSelectorProps & Dependencies) => {
const [tooltipVisible, setTooltipVisible] = useState(false);
const tooltipTimeout = useRef<number>();
function clearTimer() {
clearTimeout(tooltipTimeout.current);
}
function onTooltipShow() {
setTooltipVisible(true);
clearTimer();
tooltipTimeout.current = window.setTimeout(() => setTooltipVisible(false), 1500);
}
function onPrevClick() {
onTooltipShow();
hotbarStore.switchToPrevious();
}
function onNextClick() {
onTooltipShow();
hotbarStore.switchToNext();
}
function onMouseEvent(event: React.MouseEvent) {
clearTimer();
setTooltipVisible(event.type == "mouseenter");
}
return (
<div className={styles.HotbarSelector}>
<Icon
material="arrow_left"
className={cssNames(styles.Icon, styles.previous)}
onClick={onPrevClick}/>
<div className={styles.HotbarIndex}>
<Badge
id="hotbarIndex"
small
label={hotbarStore.getDisplayIndex(hotbarStore.getActive())}
onClick={() => openCommandOverlay(<HotbarSwitchCommand />)}
className={styles.Badge}
onMouseEnter={onMouseEvent}
onMouseLeave={onMouseEvent}
/>
<Tooltip
visible={tooltipVisible}
targetId="hotbarIndex"
preferredPositions={[TooltipPosition.TOP, TooltipPosition.TOP_LEFT]}
>
{hotbar.name}
</Tooltip>
</div>
<Icon
material="arrow_right"
className={styles.Icon}
onClick={onNextClick}
/>
</div>
);
});
export const HotbarSelector = withInjectables<Dependencies, HotbarSelectorProps>(NonInjectedHotbarSelector, {
getProps: (di, props) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
openCommandOverlay: di.inject(commandOverlayInjectable).open,
...props,
}),
});
@@ -0,0 +1,92 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { observer } from "mobx-react";
import { Select } from "../select";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import type { CommandOverlay } from "../command-palette";
import { HotbarAddCommand } from "./hotbar-add-command";
import { HotbarRemoveCommand } from "./hotbar-remove-command";
import { HotbarRenameCommand } from "./hotbar-rename-command";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import type { HotbarStore } from "../../../common/hotbars/store";
const hotbarAddAction = Symbol("hotbar-add");
const hotbarRemoveAction = Symbol("hotbar-remove");
const hotbarRenameAction = Symbol("hotbar-rename");
interface Dependencies {
hotbarStore: HotbarStore;
commandOverlay: CommandOverlay;
}
function ignoreIf<T>(check: boolean, menuItems: T[]): T[] {
return check ? [] : menuItems;
}
const NonInjectedHotbarSwitchCommand = observer(({
hotbarStore,
commandOverlay,
}: Dependencies) => (
<Select
id="switch-to-hotbar-input"
menuPortalTarget={null}
onChange={(option) => {
if (!option) {
return;
}
if (typeof option.value === "symbol") {
switch (option.value) {
case hotbarAddAction:
return commandOverlay.open(<HotbarAddCommand />);
case hotbarRemoveAction:
return commandOverlay.open(<HotbarRemoveCommand />);
case hotbarRenameAction:
return commandOverlay.open(<HotbarRenameCommand />);
}
} else {
hotbarStore.setActiveHotbar(option.value);
commandOverlay.close();
}
}}
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true}
options={[
...hotbarStore.hotbars.map(hotbar => ({
value: hotbar,
label: hotbarStore.getDisplayLabel(hotbar),
})),
{
value: hotbarAddAction,
label: "Add hotbar ...",
},
...ignoreIf(hotbarStore.hotbars.length > 1, [
{
value: hotbarRemoveAction,
label: "Remove hotbar ...",
},
]),
{
value: hotbarRenameAction,
label: "Rename hotbar ...",
},
]}
autoFocus={true}
escapeClearsValue={false}
isClearable={false}
placeholder="Switch to hotbar"
/>
));
export const HotbarSwitchCommand = withInjectables<Dependencies>(NonInjectedHotbarSwitchCommand, {
getProps: (di, props) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
commandOverlay: di.inject(commandOverlayInjectable),
...props,
}),
});