Skip to content

Commit f842bbc

Browse files
author
Bogdan Tsechoev
committed
Merge branch 'consulting-page' into 'master'
Consulting section in Console See merge request postgres-ai/database-lab!967
2 parents aebe3c3 + 322cc64 commit f842bbc

File tree

9 files changed

+420
-0
lines changed

9 files changed

+420
-0
lines changed

ui/packages/platform/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"mobx": "^6.3.2",
5656
"mobx-react-lite": "^3.2.0",
5757
"moment": "^2.24.0",
58+
"postgres-interval": "^4.0.2",
5859
"prop-types": "^15.7.2",
5960
"qs": "^6.11.0",
6061
"react": "^17.0.2",

ui/packages/platform/src/components/IndexPage/IndexPage.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import { NotificationWrapper } from 'components/Notification/NotificationWrapper
7373
import { SharedUrlWrapper } from 'components/SharedUrl/SharedUrlWrapper'
7474
import { ShareUrlDialogWrapper } from 'components/ShareUrlDialog/ShareUrlDialogWrapper'
7575
import { BotWrapper } from "pages/Bot/BotWrapper";
76+
import { ConsultingWrapper } from "pages/Consulting/ConsultingWrapper";
7677

7778
import Actions from '../../actions/actions'
7879
import JoeConfig from '../JoeConfig'
@@ -623,6 +624,23 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) {
623624
Audit
624625
</NavLink>
625626
</ListItem>)}
627+
<ListItem
628+
button
629+
className={parentProps.classes.menuSectionHeader}
630+
disabled={isBlocked}
631+
id="menuConsultingTitle"
632+
>
633+
<NavLink
634+
className={parentProps.classes.menuSectionHeaderLink}
635+
activeClassName={cn(parentProps.classes.menuSectionHeaderActiveLink, parentProps.classes.menuSingleSectionHeaderActiveLink)}
636+
to={'/' + org + '/consulting'}
637+
>
638+
<span className={parentProps.classes.menuSectionHeaderIcon}>
639+
{icons.consultingIcon}
640+
</span>
641+
Consulting
642+
</NavLink>
643+
</ListItem>
626644
<ListItem
627645
button
628646
className={cn(parentProps.classes.menuSectionHeader, parentProps.classes.menuSectionHeaderCollapsible)}
@@ -987,6 +1005,13 @@ function OrganizationWrapper(parentProps: OrganizationWrapperProps) {
9871005
return <Redirect to={`/${org}/assistant`} />;
9881006
}}
9891007
/>
1008+
<Route
1009+
path="/:org/consulting"
1010+
exact
1011+
render={(props) => (
1012+
<ConsultingWrapper {...props} {...customProps} {...queryProps} />
1013+
)}
1014+
/>
9901015
<Route
9911016
path="/:org/joe-instances"
9921017
render={(props) => (

ui/packages/platform/src/components/types/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface Orgs {
4040
owner_user_id: number
4141
is_chat_public_by_default: boolean
4242
chats_private_allowed: boolean
43+
consulting_type: string | null
4344
data: {
4445
plan: string
4546
} | null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from "react";
2+
import { Consulting } from "./index";
3+
import { RouteComponentProps } from "react-router";
4+
5+
export interface ConsultingWrapperProps {
6+
orgId?: number;
7+
history: RouteComponentProps['history']
8+
project?: string
9+
match: {
10+
params: {
11+
org?: string
12+
}
13+
}
14+
orgData: {
15+
consulting_type: string | null
16+
alias: string
17+
role: {
18+
id: number
19+
}
20+
}
21+
}
22+
23+
export const ConsultingWrapper = (props: ConsultingWrapperProps) => {
24+
return <Consulting {...props} />;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import React, { useEffect } from "react";
2+
import ConsolePageTitle from "../../components/ConsolePageTitle";
3+
import Table from '@mui/material/Table';
4+
import TableBody from '@mui/material/TableBody';
5+
import TableCell from '@mui/material/TableCell';
6+
import TableContainer from '@mui/material/TableContainer';
7+
import TableHead from '@mui/material/TableHead';
8+
import TableRow from '@mui/material/TableRow';
9+
import { Grid, Paper, Typography } from "@mui/material";
10+
import Button from "@mui/material/Button";
11+
import Box from "@mui/material/Box/Box";
12+
import { observer } from "mobx-react-lite";
13+
import { consultingStore } from "../../stores/consulting";
14+
import { ConsultingWrapperProps } from "./ConsultingWrapper";
15+
import { makeStyles } from "@material-ui/core";
16+
import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner";
17+
import { ProductCardWrapper } from "../../components/ProductCard/ProductCardWrapper";
18+
import { Link } from "@postgres.ai/shared/components/Link2";
19+
import Permissions from "../../utils/permissions";
20+
import { WarningWrapper } from "../../components/Warning/WarningWrapper";
21+
import { messages } from "../../assets/messages";
22+
import { ConsoleBreadcrumbsWrapper } from "../../components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper";
23+
import { formatPostgresInterval } from "./utils";
24+
25+
26+
27+
const useStyles = makeStyles((theme) => ({
28+
sectionLabel: {
29+
fontSize: '14px!important',
30+
fontWeight: '700!important' as 'bold',
31+
},
32+
productCardProjects: {
33+
flex: '1 1 0',
34+
marginRight: '20px',
35+
height: 'maxContent',
36+
gap: 20,
37+
maxHeight: '100%',
38+
39+
'& svg': {
40+
width: '206px',
41+
height: '130px',
42+
},
43+
44+
[theme.breakpoints.down('sm')]: {
45+
flex: '100%',
46+
marginTop: '20px',
47+
minHeight: 'auto !important',
48+
49+
'&:nth-child(1) svg': {
50+
marginBottom: 0,
51+
},
52+
53+
'&:nth-child(2) svg': {
54+
marginBottom: 0,
55+
},
56+
},
57+
},
58+
}))
59+
60+
export const Consulting = observer((props: ConsultingWrapperProps) => {
61+
const { orgId, orgData, match } = props;
62+
63+
const classes = useStyles();
64+
65+
useEffect(() => {
66+
if (orgId) {
67+
consultingStore.getOrgBalance(orgId);
68+
consultingStore.getTransactions(orgId);
69+
}
70+
}, [orgId]);
71+
72+
const breadcrumbs = (
73+
<ConsoleBreadcrumbsWrapper
74+
org={match.params.org}
75+
breadcrumbs={[{ name: "Consulting" }]}
76+
/>
77+
)
78+
79+
if (consultingStore.loading) {
80+
return (
81+
<Box>
82+
{breadcrumbs}
83+
<ConsolePageTitle title={"Consulting"} />
84+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
85+
<PageSpinner />
86+
</Box>
87+
</Box>
88+
)
89+
}
90+
91+
if (orgData === null || !Permissions.isAdmin(orgData)) {
92+
return (
93+
<Box>
94+
{breadcrumbs}
95+
<ConsolePageTitle title={"Consulting"} />
96+
<WarningWrapper>{messages.noPermissionPage}</WarningWrapper>
97+
</Box>
98+
)
99+
}
100+
101+
if (orgData.consulting_type === null) {
102+
return (
103+
<Box>
104+
{breadcrumbs}
105+
<ConsolePageTitle title={"Consulting"} />
106+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
107+
<ProductCardWrapper
108+
inline
109+
className={classes.productCardProjects}
110+
title="Not a customer yet"
111+
actions={[
112+
{
113+
id: 'learn-more',
114+
content: (<Link to="https://postgres.ai/consulting" external target="_blank">Learn more</Link>)
115+
}
116+
]}
117+
>
118+
<p>
119+
Your organization is not a consulting customer yet. To learn more about Postgres.AI consulting, visit this page: <Link to="https://postgres.ai/consulting" external target="_blank">Consulting</Link>.
120+
</p>
121+
<p>
122+
Reach out to the team to discuss consulting opportunities: <Link to="mailto:consulting@postgres.ai" external target="_blank">consulting@postgres.ai</Link>.
123+
</p>
124+
</ProductCardWrapper>
125+
</Box>
126+
</Box>
127+
)
128+
}
129+
130+
return (
131+
<div>
132+
{breadcrumbs}
133+
<ConsolePageTitle title={"Consulting"} />
134+
<Grid container spacing={3}>
135+
{orgData.consulting_type === 'retainer' && <Grid item xs={12} md={8}>
136+
<Typography variant="h6" classes={{root: classes.sectionLabel}}>
137+
Retainer balance:
138+
</Typography>
139+
<Typography variant="h5" sx={{ marginTop: 1}}>
140+
{formatPostgresInterval(consultingStore.orgBalance?.[0]?.balance || '00') || 0}
141+
</Typography>
142+
</Grid>}
143+
<Grid item xs={12} md={8}>
144+
<Box>
145+
<Button variant="contained" component="a" href="https://buy.stripe.com/7sI5odeXt3tB0Eg3cm" target="_blank">
146+
Replenish consulting hours
147+
</Button>
148+
</Box>
149+
</Grid>
150+
<Grid item xs={12} md={8}>
151+
<Box>
152+
<Typography variant="h6" classes={{root: classes.sectionLabel}}>
153+
Issue tracker (GitLab):
154+
</Typography>
155+
<Typography variant="body1" sx={{ marginTop: 1, fontSize: 14}}>
156+
<Link to={`https://gitlab.com/postgres-ai/postgresql-consulting/support/${orgData.alias}`} external target="_blank">
157+
https://gitlab.com/postgres-ai/postgresql-consulting/support/{orgData.alias}
158+
</Link>
159+
</Typography>
160+
</Box>
161+
</Grid>
162+
<Grid item xs={12} md={8}>
163+
<Box>
164+
<Typography variant="h6" classes={{root: classes.sectionLabel}}>
165+
Book a Zoom call:
166+
</Typography>
167+
<Typography variant="body1" sx={{ marginTop: 1, fontSize: 14}}>
168+
<Link to={`https://calend.ly/postgres`} external target="_blank">
169+
https://calend.ly/postgres
170+
</Link>
171+
</Typography>
172+
</Box>
173+
</Grid>
174+
<Grid item xs={12} md={8}>
175+
<Typography variant="h6" classes={{root: classes.sectionLabel}}>
176+
Activity:
177+
</Typography>
178+
{
179+
consultingStore.transactions?.length === 0
180+
? <Typography variant="body1" sx={{ marginTop: 1}}>
181+
No activity yet
182+
</Typography>
183+
: <TableContainer component={Paper} sx={{ marginTop: 1}}>
184+
<Table>
185+
<TableHead>
186+
<TableRow>
187+
<TableCell>Action</TableCell>
188+
<TableCell>Amount</TableCell>
189+
<TableCell>Date</TableCell>
190+
<TableCell>Details</TableCell>
191+
</TableRow>
192+
</TableHead>
193+
<TableBody>
194+
{
195+
consultingStore.transactions.map((transaction, index) => {
196+
return (
197+
<TableRow key={index}>
198+
<TableCell sx={{whiteSpace: 'nowrap'}}>{transaction.amount.charAt(0) === '-' ? 'Utilize' : 'Replenish'}</TableCell>
199+
<TableCell sx={{color: transaction.amount.charAt(0) === '-' ? 'red' : 'green', whiteSpace: 'nowrap'}}>
200+
{formatPostgresInterval(transaction.amount || '00')}
201+
</TableCell>
202+
<TableCell sx={{whiteSpace: 'nowrap'}}>{new Date(transaction.created_at)?.toISOString()?.split('T')?.[0]}</TableCell>
203+
<TableCell>
204+
{transaction.issue_id
205+
? <Link external to={`https://gitlab.com/postgres-ai/postgresql-consulting/support/${orgData.alias}/-/issues/${transaction.issue_id}`} target="_blank">
206+
{transaction.description}
207+
</Link>
208+
: transaction.description
209+
}
210+
</TableCell>
211+
</TableRow>
212+
);
213+
})
214+
}
215+
</TableBody>
216+
</Table>
217+
</TableContainer>
218+
}
219+
</Grid>
220+
</Grid>
221+
</div>
222+
);
223+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import parse, { IPostgresInterval } from "postgres-interval"
2+
3+
export function formatPostgresInterval(balance: string): string {
4+
const interval: IPostgresInterval = parse(balance);
5+
6+
const units: Partial<Record<keyof Omit<IPostgresInterval, 'toPostgres' | 'toISO' | 'toISOString' | 'toISOStringShort'>, string>> = {
7+
years: 'y',
8+
months: 'mo',
9+
days: 'd',
10+
hours: 'h',
11+
minutes: 'm',
12+
seconds: 's',
13+
milliseconds: 'ms',
14+
};
15+
16+
const sign = Object.keys(units)
17+
.map((key) => interval[key as keyof IPostgresInterval] || 0)
18+
.find((value) => value !== 0) ?? 0;
19+
20+
const isNegative = sign < 0;
21+
22+
const formattedParts = (Object.keys(units) as (keyof typeof units)[])
23+
.map((key) => {
24+
const value = interval[key];
25+
return value && Math.abs(value) > 0 ? `${Math.abs(value)}${units[key]}` : null;
26+
})
27+
.filter(Boolean);
28+
29+
return (isNegative ? '-' : '') + formattedParts.join(' ');
30+
}

0 commit comments

Comments
 (0)