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

Merge remote-tracking branch 'origin/master' into react-18-upgrade

This commit is contained in:
Roman 2022-04-06 17:30:57 +03:00
commit f3b06cfc76
14 changed files with 157 additions and 246 deletions

View File

@ -3,15 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import React from "react";
import { CatalogCategory, CatalogCategorySpec } from "../catalog"; import { CatalogCategory, CatalogCategorySpec } from "../catalog";
class TestCatalogCategory extends CatalogCategory { class TestCatalogCategoryWithoutBadge extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1"; public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory"; public readonly kind = "CatalogCategory";
public metadata = { public metadata = {
name: "Test Category", name: "Test Category",
icon: "", icon: "",
}; };
public spec: CatalogCategorySpec = { public spec: CatalogCategorySpec = {
group: "entity.k8slens.dev", group: "entity.k8slens.dev",
versions: [], versions: [],
@ -21,10 +24,28 @@ class TestCatalogCategory extends CatalogCategory {
}; };
} }
class TestCatalogCategoryWithBadge extends TestCatalogCategoryWithoutBadge {
getBadge() {
return (<div>Test Badge</div>);
}
}
describe("CatalogCategory", () => { describe("CatalogCategory", () => {
it("returns name", () => { it("returns name", () => {
const category = new TestCatalogCategory(); const category = new TestCatalogCategoryWithoutBadge();
expect(category.getName()).toEqual("Test Category"); expect(category.getName()).toEqual("Test Category");
}); });
it("doesn't return badge by default", () => {
const category = new TestCatalogCategoryWithoutBadge();
expect(category.getBadge()).toEqual(null);
});
it("returns a badge", () => {
const category = new TestCatalogCategoryWithBadge();
expect(category.getBadge()).toBeTruthy();
});
}); });

View File

@ -180,6 +180,15 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
return this.metadata.name; return this.metadata.name;
} }
/**
* Get the badge of this category.
* Defaults to no badge.
* The badge is displayed next to the Category name in the Catalog Category menu
*/
public getBadge(): React.ReactNode {
return null;
}
/** /**
* Add a filter for menu items of catalogAddMenu * Add a filter for menu items of catalogAddMenu
* @param fn The function that should return a truthy value if that menu item should be displayed * @param fn The function that should return a truthy value if that menu item should be displayed

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { render, screen } from "@testing-library/react";
import React from "react";
import "@testing-library/jest-dom/extend-expect";
import { CatalogCategory, CatalogCategorySpec } from "../../../../common/catalog";
import { CatalogCategoryLabel } from "../catalog-category-label";
class CatalogCategoryWithoutBadge extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
name: "Test Category",
icon: "",
};
public spec: CatalogCategorySpec = {
group: "entity.k8slens.dev",
versions: [],
names: {
kind: "Test",
},
};
}
class CatalogCategoryWithBadge extends CatalogCategoryWithoutBadge {
getBadge() {
return (<div>Test Badge</div>);
}
}
describe("CatalogCategoryLabel", () => {
it("renders without a badge", async () => {
const category = new CatalogCategoryWithoutBadge();
render(<CatalogCategoryLabel category={category}/>);
expect(await screen.findByText("Test Category")).toBeInTheDocument();
});
it("renders with a badge", async () => {
const category = new CatalogCategoryWithBadge();
render(<CatalogCategoryLabel category={category}/>);
expect(await screen.findByText("Test Category")).toBeInTheDocument();
expect(await screen.findByText("Test Badge")).toBeInTheDocument();
});
});

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { CatalogCategory } from "../../../common/catalog/catalog-entity";
interface CatalogCategoryLabelProps {
category: CatalogCategory;
}
/**
* Display label for Catalog Category for the Catalog menu
*/
export const CatalogCategoryLabel = ({ category }: CatalogCategoryLabelProps) => {
const badge = category.getBadge();
return (
<div className="flex">
<div>{category.metadata.name}</div>
{badge ? (<div className="flex items-center">{badge}</div>) : null}
</div>
);
};

