Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 322cc64

Browse filesBrowse files
author
Bogdan Tsechoev
committed
Consulting section in Console
1 parent aebe3c3 commit 322cc64
Copy full SHA for 322cc64

File tree

9 files changed

+420
-0
lines changed
Filter options

9 files changed

+420
-0
lines changed

‎ui/packages/platform/package.json

Copy file name to clipboardExpand all lines: ui/packages/platform/package.json
+1Lines changed: 1 addition & 0 deletions
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

Copy file name to clipboardExpand all lines: ui/packages/platform/src/components/IndexPage/IndexPage.tsx
+25Lines changed: 25 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: ui/packages/platform/src/components/types/index.ts
+1Lines changed: 1 addition & 0 deletions
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
+25Lines changed: 25 additions & 0 deletions
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+
}
+223Lines changed: 223 additions & 0 deletions
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+
});
+30Lines changed: 30 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.