Skip to content

Commit

Permalink
[WOR-1437] Add metrics events to the Workspace dashboard (#4582)
Browse files Browse the repository at this point in the history
  • Loading branch information
cahrens authored Jan 11, 2024
1 parent 9c84a04 commit d0dc5d2
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 12 deletions.
7 changes: 6 additions & 1 deletion src/components/Collapse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface CollapseProps extends Omit<DivProps, 'title'> {
titleFirst?: boolean;
afterTitle?: ReactNode;
onFirstOpen?: () => void;
onOpenChanged?: (isOpened: boolean) => void;
noTitleWrap?: boolean;
disabled?: boolean;
}
Expand All @@ -37,6 +38,7 @@ const Collapse = (props: CollapseProps): ReactNode => {
titleFirst,
afterTitle,
onFirstOpen = () => {},
onOpenChanged = () => {},
noTitleWrap,
disabled = false,
...rest
Expand Down Expand Up @@ -81,7 +83,10 @@ const Collapse = (props: CollapseProps): ReactNode => {
color: colors.dark(),
...(noTitleWrap ? Style.noWrapEllipsis : {}),
},
onClick: () => setIsOpened(!isOpened),
onClick: () => {
setIsOpened(!isOpened);
onOpenChanged(!isOpened);
},
hover,
tooltip,
tooltipDelay,
Expand Down
6 changes: 6 additions & 0 deletions src/libs/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ const eventsList = {
workflowsTabView: 'workflowsApp:tab:view',
workspaceClone: 'workspace:clone',
workspaceCreate: 'workspace:create',
workspaceDashboardToggleSection: 'workspace:dashboard:toggleSection',
workspaceDashboardAddTag: 'workspace:dashboard:addTag',
workspaceDashboardDeleteTag: 'workspace:dashboard:deleteTag',
workspaceDashboardEditDescription: 'workspace:dashboard:editDescription',
workspaceDashboardSaveDescription: 'workspace:dashboard:saveDescription',
workspaceDashboardBucketRequesterPays: 'workspace:dashboard:bucketLocationRequesterPays',
workspaceOpenedBucketInBrowser: 'workspace:openedBucketInBrowser',
workspaceOpenedProjectInConsole: 'workspace:openedProjectInCloudConsole',
workspaceDataAddColumn: 'workspace:data:addColumn',
Expand Down
28 changes: 28 additions & 0 deletions src/pages/workspaces/workspace/Dashboard/BucketLocation.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { asMockedFn } from '@terra-ui-packages/test-utils';
import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';
import _ from 'lodash/fp';
import { h } from 'react-hyperscript-helpers';
import { Ajax } from 'src/libs/ajax';
import Events, { extractWorkspaceDetails } from 'src/libs/events';
import { GoogleWorkspace } from 'src/libs/workspace-utils';
import { BucketLocation } from 'src/pages/workspaces/workspace/Dashboard/BucketLocation';
import { renderWithAppContexts as render } from 'src/testing/test-utils';
Expand All @@ -19,6 +21,17 @@ jest.mock('src/libs/ajax');

jest.mock('src/libs/notifications');

// Needed for bringing up the RequestPays modal
type WorkspaceProviderExports = typeof import('src/libs/ajax/workspaces/providers/WorkspaceProvider');
jest.mock(
'src/libs/ajax/workspaces/providers/WorkspaceProvider',
(): WorkspaceProviderExports => ({
workspaceProvider: {
list: jest.fn(),
},
})
);

describe('BucketLocation', () => {
const workspace: GoogleWorkspace & { workspaceInitialized: boolean } = {
...defaultGoogleWorkspace,
Expand Down Expand Up @@ -119,6 +132,7 @@ describe('BucketLocation', () => {

it('handles requester pays error', async () => {
// Arrange
const user = userEvent.setup();
const props = {
workspace,
storageDetails: _.mergeAll([
Expand All @@ -128,9 +142,11 @@ describe('BucketLocation', () => {
]),
};
const requesterPaysError = { message: 'Requester pays bucket', requesterPaysError: true };
const captureEvent = jest.fn();
asMockedFn(Ajax).mockImplementation(
() =>
({
Metrics: { captureEvent } as Partial<AjaxContract['Metrics']>,
Workspaces: {
workspace: () =>
({
Expand All @@ -145,5 +161,17 @@ describe('BucketLocation', () => {
// Assert
expect(screen.queryByText('Loading')).toBeNull();
expect(screen.getAllByText(/bucket is requester pays/)).not.toBeNull();

// Act
const loadBucketLocation = screen.getByLabelText('Load bucket location');
await user.click(loadBucketLocation);

// Assert
expect(captureEvent).toHaveBeenCalledWith(
Events.workspaceDashboardBucketRequesterPays,
extractWorkspaceDetails(workspace)
);
// In the RequesterPays modal (because the list method returns no workspaces).
expect(screen.getAllByText('Go to Workspaces')).not.toBeNull();
});
});
9 changes: 8 additions & 1 deletion src/pages/workspaces/workspace/Dashboard/BucketLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import RequesterPaysModal from 'src/components/RequesterPaysModal';
import { TooltipCell } from 'src/components/table';
import { Ajax } from 'src/libs/ajax';
import { reportError } from 'src/libs/error';
import Events, { extractWorkspaceDetails } from 'src/libs/events';
import { useCancellation } from 'src/libs/react-utils';
import { requesterPaysProjectStore } from 'src/libs/state';
import { GoogleWorkspace } from 'src/libs/workspace-utils';
Expand Down Expand Up @@ -91,7 +92,13 @@ export const BucketLocation = requesterPaysWrapper({ onDismiss: _.noop })((props
tooltip:
"This workspace's bucket is requester pays. Click to choose a workspace to bill requests to and get the bucket's location.",
style: { height: '1rem', marginLeft: '1ch' },
onClick: () => setShowRequesterPaysModal(true),
onClick: () => {
setShowRequesterPaysModal(true);
Ajax().Metrics.captureEvent(
Events.workspaceDashboardBucketRequesterPays,
extractWorkspaceDetails(workspace)
);
},
},
[icon('sync')]
),
Expand Down
50 changes: 47 additions & 3 deletions src/pages/workspaces/workspace/Dashboard/RightBoxSection.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import { act, fireEvent, screen } from '@testing-library/react';
import React from 'react';
import { renderWithAppContexts as render } from 'src/testing/test-utils';
import { Ajax } from 'src/libs/ajax';
import Events, { extractWorkspaceDetails } from 'src/libs/events';
import { asMockedFn, renderWithAppContexts as render } from 'src/testing/test-utils';
import { defaultAzureWorkspace } from 'src/testing/workspace-fixtures';

import { RightBoxSection } from './RightBoxSection';

type AjaxContract = ReturnType<typeof Ajax>;
jest.mock('src/libs/ajax');

describe('RightBoxSection', () => {
const workspace = defaultAzureWorkspace;
const captureEvent = jest.fn();

beforeEach(() => {
jest.resetAllMocks();
asMockedFn(Ajax).mockImplementation(
() =>
({
Metrics: { captureEvent } as Partial<AjaxContract['Metrics']>,
} as Partial<AjaxContract> as AjaxContract)
);
});

it('displays the title', async () => {
// Arrange
// Act
await act(async () => {
render(<RightBoxSection title="Test Title" persistenceId="testId" />);
render(<RightBoxSection title="Test Title" persistenceId="testId" workspace={workspace} />);
});
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument();
Expand All @@ -19,7 +38,7 @@ describe('RightBoxSection', () => {
// Arrange
// Act
render(
<RightBoxSection title="Test Title" persistenceId="testId">
<RightBoxSection title="Test Title" persistenceId="testId" workspace={workspace}>
Panel Content
</RightBoxSection>
);
Expand All @@ -30,4 +49,29 @@ describe('RightBoxSection', () => {
// Assert
expect(screen.getByText('Panel Content')).toBeInTheDocument();
});

it('fires a metrics event when the panel is toggled', async () => {
// Arrange
// Act
render(
<RightBoxSection title="Test Title" persistenceId="metricsId" workspace={workspace}>
Panel Content
</RightBoxSection>
);
const titleElement = screen.getByText('Test Title');
fireEvent.click(titleElement);
fireEvent.click(titleElement);

// Assert
expect(captureEvent).toHaveBeenNthCalledWith(1, Events.workspaceDashboardToggleSection, {
opened: true,
title: 'Test Title',
...extractWorkspaceDetails(workspace),
});
expect(captureEvent).toHaveBeenNthCalledWith(2, Events.workspaceDashboardToggleSection, {
opened: false,
title: 'Test Title',
...extractWorkspaceDetails(workspace),
});
});
});
15 changes: 13 additions & 2 deletions src/pages/workspaces/workspace/Dashboard/RightBoxSection.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { CSSProperties, ReactNode } from 'react';
import { div, h, h3 } from 'react-hyperscript-helpers';
import Collapse from 'src/components/Collapse';
import { Ajax } from 'src/libs/ajax';
import colors from 'src/libs/colors';
import Events, { extractWorkspaceDetails } from 'src/libs/events';
import * as Style from 'src/libs/style';
import { useLocalPref } from 'src/libs/useLocalPref';
import { WorkspaceWrapper } from 'src/libs/workspace-utils';

interface RightBoxSectionProps {
title: string;
workspace: WorkspaceWrapper; // used for metrics eventing
info?: ReactNode;
afterTitle?: ReactNode;
persistenceId: string; // persists whether or not the panel is open in local storage
Expand All @@ -15,7 +19,7 @@ interface RightBoxSectionProps {
}

export const RightBoxSection = (props: RightBoxSectionProps): ReactNode => {
const { title, info, persistenceId, afterTitle, defaultPanelOpen = false, children } = props;
const { title, info, persistenceId, afterTitle, defaultPanelOpen = false, children, workspace } = props;
const [panelOpen, setPanelOpen] = useLocalPref<boolean>(persistenceId, defaultPanelOpen);
return div({ style: { paddingTop: '1rem' } }, [
div({ style: Style.dashboard.rightBoxContainer }, [
Expand All @@ -27,7 +31,14 @@ export const RightBoxSection = (props: RightBoxSectionProps): ReactNode => {
initialOpenState: panelOpen,
titleFirst: true,
afterTitle,
onClick: () => setPanelOpen(!panelOpen),
onOpenChanged: (panelOpen) => {
setPanelOpen(panelOpen);
Ajax().Metrics.captureEvent(Events.workspaceDashboardToggleSection, {
title,
opened: panelOpen,
...extractWorkspaceDetails(workspace),
});
},
},
[children]
),
Expand Down
12 changes: 9 additions & 3 deletions src/pages/workspaces/workspace/Dashboard/WorkspaceDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,22 @@ export const WorkspaceDashboard = forwardRef(
title: 'Workspace information',
persistenceId: `${persistenceId}/workspaceInfoPanelOpen`,
defaultPanelOpen: true,
workspace,
},
[h(WorkspaceInformation, { workspace })]
),
h(RightBoxSection, { title: 'Cloud information', persistenceId: `${persistenceId}/cloudInfoPanelOpen` }, [
h(CloudInformation, { workspace, storageDetails }),
]),
h(
RightBoxSection,
{ title: 'Cloud information', persistenceId: `${persistenceId}/cloudInfoPanelOpen`, workspace },
[h(CloudInformation, { workspace, storageDetails })]
),
h(
RightBoxSection,
{
title: 'Owners',
persistenceId: `${persistenceId}/ownersPanelOpen`,
afterTitle: OwnerNotice({ workspace }),
workspace,
},
[
div(
Expand All @@ -87,6 +91,7 @@ export const WorkspaceDashboard = forwardRef(
{
title: 'Authorization domain',
persistenceId: `${persistenceId}/authDomainPanelOpen`,
workspace,
},
[h(AuthDomainPanel, { workspace })]
),
Expand All @@ -96,6 +101,7 @@ export const WorkspaceDashboard = forwardRef(
{
title: 'Notifications',
persistenceId: `${persistenceId}/notificationsPanelOpen`,
workspace,
},
[h(WorkspaceNotifications, { workspace })]
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import _ from 'lodash/fp';
import { div, h } from 'react-hyperscript-helpers';
import { MarkdownEditor } from 'src/components/markdown';
import { Ajax } from 'src/libs/ajax';
import Events, { extractWorkspaceDetails } from 'src/libs/events';
import { canEditWorkspace } from 'src/libs/workspace-utils';
import { WorkspaceDescription } from 'src/pages/workspaces/workspace/Dashboard/WorkspaceDescription';
import { asMockedFn, renderWithAppContexts as render } from 'src/testing/test-utils';
Expand Down Expand Up @@ -80,6 +81,10 @@ describe('WorkspaceDescription', () => {
// Arrange
const user = userEvent.setup();
asMockedFn(canEditWorkspace).mockReturnValue({ value: true });
const captureEvent = jest.fn();
asMockedFn(Ajax).mockReturnValue({
Metrics: { captureEvent } as Partial<AjaxContract['Metrics']>,
} as DeepPartial<AjaxContract> as AjaxContract);
const props = {
workspace: _.merge(defaultGoogleWorkspace, { workspace: { attributes: { description: undefined } } }),
refreshWorkspace: jest.fn(),
Expand All @@ -98,12 +103,19 @@ describe('WorkspaceDescription', () => {
}),
expect.any(Object)
);
expect(captureEvent).toHaveBeenCalledWith(
Events.workspaceDashboardEditDescription,
extractWorkspaceDetails(defaultGoogleWorkspace)
);
});

it('initialized editing with the original workspace description', async () => {
// Arrange
const user = userEvent.setup();
asMockedFn(canEditWorkspace).mockReturnValue({ value: true });
asMockedFn(Ajax).mockReturnValue({
Metrics: { captureEvent: jest.fn() } as Partial<AjaxContract['Metrics']>,
} as DeepPartial<AjaxContract> as AjaxContract);
const description = 'this is a very descriptive decription';
const props = {
workspace: _.merge(defaultGoogleWorkspace, { workspace: { attributes: { description } } }),
Expand Down Expand Up @@ -133,7 +145,9 @@ describe('WorkspaceDescription', () => {
refreshWorkspace: jest.fn(),
};
const mockShallowMergeNewAttributes = jest.fn().mockResolvedValue({});
const captureEvent = jest.fn();
asMockedFn(Ajax).mockReturnValue({
Metrics: { captureEvent } as Partial<AjaxContract['Metrics']>,
Workspaces: {
workspace: jest.fn().mockReturnValue({
shallowMergeNewAttributes: mockShallowMergeNewAttributes,
Expand Down Expand Up @@ -162,5 +176,15 @@ describe('WorkspaceDescription', () => {

// Assert
expect(mockShallowMergeNewAttributes).toHaveBeenCalledWith({ description: newDescription });
expect(captureEvent).toHaveBeenNthCalledWith(
1,
Events.workspaceDashboardEditDescription,
extractWorkspaceDetails(defaultGoogleWorkspace)
);
expect(captureEvent).toHaveBeenNthCalledWith(
2,
Events.workspaceDashboardSaveDescription,
extractWorkspaceDetails(defaultGoogleWorkspace)
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { icon } from 'src/components/icons';
import { MarkdownEditor, MarkdownViewer } from 'src/components/markdown';
import { Ajax } from 'src/libs/ajax';
import { reportError } from 'src/libs/error';
import Events, { extractWorkspaceDetails } from 'src/libs/events';
import * as Style from 'src/libs/style';
import { withBusyState } from 'src/libs/utils';
import { canEditWorkspace, WorkspaceWrapper as Workspace } from 'src/libs/workspace-utils';
Expand Down Expand Up @@ -35,6 +36,7 @@ export const WorkspaceDescription = (props: WorkspaceDescriptionProps): ReactNod
const { namespace, name } = workspace.workspace;
await Ajax().Workspaces.workspace(namespace, name).shallowMergeNewAttributes({ description: editedDescription });
refreshWorkspace();
Ajax().Metrics.captureEvent(Events.workspaceDashboardSaveDescription, extractWorkspaceDetails(workspace));
} catch (error) {
reportError('Error saving workspace', error);
} finally {
Expand All @@ -52,7 +54,10 @@ export const WorkspaceDescription = (props: WorkspaceDescriptionProps): ReactNod
style: { marginLeft: '0.5rem' },
disabled: !canEdit,
tooltip: canEdit ? 'Edit description' : editErrorMessage,
onClick: () => setEditedDescription(description),
onClick: () => {
setEditedDescription(description);
Ajax().Metrics.captureEvent(Events.workspaceDashboardEditDescription, extractWorkspaceDetails(workspace));
},
},
[icon('edit')]
),
Expand Down
Loading

0 comments on commit d0dc5d2

Please sign in to comment.