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

Add OnClickDecorator for UI components

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>
This commit is contained in:
Juho Heikka 2022-08-11 15:00:16 +03:00
parent 17ddee2bde
commit 396d08051f
39 changed files with 369 additions and 88 deletions

View File

@ -262,7 +262,7 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
</section>
<section>
<Button
<OpenLensButton
label={this.buttonLabel}
waiting={this.inProgress}
onClick={() => this.save()}

View File

@ -10,7 +10,7 @@ import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import installUpdateCountdownInjectable from "./install-update-countdown.injectable";
import { Dialog } from "../../components/dialog";
import { Button } from "../../components/button";
import { OpenLensButton } from "../../components/button";
import styles from "./force-update-modal.module.scss";
interface Dependencies {
@ -36,7 +36,7 @@ const NonInjectedForceUpdateModal = observer(
</div>
<div className={styles.footer}>
<Button
<OpenLensButton
primary
data-testid="update-now-from-must-update-immediately-modal"
onClick={restartAndInstallUpdate}
@ -49,7 +49,7 @@ const NonInjectedForceUpdateModal = observer(
data-testid="countdown-to-automatic-update"
/>
)
</Button>
</OpenLensButton>
</div>
</div>
</Dialog>

View File

@ -17,7 +17,7 @@ import { appEventBus } from "../../../common/app-event-bus/event-bus";
import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
import { docsUrl } from "../../../common/vars";
import { isDefined, iter } from "../../utils";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Notifications } from "../notifications";
import { SettingLayout } from "../layout/setting-layout";
import { MonacoEditor } from "../monaco-editor";
@ -138,7 +138,7 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
</>
)}
<div className="actions-panel">
<Button
<OpenLensButton
primary
disabled={this.kubeContexts.size === 0}
label={this.kubeContexts.size === 1 ? "Add cluster" : "Add clusters"}

View File

@ -11,7 +11,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { DrawerTitle } from "../drawer";
import { Notifications } from "../notifications";
import { Input } from "../input";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { configMapStore } from "./legacy-store";
import type { KubeObjectDetailsProps } from "../kube-object-details";
import { ConfigMap } from "../../../common/k8s-api/endpoints";
@ -104,7 +104,7 @@ export class ConfigMapDetails extends React.Component<ConfigMapDetailsProps> {
</div>
))
}
<Button
<OpenLensButton
primary
label="Save"
waiting={this.isSaving}

View File