View File

@ -14,6 +14,7 @@ import { StylesProvider } from "@material-ui/core";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import type { CatalogCategory } from "../../api/catalog-entity"; import type { CatalogCategory } from "../../api/catalog-entity";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CatalogCategoryLabel } from "./catalog-category-label";
export interface CatalogMenuProps { export interface CatalogMenuProps {
activeItem: string; activeItem: string;
@ -66,7 +67,7 @@ export const CatalogMenu = observer((props: CatalogMenuProps) => {
icon={getCategoryIcon(category)} icon={getCategoryIcon(category)}
key={category.getId()} key={category.getId()}
nodeId={category.getId()} nodeId={category.getId()}
label={category.metadata.name} label={<CatalogCategoryLabel category={category}/>}
data-testid={`${category.getId()}-tab`} data-testid={`${category.getId()}-tab`}
onClick={() => props.onItemClick(category.getId())} onClick={() => props.onItemClick(category.getId())}
/> />

View File

@ -83,19 +83,23 @@ export class Animate extends React.Component<AnimateProps> {
} }
render() { render() {
if (!this.isVisible) {
return null;
}
const { name, enterDuration, leaveDuration } = this.props; const { name, enterDuration, leaveDuration } = this.props;
const contentElem = this.contentElem; const contentElem = this.contentElem;
const durations = { const cssVarsForAnimation = {
"--enter-duration": `${enterDuration}ms`, "--enter-duration": `${enterDuration}ms`,
"--leave-duration": `${leaveDuration}ms`, "--leave-duration": `${leaveDuration}ms`,
} as React.CSSProperties; } as React.CSSProperties;
return React.cloneElement(contentElem, { return React.cloneElement(contentElem, {
className: cssNames("Animate", name, contentElem.props.className, this.statusClassName), className: cssNames("Animate", name, contentElem.props.className, this.statusClassName),
children: this.isVisible ? contentElem.props.children : null, children: contentElem.props.children,
style: { style: {
...contentElem.props.style, ...contentElem.props.style,
...durations, ...cssVarsForAnimation,
}, },
}); });
} }

View File

@ -44,9 +44,14 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOv
tooltipTimeout.current = setTimeout(() => setTooltipVisible(false), 1500); tooltipTimeout.current = setTimeout(() => setTooltipVisible(false), 1500);
} }
function onArrowClick(switchTo: () => void) { function onPrevClick() {
onTooltipShow(); onTooltipShow();
switchTo(); hotbarStore.switchToPrevious();
}
function onNextClick() {
onTooltipShow();
hotbarStore.switchToNext();
} }
function onMouseEvent(event: React.MouseEvent) { function onMouseEvent(event: React.MouseEvent) {
@ -56,11 +61,7 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOv
return ( return (
<div className={styles.HotbarSelector}> <div className={styles.HotbarSelector}>
<Icon <Icon material="play_arrow" className={cssNames(styles.Icon, styles.previous)} onClick={onPrevClick}/>
material="play_arrow"
className={cssNames(styles.Icon, styles.previous)}
onClick={() => onArrowClick(hotbarStore.switchToPrevious)}
/>
<div className={styles.HotbarIndex}> <div className={styles.HotbarIndex}>
<Badge <Badge
id="hotbarIndex" id="hotbarIndex"
@ -79,7 +80,7 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOv
{hotbar.name} {hotbar.name}
</Tooltip> </Tooltip>
</div> </div>
<Icon material="play_arrow" className={styles.Icon} onClick={() => onArrowClick(hotbarStore.switchToNext)} /> <Icon material="play_arrow" className={styles.Icon} onClick={onNextClick} />
</div> </div>
); );
}); });

View File

