Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SALAD-23116] WebApp - Connect demand alerts page #1243

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions packages/web-app/src/MobileRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { MobilePageNotFound } from './components'
import { FeatureFlags, useFeatureManager } from './FeatureManager'
import { MobileAccountSummaryContainer } from './modules/account-views-mobile'
import { BackupCodesPageContainer } from './modules/backup-codes/BackupCodesPageContainer'
import { DemandAlertsPageContainer } from './modules/demand-alerts-views'
import { DemandAlertsPage } from './modules/demand-alerts-views'
import { DemandMonitorPageContainer } from './modules/demand-monitor-views'
import { MobileEarningSummaryContainer } from './modules/earn-views-mobile'
import { PasskeyDeletePageContainer } from './modules/passkey-delete'
Expand All @@ -25,9 +25,7 @@ const _Routes = ({ location }: RouteComponentProps) => {
{isDemandMonitorFeatureFlagEnabled && (
<Route path="/earn/demand" exact component={DemandMonitorPageContainer} />
)}
{isDemandNotificationsFeatureFlagEnabled && (
<Route path="/account/alerts" exact component={DemandAlertsPageContainer} />
)}
{isDemandNotificationsFeatureFlagEnabled && <Route path="/account/alerts" exact component={DemandAlertsPage} />}
<Route path="/account/summary" component={MobileAccountSummaryContainer} />
<Route exact path="/rewards/:id" component={RewardDetailsContainer} />
<Redirect exact from="/account/summary" to="/account/summary" />
Expand Down
3 changes: 3 additions & 0 deletions packages/web-app/src/Store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { BackupCodesStore } from './modules/backup-codes'
import { BalanceStore } from './modules/balance'
import { BonusStore } from './modules/bonus'
import { RefreshService } from './modules/data-refresh'
import { DemandAlertsStore } from './modules/demand-alerts-views/DemandAlertsStore'
import { DemandMonitorStore } from './modules/demand-monitor-views/DemandMonitorStore'
import { EngagementStore } from './modules/engagement'
import { ErrorBoundaryStore } from './modules/error-boundary'
Expand Down Expand Up @@ -75,6 +76,7 @@ export class RootStore {
public readonly passkey: PasskeyStore
public readonly backupCodes: BackupCodesStore
public readonly demandMonitor: DemandMonitorStore
public readonly demandAlerts: DemandAlertsStore

constructor(axios: AxiosInstance, private readonly featureManager: FeatureManager) {
this.routing = new RouterStore()
Expand Down Expand Up @@ -103,6 +105,7 @@ export class RootStore {
this.startButtonUI = new StartButtonUIStore(this)
this.errorBoundary = new ErrorBoundaryStore()
this.demandMonitor = new DemandMonitorStore(axios)
this.demandAlerts = new DemandAlertsStore(axios)

// Pass AnalyticsStore to FeatureManager
featureManager.setAnalyticsStore(this.analytics)
Expand Down
4 changes: 2 additions & 2 deletions packages/web-app/src/components/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AccountContainer } from '../modules/account-views/account-views'
import { ReferralSettingsContainer } from '../modules/account-views/referral-views'
import { AchievementPageContainer } from '../modules/achievements-views'
import { BonusPageContainer } from '../modules/bonus-views'
import { DemandAlertsPageContainer } from '../modules/demand-alerts-views'
import { DemandAlertsPage } from '../modules/demand-alerts-views'
import { IconArrowLeft } from '../modules/reward-views/components/assets'
import { styles } from './SettingsPage.styles'

Expand Down Expand Up @@ -74,7 +74,7 @@ const _Settings = ({ appBuild, classes, menuButtons, isUserReferralsEnabled, onC
isDemandNotificationsFeatureFlagEnabled && {
url: '/account/alerts',
text: 'Demand Alerts',
component: DemandAlertsPageContainer,
component: DemandAlertsPage,
},
].filter((menuItem) => menuItem) as MenuItem[]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Button, Text } from '@saladtechnologies/garden-components'
import type CSS from 'csstype'
import type { FunctionComponent } from 'react'
import { useEffect, useState, type FunctionComponent } from 'react'
import type { WithStyles } from 'react-jss'
import withStyles from 'react-jss'
import { ErrorText } from '../../../../components'
import type { SaladTheme } from '../../../../SaladTheme'
import { DefaultTheme } from '../../../../SaladTheme'
import { mockedExistingAlertsList } from '../../mocks'
import type { DemandedSubscription } from '../../DemandAlertsStore'
import { UnsubscribeFromDemandAlertStatus } from '../../DemandAlertsStore'

const styles: (theme: SaladTheme) => Record<string, CSS.Properties> = (theme: SaladTheme) => ({
container: {
Expand All @@ -27,32 +29,89 @@ const styles: (theme: SaladTheme) => Record<string, CSS.Properties> = (theme: Sa
justifyContent: 'space-between',
alignItems: 'center',
},
loadingSpinnerWrap: {
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
})

interface Props extends WithStyles<typeof styles> {}
interface Props extends WithStyles<typeof styles> {
demandAlertSubscriptionList?: DemandedSubscription[]
fetchDemandAlertSubscriptionList: () => void
setUnsubscribeFromDemandAlertStatus: (unsubscribeFromDemandAlertStatus: UnsubscribeFromDemandAlertStatus) => void
unsubscribeFromDemandAlert: (subscriptionId: string) => void
unsubscribeFromDemandAlertStatus: UnsubscribeFromDemandAlertStatus
}

const _DemandAlertsList: FunctionComponent<Props> = ({
classes,
demandAlertSubscriptionList,
fetchDemandAlertSubscriptionList,
setUnsubscribeFromDemandAlertStatus,
unsubscribeFromDemandAlert,
unsubscribeFromDemandAlertStatus,
}) => {
const [currentDemandedSubscriptionId, setCurrentDemandedSubscriptionId] = useState<string | null>(null)

const demandScenario: Record<number, string> = {
0: 'Low Demand',
50: 'Moderate Demand',
80: 'High Demand',
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages/web-app/src/modules/demand-alerts-views/constants.ts
let's use this const demandScenario to avoid data duplication

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree
Changed


useEffect(() => {
fetchDemandAlertSubscriptionList()
return () => {
setUnsubscribeFromDemandAlertStatus(UnsubscribeFromDemandAlertStatus.UNKNOWN)
}
}, [fetchDemandAlertSubscriptionList, setUnsubscribeFromDemandAlertStatus])

const handleCancelSubscription = (demandedSubscriptionId: string) => {
unsubscribeFromDemandAlert(demandedSubscriptionId)
setCurrentDemandedSubscriptionId(demandedSubscriptionId)
}

const _DemandAlertsList: FunctionComponent<Props> = ({ classes }) => {
return (
<div className={classes.container}>
<Text variant="baseXL">Manage your existing alerts</Text>
<Text variant="baseS">You will get alerted on the following scenarios:</Text>
<div className={classes.existingAlertsContainer}>
{mockedExistingAlertsList.map((existingAlert) => (
<div className={classes.alertContainer}>
<Text variant="baseS">
{existingAlert.gpu} @ {existingAlert.demandScenario}
</Text>
<Button
onClick={() => {}}
outlineColor={DefaultTheme.white}
label="Unsubscribe"
variant="primary"
size="small"
/>
</div>
))}
demandAlertSubscriptionList &&
demandAlertSubscriptionList?.length > 0 && (
<div className={classes.container}>
<Text variant="baseXL">Manage your existing alerts</Text>
<Text variant="baseS">You will get alerted on the following scenarios:</Text>
<div className={classes.existingAlertsContainer}>
{demandAlertSubscriptionList.map((demandAlertSubscription) => {
const isCurrentDemandedSubscriptionId = currentDemandedSubscriptionId === demandAlertSubscription.id

const withCancelSubscriptionSubmitting =
unsubscribeFromDemandAlertStatus === UnsubscribeFromDemandAlertStatus.SUBMITTING &&
isCurrentDemandedSubscriptionId
const withCancelSubscriptionFailure =
Comment on lines +82 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not name it just
isCancelSubscriptionSubmitting
isCancelSubscriptionFailure

unsubscribeFromDemandAlertStatus === UnsubscribeFromDemandAlertStatus.FAILURE &&
isCurrentDemandedSubscriptionId
return (
<>
<div className={classes.alertContainer}>
<Text variant="baseS">
{demandAlertSubscription.gpuDisplayName} @ {demandScenario[demandAlertSubscription.utilizationPct]}
</Text>
<Button
onClick={() => handleCancelSubscription(demandAlertSubscription.id)}
isLoading={withCancelSubscriptionSubmitting}
outlineColor={DefaultTheme.white}
label="Unsubscribe"
variant="primary"
size="small"
/>
</div>
{withCancelSubscriptionFailure && <ErrorText>Something went wrong. Please try again later</ErrorText>}
</>
)
})}
</div>
</div>
</div>
)
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { connect } from '../../../../connect'
import type { RootStore } from '../../../../Store'
import { DemandAlertsList } from './DemandAlertsList'

const mapStoreToProps = (store: RootStore): any => ({
demandAlertSubscriptionList: store.demandAlerts.demandAlertSubscriptionList,
fetchDemandAlertSubscriptionList: store.demandAlerts.fetchDemandAlertSubscriptionList,
setUnsubscribeFromDemandAlertStatus: store.demandAlerts.setUnsubscribeFromDemandAlertStatus,
unsubscribeFromDemandAlert: store.demandAlerts.unsubscribeFromDemandAlert,
unsubscribeFromDemandAlertStatus: store.demandAlerts.unsubscribeFromDemandAlertStatus,
})

export const DemandAlertsListContainer = connect(mapStoreToProps, DemandAlertsList)
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './DemandAlertsList'
export * from './DemandAlertsListContainer'
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { useMediaQuery } from 'react-responsive'
import { Head, mobileSize, Scrollbar } from '../../../components'
import type { SaladTheme } from '../../../SaladTheme'
import { withLogin } from '../../auth-views'
import { DemandAlertsList } from './DemandAlertsList'
import { DemandAlertsSetUp } from './DemandAlertsSetUp'
import { DemandAlertsListContainer } from './DemandAlertsList'
import { DemandAlertsSetUpContainer } from './DemandAlertsSetUp'

const styles: (theme: SaladTheme) => Record<string, CSS.Properties> = (theme: SaladTheme) => ({
page: {
Expand Down Expand Up @@ -43,8 +43,8 @@ const _DemandAlertsPage = ({ classes }: Props) => {
specific payout tier. When that scenario arrives we will notify you through email and an in-app message.
</Text>
<div className={classes.demandContentContainer}>
<DemandAlertsSetUp />
<DemandAlertsList />
<DemandAlertsSetUpContainer />
<DemandAlertsListContainer />
</div>
</Layout>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Button, Text } from '@saladtechnologies/garden-components'
import { Button, LoadingSpinner, Text } from '@saladtechnologies/garden-components'
import type CSS from 'csstype'
import type { FunctionComponent } from 'react'
import { useEffect, useState, type FunctionComponent } from 'react'
import type { WithStyles } from 'react-jss'
import withStyles from 'react-jss'
import { DefaultTheme } from '../../../../SaladTheme'
import { ErrorText } from '../../../../components'
import type { DropdownOption } from '../../../../components/Dropdown'
import { DropdownLight } from '../../../../components/Dropdown'
import { demandScenarios, mockedGpuNames } from '../../constants'
import { DefaultTheme } from '../../../../SaladTheme'
import type { DemandedHardwarePerformance } from '../../../demand-monitor-views/DemandMonitorStore'
import { demandScenario } from '../../constants'
import { SubscribeToDemandAlertStatus } from '../../DemandAlertsStore'
import { getCreateNewSubscriptionErrorText } from './utils'

const styles: () => Record<string, CSS.Properties> = () => ({
container: {
Expand All @@ -26,26 +31,101 @@ const styles: () => Record<string, CSS.Properties> = () => ({
flexDirection: 'column',
gap: '5px',
},
loadingSpinnerWrap: {
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
})

interface Props extends WithStyles<typeof styles> {}
interface Props extends WithStyles<typeof styles> {
demandedHardwarePerformanceList?: DemandedHardwarePerformance[]
fetchDemandedHardwarePerformanceList: () => void
setSubscribeToDemandAlertStatus: (subscribeToDemandAlertStatus: SubscribeToDemandAlertStatus) => void
subscribeToDemandAlert: (gpuName: string, utilizationPct: number) => void
subscribeToDemandAlertStatus: SubscribeToDemandAlertStatus
}

const _DemandAlertsSetUp: FunctionComponent<Props> = ({
classes,
demandedHardwarePerformanceList,
fetchDemandedHardwarePerformanceList,
setSubscribeToDemandAlertStatus,
subscribeToDemandAlert,
subscribeToDemandAlertStatus,
}) => {
const createNewSubscriptionErrorText = getCreateNewSubscriptionErrorText(subscribeToDemandAlertStatus)

const demandHardwareOptions = demandedHardwarePerformanceList?.map((demandHardware) => ({
label: demandHardware.displayName,
value: demandHardware.name,
}))

const initialSelectedDemandHardwareValue = demandHardwareOptions?.[0]?.value
const initialSelectedDemandScenarioValue = demandScenario[0]?.value

const [selectedDemandHardwareValue, setSelectedDemandHardwareValue] = useState<string | undefined>(
initialSelectedDemandHardwareValue,
)
const [selectedDemandScenarioValue, setSelectedDemandScenarioValue] = useState<string | undefined>(
initialSelectedDemandScenarioValue,
)

useEffect(() => {
fetchDemandedHardwarePerformanceList()
setSelectedDemandHardwareValue(initialSelectedDemandHardwareValue)
return () => {
setSubscribeToDemandAlertStatus(SubscribeToDemandAlertStatus.UNKNOWN)
}
}, [initialSelectedDemandHardwareValue, fetchDemandedHardwarePerformanceList, setSubscribeToDemandAlertStatus])

const handleDemandHardwareOptionChange = (demandHardwareOption: DropdownOption) => {
setSelectedDemandHardwareValue(demandHardwareOption.value)
setSubscribeToDemandAlertStatus(SubscribeToDemandAlertStatus.UNKNOWN)
}

const handleDemandScenarioOptionChange = (demandScenarioOption: DropdownOption) => {
setSelectedDemandScenarioValue(demandScenarioOption.value)
setSubscribeToDemandAlertStatus(SubscribeToDemandAlertStatus.UNKNOWN)
}

const handleAddAlert = () => {
if (selectedDemandHardwareValue && selectedDemandScenarioValue) {
subscribeToDemandAlert(selectedDemandHardwareValue, Number(selectedDemandScenarioValue))
}
}

const _DemandAlertsSetUp: FunctionComponent<Props> = ({ classes }) => {
return (
<div className={classes.container}>
<Text variant="baseXL">Set up an alert</Text>
<Text variant="baseS">Select the GPU and the demand scenario you wish to get notified for.</Text>
<div className={classes.dropdownContainer}>
<div className={classes.dropdownContentContainer}>
<Text variant="baseS">GPU</Text>
<DropdownLight options={mockedGpuNames} />
</div>
<div className={classes.dropdownContentContainer}>
<Text variant="baseS">Demand Scenario</Text>
<DropdownLight options={demandScenarios} />
{demandHardwareOptions ? (
<>
<div className={classes.dropdownContainer}>
<div className={classes.dropdownContentContainer}>
<Text variant="baseS">GPU</Text>
<DropdownLight options={demandHardwareOptions} onChange={handleDemandHardwareOptionChange} />
</div>
<div className={classes.dropdownContentContainer}>
<Text variant="baseS">Demand Scenario</Text>
<DropdownLight options={demandScenario} onChange={handleDemandScenarioOptionChange} />
</div>
</div>
<Button
isLoading={subscribeToDemandAlertStatus === SubscribeToDemandAlertStatus.SUBMITTING}
onClick={handleAddAlert}
label="Add Alert"
outlineColor={DefaultTheme.darkBlue}
/>
{createNewSubscriptionErrorText && <ErrorText>{createNewSubscriptionErrorText}</ErrorText>}
</>
) : (
<div className={classes.loadingSpinnerWrap}>
<LoadingSpinner variant="light" size={100} />
</div>
</div>
<Button onClick={() => {}} label="Add Alert" outlineColor={DefaultTheme.darkBlue} />
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { connect } from '../../../../connect'
import type { RootStore } from '../../../../Store'
import { DemandAlertsSetUp } from './DemandAlertsSetUp'

const mapStoreToProps = (store: RootStore): any => ({
demandedHardwarePerformanceList: store.demandMonitor.demandedHardwarePerformanceList,
fetchDemandedHardwarePerformanceList: store.demandMonitor.fetchDemandedHardwarePerformanceList,
setSubscribeToDemandAlertStatus: store.demandAlerts.setSubscribeToDemandAlertStatus,
subscribeToDemandAlert: store.demandAlerts.subscribeToDemandAlert,
subscribeToDemandAlertStatus: store.demandAlerts.subscribeToDemandAlertStatus,
})

export const DemandAlertsSetUpContainer = connect(mapStoreToProps, DemandAlertsSetUp)
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './DemandAlertsSetUp'
export * from './DemandAlertsSetUpContainer'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SubscribeToDemandAlertStatus } from '../../DemandAlertsStore'

export const getCreateNewSubscriptionErrorText = (
subscribeToDemandAlertStatus: SubscribeToDemandAlertStatus,
): string | null => {
switch (subscribeToDemandAlertStatus) {
case SubscribeToDemandAlertStatus.FAILURE:
return 'Something went wrong. Please try again later'
case SubscribeToDemandAlertStatus.ALREADY_EXISTS:
return 'An alert with this GPU and Demand Scenario already exists.'
case SubscribeToDemandAlertStatus.INVALID_GPU:
return 'The GPU configuration provided is invalid.'
default:
return null
}
}
Loading