From 14d73fa4c3b099e9d530db76949394e16557aa73 Mon Sep 17 00:00:00 2001 From: Jigar Patel <97186961+jigar-arc10@users.noreply.github.com> Date: Wed, 11 Dec 2024 01:29:16 +0530 Subject: [PATCH] feat(provider): provider pricing feature (#475) * added server-access and server-form files * added wallet import page - wip, fixed issue with file upload in server access forms * added become-provider steps ui * server access step added with state support * added provider config and provider attribute screen * added provider process with hoc to prevent access to pages * added progress for becoming prvider for final stage * Code clean up and added navigation logic to homecontainer * package lock updated * more cleanup and remove general warnings * removed unused npm package * fix minor error on api * Added dashboard and actions page * change status api endpoint * added stat line and pie charts * Added console apis to get dashboard data and show appropriate details * fixed actions and changed home component * token varification and refresh token fix * changed wallet connect from wallet status to wallet provider * fixed issue in loading provider status * fixed home loading issue * fixed refresh token, added disabled menu items * fixed build process * feat(provider): added sentry and docker * fix(provider): fixed wallet switching and getting status * feat(provider): reduced number of events in dashboard * feat(provider): added docker compose changes for provider-console * feat(provider): added deployments and deployment detail page * fix(provider): change hours to seconds for calculation purpose) * feat(provider): added auth for deployments and deployment details page * feat(provider): added env and removed settingsprovider * fix(provider): fix lint errors and removed console.logs * fix(provider): become-provider looped, fixed it * fix(provider): router and reset process fixed * fix(provider): removed Get Started button for now * fix(provider): removed unused import in nav * fix(provider): change functions to react fc component * fix(provider): fix lint issues * fix(provider): change functions to react fc component * fix(provider): added docker build and fix build related issues * feat(provider): control machine edit, add from sidebar * feat(provider): added attributes screen * fix(provider): control machine auto connect on page load * fix(provider): fix loading not showing while connecting provider control machine * fix(provider): close drawer on successfull connection * feat(provider): change favicon to akash favicon * feat(provider): provider add, edit and remove and show acitons list page * fix(provider): fix url when provider process finish * feat(provider): provider pricing feature added * feat(provider): added pricing update and extracted provider context * fix(provider): auto import and lint issues fixed * fix(provider): pricing loading screen fix * fix(provider): merge issues with main * fix(provider): pricing update fix * chore: package-lock modified * fix(provider): added types for known props --- .../become-provider/ProviderPricing.tsx | 299 ++++++++++++++---- .../src/components/layout/Sidebar.tsx | 6 +- .../ProviderContext/ProviderContext.tsx | 60 ++++ .../src/context/ProviderContext/index.ts | 1 + apps/provider-console/src/pages/_app.tsx | 5 +- .../src/pages/dashboard/index.tsx | 34 +- .../src/pages/pricing/index.tsx | 95 ++++++ apps/provider-console/src/types/provider.ts | 111 +++++++ .../provider-console/src/utils/sanityUtils.ts | 12 + apps/provider-console/src/utils/urlUtils.ts | 1 + packages/ui/components/alert.tsx | 1 + 11 files changed, 538 insertions(+), 87 deletions(-) create mode 100644 apps/provider-console/src/context/ProviderContext/ProviderContext.tsx create mode 100644 apps/provider-console/src/context/ProviderContext/index.ts create mode 100644 apps/provider-console/src/pages/pricing/index.tsx create mode 100644 apps/provider-console/src/types/provider.ts diff --git a/apps/provider-console/src/components/become-provider/ProviderPricing.tsx b/apps/provider-console/src/components/become-provider/ProviderPricing.tsx index 38c110910..9c3d53e12 100644 --- a/apps/provider-console/src/components/become-provider/ProviderPricing.tsx +++ b/apps/provider-console/src/components/become-provider/ProviderPricing.tsx @@ -1,13 +1,30 @@ "use client"; import React, { useCallback, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { Button, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, Input, Separator, Slider } from "@akashnetwork/ui/components"; +import { + Alert, + AlertDescription, + AlertTitle, + Button, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + Input, + Separator, + Slider +} from "@akashnetwork/ui/components"; import { zodResolver } from "@hookform/resolvers/zod"; import { ArrowDown } from "iconoir-react"; import { useAtom } from "jotai"; import { z } from "zod"; +import { useControlMachine } from "@src/context/ControlMachineProvider"; import providerProcessStore from "@src/store/providerProcessStore"; +import restClient from "@src/utils/restClient"; +import { sanitizeMachineAccess } from "@src/utils/sanityUtils"; import { ResetProviderForm } from "./ResetProviderProcess"; interface ProviderPricingProps { @@ -18,7 +35,11 @@ interface ProviderPricingProps { persistentStorage: number; gpu: number; }; - onComplete: () => void; + editMode?: boolean; + existingPricing?: ProviderPricingValues; + disabled?: boolean; + providerDetails?: any; + onComplete?: () => void; } const providerPricingSchema = z.object({ @@ -33,8 +54,10 @@ const providerPricingSchema = z.object({ type ProviderPricingValues = z.infer; -export const ProviderPricing: React.FC = ({ onComplete }) => { +export const ProviderPricing: React.FC = ({ onComplete, editMode = false, existingPricing, disabled = false, providerDetails }) => { const [providerProcess, setProviderProcess] = useAtom(providerProcessStore.providerProcessAtom); + const { activeControlMachine } = useControlMachine(); + const [showSuccess, setShowSuccess] = React.useState(false); const [resources, setResources] = useState({ cpu: 24, memory: 724, @@ -42,54 +65,83 @@ export const ProviderPricing: React.FC = ({ onComplete }) persistentStorage: 7024, gpu: 5 }); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { const calculateResources = () => { - let totalCpu = 0; - let totalMemory = 0; - let totalStorage = 0; - let totalPersistentStorage = 0; - let totalGpu = 0; + if (!editMode) { + let totalCpu = 0; + let totalMemory = 0; + let totalStorage = 0; + let totalPersistentStorage = 0; + let totalGpu = 0; - providerProcess.machines.forEach(machine => { - totalCpu += parseInt(machine.systemInfo.cpus, 10); - totalMemory += parseInt(machine.systemInfo.memory.replace("Gi", ""), 10); - machine.systemInfo.storage.forEach((storage, index) => { - if (index === 0) { - totalStorage += storage.size / (1024 * 1024 * 1024); - } else { - totalPersistentStorage += storage.size / (1024 * 1024 * 1024); - } + providerProcess.machines.forEach(machine => { + totalCpu += parseInt(machine.systemInfo.cpus, 10); + totalMemory += parseInt(machine.systemInfo.memory.replace("Gi", ""), 10); + machine.systemInfo.storage.forEach((storage, index) => { + if (index === 0) { + totalStorage += storage.size / (1024 * 1024 * 1024); + } else { + totalPersistentStorage += storage.size / (1024 * 1024 * 1024); + } + }); + totalGpu += machine.systemInfo.gpu.count; }); - totalGpu += machine.systemInfo.gpu.count; - }); - setResources({ - cpu: totalCpu, - memory: totalMemory, - storage: totalStorage, - persistentStorage: totalPersistentStorage, - gpu: totalGpu - }); + setResources({ + cpu: totalCpu, + memory: totalMemory, + storage: totalStorage, + persistentStorage: totalPersistentStorage, + gpu: totalGpu + }); + } else if (providerDetails) { + const { activeStats, pendingStats, availableStats } = providerDetails; + + // Calculate totals by summing active, pending, and available stats + const totalCpu = (activeStats.cpu + pendingStats.cpu + availableStats.cpu) / 1000; + const totalGpu = activeStats.gpu + pendingStats.gpu + availableStats.gpu; + // Convert memory from bytes to GB + const totalMemory = Math.floor((activeStats.memory + pendingStats.memory + availableStats.memory) / (1024 * 1024 * 1024)); + // Convert storage from bytes to GB + const totalStorage = Math.floor((activeStats.storage + pendingStats.storage + availableStats.storage) / (1024 * 1024 * 1024)); + + setResources({ + cpu: totalCpu, + memory: totalMemory, + storage: totalStorage, + persistentStorage: 0, // Using same storage value for persistent storage + gpu: totalGpu + }); + } }; calculateResources(); - }, [providerProcess.machines]); + }, [providerProcess.machines, editMode, providerDetails]); const [showAdvanced, setShowAdvanced] = useState(false); const form = useForm({ resolver: zodResolver(providerPricingSchema), - defaultValues: { - cpu: 1.6, - memory: 0.8, - storage: 0.02, - gpu: 100, - persistentStorage: 0.3, - ipScalePrice: 5, - endpointBidPrice: 0.5 - } + defaultValues: editMode + ? existingPricing + : { + cpu: 1.6, + memory: 0.8, + storage: 0.02, + gpu: 100, + persistentStorage: 0.3, + ipScalePrice: 5, + endpointBidPrice: 0.5 + } }); + useEffect(() => { + if (editMode && existingPricing) { + form.reset(existingPricing); + } + }, [editMode, existingPricing, form]); + const watchValues = form.watch(); const calculateEstimatedEarnings = useCallback( @@ -120,16 +172,31 @@ export const ProviderPricing: React.FC = ({ onComplete }) const estimatedEarnings = calculateEstimatedEarnings(watchValues); - const updateProviderPricingAndProceed = (data: any) => { - setProviderProcess(prev => ({ - ...prev, - pricing: data, - process: { - ...prev.process, - providerPricing: true + const updateProviderPricingAndProceed = async (data: any) => { + setIsLoading(true); + if (!editMode) { + setProviderProcess(prev => ({ + ...prev, + pricing: data, + process: { + ...prev.process, + providerPricing: true + } + })); + onComplete && onComplete(); + } else { + const request = { + control_machine: sanitizeMachineAccess(activeControlMachine), + pricing: data + }; + + const response = await restClient.post(`/update-provider-pricing`, request); + if (response) { + setShowSuccess(true); + setTimeout(() => setShowSuccess(false), 20000); } - })); - onComplete(); + } + setIsLoading(false); }; return ( @@ -155,8 +222,22 @@ export const ProviderPricing: React.FC = ({ onComplete }) Scale Bid Price - USD/thread-month
- field.onChange(newValue)} max={4} step={0.01} className="w-full" /> - field.onChange(parseFloat(e.target.value))} className="w-32" step="0.001" /> + field.onChange(newValue)} + max={4} + step={0.01} + className="w-full" + /> + field.onChange(parseFloat(e.target.value))} + className="w-32" + step="0.001" + />
@@ -172,8 +253,22 @@ export const ProviderPricing: React.FC = ({ onComplete }) Scale Bid Price - USD/GB-month
- field.onChange(newValue)} max={4} step={0.001} className="w-full" /> - field.onChange(parseFloat(e.target.value))} className="w-32" step="0.001" /> + field.onChange(newValue)} + max={4} + step={0.001} + className="w-full" + /> + field.onChange(parseFloat(e.target.value))} + className="w-32" + step="0.001" + />
@@ -189,8 +284,22 @@ export const ProviderPricing: React.FC = ({ onComplete }) Scale Bid Price - USD/GB-month
- field.onChange(newValue)} max={0.1} step={0.001} className="w-full" /> - field.onChange(parseFloat(e.target.value))} className="w-32" step="0.001" /> + field.onChange(newValue)} + max={0.1} + step={0.001} + className="w-full" + /> + field.onChange(parseFloat(e.target.value))} + className="w-32" + step="0.001" + />
@@ -206,8 +315,22 @@ export const ProviderPricing: React.FC = ({ onComplete }) Scale Bid Price - USD/GPU-month
- field.onChange(newValue)} max={500} step={0.01} className="w-full" /> - field.onChange(parseFloat(e.target.value))} className="w-32" step="0.01" /> + field.onChange(newValue)} + max={500} + step={0.01} + className="w-full" + /> + field.onChange(parseFloat(e.target.value))} + className="w-32" + step="0.01" + />
@@ -223,8 +346,22 @@ export const ProviderPricing: React.FC = ({ onComplete }) Scale Bid Price - USD/GB-month
- field.onChange(newValue)} max={1} step={0.01} className="w-full" /> - field.onChange(parseFloat(e.target.value))} className="w-32" step="0.01" /> + field.onChange(newValue)} + max={1} + step={0.01} + className="w-full" + /> + field.onChange(parseFloat(e.target.value))} + className="w-32" + step="0.01" + />
@@ -232,7 +369,7 @@ export const ProviderPricing: React.FC = ({ onComplete }) />
- @@ -249,8 +386,22 @@ export const ProviderPricing: React.FC = ({ onComplete }) Scale Bid Price - USD/leased IP-month
- field.onChange(newValue)} max={10} step={0.1} className="w-full" /> - field.onChange(parseFloat(e.target.value))} className="w-32" step="0.1" /> + field.onChange(newValue)} + max={10} + step={0.1} + className="w-full" + /> + field.onChange(parseFloat(e.target.value))} + className="w-32" + step="0.1" + />
@@ -266,8 +417,22 @@ export const ProviderPricing: React.FC = ({ onComplete }) Scale Bid Price - USD/port-month
- field.onChange(newValue)} max={1} step={0.01} className="w-full" /> - field.onChange(parseFloat(e.target.value))} className="w-32" step="0.01" /> + field.onChange(newValue)} + max={1} + step={0.01} + className="w-full" + /> + field.onChange(parseFloat(e.target.value))} + className="w-32" + step="0.01" + />
@@ -318,15 +483,21 @@ export const ProviderPricing: React.FC = ({ onComplete })
-
- -
+
{!editMode && }
- +
+ {showSuccess && !isLoading && ( + + Success + Provider pricing updated successfully + + )} ); diff --git a/apps/provider-console/src/components/layout/Sidebar.tsx b/apps/provider-console/src/components/layout/Sidebar.tsx index 2035d8b98..443de05c9 100644 --- a/apps/provider-console/src/components/layout/Sidebar.tsx +++ b/apps/provider-console/src/components/layout/Sidebar.tsx @@ -64,9 +64,9 @@ export const Sidebar: React.FC = ({ isMobileOpen, handleDrawerToggle, isN { title: "Pricing", icon: props => , - url: "#", - activeRoutes: ["#"], - disabled: true + url: UrlService.pricing(), + activeRoutes: [UrlService.pricing()], + disabled: false }, { title: "Attributes", diff --git a/apps/provider-console/src/context/ProviderContext/ProviderContext.tsx b/apps/provider-console/src/context/ProviderContext/ProviderContext.tsx new file mode 100644 index 000000000..9b2ee6356 --- /dev/null +++ b/apps/provider-console/src/context/ProviderContext/ProviderContext.tsx @@ -0,0 +1,60 @@ +"use client"; +import React from "react"; +import { useQuery } from "react-query"; + +import { ProviderDashoard, ProviderDetails } from "@src/types/provider"; +import consoleClient from "@src/utils/consoleClient"; +import { useWallet } from "../WalletProvider"; + +type ContextType = { + providerDetails: ProviderDetails | undefined; + providerDashboard: ProviderDashoard | undefined; + isLoadingProviderDetails: boolean; + isLoadingProviderDashboard: boolean; +}; + +const ProviderContext = React.createContext({} as ContextType); + +export const ProviderContextProvider = ({ children }) => { + const { address } = useWallet(); + + const { + data: providerDetails, + isLoading: isLoadingProviderDetails + } = useQuery( + "providerDetails", + async () => (await consoleClient.get(`/v1/providers/${address}`)).data, + { + refetchOnWindowFocus: false, + retry: 3, + enabled: !!address + } + ); + + const { data: providerDashboard, isLoading: isLoadingProviderDashboard } = useQuery( + "providerDashboard", + async () => (await consoleClient.get(`/internal/provider-dashboard/${address}`)), + { + refetchOnWindowFocus: false, + retry: 3, + enabled: !!address + } + ); + + return ( + + {children} + + ); +}; + +export const useProvider = () => { + return { ...React.useContext(ProviderContext) }; +}; diff --git a/apps/provider-console/src/context/ProviderContext/index.ts b/apps/provider-console/src/context/ProviderContext/index.ts new file mode 100644 index 000000000..abf4b3c38 --- /dev/null +++ b/apps/provider-console/src/context/ProviderContext/index.ts @@ -0,0 +1 @@ +export { ProviderContextProvider as ProviderContext, useProvider } from "./ProviderContext"; diff --git a/apps/provider-console/src/pages/_app.tsx b/apps/provider-console/src/pages/_app.tsx index 37788383e..0323dab4d 100644 --- a/apps/provider-console/src/pages/_app.tsx +++ b/apps/provider-console/src/pages/_app.tsx @@ -12,6 +12,7 @@ import { ControlMachineProvider } from "@src/context/ControlMachineProvider"; import { CustomChainProvider } from "@src/context/CustomChainProvider"; import { ColorModeProvider } from "@src/context/CustomThemeContext"; import { PricingProvider } from "@src/context/PricingProvider"; +import { ProviderContextProvider } from "@src/context/ProviderContext/ProviderContext"; import { WalletProvider } from "@src/context/WalletProvider"; import { queryClient } from "@src/queries"; import { cn } from "@src/utils/styleUtils"; @@ -28,7 +29,9 @@ export default function App({ Component, pageProps }: AppProps) { - + + + diff --git a/apps/provider-console/src/pages/dashboard/index.tsx b/apps/provider-console/src/pages/dashboard/index.tsx index 9dbd85601..6e074bd51 100644 --- a/apps/provider-console/src/pages/dashboard/index.tsx +++ b/apps/provider-console/src/pages/dashboard/index.tsx @@ -12,11 +12,11 @@ import { Layout } from "@src/components/layout/Layout"; import { ProviderActionList } from "@src/components/shared/ProviderActionList"; import { Title } from "@src/components/shared/Title"; import { withAuth } from "@src/components/shared/withAuth"; -import { useSelectedChain } from "@src/context/CustomChainProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useAKTData } from "@src/queries"; import { useProviderActions, useProviderDashboard, useProviderDetails } from "@src/queries/useProviderQuery"; import { formatUUsd } from "@src/utils/formatUsd"; +import { useSelectedChain } from "@src/context/CustomChainProvider"; const OfflineWarningBanner: React.FC = () => (
@@ -145,25 +145,21 @@ const Dashboard: React.FC = () => { )}
- {isOnline && providerDetails && ( - <> -
-
Resources Leased Summary
-
- {isLoadingProviderDetails ? ( -
- - - - -
- ) : ( - - )} +
+
Resources Leased Summary
+
+ {isLoadingProviderDetails ? ( +
+ + + +
-
- - )} + ) : ( + + )} +
+
diff --git a/apps/provider-console/src/pages/pricing/index.tsx b/apps/provider-console/src/pages/pricing/index.tsx new file mode 100644 index 000000000..45ef05191 --- /dev/null +++ b/apps/provider-console/src/pages/pricing/index.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from "react"; +import { Alert, AlertDescription, AlertTitle, Spinner } from "@akashnetwork/ui/components"; + +import { ProviderPricing } from "@src/components/become-provider/ProviderPricing"; +import { Layout } from "@src/components/layout/Layout"; +import { useControlMachine } from "@src/context/ControlMachineProvider"; +import { useProvider } from "@src/context/ProviderContext"; +import { ProviderPricingType } from "@src/types/provider"; +import restClient from "@src/utils/restClient"; +import { convertFromPricingAPI, sanitizeMachineAccess } from "@src/utils/sanityUtils"; + +const Pricing: React.FunctionComponent = () => { + const { activeControlMachine, controlMachineLoading } = useControlMachine(); + const [existingPricing, setExistingPricing] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const { providerDetails } = useProvider(); + + const fetchPricing = async () => { + try { + setIsLoading(true); + const request = { + control_machine: sanitizeMachineAccess(activeControlMachine) + }; + const response: any = await restClient.post("/get-provider-pricing", request); + if (response) { + setExistingPricing(convertFromPricingAPI(response.pricing)); + } + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (activeControlMachine) { + fetchPricing(); + } + }, [activeControlMachine]); + + return ( + +
+ {isLoading && ( +
+
+ + Loading provider pricing... +
+
+ )} + +
+ {!activeControlMachine && !controlMachineLoading && ( + + Control Machine Required + Please connect your control machine first to start updating pricing settings. + + )} + {controlMachineLoading && ( + + Connecting to Control Machine + +
+ + Please wait while we check control machine access... +
+
+
+ )} + {activeControlMachine && !existingPricing && ( + + Unable to fetch pricing + + Please try again later. + + + + )} + {}} + disabled={activeControlMachine && existingPricing ? false : true} + providerDetails={providerDetails} + /> +
+
+
+ ); +}; + +export default Pricing; diff --git a/apps/provider-console/src/types/provider.ts b/apps/provider-console/src/types/provider.ts new file mode 100644 index 000000000..17faa1ee6 --- /dev/null +++ b/apps/provider-console/src/types/provider.ts @@ -0,0 +1,111 @@ +export interface ProviderDetails { + owner: string; + name: string | null; + hostUri: string; + createdHeight: number; + email: string | null; + website: string; + lastCheckDate: string; + deploymentCount: number; + leaseCount: number; + cosmosSdkVersion: string | null; + akashVersion: string | null; + ipRegion: string; + ipRegionCode: string; + ipCountry: string; + ipCountryCode: string; + ipLat: string; + ipLon: string; + activeStats: Stats; + pendingStats: Stats; + availableStats: Stats; + gpuModels: string[]; + uptime1d: number; + uptime7d: number; + uptime30d: number; + isValidVersion: boolean; + isOnline: boolean; + lastOnlineDate: string; + isAudited: boolean; + attributes: Attribute[]; + host: string | null; + organization: string | null; + statusPage: string | null; + locationRegion: string[]; + country: string | null; + city: string | null; + timezone: string[]; + locationType: string[]; + hostingProvider: string | null; + hardwareCpu: string[]; + hardwareCpuArch: string[]; + hardwareGpuVendor: string[]; + hardwareGpuModels: string[]; + hardwareDisk: string[]; + featPersistentStorage: boolean; + featPersistentStorageType: string[]; + hardwareMemory: string[]; + networkProvider: string | null; + networkSpeedDown: number; + networkSpeedUp: number; + tier: string[]; + featEndpointCustomDomain: boolean; + workloadSupportChia: boolean; + workloadSupportChiaCapabilities: string[]; + featEndpointIp: boolean; + uptime: Uptime[]; +} + +interface Stats { + cpu: number; + gpu: number; + memory: number; + storage: number; +} + +interface Attribute { + key: string; + value: string; + auditedBy: string[]; +} + +interface Uptime { + id: string; + isOnline: boolean; + checkDate: string; +} + +interface LeaseStats { + date: string; + height: number; + activeLeaseCount: number; + totalLeaseCount: number; + dailyLeaseCount: number; + totalUAktEarned: number; + dailyUAktEarned: number; + totalUUsdcEarned: number; + dailyUUsdcEarned: number; + totalUUsdEarned: number; + dailyUUsdEarned: number; + activeCPU: number; + activeGPU: number; + activeMemory: string; + activeEphemeralStorage: string; + activePersistentStorage: string; + activeStorage: string; +} + +export interface ProviderDashoard { + current: LeaseStats; + previous: LeaseStats; +} + +export interface ProviderPricingType { + cpu: number; + memory: number; + storage: number; + persistentStorage: number; + gpu: number; + ipScalePrice: number; + endpointBidPrice: number; +} diff --git a/apps/provider-console/src/utils/sanityUtils.ts b/apps/provider-console/src/utils/sanityUtils.ts index 72f178825..01a590777 100644 --- a/apps/provider-console/src/utils/sanityUtils.ts +++ b/apps/provider-console/src/utils/sanityUtils.ts @@ -13,3 +13,15 @@ export function sanitizeMachineAccess(machine: ControlMachineWithAddress | null) passphrase: machine.access.passphrase || null }; } + +export function convertFromPricingAPI(pricing: any) { + return { + cpu: pricing.price_target_cpu, + memory: pricing.price_target_memory, + storage: pricing.price_target_hd_ephemeral, + persistentStorage: pricing.price_target_hd_pers_ssd, + gpu: Number(pricing.price_target_gpu_mappings.split('=')[1]), + ipScalePrice: pricing.price_target_ip, + endpointBidPrice: pricing.price_target_endpoint + }; +} diff --git a/apps/provider-console/src/utils/urlUtils.ts b/apps/provider-console/src/utils/urlUtils.ts index 86ecc4089..dba63c4d3 100644 --- a/apps/provider-console/src/utils/urlUtils.ts +++ b/apps/provider-console/src/utils/urlUtils.ts @@ -22,4 +22,5 @@ export class UrlService { static privacyPolicy = () => "/privacy-policy"; static termsOfService = () => "/terms-of-service"; static actions = () => "/actions"; + static pricing = () => "/pricing"; } \ No newline at end of file diff --git a/packages/ui/components/alert.tsx b/packages/ui/components/alert.tsx index e6964a23c..e6da18cfa 100644 --- a/packages/ui/components/alert.tsx +++ b/packages/ui/components/alert.tsx @@ -10,6 +10,7 @@ const alertVariants = cva( variants: { variant: { default: "bg-background text-foreground", + success: "border-success/50 text-success dark:border-success [&>svg]:text-success", destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", warning: "border-warning/50 text-warning dark:border-warning-500 [&>svg]:text-warning" }