From 322cc64f872006cbc97987a52aa440dbff5e8afa Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 30 Jan 2025 22:12:50 +0000 Subject: [PATCH 1/2] Consulting section in Console --- ui/packages/platform/package.json | 1 + .../src/components/IndexPage/IndexPage.tsx | 25 ++ .../platform/src/components/types/index.ts | 1 + .../pages/Consulting/ConsultingWrapper.tsx | 25 ++ .../platform/src/pages/Consulting/index.tsx | 223 ++++++++++++++++++ .../platform/src/pages/Consulting/utils.ts | 30 +++ ui/packages/platform/src/stores/consulting.ts | 101 ++++++++ ui/packages/shared/styles/icons.tsx | 6 + ui/pnpm-lock.yaml | 8 + 9 files changed, 420 insertions(+) create mode 100644 ui/packages/platform/src/pages/Consulting/ConsultingWrapper.tsx create mode 100644 ui/packages/platform/src/pages/Consulting/index.tsx create mode 100644 ui/packages/platform/src/pages/Consulting/utils.ts create mode 100644 ui/packages/platform/src/stores/consulting.ts diff --git a/ui/packages/platform/package.json b/ui/packages/platform/package.json index fcdb53d2..71ef15df 100644 --- a/ui/packages/platform/package.json +++ b/ui/packages/platform/package.json @@ -55,6 +55,7 @@ "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", "moment": "^2.24.0", + "postgres-interval": "^4.0.2", "prop-types": "^15.7.2", "qs": "^6.11.0", "react": "^17.0.2", diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index 6d848ffe..8b22804f 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -73,6 +73,7 @@ import { NotificationWrapper } from 'components/Notification/NotificationWrapper import { SharedUrlWrapper } from 'components/SharedUrl/SharedUrlWrapper' import { ShareUrlDialogWrapper } from 'components/ShareUrlDialog/ShareUrlDialogWrapper' import { BotWrapper } from "pages/Bot/BotWrapper"; +import { ConsultingWrapper } from "pages/Consulting/ConsultingWrapper"; import Actions from '../../actions/actions' import JoeConfig from '../JoeConfig' @@ -623,6 +624,23 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { Audit )} + + + + {icons.consultingIcon} + + Consulting + + ; }} /> + ( + + )} + /> ( diff --git a/ui/packages/platform/src/components/types/index.ts b/ui/packages/platform/src/components/types/index.ts index 5e410daa..cb434205 100644 --- a/ui/packages/platform/src/components/types/index.ts +++ b/ui/packages/platform/src/components/types/index.ts @@ -40,6 +40,7 @@ export interface Orgs { owner_user_id: number is_chat_public_by_default: boolean chats_private_allowed: boolean + consulting_type: string | null data: { plan: string } | null diff --git a/ui/packages/platform/src/pages/Consulting/ConsultingWrapper.tsx b/ui/packages/platform/src/pages/Consulting/ConsultingWrapper.tsx new file mode 100644 index 00000000..bcf7e7c1 --- /dev/null +++ b/ui/packages/platform/src/pages/Consulting/ConsultingWrapper.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { Consulting } from "./index"; +import { RouteComponentProps } from "react-router"; + +export interface ConsultingWrapperProps { + orgId?: number; + history: RouteComponentProps['history'] + project?: string + match: { + params: { + org?: string + } + } + orgData: { + consulting_type: string | null + alias: string + role: { + id: number + } + } +} + +export const ConsultingWrapper = (props: ConsultingWrapperProps) => { + return ; +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Consulting/index.tsx b/ui/packages/platform/src/pages/Consulting/index.tsx new file mode 100644 index 00000000..fc5bb9b7 --- /dev/null +++ b/ui/packages/platform/src/pages/Consulting/index.tsx @@ -0,0 +1,223 @@ +import React, { useEffect } from "react"; +import ConsolePageTitle from "../../components/ConsolePageTitle"; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import { Grid, Paper, Typography } from "@mui/material"; +import Button from "@mui/material/Button"; +import Box from "@mui/material/Box/Box"; +import { observer } from "mobx-react-lite"; +import { consultingStore } from "../../stores/consulting"; +import { ConsultingWrapperProps } from "./ConsultingWrapper"; +import { makeStyles } from "@material-ui/core"; +import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; +import { ProductCardWrapper } from "../../components/ProductCard/ProductCardWrapper"; +import { Link } from "@postgres.ai/shared/components/Link2"; +import Permissions from "../../utils/permissions"; +import { WarningWrapper } from "../../components/Warning/WarningWrapper"; +import { messages } from "../../assets/messages"; +import { ConsoleBreadcrumbsWrapper } from "../../components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper"; +import { formatPostgresInterval } from "./utils"; + + + +const useStyles = makeStyles((theme) => ({ + sectionLabel: { + fontSize: '14px!important', + fontWeight: '700!important' as 'bold', + }, + productCardProjects: { + flex: '1 1 0', + marginRight: '20px', + height: 'maxContent', + gap: 20, + maxHeight: '100%', + + '& svg': { + width: '206px', + height: '130px', + }, + + [theme.breakpoints.down('sm')]: { + flex: '100%', + marginTop: '20px', + minHeight: 'auto !important', + + '&:nth-child(1) svg': { + marginBottom: 0, + }, + + '&:nth-child(2) svg': { + marginBottom: 0, + }, + }, + }, +})) + +export const Consulting = observer((props: ConsultingWrapperProps) => { + const { orgId, orgData, match } = props; + + const classes = useStyles(); + + useEffect(() => { + if (orgId) { + consultingStore.getOrgBalance(orgId); + consultingStore.getTransactions(orgId); + } + }, [orgId]); + + const breadcrumbs = ( + + ) + + if (consultingStore.loading) { + return ( + + {breadcrumbs} + + + + + + ) + } + + if (orgData === null || !Permissions.isAdmin(orgData)) { + return ( + + {breadcrumbs} + + {messages.noPermissionPage} + + ) + } + + if (orgData.consulting_type === null) { + return ( + + {breadcrumbs} + + + Learn more) + } + ]} + > +

