Skip to content

Commit

Permalink
feat(console): App storage - Service plan usage details (#2846)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cosmin-Parvulescu committed Feb 29, 2024
1 parent 3f3e67e commit d5025d4
Show file tree
Hide file tree
Showing 2 changed files with 366 additions and 24 deletions.
319 changes: 317 additions & 2 deletions apps/console/app/routes/apps/$clientId/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import createCoreClient from '@proofzero/platform-clients/core'
import { getAuthzHeaderConditionallyFromToken } from '@proofzero/utils'
import {
useActionData,
useFetcher,
useLoaderData,
useOutletContext,
useSubmit,
Expand All @@ -29,8 +30,16 @@ import { type AppLoaderData } from '~/root'

import { Modal } from '@proofzero/design-system/src/molecules/modal/Modal'
import { ToastWithLink } from '@proofzero/design-system/src/atoms/toast/ToastWithLink'
import { useEffect, useMemo, useState } from 'react'
import { HiArrowUp, HiOutlineShoppingCart, HiOutlineX } from 'react-icons/hi'
import { Fragment, useEffect, useMemo, useState } from 'react'
import {
HiArrowUp,
HiDotsVertical,
HiOutlinePencilAlt,
HiOutlineShoppingCart,
HiOutlineTrash,
HiOutlineX,
} from 'react-icons/hi'
import { TbDatabaseImport } from 'react-icons/tb'
import {
ToastType,
Toaster,
Expand All @@ -53,6 +62,13 @@ import { IdentityRefURN } from '@proofzero/urns/identity-ref'
import { IdentityGroupURNSpace } from '@proofzero/urns/identity-group'
import { IdentityURNSpace } from '@proofzero/urns/identity'
import plans, { PlanDetails } from '@proofzero/utils/billing/plans'
import { GetAppExternalDataUsageOutput } from '@proofzero/platform/starbase/src/jsonrpc/methods/getAppExternalDataUsage'
import AppDataStorageModal from '~/components/AppDataStorageModal/AppDataStorageModal'
import ExternalAppDataPackages from '@proofzero/utils/externalAppDataPackages'
import _ from 'lodash'
import { FaCheck, FaTimes } from 'react-icons/fa'
import { Menu, Transition } from '@headlessui/react'
import { ConfirmCancelModal } from './storage.ostrich'

export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(
async ({ request, context, params }) => {
Expand All @@ -68,6 +84,11 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(
clientId: params.clientId as string,
})

const appExternalStorageUsage =
await coreClient.starbase.getAppExternalDataUsage.query({
clientId: params.clientId as string,
})

const entitlements = await coreClient.billing.getEntitlements.query({
URN: appDetails.ownerURN,
})
Expand All @@ -94,6 +115,7 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(
toastNotification,
STRIPE_PUBLISHABLE_KEY: context.env.STRIPE_PUBLISHABLE_KEY,
groupID,
appExternalStorageUsage,
},
{
headers: {
Expand Down Expand Up @@ -773,12 +795,14 @@ export default () => {
toastNotification,
STRIPE_PUBLISHABLE_KEY,
groupID,
appExternalStorageUsage,
} = useLoaderData<{
STRIPE_PUBLISHABLE_KEY: string
entitlements: GetEntitlementsOutput
paymentData: PaymentData
toastNotification?: ToastNotification
groupID?: string
appExternalStorageUsage: GetAppExternalDataUsageOutput
}>()

const actionData = useActionData()
Expand Down Expand Up @@ -817,11 +841,302 @@ export default () => {
}
}, [toastNotification])

const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = useState(false)
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false)

const fetcher = useFetcher()
useEffect(() => {
if (fetcher.state === 'idle' && fetcher.type === 'done') {
setIsSubscriptionModalOpen(false)
}
}, [fetcher])

return (
<>
{isCancelModalOpen && (
<ConfirmCancelModal
isOpen={isCancelModalOpen}
setIsOpen={setIsCancelModalOpen}
clientID={appDetails.clientId!}
redirectToAppBilling={true}
/>
)}
{isSubscriptionModalOpen && (
<AppDataStorageModal
isOpen={isSubscriptionModalOpen}
onClose={() => setIsSubscriptionModalOpen(false)}
subscriptionFetcher={fetcher}
clientID={appDetails.clientId!}
currentPackage={
appDetails.externalAppDataPackageDefinition?.packageDetails
.packageType
}
topUp={appDetails.externalAppDataPackageDefinition?.autoTopUp}
currentPrice={
appDetails.externalAppDataPackageDefinition?.packageDetails.price
}
reads={appExternalStorageUsage?.readUsage}
writes={appExternalStorageUsage?.writeUsage}
readTopUp={appExternalStorageUsage?.readTopUp}
writeTopUp={appExternalStorageUsage?.writeTopUp}
/>
)}

<Toaster position="top-right" reverseOrder={false} />

<section className="mb-4 flex flex-col gap-4">
<Text size="lg" weight="semibold">
Usage based Services
</Text>
<table className="min-w-full table-auto border">
<thead className="bg-gray-50">
<tr className="rounded-tl-lg">
<th className="px-6 py-3 text-left">
<Text
size="xs"
weight="medium"
className="uppercase text-gray-500"
>
Applies to service
</Text>
</th>
<th className="px-6 py-3 text-left">
<Text
size="xs"
weight="medium"
className="uppercase text-gray-500"
>
Unit package
</Text>
</th>
<th className="px-6 py-3 text-left">
<Text
size="xs"
weight="medium"
className="uppercase text-gray-500"
>
Service status
</Text>
</th>
<th className="px-6 py-3 text-left">
<Text
size="xs"
weight="medium"
className="uppercase text-gray-500"
>
Usage
</Text>
</th>
<th className="px-6 py-3 text-left">
<Text
size="xs"
weight="medium"
className="uppercase text-gray-500"
>
Auto top-up
</Text>
</th>
<th className="px-6 py-3 text-right">
<Text
size="xs"
weight="medium"
className="uppercase text-gray-500"
>
Action
</Text>
</th>
</tr>
</thead>

{appDetails.externalAppDataPackageDefinition &&
appExternalStorageUsage && (
<tbody className="bg-white">
<tr>
<td className="px-6 py-3">
<div className=" flex items-center gap-2">
<div className="bg-gray-100 rounded-full p-2">
<TbDatabaseImport className="w-4 h-4 text-gray-600" />
</div>

<Text size="sm" className="text-gray-500">
App data storage
</Text>
</div>
</td>
<td className="px-6 py-3">
<Text size="sm" className="text-gray-500">
{`${
ExternalAppDataPackages[
appDetails.externalAppDataPackageDefinition
.packageDetails.packageType
].title
} Package`}
</Text>
</td>
<td className="px-6 py-3">
<Text size="sm" className="text-gray-500">
{_.upperFirst(
appDetails.externalAppDataPackageDefinition.status
)}
</Text>
</td>
<td className="px-6 py-3">
<div className="flex flex-col">
<Text size="xs" className="text-gray-500">
{`Writes: ${appExternalStorageUsage.writeUsage}/${appExternalStorageUsage.writeAvailable}`}
</Text>
<Text size="xs" className="text-gray-500">
{`Reads: ${appExternalStorageUsage.readUsage}/${appExternalStorageUsage.readAvailable}`}
</Text>
</div>
</td>
<td className="px-6 py-3">
<Text size="sm" className="text-gray-500">
{appDetails.externalAppDataPackageDefinition.autoTopUp ? (
<FaCheck className="text-green-500" />
) : (
<FaTimes className="text-red-500" />
)}
</Text>
</td>
<td className="px-6 py-3">
<div className="flex justify-end">
<Menu>
<Menu.Button>
<div
className="w-8 h-8 flex justify-center items-center cursor-pointer
hover:bg-gray-100 hover:rounded-[6px]"
>
<HiDotsVertical className="text-lg text-gray-400" />
</div>
</Menu.Button>

<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute z-10 right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100
rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none divide-y
divide-gray-100"
>
<div className="p-1 ">
<div
onClick={() => {
setIsSubscriptionModalOpen(true)
}}
className="cursor-pointer"
>
<Menu.Item
as="div"
className="py-2 px-4 flex items-center space-x-3 cursor-pointer
hover:rounded-[6px] hover:bg-gray-100"
>
<HiOutlinePencilAlt className="text-xl font-normal text-gray-400" />
<Text
size="sm"
weight="normal"
className="text-gray-700"
>
Edit Package
</Text>
</Menu.Item>
</div>
</div>

<div className="p-1">
<Menu.Item
as="div"
className="py-2 px-4 flex items-center space-x-3 cursor-pointer
hover:rounded-[6px] hover:bg-gray-100 "
onClick={() => {
setIsCancelModalOpen(true)
}}
>
<HiOutlineTrash className="text-xl font-normal text-red-500" />

<Text
size="sm"
weight="normal"
className="text-red-500"
>
Cancel Service
</Text>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</td>
</tr>
</tbody>
)}
{!appExternalStorageUsage && (
<tbody className="bg-white">
<tr>
<td className="px-6 py-3">
<div className=" flex items-center gap-2">
<div className="bg-gray-100 rounded-full p-2">
<TbDatabaseImport className="w-4 h-4 text-gray-600" />
</div>

<Text size="sm" className="text-gray-500">
App data storage
</Text>
</div>
</td>
<td className="px-6 py-3">
<Text size="sm" className="text-gray-500">
-
</Text>
</td>
<td className="px-6 py-3">
<Text size="sm" className="text-gray-500">
Inactive
</Text>
</td>
<td className="px-6 py-3">
<Text size="sm" className="text-gray-500">
-
</Text>
</td>
<td className="px-6 py-3">
<Text size="sm" className="text-gray-500">
-
</Text>
</td>
<td className="px-6 py-3">
<div className="flex justify-end">
<Button
btnType="secondary-alt"
btnSize="xs"
className="flex flex-row items-center gap-3"
type="submit"
onClick={() => {
setIsSubscriptionModalOpen(true)
}}
>
<HiOutlineShoppingCart className="w-3.5 h-3.5" />
<Text size="sm">Purchase Package</Text>
</Button>
</div>
</td>
</tr>
</tbody>
)}
</table>
</section>

<section className="flex flex-col gap-4">
<Text size="lg" weight="semibold">
Plan based Services
</Text>
<PlanCard
hasUnpaidInvoices={hasUnpaidInvoices}
currentPlan={appDetails.appPlan}
Expand Down
Loading

0 comments on commit d5025d4

Please sign in to comment.