@ -37,10 +37,6 @@ exports[`kube-object-menu given kube object renders 1`] = `
</ul> </ul>
</div> </div>
</div> </div>
<div
class="Animate opacity-scale Dialog flex center ConfirmDialog modal"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
/>
</body> </body>
`; `;

View File

@ -16,58 +16,6 @@ exports[`kube-object-status-icon given info and warning statuses are present, wh
<div /> <div />
</i> </i>
</div> </div>
<div
class="Tooltip narrow formatter"
>
<div
class="KubeObjectStatusTooltip"
>
<div
class="level warning"
>
<span
class="title"
>
Warning
</span>
<div
class="status msg"
>
-
Some warning status for some-name
<span
class="age"
>
·
2d
</span>
</div>
</div>
<div
class="level info"
>
<span
class="title"
>
Info
</span>
<div
class="status msg"
>
-
Some info status for some-name
<span
class="age"
>
·
2d
</span>
</div>
</div>
</div>
</div>
</body> </body>
`; `;
@ -87,36 +35,6 @@ exports[`kube-object-status-icon given level "critical" status, when rendered, r
<div /> <div />
</i> </i>
</div> </div>
<div
class="Tooltip narrow formatter"
>
<div
class="KubeObjectStatusTooltip"
>
<div
class="level error"
>
<span
class="title"
>
Critical
</span>
<div
class="status msg"
>
-
Some critical status for some-name
<span
class="age"
>
·
2d
</span>
</div>
</div>
</div>
</div>
</body> </body>
`; `;
@ -136,36 +54,6 @@ exports[`kube-object-status-icon given level "info" status, when rendered, rende
<div /> <div />
</i> </i>
</div> </div>
<div
class="Tooltip narrow formatter"
>
<div
class="KubeObjectStatusTooltip"
>
<div
class="level info"
>
<span
class="title"
>
Info
</span>
<div
class="status msg"
>
-
Some info status for some-name
<span
class="age"
>
·
2d
</span>
</div>
</div>
</div>
</div>
</body> </body>
`; `;
@ -185,36 +73,6 @@ exports[`kube-object-status-icon given level "warning" status, when rendered, re
<div /> <div />
</i> </i>
</div> </div>
<div
class="Tooltip narrow formatter"
>
<div
class="KubeObjectStatusTooltip"
>
<div
class="level warning"
>
<span
class="title"
>
Warning
</span>
<div
class="status msg"
>
-
Some warning status for some-name
<span
class="age"
>
·
2d
</span>
</div>
</div>
</div>
</div>
</body> </body>
`; `;
@ -254,79 +112,5 @@ exports[`kube-object-status-icon given status for all levels is present, when re
<div /> <div />
</i> </i>
</div> </div>
<div
class="Tooltip narrow formatter"
>
<div
class="KubeObjectStatusTooltip"
>
<div
class="level error"
>
<span
class="title"
>
Critical
</span>
<div
class="status msg"
>
-
Some critical status for some-name
<span
class="age"
>
·
2d
</span>
</div>
</div>
<div
class="level warning"
>
<span
class="title"
>
Warning
</span>
<div
class="status msg"
>
-
Some warning status for some-name
<span
class="age"
>
·
2d
</span>
</div>
</div>
<div
class="level info"
>
<span
class="title"
>
Info
</span>
<div
class="status msg"
>
-
Some info status for some-name
<span
class="age"
>
·
2d
</span>
</div>
</div>
</div>
</div>
</body> </body>
`; `;

View File

@ -93,7 +93,6 @@ export class Menu extends React.Component<MenuProps, State> {
this.opener.addEventListener(this.props.toggleEvent, this.toggle); this.opener.addEventListener(this.props.toggleEvent, this.toggle);
this.opener.addEventListener("keydown", this.onKeyDown); this.opener.addEventListener("keydown", this.onKeyDown);
} }
this.elem.addEventListener("keydown", this.onKeyDown);
window.addEventListener("resize", this.onWindowResize); window.addEventListener("resize", this.onWindowResize);
window.addEventListener("click", this.onClickOutside, true); window.addEventListener("click", this.onClickOutside, true);
window.addEventListener("scroll", this.onScrollOutside, true); window.addEventListener("scroll", this.onScrollOutside, true);
@ -106,7 +105,6 @@ export class Menu extends React.Component<MenuProps, State> {
this.opener.removeEventListener(this.props.toggleEvent, this.toggle); this.opener.removeEventListener(this.props.toggleEvent, this.toggle);
this.opener.removeEventListener("keydown", this.onKeyDown); this.opener.removeEventListener("keydown", this.onKeyDown);
} }
this.elem.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("resize", this.onWindowResize); window.removeEventListener("resize", this.onWindowResize);
window.removeEventListener("click", this.onClickOutside, true); window.removeEventListener("click", this.onClickOutside, true);
window.removeEventListener("scroll", this.onScrollOutside, true); window.removeEventListener("scroll", this.onScrollOutside, true);
@ -218,7 +216,7 @@ export class Menu extends React.Component<MenuProps, State> {
} }
} }
onKeyDown(evt: KeyboardEvent) { onKeyDown(evt: React.KeyboardEvent | KeyboardEvent) {
if (!this.isOpen) return; if (!this.isOpen) return;
switch (evt.code) { switch (evt.code) {
@ -330,6 +328,7 @@ export class Menu extends React.Component<MenuProps, State> {
left: this.state?.menuStyle?.left, left: this.state?.menuStyle?.left,
top: this.state?.menuStyle?.top, top: this.state?.menuStyle?.top,
}} }}
onKeyDown={this.onKeyDown}
> >
{menuItems} {menuItems}
</ul> </ul>

View File

@ -9,7 +9,7 @@ import React from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { boundMethod, cssNames, IClassName } from "../../utils"; import { boundMethod, cssNames, IClassName } from "../../utils";
import { observable, makeObservable } from "mobx"; import { observable, makeObservable, action } from "mobx";
export enum TooltipPosition { export enum TooltipPosition {
TOP = "top", TOP = "top",
@ -54,7 +54,8 @@ export class Tooltip extends React.Component<TooltipProps> {
@observable.ref elem: HTMLElement; @observable.ref elem: HTMLElement;
@observable activePosition: TooltipPosition; @observable activePosition: TooltipPosition;
@observable isVisible = false; @observable isVisible = this.props.visible ?? false;
@observable isContentVisible = false; // animation manager
constructor(props: TooltipProps) { constructor(props: TooltipProps) {
super(props); super(props);
@ -87,15 +88,16 @@ export class Tooltip extends React.Component<TooltipProps> {
this.hoverTarget.removeEventListener("mouseleave", this.onLeaveTarget); this.hoverTarget.removeEventListener("mouseleave", this.onLeaveTarget);
} }
@boundMethod @action.bound
protected onEnterTarget() { protected onEnterTarget() {
this.isVisible = true; this.isVisible = true;
this.refreshPosition(); requestAnimationFrame(action(() => this.isContentVisible = true));
} }
@boundMethod @action.bound
protected onLeaveTarget() { protected onLeaveTarget() {
this.isVisible = false; this.isVisible = false;
this.isContentVisible = false;
} }
@boundMethod @boundMethod
@ -103,6 +105,10 @@ export class Tooltip extends React.Component<TooltipProps> {
const { preferredPositions } = this.props; const { preferredPositions } = this.props;
const { elem, targetElem } = this; const { elem, targetElem } = this;
if (!elem) {
return;
}
let positions = new Set<TooltipPosition>([ let positions = new Set<TooltipPosition>([
TooltipPosition.RIGHT, TooltipPosition.RIGHT,
TooltipPosition.BOTTOM, TooltipPosition.BOTTOM,
@ -150,6 +156,10 @@ export class Tooltip extends React.Component<TooltipProps> {
} }
protected setPosition(pos: { left: number; top: number }) { protected setPosition(pos: { left: number; top: number }) {
if (!this.elem) {
return;
}
const elemStyle = this.elem.style; const elemStyle = this.elem.style;
elemStyle.left = `${pos.left}px`; elemStyle.left = `${pos.left}px`;
@ -214,13 +224,17 @@ export class Tooltip extends React.Component<TooltipProps> {
} }
render() { render() {
const { style, formatters, usePortal, children, visible } = this.props; if (!this.isVisible) {
return null;
}
const { style, formatters, usePortal, children } = this.props;
const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, { const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, {
visible: visible ?? this.isVisible, visible: this.isContentVisible,
formatter: !!formatters, formatter: !!formatters,
}); });
const tooltip = ( const tooltip = (
<div className={className} style={style} ref={this.bindRef}> <div className={className} style={style} ref={this.bindRef} role="tooltip">
{children} {children}
</div> </div>
); );

View File

@ -29,6 +29,7 @@
"sidebarActiveColor": "#ffffff", "sidebarActiveColor": "#ffffff",
"sidebarSubmenuActiveColor": "#ffffff", "sidebarSubmenuActiveColor": "#ffffff",
"sidebarItemHoverBackground": "#3a3e44", "sidebarItemHoverBackground": "#3a3e44",
"badgeBackgroundColor": "#ffba44",
"buttonPrimaryBackground": "#3d90ce", "buttonPrimaryBackground": "#3d90ce",
"buttonDefaultBackground": "#414448", "buttonDefaultBackground": "#414448",
"buttonLightBackground": "#f1f1f1", "buttonLightBackground": "#f1f1f1",

View File

@ -29,6 +29,7 @@
"sidebarSubmenuActiveColor": "#3d90ce", "sidebarSubmenuActiveColor": "#3d90ce",
"sidebarBackground": "#e8e8e8", "sidebarBackground": "#e8e8e8",
"sidebarItemHoverBackground": "#f0f2f5", "sidebarItemHoverBackground": "#f0f2f5",
"badgeBackgroundColor": "#ffba44",
"buttonPrimaryBackground": "#3d90ce", "buttonPrimaryBackground": "#3d90ce",
"buttonDefaultBackground": "#414448", "buttonDefaultBackground": "#414448",
"buttonLightBackground": "#f1f1f1", "buttonLightBackground": "#f1f1f1",

View File

@ -29,6 +29,7 @@
--sidebarActiveColor: #ffffff; --sidebarActiveColor: #ffffff;
--sidebarSubmenuActiveColor: #ffffff; --sidebarSubmenuActiveColor: #ffffff;
--sidebarItemHoverBackground: #3a3e44; --sidebarItemHoverBackground: #3a3e44;
--badgeBackgroundColor: #ffba44;
--buttonPrimaryBackground: #3d90ce; --buttonPrimaryBackground: #3d90ce;
--buttonDefaultBackground: #414448; --buttonDefaultBackground: #414448;
--buttonLightBackground: #f1f1f1; --buttonLightBackground: #f1f1f1;
@ -73,6 +74,8 @@
--dockEditorComment: #808080; --dockEditorComment: #808080;
--dockEditorActiveLineBackground: #3a3d41; --dockEditorActiveLineBackground: #3a3d41;
--dockBadgeBackground: #36393e; --dockBadgeBackground: #36393e;
--dockTabBorderColor: #43424d;
--dockTabActiveBackground: #3a3e45;
--logsBackground: #000000; --logsBackground: #000000;
--logsForeground: #ffffff; --logsForeground: #ffffff;
--logRowHoverBackground: #35373a; --logRowHoverBackground: #35373a;
@ -125,7 +128,7 @@
--inputControlHoverBorder: #474a4f; --inputControlHoverBorder: #474a4f;
--lineProgressBackground: #414448; --lineProgressBackground: #414448;
--radioActiveBackground: #36393e; --radioActiveBackground: #36393e;
--menuActiveBackground: #36393e; --menuActiveBackground: #3d90ce;
--menuSelectedOptionBgc: #36393e; --menuSelectedOptionBgc: #36393e;
--canvasBackground: #24292e; --canvasBackground: #24292e;
--scrollBarColor: #5f6064; --scrollBarColor: #5f6064;