diff --git a/apps/console/app/components/AppDataStorageModal/AppDataStorageModal.tsx b/apps/console/app/components/AppDataStorageModal/AppDataStorageModal.tsx new file mode 100644 index 0000000000..562f9f0fa1 --- /dev/null +++ b/apps/console/app/components/AppDataStorageModal/AppDataStorageModal.tsx @@ -0,0 +1,197 @@ +import { Modal } from '@proofzero/design-system/src/molecules/modal/Modal' +import { Button, Text } from '@proofzero/design-system' +import { FetcherWithComponents } from '@remix-run/react' + +import ExternalAppDataPackages from '@proofzero/utils/externalAppDataPackages' +import { ExternalAppDataPackageType } from '@proofzero/types/billing' +import { useEffect, useState } from 'react' +import { HiOutlineX } from 'react-icons/hi' +import { InputToggle } from '@proofzero/design-system/src/atoms/form/InputToggle' +import { FaCheck, FaTimes } from 'react-icons/fa' + +type AppDataStorageModalProps = { + isOpen: boolean + onClose: () => void + subscriptionFetcher: FetcherWithComponents + currentPackage?: ExternalAppDataPackageType + topUp?: boolean + clientID: string +} + +const AppDataStorageModal: React.FC = ({ + isOpen, + onClose, + subscriptionFetcher, + currentPackage, + topUp = false, + clientID, +}) => { + const [selectedPackage, setSelectedPackage] = + useState( + currentPackage ?? ExternalAppDataPackageType.STARTER + ) + const [autoTopUp, setAutoTopUp] = useState(topUp) + + useEffect(() => { + if (currentPackage) { + setSelectedPackage(currentPackage) + } + }, [currentPackage]) + return ( + <> + onClose()}> + + {selectedPackage && } + {selectedPackage && ( + + )} +
+ + Purchase Entitlement(s) + + +
+ +
+
+ + Choose Package + +
+ + +
+
+ +
+
+ + Reads: + + + {ExternalAppDataPackages[selectedPackage].reads} + +
+
+
+ + Writes: + + + {ExternalAppDataPackages[selectedPackage].writes} + +
+
+
+ +
+ + Auto Top-Up + + +
+ + When enabled it allows for automatic package purchasing to + prevent stopping services from running if you ever ran out of + units. The unused value of the top-up carries over to the next + billing cycle. + + + setAutoTopUp(val)} + disabled={subscriptionFetcher.state !== 'idle'} + /> +
+
+ +
+ + Changes to your subscription + + +
+
+ + {autoTopUp ? ( + + ) : ( + + )} + + + Auto top-up + +
+
+
+ +
+ + +
+
+
+ + ) +} + +export default AppDataStorageModal diff --git a/apps/console/app/routes/apps/$clientId/storage.ostrich.tsx b/apps/console/app/routes/apps/$clientId/storage.ostrich.tsx index 4480dc274c..0f54bd2546 100644 --- a/apps/console/app/routes/apps/$clientId/storage.ostrich.tsx +++ b/apps/console/app/routes/apps/$clientId/storage.ostrich.tsx @@ -1,4 +1,9 @@ -import { Form, useOutletContext, useTransition } from '@remix-run/react' +import { + Form, + useFetcher, + useOutletContext, + useTransition, +} from '@remix-run/react' import { Button, Text } from '@proofzero/design-system' import { DocumentationBadge } from '~/components/DocumentationBadge' import { getRollupReqFunctionErrorWrapper } from '@proofzero/utils/errors' @@ -12,16 +17,21 @@ import classNames from 'classnames' import { appDetailsProps } from '~/types' import { ExternalAppDataPackageType } from '@proofzero/types/billing' import { + HiDotsVertical, + HiOutlinePencilAlt, HiOutlineShoppingCart, HiOutlineTrash, HiOutlineX, } from 'react-icons/hi' import { ExternalAppDataPackageStatus } from '@proofzero/platform.starbase/src/jsonrpc/validators/externalAppDataPackageDefinition' import { Spinner } from '@proofzero/design-system/src/atoms/spinner/Spinner' -import { useState } from 'react' +import { Fragment, useEffect, useState } from 'react' import { Modal } from '@proofzero/design-system/src/molecules/modal/Modal' import dangerVector from '~/images/danger.svg' import { Input } from '@proofzero/design-system/src/atoms/form/Input' +import AppDataStorageModal from '~/components/AppDataStorageModal/AppDataStorageModal' +import { Menu, Transition } from '@headlessui/react' +import { Loader } from '@proofzero/design-system/src/molecules/loader/Loader' export const action: ActionFunction = getRollupReqFunctionErrorWrapper( async ({ request, context, params }) => { @@ -42,9 +52,12 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper( const fd = await request.formData() switch (fd.get('op')) { case 'enable': + const packageType = fd.get('package') as ExternalAppDataPackageType + const autoTopUp = fd.get('top-up') !== '0' await coreClient.starbase.setExternalAppDataPackage.mutate({ clientId, - packageType: ExternalAppDataPackageType.STARTER, + packageType, + autoTopUp, }) break case 'disable': @@ -157,13 +170,39 @@ export default () => { appDetails: appDetailsProps }>() - const [isModalOpen, setIsModalOpen] = useState(false) + const [isCancelModalOpen, setIsCancelModalOpen] = useState(false) + const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = useState(false) + + const fetcher = useFetcher() + useEffect(() => { + if (fetcher.state === 'idle' && fetcher.type === 'done') { + setIsSubscriptionModalOpen(false) + } + }, [fetcher]) return ( <> - {isModalOpen && ( - + {fetcher.state !== 'idle' && } + {isCancelModalOpen && ( + + )} + {isSubscriptionModalOpen && ( + setIsSubscriptionModalOpen(false)} + subscriptionFetcher={fetcher} + clientID={appDetails.clientId!} + currentPackage={ + appDetails.externalAppDataPackageDefinition?.packageDetails + .packageType + } + topUp={appDetails.externalAppDataPackageDefinition?.autoTopUp} + /> )} +
@@ -213,31 +252,91 @@ export default () => { ExternalAppDataPackageStatus.Deleting && ( <> {!Boolean(appDetails.externalAppDataPackageDefinition) && ( -
- - -
- )} - {Boolean(appDetails.externalAppDataPackageDefinition) && ( )} + {Boolean(appDetails.externalAppDataPackageDefinition) && ( + + +
+ +
+
+ + + +
+
{ + setIsSubscriptionModalOpen(true) + }} + className="cursor-pointer" + > + + + + Edit Package + + +
+
+ +
+ { + setIsCancelModalOpen(true) + }} + > + + + + Cancel Service + + +
+
+
+
+ )} )}
diff --git a/packages/types/billing.ts b/packages/types/billing.ts index 9284e48843..fc69716bc2 100644 --- a/packages/types/billing.ts +++ b/packages/types/billing.ts @@ -7,6 +7,7 @@ export enum ServicePlanType { export enum ExternalAppDataPackageType { STARTER = 'STARTER', + SCALE = 'SCALE', } export type ServicePlans = { diff --git a/platform/starbase/src/utils/externalAppDataPackages.ts b/packages/utils/externalAppDataPackages.ts similarity index 58% rename from platform/starbase/src/utils/externalAppDataPackages.ts rename to packages/utils/externalAppDataPackages.ts index 05556a391c..12e23137cf 100644 --- a/platform/starbase/src/utils/externalAppDataPackages.ts +++ b/packages/utils/externalAppDataPackages.ts @@ -2,8 +2,13 @@ import { ExternalAppDataPackageType } from '@proofzero/types/billing' export default { [ExternalAppDataPackageType.STARTER]: { - title: 'Starter Plan', + title: 'Starter', reads: 1000, writes: 1000, }, + [ExternalAppDataPackageType.SCALE]: { + title: 'Scale', + reads: 2000, + writes: 2000, + }, } diff --git a/platform/starbase/src/jsonrpc/methods/setExternalAppDataPackage.ts b/platform/starbase/src/jsonrpc/methods/setExternalAppDataPackage.ts index 2420d4692a..e23700a69d 100644 --- a/platform/starbase/src/jsonrpc/methods/setExternalAppDataPackage.ts +++ b/platform/starbase/src/jsonrpc/methods/setExternalAppDataPackage.ts @@ -19,6 +19,7 @@ import { CoreQueueMessageType } from '@proofzero/platform.core/src/types' export const SetExternalAppDataPackageInputSchema = AppClientIdParamSchema.extend({ packageType: z.nativeEnum(ExternalAppDataPackageType).optional(), + autoTopUp: z.boolean().optional(), }) type SetExternalAppDataPackageInput = z.infer< typeof SetExternalAppDataPackageInputSchema @@ -31,7 +32,7 @@ export const setExternalAppDataPackage = async ({ input: SetExternalAppDataPackageInput ctx: Context }): Promise => { - const { packageType, clientId } = input + const { packageType, clientId, autoTopUp } = input const appURN = ApplicationURNSpace.componentizedUrn(clientId) if (!ctx.allAppURNs || !ctx.allAppURNs.includes(appURN)) @@ -74,7 +75,8 @@ export const setExternalAppDataPackage = async ({ const { error } = await appDO.class.setExternalAppDataPackage( clientId, - packageType + packageType, + autoTopUp ) if (error) throw getErrorCause(error) diff --git a/platform/starbase/src/jsonrpc/validators/externalAppDataPackageDefinition.ts b/platform/starbase/src/jsonrpc/validators/externalAppDataPackageDefinition.ts index b0417f2455..5c378eb170 100644 --- a/platform/starbase/src/jsonrpc/validators/externalAppDataPackageDefinition.ts +++ b/platform/starbase/src/jsonrpc/validators/externalAppDataPackageDefinition.ts @@ -16,4 +16,5 @@ export const ExternalAppDataPackageDetailsSchema = z.object({ export const ExternalAppDataPackageDefinitionSchema = z.object({ packageDetails: ExternalAppDataPackageDetailsSchema, status: z.nativeEnum(ExternalAppDataPackageStatus), + autoTopUp: z.boolean(), }) diff --git a/platform/starbase/src/nodes/application.ts b/platform/starbase/src/nodes/application.ts index 5cb52f134c..4bfa3c557a 100644 --- a/platform/starbase/src/nodes/application.ts +++ b/platform/starbase/src/nodes/application.ts @@ -43,12 +43,9 @@ import { } from '@proofzero/types/billing' import { KeyPairSerialized } from '@proofzero/packages/types/application' import { generateUsageKey, UsageCategory } from '@proofzero/utils/usage' -import ExternalAppDataPackages from '../utils/externalAppDataPackages' +import ExternalAppDataPackages from '@proofzero/utils/externalAppDataPackages' import { NodeMethodReturnValue } from '@proofzero/types/node' -import { - ExternalStorageAlreadyDisabledError, - ExternalStorageAlreadyEnabledError, -} from '../errors' +import { ExternalStorageAlreadyDisabledError } from '../errors' import { ExternalAppDataPackageStatus } from '../jsonrpc/validators/externalAppDataPackageDefinition' type AppDetails = AppUpdateableFields & AppReadableFields @@ -343,7 +340,8 @@ export default class StarbaseApplication extends DOProxy { async setExternalAppDataPackage( clientId: string, - packageType: ExternalAppDataPackageType | undefined + packageType: ExternalAppDataPackageType | undefined, + autoTopUp = false ): Promise> { const externalStorageUsageWriteKey = generateUsageKey( clientId, @@ -370,9 +368,7 @@ export default class StarbaseApplication extends DOProxy { : undefined if (packageDetails) { - if (externalStorageWrites && externalStorageReads) { - return { error: ExternalStorageAlreadyEnabledError } - } else if (!externalStorageWrites || !externalStorageReads) { + if (!externalStorageWrites || !externalStorageReads) { console.warn( `external storage reads or writes for ${clientId} in a bad state; ${externalStorageWrites} writes and ${externalStorageReads} reads.` ) @@ -411,6 +407,7 @@ export default class StarbaseApplication extends DOProxy { { packageDetails, status: ExternalAppDataPackageStatus.Enabled, + autoTopUp, } ) } else { @@ -430,6 +427,7 @@ export default class StarbaseApplication extends DOProxy { { packageDetails: currentPackageDefinition.packageDetails, status: ExternalAppDataPackageStatus.Deleting, + autoTopUp: false, } ) }