diff --git a/extensions/metrics-cluster-feature/src/metrics-settings.tsx b/extensions/metrics-cluster-feature/src/metrics-settings.tsx index 3f20a80df1..b3052fbf37 100644 --- a/extensions/metrics-cluster-feature/src/metrics-settings.tsx +++ b/extensions/metrics-cluster-feature/src/metrics-settings.tsx @@ -262,7 +262,7 @@ export class MetricsSettings extends React.Component {
- + diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 6630420ad6..6fd648f9e1 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -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 { )}
-
)) } - +
{this.quotaEntries.map(([quota, value]) => ( diff --git a/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx b/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx index 6903e853bc..a38f498738 100644 --- a/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx +++ b/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx @@ -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(); expect(result.getByTestId("foobar-secret-entry").querySelector(".Icon")).toBeDefined(); diff --git a/src/renderer/components/+config-secrets/secret-details.tsx b/src/renderer/components/+config-secrets/secret-details.tsx index 2176b7c1ce..3710f1d1ce 100644 --- a/src/renderer/components/+config-secrets/secret-details.tsx +++ b/src/renderer/components/+config-secrets/secret-details.tsx @@ -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 { <> Data {secrets.map(this.renderSecret)} -
- + {this.waiting && ( )} diff --git a/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-dialog-content.tsx b/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-dialog-content.tsx index 091dce407f..54d84fdd0c 100644 --- a/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-dialog-content.tsx +++ b/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-dialog-content.tsx @@ -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" /> - + {maximalOptionsAreShown.get() && (
diff --git a/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-open-button.tsx b/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-open-button.tsx index 99f16a83ed..a3f2243263 100644 --- a/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-open-button.tsx +++ b/src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-open-button.tsx @@ -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) => ( - + {this.waiting && ( )} diff --git a/src/renderer/components/add-remove-buttons/add-remove-buttons.tsx b/src/renderer/components/add-remove-buttons/add-remove-buttons.tsx index e38e0ffe67..9edc5090f4 100644 --- a/src/renderer/components/add-remove-buttons/add-remove-buttons.tsx +++ b/src/renderer/components/add-remove-buttons/add-remove-buttons.tsx @@ -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 { @@ -37,7 +37,7 @@ export class AddRemoveButtons extends React.PureComponent ] .filter(button => button.onClick) .map(({ icon, ...props }) => ( - + )); } diff --git a/src/renderer/components/avatar/avatar.tsx b/src/renderer/components/avatar/avatar.tsx index a3b9837295..6a4588306f 100644 --- a/src/renderer/components/avatar/avatar.tsx +++ b/src/renderer/components/avatar/avatar.tsx @@ -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) => ( -
) : children || getLabelFromTitle(title)} -
+ ); diff --git a/src/renderer/components/button/button.tsx b/src/renderer/components/button/button.tsx index 756f50a584..64890b52dd 100644 --- a/src/renderer/components/button/button.tsx +++ b/src/renderer/components/button/button.tsx @@ -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 { label?: React.ReactNode; @@ -23,16 +25,22 @@ export interface ButtonProps extends ButtonHTMLAttributes { round?: boolean; href?: string; // render as hyperlink target?: "_blank"; // in case of using @href + Component?: React.ComponentType; } +const PlainButton = (props: React.DetailedHTMLProps, HTMLButtonElement>) => + ); }); + +export const OpenLensButton = (props: ButtonProps & TooltipDecoratorProps) => { + return
- - +
)} > diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx index 48a53a0308..20346a945f 100644 --- a/src/renderer/components/dock/info-panel.tsx +++ b/src/renderer/components/dock/info-panel.tsx @@ -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 { )} {showButtons && ( <> - - - + )} prev={this.close} diff --git a/src/renderer/components/on-click-decorated/__snapshots__/foo-bar.test.tsx.snap b/src/renderer/components/on-click-decorated/__snapshots__/foo-bar.test.tsx.snap new file mode 100644 index 0000000000..d9847c3c80 --- /dev/null +++ b/src/renderer/components/on-click-decorated/__snapshots__/foo-bar.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`foobar given A -element 1`] = ` + +
+ +
+ +`; diff --git a/src/renderer/components/on-click-decorated/__snapshots__/on-click-decorated.test.tsx.snap b/src/renderer/components/on-click-decorated/__snapshots__/on-click-decorated.test.tsx.snap new file mode 100644 index 0000000000..d9847c3c80 --- /dev/null +++ b/src/renderer/components/on-click-decorated/__snapshots__/on-click-decorated.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`foobar given A -element 1`] = ` + + + +`; diff --git a/src/renderer/components/on-click-decorated/on-click-decorated.test.tsx b/src/renderer/components/on-click-decorated/on-click-decorated.test.tsx new file mode 100644 index 0000000000..413b346df6 --- /dev/null +++ b/src/renderer/components/on-click-decorated/on-click-decorated.test.tsx @@ -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( + {}} + 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( + , + ); + }); + + 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(); + }); + }); + }); +}); diff --git a/src/renderer/components/on-click-decorated/on-click-decorated.tsx b/src/renderer/components/on-click-decorated/on-click-decorated.tsx new file mode 100644 index 0000000000..75f8441044 --- /dev/null +++ b/src/renderer/components/on-click-decorated/on-click-decorated.tsx @@ -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 { + onClick?: MouseEventHandler; + 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 ; +}; + +export const OnClickDecorated = withInjectables( + NonInjectedOnClickDecorated, + + { + getProps: (di, props) => ({ + decorators: di.injectMany(onClickDecoratorInjectionToken), + ...props, + }), + }, +); diff --git a/src/renderer/components/on-click-decorated/on-click-decorator-injection-token.ts b/src/renderer/components/on-click-decorated/on-click-decorator-injection-token.ts new file mode 100644 index 0000000000..30a3d04a52 --- /dev/null +++ b/src/renderer/components/on-click-decorated/on-click-decorator-injection-token.ts @@ -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) => (event: React.MouseEvent) => void; + +export interface OnClickDecorator { + onClick: OnClick; +} + +export const onClickDecoratorInjectionToken = getInjectionToken({ + id: "onclick-decorator-injection-token", +}); diff --git a/src/renderer/components/on-click-decorated/on-click-telemetry-decorator.injectable.ts b/src/renderer/components/on-click-decorated/on-click-telemetry-decorator.injectable.ts new file mode 100644 index 0000000000..219ed8c3db --- /dev/null +++ b/src/renderer/components/on-click-decorated/on-click-telemetry-decorator.injectable.ts @@ -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; diff --git a/src/renderer/components/path-picker/path-picker.tsx b/src/renderer/components/path-picker/path-picker.tsx index 416bd3fd42..89a53b4352 100644 --- a/src/renderer/components/path-picker/path-picker.tsx +++ b/src/renderer/components/path-picker/path-picker.tsx @@ -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 { const { className, label, disabled } = this.props; return ( -