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

Auto capture

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>
This commit is contained in:
Juho Heikka 2022-04-28 10:57:20 +03:00
parent f162c8b6eb
commit 98b88180d8
18 changed files with 403 additions and 30 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "kube-object-event-status", "name": "kube-object-event-status",
"version": "0.0.1", "version": "5.5.0-alpha.0.1651664036833",
"description": "Adds kube object status from events", "description": "Adds kube object status from events",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -1,6 +1,6 @@
{ {
"name": "lens-metrics-cluster-feature", "name": "lens-metrics-cluster-feature",
"version": "0.0.1", "version": "5.5.0-alpha.0.1651664036833",
"description": "Lens metrics cluster feature", "description": "Lens metrics cluster feature",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -1,6 +1,6 @@
{ {
"name": "lens-node-menu", "name": "lens-node-menu",
"version": "0.0.1", "version": "5.5.0-alpha.0.1651664036833",
"description": "Lens node menu", "description": "Lens node menu",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -1,6 +1,6 @@
{ {
"name": "lens-pod-menu", "name": "lens-pod-menu",
"version": "0.0.1", "version": "5.5.0-alpha.0.1651664036833",
"description": "Lens pod menu", "description": "Lens pod menu",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -8,6 +8,11 @@ import type { ButtonHTMLAttributes } from "react";
import React from "react"; import React from "react";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { withTooltip } from "../tooltip"; import { withTooltip } from "../tooltip";
import { withInjectables } from "@ogre-tools/injectable-react";
import trackEventInjectable from "../../telemetry/track-event.injectable";
interface Dependencies {
track: (e: React.MouseEvent) => void;
}
export interface ButtonProps extends ButtonHTMLAttributes<any> { export interface ButtonProps extends ButtonHTMLAttributes<any> {
label?: React.ReactNode; label?: React.ReactNode;
@ -25,10 +30,10 @@ export interface ButtonProps extends ButtonHTMLAttributes<any> {
target?: "_blank"; // in case of using @href target?: "_blank"; // in case of using @href
} }
export const Button = withTooltip((props: ButtonProps) => { const NonInjectedButton = withTooltip((props: ButtonProps & Dependencies) => {
const { const {
waiting, label, primary, accent, plain, hidden, active, big, waiting, label, primary, accent, plain, hidden, active, big,
round, outlined, light, children, ...btnProps round, outlined, light, children, track, ...btnProps
} = props; } = props;
if (hidden) return null; if (hidden) return null;
@ -37,10 +42,18 @@ export const Button = withTooltip((props: ButtonProps) => {
waiting, primary, accent, plain, active, big, round, outlined, light, waiting, primary, accent, plain, active, big, round, outlined, light,
}); });
const onClick = (e: React.MouseEvent) => {
track(e);
if (btnProps.onClick) {
btnProps.onClick(e);
}
};
// render as link // render as link
if (props.href) { if (props.href) {
return ( return (
<a {...btnProps}> <a {...btnProps} onClick={onClick}>
{label} {label}
{children} {children}
</a> </a>
@ -49,9 +62,24 @@ export const Button = withTooltip((props: ButtonProps) => {
// render as button // render as button
return ( return (
<button type="button" {...btnProps}> <button
type="button"
{...btnProps}
onClick={onClick}>
{label} {label}
{children} {children}
</button> </button>
); );
}); });
export const Button = withInjectables<Dependencies, ButtonProps>(
NonInjectedButton,
{
getProps: (di, props) => ({
track: di.inject(trackEventInjectable),
...props,
}),
},
);

View File

@ -7,6 +7,8 @@ import "./checkbox.scss";
import React from "react"; import React from "react";
import type { SingleOrMany } from "../../utils"; import type { SingleOrMany } from "../../utils";
import { cssNames, noop } from "../../utils"; import { cssNames, noop } from "../../utils";
import { withInjectables } from "@ogre-tools/injectable-react";
import trackWithIdInjectable from "../../telemetry/track-with-id.injectable";
export interface CheckboxProps { export interface CheckboxProps {
className?: string; className?: string;
@ -18,7 +20,11 @@ export interface CheckboxProps {
children?: SingleOrMany<React.ReactChild | React.ReactFragment>; children?: SingleOrMany<React.ReactChild | React.ReactFragment>;
} }
export function Checkbox({ label, inline, className, value, children, onChange = noop, disabled, ...inputProps }: CheckboxProps) { interface Dependencies {
captureClick: (id: string, action: string) => void;
}
function NonInjectedCheckbox({ label, inline, className, value, children, onChange = noop, disabled, captureClick, ...inputProps }: CheckboxProps & Dependencies) {
const componentClass = cssNames("Checkbox flex align-center", className, { const componentClass = cssNames("Checkbox flex align-center", className, {
inline, inline,
checked: value, checked: value,
@ -32,7 +38,13 @@ export function Checkbox({ label, inline, className, value, children, onChange =
type="checkbox" type="checkbox"
checked={value} checked={value}
disabled={disabled} disabled={disabled}
onChange={event => onChange(event.target.checked, event)} onChange={event => {
if (label) {
captureClick(`${window.location.pathname} ${label.toString()}`, `Checkbox ${event.target.checked ? "On" : "Off"}`);
}
onChange(event.target.checked, event);
}}
/> />
<i className="box flex align-center"/> <i className="box flex align-center"/>
{label ? <span className="label">{label}</span> : null} {label ? <span className="label">{label}</span> : null}
@ -40,3 +52,14 @@ export function Checkbox({ label, inline, className, value, children, onChange =
</label> </label>
); );
} }
export const Checkbox = withInjectables<Dependencies, CheckboxProps>(
NonInjectedCheckbox,
{
getProps: (di, props) => ({
captureClick: di.inject(trackWithIdInjectable),
...props,
}),
},
);

View File

@ -18,6 +18,7 @@ import { Tooltip } from "../tooltip";
import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable"; import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable"; import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable";
import trackWithIdInjectable from "../../../renderer/telemetry/track-with-id.injectable";
export interface HotbarIconProps extends AvatarProps { export interface HotbarIconProps extends AvatarProps {
uid: string; uid: string;
@ -32,6 +33,7 @@ export interface HotbarIconProps extends AvatarProps {
interface Dependencies { interface Dependencies {
normalizeMenuItem: NormalizeCatalogEntityContextMenu; normalizeMenuItem: NormalizeCatalogEntityContextMenu;
capture: (id: string, action: string) => void;
} }
const NonInjectedHotbarIcon = observer(({ const NonInjectedHotbarIcon = observer(({
@ -41,7 +43,7 @@ const NonInjectedHotbarIcon = observer(({
normalizeMenuItem, normalizeMenuItem,
...props ...props
}: HotbarIconProps & Dependencies) => { }: HotbarIconProps & Dependencies) => {
const { uid, title, src, material, active, className, source, disabled, onMenuOpen, onClick, children, ...rest } = props; const { uid, title, src, material, active, className, source, disabled, onMenuOpen, onClick, children, capture, ...rest } = props;
const id = `hotbarIcon-${uid}`; const id = `hotbarIcon-${uid}`;
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
@ -65,8 +67,13 @@ const NonInjectedHotbarIcon = observer(({
disabled={disabled} disabled={disabled}
size={size} size={size}
src={src} src={src}
onClick={(event) => !disabled && onClick?.(event)} onClick={(event) => {
> if (disabled) {
return;
}
capture(title, "Click Hotbar Item");
onClick?.(event);
}}>
{material && <Icon material={material} />} {material && <Icon material={material} />}
</Avatar> </Avatar>
{children} {children}
@ -100,5 +107,6 @@ export const HotbarIcon = withInjectables<Dependencies, HotbarIconProps>(NonInje
getProps: (di, props) => ({ getProps: (di, props) => ({
...props, ...props,
normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable), normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable),
capture: di.inject(trackWithIdInjectable),
}), }),
}); });

View File

@ -32,6 +32,7 @@ import userStoreInjectable from "../../../common/user-store/user-store.injectabl
import pageFiltersStoreInjectable from "./page-filters/store.injectable"; import pageFiltersStoreInjectable from "./page-filters/store.injectable";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable"; import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable"; import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
import trackWithIdInjectable from "../../../renderer/telemetry/track-with-id.injectable";
export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStores extends boolean> { export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStores extends boolean> {
getFilters: () => Filter[]; getFilters: () => Filter[];
@ -75,6 +76,7 @@ interface Dependencies {
userStore: UserStore; userStore: UserStore;
pageFiltersStore: PageFiltersStore; pageFiltersStore: PageFiltersStore;
openConfirmDialog: OpenConfirmDialog; openConfirmDialog: OpenConfirmDialog;
capture: (id: string, action: string) => void;
} }
@observer @observer
@ -110,7 +112,11 @@ class NonInjectedItemListLayoutContent<
searchItem={item} searchItem={item}
sortItem={item} sortItem={item}
selected={detailsItem && detailsItem.getId() === item.getId()} selected={detailsItem && detailsItem.getId() === item.getId()}
onClick={hasDetailsView ? prevDefault(() => onDetails?.(item)) : undefined} onClick={hasDetailsView ? prevDefault(() => {
this.props.capture(this.props.tableId, "Table Row Click");
return onDetails?.(item);
}) : undefined}
{...customizeTableRowProps(item)} {...customizeTableRowProps(item)}
> >
{isSelectable && ( {isSelectable && (
@ -381,5 +387,6 @@ export const ItemListLayoutContent = withInjectables<Dependencies, ItemListLayou
userStore: di.inject(userStoreInjectable), userStore: di.inject(userStoreInjectable),
pageFiltersStore: di.inject(pageFiltersStoreInjectable), pageFiltersStore: di.inject(pageFiltersStoreInjectable),
openConfirmDialog: di.inject(openConfirmDialogInjectable), openConfirmDialog: di.inject(openConfirmDialogInjectable),
capture: di.inject(trackWithIdInjectable),
}), }),
}) as <Item extends ItemObject, PreLoadStores extends boolean>(props: ItemListLayoutContentProps<Item, PreLoadStores>) => React.ReactElement; }) as <Item extends ItemObject, PreLoadStores extends boolean>(props: ItemListLayoutContentProps<Item, PreLoadStores>) => React.ReactElement;

View File

@ -8,13 +8,26 @@ import styles from "./close-button.module.scss";
import type { HTMLAttributes } from "react"; import type { HTMLAttributes } from "react";
import React from "react"; import React from "react";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import trackWithIdInjectable from "../../telemetry/track-with-id.injectable";
export interface CloseButtonProps extends HTMLAttributes<HTMLDivElement> { export interface CloseButtonProps extends HTMLAttributes<HTMLDivElement> {
} }
export function CloseButton(props: CloseButtonProps) { interface Dependencies {
capture: (id: string, action: string) => void;
}
function NonInjectedCloseButton(props: CloseButtonProps & Dependencies) {
const { capture, ...rest } = props;
return ( return (
<div {...props}> <div
{...rest}
onClick={(e) => {
capture(`${window.location.pathname}`, "Close Button Click");
props?.onClick(e);
}}>
<div <div
className={styles.closeButton} className={styles.closeButton}
role="button" role="button"
@ -28,3 +41,14 @@ export function CloseButton(props: CloseButtonProps) {
</div> </div>
); );
} }
export const CloseButton = withInjectables<Dependencies, CloseButtonProps>(
NonInjectedCloseButton,
{
getProps: (di, props) => ({
capture: di.inject(trackWithIdInjectable),
...props,
}),
},
);

View File

@ -16,9 +16,11 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import type { SidebarStorageState } from "./sidebar-storage/sidebar-storage.injectable"; import type { SidebarStorageState } from "./sidebar-storage/sidebar-storage.injectable";
import sidebarStorageInjectable from "./sidebar-storage/sidebar-storage.injectable"; import sidebarStorageInjectable from "./sidebar-storage/sidebar-storage.injectable";
import type { HierarchicalSidebarItem } from "./sidebar-items.injectable"; import type { HierarchicalSidebarItem } from "./sidebar-items.injectable";
import trackWithIdInjectable from "../../../renderer/telemetry/track-with-id.injectable";
interface Dependencies { interface Dependencies {
sidebarStorage: StorageLayer<SidebarStorageState>; sidebarStorage: StorageLayer<SidebarStorageState>;
capture: (id: string, action: string) => void;
} }
export interface SidebarItemProps { export interface SidebarItemProps {
@ -96,6 +98,8 @@ class NonInjectedSidebarItem extends React.Component<
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.props.capture(this.registration.title.toString(), "Click Side Bar Item");
if (this.isExpandable) { if (this.isExpandable) {
this.toggleExpand(); this.toggleExpand();
} else { } else {
@ -125,5 +129,7 @@ export const SidebarItem = withInjectables<Dependencies, SidebarItemProps>(NonIn
getProps: (di, props) => ({ getProps: (di, props) => ({
...props, ...props,
sidebarStorage: di.inject(sidebarStorageInjectable), sidebarStorage: di.inject(sidebarStorageInjectable),
capture: di.inject(trackWithIdInjectable),
}), }),
}); });

View File

@ -11,17 +11,23 @@ import { cssNames } from "../../utils";
import { Tab, Tabs } from "../tabs"; import { Tab, Tabs } from "../tabs";
import { ErrorBoundary } from "../error-boundary"; import { ErrorBoundary } from "../error-boundary";
import type { HierarchicalSidebarItem } from "./sidebar-items.injectable"; import type { HierarchicalSidebarItem } from "./sidebar-items.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import trackWithIdInjectable from "../../../renderer/telemetry/track-with-id.injectable";
interface Dependencies {
captureClick: (id: string, action: string) => void;
}
export interface TabLayoutProps { export interface TabLayoutProps {
tabs?: HierarchicalSidebarItem[]; tabs?: HierarchicalSidebarItem[];
children?: React.ReactNode; children?: React.ReactNode;
} }
export const TabLayout = observer( const NonInjectedTabLayout = observer(
({ ({
tabs = [], tabs = [],
children, children,
}: TabLayoutProps) => { captureClick,
}: TabLayoutProps & Dependencies) => {
const hasTabs = tabs.length > 0; const hasTabs = tabs.length > 0;
return ( return (
@ -37,7 +43,10 @@ export const TabLayout = observer(
return ( return (
<Tab <Tab
onClick={registration.onClick} onClick={() => {
captureClick(registration.title.toString(), "Tab Click");
registration.onClick();
}}
key={registration.id} key={registration.id}
label={registration.title} label={registration.title}
active={active} active={active}
@ -58,3 +67,14 @@ export const TabLayout = observer(
); );
}, },
); );
export const TabLayout = withInjectables<Dependencies, TabLayoutProps>(
NonInjectedTabLayout,
{
getProps: (di, props) => ({
captureClick: di.inject(trackWithIdInjectable),
...props,
}),
},
);

View File

@ -13,6 +13,8 @@ import { Animate } from "../animate";
import type { IconProps } from "../icon"; import type { IconProps } from "../icon";
import { Icon } from "../icon"; import { Icon } from "../icon";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { withInjectables } from "@ogre-tools/injectable-react";
import trackWithIdInjectable from "../../../renderer/telemetry/track-with-id.injectable";
export const MenuContext = React.createContext<MenuContextValue | null>(null); export const MenuContext = React.createContext<MenuContextValue | null>(null);
export type MenuContextValue = Menu; export type MenuContextValue = Menu;
@ -71,7 +73,7 @@ export class Menu extends React.Component<MenuProps, State> {
} }
public opener: HTMLElement | null = null; public opener: HTMLElement | null = null;
public elem: HTMLUListElement | null = null; public elem: HTMLUListElement | null = null;
protected items: { [index: number]: MenuItem } = {}; protected items: { [index: number]: NonInjectedMenuItem } = {};
public state: State = {}; public state: State = {};
get isOpen() { get isOpen() {
@ -308,7 +310,7 @@ export class Menu extends React.Component<MenuProps, State> {
this.elem = elem; this.elem = elem;
} }
protected bindItemRef(item: MenuItem, index: number) { protected bindItemRef(item: NonInjectedMenuItem, index: number) {
this.items[index] = item; this.items[index] = item;
} }
@ -328,7 +330,7 @@ export class Menu extends React.Component<MenuProps, State> {
const menuItems = React.Children.toArray(children).map((item, index) => { const menuItems = React.Children.toArray(children).map((item, index) => {
if (typeof item === "object" && (item as ReactElement).type === MenuItem) { if (typeof item === "object" && (item as ReactElement).type === MenuItem) {
return React.cloneElement(item as ReactElement, { return React.cloneElement(item as ReactElement, {
ref: (item: MenuItem) => this.bindItemRef(item, index), ref: (item: NonInjectedMenuItem) => this.bindItemRef(item, index),
}); });
} }
@ -389,6 +391,9 @@ export function SubMenu(props: Partial<MenuProps>) {
); );
} }
interface Dependencies {
captureClick: (id: string, action: string) => void;
}
export interface MenuItemProps extends React.HTMLProps<any> { export interface MenuItemProps extends React.HTMLProps<any> {
icon?: string | Partial<IconProps>; icon?: string | Partial<IconProps>;
disabled?: boolean; disabled?: boolean;
@ -401,14 +406,14 @@ const defaultPropsMenuItem: Partial<MenuItemProps> = {
onClick: noop, onClick: noop,
}; };
export class MenuItem extends React.Component<MenuItemProps> { class NonInjectedMenuItem extends React.Component<MenuItemProps & Dependencies> {
static defaultProps = defaultPropsMenuItem as object; static defaultProps = defaultPropsMenuItem as object;
static contextType = MenuContext; static contextType = MenuContext;
declare context: MenuContextValue; declare context: MenuContextValue;
public elem: HTMLElement | null = null; public elem: HTMLElement | null = null;
constructor(props: MenuItemProps) { constructor(props: MenuItemProps & Dependencies) {
super(props); super(props);
autoBind(this); autoBind(this);
} }
@ -431,6 +436,14 @@ export class MenuItem extends React.Component<MenuItemProps> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onClick!(evt); onClick!(evt);
const name = this.elem.querySelectorAll(".title")[0]?.textContent;
if (name) {
const id = `${window.location.pathname.split("/").pop()} ${name}`;
this.props.captureClick(id, "Menu Item Click");
}
if (menu.props.closeOnClickItem && !evt.defaultPrevented) { if (menu.props.closeOnClickItem && !evt.defaultPrevented) {
menu.close(); menu.close();
} }
@ -441,7 +454,7 @@ export class MenuItem extends React.Component<MenuItemProps> {
} }
render() { render() {
const { className, disabled, active, spacer, icon, children, ...props } = this.props; const { className, disabled, active, spacer, icon, children, captureClick, ...props } = this.props;
const iconProps: Partial<IconProps> = {}; const iconProps: Partial<IconProps> = {};
if (icon) { if (icon) {
@ -474,3 +487,14 @@ export class MenuItem extends React.Component<MenuItemProps> {
return <li {...elemProps}/>; return <li {...elemProps}/>;
} }
} }
export const MenuItem = withInjectables<Dependencies, MenuItemProps>(
NonInjectedMenuItem,
{
getProps: (di, props) => ({
captureClick: di.inject(trackWithIdInjectable),
...props,
}),
},
);

View File

@ -17,6 +17,7 @@ import type { ThemeStore } from "../../themes/store";
import { autoBind, cssNames } from "../../utils"; import { autoBind, cssNames } from "../../utils";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable"; import themeStoreInjectable from "../../themes/store.injectable";
import trackWithIdInjectable from "../../telemetry/track-with-id.injectable";
const { Menu } = components; const { Menu } = components;
@ -81,6 +82,7 @@ const defaultFilter = createFilter({
interface Dependencies { interface Dependencies {
themeStore: ThemeStore; themeStore: ThemeStore;
capture: (id: string, action: string) => void;
} }
export function onMultiSelectFor<Value, Option extends SelectOption<Value>, Group extends GroupBase<Option> = GroupBase<Option>>(collection: Set<Value> | ObservableSet<Value>): SelectProps<Value, Option, true, Group>["onChange"] { export function onMultiSelectFor<Value, Option extends SelectOption<Value>, Group extends GroupBase<Option> = GroupBase<Option>>(collection: Set<Value> | ObservableSet<Value>): SelectProps<Value, Option, true, Group>["onChange"] {
@ -228,7 +230,13 @@ class NonInjectedSelect<
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
className={cssNames("Select", this.themeClass, className)} className={cssNames("Select", this.themeClass, className)}
classNamePrefix="Select" classNamePrefix="Select"
onChange={action(onChange)} // This is done so that all changes are actionable onChange={action(() => {
if (inputId) {
props.capture(inputId, "Select Change");
}
onChange();
})} // This is done so that all changes are actionable
components={{ components={{
...components, ...components,
Menu: ({ className, ...props }) => ( Menu: ({ className, ...props }) => (
@ -249,6 +257,7 @@ export const Select = withInjectables<Dependencies, SelectProps<unknown, SelectO
getProps: (di, props) => ({ getProps: (di, props) => ({
...props, ...props,
themeStore: di.inject(themeStoreInjectable), themeStore: di.inject(themeStoreInjectable),
capture: di.inject(trackWithIdInjectable),
}), }),
}) as < }) as <
Value, Value,

View File

@ -8,12 +8,18 @@ import styles from "./switch.module.scss";
import type { ChangeEvent, HTMLProps } from "react"; import type { ChangeEvent, HTMLProps } from "react";
import React from "react"; import React from "react";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { withInjectables } from "@ogre-tools/injectable-react";
import trackWithIdInjectable from "../../telemetry/track-with-id.injectable";
export interface SwitchProps extends Omit<HTMLProps<HTMLInputElement>, "onChange"> { export interface SwitchProps extends Omit<HTMLProps<HTMLInputElement>, "onChange"> {
onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void; onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void;
} }
export function Switch({ children, disabled, onChange, ...props }: SwitchProps) { interface Dependencies {
captureChange: (id: string, action: string) => void;
}
function NonInjectedSwitch({ children, disabled, onChange, captureChange, ...props }: SwitchProps & Dependencies) {
return ( return (
<label className={cssNames(styles.Switch, { [styles.disabled]: disabled })} data-testid="switch"> <label className={cssNames(styles.Switch, { [styles.disabled]: disabled })} data-testid="switch">
{children} {children}
@ -21,9 +27,23 @@ export function Switch({ children, disabled, onChange, ...props }: SwitchProps)
type="checkbox" type="checkbox"
role="switch" role="switch"
disabled={disabled} disabled={disabled}
onChange={(event) => onChange?.(event.target.checked, event)} onChange={(event) =>{
onChange?.(event.target.checked, event);
captureChange(children.toString(), `Switch ${props.checked ? "On" : "Off"}`);
}}
{...props} {...props}
/> />
</label> </label>
); );
} }
export const Switch = withInjectables<Dependencies, SwitchProps>(
NonInjectedSwitch,
{
getProps: (di, props) => ({
captureChange: di.inject(trackWithIdInjectable),
...props,
}),
},
);

View File

@ -8,6 +8,8 @@ import type { DOMAttributes } from "react";
import React from "react"; import React from "react";
import { autoBind, cssNames } from "../../utils"; import { autoBind, cssNames } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import trackWithIdInjectable from "../../../renderer/telemetry/track-with-id.injectable";
const TabsContext = React.createContext<TabsContextValue>({}); const TabsContext = React.createContext<TabsContextValue>({});
@ -59,12 +61,12 @@ export interface TabProps<D = any> extends DOMAttributes<HTMLElement> {
value?: D; value?: D;
} }
export class Tab extends React.PureComponent<TabProps> { class NonInjectedTab extends React.PureComponent<TabProps & Dependencies> {
static contextType = TabsContext; static contextType = TabsContext;
declare context: TabsContextValue; declare context: TabsContextValue;
public ref = React.createRef<HTMLDivElement>(); public ref = React.createRef<HTMLDivElement>();
constructor(props: TabProps) { constructor(props: TabProps & Dependencies) {
super(props); super(props);
autoBind(this); autoBind(this);
} }
@ -91,6 +93,9 @@ export class Tab extends React.PureComponent<TabProps> {
return; return;
} }
if (value?.kind) {
this.props.capture(`${value?.kind} `, "Tab Click");
}
onClick?.(evt); onClick?.(evt);
this.context.onChange?.(value); this.context.onChange?.(value);
} }
@ -142,3 +147,18 @@ export class Tab extends React.PureComponent<TabProps> {
); );
} }
} }
interface Dependencies {
capture: (id: string, action: string) => void;
}
export const Tab = withInjectables<Dependencies, TabProps>(
NonInjectedTab,
{
getProps: (di, props) => ({
capture: di.inject(trackWithIdInjectable),
...props,
}),
},
);

View File

@ -0,0 +1,85 @@
/**
* 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 appEventBusInjectable from "../../common/app-event-bus/app-event-bus.injectable";
import type { EventEmitter } from "../../common/event-emitter";
function getButtonEventName(el: HTMLElement) {
let headers: string[] = [];
const levels = 3;
let parent = el;
for (let i = 0; i < levels; i++) {
if (parent.parentElement) {
parent = parent.parentElement;
}
}
const nodelist = parent.querySelectorAll("h1, h2, h3, h4, h5, h6, .header");
nodelist.forEach(node => headers.push(node.textContent));
if (headers.length === 0) {
const path = window.location.pathname.split("/");
headers.push(path[path.length-1]);
}
headers = [...new Set(headers)];
headers.push(el.textContent);
const buttonEventName = headers.join(" ");
return buttonEventName;
}
// foo_bar-baz => Foo Bar Baz
function getNameFromId(id: string) {
return id.split(/[_,-]/).map((part) => `${part[0].toUpperCase()+part.substring(1)}`).join("");
}
export class Telemetry {
private eventBus: EventEmitter<[AppEvent]>;
private destination = "jep";
private debug = false;
constructor(eventBus: EventEmitter<[AppEvent]>) {
this.eventBus = eventBus;
}
private emitEvent(action: string, name: string) {
console.log(`[TELEMETRY]: ${action} ${name}`);
if (!this.debug) {
this.eventBus.emit({
destination: this.destination,
name,
action,
});
}
}
buttonClickEvent(e: React.MouseEvent) {
const el = e.target as HTMLElement;
this.emitEvent("Click", getButtonEventName(el));
}
selectChangeEvent(id: string) {
this.emitEvent("Select Change", getNameFromId(id));
}
tableRowClick(id: string) {
this.emitEvent("Table Row Click", getNameFromId(id));
}
}
const telemetryInjectable = getInjectable({
id: "telemetry",
instantiate: (di) => new Telemetry(di.inject(appEventBusInjectable)),
});
export default telemetryInjectable;

View File

@ -0,0 +1,63 @@
/**
* 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 appEventBusInjectable from "../../common/app-event-bus/app-event-bus.injectable";
import type { EventEmitter } from "../../common/event-emitter";
import capitalize from "lodash/capitalize";
function getEventName(el: HTMLElement) {
let headers: string[] = [];
const levels = 3;
let parent = el;
const path = window.location.pathname.split("/");
headers.push(capitalize(path[path.length-1]));
for (let i = 0; i < levels; 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 trackEventName(eventBus: EventEmitter<[AppEvent]>, event: React.MouseEvent) {
const name = getEventName(event.target as HTMLElement);
const action = capitalize(event.type);
console.log("track event name");
console.log(`${action} ${name}`);
eventBus.emit({
destination: "MixPanel",
name,
action: capitalize(event.type),
});
}
const trackEventInjectable = getInjectable({
id: "track-mouse-event",
instantiate: (di) => {
return (event: React.MouseEvent) => {
return trackEventName(di.inject(appEventBusInjectable), event);
};
},
});
export default trackEventInjectable;

View File

@ -0,0 +1,36 @@
/**
* 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 appEventBusInjectable from "../../common/app-event-bus/app-event-bus.injectable";
import type { EventEmitter } from "../../common/event-emitter";
// foo_bar-baz => Foo Bar Baz
function getNameFromId(id: string) {
return id.split(/[/,-,--]/).filter(Boolean).map((part) => `${part[0].toUpperCase()+part.substring(1)}`).join("");
}
function trackWithId(eventBus: EventEmitter<[AppEvent]>, id: string, action: string) {
const target = getNameFromId(id);
console.log(`[trackWithId]: ${name}`);
eventBus.emit({
name: target,
action,
destination: "MixPanel",
});
}
const trackWithIdInjectable = getInjectable({
id: "track-with-id",
instantiate: (di) => {
return (id: string, action: string) => {
return trackWithId(di.inject(appEventBusInjectable), id, action);
};
},
});
export default trackWithIdInjectable;