diff --git a/package-lock.json b/package-lock.json index 97dc50654..6725a53d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,14 @@ "name": "front-aleph-cloud", "version": "0.34.5", "dependencies": { - "@aleph-front/core": "^1.29.1", + "@aleph-front/core": "^1.31.0", "@aleph-sdk/account": "^1.2.0", "@aleph-sdk/avalanche": "^1.5.0", "@aleph-sdk/client": "^1.4.5", "@aleph-sdk/core": "^1.6.2", "@aleph-sdk/ethereum": "^1.5.0", "@aleph-sdk/evm": "^1.6.2", - "@aleph-sdk/message": "^1.6.2", + "@aleph-sdk/message": "^1.6.3", "@aleph-sdk/solana": "^1.6.2", "@aleph-sdk/superfluid": "^1.4.5", "@fortawesome/fontawesome-svg-core": "^6.3.0", @@ -61,9 +61,9 @@ } }, "node_modules/@aleph-front/core": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.29.1.tgz", - "integrity": "sha512-qt363jeyU5c9Z9xIZAV9puhlRtlTiJuLMl12kp5Xu24271fcSia+WW0I0J4271ALeIpNmlwctVCi4TIsf6Ao2w==", + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.31.0.tgz", + "integrity": "sha512-Lw2y+jf9kylmaUp7QqIL0cpu9BSK0RyBzO/smDUPlCfSzYCgbM6X8na9pYs2Uu+95a21yHV+6ChmdCU0/DY0dg==", "dependencies": { "@monaco-editor/react": "^4.4.6", "react-infinite-scroll-hook": "^4.1.1", @@ -241,10 +241,9 @@ } }, "node_modules/@aleph-sdk/message": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@aleph-sdk/message/-/message-1.6.2.tgz", - "integrity": "sha512-Z+fmovtbOs/8GW/r7C85y7P1af0Z08gxaHjo+Gj7UiDpnVaSskq2zNXhA+lvMFtfzu635/d9kHMxruz7/1kIww==", - "license": "MIT", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@aleph-sdk/message/-/message-1.6.3.tgz", + "integrity": "sha512-UMs1TVf28Fj24jwdgtH8rDLdbVGRJyo/pAZmEfJl0dWAyRKzaJWB0EsLJVF07I3Ls4YOhACy9AHz7RVCszNtyA==", "dependencies": { "axios": "^1.5.1", "form-data": "^4.0.0", @@ -19909,9 +19908,9 @@ }, "dependencies": { "@aleph-front/core": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.29.1.tgz", - "integrity": "sha512-qt363jeyU5c9Z9xIZAV9puhlRtlTiJuLMl12kp5Xu24271fcSia+WW0I0J4271ALeIpNmlwctVCi4TIsf6Ao2w==", + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.31.0.tgz", + "integrity": "sha512-Lw2y+jf9kylmaUp7QqIL0cpu9BSK0RyBzO/smDUPlCfSzYCgbM6X8na9pYs2Uu+95a21yHV+6ChmdCU0/DY0dg==", "requires": { "@monaco-editor/react": "^4.4.6", "react-infinite-scroll-hook": "^4.1.1", @@ -20029,9 +20028,9 @@ } }, "@aleph-sdk/message": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@aleph-sdk/message/-/message-1.6.2.tgz", - "integrity": "sha512-Z+fmovtbOs/8GW/r7C85y7P1af0Z08gxaHjo+Gj7UiDpnVaSskq2zNXhA+lvMFtfzu635/d9kHMxruz7/1kIww==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@aleph-sdk/message/-/message-1.6.3.tgz", + "integrity": "sha512-UMs1TVf28Fj24jwdgtH8rDLdbVGRJyo/pAZmEfJl0dWAyRKzaJWB0EsLJVF07I3Ls4YOhACy9AHz7RVCszNtyA==", "requires": { "axios": "^1.5.1", "form-data": "^4.0.0", diff --git a/package.json b/package.json index f473c6f35..f466d5180 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,14 @@ "lint:fix": "next lint --fix" }, "dependencies": { - "@aleph-front/core": "^1.29.1", + "@aleph-front/core": "^1.31.0", "@aleph-sdk/account": "^1.2.0", "@aleph-sdk/avalanche": "^1.5.0", "@aleph-sdk/client": "^1.4.5", "@aleph-sdk/core": "^1.6.2", "@aleph-sdk/ethereum": "^1.5.0", "@aleph-sdk/evm": "^1.6.2", - "@aleph-sdk/message": "^1.6.2", + "@aleph-sdk/message": "^1.6.3", "@aleph-sdk/solana": "^1.6.2", "@aleph-sdk/superfluid": "^1.4.5", "@fortawesome/fontawesome-svg-core": "^6.3.0", diff --git a/src/components/common/BorderBox/styles.tsx b/src/components/common/BorderBox/styles.tsx index d4334ba33..cc2055719 100644 --- a/src/components/common/BorderBox/styles.tsx +++ b/src/components/common/BorderBox/styles.tsx @@ -13,6 +13,8 @@ export const BorderBox = styled.div<{ backdrop-filter: blur(50px); color: ${theme.color.text}b3; + background: linear-gradient(90deg, ${g0}1a 0%, ${g1}1a 100%); + &::before { content: ''; position: absolute; @@ -33,6 +35,7 @@ export const BorderBox = styled.div<{ -webkit-mask-composite: exclude; mask-composite: exclude; -webkit-mask-composite: xor; + background-image: linear-gradient(90deg, ${g0} 0%, ${g1} 100%); } ` diff --git a/src/components/common/ButtonLink/cmp.tsx b/src/components/common/ButtonLink/cmp.tsx index 12028a839..988e4bf99 100644 --- a/src/components/common/ButtonLink/cmp.tsx +++ b/src/components/common/ButtonLink/cmp.tsx @@ -1,7 +1,8 @@ -import { memo } from 'react' +import { memo, useEffect, useRef, useState } from 'react' import Link from 'next/link' import { Button } from '@aleph-front/core' import { ButtonLinkProps } from './types' +import ResponsiveTooltip from '../ResponsiveTooltip' /** * A wrapper for the nextjs links that are styled as buttons @@ -14,9 +15,19 @@ export const ButtonLink = ({ kind = 'default', size = 'md', disabled, + disabledMessage, + tooltipPosition, children, ...rest }: ButtonLinkProps) => { + const targetRef = useRef(null) + + // Wait until after client-side hydration to show tooltip + const [renderTooltip, setRenderTooltip] = useState(false) + useEffect(() => { + setRenderTooltip(true) + }, []) + const buttonNode = ( + + ) +} +DetailsMenuButton.displayName = 'DetailsMenuButton' + +export default memo(DetailsMenuButton) as typeof DetailsMenuButton diff --git a/src/components/common/DetailsMenuButton/index.ts b/src/components/common/DetailsMenuButton/index.ts new file mode 100644 index 000000000..1e69fa948 --- /dev/null +++ b/src/components/common/DetailsMenuButton/index.ts @@ -0,0 +1,2 @@ +export { default } from './cmp' +export type { ButtonLinkProps } from './types' diff --git a/src/components/common/DetailsMenuButton/types.ts b/src/components/common/DetailsMenuButton/types.ts new file mode 100644 index 000000000..da7f35fdf --- /dev/null +++ b/src/components/common/DetailsMenuButton/types.ts @@ -0,0 +1,9 @@ +import { ButtonProps } from '@aleph-front/core' +import { AnchorHTMLAttributes, ReactNode } from 'react' + +export type ButtonLinkProps = AnchorHTMLAttributes & + Omit & + Partial> & { + href: string + children: ReactNode + } diff --git a/src/components/common/EntityCard/cmp.tsx b/src/components/common/EntityCard/cmp.tsx index 58a8ccd6a..b61635244 100644 --- a/src/components/common/EntityCard/cmp.tsx +++ b/src/components/common/EntityCard/cmp.tsx @@ -52,6 +52,8 @@ export const EntityCard = ({ dashboardPath = '#', createPath = '#', createTarget = '_self', + createDisabled = false, + createDisabledMessage, introductionButtonText, subItems = [], information, @@ -159,6 +161,8 @@ export const EntityCard = ({ size="sm" href={createPath} target={createTarget} + disabled={createDisabled} + disabledMessage={createDisabledMessage} > {introductionButtonText} @@ -168,6 +172,8 @@ export const EntityCard = ({ }, [ createPath, createTarget, + createDisabled, + createDisabledMessage, dashboardPath, information, introductionButtonText, diff --git a/src/components/common/EntityCard/types.ts b/src/components/common/EntityCard/types.ts index e34cceb31..ee61a1462 100644 --- a/src/components/common/EntityCard/types.ts +++ b/src/components/common/EntityCard/types.ts @@ -32,6 +32,8 @@ export type EntityCardProps = { dashboardPath?: string createPath?: string createTarget?: HTMLAttributeAnchorTarget + createDisabled?: boolean + createDisabledMessage?: ReactNode introductionButtonText?: string information: InformationProps storage?: number diff --git a/src/components/common/ExternalLink/cmp.tsx b/src/components/common/ExternalLink/cmp.tsx index a8d15a540..eaf0cf9f5 100644 --- a/src/components/common/ExternalLink/cmp.tsx +++ b/src/components/common/ExternalLink/cmp.tsx @@ -9,6 +9,7 @@ export const ExternalLink = ({ color, typo, underline, + disabled = false, ...props }: ExternalLinkProps) => { return ( @@ -19,10 +20,11 @@ export const ExternalLink = ({ $color={color} $typo={typo} $underline={underline} + $disabled={disabled} {...props} > {text ? text : href} - + ) diff --git a/src/components/common/ExternalLink/styles.ts b/src/components/common/ExternalLink/styles.ts index 3ee278683..f3208001f 100644 --- a/src/components/common/ExternalLink/styles.ts +++ b/src/components/common/ExternalLink/styles.ts @@ -6,12 +6,20 @@ export type StyledExternalLinkProps = { $color?: keyof CoreTheme['color'] $typo?: keyof CoreTheme['typo'] $underline?: boolean + $disabled: boolean } export const StyledExternalLink = styled.a` - ${({ theme, $color = 'white', $typo, $underline = false }) => css` + ${({ theme, $color = 'white', $typo, $underline = false, $disabled }) => css` color: ${theme.color[$color]}; text-decoration: ${$underline ? 'underline' : 'none'}; ${$typo ? getTypoCss($typo) : ''} + + ${$disabled && + css` + // pointer-events: none; + cursor: not-allowed; + color: ${theme.color.disabled}; + `}; `} ` diff --git a/src/components/common/ExternalLink/types.ts b/src/components/common/ExternalLink/types.ts index 5c3802da7..6607c9cac 100644 --- a/src/components/common/ExternalLink/types.ts +++ b/src/components/common/ExternalLink/types.ts @@ -7,4 +7,5 @@ export type ExternalLinkProps = AnchorHTMLAttributes & { color?: keyof CoreTheme['color'] typo?: keyof CoreTheme['typo'] underline?: boolean + disabled?: boolean } diff --git a/src/components/common/Header/cmp.tsx b/src/components/common/Header/cmp.tsx index 1ec7a8684..573175cd6 100644 --- a/src/components/common/Header/cmp.tsx +++ b/src/components/common/Header/cmp.tsx @@ -1,18 +1,145 @@ -import { memo } from 'react' +import { memo, useCallback, useState } from 'react' import Link from 'next/link' -import { AccountPicker, RenderLinkProps } from '@aleph-front/core' +import { + AccountPicker, + Button, + Icon, + RenderLinkProps, + TextInput, +} from '@aleph-front/core' import { StyledHeader, StyledNavbarDesktop, StyledNavbarMobile } from './styles' import { useHeader } from '@/components/common/Header/hook' import AutoBreadcrumb from '@/components/common/AutoBreadcrumb' -import { websiteUrl } from '@/helpers/constants' +import { NAVIGATION_URLS, websiteUrl } from '@/helpers/constants' import { blockchains } from '@/domain/connect/base' import { useEnsNameLookup } from '@/hooks/common/useENSLookup' import LoadingProgress from '../LoadingProgres' +import { useSettings } from '@/hooks/common/useSettings' const CustomLink = (props: RenderLinkProps) => { return props.route.children ? : } +const Settings = () => { + const { apiServerDisplay, handleSetApiServer } = useSettings() + + const preferredServers = ['api.aleph.im', 'api2.aleph.im', 'api3.aleph.im'] + + const isCustomServer = !preferredServers.includes(apiServerDisplay) + const serverList = isCustomServer + ? [...preferredServers, apiServerDisplay] + : preferredServers + + const [currentView, setCurrentView] = useState<'main' | 'apiServer'>('main') + const setView = useCallback((view: 'main' | 'apiServer') => { + setCurrentView(view) + }, []) + + const [customApiServer, setCustomApiServer] = useState('') + const handleCustomApiServerChange = useCallback( + (event: React.ChangeEvent) => { + setCustomApiServer(event.target.value) + }, + [], + ) + + const setApiServer = useCallback( + (server: string) => { + handleSetApiServer(server) + }, + [handleSetApiServer], + ) + + return ( +
+
+ {currentView === 'main' ? ( + + ) : currentView === 'apiServer' ? ( +
+
+ +

+ API Servers +

+
+
+
+ {serverList.map((server) => ( + + ))} +
+
+

+ Custom +

+ setApiServer(customApiServer)}> + + + } + /> +
+ +
+

+ Accepts bare hostnames (e.g. api.example.com) or full URLs. +

+

+ Full URLs are stored as their origin. +

