From 40bdb61247e3188cdd1b2f9425567d67084592db Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Tue, 12 Nov 2024 15:28:35 -0500 Subject: [PATCH 01/41] Added basic tests. --- .../profile/initial-credits-panel.spec.tsx | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 ui/src/app/pages/profile/initial-credits-panel.spec.tsx diff --git a/ui/src/app/pages/profile/initial-credits-panel.spec.tsx b/ui/src/app/pages/profile/initial-credits-panel.spec.tsx new file mode 100644 index 00000000000..d7c3f408986 --- /dev/null +++ b/ui/src/app/pages/profile/initial-credits-panel.spec.tsx @@ -0,0 +1,49 @@ +import '@testing-library/jest-dom'; + +import * as React from 'react'; + +import { screen } from '@testing-library/dom'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { InitialCreditsPanel } from './initial-credits-panel'; + +const setup = ( + freeTierUsage: number, + freeTierDollarQuota: number, + expirationDate: number +) => { + return { + container: render( + + ).container, + user: userEvent.setup(), + }; +}; + +it('should display initial credits for a user with an expiration record', async () => { + const freeTierUsage = 100.0; + const freeTierDollarQuota = 234; + const expirationDate = new Date('2023-12-03T20:00:00Z').getTime(); + setup(freeTierUsage, freeTierDollarQuota, expirationDate); + expect(screen.getByText(`$100.00`)).toBeInTheDocument(); + // This should reflect the quota minus the usage + expect(screen.getByText(`$134.00`)).toBeInTheDocument(); + expect( + screen.getByText(`initial credits epiration date:`) + ).toBeInTheDocument(); + expect(screen.getByText(`Dec 3, 2023`)).toBeInTheDocument(); +}); + +it('should display initial credits for a user without an expiration record', async () => { + const freeTierUsage = 100.0; + const freeTierDollarQuota = 234; + const expirationDate = null; + setup(freeTierUsage, freeTierDollarQuota, expirationDate); + expect(screen.getByText(`$100.00`)).toBeInTheDocument(); + expect( + screen.queryByText(`initial credits epiration date:`) + ).not.toBeInTheDocument(); +}); From cc7918ca5bbc8959e5e89801691f85411d9fe118 Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Tue, 12 Nov 2024 15:29:10 -0500 Subject: [PATCH 02/41] Add expiration date. --- .../pages/profile/initial-credits-panel.tsx | 53 +++++++++++-------- .../app/pages/profile/profile-component.tsx | 3 ++ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/ui/src/app/pages/profile/initial-credits-panel.tsx b/ui/src/app/pages/profile/initial-credits-panel.tsx index 8d5fc11fc85..071e88cd794 100644 --- a/ui/src/app/pages/profile/initial-credits-panel.tsx +++ b/ui/src/app/pages/profile/initial-credits-panel.tsx @@ -3,32 +3,43 @@ import * as React from 'react'; import { FlexColumn, FlexRow } from 'app/components/flex'; import { AoU } from 'app/components/text-wrappers'; import { formatInitialCreditsUSD } from 'app/utils'; +import { displayDateWithoutHours } from 'app/utils/dates'; import { styles } from './profile-styles'; interface Props { freeTierUsage: number; freeTierDollarQuota: number; + expirationDate: number; } -export const InitialCreditsPanel = (props: Props) => ( - - -
- initial credits used: -
-
- Remaining initial credits: -
-
- -
- {formatInitialCreditsUSD(props.freeTierUsage)} -
-
- {formatInitialCreditsUSD( - props.freeTierDollarQuota - (props.freeTierUsage ?? 0) +export const InitialCreditsPanel = (props: Props) => { + const { expirationDate, freeTierUsage, freeTierDollarQuota } = props; + return ( + + +
+ initial credits used: +
+
+ Remaining initial credits: +
+ {expirationDate && ( +
+ initial credits epiration date: +
)} -
-
-
-); + + +
+ {formatInitialCreditsUSD(freeTierUsage)} +
+
+ {formatInitialCreditsUSD(freeTierDollarQuota - (freeTierUsage ?? 0))} +
+
+ {displayDateWithoutHours(expirationDate)} +
+
+ + ); +}; diff --git a/ui/src/app/pages/profile/profile-component.tsx b/ui/src/app/pages/profile/profile-component.tsx index aee90471b27..dca185d142c 100644 --- a/ui/src/app/pages/profile/profile-component.tsx +++ b/ui/src/app/pages/profile/profile-component.tsx @@ -556,6 +556,9 @@ export const ProfileComponent = fp.flow( )} From dee730bb41f50809e3317863b28e65c613a9dba0 Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Tue, 12 Nov 2024 15:29:53 -0500 Subject: [PATCH 03/41] Update styling --- ui/src/app/pages/profile/profile-styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/pages/profile/profile-styles.ts b/ui/src/app/pages/profile/profile-styles.ts index 689c7ec9d0f..509f5cb4668 100644 --- a/ui/src/app/pages/profile/profile-styles.ts +++ b/ui/src/app/pages/profile/profile-styles.ts @@ -71,11 +71,11 @@ export const styles = reactStyles({ }, initialCreditsBox: { borderRadius: '0.6rem', - height: '4.5rem', marginTop: '1.05rem', marginBottom: '2.55rem', color: colors.primary, backgroundColor: colorWithWhiteness(colors.disabled, 0.7), + padding: '1rem', }, updateSurveyButton: { textTransform: 'none', From e76615e81e3fe41cd41219374ca5f940fb07806a Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Tue, 12 Nov 2024 16:12:35 -0500 Subject: [PATCH 04/41] Add checks for extension button. --- ui/src/app/pages/profile/initial-credits-panel.spec.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/src/app/pages/profile/initial-credits-panel.spec.tsx b/ui/src/app/pages/profile/initial-credits-panel.spec.tsx index d7c3f408986..e6b7ab2e99f 100644 --- a/ui/src/app/pages/profile/initial-credits-panel.spec.tsx +++ b/ui/src/app/pages/profile/initial-credits-panel.spec.tsx @@ -35,6 +35,9 @@ it('should display initial credits for a user with an expiration record', async screen.getByText(`initial credits epiration date:`) ).toBeInTheDocument(); expect(screen.getByText(`Dec 3, 2023`)).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /request credit extension/i }) + ).toBeInTheDocument(); }); it('should display initial credits for a user without an expiration record', async () => { @@ -46,4 +49,7 @@ it('should display initial credits for a user without an expiration record', asy expect( screen.queryByText(`initial credits epiration date:`) ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /request credit extension/i }) + ).not.toBeInTheDocument(); }); From a5ff2f3baecf2a3cb9c4e0cbda284a204ca6f63f Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Tue, 12 Nov 2024 16:13:28 -0500 Subject: [PATCH 05/41] Add extension and info buttons. --- ui/src/app/components/buttons.tsx | 13 +++ .../pages/profile/initial-credits-panel.tsx | 79 +++++++++++++------ 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/ui/src/app/components/buttons.tsx b/ui/src/app/components/buttons.tsx index 1de34436d16..5116b16b0ad 100644 --- a/ui/src/app/components/buttons.tsx +++ b/ui/src/app/components/buttons.tsx @@ -115,6 +115,19 @@ const buttonVariants = { }, hover: { backgroundColor: colorWithWhiteness(colors.primary, hoverAlpha) }, }, + primarySmall: { + style: { + ...styles.baseNew, + borderRadius: '0.23rem', + height: '30px', + backgroundColor: colors.primary, + color: colors.white, + }, + disabledStyle: { + backgroundColor: colorWithWhiteness(colors.dark, disabledAlpha), + }, + hover: { backgroundColor: colorWithWhiteness(colors.primary, hoverAlpha) }, + }, secondary: { style: { ...styles.baseNew, diff --git a/ui/src/app/pages/profile/initial-credits-panel.tsx b/ui/src/app/pages/profile/initial-credits-panel.tsx index 071e88cd794..cddf53ec554 100644 --- a/ui/src/app/pages/profile/initial-credits-panel.tsx +++ b/ui/src/app/pages/profile/initial-credits-panel.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; +import { Button } from 'app/components/buttons'; import { FlexColumn, FlexRow } from 'app/components/flex'; import { AoU } from 'app/components/text-wrappers'; import { formatInitialCreditsUSD } from 'app/utils'; @@ -15,31 +16,61 @@ interface Props { export const InitialCreditsPanel = (props: Props) => { const { expirationDate, freeTierUsage, freeTierDollarQuota } = props; return ( - - -
- initial credits used: -
-
- Remaining initial credits: -
- {expirationDate && ( +
+ +
- initial credits epiration date: + initial credits used:
- )} -
- -
- {formatInitialCreditsUSD(freeTierUsage)} -
-
- {formatInitialCreditsUSD(freeTierDollarQuota - (freeTierUsage ?? 0))} -
-
- {displayDateWithoutHours(expirationDate)} -
-
-
+
+ Remaining initial credits: +
+ {expirationDate && ( +
+ initial credits epiration date: +
+ )} + + +
+ {formatInitialCreditsUSD(freeTierUsage)} +
+
+ {formatInitialCreditsUSD( + freeTierDollarQuota - (freeTierUsage ?? 0) + )} +
+ {expirationDate && ( +
+ {displayDateWithoutHours(expirationDate)} +
+ )} +
+ + {expirationDate && ( + + + + )} + + + +
); }; From c21412918c6ae97a1d7cce320ef95b930c1251d6 Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Mon, 18 Nov 2024 11:48:15 -0500 Subject: [PATCH 06/41] Added extension field. --- .../pmiops/workbench/utils/mappers/WorkspaceMapper.java | 8 ++++++++ api/src/main/resources/workbench-api.yaml | 3 +++ 2 files changed, 11 insertions(+) diff --git a/api/src/main/java/org/pmiops/workbench/utils/mappers/WorkspaceMapper.java b/api/src/main/java/org/pmiops/workbench/utils/mappers/WorkspaceMapper.java index b80e2a2c3a3..36f183a12c3 100644 --- a/api/src/main/java/org/pmiops/workbench/utils/mappers/WorkspaceMapper.java +++ b/api/src/main/java/org/pmiops/workbench/utils/mappers/WorkspaceMapper.java @@ -53,6 +53,10 @@ public interface WorkspaceMapper { target = "initialCredits.expirationEpochMillis", source = "dbWorkspace.creator", qualifiedByName = "getInitialCreditsExpiration") + @Mapping( + target = "initialCredits.extensionEpochMillis", + source = "dbWorkspace.creator", + qualifiedByName = "getInitialCreditsExpiration") @Mapping(target = "cdrVersionId", source = "dbWorkspace.cdrVersion") @Mapping(target = "accessTierShortName", source = "dbWorkspace.cdrVersion.accessTier.shortName") @Mapping(target = "googleProject", source = "dbWorkspace.googleProject") @@ -128,6 +132,10 @@ default List toApiWorkspaceResponseList( target = "initialCredits.expirationEpochMillis", source = "creator", qualifiedByName = "getInitialCreditsExpiration") + @Mapping( + target = "initialCredits.extensionEpochMillis", + source = "creator", + qualifiedByName = "getInitialCreditsExpiration") @Mapping(target = "initialCredits.exhausted", source = "dbWorkspace.initialCreditsExhausted") @Mapping(target = "etag", source = "version", qualifiedByName = "versionToEtag") @Mapping( diff --git a/api/src/main/resources/workbench-api.yaml b/api/src/main/resources/workbench-api.yaml index 1752c46ba6f..a31cba5a2af 100644 --- a/api/src/main/resources/workbench-api.yaml +++ b/api/src/main/resources/workbench-api.yaml @@ -13047,6 +13047,9 @@ components: expirationEpochMillis: type: integer format: int64 + extensionEpochMillis: + type: integer + format: int64 parameters: userId: name: userId From cd7f7d3b24bdbaa45c26e2f2fb8289852ce28715 Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Mon, 18 Nov 2024 11:48:50 -0500 Subject: [PATCH 07/41] First pass at banners --- .../workspace/invalid-billing-banner.tsx | 130 ++++++++++++++++-- 1 file changed, 117 insertions(+), 13 deletions(-) diff --git a/ui/src/app/pages/workspace/invalid-billing-banner.tsx b/ui/src/app/pages/workspace/invalid-billing-banner.tsx index 42b5edda038..c911a143439 100644 --- a/ui/src/app/pages/workspace/invalid-billing-banner.tsx +++ b/ui/src/app/pages/workspace/invalid-billing-banner.tsx @@ -4,8 +4,10 @@ import * as fp from 'lodash'; import { Profile } from 'generated/fetch'; import { Button, LinkButton } from 'app/components/buttons'; +import { AoU } from 'app/components/text-wrappers'; import { ToastBanner, ToastType } from 'app/components/toast-banner'; import { withCurrentWorkspace, withUserProfile } from 'app/utils'; +import { plusDays } from 'app/utils/dates'; import { NavigationProps } from 'app/utils/navigation'; import { withNavigation } from 'app/utils/with-navigation-hoc'; import { WorkspaceData } from 'app/utils/workspace-data'; @@ -19,23 +21,125 @@ interface Props extends NavigationProps { onClose: Function; } +const InitialCreditsArticleLink = () => ( + <> + " + window.open(supportUrls.createBillingAccount, '_blank')} + > + Using Initial Credits + + " + +); + +const BillingAccountArticleLink = () => ( + <> + " + window.open(supportUrls.createBillingAccount, '_blank')} + > + Paying for Your Research + + " + +); + export const InvalidBillingBanner = fp.flow( withCurrentWorkspace(), withUserProfile(), withNavigation -)(({ onClose, navigate, workspace }: Props) => { - const message = ( -
- The initial credits for the creator of this workspace have run out. Please - provide a valid billing account. - window.open(supportUrls.createBillingAccount, '_blank')} - > - Learn how to link a billing account. - -
- ); - const footer = ( +)(({ onClose, navigate, workspace, profileState }: Props) => { + const { profile } = profileState; + const isCreator = + workspace && profile && profile.username === workspace.creator; + const eligibleForExtension = + workspace && !workspace.initialCredits.extensionEpochMillis; + const isExpired = + workspace && workspace.initialCredits.expirationEpochMillis < Date.now(); + const isExpiringSoon = + workspace && + !isExpired && + plusDays(workspace.initialCredits.expirationEpochMillis, 5) < Date.now(); + let message; + if (isCreator) { + if (eligibleForExtension) { + if (isExpired) { + // Banner 3 in spec (changed to trigger modal from here instead of on Profile page) + message = ( +
+ Your initial credits have expired. You can request an extension + here. For more information, read the {' '} + article on the User Support Hub. +
+ ); + } else if (isExpiringSoon) { + // Banner 1 in spec (changed to trigger modal from here instead of on Profile page) + message = ( +
+ Your initial credits are expiring soon. You can request an extension + here. For more information, read the {' '} + article on the User Support Hub. +
+ ); + } + } else { + if (isExpired) { + // Banner 5 in spec (use this also for exhausted) + message = ( +
+ Your initial credits have run out. To use the workspace, a valid + billing account needs to be provided. To learn more about + establishing a billing account, read the{' '} + article on the User Support Hub. +
+ ); + } else if (isExpiringSoon) { + // Not accounted for in spec + } + } + } else { + if (eligibleForExtension) { + if (isExpired) { + // Banner 4 in spec + message = ( +
+ This workspace creator’s initial credits have expired. This + workspace was created by FIRST NAME LAST NAME. For more information, + read the article on the User Support + Hub. +
+ ); + } else if (isExpiringSoon) { + // Banner 2 in spec + message = ( +
+ This workspace creator’s initial credits are expiring soon. This + workspace was created by FIRST NAME LAST NAME. For more information, + read the article on the User Support + Hub. +
+ ); + } + } else { + if (isExpired) { + // Banner 6 in spec + message = ( +
+ This workspace creator’s initial credits have run out. This + workspace was created by FIRST NAME LAST NAME. To use the workspace, + a valid billing account needs to be provided. To learn more about + establishing a billing account, read the{' '} + article on the User Support Hub. +
+ ); + } else if (isExpiringSoon) { + // Not accounted for in spec + } + } + } + + const footer = isCreator && ( + + + + ); +}; diff --git a/ui/src/app/pages/profile/initial-credits-panel.spec.tsx b/ui/src/app/pages/profile/initial-credits-panel.spec.tsx index e6b7ab2e99f..1678b092731 100644 --- a/ui/src/app/pages/profile/initial-credits-panel.spec.tsx +++ b/ui/src/app/pages/profile/initial-credits-panel.spec.tsx @@ -5,18 +5,27 @@ import * as React from 'react'; import { screen } from '@testing-library/dom'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { serverConfigStore } from 'app/utils/stores'; import { InitialCreditsPanel } from './initial-credits-panel'; const setup = ( freeTierUsage: number, freeTierDollarQuota: number, - expirationDate: number + expirationDate: number, + eligibleForExtension: boolean ) => { + const updateInitialCredits = jest.fn(); return { container: render( ).container, user: userEvent.setup(), @@ -27,12 +36,18 @@ it('should display initial credits for a user with an expiration record', async const freeTierUsage = 100.0; const freeTierDollarQuota = 234; const expirationDate = new Date('2023-12-03T20:00:00Z').getTime(); - setup(freeTierUsage, freeTierDollarQuota, expirationDate); + const eligibleForExtension = true; + setup( + freeTierUsage, + freeTierDollarQuota, + expirationDate, + eligibleForExtension + ); expect(screen.getByText(`$100.00`)).toBeInTheDocument(); // This should reflect the quota minus the usage expect(screen.getByText(`$134.00`)).toBeInTheDocument(); expect( - screen.getByText(`initial credits epiration date:`) + screen.getByText(`initial credits expiration date:`) ).toBeInTheDocument(); expect(screen.getByText(`Dec 3, 2023`)).toBeInTheDocument(); expect( @@ -40,14 +55,61 @@ it('should display initial credits for a user with an expiration record', async ).toBeInTheDocument(); }); +it('should show extension modal when extension button is clicked', async () => { + const freeTierUsage = 100.0; + const freeTierDollarQuota = 234; + const expirationDate = new Date('2023-12-03T20:00:00Z').getTime(); + const eligibleForExtension = true; + serverConfigStore.set({ + config: { + gsuiteDomain: 'fake-research-aou.org', + initialCreditsValidityPeriodDays: 100, + initialCreditsExpirationWarningDays: 5, + }, + }); + const { user } = setup( + freeTierUsage, + freeTierDollarQuota, + expirationDate, + eligibleForExtension + ); + await user.click( + screen.getByRole('button', { name: /request credit extension/i }) + ); + screen.getByText(/request credit expiration date extension/i); +}); + +it('should not show extension button if the user is not eligible', async () => { + const freeTierUsage = 100.0; + const freeTierDollarQuota = 234; + const expirationDate = new Date('2023-12-03T20:00:00Z').getTime(); + const eligibleForExtension = false; + setup( + freeTierUsage, + freeTierDollarQuota, + expirationDate, + eligibleForExtension + ); + expect(screen.getByText(`$100.00`)).toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /request credit extension/i }) + ).not.toBeInTheDocument(); +}); + it('should display initial credits for a user without an expiration record', async () => { const freeTierUsage = 100.0; const freeTierDollarQuota = 234; const expirationDate = null; - setup(freeTierUsage, freeTierDollarQuota, expirationDate); + const eligibleForExtension = false; + setup( + freeTierUsage, + freeTierDollarQuota, + expirationDate, + eligibleForExtension + ); expect(screen.getByText(`$100.00`)).toBeInTheDocument(); expect( - screen.queryByText(`initial credits epiration date:`) + screen.queryByText(`initial credits expiration date:`) ).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: /request credit extension/i }) diff --git a/ui/src/app/pages/profile/initial-credits-panel.tsx b/ui/src/app/pages/profile/initial-credits-panel.tsx index cddf53ec554..aa403acac9e 100644 --- a/ui/src/app/pages/profile/initial-credits-panel.tsx +++ b/ui/src/app/pages/profile/initial-credits-panel.tsx @@ -1,20 +1,35 @@ import * as React from 'react'; -import { Button } from 'app/components/buttons'; +import { Profile } from 'generated/fetch'; + +import { Button, StyledExternalLink } from 'app/components/buttons'; +import { ExtendInitialCreditsModal } from 'app/components/extend-initial-credits-modal'; import { FlexColumn, FlexRow } from 'app/components/flex'; import { AoU } from 'app/components/text-wrappers'; import { formatInitialCreditsUSD } from 'app/utils'; import { displayDateWithoutHours } from 'app/utils/dates'; +import { supportUrls } from 'app/utils/zendesk'; import { styles } from './profile-styles'; interface Props { freeTierUsage: number; freeTierDollarQuota: number; + eligibleForExtension: boolean; expirationDate: number; + updateInitialCredits: Function; } export const InitialCreditsPanel = (props: Props) => { - const { expirationDate, freeTierUsage, freeTierDollarQuota } = props; + const [showExtendInitialCreditsModal, setShowExtendInitialCreditsModal] = + React.useState(false); + const { + eligibleForExtension, + expirationDate, + freeTierUsage, + freeTierDollarQuota, + updateInitialCredits, + } = props; + return (
@@ -27,7 +42,7 @@ export const InitialCreditsPanel = (props: Props) => {
{expirationDate && (
- initial credits epiration date: + initial credits expiration date:
)}
@@ -47,29 +62,37 @@ export const InitialCreditsPanel = (props: Props) => { )}
- {expirationDate && ( + {eligibleForExtension && ( + {showExtendInitialCreditsModal && ( + { + if (updatedProfile) { + updateInitialCredits( + updatedProfile.initialCreditsExpirationEpochMillis, + updatedProfile.eligibleForInitialCreditsExtension + ); + } + setShowExtendInitialCreditsModal(false); + }} + /> + )} )} - + ); diff --git a/ui/src/app/pages/profile/profile-component.tsx b/ui/src/app/pages/profile/profile-component.tsx index dca185d142c..761a88c9fda 100644 --- a/ui/src/app/pages/profile/profile-component.tsx +++ b/ui/src/app/pages/profile/profile-component.tsx @@ -552,12 +552,29 @@ export const ProfileComponent = fp.flow(
Initial credits balance

- {profile && ( + {currentProfile && ( + this.setState({ + currentProfile: { + ...currentProfile, + initialCreditsExpirationEpochMillis: + updatedExpiration, + eligibleForInitialCreditsExtension: + updatedExtensionEligibility, + }, + }) } /> )} diff --git a/ui/src/app/utils/dates.tsx b/ui/src/app/utils/dates.tsx index 43eca983717..7a4a32d8e12 100644 --- a/ui/src/app/utils/dates.tsx +++ b/ui/src/app/utils/dates.tsx @@ -5,6 +5,8 @@ export const getWholeDaysFromNow = (timeInMillis: number): number => Math.floor((timeInMillis - Date.now()) / MILLIS_PER_DAY); export const plusDays = (date: number, days: number): number => date + MILLIS_PER_DAY * days; +export const minusDays = (date: number, days: number): number => + plusDays(date, -days); export const nowPlusDays = (days: number) => plusDays(Date.now(), days); // To convert datetime strings into human-readable dates in the format diff --git a/ui/src/app/utils/zendesk.ts b/ui/src/app/utils/zendesk.ts index 81a26b8f8be..2bc58a65c0a 100644 --- a/ui/src/app/utils/zendesk.ts +++ b/ui/src/app/utils/zendesk.ts @@ -22,6 +22,7 @@ interface ZendeskUrls { cromwellInformation: string; sasHowToRun: string; sasExplore: string; + initialCredits: string; } const zendeskConfigs = { @@ -59,6 +60,7 @@ export const supportUrls: ZendeskUrls = ((env) => { const category = (id) => `${baseUrl}/categories/${id}`; const commonUrls = { helpCenter: baseUrl, + initialCredits: article('28920849686036'), rStudioHowToRun: article('22078658566804'), rStudioHowToDataset: article('360039585831'), cromwellInformation: article('14428263737620'), From bd12bb97a9ff739fb7afb3bae13a7699f9d00a6b Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Tue, 19 Nov 2024 11:15:42 -0500 Subject: [PATCH 11/41] Fix title and eligibility usage --- ui/src/app/pages/workspace/invalid-billing-banner.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ui/src/app/pages/workspace/invalid-billing-banner.tsx b/ui/src/app/pages/workspace/invalid-billing-banner.tsx index febd3da17f2..f79880a712b 100644 --- a/ui/src/app/pages/workspace/invalid-billing-banner.tsx +++ b/ui/src/app/pages/workspace/invalid-billing-banner.tsx @@ -54,8 +54,7 @@ export const InvalidBillingBanner = fp.flow( const { profile } = profileState; const isCreator = workspace && profile && profile.username === workspace.creator; - const isEligibleForExtension = - workspace && !workspace.initialCredits.extensionEpochMillis; + const isEligibleForExtension = profile.eligibleForInitialCreditsExtension; const isExpired = workspace && workspace.initialCredits.expirationEpochMillis < Date.now(); const isExpiringSoon = @@ -159,8 +158,7 @@ export const InvalidBillingBanner = fp.flow( return ( From 6480a2f2d7f8209e7c03047f4a0dfa41ef69a6f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:50:15 -0500 Subject: [PATCH 12/41] [risk=low][no ticket] [dependabot] Bump cross-spawn from 7.0.3 to 7.0.6 in /ui (#8953) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/yarn.lock b/ui/yarn.lock index 466f86b4b2d..77e1b8e27d2 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -4093,9 +4093,9 @@ create-jest@^29.7.0: prompts "^2.0.1" cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" From 7881475d6e76a3bd717fc4d3cfa510c05139891d Mon Sep 17 00:00:00 2001 From: Yonghao Yu Date: Tue, 19 Nov 2024 12:00:12 -0500 Subject: [PATCH 13/41] laucn multi-zone (#8956) --- api/config/config_preprod.json | 2 +- api/config/config_prod.json | 2 +- api/config/config_stable.json | 2 +- api/config/config_staging.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/config/config_preprod.json b/api/config/config_preprod.json index 53e51357d44..560d30152b4 100644 --- a/api/config/config_preprod.json +++ b/api/config/config_preprod.json @@ -15,7 +15,7 @@ "jupyterDockerImage": "us.gcr.io/broad-dsp-gcr-public/terra-jupyter-aou:2.2.13", "workspaceLogsProject": "fc-aou-logs-preprod", "workspaceBucketLocation": "us-central1", - "gceVmZones": ["us-central1-a"], + "gceVmZones": ["us-central1-a","us-central1-b","us-central1-c", "us-central1-f"], "defaultGceVmZone": "us-central1-a" }, "billing": { diff --git a/api/config/config_prod.json b/api/config/config_prod.json index e2d5959a8e3..51868aff71e 100644 --- a/api/config/config_prod.json +++ b/api/config/config_prod.json @@ -15,7 +15,7 @@ "jupyterDockerImage": "us.gcr.io/broad-dsp-gcr-public/terra-jupyter-aou:2.2.13", "workspaceLogsProject": "fc-aou-logs-prod", "workspaceBucketLocation": "us-central1", - "gceVmZones": ["us-central1-a"], + "gceVmZones": ["us-central1-a","us-central1-b","us-central1-c", "us-central1-f"], "defaultGceVmZone": "us-central1-a" }, "billing": { diff --git a/api/config/config_stable.json b/api/config/config_stable.json index 7b18471363a..603f5f03395 100644 --- a/api/config/config_stable.json +++ b/api/config/config_stable.json @@ -15,7 +15,7 @@ "jupyterDockerImage": "us.gcr.io/broad-dsp-gcr-public/terra-jupyter-aou:2.2.13", "workspaceLogsProject": "fc-aou-logs-stable", "workspaceBucketLocation": "us-central1", - "gceVmZones": ["us-central1-a"], + "gceVmZones": ["us-central1-a","us-central1-b","us-central1-c", "us-central1-f"], "defaultGceVmZone": "us-central1-a" }, "billing": { diff --git a/api/config/config_staging.json b/api/config/config_staging.json index 3dd92b35524..3a9d2bbfd79 100644 --- a/api/config/config_staging.json +++ b/api/config/config_staging.json @@ -15,7 +15,7 @@ "jupyterDockerImage": "us.gcr.io/broad-dsp-gcr-public/terra-jupyter-aou:2.2.13", "workspaceLogsProject": "fc-aou-logs-staging", "workspaceBucketLocation": "us-central1", - "gceVmZones": ["us-central1-a"], + "gceVmZones": ["us-central1-a","us-central1-b","us-central1-c", "us-central1-f"], "defaultGceVmZone": "us-central1-a" }, From 92a2784d78eae6e8b893989ca4cfacf888e597a0 Mon Sep 17 00:00:00 2001 From: Qi Wang <32771737+Qi77Qi@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:45:58 -0500 Subject: [PATCH 14/41] remove getUploadResult in verifyAndLog (#8955) --- .../reporting/ReportingVerificationServiceImpl.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/api/src/main/java/org/pmiops/workbench/reporting/ReportingVerificationServiceImpl.java b/api/src/main/java/org/pmiops/workbench/reporting/ReportingVerificationServiceImpl.java index dfa0cde190c..65d520a1177 100644 --- a/api/src/main/java/org/pmiops/workbench/reporting/ReportingVerificationServiceImpl.java +++ b/api/src/main/java/org/pmiops/workbench/reporting/ReportingVerificationServiceImpl.java @@ -167,11 +167,7 @@ private ReportingUploadDetails getUploadDetails(ReportingSnapshot snapshot) { getUploadResult( snapshot, DatasetConceptSetColumnValueExtractor.TABLE_NAME, - ReportingSnapshot::getDatasetConceptSets), - getUploadResult( - snapshot, - LeonardoAppUsageColumnValueExtractor.TABLE_NAME, - ReportingSnapshot::getLeonardoAppUsage))); + ReportingSnapshot::getDatasetConceptSets))); verifyStopwatch.stop(); logger.info(LogFormatters.duration("Verification queries", verifyStopwatch.elapsed())); return result; From 0f0326eab47cf399988b4897618473f79901d1cc Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Tue, 19 Nov 2024 13:35:38 -0500 Subject: [PATCH 15/41] Update banner --- .../workspace/invalid-billing-banner.tsx | 76 ++++++++++++------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/ui/src/app/pages/workspace/invalid-billing-banner.tsx b/ui/src/app/pages/workspace/invalid-billing-banner.tsx index f79880a712b..94716c07564 100644 --- a/ui/src/app/pages/workspace/invalid-billing-banner.tsx +++ b/ui/src/app/pages/workspace/invalid-billing-banner.tsx @@ -4,6 +4,7 @@ import * as fp from 'lodash'; import { Profile } from 'generated/fetch'; import { Button, LinkButton } from 'app/components/buttons'; +import { ExtendInitialCreditsModal } from 'app/components/extend-initial-credits-modal'; import { AoU } from 'app/components/text-wrappers'; import { ToastBanner, ToastType } from 'app/components/toast-banner'; import { withCurrentWorkspace, withUserProfile } from 'app/utils'; @@ -23,26 +24,32 @@ interface Props extends NavigationProps { } const InitialCreditsArticleLink = () => ( - <> - " - window.open(supportUrls.createBillingAccount, '_blank')} - > - Using Initial Credits - - " - + window.open(supportUrls.createBillingAccount, '_blank')} + > + Using Initial Credits + ); const BillingAccountArticleLink = () => ( + window.open(supportUrls.createBillingAccount, '_blank')} + > + Paying for Your Research + +); + +interface ExtensionRequestLinkProps { + onClick: Function; +} + +const ExtensionRequestLink = ({ onClick }: ExtensionRequestLinkProps) => ( <> - " - window.open(supportUrls.createBillingAccount, '_blank')} - > - Paying for Your Research + You can request an extension{' '} + + here - " + . ); @@ -51,7 +58,10 @@ export const InvalidBillingBanner = fp.flow( withUserProfile(), withNavigation )(({ onClose, navigate, workspace, profileState }: Props) => { - const { profile } = profileState; + const [profile, setProfile] = React.useState( + profileState?.profile + ); + const [showExtensionModal, setShowExtensionModal] = React.useState(false); const isCreator = workspace && profile && profile.username === workspace.creator; const isEligibleForExtension = profile.eligibleForInitialCreditsExtension; @@ -72,9 +82,10 @@ export const InvalidBillingBanner = fp.flow( // Banner 1 in spec message = (
- Your initial credits are expiring soon. You can request an extension - here. For more information, read the {' '} - article on the User Support Hub. + Your initial credits are expiring soon.{' '} + setShowExtensionModal(true)} />{' '} + For more information, read the article + on the User Support Hub.
); } else { @@ -95,9 +106,10 @@ export const InvalidBillingBanner = fp.flow( // Banner 3 in spec (changed to trigger modal from here instead of on Profile page) message = (
- Your initial credits have expired. You can request an extension - here. For more information, read the {' '} - article on the User Support Hub. + Your initial credits have expired.{' '} + setShowExtensionModal(true)} />{' '} + For more information, read the article + on the User Support Hub.
); } else { @@ -157,10 +169,20 @@ export const InvalidBillingBanner = fp.flow( ); return ( - + <> + + {showExtensionModal && ( + { + setShowExtensionModal(false); + setProfile(updatedProfile); + }} + /> + )} + ); }); From 8092dba47739e190cedc42e5338a1364608f7cd0 Mon Sep 17 00:00:00 2001 From: Eric Rollins Date: Tue, 19 Nov 2024 15:24:39 -0500 Subject: [PATCH 16/41] Cleaner banner? --- .../workspace/invalid-billing-banner.tsx | 180 ++++++++++-------- 1 file changed, 105 insertions(+), 75 deletions(-) diff --git a/ui/src/app/pages/workspace/invalid-billing-banner.tsx b/ui/src/app/pages/workspace/invalid-billing-banner.tsx index 94716c07564..6266c5d26c4 100644 --- a/ui/src/app/pages/workspace/invalid-billing-banner.tsx +++ b/ui/src/app/pages/workspace/invalid-billing-banner.tsx @@ -53,6 +53,80 @@ const ExtensionRequestLink = ({ onClick }: ExtensionRequestLinkProps) => ( ); +const whoseCredits = (isCreator: boolean) => { + return isCreator ? 'Your' : 'This workspace creator’s'; +}; + +const workspaceCreatorInformation = (isCreator: boolean) => { + return isCreator ? '' : 'This workspace was created by First Name Last Name.'; +}; + +const whatHappened = ( + isExhausted: boolean, + isExpired: boolean, + isExpiringSoon: boolean, + isEligibleForExtension: boolean, + isCreator: boolean +) => { + const whose = whoseCredits(isCreator); + let whatIsHappening: string; + if (isExhausted || (isExpired && !isEligibleForExtension)) { + whatIsHappening = 'have run out.'; + } else if (isExpired && isEligibleForExtension) { + whatIsHappening = 'have expired.'; + } else if (isExpiringSoon && isEligibleForExtension) { + whatIsHappening = 'are expiring soon.'; + } + return ( + <> + {whose} initial credits {whatIsHappening} + + ); +}; + +const whatToDo = ( + isExhausted: boolean, + isExpired: boolean, + isExpiringSoon: boolean, + isEligibleForExtension: boolean, + onClick: Function +) => { + if (isExhausted || (isExpired && !isEligibleForExtension)) { + return ( + <> + To use the workspace, a valid billing account needs to be provided. To + learn more about establishing a billing account, read the{' '} + article on the User Support Hub. + + ); + } else { + return ( + <> + {isEligibleForExtension && (isExpired || isExpiringSoon) && ( + + )}{' '} + For more information, read the article on + the User Support Hub. + + ); + } +}; + +const titleText = ( + isExhausted: boolean, + isExpired: boolean, + isExpiringSoon: boolean, + isEligibleForExtension: boolean +) => { + if (isExhausted || (isExpired && !isEligibleForExtension)) { + return 'This workspace is out of initial credits'; + } else if (isExpired && isEligibleForExtension) { + return 'Workspace credits have expired'; + } else if (isExpiringSoon && isEligibleForExtension) { + return 'Workspace credits are expiring soon'; + } +}; + export const InvalidBillingBanner = fp.flow( withCurrentWorkspace(), withUserProfile(), @@ -74,81 +148,37 @@ export const InvalidBillingBanner = fp.flow( workspace.initialCredits.expirationEpochMillis, -serverConfigStore.get().config.initialCreditsExpirationWarningDays ) < Date.now(); - let message: JSX.Element; - let title: string; - if (isExpiringSoon && isEligibleForExtension) { - title = 'Workspace credits are expiring soon'; - if (isCreator) { - // Banner 1 in spec - message = ( -
- Your initial credits are expiring soon.{' '} - setShowExtensionModal(true)} />{' '} - For more information, read the article - on the User Support Hub. -
- ); - } else { - // Banner 2 in spec - message = ( -
- This workspace creator’s initial credits are expiring soon. This - workspace was created by FIRST NAME LAST NAME. For more information, - read the article on the User Support - Hub. -
- ); - } - } else if (isExpired) { - if (isEligibleForExtension) { - title = 'Workspace credits have expired'; - if (isCreator) { - // Banner 3 in spec (changed to trigger modal from here instead of on Profile page) - message = ( -
- Your initial credits have expired.{' '} - setShowExtensionModal(true)} />{' '} - For more information, read the article - on the User Support Hub. -
- ); - } else { - // Banner 4 in spec - message = ( -
- This workspace creator’s initial credits have expired. This - workspace was created by FIRST NAME LAST NAME. For more information, - read the article on the User Support - Hub. -
- ); - } - } else { - title = 'This workspace is out of initial credits'; - if (isCreator) { - // Banner 5 in spec - message = ( -
- Your initial credits have run out. To use the workspace, a valid - billing account needs to be provided. To learn more about - establishing a billing account, read the{' '} - article on the User Support Hub. -
- ); - } else { - // Banner 6 in spec - message = ( -
- This workspace creator’s initial credits have run out. This - workspace was created by FIRST NAME LAST NAME. To use the workspace, - a valid billing account needs to be provided. To learn more about - establishing a billing account, read the{' '} - article on the User Support Hub. -
- ); - } - } - } + const isExhausted = workspace && workspace.initialCredits.exhausted; + const title = titleText( + isExhausted, + isExpired, + isExpiringSoon, + isEligibleForExtension + ); + + const workspaceCreatorInformationIfApplicable = + workspaceCreatorInformation(isCreator); + const whatHappenedMessage = whatHappened( + isExhausted, + isExpired, + isExpiringSoon, + isEligibleForExtension, + isCreator + ); + const whatToDoMessage = whatToDo( + isExhausted, + isExpired, + isExpiringSoon, + isEligibleForExtension, + () => setShowExtensionModal(true) + ); + + const message = ( + <> + {whatHappenedMessage} {workspaceCreatorInformationIfApplicable} + {whatToDoMessage} + + ); const footer = isCreator && (