Skip to content

Commit 4f7f061

Browse files
committed
Merge branch 'billing-status-ui-changes' into 'master'
feat(ui): dle sticky bar for displaying billing status See merge request postgres-ai/database-lab!717
2 parents 895d4c1 + f7137c2 commit 4f7f061

File tree

10 files changed

+310
-5
lines changed

10 files changed

+310
-5
lines changed
+6-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import styles from './styles.module.scss'
2+
import { StickyTopBar } from 'App/Menu/StickyTopBar'
23

34
type Props = {
5+
isTokenValid: boolean | undefined
46
menu: React.ReactNode
57
children: React.ReactNode
68
}
79

810
export const Layout = (props: Props) => {
911
return (
1012
<div className={styles.root}>
13+
{props.isTokenValid && <StickyTopBar />}
1114
<div className={styles.menu}>{props.menu}</div>
12-
<div id="content-container" className={styles.content}>{props.children}</div>
15+
<div id="content-container" className={styles.content}>
16+
{props.children}
17+
</div>
1318
</div>
1419
)
1520
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import CloseIcon from '@material-ui/icons/Close'
2+
import { useState, useEffect, useCallback } from 'react'
3+
import { makeStyles, Snackbar } from '@material-ui/core'
4+
import CheckCircleIcon from '@material-ui/icons/CheckCircle'
5+
import { Button } from '@postgres.ai/shared/components/MenuButton'
6+
import { Spinner } from '@postgres.ai/shared/components/Spinner'
7+
import { capitalizeFirstLetter } from './utils'
8+
9+
import { getBillingStatus } from 'api/configs/getBillingStatus'
10+
import { activateBilling } from 'api/configs/activateBilling'
11+
12+
import styles from './styles.module.scss'
13+
14+
const AUTO_HIDE_DURATION = 1500
15+
16+
export const StickyTopBar = () => {
17+
const useStyles = makeStyles(
18+
{
19+
errorNotification: {
20+
'& > div.MuiSnackbarContent-root': {
21+
backgroundColor: '#f44336!important',
22+
minWidth: '100%',
23+
},
24+
25+
'& div.MuiSnackbarContent-message': {
26+
display: 'flex',
27+
alignItems: 'center',
28+
justifyContent: 'center',
29+
padding: '4px 0',
30+
gap: '10px',
31+
fontSize: '13px',
32+
},
33+
},
34+
successNotification: {
35+
'& > div.MuiSnackbarContent-root': {
36+
backgroundColor: '#4caf50!important',
37+
minWidth: '100%',
38+
},
39+
40+
'& div.MuiSnackbarContent-message': {
41+
display: 'flex',
42+
alignItems: 'center',
43+
justifyContent: 'center',
44+
padding: '4px 0',
45+
gap: '10px',
46+
fontSize: '13px',
47+
},
48+
},
49+
},
50+
{ index: 1 },
51+
)
52+
53+
const classes = useStyles()
54+
const [isLoading, setIsLoading] = useState(false)
55+
const [snackbarState, setSnackbarState] = useState<{
56+
isOpen: boolean
57+
message: string | null
58+
type: 'error' | 'success' | null
59+
}>({
60+
isOpen: false,
61+
message: null,
62+
type: null,
63+
})
64+
const [state, setState] = useState<{
65+
type: 'billingInactive' | 'missingOrgKey' | 'noConnection' | null
66+
pageUrl?: string
67+
message: string
68+
}>({
69+
type: null,
70+
pageUrl: '',
71+
message: '',
72+
})
73+
74+
const handleReset = useCallback(() => {
75+
setState({
76+
type: null,
77+
message: '',
78+
})
79+
}, [])
80+
81+
const handleResetSnackbar = useCallback(() => {
82+
setTimeout(() => {
83+
setSnackbarState({
84+
isOpen: false,
85+
message: null,
86+
type: null,
87+
})
88+
}, AUTO_HIDE_DURATION)
89+
}, [])
90+
91+
const handleActivate = useCallback(() => {
92+
setIsLoading(true)
93+
activateBilling()
94+
.then((res) => {
95+
setIsLoading(false)
96+
if (res.response) {
97+
handleReset()
98+
setSnackbarState({
99+
isOpen: true,
100+
message: 'All DLE SE features are now active.',
101+
type: 'success',
102+
})
103+
} else {
104+
setSnackbarState({
105+
isOpen: true,
106+
message: capitalizeFirstLetter(res.error.message),
107+
type: 'error',
108+
})
109+
}
110+
handleResetSnackbar()
111+
})
112+
.finally(() => {
113+
setIsLoading(false)
114+
})
115+
}, [setIsLoading, handleReset, handleResetSnackbar])
116+
117+
useEffect(() => {
118+
getBillingStatus().then((res) => {
119+
if (!res.response?.billing_active && res.response?.recognized_org) {
120+
setState({
121+
type: 'billingInactive',
122+
pageUrl: res.response?.recognized_org.billing_page,
123+
message:
124+
'No active payment methods are found for your organization on the Postgres.ai Platform; please, visit the',
125+
})
126+
} else if (!res.response?.recognized_org) {
127+
setState({
128+
type: 'missingOrgKey',
129+
message: capitalizeFirstLetter(res.error.message),
130+
})
131+
}
132+
})
133+
}, [handleResetSnackbar])
134+
135+
useEffect(() => {
136+
const handleNoConnection = () => {
137+
setState({
138+
type: 'noConnection',
139+
message: 'No internet connection',
140+
})
141+
}
142+
143+
const handleConnectionRestored = () => {
144+
setState({
145+
type: null,
146+
message: '',
147+
})
148+
handleActivate()
149+
}
150+
151+
window.addEventListener('offline', handleNoConnection)
152+
window.addEventListener('online', handleConnectionRestored)
153+
154+
return () => {
155+
window.removeEventListener('offline', handleNoConnection)
156+
window.removeEventListener('online', handleConnectionRestored)
157+
}
158+
}, [handleActivate])
159+
160+
return (
161+
<>
162+
{state.type && (
163+
<div className={styles.container}>
164+
<p>{state.message}</p>
165+
&nbsp;
166+
{state.type === 'billingInactive' ? (
167+
<>
168+
<a href={state.pageUrl} target="_blank" rel="noreferrer">
169+
billing page
170+
</a>
171+
.&nbsp;Once resolved, &nbsp;
172+
<Button
173+
type="button"
174+
className={styles.activateBtn}
175+
onClick={handleActivate}
176+
disabled={isLoading}
177+
>
178+
re-activate DLE
179+
{isLoading && <Spinner size="sm" className={styles.spinner} />}
180+
</Button>
181+
</>
182+
) : state.type === 'missingOrgKey' ? (
183+
<>
184+
Once resolved,&nbsp;
185+
<Button
186+
type="button"
187+
className={styles.activateBtn}
188+
onClick={handleActivate}
189+
disabled={isLoading}
190+
>
191+
re-activate DLE
192+
{isLoading && <Spinner size="sm" className={styles.spinner} />}
193+
</Button>
194+
</>
195+
) : null}
196+
</div>
197+
)}
198+
<Snackbar
199+
open={snackbarState.isOpen}
200+
className={
201+
snackbarState.type === 'error'
202+
? classes.errorNotification
203+
: snackbarState.type === 'success'
204+
? classes.successNotification
205+
: ''
206+
}
207+
autoHideDuration={AUTO_HIDE_DURATION}
208+
message={
209+
<>
210+
{snackbarState.type === 'error' ? (
211+
<CloseIcon />
212+
) : (
213+
snackbarState.type === 'success' && <CheckCircleIcon />
214+
)}
215+
{snackbarState.message}
216+
</>
217+
}
218+
/>
219+
</>
220+
)
221+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
@import '@postgres.ai/shared/styles/vars';
2+
@import '@postgres.ai/shared/styles/mixins';
3+
4+
.container {
5+
background-color: #fff2e5;
6+
position: fixed;
7+
top: 0;
8+
width: 100%;
9+
display: flex;
10+
align-items: center;
11+
justify-content: center;
12+
padding: 4px;
13+
font-size: 12px;
14+
z-index: 1000;
15+
}
16+
17+
.activateBtn {
18+
height: 19px;
19+
color: #fff;
20+
max-width: max-content;
21+
background-color: $color-orange;
22+
23+
&:hover {
24+
color: #fff;
25+
background-color: $color-orange--hover;
26+
}
27+
28+
&:disabled {
29+
cursor: not-allowed;
30+
background-color: #ccc;
31+
}
32+
}
33+
34+
.spinner {
35+
margin-left: 8px;
36+
color: #fff;
37+
width: 12px !important;
38+
height: 12px !important;
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const capitalizeFirstLetter = (string: string) =>
2+
string.charAt(0).toUpperCase() + string.slice(1) + '.'

ui/packages/ce/src/App/Menu/styles.module.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
.root {
55
display: flex;
66
flex-direction: column;
7-
padding: 24px 8px 8px 8px;
7+
padding: 28px 8px 8px 8px;
88
justify-content: space-between;
99
background: $color-gray-dark;
1010
height: 100%;

ui/packages/ce/src/App/index.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ export const App = observer(() => {
2222

2323
return (
2424
<BrowserRouter>
25-
<Layout menu={<Menu isValidToken={appStore.isValidAuthToken} />}>
25+
<Layout
26+
isTokenValid={appStore.isValidAuthToken && !appStore.engine.isLoading}
27+
menu={<Menu isValidToken={appStore.isValidAuthToken} />}
28+
>
2629
{appStore.isValidAuthToken ? (
2730
<Switch>
2831
<Route path={ROUTES.INSTANCE.path}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { request } from 'helpers/request'
2+
3+
export const activateBilling = async () => {
4+
const response = await request('/admin/activate', {
5+
method: 'POST',
6+
})
7+
8+
return {
9+
response: response.ok ? await response.json() : null,
10+
error: response.ok ? null : await response.json(),
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { request } from 'helpers/request'
2+
3+
export type ResponseType = {
4+
result: string
5+
billing_active: boolean
6+
recognized_org: {
7+
id: string
8+
name: string
9+
alias: string
10+
billing_page: string
11+
priveleged_until: Date
12+
}
13+
}
14+
15+
export const getBillingStatus = async () => {
16+
const response = await request('/admin/billing-status')
17+
18+
return {
19+
response: response.ok ? ((await response.json()) as ResponseType) : null,
20+
error: response.ok ? null : await response.json(),
21+
}
22+
}

ui/packages/ce/src/components/PageContainer/styles.module.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.root {
2-
padding: 24px;
2+
padding: 28px 24px;
33
flex: 1 1 100%;
44
display: flex;
55
flex-direction: column;

ui/packages/shared/components/MenuButton/index.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type BaseProps = {
1515
type ButtonProps = BaseProps & {
1616
type?: 'button' | 'submit'
1717
onClick?: React.MouseEventHandler<HTMLButtonElement>
18+
disabled?: boolean
1819
}
1920

2021
type LinkProps = BaseProps & {
@@ -52,7 +53,7 @@ export const Button = (props: Props) => {
5253

5354
if (!props.type || props.type === 'button' || props.type === 'submit')
5455
return (
55-
<button className={className} onClick={props.onClick}>
56+
<button className={className} onClick={props.onClick} disabled={props.disabled}>
5657
{children}
5758
</button>
5859
)

0 commit comments

Comments
 (0)