+ Your organization is not a consulting customer yet. To learn more about Postgres.AI consulting, visit this page: Consulting. +

+

+ Reach out to the team to discuss consulting opportunities: consulting@postgres.ai. +

+
+
+
+ ) + } + + return ( +
+ {breadcrumbs} + + + {orgData.consulting_type === 'retainer' && + + Retainer balance: + + + {formatPostgresInterval(consultingStore.orgBalance?.[0]?.balance || '00') || 0} + + } + + + + + + + + + Issue tracker (GitLab): + + + + https://gitlab.com/postgres-ai/postgresql-consulting/support/{orgData.alias} + + + + + + + + Book a Zoom call: + + + + https://calend.ly/postgres + + + + + + + Activity: + + { + consultingStore.transactions?.length === 0 + ? + No activity yet + + : + + + + Action + Amount + Date + Details + + + + { + consultingStore.transactions.map((transaction, index) => { + return ( + + {transaction.amount.charAt(0) === '-' ? 'Utilize' : 'Replenish'} + + {formatPostgresInterval(transaction.amount || '00')} + + {new Date(transaction.created_at)?.toISOString()?.split('T')?.[0]} + + {transaction.issue_id + ? + {transaction.description} + + : transaction.description + } + + + ); + }) + } + +
+
+ } +
+
+
+ ); +}); \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Consulting/utils.ts b/ui/packages/platform/src/pages/Consulting/utils.ts new file mode 100644 index 00000000..361feae7 --- /dev/null +++ b/ui/packages/platform/src/pages/Consulting/utils.ts @@ -0,0 +1,30 @@ +import parse, { IPostgresInterval } from "postgres-interval" + +export function formatPostgresInterval(balance: string): string { + const interval: IPostgresInterval = parse(balance); + + const units: Partial, string>> = { + years: 'y', + months: 'mo', + days: 'd', + hours: 'h', + minutes: 'm', + seconds: 's', + milliseconds: 'ms', + }; + + const sign = Object.keys(units) + .map((key) => interval[key as keyof IPostgresInterval] || 0) + .find((value) => value !== 0) ?? 0; + + const isNegative = sign < 0; + + const formattedParts = (Object.keys(units) as (keyof typeof units)[]) + .map((key) => { + const value = interval[key]; + return value && Math.abs(value) > 0 ? `${Math.abs(value)}${units[key]}` : null; + }) + .filter(Boolean); + + return (isNegative ? '-' : '') + formattedParts.join(' '); +} \ No newline at end of file diff --git a/ui/packages/platform/src/stores/consulting.ts b/ui/packages/platform/src/stores/consulting.ts new file mode 100644 index 00000000..0fe13ed7 --- /dev/null +++ b/ui/packages/platform/src/stores/consulting.ts @@ -0,0 +1,101 @@ +import { makeAutoObservable, runInAction } from "mobx"; +import { request } from "../helpers/request"; + +const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; + +interface Transaction { + id: string; + org_id: number; + issue_id: number; + amount: string; + description?: string; + source: string; + created_at: string; +} + +interface OrgBalance { + org_id: number; + balance: string; +} + +class ConsultingStore { + orgBalance: OrgBalance[] | null = null; + transactions: Transaction[] = []; + loading: boolean = false; + error: string | null = null; + + constructor() { + makeAutoObservable(this); + } + + async getOrgBalance(orgId: number) { + this.loading = true; + this.error = null; + + try { + const response = await request(`${apiServer}/org_balance?org_id=eq.${orgId}`, { + method: "GET", + headers: { + + Prefer: "return=representation", + }, + }); + if (!response.ok) { + console.error(`Error: ${response.statusText}`); + } + + const data: OrgBalance[] = await response.json(); + runInAction(() => { + this.orgBalance = data; + }); + } catch (err: unknown) { + runInAction(() => { + if (err instanceof Error) { + this.error = err.message || "Failed to fetch org_balance"; + } else { + this.error = err as string; + } + }); + } finally { + runInAction(() => { + this.loading = false; + }); + } + } + + async getTransactions(orgId: number) { + this.loading = true; + this.error = null; + + try { + const response = await request(`${apiServer}/consulting_transactions?org_id=eq.${orgId}`, { + method: "GET", + headers: { + Prefer: "return=representation", + }, + }); + if (!response.ok) { + console.error(`Error: ${response.statusText}`); + } + + const data: Transaction[] = await response.json(); + runInAction(() => { + this.transactions = data; + }); + } catch (err: unknown) { + runInAction(() => { + if (err instanceof Error) { + this.error = err.message || "Failed to fetch transactions"; + } else { + this.error = err as string; + } + }); + } finally { + runInAction(() => { + this.loading = false; + }); + } + } +} + +export const consultingStore = new ConsultingStore(); \ No newline at end of file diff --git a/ui/packages/shared/styles/icons.tsx b/ui/packages/shared/styles/icons.tsx index d1d52fc5..b8dfbff5 100644 --- a/ui/packages/shared/styles/icons.tsx +++ b/ui/packages/shared/styles/icons.tsx @@ -1907,5 +1907,11 @@ export const icons = { d="m384 85.3333337 85.333333 85.3333333v256H42.66666678L42.66525 193.996358c10.0983011 15.352321 24.2153849 33.106855 42.6673443 48.165701L85.3333334 384H426.666667V181.333334l-53.333334-53.3333337-39.735846.0017872c-5.439498-10.6533523-14.584184-26.4898523-27.734229-42.6683963zM384 320v21.333334H128.0000001V320zm0-64v21.333334H256l-.000063-20.370657c.541196-.318106 1.079687-.63898 1.615477-.962551zM181.333333 42.666667C278.4 42.666667 320 149.333334 320 149.333334S278.4 256 181.333333 256C84.2666668 256 42.66666678 149.333334 42.66666678 149.333334S84.2666668 42.666667 181.333333 42.666667zm0 26.6666667c-61.2906662 0-97.0666662 57.0666666-108.2986662 80.0000003 11.232 22.933333 47.008 80 108.2986662 80 61.290667 0 97.066667-57.066667 108.298667-80-11.232-22.9333337-47.008-80.0000003-108.298667-80.0000003zm0 33.3333333c26.80422 0 48.533334 20.8933783 48.533334 46.666667 0 25.773288-21.729114 46.666666-48.533334 46.666666-26.804219 0-48.5333329-20.893378-48.5333329-46.666666 0-25.7732887 21.7291139-46.666667 48.5333329-46.666667zm0 26.6666667c-11.487522 0-20.8 8.954305-20.8 20.0000003 0 11.045695 9.312478 20 20.8 20 11.487523 0 20.8-8.954305 20.8-20 0-11.0456953-9.312477-20.0000003-20.8-20.0000003z" /> + ), + consultingIcon: ( + + + ) } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index db0ffb93..005f5987 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -378,6 +378,9 @@ importers: moment: specifier: '>=2.29.2' version: 2.30.1 + postgres-interval: + specifier: ^4.0.2 + version: 4.0.2 prop-types: specifier: ^15.7.2 version: 15.8.1 @@ -12307,6 +12310,11 @@ packages: picocolors: 1.0.0 source-map-js: 1.2.0 + /postgres-interval@4.0.2: + resolution: {integrity: sha512-EMsphSQ1YkQqKZL2cuG0zHkmjCCzQqQ71l2GXITqRwjhRleCdv00bDk/ktaSi0LnlaPzAc3535KTrjXsTdtx7A==} + engines: {node: '>=12'} + dev: false + /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} From 5be5369f1281da3741718d66b1b14942bce6ac29 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 30 Jan 2025 22:13:47 +0000 Subject: [PATCH 2/2] fix(ui): Move audit logs into Manage menu + small screen layout fix --- .../AuditSettingsForm/AuditSettingsForm.tsx | 5 ++- .../src/components/IndexPage/IndexPage.tsx | 41 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx index a952f1bd..5d3538f3 100644 --- a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx +++ b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx @@ -91,6 +91,9 @@ const useStyles = makeStyles( 'margin-top': '20px', }, }, + formContainer: { + flexWrap: 'nowrap' + }, textField: { ...styles.inputField, }, @@ -304,7 +307,7 @@ const AuditSettingsForm: React.FC = (props) => {
- + {!isAuditLogsSettingsAvailable && Become an Enterprise customer diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index 6d848ffe..7a6ad1d3 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -602,27 +602,6 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { - {orgPermissions && orgPermissions.auditLogView && ( - - - {icons.auditLogIcon} - - Audit - - )} )} + {orgPermissions && orgPermissions.auditLogView && ( + + + Audit logs + + + )} {orgData !== null && orgPermissions && Permissions.isAdmin(orgData) && orgPermissions.auditLogView && (