@ -17,7 +17,7 @@ import type { IResourceQuotaValues } from "../../../common/k8s-api/endpoints";
import { resourceQuotaApi } from "../../../common/k8s-api/endpoints";
import { Select } from "../select";
import { Icon } from "../icon";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Notifications } from "../notifications";
import { NamespaceSelect } from "../+namespaces/namespace-select";
import { SubTitle } from "../layout/sub-title";
@ -218,7 +218,7 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
onKeyDown={this.onInputQuota}
className="box grow"
/>
<Button
<OpenLensButton
round
primary
onClick={this.setQuota}
@ -227,7 +227,7 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
material={this.quotaSelectValue && this.quotas[this.quotaSelectValue] ? "edit" : "add"}
tooltip="Set quota"
/>
</Button>
</OpenLensButton>
</div>
<div className="quota-entries">
{this.quotaEntries.map(([quota, value]) => (

View File

@ -4,9 +4,12 @@
*/
import React from "react";
import { render } from "@testing-library/react";
import { SecretDetails } from "../secret-details";
import { Secret, SecretType } from "../../../../common/k8s-api/endpoints";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import type { DiContainer } from "@ogre-tools/injectable";
jest.mock("../../kube-object-meta/kube-object-meta", () => ({
KubeObjectMeta: () => null,
@ -14,6 +17,14 @@ jest.mock("../../kube-object-meta/kube-object-meta", () => ({
describe("SecretDetails tests", () => {
let di: DiContainer;
let render: DiRender;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
});
it("should show the visibility toggle when the secret value is ''", () => {
const secret = new Secret({
apiVersion: "v1",
@ -30,6 +41,7 @@ describe("SecretDetails tests", () => {
},
type: SecretType.Opaque,
});
const result = render(<SecretDetails object={secret}/>);
expect(result.getByTestId("foobar-secret-entry").querySelector(".Icon")).toBeDefined();

View File

@ -10,7 +10,7 @@ import { autorun, observable, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Input } from "../input";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Notifications } from "../notifications";
import { base64, toggle } from "../../utils";
import { Icon } from "../icon";
@ -122,7 +122,7 @@ export class SecretDetails extends React.Component<SecretDetailsProps> {
<>
<DrawerTitle>Data</DrawerTitle>
{secrets.map(this.renderSecret)}
<Button
<OpenLensButton
primary
label="Save"
waiting={this.isSaving}

View File

@ -9,7 +9,7 @@ import {
disposer,
} from "../../../../common/utils";
import { Notifications } from "../../notifications";
import { Button } from "../../button";
import { OpenLensButton } from "../../button";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import React from "react";
@ -110,7 +110,7 @@ export const attemptInstall =
{` ${name}@${oldVersion} will be removed before installation.`}
</div>
</div>
<Button
<OpenLensButton
autoFocus
label="Install"
onClick={async () => {

View File

@ -6,7 +6,7 @@
import styles from "./install.module.scss";
import React from "react";
import { prevDefault } from "../../utils";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Icon } from "../icon";
import { observer } from "mobx-react";
import { Input, InputValidators } from "../input";
@ -77,7 +77,7 @@ const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
/>
</div>
<div>
<Button
<OpenLensButton
className={styles.button}
primary
label="Install"

View File

@ -12,7 +12,7 @@ import { Drawer, DrawerItem } from "../drawer";
import { autoBind, stopPropagation } from "../../utils";
import { MarkdownViewer } from "../markdown-viewer";
import { Spinner } from "../spinner";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Select } from "../select";
import { Badge } from "../badge";
import { Tooltip, withStyles } from "@material-ui/core";
@ -76,7 +76,7 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
<div className="intro-contents box grow">
<div className="description flex align-center justify-space-between" data-testid="selected-chart-description">
{selectedChart.getDescription()}
<Button
<OpenLensButton
primary
label="Install"
onClick={this.install}

View File

@ -14,7 +14,7 @@ import { observer } from "mobx-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { ConfigurationInput, MinimalResourceGroup, OnlyUserSuppliedValuesAreShownToggle, ReleaseDetailsModel } from "./release-details-model/release-details-model.injectable";
import releaseDetailsModelInjectable from "./release-details-model/release-details-model.injectable";
import { Button } from "../../button";
import { OpenLensButton } from "../../button";
import { kebabCase } from "lodash/fp";
import { Badge } from "../../badge";
import { SubTitle } from "../../layout/sub-title";
@ -64,7 +64,7 @@ const NonInjectedReleaseDetailsContent = observer(({ model }: Dependencies & Rel
<div className="flex gaps align-center">
<span>{model.release.chart}</span>
<Button
<OpenLensButton
primary
label="Upgrade"
className="box right upgrade"
@ -196,7 +196,7 @@ const ReleaseValues = observer(({ configuration, onlyUserSuppliedValuesAreShown
onChange={configuration.onChange}
/>
<Button
<OpenLensButton
primary
label="Save"
waiting={configuration.isSaving.get()}

View File

@ -11,7 +11,7 @@ import type { Service, ServicePort } from "../../../common/k8s-api/endpoints";
import { action, makeObservable, observable, reaction } from "mobx";
import { cssNames } from "../../utils";
import { Notifications } from "../notifications";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import type { ForwardedPort, PortForwardStore } from "../../port-forward";
import { predictProtocol } from "../../port-forward";
import { Spinner } from "../spinner";
@ -183,11 +183,11 @@ class NonInjectedServicePortComponent extends React.Component<ServicePortCompone
<span title="Open in a browser" onClick={() => this.portForward()}>
{port.toString()}
</span>
<Button primary onClick={portForwardAction}>
<OpenLensButton primary onClick={portForwardAction}>
{" "}
{this.isPortForwarded ? (this.isActive ? "Stop/Remove" : "Remove") : "Forward..."}
{" "}
</Button>
</OpenLensButton>
{this.waiting && (
<Spinner />
)}

View File

@ -17,7 +17,7 @@ import type { IObservableValue } from "mobx";
import { action } from "mobx";
import submitCustomHelmRepositoryInjectable from "./submit-custom-helm-repository.injectable";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
import { Button } from "../../../../button";
import { OpenLensButton } from "../../../../button";
import { Icon } from "../../../../icon";
import maximalCustomHelmRepoOptionsAreShownInjectable from "./maximal-custom-helm-repo-options-are-shown.injectable";
import { SubTitle } from "../../../../layout/sub-title";
@ -59,7 +59,7 @@ const NonInjectedActivationOfCustomHelmRepositoryDialogContent = observer(({ hel
onChange={action(v => helmRepo.url = v)}
data-testid="custom-helm-repository-url-input"
/>
<Button
<OpenLensButton
plain
className="accordion"
data-testid="toggle-maximal-options-for-custom-helm-repository-button"
@ -71,7 +71,7 @@ const NonInjectedActivationOfCustomHelmRepositoryDialogContent = observer(({ hel
tooltip="More"
material={maximalOptionsAreShown.get() ? "remove" : "add"}
/>
</Button>
</OpenLensButton>
{maximalOptionsAreShown.get() && (
<div data-testid="maximal-options-for-custom-helm-repository-dialog">

View File

@ -4,7 +4,7 @@
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import { Button } from "../../../../button";
import { OpenLensButton } from "../../../../button";
import showDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/show-dialog-for-adding-custom-helm-repository.injectable";
interface Dependencies {
@ -12,7 +12,7 @@ interface Dependencies {
}
const NonInjectedActivationOfCustomHelmRepositoryOpenButton = ({ showDialog }: Dependencies) => (
<Button
<OpenLensButton
primary
label="Add Custom Helm Repo"
onClick={showDialog}

View File

@ -11,7 +11,7 @@ import type { ContainerPort, Pod } from "../../../common/k8s-api/endpoints";
import { action, makeObservable, observable, reaction } from "mobx";
import { cssNames } from "../../utils";
import { Notifications } from "../notifications";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import type { ForwardedPort, PortForwardStore } from "../../port-forward";
import { predictProtocol } from "../../port-forward";
import { Spinner } from "../spinner";
@ -181,11 +181,11 @@ class NonInjectedPodContainerPort extends React.Component<PodContainerPortProps
<span title="Open in a browser" onClick={() => this.portForward()}>
{text}
</span>
<Button primary onClick={portForwardAction}>
<OpenLensButton primary onClick={portForwardAction}>
{" "}
{this.isPortForwarded ? (this.isActive ? "Stop/Remove" : "Remove") : "Forward..."}
{" "}
</Button>
</OpenLensButton>
{this.waiting && (
<Spinner />
)}

View File

@ -7,7 +7,7 @@ import "./add-remove-buttons.scss";
import React from "react";
import { cssNames } from "../../utils";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Icon } from "../icon";
export interface AddRemoveButtonsProps extends React.HTMLAttributes<any> {
@ -37,7 +37,7 @@ export class AddRemoveButtons extends React.PureComponent<AddRemoveButtonsProps>
]
.filter(button => button.onClick)
.map(({ icon, ...props }) => (
<Button
<OpenLensButton
key={icon}
big
round
@ -45,7 +45,7 @@ export class AddRemoveButtons extends React.PureComponent<AddRemoveButtonsProps>
{...props}
>
<Icon material={icon} />
</Button>
</OpenLensButton>
));
}

View File

@ -11,6 +11,7 @@ import randomColor from "randomcolor";
import GraphemeSplitter from "grapheme-splitter";
import type { SingleOrMany } from "../../utils";
import { cssNames, isDefined, iter } from "../../utils";
import { OpenLensButton } from "../button";
export interface AvatarProps {
title: string;
@ -77,7 +78,7 @@ export const Avatar = ({
onClick,
"data-testid": dataTestId,
}: AvatarProps) => (
<div
<OpenLensButton
className={cssNames(styles.Avatar, {
[styles.circle]: variant == "circle",
[styles.rounded]: variant == "rounded",
@ -105,5 +106,5 @@ export const Avatar = ({
/>
)
: children || getLabelFromTitle(title)}
</div>
</OpenLensButton>
);

View File

@ -7,7 +7,9 @@ import "./button.scss";
import type { ButtonHTMLAttributes } from "react";
import React from "react";
import { cssNames } from "../../utils";
import type { TooltipDecoratorProps } from "../tooltip";
import { withTooltip } from "../tooltip";
import { OnClickDecorated } from "../on-click-decorated/on-click-decorated";
export interface ButtonProps extends ButtonHTMLAttributes<any> {
label?: React.ReactNode;
@ -23,16 +25,22 @@ export interface ButtonProps extends ButtonHTMLAttributes<any> {
round?: boolean;
href?: string; // render as hyperlink
target?: "_blank"; // in case of using @href
Component?: React.ComponentType<any>;
}
const PlainButton = (props: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>) => <button {...props} />;
const PlainAnchor = (props: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => <a {...props} />;
export const Button = withTooltip((props: ButtonProps) => {
const {
waiting, label, primary, accent, plain, hidden, active, big,
round, outlined, light, children, ...btnProps
round, outlined, light, children, Component, ...btnProps
} = props;
if (hidden) return null;
const ButtonComponent = Component ?? (props.href ? PlainAnchor : PlainButton);
btnProps.className = cssNames("Button", btnProps.className, {
waiting, primary, accent, plain, active, big, round, outlined, light,
});
@ -40,18 +48,25 @@ export const Button = withTooltip((props: ButtonProps) => {
// render as link
if (props.href) {
return (
<a {...btnProps}>
<ButtonComponent
{...btnProps}>
{label}
{children}
</a>
</ButtonComponent>
);
}
// render as button
return (
<button type="button" {...btnProps}>
<ButtonComponent
type="button"
{...btnProps}>
{label}
{children}
</button>
</ButtonComponent>
);
});
export const OpenLensButton = (props: ButtonProps & TooltipDecoratorProps) => {
return <Button {...props} Component={(props) => <OnClickDecorated {...props} tagName={props.href ? "a" : "button"} />} />;
};

View File

@ -12,7 +12,7 @@ import { ipcRendererOn } from "../../../common/ipc";
import type { Cluster } from "../../../common/cluster/cluster";
import type { IClassName } from "../../utils";
import { isBoolean, hasTypedProperty, isObject, isString, cssNames } from "../../utils";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Icon } from "../icon";
import { Spinner } from "../spinner";
import type { KubeAuthUpdate } from "../../../common/cluster-types";
@ -134,7 +134,7 @@ class NonInjectedClusterStatus extends React.Component<ClusterStatusProps & Depe
if (this.hasErrors && !this.isReconnecting) {
return (
<>
<Button
<OpenLensButton
primary
label="Reconnect"
className="box center"

View File

@ -7,7 +7,7 @@ import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { onMultiSelectFor, Select } from "../../select/select";
import { Icon } from "../../icon/icon";
import { Button } from "../../button/button";
import { OpenLensButton } from "../../button/button";
import { SubTitle } from "../../layout/sub-title";
import type { Cluster } from "../../../../common/cluster/cluster";
import { observable, reaction, makeObservable } from "mobx";
@ -85,12 +85,12 @@ export class ClusterMetricsSetting extends React.Component<ClusterMetricsSetting
)}
themeName="lens"
/>
<Button
<OpenLensButton
primary
label="Hide all metrics"
onClick={this.onChangeButton}
/>
<Button
<OpenLensButton
primary
label="Reset"
onClick={this.reset}

View File

@ -12,7 +12,7 @@ import { observable, makeObservable, computed } from "mobx";
import { observer } from "mobx-react";
import { cssNames, noop, prevDefault } from "../../utils";
import type { ButtonProps } from "../button";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import type { DialogProps } from "../dialog";
import { Dialog } from "../dialog";
import { Icon } from "../icon";
@ -140,14 +140,14 @@ class NonInjectedConfirmDialog extends React.Component<ConfirmDialogProps & Depe
{message}
</div>
<div className="confirm-buttons">
<Button
<OpenLensButton
plain
className="cancel"
label={labelCancel}
onClick={prevDefault(this.close)}
{...cancelButtonProps}
/>
<Button
<OpenLensButton
autoFocus
primary
className="ok"

View File

@ -9,7 +9,7 @@ import { action, observable } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { saveKubeconfig } from "./save-config";
import { Notifications } from "../notifications";
import { Dialog } from "../dialog";
@ -226,12 +226,12 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
)}
</div>
<div className={styles.dialogButtons}>
<Button
<OpenLensButton
onClick={this.close}
plain
label="Cancel"
/>
<Button
<OpenLensButton
onClick={() => this.onDelete(state)}
autoFocus
accent

View File

@ -10,7 +10,7 @@ import type { DialogProps } from "../dialog";
import { Dialog } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { Notifications } from "../notifications";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Icon } from "../icon";
import { clipboard } from "electron";
import { kebabCase } from "lodash/fp";
@ -35,7 +35,7 @@ export function LogsDialog({ title, logs, ...dialogProps }: LogsDialogProps) {
scrollable={false}
customButtons={(
<div className="buttons flex gaps align-center justify-space-between">
<Button
<OpenLensButton
plain
onClick={() => {
clipboard.writeText(logs);
@ -44,10 +44,10 @@ export function LogsDialog({ title, logs, ...dialogProps }: LogsDialogProps) {
>
<Icon material="assignment"/>
{" Copy to clipboard"}
</Button>
<Button plain onClick={dialogProps.close}>
</OpenLensButton>
<OpenLensButton plain onClick={dialogProps.close}>
Close
</Button>
</OpenLensButton>
</div>
)}
>

View File

@ -10,7 +10,7 @@ import React, { Component } from "react";
import { computed, observable, reaction, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { cssNames } from "../../utils";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Icon } from "../icon";
import { Spinner } from "../spinner";
import type { DockStore, TabId } from "./dock/store";
@ -146,13 +146,13 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
)}
{showButtons && (
<>
<Button
<OpenLensButton
plain
label="Cancel"
onClick={close}
data-testid={this.props.cancelTestId}
/>
<Button
<OpenLensButton
active
outlined={showSubmitClose}
primary={!showSubmitClose}// one button always should be primary (blue)
@ -162,7 +162,7 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
data-testid={this.props.submitTestId}
/>
{showSubmitClose && (
<Button
<OpenLensButton
primary
active
label={`${submitLabel} & Close`}

View File

@ -13,7 +13,7 @@ import { Badge } from "../../badge";
import { NamespaceSelect } from "../../+namespaces/namespace-select";
import { prevDefault } from "../../../utils";
import { Icon } from "../../icon";
import { Button } from "../../button";
import { OpenLensButton } from "../../button";
import { LogsDialog } from "../../dialog/logs-dialog";
import { Select } from "../../select";
import { Input } from "../../input";
@ -46,14 +46,14 @@ const NonInjectedInstallChart = observer(
</p>
<p>Installation complete!</p>
<div className="flex gaps align-center">
<Button
<OpenLensButton
autoFocus
primary
label="View Helm Release"
onClick={prevDefault(model.navigateToInstalledRelease)}
data-testid={`show-release-${installed.release.name}-for-${tabId}`}
/>
<Button
<OpenLensButton
plain
active
label="Show Notes"

View File

@ -8,7 +8,7 @@ import "./error-boundary.scss";
import type { ErrorInfo } from "react";
import React from "react";
import { observer } from "mobx-react";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { navigation } from "../../navigation";
import { issuesTrackerUrl, slackUrl } from "../../../common/vars";
import type { SingleOrMany } from "../../utils";
@ -75,7 +75,7 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, State> {
{error.stack}
</code>
</div>
<Button
<OpenLensButton
className="box self-flex-start"
primary
label="Back"

View File

@ -10,7 +10,7 @@ import { observer } from "mobx-react";
import yaml from "js-yaml";
import type { ServiceAccount } from "../../../common/k8s-api/endpoints";
import { cssNames, saveFileDialog } from "../../utils";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import type { DialogProps } from "../dialog";
import { Dialog } from "../dialog";
import { Icon } from "../icon";
@ -81,21 +81,21 @@ export class KubeConfigDialog extends React.Component<KubeConfigDialogProps> {
<WizardStep
customButtons={(
<div className="actions flex gaps">
<Button plain onClick={this.copyToClipboard}>
<OpenLensButton plain onClick={this.copyToClipboard}>
<Icon material="assignment"/>
{" Copy to clipboard"}
</Button>
<Button plain onClick={this.download}>
</OpenLensButton>
<OpenLensButton plain onClick={this.download}>
<Icon material="cloud_download"/>
{" Download file"}
</Button>
<Button
</OpenLensButton>
<OpenLensButton
plain
className="box right"
onClick={this.close}
>
Close
</Button>
</OpenLensButton>
</div>
)}
prev={this.close}

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`foobar given A -element 1`] = `
<body>
<div>
<a
data-testid="some-anchor"
/>
</div>
</body>
`;

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`foobar given A -element 1`] = `
<body>
<div>
<a
data-testid="some-anchor"
/>
</div>
</body>
`;

View File

@ -0,0 +1,104 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import { OpenLensButton } from "../button";
import React from "react";
import { fireEvent } from "@testing-library/dom";
import type { RenderResult } from "@testing-library/react";
import { onClickDecoratorInjectionToken } from "./on-click-decorator-injection-token";
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { OnClickDecorated } from "./on-click-decorated";
describe("foobar", () => {
let di: DiContainer;
let render: DiRender;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
});
it("given A -element", () => {
const rendered = render(
<OnClickDecorated
tagName="a"
onClick={() => {}}
data-testid="some-anchor"
/>,
);
expect(rendered.baseElement).toMatchSnapshot();
});
describe("given button", () => {
let onClickMock: jest.Mock;
let firstHigherOrderFunctionMock: jest.Mock;
let secondHigherOrderFunctionMock: jest.Mock;
let rendered: RenderResult;
beforeEach(() => {
onClickMock = jest.fn();
firstHigherOrderFunctionMock = jest.fn((x) => x);
secondHigherOrderFunctionMock = jest.fn((x) => x);
const someInjectable = getInjectable({
id: "some-injectable",
instantiate: () => ({
onClick: firstHigherOrderFunctionMock,
}),
injectionToken: onClickDecoratorInjectionToken,
});
const someOtherInjectable = getInjectable({
id: "some-other-injectable",
instantiate: () => ({
onClick: secondHigherOrderFunctionMock,
}),
injectionToken: onClickDecoratorInjectionToken,
});
di.register(someInjectable, someOtherInjectable);
rendered = render(
<OpenLensButton
primary
label="Add Custom Helm Repo"
onClick={onClickMock}
data-testid="add-custom-helm-repo-button"
/>,
);
});
describe("when button is clicked", () => {
beforeEach(() => {
fireEvent.click(rendered.getByTestId("add-custom-helm-repo-button"));
});
it("calls the original onClick", () => {
expect(onClickMock).toHaveBeenCalled();
});
it("calls the original onClick with an argument", () => {
expect(onClickMock).toHaveBeenCalledWith(expect.any(Object));
});
it("calls the first higher order function", () => {
expect(firstHigherOrderFunctionMock).toHaveBeenCalled();
});
it("calls the second higher order function", () => {
expect(secondHigherOrderFunctionMock).toHaveBeenCalled();
});
});
});
});

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import { flow } from "lodash/fp";
import type { HTMLAttributes, MouseEventHandler } from "react";
import React from "react";
import type { OnClickDecorator } from "./on-click-decorator-injection-token";
import { onClickDecoratorInjectionToken } from "./on-click-decorator-injection-token";
interface Dependencies {
decorators: OnClickDecorator[];
}
interface ClickDecoratedProps extends HTMLAttributes<any> {
onClick?: MouseEventHandler<any>;
tagName: "button" | "a";
}
const NonInjectedOnClickDecorated = ({ decorators, tagName: TagName, onClick, ...props }: Dependencies & ClickDecoratedProps) => {
const onClickDecorators = decorators.map(decorator => decorator.onClick);
const decoratedOnClick = onClick ? flow(...onClickDecorators)(onClick) : undefined;
return <TagName {...props} onClick={decoratedOnClick} />;
};
export const OnClickDecorated = withInjectables<Dependencies, ClickDecoratedProps>(
NonInjectedOnClickDecorated,
{
getProps: (di, props) => ({
decorators: di.injectMany(onClickDecoratorInjectionToken),
...props,
}),
},
);

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MouseEventHandler } from "react";
import type React from "react";
type OnClick = (toBeDecorated: MouseEventHandler<any>) => (event: React.MouseEvent) => void;
export interface OnClickDecorator {
onClick: OnClick;
}
export const onClickDecoratorInjectionToken = getInjectionToken<OnClickDecorator>({
id: "onclick-decorator-injection-token",
});

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
import type { EventEmitter } from "../../../common/event-emitter";
import capitalize from "lodash/capitalize";
import { onClickDecoratorInjectionToken } from "./on-click-decorator-injection-token";
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
function getEventName(el: HTMLElement, pathname: string, parentLevels = 3) {
let headers: string[] = [];
let parent = el;
const path = pathname.split("/");
headers.push(capitalize(path[path.length-1]));
for (let i = 0; i < parentLevels; i++) {
if (parent.parentElement) {
parent = parent.parentElement;
}
}
const nodelist = parent.querySelectorAll("h1, h2, h3, h4, h5, h6, .header");
nodelist.forEach(node => node.textContent && headers.push(node.textContent));
headers = [...new Set(headers)];
if (el?.textContent) {
headers.push(el.textContent);
}
const eventName = headers.join(" ");
return eventName;
}
function captureMouseEvent(eventBus: EventEmitter<[AppEvent]>, event: React.MouseEvent) {
const name = getEventName(event.target as HTMLElement, window.location.pathname);
eventBus.emit({
name,
action: capitalize(event.type),
destination: "AutoCapture",
});
}
const onClickTelemetryDecoratorInjectable = getInjectable({
id: "on-click-telemetry-decorator",
instantiate: (di) => ({
onClick: (toBeDecorated) => {
return (event) => {
captureMouseEvent(di.inject(appEventBusInjectable), event);
return toBeDecorated(event);
};
},
}),
injectionToken: onClickDecoratorInjectionToken,
});
export default onClickTelemetryDecoratorInjectable;

View File

@ -7,7 +7,7 @@ import type { FileFilter, OpenDialogOptions } from "electron";
import { observer } from "mobx-react";
import React from "react";
import { cssNames } from "../../utils";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { requestOpenFilePickingDialog } from "../../ipc";
export interface PathPickOpts {
@ -53,7 +53,7 @@ export class PathPicker extends React.Component<PathPickerProps> {
const { className, label, disabled } = this.props;
return (
<Button
<OpenLensButton
primary
label={label}
disabled={disabled}

View File

@ -6,7 +6,7 @@
import "./wizard.scss";
import React from "react";
import { cssNames, prevDefault } from "../../utils";
import { Button } from "../button";
import { OpenLensButton } from "../button";
import { Stepper } from "../stepper";
import { SubTitle } from "../layout/sub-title";
import { Spinner } from "../spinner";
@ -238,7 +238,7 @@ export class WizardStep<D> extends React.Component<WizardStepProps<D>, WizardSte
{customButtons ?? (
<div className="buttons flex gaps align-center">
{moreButtons}
<Button
<OpenLensButton
className="back-btn"
plain
label={prevLabel || (isFirst?.() ? "Cancel" : "Back")}
@ -246,7 +246,7 @@ export class WizardStep<D> extends React.Component<WizardStepProps<D>, WizardSte
onClick={this.prev}
data-testid={testIdForPrev}
/>
<Button
<OpenLensButton
primary
type="submit"
label={nextLabel || (isLast?.() ? "Submit" : "Next")}

View File

@ -73,6 +73,9 @@ import forceUpdateModalRootFrameComponentInjectable from "./application-update/f
import legacyOnChannelListenInjectable from "./ipc/legacy-channel-listen.injectable";
import getEntitySettingCommandsInjectable from "./components/command-palette/registered-commands/get-entity-setting-commands.injectable";
import storageSaveDelayInjectable from "./utils/create-storage/storage-save-delay.injectable";
import appEventBusInjectable from "../common/app-event-bus/app-event-bus.injectable";
import { EventEmitter } from "./utils";
import type { AppEvent } from "../common/app-event-bus/event-bus";
export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => {
const {
@ -227,6 +230,8 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
info: noop,
silly: noop,
}));
di.override(appEventBusInjectable, () => new EventEmitter<[AppEvent]>());
}
return di;

View File

@ -7,7 +7,7 @@ import navigateToEntitySettingsInjectable from "../../common/front-end-routing/r
import type { ListNamespaceForbiddenArgs } from "../../common/ipc/cluster";
import { Notifications } from "../components/notifications";
import { ClusterStore } from "../../common/cluster-store/cluster-store";
import { Button } from "../components/button";
import { OpenLensButton } from "../components/button";
import type { IpcRendererEvent } from "electron";
import React from "react";
import notificationsStoreInjectable from "../components/notifications/notifications-store.injectable";
@ -56,7 +56,7 @@ const listNamespacesForbiddenHandlerInjectable = getInjectable({
{" does not have permissions to list namespaces. Please add the namespaces you have access to."}
</p>
<div className="flex gaps row align-left box grow">
<Button
<OpenLensButton
active
outlined
label="Go to Accessible Namespaces Settings"

View File

@ -4,7 +4,7 @@
*/
import React from "react";
import { Button } from "../components/button";
import { OpenLensButton } from "../components/button";
import { Notifications } from "../components/notifications";
import type { NavigateToPortForwards } from "../../common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable";
import type { NotificationsStore } from "../components/notifications/notifications.store";
@ -30,7 +30,7 @@ export const aboutPortForwarding = ({
You can manage your port forwards on the Port Forwarding Page.
</p>
<div className="flex gaps row align-left box grow">
<Button
<OpenLensButton
active
outlined
label="Go to Port Forwarding"
@ -71,7 +71,7 @@ export const notifyErrorPortForwarding = ({
{msg}
</p>
<div className="flex gaps row align-left box grow">
<Button
<OpenLensButton
active
outlined
label="Check Port Forwarding"