Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(console): App storage - Service plan usage details #2846

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading