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

feat(operators): Add reactivate account button #6747

Merged
merged 8 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"prettier:fix": "pretty-quick --config .prettierrc.json --write '{src,cypress}/**/*.{ts,tsx}'",
"tsc": "tsc -p ./tsconfig.json --noEmit --pretty --skipLibCheck",
"tsc:watch": "yarn tsc --watch",
"generate": "export SHA=993f6756500aebe47903a3ddaee62f9f75d207c1 && export REMOTE=https://raw.githubusercontent.com/influxdata/openapi/${SHA}/ && yarn generate-meta",
"generate": "export SHA=d05381fbcee0dd5d88833e71057a4af647e0d169 && export REMOTE=https://raw.githubusercontent.com/influxdata/openapi/${SHA}/ && yarn generate-meta",
"generate-local": "export REMOTE=../openapi/ && yarn generate-meta",
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at this openapi SHA - it looks like the request body is just an object? Could we add a more specific type in openapi? I worry this is going to cause issues (or at min some warnings) with type validation on the UI side if we don't have a schema for the body.

Example:

Screen Shot 2023-06-28 at 10 47 26 AM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@wdoconnell thanks! So what do we usually do whenever there's no request body necessary? IIRC, without a requestBody, OATS won't generate it right. But it's not strictly necessary, so that's why I ended up with object.

Copy link
Contributor

Choose a reason for hiding this comment

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

@abshierjoel Oh, in that case, just ignore what I said 😓 - I had thought this would be relied on. If there's no request body being used I think it's fine to leave it as object.

