Skip to content

Commit

Permalink
[WOR-1461] Emit events for clipboard copy events (WOR-1461). (#4638)
Browse files Browse the repository at this point in the history
  • Loading branch information
cahrens authored Feb 7, 2024
1 parent dc3d28b commit 130649d
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 29 deletions.
5 changes: 5 additions & 0 deletions src/libs/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ const eventsList = {
workspaceCreate: 'workspace:create',
workspaceDashboardToggleSection: 'workspace:dashboard:toggleSection',
workspaceDashboardAddTag: 'workspace:dashboard:addTag',
workspaceDashboardCopyGoogleProjectId: 'workspace:dashboard:copyGoogleProjectId',
workspaceDashboardCopyBucketName: 'workspace:dashboard:copyBucketName',
workspaceDashboardCopyResourceGroup: 'workspace:dashboard:copyResourceGroup',
workspaceDashboardCopySASUrl: 'workspace:dashboard:copySASUrl',
workspaceDashboardCopyStorageContainerUrl: 'workspace:dashboard:copyStorageContainerUrl',
workspaceDashboardDeleteTag: 'workspace:dashboard:deleteTag',
workspaceDashboardEditDescription: 'workspace:dashboard:editDescription',
workspaceDashboardSaveDescription: 'workspace:dashboard:saveDescription',
Expand Down
115 changes: 101 additions & 14 deletions src/workspaces/dashboard/AzureStorageDetails.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as clipboard from 'clipboard-polyfill/text';
import { axe } from 'jest-axe';
import _ from 'lodash/fp';
import { dl, h } from 'react-hyperscript-helpers';
import { Ajax } from 'src/libs/ajax';
import { azureRegions } from 'src/libs/azure-regions';
import { renderWithAppContexts as render } from 'src/testing/test-utils';
import { defaultAzureStorageOptions, defaultGoogleBucketOptions } from 'src/testing/workspace-fixtures';
import { AzureStorageDetails } from 'src/workspaces/dashboard/AzureStorageDetails';
import Events, { extractWorkspaceDetails } from 'src/libs/events';
import { asMockedFn, renderWithAppContexts as render } from 'src/testing/test-utils';
import {
defaultAzureStorageOptions,
defaultAzureWorkspace,
defaultGoogleBucketOptions,
} from 'src/testing/workspace-fixtures';
import { AzureStorageDetails, AzureStorageDetailsProps } from 'src/workspaces/dashboard/AzureStorageDetails';

type AjaxContract = ReturnType<typeof Ajax>;

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

type ClipboardPolyfillExports = typeof import('clipboard-polyfill/text');
jest.mock('clipboard-polyfill/text', (): ClipboardPolyfillExports => {
const actual = jest.requireActual<ClipboardPolyfillExports>('clipboard-polyfill/text');
return {
...actual,
writeText: jest.fn().mockResolvedValue(undefined),
};
});

describe('AzureStorageDetails', () => {
const azureContext = {
Expand All @@ -14,6 +35,18 @@ describe('AzureStorageDetails', () => {
tenantId: 'dummy-tenant-id',
};

const eventWorkspaceDetails = extractWorkspaceDetails(defaultAzureWorkspace);

const azureStorageDetailsProps: AzureStorageDetailsProps = {
azureContext,
storageDetails: _.merge(defaultGoogleBucketOptions, {
azureContainerRegion: 'westus',
azureContainerUrl: 'only-container-url',
azureContainerSasUrl: 'url-with-sas-token',
}),
eventWorkspaceDetails,
};

afterEach(() => {
jest.resetAllMocks();
});
Expand All @@ -23,6 +56,7 @@ describe('AzureStorageDetails', () => {
const props = {
azureContext,
storageDetails: _.merge(defaultGoogleBucketOptions, defaultAzureStorageOptions),
eventWorkspaceDetails,
};

// Act
Expand All @@ -37,18 +71,8 @@ describe('AzureStorageDetails', () => {
});

it('shows storage information when present', async () => {
// Arrange
const props = {
azureContext,
storageDetails: _.merge(defaultGoogleBucketOptions, {
azureContainerRegion: 'westus',
azureContainerUrl: 'only-container-url',
azureContainerSasUrl: 'url-with-sas-token',
}),
};

// Act
const { container } = render(dl([h(AzureStorageDetails, props)]));
const { container } = render(dl([h(AzureStorageDetails, azureStorageDetailsProps)]));

// Assert
expect(screen.queryByText('Loading')).toBeNull();
Expand All @@ -58,4 +82,67 @@ describe('AzureStorageDetails', () => {
expect(screen.getAllByText(/url-with-sas-token/)).not.toBeNull();
expect(await axe(container)).toHaveNoViolations();
});

it('emits an event when the copy resource group clipboard button is clicked', async () => {
// Arrange
const user = userEvent.setup();
const captureEvent = jest.fn();
asMockedFn(Ajax).mockImplementation(
() =>
({
Metrics: { captureEvent } as Partial<AjaxContract['Metrics']>,
} as Partial<AjaxContract> as AjaxContract)
);
render(dl([h(AzureStorageDetails, azureStorageDetailsProps)]));

// Act
const copyResourceGroup = screen.getByLabelText('Copy resource group ID to clipboard');
await user.click(copyResourceGroup);

// Assert
expect(captureEvent).toHaveBeenCalledWith(Events.workspaceDashboardCopyResourceGroup, eventWorkspaceDetails);
expect(clipboard.writeText).toHaveBeenCalledWith(azureContext.managedResourceGroupId);
});

it('emits an event when the copy storage container URL clipboard button is clicked', async () => {
// Arrange
const user = userEvent.setup();
const captureEvent = jest.fn();
asMockedFn(Ajax).mockImplementation(
() =>
({
Metrics: { captureEvent } as Partial<AjaxContract['Metrics']>,
} as Partial<AjaxContract> as AjaxContract)
);
render(dl([h(AzureStorageDetails, azureStorageDetailsProps)]));

// Act
const copyButton = screen.getByLabelText('Copy storage container URL to clipboard');
await user.click(copyButton);

// Assert
expect(captureEvent).toHaveBeenCalledWith(Events.workspaceDashboardCopyStorageContainerUrl, eventWorkspaceDetails);
expect(clipboard.writeText).toHaveBeenCalledWith(azureStorageDetailsProps.storageDetails.azureContainerUrl);
});

it('emits an event when the copy SAS URL clipboard button is clicked', async () => {
// Arrange
const user = userEvent.setup();
const captureEvent = jest.fn();
asMockedFn(Ajax).mockImplementation(
() =>
({
Metrics: { captureEvent } as Partial<AjaxContract['Metrics']>,
} as Partial<AjaxContract> as AjaxContract)
);
render(dl([h(AzureStorageDetails, azureStorageDetailsProps)]));

// Act
const copyButton = screen.getByLabelText('Copy SAS URL to clipboard');
await user.click(copyButton);

// Assert
expect(captureEvent).toHaveBeenCalledWith(Events.workspaceDashboardCopySASUrl, eventWorkspaceDetails);
expect(clipboard.writeText).toHaveBeenCalledWith(azureStorageDetailsProps.storageDetails.azureContainerSasUrl);
});
});
22 changes: 20 additions & 2 deletions src/workspaces/dashboard/AzureStorageDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ import { h } from 'react-hyperscript-helpers';
import { ClipboardButton } from 'src/components/ClipboardButton';
import { TooltipCell } from 'src/components/table';
import { ReactComponent as AzureLogo } from 'src/images/azure.svg';
import { Ajax } from 'src/libs/ajax';
import { getRegionFlag, getRegionLabel } from 'src/libs/azure-utils';
import Events, { EventWorkspaceDetails } from 'src/libs/events';
import { StorageDetails } from 'src/workspaces/common/state/useWorkspace';
import { InfoRow } from 'src/workspaces/dashboard/InfoRow';
import { AzureContext } from 'src/workspaces/utils';

interface AzureStorageDetailsProps {
export interface AzureStorageDetailsProps {
azureContext: AzureContext;
storageDetails: StorageDetails;
eventWorkspaceDetails: EventWorkspaceDetails;
}

export const AzureStorageDetails = (props: AzureStorageDetailsProps): ReactNode => {
const { azureContext, storageDetails } = props;
const { azureContext, storageDetails, eventWorkspaceDetails } = props;
return h(Fragment, [
h(InfoRow, { title: 'Cloud Name' }, [
h(AzureLogo, { title: 'Microsoft Azure', role: 'img', style: { height: 16 } }),
Expand All @@ -37,6 +40,11 @@ export const AzureStorageDetails = (props: AzureStorageDetailsProps): ReactNode
'aria-label': 'Copy resource group ID to clipboard',
text: azureContext.managedResourceGroupId,
style: { marginLeft: '0.25rem' },
onClick: (_) => {
Ajax().Metrics.captureEvent(Events.workspaceDashboardCopyResourceGroup, {
...eventWorkspaceDetails,
});
},
}),
]),
h(InfoRow, { title: 'Storage Container URL' }, [
Expand All @@ -45,6 +53,11 @@ export const AzureStorageDetails = (props: AzureStorageDetailsProps): ReactNode
'aria-label': 'Copy storage container URL to clipboard',
text: storageDetails.azureContainerUrl || '',
style: { marginLeft: '0.25rem' },
onClick: (_) => {
Ajax().Metrics.captureEvent(Events.workspaceDashboardCopyStorageContainerUrl, {
...eventWorkspaceDetails,
});
},
}),
]),
h(InfoRow, { title: 'Storage SAS URL' }, [
Expand All @@ -53,6 +66,11 @@ export const AzureStorageDetails = (props: AzureStorageDetailsProps): ReactNode
'aria-label': 'Copy SAS URL to clipboard',
text: storageDetails.azureContainerSasUrl || '',
style: { marginLeft: '0.25rem' },
onClick: (_) => {
Ajax().Metrics.captureEvent(Events.workspaceDashboardCopySASUrl, {
...eventWorkspaceDetails,
});
},
}),
]),
]);
Expand Down
86 changes: 74 additions & 12 deletions src/workspaces/dashboard/CloudInformation.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { DeepPartial } from '@terra-ui-packages/core-utils';
import { asMockedFn } from '@terra-ui-packages/test-utils';
import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as clipboard from 'clipboard-polyfill/text';
import { h } from 'react-hyperscript-helpers';
import { Ajax } from 'src/libs/ajax';
import Events, { extractWorkspaceDetails } from 'src/libs/events';
import { renderWithAppContexts as render } from 'src/testing/test-utils';
import {
defaultAzureStorageOptions,
Expand All @@ -23,7 +26,22 @@ jest.mock('src/libs/ajax', (): AjaxExports => {
};
});

type ClipboardPolyfillExports = typeof import('clipboard-polyfill/text');
jest.mock('clipboard-polyfill/text', (): ClipboardPolyfillExports => {
const actual = jest.requireActual<ClipboardPolyfillExports>('clipboard-polyfill/text');
return {
...actual,
writeText: jest.fn().mockResolvedValue(undefined),
};
});

describe('CloudInformation', () => {
const storageDetails: StorageDetails = {
googleBucketLocation: defaultGoogleBucketOptions.googleBucketLocation,
googleBucketType: defaultGoogleBucketOptions.googleBucketType,
fetchedGoogleBucketLocation: defaultGoogleBucketOptions.fetchedGoogleBucketLocation,
};

afterEach(() => {
jest.resetAllMocks();
});
Expand All @@ -50,12 +68,6 @@ describe('CloudInformation', () => {

it('does not retrieve bucket and storage estimate when the workspace is not initialized', async () => {
// Arrange
const storageDetails: StorageDetails = {
googleBucketLocation: defaultGoogleBucketOptions.googleBucketLocation,
googleBucketType: defaultGoogleBucketOptions.googleBucketType,
fetchedGoogleBucketLocation: defaultGoogleBucketOptions.fetchedGoogleBucketLocation,
};

const mockBucketUsage = jest.fn();
const mockStorageCostEstimate = jest.fn();
asMockedFn(Ajax).mockReturnValue({
Expand All @@ -81,12 +93,6 @@ describe('CloudInformation', () => {

it('retrieves bucket and storage estimate when the workspace is initialized', async () => {
// Arrange
const storageDetails: StorageDetails = {
googleBucketLocation: defaultGoogleBucketOptions.googleBucketLocation,
googleBucketType: defaultGoogleBucketOptions.googleBucketType,
fetchedGoogleBucketLocation: defaultGoogleBucketOptions.fetchedGoogleBucketLocation,
};

const mockBucketUsage = jest.fn().mockResolvedValue({ usageInBytes: 100, lastUpdated: '2023-12-01' });
const mockStorageCostEstimate = jest.fn().mockResolvedValue({
estimate: '1 million dollars',
Expand Down Expand Up @@ -114,4 +120,60 @@ describe('CloudInformation', () => {
expect(mockBucketUsage).toHaveBeenCalled();
expect(mockStorageCostEstimate).toHaveBeenCalled();
});

const copyButtonTestSetup = async () => {
const captureEvent = jest.fn();
const mockBucketUsage = jest.fn();
const mockStorageCostEstimate = jest.fn();
asMockedFn(Ajax).mockReturnValue({
Workspaces: {
workspace: jest.fn().mockReturnValue({
storageCostEstimate: mockStorageCostEstimate,
bucketUsage: mockBucketUsage,
}),
},
Metrics: { captureEvent } as Partial<AjaxContract['Metrics']>,
} as DeepPartial<AjaxContract> as AjaxContract);

await act(() =>
render(
h(CloudInformation, { workspace: { ...defaultGoogleWorkspace, workspaceInitialized: false }, storageDetails })
)
);
return captureEvent;
};

it('emits an event when the copy google project ID button is clicked', async () => {
// Arrange
const user = userEvent.setup();
const captureEvent = await copyButtonTestSetup();

// Act
const copyButton = screen.getByLabelText('Copy google project ID to clipboard');
await user.click(copyButton);

// Assert
expect(captureEvent).toHaveBeenCalledWith(
Events.workspaceDashboardCopyGoogleProjectId,
extractWorkspaceDetails(defaultGoogleWorkspace)
);
expect(clipboard.writeText).toHaveBeenCalledWith(defaultGoogleWorkspace.workspace.googleProject);
});

it('emits an event when the copy bucket name button is clicked', async () => {
// Arrange
const user = userEvent.setup();
const captureEvent = await copyButtonTestSetup();

// Act
const copyButton = screen.getByLabelText('Copy bucket name to clipboard');
await user.click(copyButton);

// Assert
expect(captureEvent).toHaveBeenCalledWith(
Events.workspaceDashboardCopyBucketName,
extractWorkspaceDetails(defaultGoogleWorkspace)
);
expect(clipboard.writeText).toHaveBeenCalledWith(defaultGoogleWorkspace.workspace.bucketName);
});
});
18 changes: 17 additions & 1 deletion src/workspaces/dashboard/CloudInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ const AzureCloudInformation = (props: AzureCloudInformationProps): ReactNode =>
const { workspace, storageDetails } = props;
const azureContext = workspace.azureContext;
return h(Fragment, [
dl([h(AzureStorageDetails, { azureContext, storageDetails })]),
dl([
h(AzureStorageDetails, {
azureContext,
storageDetails,
eventWorkspaceDetails: extractWorkspaceDetails(workspace),
}),
]),
div({ style: { margin: '0.5rem', fontSize: 12 } }, [
div([
'Use SAS URL in conjunction with ',
Expand Down Expand Up @@ -126,6 +132,11 @@ const GoogleCloudInformation = (props: GoogleCloudInformationProps): ReactNode =
'aria-label': 'Copy google project ID to clipboard',
text: googleProject,
style: { marginLeft: '0.25rem' },
onClick: (_) => {
Ajax().Metrics.captureEvent(Events.workspaceDashboardCopyGoogleProjectId, {
...extractWorkspaceDetails(workspace),
});
},
}),
]),
h(InfoRow, { title: 'Bucket Name' }, [
Expand All @@ -134,6 +145,11 @@ const GoogleCloudInformation = (props: GoogleCloudInformationProps): ReactNode =
'aria-label': 'Copy bucket name to clipboard',
text: bucketName,
style: { marginLeft: '0.25rem' },
onClick: (_) => {
Ajax().Metrics.captureEvent(Events.workspaceDashboardCopyBucketName, {
...extractWorkspaceDetails(workspace),
});
},
}),
]),
canWrite(accessLevel) &&
Expand Down

0 comments on commit 130649d

Please sign in to comment.