Skip to content

Commit 2731056

Browse files
authored
[AN-788] Add quota section to workspace dashboard (#5450)
1 parent 7c67cd9 commit 2731056

File tree

9 files changed

+355
-34
lines changed

9 files changed

+355
-34
lines changed

.pnp.cjs

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.
Binary file not shown.

src/libs/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ const eventsList = {
209209
workspacesListSelectTab: 'workspace:list:tab',
210210
workspaceFindWorkflowDockstore: 'workspace:find-workflow:dockstoreClick',
211211
workspaceFindWorkflowTerraRepo: 'workspace:find-workflow:terraWorkflowRepoClick',
212+
workspaceOpenQuotaInConsole: 'workspace:openedQuotaInConsole',
212213
} as const;
213214

214215
// Helper type to create BaseMetricsEventName.

src/workspaces/dashboard/CloudInformation.test.ts

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ describe('CloudInformation', () => {
9595

9696
await act(() =>
9797
render(
98-
h(CloudInformation, { workspace: { ...defaultGoogleWorkspace, workspaceInitialized: false }, storageDetails })
98+
h(CloudInformation, {
99+
workspace: { ...defaultGoogleWorkspace, workspaceInitialized: false },
100+
storageDetails,
101+
})
99102
)
100103
);
101104
return captureEvent;
@@ -161,7 +164,10 @@ describe('CloudInformation', () => {
161164
// Act
162165
await act(() =>
163166
render(
164-
h(CloudInformation, { workspace: { ...defaultGoogleWorkspace, workspaceInitialized: true }, storageDetails })
167+
h(CloudInformation, {
168+
workspace: { ...defaultGoogleWorkspace, workspaceInitialized: true },
169+
storageDetails,
170+
})
165171
)
166172
);
167173

@@ -211,27 +217,34 @@ describe('CloudInformation', () => {
211217
);
212218

213219
// Act
214-
render(
215-
h(CloudInformation, { workspace: { ...defaultGoogleWorkspace, workspaceInitialized: true }, storageDetails })
216-
);
217-
await user.click(screen.getByLabelText('More info'));
220+
await act(async () => {
221+
render(
222+
h(CloudInformation, { workspace: { ...defaultGoogleWorkspace, workspaceInitialized: true }, storageDetails })
223+
);
224+
});
225+
226+
const moreInfoButtons = screen.getAllByLabelText('More info');
227+
await user.click(moreInfoButtons[0]); // First button is in the storage details
218228

219229
// Assert
220230
expect(screen.getByText(/Only shows object storage costs/i)).toBeInTheDocument();
221231
expect(screen.getByText(/Based on GCP list prices/i)).toBeInTheDocument();
222232

223233
// Clicking the info button again should hide the tooltip
224-
await user.click(screen.getByLabelText('More info'));
234+
await user.click(moreInfoButtons[0]);
225235

226236
// Expect the tooltip content to disappear
227237
expect(screen.queryByText(/Only shows object storage costs/i)).not.toBeInTheDocument();
228238
});
229239

230240
it('displays bucket size for users with reader access', async () => {
231241
// Arrange
232-
const mockStorageCostEstimateV2 = jest
233-
.fn()
234-
.mockResolvedValue({ estimate: 1.23, usageInBytes: 50, usage: { 'live-object': 50 }, lastUpdated: '2024-07-26' });
242+
const mockStorageCostEstimateV2 = jest.fn().mockResolvedValue({
243+
estimate: 1.23,
244+
usageInBytes: 50,
245+
usage: { 'live-object': 50 },
246+
lastUpdated: '2024-07-26',
247+
});
235248
asMockedFn(Workspaces).mockReturnValue(
236249
partial<WorkspacesAjaxContract>({
237250
workspace: () => partial<WorkspaceContract>({ storageCostEstimateV2: mockStorageCostEstimateV2 }),
@@ -355,4 +368,38 @@ describe('CloudInformation', () => {
355368
extractWorkspaceDetails(defaultGoogleWorkspace)
356369
);
357370
});
371+
it('renders the quota section', async () => {
372+
// Arrange
373+
const mockStorageCostEstimateV2 = jest.fn().mockResolvedValue({
374+
estimate: 1000000,
375+
usageInBytes: 100,
376+
usage: {
377+
'live-object': 100,
378+
},
379+
lastUpdated: '2023-12-01',
380+
});
381+
asMockedFn(Workspaces).mockReturnValue(
382+
partial<WorkspacesAjaxContract>({
383+
workspace: () =>
384+
partial<WorkspaceContract>({
385+
storageCostEstimateV2: mockStorageCostEstimateV2,
386+
}),
387+
})
388+
);
389+
390+
// Act
391+
await act(() =>
392+
render(
393+
h(CloudInformation, {
394+
workspace: { ...defaultGoogleWorkspace, workspaceInitialized: true },
395+
storageDetails,
396+
})
397+
)
398+
);
399+
400+
// Assert
401+
expect(screen.getByText('Quota')).toBeInTheDocument();
402+
expect(screen.getByText('View quotas')).toBeInTheDocument();
403+
expect(screen.getByText('Open quota adjuster')).toBeInTheDocument();
404+
});
358405
});

src/workspaces/dashboard/CloudInformation.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { newTabLinkProps } from 'src/libs/utils';
1818
import { InitializedWorkspaceWrapper as Workspace, StorageDetails } from 'src/workspaces/common/state/useWorkspace';
1919
import { BucketLocation } from 'src/workspaces/dashboard/BucketLocation';
2020
import { InfoRow } from 'src/workspaces/dashboard/InfoRow';
21+
import { Quota } from 'src/workspaces/dashboard/Quota';
2122
import { canRead, canWrite, GoogleWorkspace, isGoogleWorkspace } from 'src/workspaces/utils';
2223

2324
interface CloudInformationProps {
@@ -113,6 +114,22 @@ const GoogleCloudInformation = (props: GoogleCloudInformationProps): ReactNode =
113114
},
114115
}),
115116
]),
117+
div({ style: { paddingBottom: '0.5rem' } }, [
118+
h(
119+
Link,
120+
{
121+
style: { margin: '1rem 0.5rem' },
122+
...newTabLinkProps,
123+
onClick: () => {
124+
void Metrics().captureEvent(Events.workspaceOpenedProjectInConsole, {
125+
...extractWorkspaceDetails(workspace),
126+
});
127+
},
128+
href: `https://console.cloud.google.com/welcome?project=${googleProject}&authuser=${getTerraUser().email}`,
129+
},
130+
['Open project in Google Cloud Console', icon('pop-out', { size: 12, style: { marginLeft: '0.25rem' } })]
131+
),
132+
]),
116133
h(hr),
117134
h(InfoRow, {
118135
title: h3({ style: { ...Style.dashboard.collapsibleHeader, padding: 0, margin: 0 } as CSSProperties }, [
@@ -191,22 +208,8 @@ const GoogleCloudInformation = (props: GoogleCloudInformationProps): ReactNode =
191208
['Open bucket in browser', icon('pop-out', { size: 12, style: { marginLeft: '0.25rem' } })]
192209
),
193210
]),
194-
div({ style: { paddingBottom: '0.5rem' } }, [
195-
h(
196-
Link,
197-
{
198-
style: { margin: '1rem 0.5rem' },
199-
...newTabLinkProps,
200-
onClick: () => {
201-
void Metrics().captureEvent(Events.workspaceOpenedProjectInConsole, {
202-
...extractWorkspaceDetails(workspace),
203-
});
204-
},
205-
href: `https://console.cloud.google.com/welcome?project=${googleProject}&authuser=${getTerraUser().email}`,
206-
},
207-
['Open project in Google Cloud Console', icon('pop-out', { size: 12, style: { marginLeft: '0.25rem' } })]
208-
),
209-
]),
211+
h(hr),
212+
h(Quota, { workspace }),
210213
]);
211214
};
212215

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { asMockedFn, partial } from '@terra-ui-packages/test-utils';
2+
import { screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { h } from 'react-hyperscript-helpers';
5+
import { Metrics, MetricsContract } from 'src/libs/ajax/Metrics';
6+
import Events, { extractWorkspaceDetails } from 'src/libs/events';
7+
import { renderWithAppContexts as render } from 'src/testing/test-utils';
8+
import { defaultGoogleWorkspace } from 'src/testing/workspace-fixtures';
9+
import { Quota } from 'src/workspaces/dashboard/Quota';
10+
11+
jest.mock('src/libs/ajax/Metrics');
12+
jest.mock('src/libs/ajax/workspaces/Workspaces');
13+
14+
describe('Quota', () => {
15+
afterEach(() => {
16+
jest.resetAllMocks();
17+
});
18+
19+
it('displays the quota section with view quotas link when user is a writer with canCompute', () => {
20+
// Arrange
21+
const workspace = { ...defaultGoogleWorkspace, workspaceInitialized: true };
22+
23+
// Act
24+
render(h(Quota, { workspace }));
25+
26+
// Assert
27+
const docLink = screen.getByText('View quotas');
28+
expect(docLink).toBeInTheDocument();
29+
expect(docLink.closest('a')).toHaveAttribute(
30+
'href',
31+
'https://console.cloud.google.com/iam-admin/quotas?project=test-gcp-ws-project&authuser=undefined'
32+
);
33+
});
34+
35+
it('shows enabled quota adjuster link for owners', () => {
36+
// Act
37+
render(h(Quota, { workspace: { ...defaultGoogleWorkspace, accessLevel: 'OWNER', workspaceInitialized: true } }));
38+
39+
// Assert
40+
const quotaAdjusterLink = screen.getByText('Open quota adjuster');
41+
expect(quotaAdjusterLink).toBeInTheDocument();
42+
43+
const linkElement = quotaAdjusterLink.closest('a');
44+
expect(linkElement).toHaveAttribute(
45+
'href',
46+
'https://console.cloud.google.com/iam-admin/quotas/configurations?project=test-gcp-ws-project&authuser=undefined'
47+
);
48+
expect(linkElement).not.toHaveStyle({ pointerEvents: 'none' });
49+
});
50+
51+
it('shows disabled quota adjuster link with tooltip for non-owners', async () => {
52+
// Arrange
53+
const user = userEvent.setup();
54+
55+
// Act
56+
render(h(Quota, { workspace: { ...defaultGoogleWorkspace, workspaceInitialized: true, accessLevel: 'WRITER' } }));
57+
58+
// Assert
59+
const quotaAdjusterLink = screen.getByText('Open quota adjuster');
60+
expect(quotaAdjusterLink).toBeInTheDocument();
61+
62+
const linkElement = quotaAdjusterLink.closest('a');
63+
expect(linkElement).toHaveStyle({ pointerEvents: 'none' });
64+
65+
// Hover over the link to trigger tooltip
66+
const spanWrapper = linkElement?.closest('span');
67+
await user.hover(spanWrapper!);
68+
69+
// Check that tooltip appears
70+
const tooltip = await screen.findByRole('tooltip');
71+
expect(tooltip).toHaveTextContent(
72+
'You do not have permission to adjust quotas for this project. Please contact your workspace owner(s) for assistance.'
73+
);
74+
});
75+
76+
it('emits an event when view quotas link is clicked', async () => {
77+
// Arrange
78+
const user = userEvent.setup();
79+
const captureEvent = jest.fn();
80+
const workspace = { ...defaultGoogleWorkspace, workspaceInitialized: true };
81+
82+
asMockedFn(Metrics).mockReturnValue(partial<MetricsContract>({ captureEvent }));
83+
84+
// Act
85+
render(h(Quota, { workspace }));
86+
const viewQuotasLink = screen.getByText('View quotas');
87+
await user.click(viewQuotasLink);
88+
89+
// Assert
90+
expect(captureEvent).toHaveBeenCalledWith(
91+
Events.workspaceOpenQuotaInConsole,
92+
extractWorkspaceDetails(defaultGoogleWorkspace)
93+
);
94+
});
95+
96+
it('shows disabled view quotas link with tooltip when user does not have canCompute', async () => {
97+
// Arrange
98+
const user = userEvent.setup();
99+
100+
// Act
101+
render(
102+
h(Quota, {
103+
workspace: {
104+
...defaultGoogleWorkspace,
105+
workspaceInitialized: true,
106+
accessLevel: 'WRITER',
107+
canCompute: false,
108+
},
109+
})
110+
);
111+
112+
// Assert
113+
const viewQuotasLink = screen.getByText('View quotas');
114+
expect(viewQuotasLink).toBeInTheDocument();
115+
116+
const linkElement = viewQuotasLink.closest('a');
117+
expect(linkElement).toHaveStyle({ pointerEvents: 'none' });
118+
119+
// Hover over the link to trigger tooltip
120+
const spanWrapper = linkElement?.closest('span');
121+
await user.hover(spanWrapper!);
122+
123+
// Check that tooltip appears
124+
const tooltip = await screen.findByRole('tooltip');
125+
expect(tooltip).toHaveTextContent(
126+
'You do not have permission to view quotas for this project. Please contact your workspace owner(s) for assistance.'
127+
);
128+
});
129+
130+
it('renders quota documentation link in info box', async () => {
131+
// Arrange
132+
const user = userEvent.setup();
133+
const workspace = { ...defaultGoogleWorkspace, workspaceInitialized: true };
134+
135+
// Act
136+
render(h(Quota, { workspace }));
137+
const infoButton = screen.getByLabelText('More info');
138+
await user.click(infoButton);
139+
140+
// Assert
141+
expect(screen.getByText('For more information, please refer to the')).toBeInTheDocument();
142+
const docLink = screen.getByText('resource quota documentation');
143+
expect(docLink).toBeInTheDocument();
144+
expect(docLink.closest('a')).toHaveAttribute(
145+
'href',
146+
'https://support.terra.bio/hc/en-us/articles/6396351981595-Are-resource-quotas-slowing-your-analysis-down'
147+
);
148+
});
149+
});

0 commit comments

Comments
 (0)