"generate-local-cloud": "export REMOTE=../openapi/ && yarn generate-meta-cloud",
"generate-meta": "if [ -z \"${CLOUD_URL}\" ]; then yarn generate-meta-oss; else yarn generate-meta-cloud; fi",
Expand Down
2 changes: 2 additions & 0 deletions src/operator/account/AccountView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import AssociatedUsersTable from 'src/operator/account/AssociatedUsersTable'
import ConvertAccountToContractOverlay from 'src/operator/account/ConvertAccountToContractOverlay'
import CancelAccountOverlay from 'src/operator/account/CancelAccountOverlay'
import DeleteAccountOverlay from 'src/operator/account/DeleteAccountOverlay'
import ReactivateAccountOverlay from 'src/operator/account/ReactivateAccountOverlay'
import AccountViewHeader from 'src/operator/account/AccountViewHeader'
import AccountGrid from 'src/operator/account/AccountGrid'
import {AccountContext} from 'src/operator/context/account'
Expand All @@ -35,6 +36,7 @@ const AccountView: FC = () => {
<ConvertAccountToContractOverlay />
<CancelAccountOverlay />
<DeleteAccountOverlay />
<ReactivateAccountOverlay />
<AccountViewHeader />
<AccountGrid />
<h2 data-testid="associated-users--title">Associated Users</h2>
Expand Down
11 changes: 11 additions & 0 deletions src/operator/account/AccountViewHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const AccountViewHeader: FC = () => {
setCancelOverlayVisible,
cancelOverlayVisible,
setDeleteOverlayVisible,
setReactivateOverlayVisible,
deleteOverlayVisible,
} = useContext(AccountContext)
const {hasWritePermissions} = useContext(OperatorContext)
Expand Down Expand Up @@ -61,6 +62,16 @@ const AccountViewHeader: FC = () => {
Convert to Contract
</ButtonBase>
)}
{hasWritePermissions && account?.reactivatable && (
<ButtonBase
color={ComponentColor.Primary}
shape={ButtonShape.Default}
onClick={() => setReactivateOverlayVisible(true)}
testID="account-reactivate--button"
>
Reactivate Account
</ButtonBase>
)}
{hasWritePermissions && account?.deletable && (
<ButtonBase
color={ComponentColor.Danger}
Expand Down
87 changes: 87 additions & 0 deletions src/operator/account/ReactivateAccountOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, {FC, useContext} from 'react'
import {
Alert,
ButtonBase,
ButtonShape,
ComponentColor,
ComponentStatus,
Gradients,
IconFont,
Overlay,
RemoteDataState,
} from '@influxdata/clockface'
import {AccountContext} from 'src/operator/context/account'

const ReactivateAccountOverlay: FC = () => {
const {
account,
organizations,
reactivateStatus,
handleReactivateAccount,
setReactivateOverlayVisible,
reactivateOverlayVisible,
} = useContext(AccountContext)

const reactivateAccount = () => {
if (account?.reactivatable) {
try {
handleReactivateAccount()
} catch (e) {
setReactivateOverlayVisible(false)
}
}
}

const message = `
This action will reactivate the Account
${account?.id ?? 'N/A'} and unsuspend the organizations:`

const active = reactivateStatus === RemoteDataState.NotStarted

return (
<Overlay
visible={reactivateOverlayVisible}
renderMaskElement={() => (
<Overlay.Mask gradient={Gradients.DangerDark} style={{opacity: 0.5}} />
)}
testID="reactivate-overlay"
transitionDuration={0}
>
<Overlay.Container maxWidth={600}>
<Overlay.Header
title="Reactivate Account"
style={{color: '#FFFFFF'}}
onDismiss={() => setReactivateOverlayVisible(false)}
/>
<Overlay.Body>
<Alert color={ComponentColor.Danger} icon={IconFont.AlertTriangle}>
This action cannot be undone
</Alert>
<h4 style={{color: '#FFFFFF'}}>
<strong>Warning</strong>
</h4>
{message}
<ul>
{organizations.map(o => (
<li key={o.id}>{o.name ?? 'N/A'} </li>
))}
</ul>
</Overlay.Body>
<Overlay.Footer>
<ButtonBase
color={ComponentColor.Primary}
shape={ButtonShape.Default}
onClick={reactivateAccount}
testID="reactivate-account--confirmation-button"
active={active}
status={active ? ComponentStatus.Default : ComponentStatus.Disabled}
>
I understand, reactivate account.
</ButtonBase>
</Overlay.Footer>
</Overlay.Container>
</Overlay>
)
}

export default ReactivateAccountOverlay
36 changes: 35 additions & 1 deletion src/operator/context/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {useDispatch} from 'react-redux'
// Utils
import {
patchOperatorAccountsConvert,
patchOperatorAccountsReactivate,
deleteOperatorAccount,
getOperatorAccount,
} from 'src/client/unityRoutes'
Expand All @@ -14,6 +15,7 @@ import {
getAccountError,
convertAccountError,
deleteAccountError,
reactivateAccountError,
} from 'src/shared/copy/notifications'

// Types
Expand All @@ -28,33 +30,41 @@ export interface AccountContextType {
accountStatus: RemoteDataState
convertStatus: RemoteDataState
deleteStatus: RemoteDataState
reactivateStatus: RemoteDataState
handleConvertAccountToContract: (contractStartDate: string) => void
handleDeleteAccount: () => void
handleGetAccount: () => void
handleReactivateAccount: () => void
organizations: OperatorOrg[]
setConvertToContractOverlayVisible: (vis: boolean) => void
convertToContractOverlayVisible: boolean
setCancelOverlayVisible: (vis: boolean) => void
cancelOverlayVisible: boolean
setDeleteOverlayVisible: (vis: boolean) => void
deleteOverlayVisible: boolean
setReactivateOverlayVisible: (vis: boolean) => void
reactivateOverlayVisible: boolean
}

export const DEFAULT_CONTEXT: AccountContextType = {
account: null,
accountStatus: RemoteDataState.NotStarted,
convertStatus: RemoteDataState.NotStarted,
deleteStatus: RemoteDataState.NotStarted,
reactivateStatus: RemoteDataState.NotStarted,
handleConvertAccountToContract: () => {},
handleDeleteAccount: () => {},
handleGetAccount: () => {},
handleReactivateAccount: () => {},
organizations: null,
setConvertToContractOverlayVisible: (_: boolean) => {},
convertToContractOverlayVisible: false,
cancelOverlayVisible: false,
setCancelOverlayVisible: (_: boolean) => {},
setDeleteOverlayVisible: (_: boolean) => {},
setReactivateOverlayVisible: (_: boolean) => {},
cancelOverlayVisible: false,
deleteOverlayVisible: false,
reactivateOverlayVisible: false,
}

export const AccountContext =
Expand All @@ -67,9 +77,14 @@ export const AccountProvider: FC<Props> = React.memo(({children}) => {
useState(false)
const [cancelOverlayVisible, setCancelOverlayVisible] = useState(false)
const [deleteOverlayVisible, setDeleteOverlayVisible] = useState(false)
const [reactivateOverlayVisible, setReactivateOverlayVisible] =
useState(false)
const [accountStatus, setAccountStatus] = useState(RemoteDataState.NotStarted)
const [convertStatus, setConvertStatus] = useState(RemoteDataState.NotStarted)
const [deleteStatus, setDeleteStatus] = useState(RemoteDataState.NotStarted)
const [reactivateStatus, setReactivateStatus] = useState(
RemoteDataState.NotStarted
)

const {accountID} = useParams<{accountID: string}>()
const history = useHistory()
Expand Down Expand Up @@ -138,23 +153,42 @@ export const AccountProvider: FC<Props> = React.memo(({children}) => {
}
}, [dispatch, history, accountID])

const handleReactivateAccount = useCallback(async () => {
try {
setReactivateStatus(RemoteDataState.Loading)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is reactivateStatus being used? This pattern gets used in our Redux state-managed components to ensure that in useEffect hooks, we only trigger API calls once on page load, not when loading or when there's an error. But I don't see anything in these components that relies on this status.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I started to remove it, but I instead kept it to disable the reactivate button once they click it. Technically, the operation is idempotent, but it's probably better if they're not smashing it, since it can be a slow operation.

const resp = await patchOperatorAccountsReactivate({accountId: accountID})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
setReactivateStatus(RemoteDataState.Done)
history.push('/operator')
} catch (error) {
console.error({error})
dispatch(notify(reactivateAccountError(accountID)))
}
}, [dispatch, history, accountID])

return (
<AccountContext.Provider
value={{
account,
accountStatus,
convertStatus,
deleteStatus,
reactivateStatus,
handleConvertAccountToContract,
handleDeleteAccount,
handleGetAccount,
handleReactivateAccount,
organizations,
setConvertToContractOverlayVisible,
convertToContractOverlayVisible,
setCancelOverlayVisible,
cancelOverlayVisible,
setDeleteOverlayVisible,
deleteOverlayVisible,
setReactivateOverlayVisible,
reactivateOverlayVisible,
}}
>
{children}
Expand Down
6 changes: 6 additions & 0 deletions src/shared/copy/notifications/categories/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ export const deleteAccountError = (id: string): Notification => ({
message: `Failed to delete the account with the ID ${id}, please try again.`,
})

export const reactivateAccountError = (id: string): Notification => ({
...defaultErrorNotification,
duration: FIVE_SECONDS,
message: `Failed to reactivate the account with the ID ${id}, please try again.`,
})

export const removeUserAccountError = (id: string): Notification => ({
...defaultErrorNotification,
duration: FIVE_SECONDS,
Expand Down