+
+
+
+
+ ) : null} +
+
+ ) +} + // ---------------------------- export const Header = () => { @@ -25,7 +152,7 @@ export const Header = () => { networks, accountAddress, accountBalance, - accountVouchers, + accountCreditBalance, rewards, selectedNetwork, handleToggle, @@ -54,15 +181,23 @@ export const Header = () => { isMobile accountAddress={accountAddress} accountBalance={accountBalance} - accountVouchers={accountVouchers} + showCredits + disabledTopUp + accountCredits={accountCreditBalance} blockchains={blockchains} networks={networks} selectedNetwork={selectedNetwork} rewards={rewards} ensName={ensName} + settingsContent={} handleConnect={handleConnect} handleDisconnect={handleDisconnect} handleSwitchNetwork={handleSwitchNetwork} + Link={CustomLinkMemo} + externalUrl={{ + text: 'Legacy console', + url: NAVIGATION_URLS.legacyConsole.home, + }} /> ), logoHref: websiteUrl, @@ -74,15 +209,23 @@ export const Header = () => { } handleConnect={handleConnect} handleDisconnect={handleDisconnect} handleSwitchNetwork={handleSwitchNetwork} + Link={CustomLinkMemo} + externalUrl={{ + text: 'Legacy console', + url: NAVIGATION_URLS.legacyConsole.home, + }} /> diff --git a/src/components/common/Header/hook.ts b/src/components/common/Header/hook.ts index 3cdd832a3..32898c364 100644 --- a/src/components/common/Header/hook.ts +++ b/src/components/common/Header/hook.ts @@ -1,5 +1,5 @@ import { useRouter } from 'next/router' -import { useCallback, useState, useMemo, useEffect } from 'react' +import { useCallback, useState, useMemo } from 'react' import { useAppState } from '@/contexts/appState' import { AccountPickerProps, @@ -20,7 +20,7 @@ import { useAccountRewards as useNodeRewards } from '../../../hooks/common/node/ export type UseHeaderReturn = UseRoutesReturn & { accountAddress?: string accountBalance?: number - accountVouchers?: AccountPickerProps['accountVouchers'] | undefined + accountCreditBalance?: number networks: Network[] pathname: string breadcrumbNames: UseBreadcrumbNamesReturn['names'] @@ -41,8 +41,8 @@ export function useHeader(): UseHeaderReturn { blockchain, account, balance: accountBalance, + creditBalance: accountCreditBalance, } = state.connection - const { voucherManager } = state.manager const { handleConnect: connect, handleDisconnect: disconnect } = useConnection({ triggerOnMount: true }) @@ -117,46 +117,6 @@ export function useHeader(): UseHeaderReturn { [wallets], ) - // -------------------- - const [accountVouchers, setAccountVouchers] = useState< - AccountPickerProps['accountVouchers'] - >([]) - - useEffect(() => { - const fetchAndFormatVouchers = async () => { - if (!account || !voucherManager) return - - const vouchers = await voucherManager.getAll() - const groupedVouchers = vouchers.reduce( - (grouped, voucher) => { - const { metadataId } = voucher - if (!grouped[metadataId]) grouped[metadataId] = [] - grouped[metadataId].push(voucher) - return grouped - }, - {} as Record, - ) - - const formattedVouchers = Object.values(groupedVouchers).flatMap( - (vouchers) => { - if (!vouchers.length) return [] - - const { name, icon } = vouchers[0] - return { - name: name, - image: icon, - imageAlt: name, - amount: vouchers.length, - } - }, - ) - - setAccountVouchers(formattedVouchers) - } - - fetchAndFormatVouchers() - }, [account, voucherManager]) - // -------------------- const handleConnect = useCallback( @@ -227,7 +187,7 @@ export function useHeader(): UseHeaderReturn { return { accountAddress: account?.address, accountBalance, - accountVouchers, + accountCreditBalance, networks, pathname, routes, diff --git a/src/components/common/NewEntityTab/cmp.tsx b/src/components/common/NewEntityTab/cmp.tsx index 28ced088e..9efa597c4 100644 --- a/src/components/common/NewEntityTab/cmp.tsx +++ b/src/components/common/NewEntityTab/cmp.tsx @@ -14,6 +14,7 @@ export default function NewEntityTab(props: NewEntityTabProps) { { id: 'function', name: 'Function', + disabled: true, }, { id: 'instance', @@ -28,6 +29,7 @@ export default function NewEntityTab(props: NewEntityTabProps) { id: 'confidential', name: 'Confidential', label: { label: 'BETA', position: 'top' }, + disabled: true, }, ]} tw="overflow-hidden" diff --git a/src/components/common/Price/cmp.tsx b/src/components/common/Price/cmp.tsx index 8a4573fa8..904b20dcd 100644 --- a/src/components/common/Price/cmp.tsx +++ b/src/components/common/Price/cmp.tsx @@ -3,17 +3,30 @@ import { StyledPrice } from './styles' import { PriceProps } from './types' import { Logo } from '@aleph-front/core' import { humanReadableCurrency } from '@/helpers/utils' +import Skeleton from '@/components/common/Skeleton' export const Price = ({ value, + type = 'token', duration, iconSize = '0.75em', + loading = false, ...rest }: PriceProps) => { + if (loading) { + return ( + + + + ) + } + return ( {humanReadableCurrency(value)} - + {type === 'token' && ( + + )} {duration && / {duration}} ) diff --git a/src/components/common/Price/types.ts b/src/components/common/Price/types.ts index 4c47eb3c6..316230a28 100644 --- a/src/components/common/Price/types.ts +++ b/src/components/common/Price/types.ts @@ -1,8 +1,12 @@ import { StreamDurationUnit } from '@/hooks/form/useSelectStreamDuration' import { HTMLAttributes } from 'react' +export type PriceType = 'token' | 'credit' + export type PriceProps = HTMLAttributes & { value: number | undefined + type?: PriceType duration?: StreamDurationUnit iconSize?: string + loading?: boolean } diff --git a/src/components/common/StakingNodesTable/cmp.tsx b/src/components/common/StakingNodesTable/cmp.tsx index dba018a60..3b48dda30 100644 --- a/src/components/common/StakingNodesTable/cmp.tsx +++ b/src/components/common/StakingNodesTable/cmp.tsx @@ -14,7 +14,7 @@ import StakeButton from '@/components/common/StakeButton' import { Account } from '@aleph-sdk/account' import NodeAmount from '@/components/common/NodeAmount' import ButtonLink from '../ButtonLink' -import { apiServer } from '@/helpers/constants' +import { useSettings } from '@/hooks/common/useSettings' import Image from 'next/image' import { UseSortedListReturn } from '@/hooks/common/useSortedList' @@ -45,6 +45,8 @@ export const StakingNodesTable = ({ handleStake: onStake, handleUnstake: onUnstake, }: StakingNodesTableProps) => { + const { apiServer } = useSettings() + const columns = useMemo(() => { const cols = [ { @@ -140,6 +142,7 @@ export const StakingNodesTable = ({ }, [ account, accountBalance, + apiServer, nodes, nodesIssues, onStake, diff --git a/src/components/common/ToggleDashboard/cmp.tsx b/src/components/common/ToggleDashboard/cmp.tsx index fbda95468..7020553b3 100644 --- a/src/components/common/ToggleDashboard/cmp.tsx +++ b/src/components/common/ToggleDashboard/cmp.tsx @@ -1,20 +1,41 @@ -import { ReactNode, memo, useCallback, useRef, useState } from 'react' +import { + Dispatch, + ReactNode, + SetStateAction, + memo, + useCallback, + useRef, +} from 'react' import tw from 'twin.macro' import { Button, Icon, useTransition, useBounds } from '@aleph-front/core' import { StyledButtonsContainer, StyledToggleContainer } from './styles' export type ToggleDashboardProps = { + open: boolean + setOpen: Dispatch> + buttons?: ReactNode + toggleButton?: { children: ReactNode; disabled?: boolean } children?: ReactNode } export const ToggleDashboard = ({ buttons, children, + open, + setOpen, + toggleButton = { + children: ( + <> + + open dashboard + + ), + disabled: false, + }, ...rest }: ToggleDashboardProps) => { - const [open, setOpen] = useState(true) - const handleToogle = useCallback(() => setOpen((prev) => !prev), []) + const handleToogle = useCallback(() => setOpen((prev) => !prev), [setOpen]) const ref = useRef(null) @@ -57,9 +78,9 @@ export const ToggleDashboard = ({ size="md" onClick={handleToogle} tw="gap-2.5" + disabled={toggleButton.disabled} > - - open dashboard + {toggleButton.children} )} diff --git a/src/components/common/entityData/EntityConnectionMethods/cmp.tsx b/src/components/common/entityData/EntityConnectionMethods/cmp.tsx index 180b9f35b..1954b3887 100644 --- a/src/components/common/entityData/EntityConnectionMethods/cmp.tsx +++ b/src/components/common/entityData/EntityConnectionMethods/cmp.tsx @@ -1,5 +1,5 @@ import React, { memo } from 'react' -import { NoisyContainer } from '@aleph-front/core' +import { NoisyContainer, Tabs } from '@aleph-front/core' import { EntityConnectionMethodsProps } from './types' import Skeleton from '../../Skeleton' import { Text } from '@/components/pages/console/common' @@ -9,16 +9,21 @@ import InfoTitle from '../InfoTitle' export const EntityConnectionMethods = ({ executableStatus, + sshForwardedPort, }: EntityConnectionMethodsProps) => { const { isLoading, formattedIPv4, formattedIPv6, - formattedSSHCommand, + formattedIpv4SSHCommand, + formattedIpv6SSHCommand, handleCopyIpv4, handleCopyIpv6, - handleCopyCommand, - } = useEntityConnectionMethods({ executableStatus }) + handleCopyIpv4Command, + handleCopyIpv6Command, + } = useEntityConnectionMethods({ executableStatus, sshForwardedPort }) + + const [tabSelected, setTabSelected] = React.useState<'ipv4' | 'ipv6'>('ipv4') return ( <> @@ -27,39 +32,67 @@ export const EntityConnectionMethods = ({
+ { + setTabSelected(tabSelected === 'ipv4' ? 'ipv6' : 'ipv4') + }} + align="left" + tabs={[ + { + id: 'ipv4', + name: 'ipv4', + }, + { + id: 'ipv6', + name: 'ipv6', + }, + ]} + tw="overflow-hidden -mt-3" + />
SSH COMMAND
{!isLoading ? ( - - >_ {formattedSSHCommand} + { + if (tabSelected === 'ipv4') { + handleCopyIpv4Command() + } else { + handleCopyIpv6Command() + } + }} + > + + >_{' '} + {tabSelected === 'ipv4' + ? formattedIpv4SSHCommand + : formattedIpv6SSHCommand} + ) : ( )}
- {(isLoading || formattedIPv4 !== '') && ( -
- IPV4 -
- {!isLoading ? ( - - {formattedIPv4} - - ) : ( - - )} -
-
- )} -
- IPV6 + {tabSelected === 'ipv4' ? 'IPV4' : 'IPV6'}
{!isLoading ? ( - - {formattedIPv6} + { + if (tabSelected === 'ipv4') { + handleCopyIpv4() + } else { + handleCopyIpv6() + } + }} + > + + {tabSelected === 'ipv4' ? formattedIPv4 : formattedIPv6} + ) : ( diff --git a/src/components/common/entityData/EntityConnectionMethods/hook.ts b/src/components/common/entityData/EntityConnectionMethods/hook.ts index 69ec93cff..2a11bbd87 100644 --- a/src/components/common/entityData/EntityConnectionMethods/hook.ts +++ b/src/components/common/entityData/EntityConnectionMethods/hook.ts @@ -11,6 +11,7 @@ import { */ export function useEntityConnectionMethods({ executableStatus, + sshForwardedPort = '????', }: EntityConnectionMethodsProps): UseEntityConnectionMethodsReturn { // Check if data is still loading const isLoading = !executableStatus @@ -25,23 +26,35 @@ export function useEntityConnectionMethods({ return executableStatus?.hostIpv4 || '' }, [executableStatus?.hostIpv4]) - // Format the SSH command - const formattedSSHCommand = useMemo(() => { + // Format the IPV6 SSH command + const formattedIpv4SSHCommand = useMemo(() => { + return `ssh root@${formattedIPv4} -p ${sshForwardedPort}` + }, [formattedIPv4, sshForwardedPort]) + + // Format the IPV4 SSH command + const formattedIpv6SSHCommand = useMemo(() => { return `ssh root@${formattedIPv6}` }, [formattedIPv6]) // Create clipboard handlers const handleCopyIpv4 = useCopyToClipboardAndNotify(formattedIPv4) const handleCopyIpv6 = useCopyToClipboardAndNotify(formattedIPv6) - const handleCopyCommand = useCopyToClipboardAndNotify(formattedSSHCommand) + const handleCopyIpv4Command = useCopyToClipboardAndNotify( + formattedIpv4SSHCommand, + ) + const handleCopyIpv6Command = useCopyToClipboardAndNotify( + formattedIpv6SSHCommand, + ) return { isLoading, formattedIPv6, formattedIPv4, - formattedSSHCommand, + formattedIpv4SSHCommand, + formattedIpv6SSHCommand, handleCopyIpv6, handleCopyIpv4, - handleCopyCommand, + handleCopyIpv4Command, + handleCopyIpv6Command, } } diff --git a/src/components/common/entityData/EntityConnectionMethods/types.ts b/src/components/common/entityData/EntityConnectionMethods/types.ts index 345140686..49dd0594c 100644 --- a/src/components/common/entityData/EntityConnectionMethods/types.ts +++ b/src/components/common/entityData/EntityConnectionMethods/types.ts @@ -3,6 +3,7 @@ import { ExecutableStatus } from '@/domain/executable' // Raw data input props export type EntityConnectionMethodsProps = { executableStatus?: ExecutableStatus + sshForwardedPort?: string } // Formatted data returned by the hook @@ -10,8 +11,10 @@ export type UseEntityConnectionMethodsReturn = { isLoading: boolean formattedIPv6: string formattedIPv4: string - formattedSSHCommand: string + formattedIpv4SSHCommand: string + formattedIpv6SSHCommand: string handleCopyIpv6: () => void handleCopyIpv4: () => void - handleCopyCommand: () => void + handleCopyIpv4Command: () => void + handleCopyIpv6Command: () => void } diff --git a/src/components/common/entityData/EntityPayment/cmp.tsx b/src/components/common/entityData/EntityPayment/cmp.tsx index 338fa37f0..74da686c3 100644 --- a/src/components/common/entityData/EntityPayment/cmp.tsx +++ b/src/components/common/entityData/EntityPayment/cmp.tsx @@ -15,6 +15,7 @@ import InfoTitle from '../InfoTitle' const PaymentCard = ({ paymentData }: { paymentData: PaymentData }) => { const { isStream, + isCredit, totalSpent, formattedBlockchain, formattedFlowRate, @@ -36,7 +37,9 @@ const PaymentCard = ({ paymentData }: { paymentData: PaymentData }) => { tw="flex items-center gap-1 px-3 py-1" > -
ALEPH
+
+ {isCredit ? 'CREDITS' : 'ALEPH'} +

{totalSpent ? totalSpent : } @@ -50,6 +53,8 @@ const PaymentCard = ({ paymentData }: { paymentData: PaymentData }) => { {loading ? ( + ) : isCredit ? ( + 'Credit' ) : isStream ? ( 'Stream' ) : ( @@ -96,29 +101,29 @@ const PaymentCard = ({ paymentData }: { paymentData: PaymentData }) => {

- {isStream && ( - <> -
- FLOW RATE - - {formattedFlowRate ? ( - formattedFlowRate - ) : ( - - )} - -
-
- TIME ELAPSED - - {formattedDuration ? ( - formattedDuration - ) : ( - - )} - -
- + {(isStream || isCredit) && ( +
+ FLOW RATE + + {formattedFlowRate ? ( + formattedFlowRate + ) : ( + + )} + +
+ )} + {(isStream || isCredit) && ( +
+ TIME ELAPSED + + {formattedDuration ? ( + formattedDuration + ) : ( + + )} + +
)}
diff --git a/src/components/common/entityData/EntityPayment/hook.ts b/src/components/common/entityData/EntityPayment/hook.ts index eee7ae4d8..4f153fb74 100644 --- a/src/components/common/entityData/EntityPayment/hook.ts +++ b/src/components/common/entityData/EntityPayment/hook.ts @@ -45,17 +45,24 @@ export function useFormatPayment( [paymentType], ) + // Determine if payment is pay-as-you-go + const isCredit = useMemo( + () => paymentType === PaymentType.credit, + [paymentType], + ) + // Format total spent amount using the time components for PAYG type const totalSpent = useMemo(() => { if (!cost) return - if (!isStream) return cost.toString() + if (!isStream && !isCredit) return cost.toString() if (!runningTime) return // Use only the remainder (hours, minutes, seconds) from runningTime const { days, hours, minutes } = getTimeComponents(runningTime) const runningTimeInHours = days * 24 + hours + minutes / 60 - return (cost * runningTimeInHours).toFixed(6) - }, [cost, isStream, runningTime]) + + return Math.round(cost * runningTimeInHours) + }, [cost, isStream, isCredit, runningTime]) // Format blockchain name const formattedBlockchain = useMemo(() => { @@ -65,12 +72,13 @@ export function useFormatPayment( // Format flow rate to show daily cost const formattedFlowRate = useMemo(() => { - if (!isStream) return + if (!isStream && !isCredit) return if (!cost) return - const dailyRate = cost * 24 - return `~${dailyRate.toFixed(4)}/day` - }, [cost, isStream]) + const dailyRate = Math.round(cost * 24) + + return `~${dailyRate}/day` + }, [cost, isCredit, isStream]) // Format start date const formattedStartDate = useMemo(() => { @@ -108,8 +116,6 @@ export function useFormatPayment( const receiverType = useMemo(() => { if (!receiver) return undefined - console.log('Receiver address:', receiver) - console.log('communityWalletAddress:', communityWalletAddress) if (receiver == (communityWalletAddress as string)) { return 'Community Wallet (20%)' } @@ -120,6 +126,7 @@ export function useFormatPayment( return { isStream, + isCredit, totalSpent, formattedBlockchain, formattedFlowRate, diff --git a/src/components/common/entityData/EntityPayment/types.ts b/src/components/common/entityData/EntityPayment/types.ts index a6edeb29c..dd38a8ab8 100644 --- a/src/components/common/entityData/EntityPayment/types.ts +++ b/src/components/common/entityData/EntityPayment/types.ts @@ -16,6 +16,11 @@ export interface HoldingPaymentData extends BasePaymentData { paymentType: PaymentType.hold } +// Credit payment data (credit payment) +export interface CreditPaymentData extends BasePaymentData { + paymentType: PaymentType.credit +} + // Stream payment data (pay-as-you-go) export interface StreamPaymentData extends BasePaymentData { paymentType: PaymentType.superfluid @@ -23,7 +28,10 @@ export interface StreamPaymentData extends BasePaymentData { } // Union type for all payment data types -export type PaymentData = HoldingPaymentData | StreamPaymentData +export type PaymentData = + | HoldingPaymentData + | CreditPaymentData + | StreamPaymentData // Props for the EntityPayment component - just an array of payment data export interface EntityPaymentProps { @@ -33,7 +41,8 @@ export interface EntityPaymentProps { // Formatted data returned by the hook for display export interface FormattedPaymentData { isStream: boolean - totalSpent?: string + isCredit: boolean + totalSpent?: string | number formattedBlockchain?: string formattedFlowRate?: string formattedStartDate?: string diff --git a/src/components/common/entityData/EntityPortForwarding/cmp.tsx b/src/components/common/entityData/EntityPortForwarding/cmp.tsx index 3b4815edc..64330c36b 100644 --- a/src/components/common/entityData/EntityPortForwarding/cmp.tsx +++ b/src/components/common/entityData/EntityPortForwarding/cmp.tsx @@ -17,11 +17,12 @@ export const EntityPortForwarding = ({ entityHash, executableStatus, executableManager, + ports, + onPortsChange, }: EntityPortForwardingProps) => { const { // State showPortForm, - ports, // Actions handleAddPort, handleCancelAddPort, @@ -31,6 +32,8 @@ export const EntityPortForwarding = ({ entityHash, executableStatus, executableManager, + ports, + onPortsChange, }) const tooltipContent = ( @@ -60,7 +63,7 @@ export const EntityPortForwarding = ({
+ ports: ForwardedPort[] + onPortsChange?: (ports: ForwardedPort[]) => void } export type UseEntityPortForwardingReturn = { // State showPortForm: boolean - ports: ForwardedPort[] isLoading: boolean error: string | null @@ -319,100 +315,55 @@ export function useEntityPortForwarding({ entityHash, executableStatus, executableManager, -}: UseEntityPortForwardingProps = {}): UseEntityPortForwardingReturn { + ports, + onPortsChange, +}: UseEntityPortForwardingProps): UseEntityPortForwardingReturn { // Get account address from app state const [appState] = useAppState() const { account } = appState.connection const accountAddress = account?.address // State - const [ports, setPorts] = useState(getSystemPorts()) const [showPortForm, setShowPortForm] = useState(false) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) - const forwardedPortsManager = useForwardedPortsManager() - - // Load existing ports for the entity - const loadPorts = useCallback(async () => { - if (!entityHash || !accountAddress || !forwardedPortsManager) { - setPorts(getSystemPorts()) - return - } - - try { - const existingPorts = - await forwardedPortsManager.getByEntityHash(entityHash) - - const aggregatePorts = existingPorts?.ports || {} - - // Merge aggregate ports with cached pending additions - const portsWithAdditions = mergePendingPortsWithAggregate( - entityHash, - accountAddress, - aggregatePorts, - ) - - // Apply pending removals - const finalPorts = applyPendingRemovals( - entityHash, - accountAddress, - portsWithAdditions, - ) - const userPorts: ForwardedPort[] = transformAPIPortsToUI(finalPorts) - - const allPorts = [...getSystemPorts(), ...userPorts] - setPorts(allPorts) - setError(null) - } catch (error) { - console.error('Failed to load ports:', error) - setError('Failed to load ports') - setPorts(getSystemPorts()) - } - }, [entityHash, accountAddress, forwardedPortsManager]) - - // Load ports when entityHash changes - useEffect(() => { - loadPorts() - }, [loadPorts]) - - // Update ports when executable status changes (mapped ports) - useEffect(() => { - if (executableStatus?.mappedPorts) { - setPorts((currentPorts) => - mergePortsWithMappings(currentPorts, executableStatus.mappedPorts), - ) - } - }, [executableStatus?.mappedPorts]) - // State management helpers - const addPortsToState = useCallback((newPorts: ForwardedPort[]) => { - setPorts((prev) => [...prev, ...newPorts]) - setShowPortForm(false) - setError(null) - }, []) + const addPortsToState = useCallback( + (newPorts: ForwardedPort[]) => { + const updatedPorts = [...ports, ...newPorts] + onPortsChange?.(updatedPorts) + setShowPortForm(false) + setError(null) + }, + [ports, onPortsChange], + ) - const removePortFromState = useCallback((portSource: string) => { - setPorts((prev) => prev.filter((port) => port.source !== portSource)) - setError(null) - }, []) + const removePortFromState = useCallback( + (portSource: string) => { + const updatedPorts = ports.filter((port) => port.source !== portSource) + onPortsChange?.(updatedPorts) + setError(null) + }, + [ports, onPortsChange], + ) const setPortRemovalState = useCallback( (portSource: string, isRemoving: boolean) => { - setPorts((prev) => - prev.map((port) => - port.source === portSource ? { ...port, isRemoving } : port, - ), + const updatedPorts = ports.map((port) => + port.source === portSource ? { ...port, isRemoving } : port, ) + onPortsChange?.(updatedPorts) }, - [], + [ports, onPortsChange], ) const updatePorts = useCallback( (updater: (currentPorts: ForwardedPort[]) => ForwardedPort[]) => { - setPorts(updater) + const updatedPorts = updater(ports) + onPortsChange?.(updatedPorts) }, - [], + [ports, onPortsChange], ) const toggleForm = useCallback((show?: boolean) => { @@ -479,7 +430,6 @@ export function useEntityPortForwarding({ return { // State showPortForm, - ports, isLoading, error, diff --git a/src/components/common/entityData/EntityPortForwarding/types.ts b/src/components/common/entityData/EntityPortForwarding/types.ts index 652f74347..34e7a5af0 100644 --- a/src/components/common/entityData/EntityPortForwarding/types.ts +++ b/src/components/common/entityData/EntityPortForwarding/types.ts @@ -27,4 +27,6 @@ export type EntityPortForwardingProps = { entityHash?: string executableStatus?: ExecutableStatus executableManager?: ExecutableManager + ports: ForwardedPort[] + onPortsChange?: (ports: ForwardedPort[]) => void } diff --git a/src/components/common/entityData/EntityPortForwarding/utils.ts b/src/components/common/entityData/EntityPortForwarding/utils.ts index d865a65ac..dfd59d989 100644 --- a/src/components/common/entityData/EntityPortForwarding/utils.ts +++ b/src/components/common/entityData/EntityPortForwarding/utils.ts @@ -374,3 +374,14 @@ export function applyPendingRemovals( return filteredPorts } + +/** + * Extracts the SSH forwarded port (port 22's destination) from the ports array + * Returns the destination port or undefined if not mapped yet + */ +export function getSSHForwardedPort( + ports: ForwardedPort[], +): string | undefined { + const sshPort = ports.find((port) => port.source === '22') + return sshPort?.destination +} diff --git a/src/components/common/entityData/EntityStatus/cmp.tsx b/src/components/common/entityData/EntityStatus/cmp.tsx index 190ccc89e..df540e64e 100644 --- a/src/components/common/entityData/EntityStatus/cmp.tsx +++ b/src/components/common/entityData/EntityStatus/cmp.tsx @@ -7,8 +7,14 @@ import { } from './types' import { RotatingLines } from 'react-loader-spinner' -const EntityStatusV2 = ({ theme, calculatedStatus }: EntityStatusPropsV2) => { +const EntityStatusV2 = ({ + theme, + calculatedStatus, + cannotStart, +}: EntityStatusPropsV2) => { const labelVariant = useMemo(() => { + if (cannotStart) return 'error' + switch (calculatedStatus) { case 'not-allocated': return 'warning' @@ -21,9 +27,11 @@ const EntityStatusV2 = ({ theme, calculatedStatus }: EntityStatusPropsV2) => { case 'preparing': return 'warning' } - }, [calculatedStatus]) + }, [calculatedStatus, cannotStart]) const text = useMemo(() => { + if (cannotStart) return 'STOPPED' + switch (calculatedStatus) { case 'not-allocated': return 'NOT ALLLOCATED' @@ -36,9 +44,11 @@ const EntityStatusV2 = ({ theme, calculatedStatus }: EntityStatusPropsV2) => { case 'preparing': return 'PREPARING' } - }, [calculatedStatus]) + }, [calculatedStatus, cannotStart]) const showSpinner = useMemo(() => { + if (cannotStart) return false + switch (calculatedStatus) { case 'not-allocated': return false @@ -51,7 +61,7 @@ const EntityStatusV2 = ({ theme, calculatedStatus }: EntityStatusPropsV2) => { case 'preparing': return true } - }, [calculatedStatus]) + }, [calculatedStatus, cannotStart]) return (
- - ) -} -CheckoutSummaryVolumeLine.displayName = 'CheckoutSummaryVolumeLine' +// return ( +// +//
+//
+// STORAGE +// +//
+//
+//
+//
{humanReadableSize(size, 'MiB')}
+//
+//
+//
+// {hasDiscount ? ( +// +//
+// {fullDiscount ? ( +// <> +// The cost displayed for the added storage is{' '} +// +// +// {' '} +// as this resource is already included in your selected +// package at no additional charge. +// +// ) : ( +// <> +// Good news! The displayed price is lower than usual due +// to a discount of{' '} +// +// +// +// {specs && ( +// <> +// {` for `} +// +// {convertByteUnits(specs.storage, { +// from: 'MiB', +// to: 'GiB', +// displayUnit: true, +// })} +// {' '} +// included in your package. +// +// )} +// +// )} +//
+//
+// } +// > +// +// +// ) : ( +// <> +// +// +// )} +//
+// +//
+// ) +// } +// CheckoutSummaryVolumeLine.displayName = 'CheckoutSummaryVolumeLine' -// ------------------------------------------ +// // ------------------------------------------ -const CheckoutSummaryWebsiteLine = ({ - website, - cost, -}: CheckoutSummaryWebsiteLineProps) => { - const [size, setSize] = useState(0) +// const CheckoutSummaryWebsiteLine = ({ +// website, +// cost, +// }: CheckoutSummaryWebsiteLineProps) => { +// const [size, setSize] = useState(0) - useEffect(() => { - async function load() { - const size = await WebsiteManager.getWebsiteSize({ - website, - } as AddWebsite) - setSize(size) - } +// useEffect(() => { +// async function load() { +// const size = await WebsiteManager.getWebsiteSize({ +// website, +// } as AddWebsite) +// setSize(size) +// } - load() - }, [website]) +// load() +// }, [website]) - if (!cost) return <> +// if (!cost) return <> - return ( - -
-
WEBSITE
-
-
-
{humanReadableSize(size, 'MiB')}
-
-
- -
-
- ) -} -CheckoutSummaryWebsiteLine.displayName = 'CheckoutSummaryWebsiteLine' +// return ( +// +//
+//
WEBSITE
+//
+//
+//
{humanReadableSize(size, 'MiB')}
+//
+//
+// +//
+//
+// ) +// } +// CheckoutSummaryWebsiteLine.displayName = 'CheckoutSummaryWebsiteLine' // ------------------------------------------ @@ -168,49 +154,25 @@ CheckoutSummaryWebsiteLine.displayName = 'CheckoutSummaryWebsiteLine' export const CheckoutSummary = ({ address, cost, - paymentMethod, unlockedAmount, description, button: buttonNode, footerButton = buttonNode, - control, - receiverAddress, mainRef, - disablePaymentMethod = true, - disabledStreamTooltip, - onSwitchPaymentMethod, }: CheckoutSummaryProps) => { const { blockchain } = useConnection({ triggerOnMount: false, }) const nftVoucherBalance = useNFTVoucherBalance() - const disabledHold = - disablePaymentMethod && paymentMethod !== PaymentMethod.Hold - const disabledStream = - disablePaymentMethod && paymentMethod !== PaymentMethod.Stream - - const paymentMethodSwitchNode = control && ( - - ) - return ( <> -
)} - {control && ( - <> -
-
- - Payment Method - -
{paymentMethodSwitchNode}
-
-
- - )} -
-
UNLOCKED
+
AVAILABLE CREDITS
CURRENT WALLET {ellipseAddress(address)}
-
- -
+
{humanReadableCurrency(unlockedAmount)}
{nftVoucherBalance > 0 && ( )} - {cost?.lines?.map((line) => ( + {cost?.cost?.lines?.map((line) => (
{line.name} @@ -274,15 +221,14 @@ export const CheckoutSummary = ({ {/*
{line.detail}
*/}
{line.detail}
- + {line.cost !== 0 ? ( ) : ( '-' @@ -324,42 +270,36 @@ export const CheckoutSummary = ({ })} */}
-
- {paymentMethod === PaymentMethod.Hold - ? 'Total' - : 'Total / h'} +
Total credits / h
+
+ + +
+ + +
+
Min. required
- +
- {paymentMethod === PaymentMethod.Stream && ( - -
-
Min. required
-
- - - -
-
- )}
- {paymentMethod === PaymentMethod.Stream && receiverAddress && ( - - )} - {buttonNode &&
{buttonNode}
}
diff --git a/src/components/form/CheckoutSummary/types.ts b/src/components/form/CheckoutSummary/types.ts index 37513f489..d2ae6e7bd 100644 --- a/src/components/form/CheckoutSummary/types.ts +++ b/src/components/form/CheckoutSummary/types.ts @@ -1,13 +1,10 @@ -import { EntityType, PaymentMethod } from '@/helpers/constants' +import { EntityType } from '@/helpers/constants' import { UseEntityCostReturn } from '@/hooks/common/useEntityCost' import { DomainField } from '@/hooks/form/useAddDomains' import { VolumeField } from '@/hooks/form/useAddVolume' import { WebsiteFolderField } from '@/hooks/form/useAddWebsiteFolder' import { InstanceSpecsField } from '@/hooks/form/useSelectInstanceSpecs' -import { - StreamDurationField, - StreamDurationUnit, -} from '@/hooks/form/useSelectStreamDuration' +import { StreamDurationUnit } from '@/hooks/form/useSelectStreamDuration' import { ReactNode, RefObject } from 'react' import { Control } from 'react-hook-form' @@ -21,17 +18,11 @@ export type CheckoutSummaryProps = { // volume?: VolumeField // volumes?: VolumeField[] // domains?: DomainField[] - paymentMethod: PaymentMethod button?: ReactNode footerButton?: ReactNode description?: ReactNode mainRef?: RefObject control?: Control - receiverAddress?: string - streamDuration?: StreamDurationField - disablePaymentMethod?: boolean - disabledStreamTooltip?: ReactNode - onSwitchPaymentMethod?: (e: PaymentMethod) => void } export type CheckoutSummarySpecsLineProps = { diff --git a/src/components/form/CheckoutSummaryFooter/cmp.tsx b/src/components/form/CheckoutSummaryFooter/cmp.tsx index 36b8c4789..4aae30e31 100644 --- a/src/components/form/CheckoutSummaryFooter/cmp.tsx +++ b/src/components/form/CheckoutSummaryFooter/cmp.tsx @@ -2,18 +2,16 @@ import React, { cloneElement, isValidElement, memo } from 'react' import { Button, ButtonProps } from '@aleph-front/core' import { StyledSeparator } from './styles' import { CheckoutSummaryFooterProps } from './types' -import { PaymentMethod } from '@/helpers/constants' -import Price from '@/components/common/Price' import FloatingFooter from '../FloatingFooter' +import Price from '@/components/common/Price' // ------------------------------------------ export const CheckoutSummaryFooter = ({ submitButton: submitButtonNode, - paymentMethodSwitch: paymentMethodSwitchNode, - paymentMethod, mainRef: containerRef, totalCost, + loading = false, shouldHide = true, thresholdOffset = 600, ...rest @@ -35,22 +33,24 @@ export const CheckoutSummaryFooter = ({ ...rest, }} > -
-
{paymentMethodSwitchNode}
+
-
- - {paymentMethod === PaymentMethod.Stream - ? 'Total per hour' - : 'Total hold'} - - +
+
+ + + + + Credits / h + +
+ {/* TODO: Uncomment and implement when credits top up is available */} + {/*
+ $0.30/h +
*/}
{footerSubmitButtonNode} diff --git a/src/components/form/CheckoutSummaryFooter/types.ts b/src/components/form/CheckoutSummaryFooter/types.ts index 16b5dbcb4..6d9d49913 100644 --- a/src/components/form/CheckoutSummaryFooter/types.ts +++ b/src/components/form/CheckoutSummaryFooter/types.ts @@ -1,4 +1,3 @@ -import { PaymentMethod } from '@/helpers/constants' import { ReactNode, RefObject } from 'react' import { FloatingFooterProps } from '../FloatingFooter/cmp' @@ -6,9 +5,8 @@ export type CheckoutSummaryFooterProps = Pick< FloatingFooterProps, 'shouldHide' | 'thresholdOffset' | 'deps' > & { - paymentMethod: PaymentMethod submitButton?: ReactNode - paymentMethodSwitch?: ReactNode mainRef?: RefObject totalCost?: number + loading?: boolean } diff --git a/src/components/form/SelectInstanceSpecs/cmp.tsx b/src/components/form/SelectInstanceSpecs/cmp.tsx index 941a5f563..bb7f45040 100644 --- a/src/components/form/SelectInstanceSpecs/cmp.tsx +++ b/src/components/form/SelectInstanceSpecs/cmp.tsx @@ -12,7 +12,7 @@ import { import { useCallback, useMemo } from 'react' import { convertByteUnits } from '@/helpers/utils' import { SelectInstanceSpecsProps, SpecsDetail } from './types' -import { EntityType, PaymentMethod } from '@/helpers/constants' +import { EntityType } from '@/helpers/constants' import Price from '@/components/common/Price' import Table from '@/components/common/Table' import { PriceType } from '@/domain/cost' @@ -21,7 +21,7 @@ import { useGpuPricingType } from '@/hooks/common/useGpuPricingType' import InfoTooltipButton from '@/components/common/InfoTooltipButton' export const SelectInstanceSpecs = memo((props: SelectInstanceSpecsProps) => { - const { specsCtrl, options, type, isPersistent, paymentMethod } = + const { specsCtrl, options, type, isPersistent } = useSelectInstanceSpecs(props) const columns = useMemo(() => { @@ -46,10 +46,7 @@ export const SelectInstanceSpecs = memo((props: SelectInstanceSpecsProps) => { sortBy: (row: SpecsDetail) => row.price, render: (row: SpecsDetail) => ( - + ), }, @@ -113,7 +110,7 @@ export const SelectInstanceSpecs = memo((props: SelectInstanceSpecsProps) => { } return cols - }, [paymentMethod, type]) + }, [type]) // ------------------------------------------ @@ -157,10 +154,7 @@ export const SelectInstanceSpecs = memo((props: SelectInstanceSpecsProps) => { const pricesAggregate = await costManager.getPricesAggregate() const prices = pricesAggregate[priceType] - const computeUnitPrice = - prices.price.computeUnit[ - paymentMethod === PaymentMethod.Hold ? 'holding' : 'payg' - ] + const computeUnitPrice = prices.price.computeUnit['credit'] const computeTotalCost = specs.cpu * Number(computeUnitPrice) @@ -174,7 +168,7 @@ export const SelectInstanceSpecs = memo((props: SelectInstanceSpecsProps) => { } load() - }, [costManager, options, paymentMethod, priceType]) + }, [costManager, options, priceType]) const data = useMemo(() => { return options.map((specs, i) => { diff --git a/src/components/form/SelectInstanceSpecs/types.ts b/src/components/form/SelectInstanceSpecs/types.ts index e94ee7dd5..eae4c4b92 100644 --- a/src/components/form/SelectInstanceSpecs/types.ts +++ b/src/components/form/SelectInstanceSpecs/types.ts @@ -1,5 +1,5 @@ import { Control } from 'react-hook-form' -import { EntityType, PaymentMethod } from '@/helpers/constants' +import { EntityType } from '@/helpers/constants' import { InstanceSpecsField } from '@/hooks/form/useSelectInstanceSpecs' import { CRNSpecs } from '@/domain/node' import { ReactNode } from 'react' @@ -11,7 +11,6 @@ export type SelectInstanceSpecsProps = { type: EntityType.Instance | EntityType.GpuInstance | EntityType.Program gpuModel?: string isPersistent?: boolean - paymentMethod?: PaymentMethod nodeSpecs?: CRNSpecs children?: ReactNode } diff --git a/src/components/pages/account/ComputeResourceNodeDetailPage/tabs/OverviewTabContent/cmp.tsx b/src/components/pages/account/ComputeResourceNodeDetailPage/tabs/OverviewTabContent/cmp.tsx index ab8b2742c..828dd8e23 100644 --- a/src/components/pages/account/ComputeResourceNodeDetailPage/tabs/OverviewTabContent/cmp.tsx +++ b/src/components/pages/account/ComputeResourceNodeDetailPage/tabs/OverviewTabContent/cmp.tsx @@ -12,7 +12,7 @@ import NodeDetailStatus from '@/components/common/NodeDetailStatus' import NodeDecentralization from '@/components/common/NodeDecentralization' import NodeDetailEditableField from '@/components/common/NodeDetailEditableField' import NodeDetailLink from '@/components/common/NodeDetailLink' -import { apiServer } from '@/helpers/constants' +import { useSettings } from '@/hooks/common/useSettings' import Image from 'next/image' import Price from '@/components/common/Price' import ButtonLink from '@/components/common/ButtonLink' @@ -71,6 +71,7 @@ export const OverviewTabContent = ({ handleLink, handleUnlink, }: OverviewTabContentProps) => { + const { apiServer } = useSettings() const [state] = useAppState() const { blockchain } = state.connection diff --git a/src/components/pages/account/ComputeResourceNodeDetailPage/tabs/PoliciesTabContent/hook.ts b/src/components/pages/account/ComputeResourceNodeDetailPage/tabs/PoliciesTabContent/hook.ts index 34c5a1790..5a08796e9 100644 --- a/src/components/pages/account/ComputeResourceNodeDetailPage/tabs/PoliciesTabContent/hook.ts +++ b/src/components/pages/account/ComputeResourceNodeDetailPage/tabs/PoliciesTabContent/hook.ts @@ -1,9 +1,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { UseComputeResourceNodeDetailPageReturn } from '@/components/pages/account/ComputeResourceNodeDetailPage/hook' -import { apiServer } from '@/helpers/constants' import { MessageManager } from '@/domain/message' import { MessageType } from '@aleph-sdk/message' import { FileManager } from '@/domain/file' +import { useSettings } from '@/hooks/common/useSettings' export type usePoliciesTabContentProps = Pick< UseComputeResourceNodeDetailPageReturn, @@ -39,6 +39,8 @@ export function usePoliciesTabContent({ handleRemovePolicies, ...props }: usePoliciesTabContentProps): usePoliciesTabContentReturn { + const { apiServer } = useSettings() + const { field: { onChange, value }, } = termsAndConditionsCtrl @@ -88,7 +90,7 @@ export function usePoliciesTabContent({ : currentPolicies ? `${apiServer}/api/v0/storage/raw/${currentPolicies?.cid}` : ' ', - [isLoadingHistoryMessages, currentPolicies], + [isLoadingHistoryMessages, currentPolicies, apiServer], ) const removePoliciesDisabled = useMemo( diff --git a/src/components/pages/account/ComputeResourceNodesPage/cmp.tsx b/src/components/pages/account/ComputeResourceNodesPage/cmp.tsx index d82e02526..bf7489a90 100644 --- a/src/components/pages/account/ComputeResourceNodesPage/cmp.tsx +++ b/src/components/pages/account/ComputeResourceNodesPage/cmp.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react' +import { memo, useState } from 'react' import Head from 'next/head' import Link from 'next/link' import { @@ -72,6 +72,8 @@ export const ComputeResourceNodesPage = ( ) + const [dashboardOpen, setDashboardOpen] = useState(true) + return ( <> @@ -84,7 +86,11 @@ export const ComputeResourceNodesPage = ( Compute nodes - +
diff --git a/src/components/pages/account/CoreChannelNodeDetailPage/cmp.tsx b/src/components/pages/account/CoreChannelNodeDetailPage/cmp.tsx index 47e9065ac..418534511 100644 --- a/src/components/pages/account/CoreChannelNodeDetailPage/cmp.tsx +++ b/src/components/pages/account/CoreChannelNodeDetailPage/cmp.tsx @@ -23,7 +23,7 @@ import NodeDetailLockSwitch from '@/components/common/NodeDetailLockSwitch' import NodeDetailStatus from '@/components/common/NodeDetailStatus' import NodeDetailEditableField from '@/components/common/NodeDetailEditableField' import NodeDetailLink from '@/components/common/NodeDetailLink' -import { apiServer } from '@/helpers/constants' +import { useSettings } from '@/hooks/common/useSettings' import Image from 'next/image' import Price from '@/components/common/Price' import { NodeManager } from '@/domain/node' @@ -57,6 +57,7 @@ export const CoreChannelNodeDetailPage = () => { handleSubmit, } = useCoreChannelNodeDetailPage() + const { apiServer } = useSettings() const [state] = useAppState() const { blockchain } = state.connection diff --git a/src/components/pages/account/CoreChannelNodesPage/cmp.tsx b/src/components/pages/account/CoreChannelNodesPage/cmp.tsx index 54892d87e..52840779d 100644 --- a/src/components/pages/account/CoreChannelNodesPage/cmp.tsx +++ b/src/components/pages/account/CoreChannelNodesPage/cmp.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react' +import { memo, useState } from 'react' import Head from 'next/head' import { Button, @@ -64,6 +64,7 @@ export const CoreChannelNodesPage = (props: UseCoreChannelNodesPageProps) => { ) + const [dashboardOpen, setDashboardOpen] = useState(true) return ( <> @@ -77,7 +78,11 @@ export const CoreChannelNodesPage = (props: UseCoreChannelNodesPageProps) => { Core nodes - +
diff --git a/src/components/pages/account/StakingPage/cmp.tsx b/src/components/pages/account/StakingPage/cmp.tsx index 625fd8aaf..0d0f1c144 100644 --- a/src/components/pages/account/StakingPage/cmp.tsx +++ b/src/components/pages/account/StakingPage/cmp.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react' +import { memo, useState } from 'react' import Head from 'next/head' import { Checkbox, @@ -41,6 +41,7 @@ export const StakingPage = (props: UseStakingPageProps) => { } = useStakingPage(props) const { render } = useLazyRender() + const [dashboardOpen, setDashboardOpen] = useState(true) return ( <> @@ -57,7 +58,7 @@ export const StakingPage = (props: UseStakingPageProps) => { Staking - +

diff --git a/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx new file mode 100644 index 000000000..665cb4c57 --- /dev/null +++ b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx @@ -0,0 +1,205 @@ +import React from 'react' +import { Button, Icon, NoisyContainer, ObjectImg } from '@aleph-front/core' +import { SectionTitle } from '@/components/common/CompositeTitle' + +import ToggleDashboard from '@/components/common/ToggleDashboard' +import Skeleton from '@/components/common/Skeleton' +import { useCreditsDashboard } from './hook' + +export default function CreditsDashboard() { + const { + runRateDays, + creditsDashboardOpen, + setCreditsDashboardOpen, + isConnected, + accountCreditBalance, + isCalculatingCosts, + } = useCreditsDashboard() + + // const data = [ + // { + // id: 1, + // status: 'finished', + // date: 1756121519678, + // amount: 100, + // asset: 'USDC', + // credits: 120, + // }, + // { + // id: 2, + // status: 'ongoing', + // date: 1756121546481, + // amount: 80, + // asset: 'USDC', + // credits: 95, + // }, + // ] + + return ( +
+ Credits + + Open credits + + ), + disabled: !isConnected, + }} + > +
+ +
+
+ + +
+
+

AVAILABLE

+ {accountCreditBalance !== undefined ? ( +

{accountCreditBalance}

+ ) : ( + + )} +
+
+

RUN-RATE

+ {isCalculatingCosts ? ( + + ) : ( +
+

{runRateDays || '∞'}

+

+ {runRateDays === 1 ? 'DAY' : 'DAYS'} +

+
+ )} +
+
+
+
+ +
+
+
+ + {/*
+
+ + Purchases + + + +
+
+ row.id} + data={data} + columns={[ + { + label: 'STATUS', + align: 'left', + sortable: true, + render: (row) => { + let color = 'warning' + + switch (row.status) { + case 'finished': + color = 'success' + break + case 'ongoing': + color = 'warning' + break + case 'failed': + color = 'error' + break + } + + return ( +
+ + + +
+ ) + }, + }, + { + label: 'DATE', + align: 'left', + sortable: true, + render: (row) => row.date, + }, + // { + // label: 'AMOUNT', + // align: 'left', + // sortable: true, + // render: (row) => row.amount, + // }, + // { + // label: 'ASSET', + // align: 'left', + // sortable: true, + // render: (row) => row.asset, + // }, + { + label: 'CREDITS', + align: 'left', + sortable: true, + render: (row) => `~${row.credits}`, + }, + { + label: '', + width: '100%', + align: 'right', + render: (row) => { + return ( + + ) + }, + cellProps: () => ({ + css: tw`pl-3!`, + }), + }, + ]} + /> +
+
*/} +
+
+
+ ) +} diff --git a/src/components/pages/console/DashboardPage/CreditsDashboard/hook.ts b/src/components/pages/console/DashboardPage/CreditsDashboard/hook.ts new file mode 100644 index 000000000..f3f1c3f92 --- /dev/null +++ b/src/components/pages/console/DashboardPage/CreditsDashboard/hook.ts @@ -0,0 +1,200 @@ +import { + useEffect, + useState, + useMemo, + Dispatch, + SetStateAction, + useCallback, +} from 'react' +import { useAccountEntities } from '@/hooks/common/useAccountEntities' +import { useInstanceManager } from '@/hooks/common/useManager/useInstanceManager' +import { useGpuInstanceManager } from '@/hooks/common/useManager/useGpuInstanceManager' +import { useConfidentialManager } from '@/hooks/common/useManager/useConfidentialManager' +import { PaymentMethod } from '@/helpers/constants' +import { useRequestExecutableStatus } from '@/hooks/common/useRequestEntity/useRequestExecutableStatus' +import { PaymentType } from '@aleph-sdk/message' +import { useConnection } from '@/hooks/common/useConnection' + +export type UseCreditsDashboardReturn = { + totalCostPerHour: number + runRateDays: number + creditsDashboardOpen: boolean + setCreditsDashboardOpen: Dispatch> + isConnected: boolean + accountCreditBalance?: number + isCalculatingCosts: boolean +} + +export function useCreditsDashboard(): UseCreditsDashboardReturn { + const [totalCostPerHour, setTotalCostPerHour] = useState(0) + const [creditsDashboardOpen, setCreditsDashboardOpen] = useState(false) + const [isCalculatingCosts, setIsCalculatingCosts] = useState(true) + + const { account, creditBalance: accountCreditBalance } = useConnection({ + triggerOnMount: false, + }) + + const isConnected = useMemo(() => !!account, [account]) + + // Get all entities + const { instances, gpuInstances, confidentials } = useAccountEntities() + + const creditInstances = useMemo( + () => + instances.filter( + (instance) => instance.payment?.type === PaymentType.credit, + ), + [instances], + ) + const creditGpuInstances = useMemo( + () => + gpuInstances.filter( + (gpuInstance) => gpuInstance.payment?.type === PaymentType.credit, + ), + [gpuInstances], + ) + const creditConfidentials = useMemo( + () => + confidentials.filter( + (confidential) => confidential.payment?.type === PaymentType.credit, + ), + [confidentials], + ) + + // Get managers + const instanceManager = useInstanceManager() + const gpuInstanceManager = useGpuInstanceManager() + const confidentialManager = useConfidentialManager() + + // Get status for running entities + const { status: creditInstancesStatus } = useRequestExecutableStatus({ + entities: creditInstances, + }) + const { status: creditGpuInstancesStatus } = useRequestExecutableStatus({ + entities: creditGpuInstances, + }) + const { status: creditConfidentialsStatus } = useRequestExecutableStatus({ + entities: creditConfidentials, + managerHook: useConfidentialManager, + }) + + // Helper function to check if entity is running + const isRunning = useCallback((entityId: string, status?: any) => { + const statusData = status?.data + + return ( + statusData && + (statusData.vm_ipv6 || statusData.ipv6Parsed || statusData.ipv6) + ) + }, []) + + // Helper function to calculate cost for a computing entity + const calculateComputingEntityCost = useCallback( + async (entityId: string, entityStatus: any, manager: any) => { + try { + if (isRunning(entityId, entityStatus) && manager) { + const cost = await manager.getTotalCostByHash( + PaymentMethod.Credit, + entityId, + ) + + return cost + } else { + return 0 + } + } catch (err) { + console.error('Error calculating entity cost:', err) + return 0 + } + }, + [isRunning], + ) + + // Calculate total cost per hour + useEffect(() => { + async function calculateTotalCost() { + setIsCalculatingCosts(true) + + try { + let total = 0 + + // Calculate costs for regular credit instances + for (const instance of creditInstances) { + total += await calculateComputingEntityCost( + instance.id, + creditInstancesStatus[instance.id], + instanceManager, + ) + } + + // Calculate costs for credit GPU instances + for (const gpuInstance of creditGpuInstances) { + total += await calculateComputingEntityCost( + gpuInstance.id, + creditGpuInstancesStatus[gpuInstance.id], + gpuInstanceManager, + ) + } + + // Calculate costs for credit confidential instances + for (const confidential of creditConfidentials) { + total += await calculateComputingEntityCost( + confidential.id, + creditConfidentialsStatus[confidential.id], + confidentialManager, + ) + } + + setTotalCostPerHour(total) + } catch (err) { + console.error('Error calculating total cost:', err) + } finally { + setIsCalculatingCosts(false) + } + } + + calculateTotalCost() + }, [ + calculateComputingEntityCost, + confidentialManager, + creditConfidentials, + creditConfidentialsStatus, + creditGpuInstances, + creditGpuInstancesStatus, + creditInstances, + creditInstancesStatus, + gpuInstanceManager, + instanceManager, + ]) + + // Calculate run rate days + const runRateDays = useMemo(() => { + if ( + !accountCreditBalance || + accountCreditBalance <= 0 || + totalCostPerHour <= 0 + ) { + return 0 + } + + const totalCostPerDay = totalCostPerHour * 24 + return Math.floor(accountCreditBalance / totalCostPerDay) + }, [accountCreditBalance, totalCostPerHour]) + + // Handle dashboard open/close based on connection + useEffect(() => { + if (!isConnected && creditsDashboardOpen) { + setCreditsDashboardOpen(false) + } + }, [isConnected, creditsDashboardOpen]) + + return { + totalCostPerHour, + runRateDays, + creditsDashboardOpen, + setCreditsDashboardOpen, + isConnected, + accountCreditBalance, + isCalculatingCosts, + } +} diff --git a/src/components/pages/console/DashboardPage/CreditsDashboard/index.ts b/src/components/pages/console/DashboardPage/CreditsDashboard/index.ts new file mode 100644 index 000000000..7a9f83f3c --- /dev/null +++ b/src/components/pages/console/DashboardPage/CreditsDashboard/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/pages/console/DashboardPage/cmp.tsx b/src/components/pages/console/DashboardPage/cmp.tsx index 8186fffb3..345deb2bb 100644 --- a/src/components/pages/console/DashboardPage/cmp.tsx +++ b/src/components/pages/console/DashboardPage/cmp.tsx @@ -17,6 +17,8 @@ import { EntityTypeObject, NAVIGATION_URLS, } from '@/helpers/constants' +import CreditsDashboard from './CreditsDashboard' +import { ExternalLink } from '@/components/common/ExternalLink/cmp' export default function DashboardPage() { const { @@ -87,180 +89,248 @@ export default function DashboardPage() { /> -
- - Web3 Hosting - - - - Experience the future of web hosting with our Web3 solutions. - Whether you're building static sites or dynamic web apps with - Next.js, React, or Vue.js, our platform offers seamless deployment - and robust support. Connect your custom domains and leverage the - power of decentralized technology. - - - - - - -
-
- - Computing - - - - Unleash the full potential of your applications with our advanced - computing solutions. From serverless functions that run on-demand to - fully managed instances and confidential VMs, our platform provides - the flexibility and power you need. Secure, scalable, and easy to - manage. - - - - - - - - - - - - - - - -
-
- - Storage - - - - Ensure your data is safe, secure, and always available with our - cutting-edge storage solutions. Create immutable volumes for - consistent and reliable data storage, perfect for dependency volumes - and other critical data. Harness the power of decentralized storage - with ease. - - - - - - -
+ +
+
+
+ + Computing + + + + Unleash the full potential of your applications with our + advanced computing solutions. From serverless functions that run + on-demand to fully managed instances and confidential VMs, our + platform provides the flexibility and power you need. Secure, + scalable, and easy to manage. + + + + + To create a Function, navigate to the{' '} + +

+ } + information={{ + type: 'amount', + data: programAggregatedStatus.total, + }} + subItems={programsCardItems} + /> +
+ + + + + + + + + To create a confidential instance, navigate to the{' '} + +

+ } + description="Protect your sensitive workloads with our Confidential VMs. Designed for maximum privacy and security, ensuring your data stays safe." + introductionButtonText="Create your confidential" + information={{ + type: 'computing', + data: confidentialsAggregatedStatus.total, + }} + /> +
+
+
+
+
+
+ + Web3 Hosting + + + + Experience the future of web hosting with our Web3 solutions. + Whether you're building static sites or dynamic web apps + with Next.js, React, or Vue.js, our platform offers seamless + deployment and robust support. Connect your custom domains and + leverage the power of decentralized technology. + + + + + To deploy a Website, navigate to the{' '} + +

+ } + information={{ + type: 'amount', + data: websitesAggregatedStatus.total, + }} + /> +
+
+
+
+ + Storage + + + + Ensure your data is safe, secure, and always available with our + cutting-edge storage solutions. Create immutable volumes for + consistent and reliable data storage, perfect for dependency + volumes and other critical data. Harness the power of + decentralized storage with ease. + + + + + To create a new volume, navigate to the{' '} + +

+ } + description="Secure and reliable immutable volumes for your data storage needs. Ideal for dependency volumes and critical data, ensuring consistency and integrity." + introductionButtonText="Create your volume" + information={{ + type: 'storage', + data: volumesAggregatedStatus.total, + }} + subItems={volumesCardItems} + /> +
+
+
+
+
diff --git a/src/components/pages/console/confidential/ConfidentialsTabContent/cmp.tsx b/src/components/pages/console/confidential/ConfidentialsTabContent/cmp.tsx index a759bb836..d3866d413 100644 --- a/src/components/pages/console/confidential/ConfidentialsTabContent/cmp.tsx +++ b/src/components/pages/console/confidential/ConfidentialsTabContent/cmp.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react' +import React, { memo, useCallback } from 'react' import tw from 'twin.macro' import { ConfidentialsTabContentProps } from './types' import ButtonLink from '@/components/common/ButtonLink' @@ -9,6 +9,10 @@ import { } from '@/helpers/utils' import EntityTable from '@/components/common/EntityTable' import { Icon } from '@aleph-front/core' +import { Confidential } from '@/domain/confidential' +import { PaymentType } from '@aleph-sdk/message' +import ExternalLink from '@/components/common/ExternalLink' +import { NAVIGATION_URLS } from '@/helpers/constants' const CreateConfidentialButton = ({ children, @@ -27,6 +31,10 @@ CreateConfidentialButton.displayName = 'CreateConfidentialButton' export const ConfidentialsTabContent = memo( ({ data }: ConfidentialsTabContentProps) => { + const isCredit = useCallback((row: Confidential) => { + return row.payment?.type === PaymentType.credit + }, []) + return ( <> {data.length > 0 ? ( @@ -36,6 +44,9 @@ export const ConfidentialsTabContent = memo( borderType="none" rowNoise rowKey={(row) => row.id} + rowProps={(row) => ({ + css: isCredit(row) ? '' : tw`opacity-40`, + })} data={data} columns={[ { @@ -77,15 +88,39 @@ export const ConfidentialsTabContent = memo( { label: '', align: 'right', - render: (row) => ( - - - - ), + render: (row) => { + const disabled = !isCredit(row) + + return ( + + To manage this confidential instance, go to the{' '} + +

+ ) + } + tooltipPosition={{ + my: 'bottom-right', + at: 'bottom-center', + }} + > + +
+ ) + }, cellProps: () => ({ css: tw`pl-3!`, }), diff --git a/src/components/pages/console/confidential/ManageConfidential/cmp.tsx b/src/components/pages/console/confidential/ManageConfidential/cmp.tsx index 417444c03..ea92d42cc 100644 --- a/src/components/pages/console/confidential/ManageConfidential/cmp.tsx +++ b/src/components/pages/console/confidential/ManageConfidential/cmp.tsx @@ -83,6 +83,11 @@ export default function ManageConfidential() { // Navigation handlers handleBack, + + // Ports + ports, + sshForwardedPort, + handlePortsChange, } = useManageGpuInstance() return ( @@ -162,6 +167,7 @@ export default function ManageConfidential() { , immutableVolumes.length && ( , ]} /> diff --git a/src/components/pages/console/confidential/ManageConfidential/hook.ts b/src/components/pages/console/confidential/ManageConfidential/hook.ts index eea4b76d6..75e0def76 100644 --- a/src/components/pages/console/confidential/ManageConfidential/hook.ts +++ b/src/components/pages/console/confidential/ManageConfidential/hook.ts @@ -1,4 +1,5 @@ import { useRouter } from 'next/router' +import { useMemo, useState } from 'react' import { Confidential, ConfidentialManager } from '@/domain/confidential' import { useConfidentialManager } from '@/hooks/common/useManager/useConfidentialManager' import { useRequestConfidentials } from '@/hooks/common/useRequestEntity/useRequestConfidentials' @@ -6,10 +7,16 @@ import { useManageInstanceEntity, UseManageInstanceEntityReturn, } from '@/hooks/common/useEntity/useManageInstanceEntity' +import { useForwardedPorts } from '@/hooks/common/useForwardedPorts' +import { getSSHForwardedPort } from '@/components/common/entityData/EntityPortForwarding/utils' +import { ForwardedPort } from '@/components/common/entityData/EntityPortForwarding/types' export type UseManageGpuInstanceReturn = UseManageInstanceEntityReturn & { confidentialInstance?: Confidential confidentialInstanceManager?: ConfidentialManager + ports: ForwardedPort[] + sshForwardedPort?: string + handlePortsChange: (ports: ForwardedPort[]) => void } export function useManageGpuInstance(): UseManageGpuInstanceReturn { @@ -29,9 +36,38 @@ export function useManageGpuInstance(): UseManageGpuInstanceReturn { entityManager: confidentialInstanceManager, }) + const { status } = manageInstanceEntityProps + + // Fetch forwarded ports + const { ports: fetchedPorts } = useForwardedPorts({ + entityHash: confidentialInstance?.id, + executableStatus: status, + }) + + // Local state for ports to allow updates + const [ports, setPorts] = useState(fetchedPorts) + + // Update local ports state when fetched ports change + useMemo(() => { + setPorts(fetchedPorts) + }, [fetchedPorts]) + + // Extract SSH forwarded port + const sshForwardedPort = useMemo(() => { + return getSSHForwardedPort(ports) + }, [ports]) + + // Handler to update ports + const handlePortsChange = (updatedPorts: ForwardedPort[]) => { + setPorts(updatedPorts) + } + return { confidentialInstance, confidentialInstanceManager, + ports, + sshForwardedPort, + handlePortsChange, ...manageInstanceEntityProps, } } diff --git a/src/components/pages/console/confidential/NewConfidentialPage/cmp.tsx b/src/components/pages/console/confidential/NewConfidentialPage/cmp.tsx index 27d531203..ccb3a85cc 100644 --- a/src/components/pages/console/confidential/NewConfidentialPage/cmp.tsx +++ b/src/components/pages/console/confidential/NewConfidentialPage/cmp.tsx @@ -1,7 +1,7 @@ import Head from 'next/head' import BackButtonSection from '@/components/common/BackButtonSection' import { CenteredContainer } from '@/components/common/CenteredContainer' -import { SectionTitle } from '@/components/common/CompositeTitle' +import { CompositeSectionTitle } from '@/components/common/CompositeTitle' import { BulletItem, BulletList, @@ -97,7 +97,11 @@ export default function NewConfidentialPage() {
Requirements} + toggleTitle={ + + Requirements + + } >
@@ -153,9 +157,9 @@ export default function NewConfidentialPage() { + Create encrypted disk image - + } >
@@ -294,9 +298,9 @@ export default function NewConfidentialPage() { + Upload encrypted disk image - + } >
@@ -369,9 +373,9 @@ export default function NewConfidentialPage() { + Create Confidential Instance - + } >
diff --git a/src/components/pages/console/domain/NewDomainPage/cmp.tsx b/src/components/pages/console/domain/NewDomainPage/cmp.tsx index 0b576e61d..19c01691f 100644 --- a/src/components/pages/console/domain/NewDomainPage/cmp.tsx +++ b/src/components/pages/console/domain/NewDomainPage/cmp.tsx @@ -20,7 +20,7 @@ import { NAVIGATION_URLS, } from '@/helpers/constants' import { useNewDomainPage } from './hook' -import { SectionTitle } from '@/components/common/CompositeTitle' +import { CompositeSectionTitle } from '@/components/common/CompositeTitle' import BackButtonSection from '@/components/common/BackButtonSection' export default function NewDomain() { @@ -67,7 +67,9 @@ export default function NewDomain() {
- Custom domain + + Custom domain +

Assign a user-friendly domain to your website, instance or function to not only simplify access to your web3 application but @@ -93,7 +95,9 @@ export default function NewDomain() {

- Select Resource + + Select Resource +

You'll need to specify the resource your custom domain will be associated with. This could either be a website, an instance or diff --git a/src/components/pages/console/function/FunctionDashboardPage/cmp.tsx b/src/components/pages/console/function/FunctionDashboardPage/cmp.tsx index d852097e7..c354e39ed 100644 --- a/src/components/pages/console/function/FunctionDashboardPage/cmp.tsx +++ b/src/components/pages/console/function/FunctionDashboardPage/cmp.tsx @@ -40,10 +40,14 @@ function FunctionDashboardPage() { info="WHAT ARE..." title="Functions" description="Deploy and manage serverless functions effortlessly with our robust computing platform. Run code on-demand or persistently, with seamless integration and scalability." - withButton={programs?.length === 0} - buttonUrl="/console/computing/function/new" - buttonText="Create function" - externalLinkUrl={NAVIGATION_URLS.docs.functions} + // withButton={programs?.length === 0} + // buttonUrl="/console/computing/function/new" + // buttonText="Create function" + // externalLinkUrl={NAVIGATION_URLS.docs.functions} + externalLinkText="Create on Legacy console" + externalLinkUrl={ + NAVIGATION_URLS.legacyConsole.computing.functions.home + } /> ) : tabId === 'volume' ? ( diff --git a/src/components/pages/console/function/FunctionsTabContent/cmp.tsx b/src/components/pages/console/function/FunctionsTabContent/cmp.tsx index fd23688ca..26213cb80 100644 --- a/src/components/pages/console/function/FunctionsTabContent/cmp.tsx +++ b/src/components/pages/console/function/FunctionsTabContent/cmp.tsx @@ -6,6 +6,7 @@ import { convertByteUnits, ellipseAddress } from '@/helpers/utils' import EntityTable from '@/components/common/EntityTable' import { Icon } from '@aleph-front/core' import { NAVIGATION_URLS } from '@/helpers/constants' +import ExternalLink from '@/components/common/ExternalLink' export const FunctionsTabContent = React.memo( ({ data }: FunctionsTabContentProps) => { @@ -19,8 +20,10 @@ export const FunctionsTabContent = React.memo( rowNoise rowKey={(row) => row.id} data={data} + // eslint-disable-next-line @typescript-eslint/no-unused-vars rowProps={(row) => ({ - css: row.confirmed ? '' : tw`opacity-60`, + // css: row.confirmed ? '' : tw`opacity-60`, + css: tw`opacity-40`, })} columns={[ { @@ -77,6 +80,24 @@ export const FunctionsTabContent = React.memo( kind="functional" variant="secondary" href={`${NAVIGATION_URLS.console.computing.functions.home}/${row.id}`} + disabled={true} + disabledMessage={ +

+ To manage this function, go to the{' '} + +

+ } + tooltipPosition={{ + my: 'bottom-right', + at: 'bottom-center', + }} > @@ -88,14 +109,14 @@ export const FunctionsTabContent = React.memo( ]} />
-
+ {/*
Create function -
+
*/} ) : (
diff --git a/src/components/pages/console/function/ManageFunction/hook.ts b/src/components/pages/console/function/ManageFunction/hook.ts index e4e33d4d7..ce5674a4b 100644 --- a/src/components/pages/console/function/ManageFunction/hook.ts +++ b/src/components/pages/console/function/ManageFunction/hook.ts @@ -11,6 +11,7 @@ import { import { ellipseAddress } from '@/helpers/utils' import useDownloadLogs from '@/hooks/common/useDownloadLogs' import { + CreditPaymentData, HoldingPaymentData, PaymentData, } from '@/components/common/entityData/EntityPayment/types' @@ -195,12 +196,27 @@ export function useManageFunction(): ManageFunction { loading, } as HoldingPaymentData, ] + case PaymentType.credit: + return [ + { + cost, + paymentType: PaymentType.credit, + runningTime, + startTime: program.time, + blockchain: program.payment.chain, + loading, + } as CreditPaymentData, + ] default: return [ { + cost, paymentType: PaymentType.hold, + runningTime, + startTime: program?.time, + blockchain: program?.payment?.chain, loading: true, - } as PaymentData, + } as HoldingPaymentData, ] } }, [cost, program?.payment, runningTime, program?.time, loading]) diff --git a/src/components/pages/console/function/NewFunctionPage/cmp.tsx b/src/components/pages/console/function/NewFunctionPage/cmp.tsx index 4f1a195cd..946befb36 100644 --- a/src/components/pages/console/function/NewFunctionPage/cmp.tsx +++ b/src/components/pages/console/function/NewFunctionPage/cmp.tsx @@ -1,5 +1,5 @@ import Head from 'next/head' -import { Button, TextGradient } from '@aleph-front/core' +import { TextGradient } from '@aleph-front/core' import { EntityType, EntityDomainType } from '@/helpers/constants' import { useNewFunctionPage } from './hook' import CheckoutSummary from '@/components/form/CheckoutSummary' @@ -16,15 +16,17 @@ import Form from '@/components/form/Form' import SwitchToggleContainer from '@/components/common/SwitchToggleContainer' import SelectCustomFunctionRuntime from '@/components/form/SelectCustomFunctionRuntime' import NewEntityTab from '@/components/common/NewEntityTab' -import { SectionTitle } from '@/components/common/CompositeTitle' +import { CompositeSectionTitle } from '@/components/common/CompositeTitle' import { PageProps } from '@/types/types' import BackButtonSection from '@/components/common/BackButtonSection' +import CheckoutButton from '@/components/form/CheckoutButton' export default function NewFunctionPage({ mainRef }: PageProps) { const { address, - accountBalance, - isCreateButtonDisabled, + accountCreditBalance, + createFunctionDisabled, + createFunctionButtonTitle, values, control, errors, @@ -51,7 +53,9 @@ export default function NewFunctionPage({ mainRef }: PageProps) {
- Code to execute + + Code to execute +

If your code has any dependencies, you can upload them separately in the volume section below to ensure a faster creation. @@ -61,7 +65,9 @@ export default function NewFunctionPage({ mainRef }: PageProps) {

- Type of scheduling + + Type of scheduling +

Configure if this program should be running continuously, persistent, or only on-demand in response to a user request or an @@ -72,7 +78,9 @@ export default function NewFunctionPage({ mainRef }: PageProps) {

- Select an instance size + + Select an instance size +

Select the hardware resources allocated to your functions, ensuring optimal performance and efficient resource usage tailored @@ -88,7 +96,9 @@ export default function NewFunctionPage({ mainRef }: PageProps) {

- Name and tags + + Name and tags +

Organize and identify your functions more effectively by assigning a unique name, obtaining a hash reference, and defining multiple @@ -101,9 +111,9 @@ export default function NewFunctionPage({ mainRef }: PageProps) {

- + Advanced Configuration Options - +

Customize your function with our Advanced Configuration Options. Add volumes and custom domains to meet your specific needs. @@ -164,8 +174,7 @@ export default function NewFunctionPage({ mainRef }: PageProps) { control={control} address={address} cost={cost} - unlockedAmount={accountBalance} - paymentMethod={values.paymentMethod} + unlockedAmount={accountCreditBalance} mainRef={mainRef} description={ <> @@ -175,18 +184,12 @@ export default function NewFunctionPage({ mainRef }: PageProps) { } button={ - + } /> diff --git a/src/components/pages/console/function/NewFunctionPage/hook.ts b/src/components/pages/console/function/NewFunctionPage/hook.ts index c81d34df5..936813dd3 100644 --- a/src/components/pages/console/function/NewFunctionPage/hook.ts +++ b/src/components/pages/console/function/NewFunctionPage/hook.ts @@ -1,6 +1,5 @@ import { FormEvent, useCallback, useMemo } from 'react' import { useAppState } from '@/contexts/appState' -import { useSyncPaymentMethod } from '@/hooks/common/useSyncPaymentMethod' import { useRouter } from 'next/router' import { InstanceSpecsField } from '@/hooks/form/useSelectInstanceSpecs' import { VolumeField } from '@/hooks/form/useAddVolume' @@ -32,6 +31,11 @@ import Err from '@/helpers/errors' import { useDefaultTiers } from '@/hooks/common/pricing/useDefaultTiers' import { useCanAfford } from '@/hooks/common/useCanAfford' import { useConnection } from '@/hooks/common/useConnection' +import { + CreditPaymentConfiguration, + PaymentConfiguration, +} from '@/domain/executable' +import { BlockchainId } from '@/domain/connect/base' export type NewFunctionFormState = NameAndTagsField & { code: FunctionCodeField @@ -48,8 +52,9 @@ export type NewFunctionFormState = NameAndTagsField & { export type UseNewFunctionPage = { address: string - accountBalance: number - isCreateButtonDisabled: boolean + accountCreditBalance: number + createFunctionDisabled: boolean + createFunctionButtonTitle: string values: any control: Control errors: FieldErrors @@ -64,7 +69,8 @@ export function useNewFunctionPage(): UseNewFunctionPage { const { blockchain, account, - balance: accountBalance = 0, + creditBalance: accountCreditBalance = 0, + handleConnect, } = useConnection({ triggerOnMount: false, }) @@ -75,13 +81,21 @@ export function useNewFunctionPage(): UseNewFunctionPage { const onSubmit = useCallback( async (state: NewFunctionFormState) => { if (!manager) throw Err.ConnectYourWallet + if (!account) throw Err.InvalidAccount + + if (!blockchain) { + handleConnect({ blockchain: BlockchainId.BASE }) + throw Err.InvalidNetwork + } + + const payment: CreditPaymentConfiguration = { + chain: blockchain, + type: PaymentMethod.Credit, + } const program = { ...state, - payment: { - chain: blockchain, - type: PaymentMethod.Hold, - }, + payment, } as AddProgram const iSteps = await manager.getAddSteps(program) @@ -113,7 +127,7 @@ export function useNewFunctionPage(): UseNewFunctionPage { await stop() } }, - [blockchain, dispatch, manager, next, router, stop], + [account, blockchain, handleConnect, dispatch, manager, next, router, stop], ) const { defaultTiers } = useDefaultTiers({ type: EntityType.Program }) @@ -124,7 +138,7 @@ export function useNewFunctionPage(): UseNewFunctionPage { code: { ...defaultCode } as FunctionCodeField, specs: defaultTiers[0], isPersistent: false, - paymentMethod: PaymentMethod.Hold, + paymentMethod: PaymentMethod.Credit, }), [defaultTiers], ) @@ -133,7 +147,6 @@ export function useNewFunctionPage(): UseNewFunctionPage { control, handleSubmit, formState: { errors }, - setValue, } = useForm({ defaultValues, onSubmit, @@ -142,6 +155,13 @@ export function useNewFunctionPage(): UseNewFunctionPage { // @note: dont use watch, use useWatch instead: https://github.com/react-hook-form/react-hook-form/issues/10753 const values = useWatch({ control }) as NewFunctionFormState + const payment: PaymentConfiguration = useMemo(() => { + return { + chain: blockchain, + type: PaymentMethod.Credit, + } as CreditPaymentConfiguration + }, [blockchain]) + const costProps: UseProgramCostProps = useMemo( () => ({ entityType: EntityType.Program, @@ -152,33 +172,48 @@ export function useNewFunctionPage(): UseNewFunctionPage { volumes: values.volumes, domains: values.domains, paymentMethod: values.paymentMethod, + payment, code: values.code, }, }), - [values], + [payment, values], ) const cost = useEntityCost(costProps) - const { isCreateButtonDisabled } = useCanAfford({ + const { canAfford, isCreateButtonDisabled } = useCanAfford({ cost, - accountBalance, + accountCreditBalance, }) + // Checks if user can afford with current balance + const hasEnoughBalance = useMemo(() => { + if (!account) return false + if (!isCreateButtonDisabled) return true + return canAfford + }, [account, canAfford, isCreateButtonDisabled]) + + const createFunctionButtonTitle: UseNewFunctionPage['createFunctionButtonTitle'] = + useMemo(() => { + if (!account) return 'Connect' + if (!hasEnoughBalance) return 'Insufficient Credits' + + return 'Create function' + }, [account, hasEnoughBalance]) + + const createFunctionDisabled = useMemo(() => { + return createFunctionButtonTitle !== 'Create function' + }, [createFunctionButtonTitle]) + const handleBack = () => { router.push('.') } - // Sync form payment method with global state - useSyncPaymentMethod({ - formPaymentMethod: values.paymentMethod, - setValue, - }) - return { address: account?.address || '', - accountBalance, - isCreateButtonDisabled, + accountCreditBalance, + createFunctionDisabled, + createFunctionButtonTitle, values, control, errors, diff --git a/src/components/pages/console/gpuInstance/GpuInstancesTabContent/cmp.tsx b/src/components/pages/console/gpuInstance/GpuInstancesTabContent/cmp.tsx index 1acbe1b74..3934347d6 100644 --- a/src/components/pages/console/gpuInstance/GpuInstancesTabContent/cmp.tsx +++ b/src/components/pages/console/gpuInstance/GpuInstancesTabContent/cmp.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useCallback } from 'react' import tw from 'twin.macro' import { GpuInstancesTabContentProps } from './types' import ButtonLink from '@/components/common/ButtonLink' @@ -9,9 +9,17 @@ import { } from '@/helpers/utils' import EntityTable from '@/components/common/EntityTable' import { Icon } from '@aleph-front/core' +import { GpuInstance } from '@/domain/gpuInstance' +import { PaymentType } from '@aleph-sdk/message' +import ExternalLink from '@/components/common/ExternalLink' +import { NAVIGATION_URLS } from '@/helpers/constants' export const GpuInstancesTabContent = React.memo( ({ data }: GpuInstancesTabContentProps) => { + const isCredit = useCallback((row: GpuInstance) => { + return row.payment?.type === PaymentType.credit + }, []) + return ( <> {data.length > 0 ? ( @@ -21,6 +29,9 @@ export const GpuInstancesTabContent = React.memo( borderType="none" rowNoise rowKey={(row) => row.id} + rowProps={(row) => ({ + css: isCredit(row) ? '' : tw`opacity-40`, + })} data={data} columns={[ { @@ -62,15 +73,39 @@ export const GpuInstancesTabContent = React.memo( { label: '', align: 'right', - render: (row) => ( - - - - ), + render: (row) => { + const disabled = !isCredit(row) + + return ( + + To manage this GPU instance, go to the{' '} + +

+ ) + } + tooltipPosition={{ + my: 'bottom-right', + at: 'bottom-center', + }} + > + + + ) + }, cellProps: () => ({ css: tw`pl-3!`, }), diff --git a/src/components/pages/console/gpuInstance/ManageGpuInstance/cmp.tsx b/src/components/pages/console/gpuInstance/ManageGpuInstance/cmp.tsx index 1ed389b73..888522d43 100644 --- a/src/components/pages/console/gpuInstance/ManageGpuInstance/cmp.tsx +++ b/src/components/pages/console/gpuInstance/ManageGpuInstance/cmp.tsx @@ -83,6 +83,11 @@ export default function ManageGpuInstance() { // Navigation handlers handleBack, + + // Ports + ports, + sshForwardedPort, + handlePortsChange, } = useManageGpuInstance() return ( @@ -162,6 +167,7 @@ export default function ManageGpuInstance() { , immutableVolumes.length && ( , ]} /> diff --git a/src/components/pages/console/gpuInstance/ManageGpuInstance/hook.ts b/src/components/pages/console/gpuInstance/ManageGpuInstance/hook.ts index 5417cc665..d4bd8ae98 100644 --- a/src/components/pages/console/gpuInstance/ManageGpuInstance/hook.ts +++ b/src/components/pages/console/gpuInstance/ManageGpuInstance/hook.ts @@ -1,4 +1,5 @@ import { useRouter } from 'next/router' +import { useMemo, useState } from 'react' import { GpuInstance, GpuInstanceManager } from '@/domain/gpuInstance' import { useGpuInstanceManager } from '@/hooks/common/useManager/useGpuInstanceManager' import { useRequestGpuInstances } from '@/hooks/common/useRequestEntity/useRequestGpuInstances' @@ -6,10 +7,16 @@ import { useManageInstanceEntity, UseManageInstanceEntityReturn, } from '@/hooks/common/useEntity/useManageInstanceEntity' +import { useForwardedPorts } from '@/hooks/common/useForwardedPorts' +import { getSSHForwardedPort } from '@/components/common/entityData/EntityPortForwarding/utils' +import { ForwardedPort } from '@/components/common/entityData/EntityPortForwarding/types' export type UseManageGpuInstanceReturn = UseManageInstanceEntityReturn & { gpuInstance?: GpuInstance gpuInstanceManager?: GpuInstanceManager + ports: ForwardedPort[] + sshForwardedPort?: string + handlePortsChange: (ports: ForwardedPort[]) => void } export function useManageGpuInstance(): UseManageGpuInstanceReturn { @@ -29,9 +36,38 @@ export function useManageGpuInstance(): UseManageGpuInstanceReturn { entityManager: gpuInstanceManager, }) + const { status } = manageInstanceEntityProps + + // Fetch forwarded ports + const { ports: fetchedPorts } = useForwardedPorts({ + entityHash: gpuInstance?.id, + executableStatus: status, + }) + + // Local state for ports to allow updates + const [ports, setPorts] = useState(fetchedPorts) + + // Update local ports state when fetched ports change + useMemo(() => { + setPorts(fetchedPorts) + }, [fetchedPorts]) + + // Extract SSH forwarded port + const sshForwardedPort = useMemo(() => { + return getSSHForwardedPort(ports) + }, [ports]) + + // Handler to update ports + const handlePortsChange = (updatedPorts: ForwardedPort[]) => { + setPorts(updatedPorts) + } + return { gpuInstance, gpuInstanceManager, + ports, + sshForwardedPort, + handlePortsChange, ...manageInstanceEntityProps, } } diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx index 18728daa2..f4a0b4be5 100644 --- a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx +++ b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { useEffect, useMemo, useRef } from 'react' import Head from 'next/head' import Image from 'next/image' import { @@ -9,7 +9,6 @@ import { NodeScore, TableColumn, NoisyContainer, - TooltipProps, Checkbox, } from '@aleph-front/core' import ButtonWithInfoTooltip from '@/components/common/ButtonWithInfoTooltip' @@ -21,83 +20,31 @@ import AddSSHKeys from '@/components/form/AddSSHKeys' import AddDomains from '@/components/form/AddDomains' import AddNameAndTags from '@/components/form/AddNameAndTags' import CheckoutSummary from '@/components/form/CheckoutSummary' -import { - EntityDomainType, - EntityType, - PaymentMethod, - apiServer, -} from '@/helpers/constants' +import { EntityDomainType, EntityType } from '@/helpers/constants' +import { useSettings } from '@/hooks/common/useSettings' import { CenteredContainer } from '@/components/common/CenteredContainer' import Form from '@/components/form/Form' import SwitchToggleContainer from '@/components/common/SwitchToggleContainer' import NewEntityTab from '@/components/common/NewEntityTab' import NodesTable from '@/components/common/NodesTable' import SpinnerOverlay from '@/components/common/SpinnerOverlay' -import { SectionTitle } from '@/components/common/CompositeTitle' +import { CompositeSectionTitle } from '@/components/common/CompositeTitle' import { PageProps } from '@/types/types' import Strong from '@/components/common/Strong' import CRNList from '../../../../common/CRNList' import BackButtonSection from '@/components/common/BackButtonSection' -import BorderBox from '@/components/common/BorderBox' import ExternalLink from '@/components/common/ExternalLink' -import { useNewGpuInstancePage, UseNewGpuInstancePageReturn } from './hook' - -const CheckoutButton = React.memo( - ({ - disabled, - title = 'Create instance', - tooltipContent, - isFooter, - shouldRequestTermsAndConditions, - handleRequestTermsAndConditionsAgreement, - handleSubmit, - }: { - disabled: boolean - title?: string - tooltipContent?: TooltipProps['content'] - isFooter: boolean - shouldRequestTermsAndConditions?: boolean - handleRequestTermsAndConditionsAgreement: UseNewGpuInstancePageReturn['handleRequestTermsAndConditionsAgreement'] - handleSubmit: UseNewGpuInstancePageReturn['handleSubmit'] - }) => { - const checkoutButtonRef = useRef(null) - - return ( - - {title} - - ) - }, -) -CheckoutButton.displayName = 'CheckoutButton' +import { useNewGpuInstancePage } from './hook' +import CheckoutButton from '@/components/form/CheckoutButton' export default function NewGpuInstancePage({ mainRef }: PageProps) { const { address, - accountBalance, + accountCreditBalance, blockchainName, - streamDisabled, - disabledStreamDisabledMessage, + manuallySelectCRNDisabled, + manuallySelectCRNDisabledMessage, createInstanceDisabled, - createInstanceDisabledMessage, createInstanceButtonTitle, values, control, @@ -123,10 +70,7 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { handleCheckTermsAndConditions, } = useNewGpuInstancePage() - const sectionNumber = useCallback( - (n: number) => (values.paymentMethod === PaymentMethod.Stream ? 1 : 0) + n, - [values.paymentMethod], - ) + const { apiServer } = useSettings() // ------------------ // Handle modals @@ -286,7 +230,7 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { ), }, ] as TableColumn[] - }, [setSelectedModal]) + }, [apiServer, setSelectedModal]) const nodeData = useMemo(() => (node ? [node] : []), [node]) const manuallySelectButtonRef = useRef(null) @@ -307,18 +251,11 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {
- {createInstanceDisabledMessage && ( -
- - - {createInstanceDisabledMessage} - - -
- )}
- Selected GPU + + Selected GPU +

Your instance is configured with your manually selected GPU, operating under the Pay-as-you-go payment method @@ -343,6 +280,8 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { kind="functional" variant="warning" size="md" + disabled={manuallySelectCRNDisabled} + tooltipContent={manuallySelectCRNDisabledMessage} onClick={handleManuallySelectCRN} > Manually select GPU @@ -356,26 +295,14 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {

- + Select your tier - - {values.paymentMethod === PaymentMethod.Hold ? ( -

- Your instance is ready to be configured using our{' '} - automated CRN selection, set to run on{' '} - {blockchainName} with the{' '} - Holder-tier payment method, allowing you - seamless access while you hold ALEPH tokens. If you wish to - customize your Compute Resource Node (CRN) or use a different - payment approach, you can change your selection below. -

- ) : ( -

- Please select one of the available instance tiers as a base for - your VM. You will be able to customize the volumes further below - in the form. -

- )} + +

+ Please select one of the available instance tiers as a base for + your VM. You will be able to customize the volumes further below + in the form. +

@@ -385,7 +312,6 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { type={EntityType.GpuInstance} gpuModel={node?.selectedGpu?.model} isPersistent - paymentMethod={values.paymentMethod} nodeSpecs={nodeSpecs} > {!node && ( @@ -399,9 +325,9 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {
- + Choose an image - +

Chose a base image for your GPU Instance. It's the base system that you will be able to customize. @@ -413,9 +339,9 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {

- + Configure SSH Key - +

Access your cloud instances securely. Give existing key's below access to this instance or add new keys. Remember, storing @@ -429,7 +355,9 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {

- Name and tags + + Name and tags +

Organize and identify your instances more effectively by assigning a unique name, obtaining a hash reference, and defining multiple @@ -444,9 +372,9 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {

- + Advanced Configuration Options - +

Customize your GPU Instance with our Advanced Configuration Options. Add volumes and custom domains to meet your specific @@ -492,20 +420,15 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { control={control} address={address} cost={cost} - receiverAddress={node?.reward} - unlockedAmount={accountBalance} - paymentMethod={values.paymentMethod} - streamDuration={values.streamDuration} - disablePaymentMethod={streamDisabled} - disabledStreamTooltip={disabledStreamDisabledMessage} + unlockedAmount={accountCreditBalance} mainRef={mainRef} description={ <> - You can either leverage the traditional method of holding tokens - in your wallet for resource access, or opt for the Pay-As-You-Go - (PAYG) system, which allows you to pay precisely for what you use, - for the duration you need. The PAYG option includes a token stream - feature, enabling real-time payment for resources as you use them. + Aleph Cloud runs on a credit-based system, + designed for flexibility and transparency. You can top up credits + with fiat, USDC, or ALEPH. Your credits are + deducted only as you consume resources, ensuring you pay exactly + for what you use. } // Duplicate buttons to have different references for the tooltip on each one @@ -513,7 +436,6 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { } - footerButton={ - - } /> diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts b/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts index c2cad7ce9..a61932f21 100644 --- a/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts +++ b/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts @@ -8,11 +8,6 @@ import { useState, } from 'react' import Router, { useRouter } from 'next/router' -import { - createFromEVMAccount, - isAccountSupported as isAccountPAYGCompatible, - isBlockchainSupported as isBlockchainPAYGCompatible, -} from '@aleph-sdk/superfluid' import { useForm } from '@/hooks/common/useForm' import { defaultNameAndTags, @@ -40,10 +35,6 @@ import { } from '@/hooks/common/useEntityCost' import { useRequestCRNSpecs } from '@/hooks/common/useRequestEntity/useRequestCRNSpecs' import { CRNSpecs, NodeManager } from '@/domain/node' -import { - defaultStreamDuration, - StreamDurationField, -} from '@/hooks/form/useSelectStreamDuration' import { stepsCatalog, useCheckoutNotification, @@ -52,15 +43,12 @@ import { EntityAddAction } from '@/store/entity' import { useConnection } from '@/hooks/common/useConnection' import Err from '@/helpers/errors' import { BlockchainId, blockchains } from '@/domain/connect/base' -import { PaymentConfiguration } from '@/domain/executable' -import { EVMAccount } from '@aleph-sdk/evm' -import { isBlockchainHoldingCompatible } from '@/domain/blockchain' -import { ModalCardProps, TooltipProps, useModal } from '@aleph-front/core' import { - unsupportedHoldingDisabledMessage, - unsupportedStreamDisabledMessage, - holderTierNotSupportedMessage, -} from './disabledMessages' + CreditPaymentConfiguration, + PaymentConfiguration, +} from '@/domain/executable' +import { ModalCardProps, TooltipProps, useModal } from '@aleph-front/core' +import { accountConnectionRequiredDisabledMessage } from './disabledMessages' import useFetchTermsAndConditions, { TermsAndConditions, } from '@/hooks/common/useFetchTermsAndConditions' @@ -69,7 +57,6 @@ import { useGpuInstanceManager } from '@/hooks/common/useManager/useGpuInstanceM import { GpuInstanceManager } from '@/domain/gpuInstance' import usePrevious from '@/hooks/common/usePrevious' import { useCanAfford } from '@/hooks/common/useCanAfford' -import { useSyncPaymentMethod } from '@/hooks/common/useSyncPaymentMethod' export type NewGpuInstanceFormState = NameAndTagsField & { image: InstanceImageField @@ -80,8 +67,6 @@ export type NewGpuInstanceFormState = NameAndTagsField & { systemVolume: InstanceSystemVolumeField nodeSpecs?: CRNSpecs paymentMethod: PaymentMethod - streamDuration: StreamDurationField - streamCost: number termsAndConditions?: string } @@ -89,13 +74,12 @@ export type Modal = 'node-list' | 'terms-and-conditions' export type UseNewGpuInstancePageReturn = { address: string - accountBalance: number + accountCreditBalance: number blockchainName: string + manuallySelectCRNDisabled: boolean + manuallySelectCRNDisabledMessage?: TooltipProps['content'] createInstanceDisabled: boolean - createInstanceDisabledMessage?: TooltipProps['content'] - createInstanceButtonTitle?: string - streamDisabled: boolean - disabledStreamDisabledMessage?: TooltipProps['content'] + createInstanceButtonTitle: string values: any control: Control errors: FieldErrors @@ -126,7 +110,7 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { const { blockchain, account, - balance: accountBalance = 0, + creditBalance: accountCreditBalance = 0, handleConnect, } = useConnection({ triggerOnMount: false, @@ -203,46 +187,33 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { async (state: NewGpuInstanceFormState) => { if (!manager) throw Err.ConnectYourWallet if (!account) throw Err.InvalidAccount - if (!node || !node.stream_reward) throw Err.InvalidNode + if (!node) throw Err.InvalidNode if (!nodeSpecs) throw Err.InvalidCRNSpecs - if (!state?.streamCost) throw Err.InvalidStreamCost const [minSpecs] = defaultTiers const isValid = NodeManager.validateMinNodeSpecs(minSpecs, nodeSpecs) if (!isValid) throw Err.InvalidCRNSpecs - if ( - !blockchain || - !isBlockchainPAYGCompatible(blockchain) || - !isAccountPAYGCompatible(account) - ) { + if (!blockchain) { handleConnect({ blockchain: BlockchainId.BASE }) throw Err.InvalidNetwork } - const superfluidAccount = await createFromEVMAccount( - account as EVMAccount, - ) - - const payment = { + const payment: CreditPaymentConfiguration = { chain: blockchain, - type: PaymentMethod.Stream, - sender: account.address, - receiver: node.stream_reward, - streamCost: state.streamCost, - streamDuration: state.streamDuration, + type: PaymentMethod.Credit, } const instance = { ...state, payment, - node: state.paymentMethod === PaymentMethod.Stream ? node : undefined, + node, } as AddInstance const iSteps = await manager.getAddSteps(instance) const nSteps = iSteps.map((i) => stepsCatalog[i]) - const steps = manager.addSteps(instance, superfluidAccount) + const steps = manager.addSteps(instance) try { let accountInstance @@ -289,11 +260,9 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { () => ({ ...defaultNameAndTags, image: defaultInstanceImage, - specs: undefined, + specs: defaultTiers[0], systemVolume: { size: defaultTiers[0]?.storage }, - paymentMethod: PaymentMethod.Stream, - streamDuration: defaultStreamDuration, - streamCost: Number.POSITIVE_INFINITY, + paymentMethod: PaymentMethod.Credit, termsAndConditions: undefined, }), [defaultTiers], @@ -307,8 +276,8 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { } = useForm({ defaultValues, onSubmit, - resolver: zodResolver(GpuInstanceManager.addStreamSchema), - readyDeps: [defaultValues], + resolver: zodResolver(GpuInstanceManager.addSchema), + readyDeps: [], }) const formValues = useWatch({ control }) as NewGpuInstanceFormState @@ -321,13 +290,9 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { const payment: PaymentConfiguration = useMemo(() => { return { chain: blockchain, - type: PaymentMethod.Stream, - sender: account?.address, - receiver: node?.stream_reward, - streamCost: formValues?.streamCost || 1, - streamDuration: formValues?.streamDuration, - } as PaymentConfiguration - }, [formValues, blockchain, account, node]) + type: PaymentMethod.Credit, + } as CreditPaymentConfiguration + }, [blockchain]) const costProps: UseGpuInstanceCostProps = useMemo( () => ({ @@ -337,7 +302,6 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { specs: formValues.specs, volumes: formValues.volumes, domains: formValues.domains, - streamDuration: formValues.streamDuration, paymentMethod: formValues.paymentMethod, payment, isPersistent: true, @@ -357,26 +321,30 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { // Memos const shouldRequestTermsAndConditions = useMemo(() => { - return ( - !!node?.terms_and_conditions && - formValues.paymentMethod === PaymentMethod.Stream - ) - }, [node, formValues.paymentMethod]) + return !!node?.terms_and_conditions + }, [node]) const blockchainName = useMemo(() => { return blockchain ? blockchains[blockchain]?.name : 'Current network' }, [blockchain]) - const disabledStreamDisabledMessage: UseNewGpuInstancePageReturn['disabledStreamDisabledMessage'] = - holderTierNotSupportedMessage() + const manuallySelectCRNDisabledMessage: UseNewGpuInstancePageReturn['manuallySelectCRNDisabledMessage'] = + useMemo(() => { + if (!account) + return accountConnectionRequiredDisabledMessage( + 'manually selecting CRNs', + ) + }, [account]) - const streamDisabled = true + const manuallySelectCRNDisabled = useMemo(() => { + return !!manuallySelectCRNDisabledMessage + }, [manuallySelectCRNDisabledMessage]) const address = useMemo(() => account?.address || '', [account]) const { canAfford, isCreateButtonDisabled } = useCanAfford({ cost, - accountBalance, + accountCreditBalance, }) // Checks if user can afford with current balance @@ -386,34 +354,17 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { return canAfford }, [account, canAfford, isCreateButtonDisabled]) - const createInstanceDisabledMessage: UseNewGpuInstancePageReturn['createInstanceDisabledMessage'] = - useMemo(() => { - // Checks configuration for PAYG tier - if (formValues.paymentMethod === PaymentMethod.Stream) { - if (!isBlockchainPAYGCompatible(blockchain)) - return unsupportedStreamDisabledMessage(blockchainName) - } - - // Checks configuration for Holder tier - if (formValues.paymentMethod === PaymentMethod.Hold) { - if (!isBlockchainHoldingCompatible(blockchain)) - return unsupportedHoldingDisabledMessage(blockchainName) - } - }, [blockchain, blockchainName, formValues.paymentMethod]) - const createInstanceButtonTitle: UseNewGpuInstancePageReturn['createInstanceButtonTitle'] = useMemo(() => { if (!account) return 'Connect' - if (!hasEnoughBalance) return 'Insufficient ALEPH' + if (!hasEnoughBalance) return 'Insufficient Credits' return 'Create instance' }, [account, hasEnoughBalance]) const createInstanceDisabled = useMemo(() => { - if (createInstanceButtonTitle !== 'Create instance') return true - - return !!createInstanceDisabledMessage - }, [createInstanceButtonTitle, createInstanceDisabledMessage]) + return createInstanceButtonTitle !== 'Create instance' + }, [createInstanceButtonTitle]) // ------------------------- // Handlers @@ -494,35 +445,20 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { setValue('nodeSpecs', nodeSpecs) }, [nodeSpecs, setValue]) - // @note: Set streamCost - useEffect(() => { - if (!cost) return - if (formValues.streamCost === cost.cost) return - - setValue('streamCost', cost.cost) - }, [cost, setValue, formValues]) - - // Sync form payment method with global state - useSyncPaymentMethod({ - formPaymentMethod: formValues.paymentMethod, - setValue, - }) - return { address, - accountBalance, + accountCreditBalance, blockchainName, createInstanceDisabled, - createInstanceDisabledMessage, createInstanceButtonTitle, + manuallySelectCRNDisabled, + manuallySelectCRNDisabledMessage, values: formValues, control, errors, cost, node, nodeSpecs, - streamDisabled, - disabledStreamDisabledMessage, selectedModal, setSelectedModal, selectedNode, diff --git a/src/components/pages/console/instance/InstancesTabContent/cmp.tsx b/src/components/pages/console/instance/InstancesTabContent/cmp.tsx index bedbc9b32..5b97edbea 100644 --- a/src/components/pages/console/instance/InstancesTabContent/cmp.tsx +++ b/src/components/pages/console/instance/InstancesTabContent/cmp.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useCallback } from 'react' import tw from 'twin.macro' import { InstancesTabContentProps } from './types' import ButtonLink from '@/components/common/ButtonLink' @@ -9,9 +9,17 @@ import { } from '@/helpers/utils' import EntityTable from '@/components/common/EntityTable' import { Icon } from '@aleph-front/core' +import { PaymentType } from '@aleph-sdk/message' +import ExternalLink from '@/components/common/ExternalLink' +import { NAVIGATION_URLS } from '@/helpers/constants' +import { Instance } from '@/domain/instance' export const InstancesTabContent = React.memo( ({ data }: InstancesTabContentProps) => { + const isCredit = useCallback((row: Instance) => { + return row.payment?.type === PaymentType.credit + }, []) + return ( <> {data.length > 0 ? ( @@ -21,6 +29,9 @@ export const InstancesTabContent = React.memo( borderType="none" rowNoise rowKey={(row) => row.id} + rowProps={(row) => ({ + css: isCredit(row) ? '' : tw`opacity-40`, + })} data={data} columns={[ { @@ -48,7 +59,7 @@ export const InstancesTabContent = React.memo( }), }, { - label: 'HDD', + label: 'date', align: 'right', sortable: true, render: (row) => humanReadableSize(row.size, 'MiB'), @@ -62,15 +73,39 @@ export const InstancesTabContent = React.memo( { label: '', align: 'right', - render: (row) => ( - - - - ), + render: (row) => { + const disabled = !isCredit(row) + + return ( + + To manage this instance, go to the{' '} + +

+ ) + } + tooltipPosition={{ + my: 'bottom-right', + at: 'bottom-center', + }} + > + + + ) + }, cellProps: () => ({ css: tw`pl-3!`, }), diff --git a/src/components/pages/console/instance/ManageInstance/cmp.tsx b/src/components/pages/console/instance/ManageInstance/cmp.tsx index 31b5cd8ab..4f644694b 100644 --- a/src/components/pages/console/instance/ManageInstance/cmp.tsx +++ b/src/components/pages/console/instance/ManageInstance/cmp.tsx @@ -88,6 +88,14 @@ export default function ManageInstance() { // Navigation handlers handleBack, + + // Ports + ports, + sshForwardedPort, + handlePortsChange, + + // Credit balance + creditBalance, } = useManageInstance() return ( @@ -105,6 +113,8 @@ export default function ManageInstance() { type={EntityType.Instance} isAllocated={isAllocated} calculatedStatus={calculatedStatus} + creditBalance={creditBalance} + paymentData={paymentData} // Start action showStart startDisabled={startDisabled} @@ -163,6 +173,7 @@ export default function ManageInstance() { , immutableVolumes.length && ( , ]} /> diff --git a/src/components/pages/console/instance/ManageInstance/hook.ts b/src/components/pages/console/instance/ManageInstance/hook.ts index 3df5f98a6..617b8760b 100644 --- a/src/components/pages/console/instance/ManageInstance/hook.ts +++ b/src/components/pages/console/instance/ManageInstance/hook.ts @@ -1,4 +1,5 @@ import { useRouter } from 'next/router' +import { useMemo, useState } from 'react' import { Instance, InstanceManager } from '@/domain/instance' import { useInstanceManager } from '@/hooks/common/useManager/useInstanceManager' import { useRequestInstances } from '@/hooks/common/useRequestEntity/useRequestInstances' @@ -6,16 +7,27 @@ import { useManageInstanceEntity, UseManageInstanceEntityReturn, } from '@/hooks/common/useEntity/useManageInstanceEntity' +import { useForwardedPorts } from '@/hooks/common/useForwardedPorts' +import { getSSHForwardedPort } from '@/components/common/entityData/EntityPortForwarding/utils' +import { ForwardedPort } from '@/components/common/entityData/EntityPortForwarding/types' +import { useAppState } from '@/contexts/appState' export type UseManageInstanceReturn = UseManageInstanceEntityReturn & { instance?: Instance instanceManager?: InstanceManager + ports: ForwardedPort[] + sshForwardedPort?: string + handlePortsChange: (ports: ForwardedPort[]) => void + creditBalance?: number } export function useManageInstance(): UseManageInstanceReturn { const router = useRouter() const { hash } = router.query + const [state] = useAppState() + const { creditBalance } = state.connection + const { entities } = useRequestInstances({ ids: hash as string }) const [instance] = entities || [] @@ -29,9 +41,39 @@ export function useManageInstance(): UseManageInstanceReturn { entityManager: instanceManager, }) + const { status } = manageInstanceEntityProps + + // Fetch forwarded ports + const { ports: fetchedPorts } = useForwardedPorts({ + entityHash: instance?.id, + executableStatus: status, + }) + + // Local state for ports to allow updates + const [ports, setPorts] = useState(fetchedPorts) + + // Update local ports state when fetched ports change + useMemo(() => { + setPorts(fetchedPorts) + }, [fetchedPorts]) + + // Extract SSH forwarded port + const sshForwardedPort = useMemo(() => { + return getSSHForwardedPort(ports) + }, [ports]) + + // Handler to update ports + const handlePortsChange = (updatedPorts: ForwardedPort[]) => { + setPorts(updatedPorts) + } + return { instance, instanceManager, + ports, + sshForwardedPort, + handlePortsChange, + creditBalance, ...manageInstanceEntityProps, } } diff --git a/src/components/pages/console/instance/NewInstancePage/cmp.tsx b/src/components/pages/console/instance/NewInstancePage/cmp.tsx index f656a6227..da4312091 100644 --- a/src/components/pages/console/instance/NewInstancePage/cmp.tsx +++ b/src/components/pages/console/instance/NewInstancePage/cmp.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { useEffect, useMemo, useRef } from 'react' import Head from 'next/head' import Image from 'next/image' import { @@ -10,7 +10,6 @@ import { NodeScore, TableColumn, NoisyContainer, - TooltipProps, Checkbox, } from '@aleph-front/core' import ButtonWithInfoTooltip from '@/components/common/ButtonWithInfoTooltip' @@ -22,85 +21,30 @@ import AddSSHKeys from '@/components/form/AddSSHKeys' import AddDomains from '@/components/form/AddDomains' import AddNameAndTags from '@/components/form/AddNameAndTags' import CheckoutSummary from '@/components/form/CheckoutSummary' -import { - EntityDomainType, - EntityType, - PaymentMethod, - apiServer, -} from '@/helpers/constants' +import { EntityDomainType, EntityType } from '@/helpers/constants' +import { useSettings } from '@/hooks/common/useSettings' import { CenteredContainer } from '@/components/common/CenteredContainer' -import { useNewInstancePage, UseNewInstancePageReturn } from './hook' +import { useNewInstancePage } from './hook' import Form from '@/components/form/Form' import SwitchToggleContainer from '@/components/common/SwitchToggleContainer' import NewEntityTab from '@/components/common/NewEntityTab' import NodesTable from '@/components/common/NodesTable' import SpinnerOverlay from '@/components/common/SpinnerOverlay' -import { SectionTitle } from '@/components/common/CompositeTitle' +import { CompositeSectionTitle } from '@/components/common/CompositeTitle' import { PageProps } from '@/types/types' import Strong from '@/components/common/Strong' import CRNList from '../../../../common/CRNList' import BackButtonSection from '@/components/common/BackButtonSection' -import BorderBox from '@/components/common/BorderBox' import ExternalLink from '@/components/common/ExternalLink' - -const CheckoutButton = React.memo( - ({ - disabled, - title = 'Create instance', - tooltipContent, - isFooter, - shouldRequestTermsAndConditions, - handleRequestTermsAndConditionsAgreement, - handleSubmit, - }: { - disabled: boolean - title?: string - tooltipContent?: TooltipProps['content'] - isFooter: boolean - shouldRequestTermsAndConditions?: boolean - handleRequestTermsAndConditionsAgreement: UseNewInstancePageReturn['handleRequestTermsAndConditionsAgreement'] - handleSubmit: UseNewInstancePageReturn['handleSubmit'] - }) => { - const checkoutButtonRef = useRef(null) - - return ( - - {title} - - ) - }, -) -CheckoutButton.displayName = 'CheckoutButton' +import CheckoutButton from '@/components/form/CheckoutButton' export default function NewInstancePage({ mainRef }: PageProps) { const { address, - accountBalance, - blockchainName, - streamDisabled, - disabledStreamDisabledMessage, + accountCreditBalance, manuallySelectCRNDisabled, manuallySelectCRNDisabledMessage, createInstanceDisabled, - createInstanceDisabledMessage, createInstanceButtonTitle, values, control, @@ -127,10 +71,7 @@ export default function NewInstancePage({ mainRef }: PageProps) { handleCheckTermsAndConditions, } = useNewInstancePage() - const sectionNumber = useCallback( - (n: number) => (values.paymentMethod === PaymentMethod.Stream ? 1 : 0) + n, - [values.paymentMethod], - ) + const { apiServer } = useSettings() // ------------------ // Handle modals @@ -294,11 +235,10 @@ export default function NewInstancePage({ mainRef }: PageProps) { ), }, ] as TableColumn[] - }, [lastVersion, setSelectedModal]) + }, [apiServer, lastVersion, setSelectedModal]) const nodeData = useMemo(() => (node ? [node] : []), [node]) const manuallySelectButtonRef = useRef(null) - const manuallySelectButtonRef2 = useRef(null) return ( <> @@ -316,85 +256,63 @@ export default function NewInstancePage({ mainRef }: PageProps) {
- {createInstanceDisabledMessage && ( -
- - - {createInstanceDisabledMessage} - - -
- )} - {values.paymentMethod === PaymentMethod.Stream && ( -
- - Select your node -

- Your instance is set up with your manually selected Compute - Resource Node (CRN), operating under the{' '} - Pay-as-you-go payment method on{' '} - {blockchainName}. This setup gives you direct - control over your resource allocation and costs, requiring - active management of your instance. To adjust your CRN or - explore different payment options, you can modify your selection - below. -

-
- - ({ className: '_active' })} - /> -
- {!node && ( - <> - - Manually select CRN - - - )} -
-
-
-
-
- )}
- + + Select your node + +

+ Your instance is set up with your manually selected Compute + Resource Node (CRN) and powered by{' '} + Aleph Cloud Credits. Credits are pre-purchased + using fiat, USDC, or ALEPH and automatically deducted as your + instance consumes resources. This setup gives you full control + over your node selection, cost, and balance. To adjust your CRN or + add more credits, you can update your configuration below. +

+
+ + ({ className: '_active' })} + /> +
+ {!node && ( + <> + + Manually select CRN + + + )} +
+
+
+
+
+
+ + Select your tier - - {values.paymentMethod === PaymentMethod.Hold ? ( -

- Your instance is ready to be configured using our{' '} - automated CRN selection, set to run on{' '} - {blockchainName} with the{' '} - Holder-tier payment method, allowing you - seamless access while you hold ALEPH tokens. If you wish to - customize your Compute Resource Node (CRN) or use a different - payment approach, you can change your selection below. -

- ) : ( -

- Please select one of the available instance tiers as a base for - your VM. You will be able to customize the volumes further below - in the form. -

- )} +
+

+ Please select one of the available instance tiers as a base for + your VM. You will be able to customize the volumes further below + in the form. +

@@ -403,34 +321,12 @@ export default function NewInstancePage({ mainRef }: PageProps) { control={control} type={EntityType.Instance} isPersistent - paymentMethod={values.paymentMethod} nodeSpecs={nodeSpecs} > - {values.paymentMethod !== PaymentMethod.Stream ? ( -
- - Manually select CRN - + {!node && ( +
+ First select your node in the previous step
- ) : ( - !node && ( -
- First select your node in the previous step -
- ) )}
@@ -438,9 +334,9 @@ export default function NewInstancePage({ mainRef }: PageProps) {
- + Choose an image - +

Chose a base image for your VM. It's the base system that you will be able to customize. @@ -452,9 +348,9 @@ export default function NewInstancePage({ mainRef }: PageProps) {

- + Configure SSH Key - +

Access your cloud instances securely. Give existing key's below access to this instance or add new keys. Remember, storing @@ -468,7 +364,9 @@ export default function NewInstancePage({ mainRef }: PageProps) {

- Name and tags + + Name and tags +

Organize and identify your instances more effectively by assigning a unique name, obtaining a hash reference, and defining multiple @@ -483,9 +381,9 @@ export default function NewInstancePage({ mainRef }: PageProps) {

- + Advanced Configuration Options - +

Customize your instance with our Advanced Configuration Options. Add volumes and custom domains to meet your specific needs. @@ -530,28 +428,21 @@ export default function NewInstancePage({ mainRef }: PageProps) { control={control} address={address} cost={cost} - receiverAddress={node?.reward} - unlockedAmount={accountBalance} - paymentMethod={values.paymentMethod} - streamDuration={values.streamDuration} - disablePaymentMethod={streamDisabled} - disabledStreamTooltip={disabledStreamDisabledMessage} + unlockedAmount={accountCreditBalance} mainRef={mainRef} description={ <> - You can either leverage the traditional method of holding tokens - in your wallet for resource access, or opt for the Pay-As-You-Go - (PAYG) system, which allows you to pay precisely for what you use, - for the duration you need. The PAYG option includes a token stream - feature, enabling real-time payment for resources as you use them. + Aleph Cloud runs on a credit-based system, + designed for flexibility and transparency. You can top up credits + with fiat, USDC, or ALEPH. Your credits are + deducted only as you consume resources, ensuring you pay exactly + for what you use. } - // Duplicate buttons to have different references for the tooltip on each one button={ } - footerButton={ - - } /> diff --git a/src/components/pages/console/instance/NewInstancePage/hook.ts b/src/components/pages/console/instance/NewInstancePage/hook.ts index 430388861..aa931c825 100644 --- a/src/components/pages/console/instance/NewInstancePage/hook.ts +++ b/src/components/pages/console/instance/NewInstancePage/hook.ts @@ -7,14 +7,7 @@ import { useRef, useState, } from 'react' -import { usePaymentMethod } from '@/hooks/common/usePaymentMethod' -import { useSyncPaymentMethod } from '@/hooks/common/useSyncPaymentMethod' import Router, { useRouter } from 'next/router' -import { - createFromEVMAccount, - isAccountSupported as isAccountPAYGCompatible, - isBlockchainSupported as isBlockchainPAYGCompatible, -} from '@aleph-sdk/superfluid' import { useForm } from '@/hooks/common/useForm' import { defaultNameAndTags, @@ -43,10 +36,6 @@ import { } from '@/hooks/common/useEntityCost' import { useRequestCRNSpecs } from '@/hooks/common/useRequestEntity/useRequestCRNSpecs' import { CRNSpecs, NodeLastVersions, NodeManager } from '@/domain/node' -import { - defaultStreamDuration, - StreamDurationField, -} from '@/hooks/form/useSelectStreamDuration' import { stepsCatalog, useCheckoutNotification, @@ -54,18 +43,13 @@ import { import { EntityAddAction } from '@/store/entity' import { useConnection } from '@/hooks/common/useConnection' import Err from '@/helpers/errors' -import { BlockchainId, blockchains } from '@/domain/connect/base' -import { PaymentConfiguration } from '@/domain/executable' -import { EVMAccount } from '@aleph-sdk/evm' -import { isBlockchainHoldingCompatible } from '@/domain/blockchain' -import { ModalCardProps, TooltipProps, useModal } from '@aleph-front/core' +import { BlockchainId } from '@/domain/connect/base' import { - accountConnectionRequiredDisabledMessage, - unsupportedHoldingDisabledMessage, - unsupportedManualCRNSelectionDisabledMessage, - unsupportedStreamManualCRNSelectionDisabledMessage, - unsupportedStreamDisabledMessage, -} from './disabledMessages' + CreditPaymentConfiguration, + PaymentConfiguration, +} from '@/domain/executable' +import { ModalCardProps, TooltipProps, useModal } from '@aleph-front/core' +import { accountConnectionRequiredDisabledMessage } from './disabledMessages' import useFetchTermsAndConditions, { TermsAndConditions, } from '@/hooks/common/useFetchTermsAndConditions' @@ -83,8 +67,6 @@ export type NewInstanceFormState = NameAndTagsField & { systemVolume: InstanceSystemVolumeField nodeSpecs?: CRNSpecs paymentMethod: PaymentMethod - streamDuration: StreamDurationField - streamCost: number termsAndConditions?: string } @@ -92,15 +74,11 @@ export type Modal = 'node-list' | 'terms-and-conditions' export type UseNewInstancePageReturn = { address: string - accountBalance: number - blockchainName: string + accountCreditBalance: number manuallySelectCRNDisabled: boolean manuallySelectCRNDisabledMessage?: TooltipProps['content'] createInstanceDisabled: boolean - createInstanceDisabledMessage?: TooltipProps['content'] - createInstanceButtonTitle?: string - streamDisabled: boolean - disabledStreamDisabledMessage?: TooltipProps['content'] + createInstanceButtonTitle: string values: any control: Control errors: FieldErrors @@ -128,12 +106,11 @@ export type UseNewInstancePageReturn = { export function useNewInstancePage(): UseNewInstancePageReturn { const [, dispatch] = useAppState() - const { paymentMethod: globalPaymentMethod } = usePaymentMethod() const { blockchain, account, - balance: accountBalance = 0, + creditBalance: accountCreditBalance = 0, handleConnect, } = useConnection({ triggerOnMount: false, @@ -145,7 +122,6 @@ export function useNewInstancePage(): UseNewInstancePageReturn { const router = useRouter() const { crn: queryCRN } = router.query - const hasInitialized = useRef(false) const nodeRef = useRef(undefined) const [selectedNode, setSelectedNode] = useState() const [selectedModal, setSelectedModal] = useState() @@ -194,53 +170,33 @@ export function useNewInstancePage(): UseNewInstancePageReturn { async (state: NewInstanceFormState) => { if (!manager) throw Err.ConnectYourWallet if (!account) throw Err.InvalidAccount + if (!node) throw Err.InvalidNode + if (!nodeSpecs) throw Err.InvalidCRNSpecs - let superfluidAccount - let payment: PaymentConfiguration = { - chain: BlockchainId.ETH, - type: PaymentMethod.Hold, - } - - if (state.paymentMethod === PaymentMethod.Stream) { - if (!node || !node.stream_reward) throw Err.InvalidNode - if (!nodeSpecs) throw Err.InvalidCRNSpecs - if (!state?.streamCost) throw Err.InvalidStreamCost - - const [minSpecs] = defaultTiers - const isValid = NodeManager.validateMinNodeSpecs(minSpecs, nodeSpecs) - if (!isValid) throw Err.InvalidCRNSpecs - - if ( - !blockchain || - !isBlockchainPAYGCompatible(blockchain) || - !isAccountPAYGCompatible(account) - ) { - handleConnect({ blockchain: BlockchainId.BASE }) - throw Err.InvalidNetwork - } + const [minSpecs] = defaultTiers + const isValid = NodeManager.validateMinNodeSpecs(minSpecs, nodeSpecs) + if (!isValid) throw Err.InvalidCRNSpecs - superfluidAccount = await createFromEVMAccount(account as EVMAccount) + if (!blockchain) { + handleConnect({ blockchain: BlockchainId.BASE }) + throw Err.InvalidNetwork + } - payment = { - chain: blockchain, - type: PaymentMethod.Stream, - sender: account.address, - receiver: node.stream_reward, - streamCost: state.streamCost, - streamDuration: state.streamDuration, - } + const payment: CreditPaymentConfiguration = { + chain: blockchain, + type: PaymentMethod.Credit, } const instance = { ...state, payment, - node: state.paymentMethod === PaymentMethod.Stream ? node : undefined, + node, } as AddInstance const iSteps = await manager.getAddSteps(instance) const nSteps = iSteps.map((i) => stepsCatalog[i]) - const steps = manager.addSteps(instance, superfluidAccount) + const steps = manager.addSteps(instance) try { let accountInstance @@ -289,12 +245,10 @@ export function useNewInstancePage(): UseNewInstancePageReturn { image: defaultInstanceImage, specs: defaultTiers[0], systemVolume: { size: defaultTiers[0]?.storage }, - paymentMethod: globalPaymentMethod, - streamDuration: defaultStreamDuration, - streamCost: Number.POSITIVE_INFINITY, + paymentMethod: PaymentMethod.Credit, termsAndConditions: undefined, }), - [defaultTiers, globalPaymentMethod], + [defaultTiers], ) const { @@ -305,9 +259,7 @@ export function useNewInstancePage(): UseNewInstancePageReturn { } = useForm({ defaultValues, onSubmit, - resolver: zodResolver( - !node ? InstanceManager.addSchema : InstanceManager.addStreamSchema, - ), + resolver: zodResolver(InstanceManager.addSchema), readyDeps: [], }) @@ -319,20 +271,11 @@ export function useNewInstancePage(): UseNewInstancePageReturn { const { size: systemVolumeSize } = formValues.systemVolume const payment: PaymentConfiguration = useMemo(() => { - return formValues.paymentMethod === PaymentMethod.Stream - ? ({ - chain: blockchain, - type: PaymentMethod.Stream, - sender: account?.address, - receiver: node?.stream_reward, - streamCost: formValues?.streamCost || 1, - streamDuration: formValues?.streamDuration, - } as PaymentConfiguration) - : ({ - chain: blockchain, - type: PaymentMethod.Hold, - } as PaymentConfiguration) - }, [formValues, blockchain, account, node]) + return { + chain: blockchain, + type: PaymentMethod.Credit, + } as CreditPaymentConfiguration + }, [blockchain]) const costProps: UseInstanceCostProps = useMemo( () => ({ @@ -342,7 +285,6 @@ export function useNewInstancePage(): UseNewInstancePageReturn { specs: formValues.specs, volumes: formValues.volumes, domains: formValues.domains, - streamDuration: formValues.streamDuration, paymentMethod: formValues.paymentMethod, payment, isPersistent: true, @@ -363,19 +305,8 @@ export function useNewInstancePage(): UseNewInstancePageReturn { // Memos const shouldRequestTermsAndConditions = useMemo(() => { - return ( - !!node?.terms_and_conditions && - formValues.paymentMethod === PaymentMethod.Stream - ) - }, [node, formValues.paymentMethod]) - - const blockchainName = useMemo(() => { - return blockchain ? blockchains[blockchain]?.name : 'Current network' - }, [blockchain]) - - // No longer disable payment method switching - allow switching regardless of connection state - const disabledStreamDisabledMessage = undefined - const streamDisabled = false + return !!node?.terms_and_conditions + }, [node]) const address = useMemo(() => account?.address || '', [account]) @@ -385,15 +316,7 @@ export function useNewInstancePage(): UseNewInstancePageReturn { return accountConnectionRequiredDisabledMessage( 'manually selecting CRNs', ) - - if (!isAccountPAYGCompatible(account)) - return unsupportedStreamManualCRNSelectionDisabledMessage( - blockchainName, - ) - - if (formValues.paymentMethod === PaymentMethod.Hold) - return unsupportedManualCRNSelectionDisabledMessage() - }, [account, blockchainName, formValues.paymentMethod]) + }, [account]) const manuallySelectCRNDisabled = useMemo(() => { return !!manuallySelectCRNDisabledMessage @@ -401,7 +324,7 @@ export function useNewInstancePage(): UseNewInstancePageReturn { const { canAfford, isCreateButtonDisabled } = useCanAfford({ cost, - accountBalance, + accountCreditBalance, }) // Checks if user can afford with current balance @@ -411,34 +334,17 @@ export function useNewInstancePage(): UseNewInstancePageReturn { return canAfford }, [account, canAfford, isCreateButtonDisabled]) - const createInstanceDisabledMessage: UseNewInstancePageReturn['createInstanceDisabledMessage'] = - useMemo(() => { - // Checks configuration for PAYG tier - if (formValues.paymentMethod === PaymentMethod.Stream) { - if (!isBlockchainPAYGCompatible(blockchain)) - return unsupportedStreamDisabledMessage(blockchainName) - } - - // Checks configuration for Holder tier - if (formValues.paymentMethod === PaymentMethod.Hold) { - if (!isBlockchainHoldingCompatible(blockchain)) - return unsupportedHoldingDisabledMessage(blockchainName) - } - }, [blockchain, blockchainName, formValues.paymentMethod]) - const createInstanceButtonTitle: UseNewInstancePageReturn['createInstanceButtonTitle'] = useMemo(() => { if (!account) return 'Connect' - if (!hasEnoughBalance) return 'Insufficient ALEPH' + if (!hasEnoughBalance) return 'Insufficient Credits' return 'Create instance' }, [account, hasEnoughBalance]) const createInstanceDisabled = useMemo(() => { - if (createInstanceButtonTitle !== 'Create instance') return true - - return !!createInstanceDisabledMessage - }, [createInstanceButtonTitle, createInstanceDisabledMessage]) + return createInstanceButtonTitle !== 'Create instance' + }, [createInstanceButtonTitle]) // ------------------------- // Handlers @@ -491,16 +397,6 @@ export function useNewInstancePage(): UseNewInstancePageReturn { // ------------------------- // Effects - // @note: First time the user loads the page, set payment method to Stream if CRN is present - useEffect(() => { - if (hasInitialized.current) return - if (!router.isReady) return - - hasInitialized.current = true - - if (queryCRN) setValue('paymentMethod', PaymentMethod.Stream) - }, [queryCRN, router.isReady, setValue]) - // @note: Updates url depending on payment method useEffect(() => { if (!node) return @@ -509,12 +405,9 @@ export function useNewInstancePage(): UseNewInstancePageReturn { const { crn, ...rest } = Router.query Router.replace({ - query: - formValues.paymentMethod === PaymentMethod.Hold - ? { ...rest } - : { ...rest, crn: node.hash }, + query: { ...rest, crn: node.hash }, }) - }, [node, formValues.paymentMethod]) + }, [node]) const prevStorage = usePrevious(storage) @@ -536,27 +429,10 @@ export function useNewInstancePage(): UseNewInstancePageReturn { setValue('nodeSpecs', nodeSpecs) }, [nodeSpecs, setValue]) - // @note: Set streamCost - useEffect(() => { - if (!cost) return - if (cost.paymentMethod !== PaymentMethod.Stream) return - if (formValues.streamCost === cost.cost) return - - setValue('streamCost', cost.cost) - }, [cost, setValue, formValues]) - - // Sync form payment method with global state - useSyncPaymentMethod({ - formPaymentMethod: formValues.paymentMethod, - setValue, - }) - return { address, - accountBalance, - blockchainName, + accountCreditBalance, createInstanceDisabled, - createInstanceDisabledMessage, createInstanceButtonTitle, manuallySelectCRNDisabled, manuallySelectCRNDisabledMessage, @@ -567,8 +443,6 @@ export function useNewInstancePage(): UseNewInstancePageReturn { node, lastVersion, nodeSpecs, - streamDisabled, - disabledStreamDisabledMessage, selectedModal, setSelectedModal, selectedNode, diff --git a/src/components/pages/console/sshKey/NewSSHKeyPage/cmp.tsx b/src/components/pages/console/sshKey/NewSSHKeyPage/cmp.tsx index 1cdd8975c..4c94bce8a 100644 --- a/src/components/pages/console/sshKey/NewSSHKeyPage/cmp.tsx +++ b/src/components/pages/console/sshKey/NewSSHKeyPage/cmp.tsx @@ -4,7 +4,7 @@ import { NoisyContainer } from '@aleph-front/core' import Form from '@/components/form/Form' import { useNewSSHKeyPage } from './hook' import { Button, TextArea, TextInput } from '@aleph-front/core' -import { SectionTitle } from '@/components/common/CompositeTitle' +import { CompositeSectionTitle } from '@/components/common/CompositeTitle' import BackButtonSection from '@/components/common/BackButtonSection' export default function NewSSHKey() { @@ -24,7 +24,9 @@ export default function NewSSHKey() {

- Configure SSH Key + + Configure SSH Key +

Access your cloud instances securely. Give existing key’s below access to this instance or add new keys. Remember, storing private diff --git a/src/components/pages/console/volume/ManageVolume/cmp.tsx b/src/components/pages/console/volume/ManageVolume/cmp.tsx index 8eaff0844..99267edd3 100644 --- a/src/components/pages/console/volume/ManageVolume/cmp.tsx +++ b/src/components/pages/console/volume/ManageVolume/cmp.tsx @@ -1,4 +1,3 @@ -import ButtonLink from '@/components/common/ButtonLink' import Head from 'next/head' import { useManageVolume } from './hook' import { CenteredContainer } from '@/components/common/CenteredContainer' @@ -22,11 +21,11 @@ export default function ManageVolume() { -

+ {/*
Create new volume -
+
*/}
diff --git a/src/components/pages/console/volume/NewVolumePage/cmp.tsx b/src/components/pages/console/volume/NewVolumePage/cmp.tsx index dc100dfd2..a7ec627cc 100644 --- a/src/components/pages/console/volume/NewVolumePage/cmp.tsx +++ b/src/components/pages/console/volume/NewVolumePage/cmp.tsx @@ -1,12 +1,11 @@ import React from 'react' import Head from 'next/head' -import { PaymentMethod } from '@/helpers/constants' import { useNewVolumePage } from './hook' import CheckoutSummary from '@/components/form/CheckoutSummary' import { CenteredContainer } from '@/components/common/CenteredContainer' import { AddNewVolume } from '@/components/form/AddVolume' import { Form } from '@/components/form/Form' -import { SectionTitle } from '@/components/common/CompositeTitle' +import { CompositeSectionTitle } from '@/components/common/CompositeTitle' import BackButtonSection from '@/components/common/BackButtonSection' import ButtonWithInfoTooltip from '@/components/common/ButtonWithInfoTooltip' import { insufficientFundsDisabledMessage } from './disabledMessages' @@ -15,7 +14,7 @@ export function NewVolumePage() { const { control, address, - accountBalance, + accountCreditBalance, isCreateButtonDisabled, errors, cost, @@ -36,15 +35,14 @@ export function NewVolumePage() {
- Add volume + Add volume
This amount needs to be present in your wallet until the volume is diff --git a/src/components/pages/console/volume/NewVolumePage/hook.ts b/src/components/pages/console/volume/NewVolumePage/hook.ts index 84f2d0844..d5969b467 100644 --- a/src/components/pages/console/volume/NewVolumePage/hook.ts +++ b/src/components/pages/console/volume/NewVolumePage/hook.ts @@ -30,7 +30,7 @@ export const defaultValues: NewVolumeFormState = { export type UseNewVolumePageReturn = { address: string - accountBalance: number + accountCreditBalance: number isCreateButtonDisabled: boolean values: any control: Control @@ -43,7 +43,8 @@ export type UseNewVolumePageReturn = { export function useNewVolumePage(): UseNewVolumePageReturn { const router = useRouter() const [appState, dispatch] = useAppState() - const { account, balance: accountBalance = 0 } = appState.connection + const { account, creditBalance: accountCreditBalance = 0 } = + appState.connection const manager = useVolumeManager() const { next, stop } = useCheckoutNotification({}) @@ -107,7 +108,7 @@ export function useNewVolumePage(): UseNewVolumePageReturn { const { isCreateButtonDisabled } = useCanAfford({ cost, - accountBalance, + accountCreditBalance, }) const handleBack = () => { @@ -116,7 +117,7 @@ export function useNewVolumePage(): UseNewVolumePageReturn { return { address: account?.address || '', - accountBalance, + accountCreditBalance, isCreateButtonDisabled, values, control, diff --git a/src/components/pages/console/volume/VolumeDashboardPage/cmp.tsx b/src/components/pages/console/volume/VolumeDashboardPage/cmp.tsx index ebddc98b9..4f4bd5cd3 100644 --- a/src/components/pages/console/volume/VolumeDashboardPage/cmp.tsx +++ b/src/components/pages/console/volume/VolumeDashboardPage/cmp.tsx @@ -44,7 +44,7 @@ export default function VolumeDashboardPage() { { title: 'Volumes', img: EntityTypeObject[EntityType.Volume], - buttonUrl: '/console/storage/volume/new', + // buttonUrl: '/console/storage/volume/new', information: { type: 'storage', data: total, @@ -78,7 +78,7 @@ export default function VolumeDashboardPage() { <> {!!volumes.length && ( - + )} ) : ( diff --git a/src/components/pages/console/volume/VolumesTabContent/cmp.tsx b/src/components/pages/console/volume/VolumesTabContent/cmp.tsx index 5e377228d..fb01a1f3a 100644 --- a/src/components/pages/console/volume/VolumesTabContent/cmp.tsx +++ b/src/components/pages/console/volume/VolumesTabContent/cmp.tsx @@ -6,6 +6,7 @@ import { ellipseAddress, humanReadableSize } from '@/helpers/utils' import EntityTable from '@/components/common/EntityTable' import { Icon } from '@aleph-front/core' import { NAVIGATION_URLS } from '@/helpers/constants' +import ExternalLink from '@/components/common/ExternalLink' export const VolumesTabContent = ({ data, @@ -19,8 +20,10 @@ export const VolumesTabContent = ({ rowNoise rowKey={(row) => row.id} data={data} + // eslint-disable-next-line @typescript-eslint/no-unused-vars rowProps={(row) => ({ - css: row.confirmed ? '' : tw`opacity-60`, + // css: row.confirmed ? '' : tw`opacity-60`, + css: tw`opacity-40`, })} columns={[ { @@ -44,15 +47,35 @@ export const VolumesTabContent = ({ { label: '', align: 'right', - render: (row) => ( - - - - ), + render: (row) => { + return ( + + To manage this volume, go to the{' '} + +

+ } + tooltipPosition={{ + my: 'bottom-right', + at: 'bottom-center', + }} + > + +
+ ) + }, cellProps: () => ({ css: tw`pl-3!`, }), diff --git a/src/components/pages/console/website/ManageWebsite/cmp.tsx b/src/components/pages/console/website/ManageWebsite/cmp.tsx index adb30accd..04ceeb7eb 100644 --- a/src/components/pages/console/website/ManageWebsite/cmp.tsx +++ b/src/components/pages/console/website/ManageWebsite/cmp.tsx @@ -10,7 +10,7 @@ import { TextGradient, useCopyToClipboardAndNotify, } from '@aleph-front/core' -import { EntityTypeName, NAVIGATION_URLS } from '@/helpers/constants' +import { EntityTypeName } from '@/helpers/constants' import { useManageWebsite } from './hook' import { humanReadableSize } from '@/helpers/utils' import { Text, Separator } from '../../common' @@ -20,7 +20,6 @@ import { WebsiteFrameworks } from '@/domain/website' import { getDate, cidV0Tov1 } from '@/helpers/utils' import UpdateWebsiteFolder from '@/components/form/UpdateWebsiteFolder' import { Volume } from '@/domain/volume' -import ButtonLink from '@/components/common/ButtonLink' import IconText from '@/components/common/IconText' import BackButtonSection from '@/components/common/BackButtonSection' @@ -372,14 +371,14 @@ export function ManageWebsite() { -
+ {/*
Create new website -
+
*/}
diff --git a/src/components/pages/console/website/NewWebsitePage/cmp.tsx b/src/components/pages/console/website/NewWebsitePage/cmp.tsx index 10bb35583..3273f89dd 100644 --- a/src/components/pages/console/website/NewWebsitePage/cmp.tsx +++ b/src/components/pages/console/website/NewWebsitePage/cmp.tsx @@ -1,16 +1,12 @@ import Head from 'next/head' -import { - EntityType, - EntityDomainType, - PaymentMethod, -} from '@/helpers/constants' +import { EntityType, EntityDomainType } from '@/helpers/constants' import { useNewWebsitePage } from '@/components/pages/console/website/NewWebsitePage/hook' import { Button, TextGradient } from '@aleph-front/core' import CheckoutSummary from '@/components/form/CheckoutSummary' import { CenteredContainer } from '@/components/common/CenteredContainer' import AddWebsiteFolder from '@/components/form/AddWebsiteFolder' import { Form } from '@/components/form/Form' -import { SectionTitle } from '@/components/common/CompositeTitle' +import { CompositeSectionTitle } from '@/components/common/CompositeTitle' import AddNameAndTags from '@/components/form/AddNameAndTags' import SwitchToggleContainer from '@/components/common/SwitchToggleContainer' import AddDomains from '@/components/form/AddDomains' @@ -22,7 +18,7 @@ export default function NewWebsitePage({ mainRef }: PageProps) { const { control, address, - accountBalance, + accountCreditBalance, isCreateButtonDisabled, errors, cost, @@ -43,7 +39,9 @@ export default function NewWebsitePage({ mainRef }: PageProps) {
- Choose your framework + + Choose your framework +

Select your web development framework. This step provides guidance to properly configure your dapp, before building your project @@ -56,7 +54,9 @@ export default function NewWebsitePage({ mainRef }: PageProps) {

- Upload your website + + Upload your website +

Once your website is ready, upload your static folder here. This step transitions your local project to our decentralized cloud, @@ -69,7 +69,9 @@ export default function NewWebsitePage({ mainRef }: PageProps) {

- Name and tags + + Name and tags +

Organize and identify your websites more effectively by assigning a unique name, obtaining a hash reference, and defining multiple @@ -82,9 +84,9 @@ export default function NewWebsitePage({ mainRef }: PageProps) {

- + Advanced Configuration Options - +

Customize your website with our Advanced Configuration Options. Add custom domains or ENS domains to meet your specific needs. @@ -127,8 +129,7 @@ export default function NewWebsitePage({ mainRef }: PageProps) { control={control} address={address} cost={cost} - unlockedAmount={accountBalance} - paymentMethod={PaymentMethod.Hold} + unlockedAmount={accountCreditBalance} mainRef={mainRef} description={ <> diff --git a/src/components/pages/console/website/NewWebsitePage/hook.ts b/src/components/pages/console/website/NewWebsitePage/hook.ts index 382fc78e4..7efa642ba 100644 --- a/src/components/pages/console/website/NewWebsitePage/hook.ts +++ b/src/components/pages/console/website/NewWebsitePage/hook.ts @@ -39,12 +39,12 @@ export type NewWebsiteFormState = NameAndTagsField & export const defaultValues: Partial = { ...defaultNameAndTags, - paymentMethod: PaymentMethod.Hold, + paymentMethod: PaymentMethod.Credit, } export type UseNewWebsitePagePageReturn = { address: string - accountBalance: number + accountCreditBalance: number isCreateButtonDisabled: boolean values: any control: Control @@ -57,7 +57,8 @@ export type UseNewWebsitePagePageReturn = { export function useNewWebsitePage(): UseNewWebsitePagePageReturn { const router = useRouter() const [appState, dispatch] = useAppState() - const { account, balance: accountBalance = 0 } = appState.connection + const { account, creditBalance: accountCreditBalance = 0 } = + appState.connection const manager = useWebsiteManager() const { next, stop } = useCheckoutNotification({}) @@ -69,7 +70,7 @@ export function useNewWebsitePage(): UseNewWebsitePagePageReturn { // @todo: Refactor this const payment: WebsitePayment = { chain: BlockchainId.ETH, - type: PaymentMethod.Hold, + type: PaymentMethod.Credit, } const website = { @@ -137,7 +138,7 @@ export function useNewWebsitePage(): UseNewWebsitePagePageReturn { const { isCreateButtonDisabled } = useCanAfford({ cost, - accountBalance, + accountCreditBalance, }) const handleBack = () => { @@ -152,7 +153,7 @@ export function useNewWebsitePage(): UseNewWebsitePagePageReturn { return { address: account?.address || '', - accountBalance, + accountCreditBalance, isCreateButtonDisabled, values, control, diff --git a/src/components/pages/console/website/WebsiteDashboardPage/cmp.tsx b/src/components/pages/console/website/WebsiteDashboardPage/cmp.tsx index 5ebd9ce06..c94980280 100644 --- a/src/components/pages/console/website/WebsiteDashboardPage/cmp.tsx +++ b/src/components/pages/console/website/WebsiteDashboardPage/cmp.tsx @@ -44,9 +44,13 @@ export default function WebsiteDashboardPage() { info="HOW TO..." title="Host your Website!" description="Build and deploy your website effortlessly using our web3 hosting solutions. Support for static pages, Next.js, React, and Vue.js ensures you have the flexibility to create the perfect site." - withButton={websites?.length === 0} - buttonUrl={NAVIGATION_URLS.console.web3Hosting.website.new} - buttonText="Deploy your website" + // withButton={websites?.length === 0} + // buttonUrl={NAVIGATION_URLS.console.web3Hosting.website.new} + // buttonText="Deploy your website" + externalLinkText="Create on Legacy console" + externalLinkUrl={ + NAVIGATION_URLS.legacyConsole.web3Hosting.website.home + } /> ) : tabId === 'domain' ? ( diff --git a/src/components/pages/console/website/WebsitesTabContent/cmp.tsx b/src/components/pages/console/website/WebsitesTabContent/cmp.tsx index d7a78868c..afb8d3cb5 100644 --- a/src/components/pages/console/website/WebsitesTabContent/cmp.tsx +++ b/src/components/pages/console/website/WebsitesTabContent/cmp.tsx @@ -1,13 +1,19 @@ -import React from 'react' +import React, { useCallback } from 'react' import tw from 'twin.macro' import { WebsitesTabContentProps } from './types' import ButtonLink from '@/components/common/ButtonLink' import EntityTable from '@/components/common/EntityTable' import { Icon } from '@aleph-front/core' -import { NAVIGATION_URLS } from '@/helpers/constants' +import { NAVIGATION_URLS, PaymentMethod } from '@/helpers/constants' +import { Website } from '@/domain/website' +import ExternalLink from '@/components/common/ExternalLink' export const WebsitesTabContent = React.memo( ({ data }: WebsitesTabContentProps) => { + const isCredit = useCallback((row: Website) => { + return row.payment?.type === PaymentMethod.Credit + }, []) + return ( <> {data.length > 0 ? ( @@ -18,8 +24,10 @@ export const WebsitesTabContent = React.memo( rowNoise rowKey={(row) => row.id} data={data} + // eslint-disable-next-line @typescript-eslint/no-unused-vars rowProps={(row) => ({ - css: row.confirmed ? '' : tw`opacity-60`, + // css: row.confirmed ? '' : tw`opacity-60`, + css: tw`opacity-40`, })} columns={[ { @@ -52,15 +60,39 @@ export const WebsitesTabContent = React.memo( { label: '', align: 'right', - render: (row) => ( - - - - ), + render: (row) => { + const disabled = !isCredit(row) + + return ( + + To manage this website, go to the{' '} + +

+ ) + } + tooltipPosition={{ + my: 'bottom-right', + at: 'bottom-center', + }} + > + + + ) + }, cellProps: () => ({ css: tw`pl-3!`, }), @@ -68,14 +100,14 @@ export const WebsitesTabContent = React.memo( ]} />

-
+ {/*
Create website -
+
*/} ) : (
diff --git a/src/domain/confidential.ts b/src/domain/confidential.ts index b27043bee..767d036ba 100644 --- a/src/domain/confidential.ts +++ b/src/domain/confidential.ts @@ -23,7 +23,7 @@ import { SuperfluidAccount } from '@aleph-sdk/superfluid' export type Confidential = Omit & { type: EntityType.GpuInstance payment: Payment & { - type: PaymentType.superfluid + type: PaymentType.superfluid | PaymentType.credit } } diff --git a/src/domain/connect/base.ts b/src/domain/connect/base.ts index 7a9648aef..f7d6f829b 100644 --- a/src/domain/connect/base.ts +++ b/src/domain/connect/base.ts @@ -196,7 +196,7 @@ export abstract class BaseConnectionProviderManager { const blockchain = blockchainId || (await this.getBlockchain()) const account = await this.getAccount() - const balance = await this.getBalance(account) + const { balance } = await this.getBalance(account) this.events.emit('update', { provider: this.providerId, @@ -279,7 +279,9 @@ export abstract class BaseConnectionProviderManager { return account } - async getBalance(account: Account): Promise { + async getBalance( + account: Account, + ): Promise<{ balance: number; creditBalance: number }> { return getAccountBalance(account, PaymentMethod.Hold) } diff --git a/src/domain/cost.ts b/src/domain/cost.ts index 458b91cc4..6952c7288 100644 --- a/src/domain/cost.ts +++ b/src/domain/cost.ts @@ -59,10 +59,12 @@ export type PriceTypeObject = { storage: { payg: string holding: string + credit: string } computeUnit: { payg: string holding: string + credit: string } } tiers: { @@ -81,6 +83,7 @@ export type PricingAggregate = Record export type SettingsAggregate = { compatibleGpus: GPUDevice[] + lastCrnVersion: string communityWalletAddress: string communityWalletTimestamp: number } diff --git a/src/domain/executable.ts b/src/domain/executable.ts index d18ea29f5..e0d62c6f8 100644 --- a/src/domain/executable.ts +++ b/src/domain/executable.ts @@ -59,6 +59,11 @@ export type HoldPaymentConfiguration = { type: PaymentMethod.Hold } +export type CreditPaymentConfiguration = { + chain: BlockchainId + type: PaymentMethod.Credit +} + export type StreamPaymentConfiguration = { chain: BlockchainId type: PaymentMethod.Stream @@ -71,6 +76,7 @@ export type StreamPaymentConfiguration = { export type PaymentConfiguration = | HoldPaymentConfiguration | StreamPaymentConfiguration + | CreditPaymentConfiguration export type Executable = BaseExecutableContent & { type: @@ -298,7 +304,6 @@ export abstract class ExecutableManager { const nodes = await this.nodeManager.getAllCRNsSpecs() // @note: 1) Try to filter the node by the requirements field on the executable message (legacy messages doesn't contain it) - let node = nodes.find( (node) => node.hash === executable.requirements?.node?.node_hash, ) @@ -312,6 +317,17 @@ export abstract class ExecutableManager { return node } + if (executable.payment?.type === PaymentType.credit) { + const nodes = await this.nodeManager.getAllCRNsSpecs() + + // Try to filter the node by the requirements field on the executable message (legacy messages doesn't contain it) + const node = nodes.find( + (node) => node.hash === executable.requirements?.node?.node_hash, + ) + + return node + } + const query = await fetch( `https://scheduler.api.aleph.sh/api/v0/allocation/${executable.id}`, ) @@ -862,7 +878,10 @@ export abstract class ExecutableManager { } else { return { chain: payment?.chain || BlockchainId.ETH, - type: SDKPaymentType.hold, + type: + payment?.type === PaymentMethod.Credit + ? SDKPaymentType.credit + : SDKPaymentType.hold, } } } @@ -883,6 +902,12 @@ export abstract class ExecutableManager { } throw Err.StreamNotSupported } + if (payment.type === PaymentMethod.Credit) { + return { + chain: payment.chain, + type: SDKPaymentType.credit, + } + } return { chain: payment.chain, type: SDKPaymentType.hold, @@ -921,13 +946,22 @@ export abstract class ExecutableManager { {} as Record, ) - const paymentMethod = - costs.payment_type === PaymentType.hold - ? PaymentMethod.Hold - : PaymentMethod.Stream - - const costProp = - paymentMethod === PaymentMethod.Hold ? 'cost_hold' : 'cost_stream' + let paymentMethod: PaymentMethod + let costProp: 'cost_hold' | 'cost_stream' | 'cost_credit' + + switch (costs.payment_type) { + case PaymentType.hold: + paymentMethod = PaymentMethod.Hold + costProp = 'cost_hold' + break + case PaymentType.superfluid: + paymentMethod = PaymentMethod.Stream + costProp = 'cost_stream' + break + default: + paymentMethod = PaymentMethod.Credit + costProp = 'cost_credit' + } // Execution diff --git a/src/domain/file.ts b/src/domain/file.ts index 98bdbb404..8193c3eb7 100644 --- a/src/domain/file.ts +++ b/src/domain/file.ts @@ -1,4 +1,5 @@ -import { apiServer, channel, defaultConsoleChannel } from '@/helpers/constants' +import { channel, defaultConsoleChannel } from '@/helpers/constants' +import { apiServer } from '@/helpers/server' import { Mutex, convertByteUnits } from '@/helpers/utils' import { Account } from '@aleph-sdk/account' import { diff --git a/src/domain/gpuInstance.ts b/src/domain/gpuInstance.ts index 11bc61ea9..54298adda 100644 --- a/src/domain/gpuInstance.ts +++ b/src/domain/gpuInstance.ts @@ -21,7 +21,7 @@ export type GpuInstanceCost = CostSummary export type GpuInstance = Omit & { type: EntityType.GpuInstance payment: Payment & { - type: PaymentType.superfluid + type: PaymentType.superfluid | PaymentType.credit } } diff --git a/src/domain/instance.ts b/src/domain/instance.ts index bd726cd4b..0e2d3d091 100644 --- a/src/domain/instance.ts +++ b/src/domain/instance.ts @@ -37,12 +37,8 @@ import { DomainField } from '@/hooks/form/useAddDomains' import { DomainManager } from './domain' import { EntityManager } from './types' import { ForwardedPortsManager } from './forwardedPorts' -import { - instanceSchema, - instanceStreamSchema, -} from '@/helpers/schemas/instance' +import { instanceSchema } from '@/helpers/schemas/instance' import { NameAndTagsField } from '@/hooks/form/useAddNameAndTags' -import { getHours } from '@/hooks/form/useSelectStreamDuration' import { CRNSpecs, NodeManager } from './node' import { CheckoutStepType } from '@/hooks/form/useCheckoutNotification' import { @@ -111,7 +107,6 @@ export class InstanceManager implements EntityManager { static addSchema = instanceSchema - static addStreamSchema = instanceStreamSchema constructor( protected account: Account | undefined, @@ -252,14 +247,12 @@ export class InstanceManager // @note: Send the instance creation message to the network yield + const response = await this.sdkClient.createInstance({ ...instanceMessage, }) const [entity] = await this.parseMessages([response]) - // @note: Create PAYG superfluid flows - yield* this.addPAYGStreamSteps(newInstance, account) - // @note: Add the domain link yield* this.parseDomainsSteps(entity.id, newInstance.domains) @@ -457,7 +450,7 @@ export class InstanceManager | EntityType.GpuInstance = EntityType.Instance, ): Promise { let totalCost = Number.POSITIVE_INFINITY - const paymentMethod = newInstance.payment?.type || PaymentMethod.Hold + const paymentMethod = newInstance.payment?.type || PaymentMethod.Credit const parsedInstance: InstancePublishConfiguration = await this.parseInstanceForCostEstimation(newInstance) @@ -571,67 +564,10 @@ export class InstanceManager return instance } - protected async *addPAYGStreamSteps( - newInstance: AddInstance, - account?: SuperfluidAccount, - ): AsyncGenerator { - if (newInstance.payment?.type !== PaymentMethod.Stream) return - if (!newInstance.node || !newInstance.node.address) throw Err.InvalidNode - if (!account) throw Err.ConnectYourWallet - - const { streamCost, streamDuration, receiver } = newInstance.payment - - const { communityWalletAddress } = - await this.costManager.getSettingsAggregate() - - const costByHour = streamCost / getHours(streamDuration) - const streamCostByHourToReceiver = this.calculateReceiverFlow(costByHour) - const streamCostByHourToCommunity = this.calculateCommunityFlow(costByHour) - - const alephxBalance = await account.getALEPHBalance() - const recieverAlephxFlow = await account.getALEPHFlow(receiver) - const communityAlephxFlow = await account.getALEPHFlow( - communityWalletAddress, - ) - - const receiverTotalFlow = recieverAlephxFlow.add(streamCostByHourToReceiver) - const communityTotalFlow = communityAlephxFlow.add( - streamCostByHourToCommunity, - ) - - if ( - receiverTotalFlow.greaterThan(100) || - communityTotalFlow.greaterThan(100) - ) - throw Err.MaxFlowRate - - const totalAlephxFlow = recieverAlephxFlow.add(communityAlephxFlow) - const usedAlephInDuration = totalAlephxFlow.mul(getHours(streamDuration)) - const totalRequiredAleph = usedAlephInDuration.add(streamCost) - - if (alephxBalance.lt(totalRequiredAleph)) - throw Err.InsufficientBalance( - totalRequiredAleph.sub(alephxBalance).toNumber(), - ) - - yield - - // Split the stream cost between the community wallet (20%) and the receiver (80%) - await account.increaseALEPHFlow( - communityWalletAddress, - streamCostByHourToCommunity + EXTRA_WEI, - ) - await account.increaseALEPHFlow( - receiver, - streamCostByHourToReceiver + EXTRA_WEI, - ) - } - protected async *addPAYGReservationSteps( newInstance: AddInstance, instanceMessage: InstancePublishConfiguration, ): AsyncGenerator { - if (newInstance.payment?.type !== PaymentMethod.Stream) return if (!newInstance.node || !newInstance.node.address) throw Err.InvalidNode yield @@ -642,7 +578,6 @@ export class InstanceManager newInstance: AddInstance, entity: InstanceEntity, ): AsyncGenerator { - if (newInstance.payment?.type !== PaymentMethod.Stream) return if (!newInstance.node || !newInstance.node.address) throw Err.InvalidNode yield @@ -692,9 +627,7 @@ export class InstanceManager ): AsyncGenerator { if (!this.account) throw Err.InvalidAccount - const schema = !newInstance.node - ? InstanceManager.addSchema - : InstanceManager.addStreamSchema + const schema = InstanceManager.addSchema newInstance = await schema.parseAsync(newInstance) diff --git a/src/domain/message.ts b/src/domain/message.ts index 9937d9e1a..9e7e080f8 100644 --- a/src/domain/message.ts +++ b/src/domain/message.ts @@ -4,7 +4,8 @@ import { AlephHttpClient, AuthenticatedAlephHttpClient, } from '@aleph-sdk/client' -import { defaultConsoleChannel, apiServer } from '@/helpers/constants' +import { defaultConsoleChannel } from '@/helpers/constants' +import { apiServer } from '@/helpers/server' import Err from '@/helpers/errors' import { GetMessagesConfiguration, MessageType } from '@aleph-sdk/message' diff --git a/src/domain/node.ts b/src/domain/node.ts index 939485fe6..dfb5abe2d 100644 --- a/src/domain/node.ts +++ b/src/domain/node.ts @@ -1,14 +1,13 @@ import { - apiServer, crnListProgramUrl, defaultAccountChannel, monitorAddress, postType, scoringAddress, tags, - wsServer, channel, } from '@/helpers/constants' +import { apiServer, wsServer } from '@/helpers/server' import { Account } from '@aleph-sdk/account' import { AlephHttpClient, diff --git a/src/domain/program.ts b/src/domain/program.ts index 4fee2d578..14fd0453f 100644 --- a/src/domain/program.ts +++ b/src/domain/program.ts @@ -271,7 +271,7 @@ export class ProgramManager async getCost(newProgram: ProgramCostProps): Promise { let totalCost = Number.POSITIVE_INFINITY - const paymentMethod = newProgram.payment?.type || PaymentMethod.Hold + const paymentMethod = newProgram.payment?.type || PaymentMethod.Credit const parsedProgram: ProgramPublishConfiguration = await this.parseProgramForCostEstimation(newProgram) diff --git a/src/domain/stake.ts b/src/domain/stake.ts index 275692a5f..870aa1620 100644 --- a/src/domain/stake.ts +++ b/src/domain/stake.ts @@ -1,14 +1,13 @@ import { Account } from '@aleph-sdk/account' import { - apiServer, channel, defaultConsoleChannel, monitorAddress, postType, senderAddress, tags, - wsServer, } from '@/helpers/constants' +import { apiServer, wsServer } from '@/helpers/server' import { AlephNode, CCN, CRN } from './node' import { normalizeValue } from '@/helpers/utils' import { ItemType, PostMessage } from '@aleph-sdk/message' diff --git a/src/domain/volume.ts b/src/domain/volume.ts index 1a5113d8a..dde0862b1 100644 --- a/src/domain/volume.ts +++ b/src/domain/volume.ts @@ -1,5 +1,10 @@ import { Account } from '@aleph-sdk/account' -import { MessageCostLine, MessageType, StoreContent } from '@aleph-sdk/message' +import { + MessageCostLine, + MessageType, + PaymentType, + StoreContent, +} from '@aleph-sdk/message' import Err from '@/helpers/errors' import { EntityType, @@ -28,6 +33,7 @@ import { } from '@aleph-sdk/client' import { CostLine, CostSummary } from './cost' import { mockAccount } from './account' +import { Blockchain } from '@aleph-sdk/core' export const mockVolumeRef = 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe' @@ -312,7 +318,7 @@ export class VolumeManager implements EntityManager { async getCost(props: VolumeCostProps): Promise { let totalCost = Number.POSITIVE_INFINITY - const { volume, paymentMethod = PaymentMethod.Hold } = props + const { volume, paymentMethod = PaymentMethod.Credit } = props const emptyCost = { paymentMethod, @@ -332,6 +338,7 @@ export class VolumeManager implements EntityManager { const costs = await this.sdkClient.storeClient.getEstimatedCost({ account, fileObject: newVolume.file, + payment: { chain: Blockchain.ETH, type: PaymentType.credit }, }) totalCost = Number(costs.cost) @@ -359,7 +366,9 @@ export class VolumeManager implements EntityManager { cost: paymentMethod === PaymentMethod.Hold ? +line.cost_hold - : +line.cost_stream, + : paymentMethod === PaymentMethod.Stream + ? +line.cost_stream + : +line.cost_credit, })) } } diff --git a/src/domain/website.ts b/src/domain/website.ts index 4c1402f3e..c21c91712 100644 --- a/src/domain/website.ts +++ b/src/domain/website.ts @@ -616,7 +616,7 @@ export class WebsiteManager implements EntityManager { async getCost(props: WebsiteCostProps): Promise { let totalCost = Number.POSITIVE_INFINITY - const { website, paymentMethod = PaymentMethod.Hold } = props + const { website, paymentMethod = PaymentMethod.Credit } = props const emptyCost: WebsiteCost = { paymentMethod, @@ -660,7 +660,9 @@ export class WebsiteManager implements EntityManager { cost: paymentMethod === PaymentMethod.Hold ? +line.cost_hold - : +line.cost_stream, + : paymentMethod === PaymentMethod.Stream + ? +line.cost_stream + : +line.cost_credit, })) } diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index 82b71895c..d7222a9d8 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -1,6 +1,7 @@ // ------------------------ @todo: Refactor in domain package -------------------- import { ObjectImgProps } from '@aleph-front/core' +import { apiServer } from './server' export const defaultAccountChannel = 'ALEPH-ACCOUNT' @@ -9,8 +10,6 @@ export const channel = 'FOUNDATION' export const tags = ['mainnet'] export const postType = 'corechan-operation' -export const apiServer = 'https://api.aleph.im' -export const wsServer = 'wss://api.aleph.im' export const mbPerAleph = 3 export const communityWalletAddress = @@ -179,6 +178,7 @@ export const EntityTypeObject: Record = { export enum PaymentMethod { Hold = 'hold', Stream = 'stream', + Credit = 'credit', } export const superToken = '0x1290248E01ED2F9f863A9752A8aAD396ef3a1B00' @@ -196,7 +196,55 @@ export enum WebsiteFrameworkId { export const EXTRA_WEI = 3600 / 10 ** 18 +const LEGACY_CONSOLE_DOMAIN = 'https://app.aleph.cloud/console' export const NAVIGATION_URLS = { + legacyConsole: { + home: `${LEGACY_CONSOLE_DOMAIN}`, + settings: { + home: `${LEGACY_CONSOLE_DOMAIN}/settings`, + ssh: { + home: `${LEGACY_CONSOLE_DOMAIN}/settings/ssh`, + new: `${LEGACY_CONSOLE_DOMAIN}/settings/ssh/new`, + }, + domain: { + home: `${LEGACY_CONSOLE_DOMAIN}/settings/domain`, + new: `${LEGACY_CONSOLE_DOMAIN}/settings/domain/new`, + }, + }, + web3Hosting: { + home: `${LEGACY_CONSOLE_DOMAIN}/hosting/`, + website: { + home: `${LEGACY_CONSOLE_DOMAIN}/hosting/website`, + new: `${LEGACY_CONSOLE_DOMAIN}/hosting/website/new`, + }, + }, + computing: { + home: `${LEGACY_CONSOLE_DOMAIN}/computing`, + functions: { + home: `${LEGACY_CONSOLE_DOMAIN}/computing/function`, + new: `${LEGACY_CONSOLE_DOMAIN}/computing/function/new`, + }, + instances: { + home: `${LEGACY_CONSOLE_DOMAIN}/computing/instance`, + new: `${LEGACY_CONSOLE_DOMAIN}/computing/instance/new`, + }, + gpus: { + home: `${LEGACY_CONSOLE_DOMAIN}/computing/gpu-instance`, + new: `${LEGACY_CONSOLE_DOMAIN}/computing/gpu-instance/new`, + }, + confidentials: { + home: `${LEGACY_CONSOLE_DOMAIN}/computing/confidential`, + new: `${LEGACY_CONSOLE_DOMAIN}/computing/confidential/new`, + }, + }, + storage: { + home: `${LEGACY_CONSOLE_DOMAIN}/storage`, + volumes: { + home: `${LEGACY_CONSOLE_DOMAIN}/storage/volume`, + new: `${LEGACY_CONSOLE_DOMAIN}/storage/volume/new`, + }, + }, + }, console: { home: '/console', settings: { diff --git a/src/helpers/schemas/base.ts b/src/helpers/schemas/base.ts index 588501e5f..c6bb565cf 100644 --- a/src/helpers/schemas/base.ts +++ b/src/helpers/schemas/base.ts @@ -174,6 +174,7 @@ export const targetSchema = z.enum([ export const paymentMethodSchema = z.enum([ PaymentMethod.Hold, PaymentMethod.Stream, + PaymentMethod.Credit, ]) export const blockchainSchema = z.enum([ diff --git a/src/helpers/schemas/instance.ts b/src/helpers/schemas/instance.ts index f21bf67fe..dc19bc61f 100644 --- a/src/helpers/schemas/instance.ts +++ b/src/helpers/schemas/instance.ts @@ -107,15 +107,6 @@ export const addSSHKeysSchema = z path: ['0.isSelected'], }) -// STREAM DURATION - -export const streamDurationUnitSchema = z.enum(['h', 'd', 'm', 'y']) - -export const streamDurationSchema = z.object({ - duration: z.coerce.number(), - unit: streamDurationUnitSchema, -}) - export const systemVolumeSchema = z.object({ size: z .number() @@ -146,16 +137,10 @@ export const instanceBaseSchema = z }) .merge(addNameAndTagsSchema) -export const instanceSchema = instanceBaseSchema.superRefine( - checkMinInstanceSystemVolumeSize, -) - -export const instanceStreamSchema = instanceBaseSchema +export const instanceSchema = instanceBaseSchema .merge( z.object({ nodeSpecs: nodeSpecsSchema, - streamDuration: streamDurationSchema, - streamCost: z.number(), }), ) .refine( diff --git a/src/helpers/server.ts b/src/helpers/server.ts new file mode 100644 index 000000000..0e1abf99a --- /dev/null +++ b/src/helpers/server.ts @@ -0,0 +1,48 @@ +const defaultApiServer = 'https://api.aleph.im' +const defaultWsServer = 'wss://api.aleph.im' + +/** + * Normalizes API server input by adding protocol if missing + * Assumes https:// by default unless http:// is explicitly specified + */ +export const normalizeApiServer = (server: string): string => { + const trimmed = server.trim() + if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { + return trimmed + } + return `https://${trimmed}` +} + +/** + * Returns the API server for display purposes + * Strips https:// and wss:// (secure protocols assumed by default) + * Shows http:// and ws:// for insecure connections + */ +export const getApiServerDisplay = (server: string): string => { + if (server.startsWith('https://')) { + return server.replace('https://', '') + } + return server +} + +/** + * Gets the full API server URL from localStorage or default + */ +export const getApiServer = (): string => { + if (typeof window === 'undefined') return defaultApiServer + const stored = localStorage.getItem('apiServer') + if (!stored) return defaultApiServer + return normalizeApiServer(stored) +} + +/** + * Gets the WebSocket server URL based on the API server + */ +export const getWsServer = (): string => { + const apiServer = getApiServer() + if (apiServer === defaultApiServer) return defaultWsServer + return apiServer.replace('https://', 'wss://').replace('http://', 'ws://') +} + +export const apiServer = getApiServer() +export const wsServer = getWsServer() diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 5c1946ad9..cd1794df8 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -9,7 +9,8 @@ import { StoreMessage, } from '@aleph-sdk/message' import { MachineVolume } from '@aleph-sdk/message' -import { EntityType, PaymentMethod, apiServer } from './constants' +import { EntityType, PaymentMethod } from './constants' +import { apiServer } from './server' import { SSHKey } from '../domain/ssh' import { Instance } from '../domain/instance' import { Volume } from '@/domain/volume' @@ -56,7 +57,7 @@ export const ellipseAddress = (address: string) => { * Get the Aleph balance for a given blockchain address * * @param address An blockchain address - * returns The Aleph balance of the address + * returns The Aleph Tokens and Credit balance of the address */ export const getAddressBalance = async (address: string) => { try { @@ -65,10 +66,14 @@ export const getAddressBalance = async (address: string) => { ) // @note: 404 means the balance is 0, don't throw error in that case - if (query.status === 404) return 0 + if (query.status === 404) return { balance: 0, creditBalance: 0 } - const { balance } = await query.json() - return balance + const { balance, credit_balance } = await query.json() + + return { + balance: balance as number, + creditBalance: credit_balance as number, + } } catch (error) { throw Err.RequestFailed(error) } @@ -78,7 +83,7 @@ export async function getAccountBalance( account: Account, paymentMethod: PaymentMethod, ) { - let balance: number + let balance = 0 if (paymentMethod === PaymentMethod.Stream && isAccountSupported(account)) { try { @@ -92,12 +97,14 @@ export async function getAccountBalance( console.error(e) balance = 0 } - } else { - // For Hold payment method, fetch balance from pyaleph API - balance = await getAddressBalance(account.address) } - return balance + const addressBalance = await getAddressBalance(account.address) + + return { + balance: balance || addressBalance.balance, + creditBalance: addressBalance.creditBalance || 0, + } } export function round(num: number, decimals = 2) { @@ -651,6 +658,34 @@ export function getVersionNumber(version: string): number { } } +/** + * Compares two semantic version strings to determine if the first version + * is greater than or equal to the second version. + * + * @param version - The version to check (e.g., "1.8.5") + * @param minVersion - The minimum required version (e.g., "1.7.2") + * @returns true if version >= minVersion, false otherwise + * + * @example + * compareVersion("1.8.5", "1.7.2") // true + * compareVersion("1.7.0", "1.7.2") // false + * compareVersion("2.0.0", "1.7.2") // true + */ +export function compareVersion(version: string, minVersion: string): boolean { + const v1Parts = version.split('.').map(Number) + const v2Parts = minVersion.split('.').map(Number) + + for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { + const v1 = v1Parts[i] || 0 + const v2 = v2Parts[i] || 0 + + if (v1 > v2) return true + if (v1 < v2) return false + } + + return true +} + /** * An util for controlling concurrency (lock / unlock) forcing sequential access * to some region of the code diff --git a/src/hooks/common/node/useFileToImg.ts b/src/hooks/common/node/useFileToImg.ts index f122e6513..dfc76aacd 100644 --- a/src/hooks/common/node/useFileToImg.ts +++ b/src/hooks/common/node/useFileToImg.ts @@ -1,6 +1,6 @@ -import { apiServer } from '@/helpers/constants' import { fileToImg } from '@/helpers/utils' import { useEffect, useState } from 'react' +import { useSettings } from '@/hooks/common/useSettings' export type UseFileToImgProps = { file?: File | string @@ -11,6 +11,7 @@ export type UseFileToImgReturn = { } export function useFileToImg({ file }: UseFileToImgProps): UseFileToImgReturn { + const { apiServer } = useSettings() const [img, setImg] = useState() useEffect(() => { @@ -37,7 +38,7 @@ export function useFileToImg({ file }: UseFileToImgProps): UseFileToImgReturn { } load() - }, [file]) + }, [file, apiServer]) return { img } } diff --git a/src/hooks/common/useCRNList.ts b/src/hooks/common/useCRNList.ts index 0a8ccea5a..3d7f6f3c8 100644 --- a/src/hooks/common/useCRNList.ts +++ b/src/hooks/common/useCRNList.ts @@ -9,6 +9,7 @@ import { CRNSpecs, StreamNotSupportedIssue } from '@/domain/node' import { DropdownProps, useDebounceState, + useLocalRequest, usePaginatedList, } from '@aleph-front/core' import { @@ -18,6 +19,8 @@ import { import { useDefaultTiers } from './pricing/useDefaultTiers' import { useRequestCRNLastVersion } from './useRequestEntity/useRequestCRNLastVersion' import { EntityType } from '@/helpers/constants' +import { useCostManager } from './useManager/useCostManager' +import { compareVersion } from '@/helpers/utils' export type StreamSupportedIssues = Record @@ -59,17 +62,36 @@ export function useCRNList(props: UseCRNListProps): UseCRNListReturn { const { enableGpu } = props const nodeManager = useNodeManager() + const costManager = useCostManager() const { specs: crnSpecs, loading: isLoadingSpecs } = useRequestCRNSpecs() const { lastVersion, loading: isLoadingLastVersion } = useRequestCRNLastVersion() const [isLoadingList, setIsLoadingList] = useState(true) + const { data: settingsAggregate, loading: isLoadingSettings } = + useLocalRequest({ + doRequest: () => { + return costManager + ? costManager.getSettingsAggregate() + : Promise.resolve(undefined) + }, + onSuccess: () => null, + onError: () => null, + flushData: true, + triggerOnMount: true, + triggerDeps: [costManager], + }) + // ----------------------------- // Loading CRN List const loading = useMemo( - () => isLoadingSpecs || isLoadingLastVersion || isLoadingList, - [isLoadingLastVersion, isLoadingList, isLoadingSpecs], + () => + isLoadingSpecs || + isLoadingLastVersion || + isLoadingList || + isLoadingSettings, + [isLoadingLastVersion, isLoadingList, isLoadingSpecs, isLoadingSettings], ) // ----------------------------- @@ -228,16 +250,26 @@ export function useCRNList(props: UseCRNListProps): UseCRNListReturn { // ----------------------------- - const validPAYGNodes = useMemo(() => { + const validCreditNodes = useMemo(() => { if (!baseFilteredNodes) return if (!nodesIssues) return baseFilteredNodes + if (!settingsAggregate?.lastCrnVersion) return baseFilteredNodes + + const minVersion = settingsAggregate.lastCrnVersion - return baseFilteredNodes.filter((node) => !nodesIssues[node.hash]) - }, [baseFilteredNodes, nodesIssues]) + return baseFilteredNodes.filter((node) => { + if (nodesIssues[node.hash]) return false + + const nodeVersion = node.version || '' + if (!nodeVersion) return false + + return compareVersion(nodeVersion, minVersion) + }) + }, [baseFilteredNodes, nodesIssues, settingsAggregate]) const filteredNodes = useMemo(() => { try { - return validPAYGNodes?.filter((node) => { + return validCreditNodes?.filter((node) => { if (gpuFilter) { if (node.selectedGpu?.model !== gpuFilter) return false } @@ -261,7 +293,7 @@ export function useCRNList(props: UseCRNListProps): UseCRNListReturn { } finally { setIsLoadingList(false) } - }, [gpuFilter, cpuFilter, hddFilter, ramFilter, validPAYGNodes]) + }, [gpuFilter, cpuFilter, hddFilter, ramFilter, validCreditNodes]) const sortedNodes = useMemo(() => { if (!filteredNodes) return diff --git a/src/hooks/common/useCanAfford.ts b/src/hooks/common/useCanAfford.ts index 1239d1c08..6bf11c686 100644 --- a/src/hooks/common/useCanAfford.ts +++ b/src/hooks/common/useCanAfford.ts @@ -1,8 +1,8 @@ -import { CostSummary } from '@/domain/cost' +import { UseEntityCostReturn } from './useEntityCost' export type UseCanAffordProps = { - accountBalance: number - cost?: CostSummary + accountCreditBalance: number + cost: UseEntityCostReturn } export type UseCanAffordReturn = { @@ -11,11 +11,12 @@ export type UseCanAffordReturn = { } export function useCanAfford({ - accountBalance, + accountCreditBalance, cost, }: UseCanAffordProps): UseCanAffordReturn { const canAfford = - accountBalance >= (cost ? cost.cost : Number.MAX_SAFE_INTEGER) + accountCreditBalance >= + (cost.cost ? cost.cost.cost : Number.MAX_SAFE_INTEGER) const isCreateButtonDisabled = process.env.NEXT_PUBLIC_OVERRIDE_ALEPH_BALANCE === 'true' diff --git a/src/hooks/common/useEntity/useManageInstanceEntity.ts b/src/hooks/common/useEntity/useManageInstanceEntity.ts index 121151c38..c3c53b6de 100644 --- a/src/hooks/common/useEntity/useManageInstanceEntity.ts +++ b/src/hooks/common/useEntity/useManageInstanceEntity.ts @@ -7,6 +7,7 @@ import { useExecutableActions, } from '@/hooks/common/useExecutableActions' import { + CreditPaymentData, HoldingPaymentData, PaymentData, StreamPaymentData, @@ -217,6 +218,17 @@ export function useManageInstanceEntity< }) as StreamPaymentData, ) } + case PaymentType.credit: + return [ + { + cost, + paymentType: PaymentType.credit, + runningTime, + startTime: entity.time, + blockchain: entity.payment.chain, + loading: loadingPaymentData, + } as CreditPaymentData, + ] default: return [ { diff --git a/src/hooks/common/useEntityCost.ts b/src/hooks/common/useEntityCost.ts index a80566bd7..64651c8dc 100644 --- a/src/hooks/common/useEntityCost.ts +++ b/src/hooks/common/useEntityCost.ts @@ -46,20 +46,24 @@ export type UseEntityCostProps = | UseProgramCostProps | UseWebsiteCostProps -export type UseEntityCostReturn = CostSummary +export type UseEntityCostReturn = { + cost: CostSummary + loading: boolean +} export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { // Use useMemo to prevent the object from being recreated on every render const emptyCost = useMemo( () => ({ - paymentMethod: PaymentMethod.Hold, + paymentMethod: PaymentMethod.Credit, cost: Number.POSITIVE_INFINITY, lines: [], }), [], ) - const [cost, setCost] = useState(emptyCost) + const [cost, setCost] = useState(emptyCost) + const [loading, setLoading] = useState(false) const volumeManager = useVolumeManager() const instanceManager = useInstanceManager() @@ -76,6 +80,12 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { // Store previous debounced string to detect changes const prevDebouncedPropsString = usePrevious(debouncedPropsString) + // Track if we're waiting for debounce or actively fetching + const isDebouncing = useMemo( + () => propsString !== debouncedPropsString, + [debouncedPropsString, propsString], + ) + // Return the original props only when the debounced string changes const debouncedProps = useMemo(() => { // Check if the debounced string has changed @@ -93,29 +103,36 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { // Skip if debouncedProps is undefined (no change detected) if (!debouncedProps) return - let result: CostSummary = emptyCost - const { entityType, props } = debouncedProps - - switch (entityType) { - case EntityType.Volume: - if (volumeManager) result = await volumeManager.getCost(props) - break - case EntityType.Instance: - if (instanceManager) result = await instanceManager.getCost(props) - break - case EntityType.GpuInstance: - if (gpuInstanceManager) - result = await gpuInstanceManager.getCost(props) - break - case EntityType.Program: - if (programManager) result = await programManager.getCost(props) - break - case EntityType.Website: - if (websiteManager) result = await websiteManager.getCost(props) - break + try { + setLoading(true) + let result: CostSummary = emptyCost + const { entityType, props } = debouncedProps + + switch (entityType) { + case EntityType.Volume: + if (volumeManager) result = await volumeManager.getCost(props) + break + case EntityType.Instance: + if (instanceManager) result = await instanceManager.getCost(props) + break + case EntityType.GpuInstance: + if (gpuInstanceManager) + result = await gpuInstanceManager.getCost(props) + break + case EntityType.Program: + if (programManager) result = await programManager.getCost(props) + break + case EntityType.Website: + if (websiteManager) result = await websiteManager.getCost(props) + break + } + + setCost(result) + } catch (e) { + console.error('Error fetching entity cost:', e) + } finally { + setLoading(false) } - - setCost(result) } load() @@ -129,5 +146,7 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { websiteManager, ]) - return cost + const isLoading = loading || isDebouncing + + return { cost, loading: isLoading } } diff --git a/src/hooks/common/useExecutableActions.ts b/src/hooks/common/useExecutableActions.ts index d5b953f70..abaea16e5 100644 --- a/src/hooks/common/useExecutableActions.ts +++ b/src/hooks/common/useExecutableActions.ts @@ -71,6 +71,7 @@ export function useExecutableActions({ subscribeLogs, }: UseExecutableActionsProps): UseExecutableActionsReturn { const isPAYG = executable?.payment?.type === PaymentType.superfluid + const isCredit = executable?.payment?.type === PaymentType.credit const executableId = executable?.id const { status, calculatedStatus } = useExecutableStatus({ @@ -179,51 +180,79 @@ export function useExecutableActions({ const handleStart = useCallback(async () => { if (!manager) throw Err.ConnectYourWallet if (!executable) throw Err.InstanceNotFound - if (!isPAYG) throw Err.StreamNotSupported - try { - setStartLoading(true) + if (isPAYG) { + try { + setStartLoading(true) - const instanceNetwork = executable.payment?.chain - const incompatibleNetwork = checkNetworkCompatibility(instanceNetwork) + const instanceNetwork = executable.payment?.chain + const incompatibleNetwork = checkNetworkCompatibility(instanceNetwork) - if (incompatibleNetwork) { - throw Err.NetworkMismatch(incompatibleNetwork) - } + if (incompatibleNetwork) { + throw Err.NetworkMismatch(incompatibleNetwork) + } - if (isPAYG && !isAccountPAYGCompatible(account)) { - throw Err.ConnectYourPaymentWallet + if (isPAYG && !isAccountPAYGCompatible(account)) { + throw Err.ConnectYourPaymentWallet + } + + if (!crn) throw Err.InvalidCRNAddress + + // For PAYG, notify CRN + if (isPAYG) { + await manager.notifyCRNAllocation(crn, executable.id) + } + } catch (e) { + noti?.add({ + variant: 'error', + title: 'Error', + text: (e as Error)?.message, + }) + } finally { + setStartLoading(false) } + } else if (!isCredit) { + throw Err.StreamNotSupported + } else if (isCredit) { + try { + setStartLoading(true) - if (!crn) throw Err.ConnectYourPaymentWallet + const instanceNetwork = executable.payment?.chain + const incompatibleNetwork = checkNetworkCompatibility(instanceNetwork) + + if (incompatibleNetwork) { + throw Err.NetworkMismatch(incompatibleNetwork) + } - // For PAYG, notify CRN - if (isPAYG) { + if (!crn) throw Err.InvalidCRNAddress + + // For PAYG, notify CRN await manager.notifyCRNAllocation(crn, executable.id) + } catch (e) { + noti?.add({ + variant: 'error', + title: 'Error', + text: (e as Error)?.message, + }) + } finally { + setStartLoading(false) } - } catch (e) { - noti?.add({ - variant: 'error', - title: 'Error', - text: (e as Error)?.message, - }) - } finally { - setStartLoading(false) } }, [ - crn, + manager, executable, isPAYG, - manager, - noti, + isCredit, checkNetworkCompatibility, account, + crn, + noti, ]) const isAllocated = !!status?.ipv6Parsed const stopDisabled = useMemo(() => { - if (!isPAYG || !crn) return true + if (!crn) return true switch (calculatedStatus) { case 'v1': @@ -233,7 +262,7 @@ export function useExecutableActions({ default: return true } - }, [calculatedStatus, crn, isAllocated, isPAYG]) + }, [calculatedStatus, crn, isAllocated]) const startDisabled = useMemo(() => { if (!crn) return true diff --git a/src/hooks/common/useForwardedPorts.ts b/src/hooks/common/useForwardedPorts.ts new file mode 100644 index 000000000..1cfa2e000 --- /dev/null +++ b/src/hooks/common/useForwardedPorts.ts @@ -0,0 +1,106 @@ +import { useState, useEffect, useCallback } from 'react' +import { useForwardedPortsManager } from '@/hooks/common/useManager/useForwardedPortsManager' +import { useAppState } from '@/contexts/appState' +import { ExecutableStatus } from '@/domain/executable' +import { + getSystemPorts, + transformAPIPortsToUI, + mergePortsWithMappings, + mergePendingPortsWithAggregate, + applyPendingRemovals, +} from '@/components/common/entityData/EntityPortForwarding/utils' +import { ForwardedPort } from '@/components/common/entityData/EntityPortForwarding/types' + +export type UseForwardedPortsProps = { + entityHash?: string + executableStatus?: ExecutableStatus +} + +export type UseForwardedPortsReturn = { + ports: ForwardedPort[] + isLoading: boolean + error: string | null + reloadPorts: () => Promise +} + +/** + * Hook for fetching and managing forwarded ports for an entity + * Extracts port fetching logic to be used at parent component level + */ +export function useForwardedPorts({ + entityHash, + executableStatus, +}: UseForwardedPortsProps = {}): UseForwardedPortsReturn { + const [appState] = useAppState() + const { account } = appState.connection + const accountAddress = account?.address + + const [ports, setPorts] = useState(getSystemPorts()) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const forwardedPortsManager = useForwardedPortsManager() + + // Load existing ports for the entity + const loadPorts = useCallback(async () => { + if (!entityHash || !accountAddress || !forwardedPortsManager) { + setPorts(getSystemPorts()) + setIsLoading(false) + return + } + + setIsLoading(true) + try { + const existingPorts = + await forwardedPortsManager.getByEntityHash(entityHash) + + const aggregatePorts = existingPorts?.ports || {} + + // Merge aggregate ports with cached pending additions + const portsWithAdditions = mergePendingPortsWithAggregate( + entityHash, + accountAddress, + aggregatePorts, + ) + + // Apply pending removals + const finalPorts = applyPendingRemovals( + entityHash, + accountAddress, + portsWithAdditions, + ) + const userPorts: ForwardedPort[] = transformAPIPortsToUI(finalPorts) + + const allPorts = [...getSystemPorts(), ...userPorts] + setPorts(allPorts) + setError(null) + } catch (err) { + console.error('Failed to load ports:', err) + setError('Failed to load ports') + setPorts(getSystemPorts()) + } finally { + setIsLoading(false) + } + }, [entityHash, accountAddress, forwardedPortsManager]) + + // Load ports when dependencies change + useEffect(() => { + loadPorts() + }, [loadPorts]) + + // Update ports when executable status changes (mapped ports) + useEffect(() => { + if (executableStatus?.mappedPorts) { + setPorts((currentPorts) => + mergePortsWithMappings(currentPorts, executableStatus.mappedPorts), + ) + } + }, [executableStatus?.mappedPorts]) + + return { + ports, + isLoading, + error, + reloadPorts: loadPorts, + } +} diff --git a/src/hooks/common/usePaymentMethod.ts b/src/hooks/common/usePaymentMethod.ts index 567613652..158a8556e 100644 --- a/src/hooks/common/usePaymentMethod.ts +++ b/src/hooks/common/usePaymentMethod.ts @@ -51,10 +51,13 @@ export function usePaymentMethod({ if (!account) return try { - const balance = await getAccountBalance(account, paymentMethod) + const { balance, creditBalance } = await getAccountBalance( + account, + paymentMethod, + ) - if (balance !== undefined) { - dispatch(new ConnectionSetBalanceAction({ balance })) + if (balance !== undefined || creditBalance !== undefined) { + dispatch(new ConnectionSetBalanceAction({ balance, creditBalance })) } } catch (error) { console.error('Error fetching balance:', error) diff --git a/src/hooks/common/useRoutes.ts b/src/hooks/common/useRoutes.ts index caeab51d3..76bb96e44 100644 --- a/src/hooks/common/useRoutes.ts +++ b/src/hooks/common/useRoutes.ts @@ -71,6 +71,7 @@ export function useRoutes(): UseRoutesReturn { name: 'Manage your website', href: NAVIGATION_URLS.console.web3Hosting.website.home, icon: 'manageWebsite', + disabled: true, }, ], }, @@ -83,6 +84,7 @@ export function useRoutes(): UseRoutesReturn { name: 'Functions', href: NAVIGATION_URLS.console.computing.functions.home, icon: 'functions', + disabled: true, }, { name: 'Instances', @@ -100,6 +102,7 @@ export function useRoutes(): UseRoutesReturn { href: NAVIGATION_URLS.console.computing.confidentials.home, label: '(BETA)', icon: 'confidential', + disabled: true, }, ], }, @@ -112,6 +115,7 @@ export function useRoutes(): UseRoutesReturn { name: 'Volumes', href: NAVIGATION_URLS.console.storage.home, icon: 'storageSolutions', + disabled: true, }, ], }, diff --git a/src/hooks/common/useSettings.ts b/src/hooks/common/useSettings.ts new file mode 100644 index 000000000..4803dc22d --- /dev/null +++ b/src/hooks/common/useSettings.ts @@ -0,0 +1,39 @@ +import { useCallback, useEffect, useState } from 'react' +import { + getApiServer, + getApiServerDisplay, + normalizeApiServer, +} from '@/helpers/server' + +export type UseSettingsReturn = { + apiServer: string + apiServerDisplay: string + handleSetApiServer: (apiServer: string) => void +} + +export function useSettings(): UseSettingsReturn { + const [apiServer, setApiServer] = useState(getApiServer()) + + useEffect(() => { + const handleStorageChange = () => { + console.log('Storage changed, updating apiServer') + setApiServer(getApiServer()) + } + + window.addEventListener('storage', handleStorageChange) + return () => window.removeEventListener('storage', handleStorageChange) + }, []) + + const handleSetApiServer = useCallback((newApiServer: string) => { + const normalized = normalizeApiServer(newApiServer) + localStorage.setItem('apiServer', normalized) + setApiServer(normalized) + window.location.reload() + }, []) + + return { + apiServer, + apiServerDisplay: getApiServerDisplay(apiServer), + handleSetApiServer, + } +} diff --git a/src/hooks/common/useSyncPaymentMethod.ts b/src/hooks/common/useSyncPaymentMethod.ts index 381feec9b..b5d310748 100644 --- a/src/hooks/common/useSyncPaymentMethod.ts +++ b/src/hooks/common/useSyncPaymentMethod.ts @@ -40,7 +40,6 @@ export function useSyncPaymentMethod({ if (isUpdatingRef.current) return isUpdatingRef.current = true - console.log('Update global state when form changes', formPaymentMethod) setPaymentMethod(formPaymentMethod) setTimeout(() => (isUpdatingRef.current = false), 0) @@ -50,7 +49,6 @@ export function useSyncPaymentMethod({ useEffect(() => { if (isUpdatingRef.current) return - console.log('Update form when global state changes', globalPaymentMethod) setValue(fieldName, globalPaymentMethod) }, [fieldName, globalPaymentMethod, setValue]) } diff --git a/src/hooks/form/useAddVolume.ts b/src/hooks/form/useAddVolume.ts index d2b19068e..3387df8e7 100644 --- a/src/hooks/form/useAddVolume.ts +++ b/src/hooks/form/useAddVolume.ts @@ -8,13 +8,19 @@ export type NewVolumeStandaloneField = { file?: File } +export type ExistingVolumeStandaloneField = { + volumeType: VolumeType.Existing + mountPath?: string + refHash?: string + useLatest?: boolean +} + export type NewVolumeField = NewVolumeStandaloneField & { mountPath: string useLatest: boolean } -export type ExistingVolumeField = { - volumeType: VolumeType.Existing +export type ExistingVolumeField = ExistingVolumeStandaloneField & { mountPath: string refHash: string useLatest: boolean @@ -31,8 +37,8 @@ export type InstanceSystemVolumeField = { size: number } -export const defaultVolume: NewVolumeStandaloneField = { - volumeType: VolumeType.New, +export const defaultVolume: ExistingVolumeStandaloneField = { + volumeType: VolumeType.Existing, } export type VolumeField = @@ -313,7 +319,7 @@ export function useAddVolume({ const volumeTypeCtrl = useController({ control, name: `${n}.volumeType`, - defaultValue: VolumeType.New, + defaultValue: VolumeType.Existing, }) const handleRemove = useCallback(() => { diff --git a/src/hooks/form/useAddVolumes.ts b/src/hooks/form/useAddVolumes.ts index b3f0a0290..3fd29a49b 100644 --- a/src/hooks/form/useAddVolumes.ts +++ b/src/hooks/form/useAddVolumes.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react' import { + ExistingVolumeStandaloneField, InstanceSystemVolumeField, - NewVolumeField, VolumeField, defaultVolume as defaultStandaloneVolume, } from './useAddVolume' @@ -22,7 +22,7 @@ export type UseAddVolumesReturn = { handleRemove: (index?: number) => void } -export const defaultVolume: NewVolumeField = { +export const defaultVolume: ExistingVolumeStandaloneField = { ...defaultStandaloneVolume, mountPath: '', useLatest: false, diff --git a/src/hooks/form/useSelectInstanceSpecs.ts b/src/hooks/form/useSelectInstanceSpecs.ts index db5cde96b..82070fd21 100644 --- a/src/hooks/form/useSelectInstanceSpecs.ts +++ b/src/hooks/form/useSelectInstanceSpecs.ts @@ -1,5 +1,5 @@ import { CRNSpecs, ReducedCRNSpecs } from '@/domain/node' -import { EntityType, PaymentMethod } from '@/helpers/constants' +import { EntityType } from '@/helpers/constants' import { convertByteUnits } from '@/helpers/utils' import { useCallback, useEffect, useMemo } from 'react' import { Control, UseControllerReturn, useController } from 'react-hook-form' @@ -15,13 +15,9 @@ export type InstanceSpecsField = ReducedCRNSpecs & { export function updateSpecsStorage( specs: InstanceSpecsField, isPersistent = true, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - paymentMethod: PaymentMethod = PaymentMethod.Hold, ): InstanceSpecsField { return { ...specs, - // @todo: Reactivate it for Stream once that it is supported on backend - // disabled: paymentMethod !== PaymentMethod.Stream && isPersistent && specs.cpu >= 6, disabled: false, storage: convertByteUnits(specs.cpu * 2 * (isPersistent ? 10 : 1), { from: 'GiB', @@ -37,7 +33,6 @@ export type UseSelectInstanceSpecsProps = { type: EntityType.Instance | EntityType.GpuInstance | EntityType.Program gpuModel?: string isPersistent?: boolean - paymentMethod?: PaymentMethod nodeSpecs?: CRNSpecs } @@ -46,7 +41,6 @@ export type UseSelectInstanceSpecsReturn = { options: InstanceSpecsField[] type: EntityType.Instance | EntityType.GpuInstance | EntityType.Program isPersistent: boolean - paymentMethod: PaymentMethod nodeSpecs?: CRNSpecs } @@ -57,7 +51,6 @@ export function useSelectInstanceSpecs({ type, gpuModel, isPersistent = false, - paymentMethod = PaymentMethod.Hold, nodeSpecs, ...rest }: UseSelectInstanceSpecsProps): UseSelectInstanceSpecsReturn { @@ -68,26 +61,12 @@ export function useSelectInstanceSpecs({ const filterValidNodeSpecs = useCallback( (option: Tier) => { - if (paymentMethod === PaymentMethod.Hold) return option + if (type === EntityType.Program) return true if (!nodeSpecs) return false return nodeManager.validateMinNodeSpecs(option, nodeSpecs) }, - [nodeManager, nodeSpecs, paymentMethod], - ) - - const disableHighTiersForHolding = useCallback( - (option: Tier) => { - if (paymentMethod !== PaymentMethod.Hold) return option - if (option.cpu <= 4) return option - - return { - ...option, - disabled: true, - disabledReason: 'High tiers are only avaiable for Pay-as-you-go.', - } - }, - [paymentMethod], + [nodeManager, nodeSpecs, type], ) // Process and cache valid voucher configurations @@ -149,14 +128,8 @@ export function useSelectInstanceSpecs({ const options = useMemo(() => { return defaultTiers .filter(filterValidNodeSpecs) - .map(disableHighTiersForHolding) .map(enableTiersWithVouchers) - }, [ - defaultTiers, - filterValidNodeSpecs, - disableHighTiersForHolding, - enableTiersWithVouchers, - ]) + }, [defaultTiers, filterValidNodeSpecs, enableTiersWithVouchers]) const specsCtrl = useController({ control, @@ -184,36 +157,23 @@ export function useSelectInstanceSpecs({ // 1. No tier is selected yet // 2. Current selected tier is disabled // 3. Current selected tier is not in the options anymore - let shouldAutoSelect = !value || !valueOption || valueOption.disabled - - if (paymentMethod === PaymentMethod.Stream && !shouldAutoSelect) { - if (!nodeSpecs) return - // Cases when we should auto-select first available tier for PAYG: - // 1. Current selected tier is not compatible with the node - - shouldAutoSelect = !nodeManager.validateMinNodeSpecs(value, nodeSpecs) - } + // 4. Current selected tier is not compatible with the selected node + const shouldAutoSelect = + !value || + !valueOption || + valueOption.disabled || + (nodeSpecs && !nodeManager.validateMinNodeSpecs(value, nodeSpecs)) if (shouldAutoSelect) { const firstAvailableTier = options[0] - onChange( - updateSpecsStorage(firstAvailableTier, isPersistent, paymentMethod), - ) + onChange(updateSpecsStorage(firstAvailableTier, isPersistent)) } - }, [ - options, - nodeSpecs, - paymentMethod, - isPersistent, - value, - onChange, - nodeManager, - ]) + }, [options, nodeSpecs, isPersistent, value, onChange, nodeManager]) useEffect(() => { if (!value) return - let updatedSpecs = updateSpecsStorage(value, isPersistent, paymentMethod) + let updatedSpecs = updateSpecsStorage(value, isPersistent) if (updatedSpecs.storage === value.storage) return if (updatedSpecs.disabled) { @@ -221,14 +181,13 @@ export function useSelectInstanceSpecs({ } onChange(updatedSpecs) - }, [isPersistent, value, onChange, options, paymentMethod]) + }, [isPersistent, value, onChange, options]) return { specsCtrl, options, type, isPersistent, - paymentMethod, ...rest, } } diff --git a/src/store/connection.ts b/src/store/connection.ts index f73387b58..5f328a2e3 100644 --- a/src/store/connection.ts +++ b/src/store/connection.ts @@ -10,6 +10,7 @@ import { PaymentMethod } from '@/helpers/constants' export type ConnectionState = { account?: Account balance?: number + creditBalance?: number blockchain?: BlockchainId provider?: ProviderId paymentMethod: PaymentMethod @@ -59,6 +60,7 @@ export class ConnectionUpdateAction { provider: ProviderId blockchain: BlockchainId balance?: number + creditBalance?: number }, ) {} } @@ -68,6 +70,7 @@ export class ConnectionSetBalanceAction { constructor( public payload: { balance: number + creditBalance?: number }, ) {} } @@ -113,6 +116,9 @@ export function getConnectionReducer(): ConnectionReducer { let newProvider = provider let newBalance = (action as ConnectionUpdateAction).payload.balance || state.balance + let newCreditBalance = + (action as ConnectionUpdateAction).payload.creditBalance || + state.creditBalance // If we are switching between EVM and Solana, hardcode the provider if (currentProvider) { @@ -130,14 +136,17 @@ export function getConnectionReducer(): ConnectionReducer { } // If we are switching blockchains, reset the balance - if (currentBlockchain && currentBlockchain !== blockchain) + if (currentBlockchain && currentBlockchain !== blockchain) { newBalance = undefined + newCreditBalance = undefined + } return { ...state, ...action.payload, provider: newProvider, balance: newBalance, + creditBalance: newCreditBalance, } } case ConnectionActionType.CONNECTION_SET_BALANCE: { diff --git a/src/store/manager.ts b/src/store/manager.ts index bd40b18e8..14aca811c 100644 --- a/src/store/manager.ts +++ b/src/store/manager.ts @@ -8,7 +8,7 @@ import { MessageManager } from '@/domain/message' import { DomainManager } from '@/domain/domain' import { WebsiteManager } from '@/domain/website' import { NodeManager } from '@/domain/node' -import { apiServer } from '@/helpers/constants' +import { apiServer } from '@/helpers/server' import { AlephHttpClient, AuthenticatedAlephHttpClient,