Skip to content

Commit

Permalink
[CORE-175] use getStorageCostEstimateV2 instead of v2 and getBucketUs…
Browse files Browse the repository at this point in the history
…age (#5249)
  • Loading branch information
calypsomatic authored Feb 14, 2025
1 parent 8040da1 commit 0f75d5d
Show file tree
Hide file tree
Showing 12 changed files with 62 additions and 156 deletions.
10 changes: 2 additions & 8 deletions src/libs/ajax/workspaces/Workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { GoogleStorage } from 'src/libs/ajax/GoogleStorage';
import { FieldsArg } from 'src/libs/ajax/workspaces/providers/WorkspaceProvider';
import {
AttributeEntityReference,
BucketUsageResponse,
EntityUpdateDefinition,
MethodConfiguration,
RawWorkspaceAcl,
Expand Down Expand Up @@ -606,9 +605,9 @@ export const Workspaces = (signal?: AbortSignal) => ({
return res.blob();
},

storageCostEstimate: async (): Promise<StorageCostEstimate> => {
storageCostEstimateV2: async (): Promise<StorageCostEstimate> => {
const res = await fetchOrchestration(
`api/workspaces/${namespace}/${name}/storageCostEstimate`,
`api/workspaces/v2/${namespace}/${name}/storageCostEstimate`,
_.merge(authOpts(), { signal })
);
return res.json();
Expand Down Expand Up @@ -638,11 +637,6 @@ export const Workspaces = (signal?: AbortSignal) => ({
return res.json();
},

bucketUsage: async (): Promise<BucketUsageResponse> => {
const res = await fetchRawls(`${root}/bucketUsage`, _.merge(authOpts(), { signal }));
return res.json();
},

listActiveFileTransfers: async (): Promise<any[]> => {
const res = await fetchRawls(`${root}/fileTransfers`, _.merge(authOpts(), { signal }));
return res.json();
Expand Down
6 changes: 1 addition & 5 deletions src/libs/ajax/workspaces/workspace-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,7 @@ export interface AttributeEntityReference {
}

export interface StorageCostEstimate {
estimate: string;
lastUpdated?: string;
}

export interface BucketUsageResponse {
estimate: number;
usageInBytes: number;
lastUpdated?: string;
}
Expand Down
3 changes: 1 addition & 2 deletions src/pages/workspaces/workspace/Workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ describe('Find Workflow modal in Workflows view', () => {
partial<WorkspaceContract>({
details: jest.fn().mockResolvedValue(mockGoogleWorkspace),
checkBucketReadAccess: jest.fn(),
storageCostEstimate: jest.fn(),
bucketUsage: jest.fn(),
storageCostEstimateV2: jest.fn(),
checkBucketLocation: jest.fn().mockResolvedValue(mockStorageDetails),
listMethodConfigs: jest.fn(),
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,7 @@ describe('Workflow View (GCP)', () => {
gcpDataRepoSnapshots: [],
}),
checkBucketReadAccess: jest.fn(),
storageCostEstimate: jest.fn(),
bucketUsage: jest.fn(),
storageCostEstimateV2: jest.fn(),
checkBucketLocation: jest.fn().mockResolvedValue(mockStorageDetails),
methodConfig: () => ({
save: jest.fn().mockReturnValue(mockSave),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { act } from '@testing-library/react';
import { generateTestApp } from 'src/analysis/_testData/testData';
import { Apps, AppsAjaxContract } from 'src/libs/ajax/leonardo/Apps';
import { Runtimes, RuntimesAjaxContract } from 'src/libs/ajax/leonardo/Runtimes';
import { BucketUsageResponse, RawAccessEntry } from 'src/libs/ajax/workspaces/workspace-models';
import { RawAccessEntry, StorageCostEstimate } from 'src/libs/ajax/workspaces/workspace-models';
import {
WorkspaceContract,
Workspaces,
Expand Down Expand Up @@ -73,12 +73,13 @@ describe('useDeleteWorkspaceState', () => {

const getAcl: WorkspaceContract['getAcl'] = jest.fn();
asMockedFn(getAcl).mockResolvedValue({ acl: { '[email protected]': partial<RawAccessEntry>({}) } });
const bucketUsage: WorkspaceContract['bucketUsage'] = jest.fn();
asMockedFn(bucketUsage).mockResolvedValue({ usageInBytes: 1234 });

const storageCostEstimateV2: WorkspaceContract['storageCostEstimateV2'] = jest.fn();
asMockedFn(storageCostEstimateV2).mockResolvedValue({ estimate: 0.02, usageInBytes: 1234 });

asMockedFn(Workspaces).mockReturnValue(
partial<WorkspacesAjaxContract>({
workspace: () => partial<WorkspaceContract>({ getAcl, bucketUsage }),
workspace: () => partial<WorkspaceContract>({ getAcl, storageCostEstimateV2 }),
})
);
asMockedFn(Apps).mockReturnValue(partial<AppsAjaxContract>({ listWithoutProject }));
Expand All @@ -99,7 +100,7 @@ describe('useDeleteWorkspaceState', () => {
saturnWorkspaceName: googleWorkspace.workspace.name,
});
expect(getAcl).toHaveBeenCalledTimes(1);
expect(bucketUsage).toHaveBeenCalledTimes(1);
expect(storageCostEstimateV2).toHaveBeenCalledTimes(1);
});

it('can initialize state for an azure workspace', async () => {
Expand Down Expand Up @@ -191,7 +192,7 @@ describe('useDeleteWorkspaceState', () => {
workspace: () =>
partial<WorkspaceContract>({
getAcl: async () => ({ acl: {} }),
bucketUsage: async () => partial<BucketUsageResponse>({}),
storageCostEstimateV2: async () => partial<StorageCostEstimate>({}),
}),
workspaceV2: () =>
partial<WorkspaceV2Contract>({
Expand Down Expand Up @@ -227,7 +228,7 @@ describe('useDeleteWorkspaceState', () => {
workspace: () =>
partial<WorkspaceContract>({
getAcl: async () => ({ acl: {} }),
bucketUsage: async () => partial<BucketUsageResponse>({}),
storageCostEstimateV2: async () => partial<StorageCostEstimate>({}),
}),
workspaceV2: () =>
partial<WorkspaceV2Contract>({
Expand Down Expand Up @@ -263,7 +264,7 @@ describe('useDeleteWorkspaceState', () => {
workspace: () =>
partial<WorkspaceContract>({
getAcl: async () => ({ acl: {} }),
bucketUsage: async () => {
storageCostEstimateV2: async () => {
throw new Error('no project!');
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const useDeleteWorkspaceState = (hookArgs: DeleteWorkspaceHookArgs): Dele
Workspaces(signal).workspace(workspaceInfo.namespace, workspaceInfo.name).getAcl(),
Workspaces(signal)
.workspace(workspaceInfo.namespace, workspaceInfo.name)
.bucketUsage()
.storageCostEstimateV2()
.catch((_error) => undefined),
]);
setCollaboratorEmails(_.without([getTerraUser().email!], _.keys(acl)));
Expand Down
63 changes: 4 additions & 59 deletions src/workspaces/common/state/useWorkspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,7 @@ describe('useWorkspace', () => {
partial<WorkspaceContract>({
checkBucketLocation: jest.fn().mockResolvedValue(bucketLocationResponse),
checkBucketReadAccess: jest.fn(),
storageCostEstimate: jest.fn(),
bucketUsage: jest.fn(),
storageCostEstimateV2: jest.fn(),
}),
})
);
Expand Down Expand Up @@ -345,57 +344,6 @@ describe('useWorkspace', () => {
expect(captureEventFn).toHaveBeenCalledTimes(1);
});

it('can read workspace details from server, and poll until permissions synced (handling storageCostEstimate failure)', async () => {
// Arrange
// remove workspaceInitialized because the server response does not include this information
const { workspaceInitialized, ...serverWorkspaceResponse } = initializedGoogleWorkspace;

asMockedFn(Workspaces).mockReturnValue(
partial<WorkspacesAjaxContract>({
workspace: () =>
partial<WorkspaceContract>({
details: jest.fn().mockResolvedValue(serverWorkspaceResponse),
checkBucketLocation: jest.fn().mockResolvedValue(bucketLocationResponse),
checkBucketReadAccess: jest.fn(),
storageCostEstimate: () =>
Promise.reject(new Response('Mock storage cost estimate error', { status: 500 })),
bucketUsage: jest.fn(),
}),
})
);

// Verify initial failure based on error mock.
const { result } = await verifyGooglePermissionsFailure();

// Finally, change mock to pass all checks verify success.
await verifyGooglePermissionsSuccess(result);
});

it('can read workspace details from server, and poll until permissions synced (handling bucketUsage failure)', async () => {
// Arrange
// remove workspaceInitialized because the server response does not include this information
const { workspaceInitialized, ...serverWorkspaceResponse } = initializedGoogleWorkspace;

asMockedFn(Workspaces).mockReturnValue(
partial<WorkspacesAjaxContract>({
workspace: () =>
partial<WorkspaceContract>({
details: jest.fn().mockResolvedValue(serverWorkspaceResponse),
checkBucketLocation: jest.fn().mockResolvedValue(bucketLocationResponse),
checkBucketReadAccess: jest.fn(),
storageCostEstimate: jest.fn(),
bucketUsage: () => Promise.reject(new Response('Mock bucket usage error', { status: 500 })),
}),
})
);

// Verify initial failure based on error mock.
const { result } = await verifyGooglePermissionsFailure();

// Finally, change mock to pass all checks verify success.
await verifyGooglePermissionsSuccess(result);
});

it('can read workspace details from server, and poll until permissions synced (handling checkBucketLocation failure)', async () => {
// Arrange
// remove workspaceInitialized because the server response does not include this information
Expand All @@ -408,8 +356,7 @@ describe('useWorkspace', () => {
details: jest.fn().mockResolvedValue(serverWorkspaceResponse),
checkBucketLocation: () => Promise.reject(new Response('Mock check bucket location', { status: 500 })),
checkBucketReadAccess: jest.fn(),
storageCostEstimate: jest.fn(),
bucketUsage: jest.fn(),
storageCostEstimateV2: jest.fn(),
}),
})
);
Expand Down Expand Up @@ -437,8 +384,7 @@ describe('useWorkspace', () => {
details: jest.fn().mockResolvedValue(serverWorkspaceResponse),
checkBucketLocation: jest.fn().mockResolvedValue(bucketLocationResponse),
checkBucketReadAccess: jest.fn(),
storageCostEstimate: () => Promise.reject(new Response('Should not call', { status: 500 })),
bucketUsage: () => Promise.reject(new Response('Should not call', { status: 500 })),
storageCostEstimateV2: () => Promise.reject(new Response('Should not call', { status: 500 })),
}),
})
);
Expand Down Expand Up @@ -476,8 +422,7 @@ describe('useWorkspace', () => {
partial<WorkspaceContract>({
details: jest.fn().mockResolvedValue(serverWorkspaceResponse),
checkBucketReadAccess: jest.fn(),
storageCostEstimate: jest.fn(),
bucketUsage: jest.fn(),
storageCostEstimateV2: jest.fn(),
checkBucketLocation: checkBucketLocationMock,
}),
})
Expand Down
7 changes: 0 additions & 7 deletions src/workspaces/common/state/useWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,6 @@ export const useWorkspace = (namespace, name): WorkspaceDetails => {
// to be done syncing until all the methods that we know will be called quickly in succession succeed.
// This is not guaranteed to eliminate the issue, but it improves the odds.
await Workspaces(signal).workspace(namespace, name).checkBucketReadAccess();
if (canWrite(workspace.accessLevel)) {
// Calls done on the Workspace Dashboard. We could store the results and pass them
// through, but then we would have to do it checkWorkspaceInitialization as well,
// and nobody else actually needs these values.
await Workspaces(signal).workspace(namespace, name).storageCostEstimate();
await Workspaces(signal).workspace(namespace, name).bucketUsage();
}
await loadGoogleBucketLocation();
updateWorkspaceInStore(workspace, true);
} catch (error: any) {
Expand Down
53 changes: 22 additions & 31 deletions src/workspaces/dashboard/CloudInformation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,12 @@ describe('CloudInformation', () => {

it('does not retrieve bucket and storage estimate when the workspace is not initialized', async () => {
// Arrange
const mockBucketUsage = jest.fn();
const mockStorageCostEstimate = jest.fn();
const mockStorageCostEstimateV2 = jest.fn();
asMockedFn(Workspaces).mockReturnValue(
partial<WorkspacesAjaxContract>({
workspace: () =>
partial<WorkspaceContract>({
storageCostEstimate: mockStorageCostEstimate,
bucketUsage: mockBucketUsage,
storageCostEstimateV2: mockStorageCostEstimateV2,
}),
})
);
Expand All @@ -81,23 +79,21 @@ describe('CloudInformation', () => {
// Assert
expect(screen.getByTitle('Google Cloud Platform')).not.toBeNull;

expect(mockBucketUsage).not.toHaveBeenCalled();
expect(mockStorageCostEstimate).not.toHaveBeenCalled();
expect(mockStorageCostEstimateV2).not.toHaveBeenCalled();
});

it('retrieves bucket and storage estimate when the workspace is initialized', async () => {
// Arrange
const mockBucketUsage = jest.fn().mockResolvedValue({ usageInBytes: 100, lastUpdated: '2023-11-01' });
const mockStorageCostEstimate = jest.fn().mockResolvedValue({
estimate: '1 million dollars',
const mockStorageCostEstimateV2 = jest.fn().mockResolvedValue({
estimate: 1000000,
usageInBytes: 100,
lastUpdated: '2023-12-01',
});
asMockedFn(Workspaces).mockReturnValue(
partial<WorkspacesAjaxContract>({
workspace: () =>
partial<WorkspaceContract>({
storageCostEstimate: mockStorageCostEstimate,
bucketUsage: mockBucketUsage,
storageCostEstimateV2: mockStorageCostEstimateV2,
}),
})
);
Expand All @@ -112,26 +108,22 @@ describe('CloudInformation', () => {
// Assert
expect(screen.getByTitle('Google Cloud Platform')).not.toBeNull;
// Cost estimate
expect(screen.getByText('Updated on 12/1/2023')).not.toBeNull();
expect(screen.getByText('1 million dollars')).not.toBeNull();
expect(screen.getAllByText('Updated on 12/1/2023')).not.toBeNull();
expect(screen.getByText('$1,000,000.00')).not.toBeNull();
// Bucket usage
expect(screen.getByText('Updated on 11/1/2023')).not.toBeNull();
expect(screen.getByText('100 B')).not.toBeNull();

expect(mockBucketUsage).toHaveBeenCalled();
expect(mockStorageCostEstimate).toHaveBeenCalled();
expect(mockStorageCostEstimateV2).toHaveBeenCalled();
});

const copyButtonTestSetup = async () => {
const captureEvent = jest.fn();
const mockBucketUsage = jest.fn();
const mockStorageCostEstimate = jest.fn();
const mockStorageCostEstimateV2 = jest.fn();
asMockedFn(Workspaces).mockReturnValue(
partial<WorkspacesAjaxContract>({
workspace: () =>
partial<WorkspaceContract>({
storageCostEstimate: mockStorageCostEstimate,
bucketUsage: mockBucketUsage,
storageCostEstimateV2: mockStorageCostEstimateV2,
}),
})
);
Expand Down Expand Up @@ -182,17 +174,16 @@ describe('CloudInformation', () => {
it('can use the info button to display additional information about cost', async () => {
// Arrange
const user = userEvent.setup();
const mockBucketUsage = jest.fn().mockResolvedValue({ usageInBytes: 15, lastUpdated: '2024-07-15' });
const mockStorageCostEstimate = jest.fn().mockResolvedValue({
estimate: '2 dollars',
const mockStorageCostEstimateV2 = jest.fn().mockResolvedValue({
estimate: 2.0,
usageInBytes: 15,
lastUpdated: '2024-07-15',
});
asMockedFn(Workspaces).mockReturnValue(
partial<WorkspacesAjaxContract>({
workspace: () =>
partial<WorkspaceContract>({
storageCostEstimate: mockStorageCostEstimate,
bucketUsage: mockBucketUsage,
storageCostEstimateV2: mockStorageCostEstimateV2,
}),
})
);
Expand All @@ -204,17 +195,17 @@ describe('CloudInformation', () => {
await user.click(screen.getByLabelText('More info'));

// Assert
expect(
screen.getAllByText('Based on list price. Does not include savings from Autoclass or other discounts.')
).not.toBeNull();
expect(screen.getAllByText('Based on list price. Does not include discounts.')).not.toBeNull();
});

it('displays bucket size for users with reader access', async () => {
// Arrange
const mockBucketUsage = jest.fn().mockResolvedValue({ usageInBytes: 50, lastUpdated: '2024-07-26' });
const mockStorageCostEstimateV2 = jest
.fn()
.mockResolvedValue({ estimate: 1.23, usageInBytes: 50, lastUpdated: '2024-07-26' });
asMockedFn(Workspaces).mockReturnValue(
partial<WorkspacesAjaxContract>({
workspace: () => partial<WorkspaceContract>({ bucketUsage: mockBucketUsage }),
workspace: () => partial<WorkspaceContract>({ storageCostEstimateV2: mockStorageCostEstimateV2 }),
})
);

Expand All @@ -231,6 +222,6 @@ describe('CloudInformation', () => {
// Assert
expect(screen.getByText('Updated on 7/26/2024')).not.toBeNull();
expect(screen.getByText('50 B')).not.toBeNull();
expect(mockBucketUsage).toHaveBeenCalled();
expect(mockStorageCostEstimateV2).toHaveBeenCalled();
});
});
Loading

0 comments on commit 0f75d5d

Please sign in to comment.