From 038159749a0f9da1eae2a48afb0793987f17ffae Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 25 Aug 2025 13:42:53 +0200 Subject: [PATCH 01/41] wip: credits dashboard --- src/components/common/CompositeTitle/cmp.tsx | 12 +- src/components/common/ToggleDashboard/cmp.tsx | 30 +- .../account/ComputeResourceNodesPage/cmp.tsx | 10 +- .../account/CoreChannelNodesPage/cmp.tsx | 9 +- .../pages/account/StakingPage/cmp.tsx | 5 +- .../DashboardPage/CreditsDashboard/cmp.tsx | 165 ++++++++ .../DashboardPage/CreditsDashboard/index.ts | 1 + .../pages/console/DashboardPage/cmp.tsx | 366 +++++++++--------- 8 files changed, 406 insertions(+), 192 deletions(-) create mode 100644 src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx create mode 100644 src/components/pages/console/DashboardPage/CreditsDashboard/index.ts diff --git a/src/components/common/CompositeTitle/cmp.tsx b/src/components/common/CompositeTitle/cmp.tsx index ab887f745..a3dcbe515 100644 --- a/src/components/common/CompositeTitle/cmp.tsx +++ b/src/components/common/CompositeTitle/cmp.tsx @@ -1,11 +1,11 @@ -import { memo } from 'react' -import { CompositeTitle, CompositeTitleProps } from '@aleph-front/core' +import { memo, ReactNode } from 'react' +import { TextGradient } from '@aleph-front/core' -export const SectionTitle = (props: CompositeTitleProps) => { +export const SectionTitle = ({ children }: { children: ReactNode }) => { return ( - + + {children} + ) } SectionTitle.displayName = 'SectionTitle' diff --git a/src/components/common/ToggleDashboard/cmp.tsx b/src/components/common/ToggleDashboard/cmp.tsx index fbda95468..0bb7d582a 100644 --- a/src/components/common/ToggleDashboard/cmp.tsx +++ b/src/components/common/ToggleDashboard/cmp.tsx @@ -1,19 +1,41 @@ -import { ReactNode, memo, useCallback, useRef, useState } from 'react' +import { + Dispatch, + ReactNode, + SetStateAction, + memo, + useCallback, + useRef, + useState, +} 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 ref = useRef(null) @@ -57,9 +79,9 @@ export const ToggleDashboard = ({ size="md" onClick={handleToogle} tw="gap-2.5" + disabled={toggleButton.disabled} > - - open dashboard + {toggleButton.children} )} 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/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..53fcd24ca --- /dev/null +++ b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx @@ -0,0 +1,165 @@ +import React, { useState } from 'react' +import { + Button, + Icon, + NoisyContainer, + ObjectImg, + TextGradient, +} from '@aleph-front/core' +import { SectionTitle } from '@/components/common/CompositeTitle' + +import ToggleDashboard from '@/components/common/ToggleDashboard' +import StyledTable from '@/components/common/Table' +import ButtonLink from '@/components/common/ButtonLink' +import tw from 'twin.macro' + +export default function CreditsDashboard() { + const [creditsDashboardOpen, setCreditsDashboardOpen] = useState(true) + + 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: false, + }} + > +
+ +
+
+ + +
+
+

AVAILABLE

+

1080

+
+
+

RUN-RATE

+
+

34

+

+ DAYS +

+
+
+
+
+
+ +
+
+
+ +
+
+ + Purchases + + + +
+ row.id} + data={data} + columns={[ + { + label: 'STATUS', + align: 'left', + sortable: true, + render: (row) => row.status, + }, + { + 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) => ( + + ), + cellProps: () => ({ + css: tw`pl-3!`, + }), + }, + ]} + /> +
+
+
+
+ ) +} 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..3549c521d 100644 --- a/src/components/pages/console/DashboardPage/cmp.tsx +++ b/src/components/pages/console/DashboardPage/cmp.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useState } from 'react' import Head from 'next/head' -import { Icon } from '@aleph-front/core' +import { Button, Icon, NoisyContainer, ObjectImg } from '@aleph-front/core' import { useDashboardPage } from '@/components/pages/console/DashboardPage/hook' import { SectionTitle } from '@/components/common/CompositeTitle' import { CenteredContainer } from '@/components/common/CenteredContainer' @@ -17,6 +17,8 @@ import { EntityTypeObject, NAVIGATION_URLS, } from '@/helpers/constants' +import ToggleDashboard from '@/components/common/ToggleDashboard' +import CreditsDashboard from './CreditsDashboard' export default function DashboardPage() { const { @@ -87,180 +89,192 @@ 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. + + + + + + + + + + + + + + + +
+
+
+
+ + 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. + + + + + + +
+
+ + 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. + + + + + + +
+
+
From 13c274e5523031470c03dea31f55262c5e1239b7 Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 25 Aug 2025 17:18:35 +0200 Subject: [PATCH 02/41] wip: detail menu button --- .../common/DetailsMenuButton/cmp.tsx | 65 +++++++ .../common/DetailsMenuButton/index.ts | 2 + .../common/DetailsMenuButton/types.ts | 9 + .../DashboardPage/CreditsDashboard/cmp.tsx | 159 +++++++++++------- 4 files changed, 173 insertions(+), 62 deletions(-) create mode 100644 src/components/common/DetailsMenuButton/cmp.tsx create mode 100644 src/components/common/DetailsMenuButton/index.ts create mode 100644 src/components/common/DetailsMenuButton/types.ts diff --git a/src/components/common/DetailsMenuButton/cmp.tsx b/src/components/common/DetailsMenuButton/cmp.tsx new file mode 100644 index 000000000..ec80e701c --- /dev/null +++ b/src/components/common/DetailsMenuButton/cmp.tsx @@ -0,0 +1,65 @@ +import { memo, RefObject, useCallback, useRef, useState } from 'react' +import { Button, Icon, Tooltip } from '@aleph-front/core' +import Link from 'next/link' + +type MenuItem = { + label: string + href: string +} + +type DetailsMenuButtonProps = { + menuItems: MenuItem[] +} + +export const DetailsMenuButton = ({ menuItems }: DetailsMenuButtonProps) => { + const [open, setOpen] = useState(false) + const tooltipRef: RefObject = useRef(null) + + const handleClick = useCallback(() => setOpen(!open), [open, setOpen]) + const handleClose = useCallback(() => setOpen(false), [setOpen]) + + return ( + <> + + {menuItems.map((item) => ( + + {item.label} + + ))} +

+ } + my="top-right" + at="bottom-right" + margin={{ x: 0, y: 2 }} + /> + + + ) +} +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/pages/console/DashboardPage/CreditsDashboard/cmp.tsx b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx index 53fcd24ca..db74a4858 100644 --- a/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx +++ b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx @@ -10,8 +10,8 @@ import { SectionTitle } from '@/components/common/CompositeTitle' import ToggleDashboard from '@/components/common/ToggleDashboard' import StyledTable from '@/components/common/Table' -import ButtonLink from '@/components/common/ButtonLink' import tw from 'twin.macro' +import DetailsMenuButton from '@/components/common/DetailsMenuButton' export default function CreditsDashboard() { const [creditsDashboardOpen, setCreditsDashboardOpen] = useState(true) @@ -94,69 +94,104 @@ export default function CreditsDashboard() {
- row.id} - data={data} - columns={[ - { - label: 'STATUS', - align: 'left', - sortable: true, - render: (row) => row.status, - }, - { - 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) => ( - - ), - cellProps: () => ({ - css: tw`pl-3!`, - }), - }, - ]} - /> +
+ 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!`, + }), + }, + ]} + /> +
From 0a084e66c11044484cc0e93032317a5308484ce1 Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 1 Sep 2025 12:36:39 +0200 Subject: [PATCH 03/41] wip: submenu table fix --- .../common/DetailsMenuButton/cmp.tsx | 30 ++++++++++++++++++- .../DashboardPage/CreditsDashboard/cmp.tsx | 5 +++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/common/DetailsMenuButton/cmp.tsx b/src/components/common/DetailsMenuButton/cmp.tsx index ec80e701c..1c2e91651 100644 --- a/src/components/common/DetailsMenuButton/cmp.tsx +++ b/src/components/common/DetailsMenuButton/cmp.tsx @@ -1,4 +1,11 @@ -import { memo, RefObject, useCallback, useRef, useState } from 'react' +import { + memo, + RefObject, + useCallback, + useEffect, + useRef, + useState, +} from 'react' import { Button, Icon, Tooltip } from '@aleph-front/core' import Link from 'next/link' @@ -18,6 +25,27 @@ export const DetailsMenuButton = ({ menuItems }: DetailsMenuButtonProps) => { const handleClick = useCallback(() => setOpen(!open), [open, setOpen]) const handleClose = useCallback(() => setOpen(false), [setOpen]) + // Handle click outside of the tooltip to close it + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + open && + tooltipRef.current && + !tooltipRef.current.contains(event.target as Node) + ) { + setOpen(false) + } + } + + if (open) { + document.addEventListener('mousedown', handleClickOutside) + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [open]) + return ( <> From 28745278acfa2c2617fd5ddb9a8cc25b11254a23 Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 1 Sep 2025 13:59:12 +0200 Subject: [PATCH 04/41] feat: toggle dashboard states --- .../DashboardPage/CreditsDashboard/cmp.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx index 1afedbe32..84ba79bd1 100644 --- a/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx +++ b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { Button, Icon, @@ -12,9 +12,18 @@ import ToggleDashboard from '@/components/common/ToggleDashboard' import StyledTable from '@/components/common/Table' import tw from 'twin.macro' import DetailsMenuButton from '@/components/common/DetailsMenuButton' +import { useConnection } from '@/hooks/common/useConnection' export default function CreditsDashboard() { - const [creditsDashboardOpen, setCreditsDashboardOpen] = useState(true) + const [creditsDashboardOpen, setCreditsDashboardOpen] = useState(false) + const { account } = useConnection({ triggerOnMount: false }) + const isConnected = !!account + + useEffect(() => { + if (!isConnected && creditsDashboardOpen) { + setCreditsDashboardOpen(false) + } + }, [isConnected, creditsDashboardOpen]) const data = [ { @@ -47,7 +56,7 @@ export default function CreditsDashboard() { Open credits ), - disabled: false, + disabled: !isConnected, }} >
From c5fef15addba09bfa16e1f9a9b18c06859f38174 Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 2 Sep 2025 14:32:37 +0200 Subject: [PATCH 05/41] wip: checkout summary --- src/components/form/CheckoutSummary/cmp.tsx | 102 ++++++------------ .../form/CheckoutSummaryFooter/cmp.tsx | 33 +++--- 2 files changed, 50 insertions(+), 85 deletions(-) diff --git a/src/components/form/CheckoutSummary/cmp.tsx b/src/components/form/CheckoutSummary/cmp.tsx index 020686e21..cf6d5919e 100644 --- a/src/components/form/CheckoutSummary/cmp.tsx +++ b/src/components/form/CheckoutSummary/cmp.tsx @@ -185,30 +185,12 @@ export const CheckoutSummary = ({ }) 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)}
- + {unlockedAmount} + {/* */}
{nftVoucherBalance > 0 && ( @@ -274,17 +244,21 @@ export const CheckoutSummary = ({ {/*
{line.detail}
*/}
{line.detail}
- + {line.cost !== 0 ? ( - +
+ {line.cost} + / h +
) : ( + // '-' )}
@@ -324,42 +298,32 @@ export const CheckoutSummary = ({ })} */}
-
- {paymentMethod === PaymentMethod.Hold - ? 'Total' - : 'Total / h'} +
Total credits / h
+
+ + {/* */} +
+ {cost?.cost} + / h +
+
+ + +
+
Min. required
- + {/* */} +
+ {cost?.cost * 4} +
- {paymentMethod === PaymentMethod.Stream && ( - -
-
Min. required
-
- - - -
-
- )}
- {paymentMethod === PaymentMethod.Stream && receiverAddress && ( - - )} - {buttonNode &&
{buttonNode}
}
diff --git a/src/components/form/CheckoutSummaryFooter/cmp.tsx b/src/components/form/CheckoutSummaryFooter/cmp.tsx index 36b8c4789..8ecdc192f 100644 --- a/src/components/form/CheckoutSummaryFooter/cmp.tsx +++ b/src/components/form/CheckoutSummaryFooter/cmp.tsx @@ -2,8 +2,6 @@ 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' // ------------------------------------------ @@ -35,23 +33,26 @@ export const CheckoutSummaryFooter = ({ ...rest, }} > -
-
{paymentMethodSwitchNode}
+
-
- - {paymentMethod === PaymentMethod.Stream - ? 'Total per hour' - : 'Total hold'} - - +
+ 3 + + Credits / h + +
+
+ $0.30/h +
+
+ {/* -
+ /> */} {footerSubmitButtonNode}
From 3c174f773186ab659a0de30714559cedd6d8784a Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 2 Sep 2025 14:33:37 +0200 Subject: [PATCH 06/41] use staging --- src/helpers/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index 82b71895c..e53cda6c2 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -9,8 +9,8 @@ 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 apiServer = 'http://51.159.106.166:4024' +export const wsServer = 'ws://51.159.106.166:4024' export const mbPerAleph = 3 export const communityWalletAddress = From 092a9d3ef93f551c0b4cdd67a48792001bd3cd80 Mon Sep 17 00:00:00 2001 From: gmolki Date: Wed, 3 Sep 2025 12:17:02 +0200 Subject: [PATCH 07/41] fix: composite title --- src/components/common/CompositeTitle/cmp.tsx | 19 +++++++++++--- src/components/common/CompositeTitle/index.ts | 2 +- .../confidential/NewConfidentialPage/cmp.tsx | 20 ++++++++------ .../console/domain/NewDomainPage/cmp.tsx | 10 ++++--- .../console/function/ManageFunction/hook.ts | 4 +-- .../console/function/NewFunctionPage/cmp.tsx | 22 +++++++++++----- .../gpuInstance/NewGpuInstancePage/cmp.tsx | 26 +++++++++++-------- .../console/instance/NewInstancePage/cmp.tsx | 26 +++++++++++-------- .../console/sshKey/NewSSHKeyPage/cmp.tsx | 6 +++-- .../console/volume/NewVolumePage/cmp.tsx | 4 +-- .../console/website/NewWebsitePage/cmp.tsx | 18 ++++++++----- 11 files changed, 100 insertions(+), 57 deletions(-) diff --git a/src/components/common/CompositeTitle/cmp.tsx b/src/components/common/CompositeTitle/cmp.tsx index a3dcbe515..2cf726e1b 100644 --- a/src/components/common/CompositeTitle/cmp.tsx +++ b/src/components/common/CompositeTitle/cmp.tsx @@ -1,5 +1,18 @@ -import { memo, ReactNode } from 'react' -import { TextGradient } from '@aleph-front/core' +import { ReactNode } from 'react' +import { + CompositeTitle, + CompositeTitleProps, + TextGradient, +} from '@aleph-front/core' + +export const CompositeSectionTitle = (props: CompositeTitleProps) => { + return ( + + ) +} +CompositeSectionTitle.displayName = 'CompositeSectionTitle' export const SectionTitle = ({ children }: { children: ReactNode }) => { return ( @@ -9,5 +22,3 @@ export const SectionTitle = ({ children }: { children: ReactNode }) => { ) } SectionTitle.displayName = 'SectionTitle' - -export default memo(SectionTitle) as typeof SectionTitle diff --git a/src/components/common/CompositeTitle/index.ts b/src/components/common/CompositeTitle/index.ts index 1b31447c7..6f6984db6 100644 --- a/src/components/common/CompositeTitle/index.ts +++ b/src/components/common/CompositeTitle/index.ts @@ -1 +1 @@ -export { default, default as SectionTitle } from './cmp' +export { SectionTitle, CompositeSectionTitle } from './cmp' 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/ManageFunction/hook.ts b/src/components/pages/console/function/ManageFunction/hook.ts index e4e33d4d7..efaf7209e 100644 --- a/src/components/pages/console/function/ManageFunction/hook.ts +++ b/src/components/pages/console/function/ManageFunction/hook.ts @@ -188,7 +188,7 @@ export function useManageFunction(): ManageFunction { return [ { cost, - paymentType: PaymentType.hold, + paymentType: 'credit', runningTime, startTime: program.time, blockchain: program.payment.chain, @@ -198,7 +198,7 @@ export function useManageFunction(): ManageFunction { default: return [ { - paymentType: PaymentType.hold, + paymentType: 'credit', loading: true, } as PaymentData, ] diff --git a/src/components/pages/console/function/NewFunctionPage/cmp.tsx b/src/components/pages/console/function/NewFunctionPage/cmp.tsx index 4f1a195cd..ae0ccf77a 100644 --- a/src/components/pages/console/function/NewFunctionPage/cmp.tsx +++ b/src/components/pages/console/function/NewFunctionPage/cmp.tsx @@ -16,7 +16,7 @@ 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' @@ -51,7 +51,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 +63,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 +76,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 +94,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 +109,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. diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx index 18728daa2..511291c75 100644 --- a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx +++ b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx @@ -33,7 +33,7 @@ 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' @@ -318,7 +318,9 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { )}

- Selected GPU + + Selected GPU +

Your instance is configured with your manually selected GPU, operating under the Pay-as-you-go payment method @@ -356,9 +358,9 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {

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

Your instance is ready to be configured using our{' '} @@ -399,9 +401,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 +415,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 +431,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 +448,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 diff --git a/src/components/pages/console/instance/NewInstancePage/cmp.tsx b/src/components/pages/console/instance/NewInstancePage/cmp.tsx index f656a6227..01a938064 100644 --- a/src/components/pages/console/instance/NewInstancePage/cmp.tsx +++ b/src/components/pages/console/instance/NewInstancePage/cmp.tsx @@ -35,7 +35,7 @@ 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' @@ -328,7 +328,9 @@ export default function NewInstancePage({ mainRef }: PageProps) { {values.paymentMethod === PaymentMethod.Stream && (

- Select your node + + Select your node +

Your instance is set up with your manually selected Compute Resource Node (CRN), operating under the{' '} @@ -375,9 +377,9 @@ export default function NewInstancePage({ mainRef }: PageProps) { )}

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

Your instance is ready to be configured using our{' '} @@ -438,9 +440,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 +454,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 +470,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 +487,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. 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/NewVolumePage/cmp.tsx b/src/components/pages/console/volume/NewVolumePage/cmp.tsx index dc100dfd2..578dfc65a 100644 --- a/src/components/pages/console/volume/NewVolumePage/cmp.tsx +++ b/src/components/pages/console/volume/NewVolumePage/cmp.tsx @@ -6,7 +6,7 @@ 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' @@ -36,7 +36,7 @@ export function NewVolumePage() {

- Add volume + Add volume
diff --git a/src/components/pages/console/website/NewWebsitePage/cmp.tsx b/src/components/pages/console/website/NewWebsitePage/cmp.tsx index 10bb35583..245d3cf5c 100644 --- a/src/components/pages/console/website/NewWebsitePage/cmp.tsx +++ b/src/components/pages/console/website/NewWebsitePage/cmp.tsx @@ -10,7 +10,7 @@ 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' @@ -43,7 +43,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 +58,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 +73,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 +88,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. From 900a3f4217c1f996724c1372d831ecaf32c0ccff Mon Sep 17 00:00:00 2001 From: gmolki Date: Wed, 3 Sep 2025 14:58:38 +0200 Subject: [PATCH 08/41] feat: instance page only use credit --- src/components/form/CheckoutSummary/cmp.tsx | 9 - src/components/form/CheckoutSummary/types.ts | 12 +- .../form/CheckoutSummaryFooter/cmp.tsx | 2 - .../form/CheckoutSummaryFooter/types.ts | 3 - .../form/SelectInstanceSpecs/cmp.tsx | 18 +- .../form/SelectInstanceSpecs/types.ts | 3 +- .../console/instance/NewInstancePage/cmp.tsx | 191 ++++++------------ .../console/instance/NewInstancePage/hook.ts | 188 ++++------------- src/domain/cost.ts | 2 + src/domain/executable.ts | 6 + src/helpers/constants.ts | 1 + src/hooks/form/useSelectInstanceSpecs.ts | 68 ++----- 12 files changed, 127 insertions(+), 376 deletions(-) diff --git a/src/components/form/CheckoutSummary/cmp.tsx b/src/components/form/CheckoutSummary/cmp.tsx index cf6d5919e..213f4b879 100644 --- a/src/components/form/CheckoutSummary/cmp.tsx +++ b/src/components/form/CheckoutSummary/cmp.tsx @@ -11,19 +11,16 @@ import { } from './types' import { memo, useEffect, useState } from 'react' import React from 'react' -import { PaymentMethod } from '@/helpers/constants' import { VolumeManager, VolumeType } from '@/domain/volume' import InfoTooltipButton from '../../common/InfoTooltipButton' import { CenteredContainer } from '@/components/common/CenteredContainer' import { TextGradient } from '@aleph-front/core' -import SelectPaymentMethod from '@/components/form/SelectPaymentMethod' import Price from '@/components/common/Price' import CheckoutSummaryFooter from '../CheckoutSummaryFooter' import { AddWebsite, WebsiteManager } from '@/domain/website' import { useConnection } from '@/hooks/common/useConnection' import { Blockchain } from '@aleph-sdk/core' import { useNFTVoucherBalance } from '@/hooks/common/useNFTVoucherBalance' -import StreamSummary from '@/components/common/StreamSummary' const CheckoutSummaryVolumeLine = ({ volume, @@ -168,17 +165,11 @@ 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, diff --git a/src/components/form/CheckoutSummary/types.ts b/src/components/form/CheckoutSummary/types.ts index 37513f489..989519108 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,12 @@ 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 8ecdc192f..202b8ad1b 100644 --- a/src/components/form/CheckoutSummaryFooter/cmp.tsx +++ b/src/components/form/CheckoutSummaryFooter/cmp.tsx @@ -8,8 +8,6 @@ import FloatingFooter from '../FloatingFooter' export const CheckoutSummaryFooter = ({ submitButton: submitButtonNode, - paymentMethodSwitch: paymentMethodSwitchNode, - paymentMethod, mainRef: containerRef, totalCost, shouldHide = true, diff --git a/src/components/form/CheckoutSummaryFooter/types.ts b/src/components/form/CheckoutSummaryFooter/types.ts index 16b5dbcb4..12f7ae73b 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,7 @@ export type CheckoutSummaryFooterProps = Pick< FloatingFooterProps, 'shouldHide' | 'thresholdOffset' | 'deps' > & { - paymentMethod: PaymentMethod submitButton?: ReactNode - paymentMethodSwitch?: ReactNode mainRef?: RefObject totalCost?: number } 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/console/instance/NewInstancePage/cmp.tsx b/src/components/pages/console/instance/NewInstancePage/cmp.tsx index 01a938064..d62f7fed9 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 { @@ -22,12 +22,7 @@ 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, apiServer } from '@/helpers/constants' import { CenteredContainer } from '@/components/common/CenteredContainer' import { useNewInstancePage, UseNewInstancePageReturn } from './hook' import Form from '@/components/form/Form' @@ -40,7 +35,6 @@ 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( @@ -95,12 +89,9 @@ export default function NewInstancePage({ mainRef }: PageProps) { address, accountBalance, blockchainName, - streamDisabled, - disabledStreamDisabledMessage, manuallySelectCRNDisabled, manuallySelectCRNDisabledMessage, createInstanceDisabled, - createInstanceDisabledMessage, createInstanceButtonTitle, values, control, @@ -127,11 +118,6 @@ export default function NewInstancePage({ mainRef }: PageProps) { handleCheckTermsAndConditions, } = useNewInstancePage() - const sectionNumber = useCallback( - (n: number) => (values.paymentMethod === PaymentMethod.Stream ? 1 : 0) + n, - [values.paymentMethod], - ) - // ------------------ // Handle modals useEffect( @@ -298,7 +284,6 @@ export default function NewInstancePage({ mainRef }: PageProps) { const nodeData = useMemo(() => (node ? [node] : []), [node]) const manuallySelectButtonRef = useRef(null) - const manuallySelectButtonRef2 = useRef(null) return ( <> @@ -316,87 +301,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), 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 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. +

@@ -405,34 +366,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 -
- ) )}
@@ -440,7 +379,7 @@ export default function NewInstancePage({ mainRef }: PageProps) {
- + Choose an image

@@ -454,7 +393,7 @@ export default function NewInstancePage({ mainRef }: PageProps) {

- + Configure SSH Key

@@ -470,7 +409,7 @@ export default function NewInstancePage({ mainRef }: PageProps) {

- + Name and tags

@@ -487,7 +426,7 @@ export default function NewInstancePage({ mainRef }: PageProps) {

- + Advanced Configuration Options

@@ -536,10 +475,6 @@ export default function NewInstancePage({ mainRef }: PageProps) { cost={cost} receiverAddress={node?.reward} unlockedAmount={accountBalance} - paymentMethod={values.paymentMethod} - streamDuration={values.streamDuration} - disablePaymentMethod={streamDisabled} - disabledStreamTooltip={disabledStreamDisabledMessage} mainRef={mainRef} description={ <> @@ -555,7 +490,6 @@ export default function NewInstancePage({ mainRef }: PageProps) { errors: FieldErrors @@ -128,7 +107,6 @@ export type UseNewInstancePageReturn = { export function useNewInstancePage(): UseNewInstancePageReturn { const [, dispatch] = useAppState() - const { paymentMethod: globalPaymentMethod } = usePaymentMethod() const { blockchain, @@ -145,7 +123,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 +171,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 +246,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 +260,7 @@ export function useNewInstancePage(): UseNewInstancePageReturn { } = useForm({ defaultValues, onSubmit, - resolver: zodResolver( - !node ? InstanceManager.addSchema : InstanceManager.addStreamSchema, - ), + resolver: zodResolver(InstanceManager.addSchema), readyDeps: [], }) @@ -319,20 +272,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 +286,6 @@ export function useNewInstancePage(): UseNewInstancePageReturn { specs: formValues.specs, volumes: formValues.volumes, domains: formValues.domains, - streamDuration: formValues.streamDuration, paymentMethod: formValues.paymentMethod, payment, isPersistent: true, @@ -363,20 +306,13 @@ export function useNewInstancePage(): UseNewInstancePageReturn { // 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]) - // No longer disable payment method switching - allow switching regardless of connection state - const disabledStreamDisabledMessage = undefined - const streamDisabled = false - const address = useMemo(() => account?.address || '', [account]) const manuallySelectCRNDisabledMessage: UseNewInstancePageReturn['manuallySelectCRNDisabledMessage'] = @@ -385,15 +321,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 @@ -411,21 +339,6 @@ 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' @@ -435,10 +348,8 @@ export function useNewInstancePage(): UseNewInstancePageReturn { }, [account, hasEnoughBalance]) const createInstanceDisabled = useMemo(() => { - if (createInstanceButtonTitle !== 'Create instance') return true - - return !!createInstanceDisabledMessage - }, [createInstanceButtonTitle, createInstanceDisabledMessage]) + return createInstanceButtonTitle !== 'Create instance' + }, [createInstanceButtonTitle]) // ------------------------- // Handlers @@ -491,16 +402,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 +410,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 +434,11 @@ 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, createInstanceDisabled, - createInstanceDisabledMessage, createInstanceButtonTitle, manuallySelectCRNDisabled, manuallySelectCRNDisabledMessage, @@ -567,8 +449,6 @@ export function useNewInstancePage(): UseNewInstancePageReturn { node, lastVersion, nodeSpecs, - streamDisabled, - disabledStreamDisabledMessage, selectedModal, setSelectedModal, selectedNode, diff --git a/src/domain/cost.ts b/src/domain/cost.ts index 458b91cc4..f15d41954 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: { diff --git a/src/domain/executable.ts b/src/domain/executable.ts index d18ea29f5..2ebecf597 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: diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index e53cda6c2..c890456b2 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -179,6 +179,7 @@ export const EntityTypeObject: Record = { export enum PaymentMethod { Hold = 'hold', Stream = 'stream', + Credit = 'credit', } export const superToken = '0x1290248E01ED2F9f863A9752A8aAD396ef3a1B00' diff --git a/src/hooks/form/useSelectInstanceSpecs.ts b/src/hooks/form/useSelectInstanceSpecs.ts index db5cde96b..29c99fac2 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,11 @@ export function useSelectInstanceSpecs({ const filterValidNodeSpecs = useCallback( (option: Tier) => { - if (paymentMethod === PaymentMethod.Hold) return option 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], ) // Process and cache valid voucher configurations @@ -149,14 +127,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 +156,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 +180,13 @@ export function useSelectInstanceSpecs({ } onChange(updatedSpecs) - }, [isPersistent, value, onChange, options, paymentMethod]) + }, [isPersistent, value, onChange, options]) return { specsCtrl, options, type, isPersistent, - paymentMethod, ...rest, } } From 7f8f4fcfce6de640c27b8d39b3ad46d134d96d86 Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 9 Sep 2025 14:57:18 +0200 Subject: [PATCH 09/41] fix: insufficient aleph to credits --- src/components/pages/console/instance/NewInstancePage/hook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/console/instance/NewInstancePage/hook.ts b/src/components/pages/console/instance/NewInstancePage/hook.ts index e0bd514fa..786b57504 100644 --- a/src/components/pages/console/instance/NewInstancePage/hook.ts +++ b/src/components/pages/console/instance/NewInstancePage/hook.ts @@ -342,7 +342,7 @@ export function useNewInstancePage(): UseNewInstancePageReturn { const createInstanceButtonTitle: UseNewInstancePageReturn['createInstanceButtonTitle'] = useMemo(() => { if (!account) return 'Connect' - if (!hasEnoughBalance) return 'Insufficient ALEPH' + if (!hasEnoughBalance) return 'Insufficient Credits' return 'Create instance' }, [account, hasEnoughBalance]) From c42deb890882b5244cccf08ee6a5a1490ece7285 Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 9 Sep 2025 15:15:04 +0200 Subject: [PATCH 10/41] feat: gpu instance credits --- .../gpuInstance/NewGpuInstancePage/cmp.tsx | 61 ++------ .../gpuInstance/NewGpuInstancePage/hook.ts | 138 +++++------------- 2 files changed, 51 insertions(+), 148 deletions(-) diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx index 511291c75..5d8be9128 100644 --- a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx +++ b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx @@ -94,10 +94,9 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { address, accountBalance, blockchainName, - streamDisabled, - disabledStreamDisabledMessage, + manuallySelectCRNDisabled, + manuallySelectCRNDisabledMessage, createInstanceDisabled, - createInstanceDisabledMessage, createInstanceButtonTitle, values, control, @@ -123,12 +122,6 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { handleCheckTermsAndConditions, } = useNewGpuInstancePage() - const sectionNumber = useCallback( - (n: number) => (values.paymentMethod === PaymentMethod.Stream ? 1 : 0) + n, - [values.paymentMethod], - ) - - // ------------------ // Handle modals useEffect( () => { @@ -307,15 +300,6 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {

- {createInstanceDisabledMessage && ( -
- - - {createInstanceDisabledMessage} - - -
- )}
@@ -345,6 +329,8 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { kind="functional" variant="warning" size="md" + disabled={manuallySelectCRNDisabled} + tooltipContent={manuallySelectCRNDisabledMessage} onClick={handleManuallySelectCRN} > Manually select GPU @@ -358,26 +344,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. +

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

@@ -415,7 +388,7 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {

- + Configure SSH Key

@@ -431,7 +404,7 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {

- + Name and tags

@@ -448,7 +421,7 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) {

- + Advanced Configuration Options

@@ -498,10 +471,6 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { cost={cost} receiverAddress={node?.reward} unlockedAmount={accountBalance} - paymentMethod={values.paymentMethod} - streamDuration={values.streamDuration} - disablePaymentMethod={streamDisabled} - disabledStreamTooltip={disabledStreamDisabledMessage} mainRef={mainRef} description={ <> @@ -517,7 +486,6 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { errors: FieldErrors @@ -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,20 +321,24 @@ 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]) @@ -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, blockchainName, createInstanceDisabled, - createInstanceDisabledMessage, createInstanceButtonTitle, + manuallySelectCRNDisabled, + manuallySelectCRNDisabledMessage, values: formValues, control, errors, cost, node, nodeSpecs, - streamDisabled, - disabledStreamDisabledMessage, selectedModal, setSelectedModal, selectedNode, From 7406087fcfa44c20f63f03058baee909bea4f73d Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 9 Sep 2025 18:42:23 +0200 Subject: [PATCH 11/41] function with credits --- .../console/function/ManageFunction/hook.ts | 10 +++- .../console/function/NewFunctionPage/cmp.tsx | 8 +-- .../console/function/NewFunctionPage/hook.ts | 58 +++++++++++++------ .../gpuInstance/NewGpuInstancePage/cmp.tsx | 10 +--- 4 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/components/pages/console/function/ManageFunction/hook.ts b/src/components/pages/console/function/ManageFunction/hook.ts index efaf7209e..17eaef50e 100644 --- a/src/components/pages/console/function/ManageFunction/hook.ts +++ b/src/components/pages/console/function/ManageFunction/hook.ts @@ -188,7 +188,7 @@ export function useManageFunction(): ManageFunction { return [ { cost, - paymentType: 'credit', + paymentType: PaymentType.hold, runningTime, startTime: program.time, blockchain: program.payment.chain, @@ -198,9 +198,13 @@ export function useManageFunction(): ManageFunction { default: return [ { - paymentType: 'credit', + 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 ae0ccf77a..1b58cd256 100644 --- a/src/components/pages/console/function/NewFunctionPage/cmp.tsx +++ b/src/components/pages/console/function/NewFunctionPage/cmp.tsx @@ -24,7 +24,8 @@ export default function NewFunctionPage({ mainRef }: PageProps) { const { address, accountBalance, - isCreateButtonDisabled, + createFunctionDisabled, + createFunctionButtonTitle, values, control, errors, @@ -173,7 +174,6 @@ export default function NewFunctionPage({ mainRef }: PageProps) { address={address} cost={cost} unlockedAmount={accountBalance} - paymentMethod={values.paymentMethod} mainRef={mainRef} description={ <> @@ -189,11 +189,11 @@ export default function NewFunctionPage({ mainRef }: PageProps) { kind="default" size="lg" variant="primary" - disabled={isCreateButtonDisabled} + disabled={createFunctionDisabled} // @note: handleSubmit is needed on the floating footer to trigger form submit (transcluded to body) onClick={handleSubmit} > - Create function + {createFunctionButtonTitle || 'Create function'} } /> diff --git a/src/components/pages/console/function/NewFunctionPage/hook.ts b/src/components/pages/console/function/NewFunctionPage/hook.ts index c81d34df5..172314e00 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,8 @@ 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 } from '@/domain/executable' +import { BlockchainId } from '@/domain/connect/base' export type NewFunctionFormState = NameAndTagsField & { code: FunctionCodeField @@ -49,7 +50,8 @@ export type NewFunctionFormState = NameAndTagsField & { export type UseNewFunctionPage = { address: string accountBalance: number - isCreateButtonDisabled: boolean + createFunctionDisabled: boolean + createFunctionButtonTitle?: string values: any control: Control errors: FieldErrors @@ -65,6 +67,7 @@ export function useNewFunctionPage(): UseNewFunctionPage { blockchain, account, balance: accountBalance = 0, + handleConnect, } = useConnection({ triggerOnMount: false, }) @@ -75,13 +78,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 +124,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 +135,7 @@ export function useNewFunctionPage(): UseNewFunctionPage { code: { ...defaultCode } as FunctionCodeField, specs: defaultTiers[0], isPersistent: false, - paymentMethod: PaymentMethod.Hold, + paymentMethod: PaymentMethod.Credit, }), [defaultTiers], ) @@ -133,7 +144,6 @@ export function useNewFunctionPage(): UseNewFunctionPage { control, handleSubmit, formState: { errors }, - setValue, } = useForm({ defaultValues, onSubmit, @@ -160,25 +170,39 @@ export function useNewFunctionPage(): UseNewFunctionPage { const cost = useEntityCost(costProps) - const { isCreateButtonDisabled } = useCanAfford({ + const { canAfford, isCreateButtonDisabled } = useCanAfford({ cost, accountBalance, }) + // 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, + createFunctionDisabled, + createFunctionButtonTitle, values, control, errors, diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx index 5d8be9128..4f58c6956 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 { @@ -21,12 +21,7 @@ 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, apiServer } from '@/helpers/constants' import { CenteredContainer } from '@/components/common/CenteredContainer' import Form from '@/components/form/Form' import SwitchToggleContainer from '@/components/common/SwitchToggleContainer' @@ -38,7 +33,6 @@ 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' From 21569fb37866670e193df0709d3e2a3027c82a77 Mon Sep 17 00:00:00 2001 From: gmolki Date: Wed, 10 Sep 2025 09:07:12 +0200 Subject: [PATCH 12/41] checkout button --- src/components/form/CheckoutButton/cmp.tsx | 44 +++++++++++++ src/components/form/CheckoutButton/index.tsx | 1 + src/components/form/CheckoutButton/types.ts | 12 ++++ src/components/form/CheckoutSummary/types.ts | 1 - .../console/function/NewFunctionPage/cmp.tsx | 19 ++---- .../console/function/NewFunctionPage/hook.ts | 2 +- .../gpuInstance/NewGpuInstancePage/cmp.tsx | 64 +----------------- .../gpuInstance/NewGpuInstancePage/hook.ts | 2 +- .../console/instance/NewInstancePage/cmp.tsx | 65 +------------------ .../console/instance/NewInstancePage/hook.ts | 2 +- 10 files changed, 71 insertions(+), 141 deletions(-) create mode 100644 src/components/form/CheckoutButton/cmp.tsx create mode 100644 src/components/form/CheckoutButton/index.tsx create mode 100644 src/components/form/CheckoutButton/types.ts diff --git a/src/components/form/CheckoutButton/cmp.tsx b/src/components/form/CheckoutButton/cmp.tsx new file mode 100644 index 000000000..1bfffbd5d --- /dev/null +++ b/src/components/form/CheckoutButton/cmp.tsx @@ -0,0 +1,44 @@ +import ButtonWithInfoTooltip from '@/components/common/ButtonWithInfoTooltip' +import React, { useRef } from 'react' +import { CheckoutButtonProps } from './types' + +const CheckoutButton = React.memo( + ({ + disabled, + title, + tooltipContent, + isFooter, + shouldRequestTermsAndConditions, + handleRequestTermsAndConditionsAgreement, + handleSubmit, + }: CheckoutButtonProps) => { + const checkoutButtonRef = useRef(null) + + return ( + + {title} + + ) + }, +) + +export default CheckoutButton +CheckoutButton.displayName = 'CheckoutButton' diff --git a/src/components/form/CheckoutButton/index.tsx b/src/components/form/CheckoutButton/index.tsx new file mode 100644 index 000000000..24d40826a --- /dev/null +++ b/src/components/form/CheckoutButton/index.tsx @@ -0,0 +1 @@ +export { default, default as CheckoutButton } from './cmp' diff --git a/src/components/form/CheckoutButton/types.ts b/src/components/form/CheckoutButton/types.ts new file mode 100644 index 000000000..13967e04f --- /dev/null +++ b/src/components/form/CheckoutButton/types.ts @@ -0,0 +1,12 @@ +import { TooltipProps } from '@aleph-front/core' +import { FormEvent } from 'react' + +export type CheckoutButtonProps = { + disabled: boolean + title: string + tooltipContent?: TooltipProps['content'] + isFooter: boolean + shouldRequestTermsAndConditions?: boolean + handleRequestTermsAndConditionsAgreement?: () => void + handleSubmit: (e: FormEvent) => Promise +} diff --git a/src/components/form/CheckoutSummary/types.ts b/src/components/form/CheckoutSummary/types.ts index 989519108..d2ae6e7bd 100644 --- a/src/components/form/CheckoutSummary/types.ts +++ b/src/components/form/CheckoutSummary/types.ts @@ -23,7 +23,6 @@ export type CheckoutSummaryProps = { description?: ReactNode mainRef?: RefObject control?: Control - receiverAddress?: string } export type CheckoutSummarySpecsLineProps = { diff --git a/src/components/pages/console/function/NewFunctionPage/cmp.tsx b/src/components/pages/console/function/NewFunctionPage/cmp.tsx index 1b58cd256..d8ced9a35 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' @@ -19,6 +19,7 @@ import NewEntityTab from '@/components/common/NewEntityTab' 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 { @@ -183,18 +184,12 @@ export default function NewFunctionPage({ mainRef }: PageProps) { } button={ - + title={createFunctionButtonTitle} + isFooter={false} + handleSubmit={handleSubmit} + /> } /> diff --git a/src/components/pages/console/function/NewFunctionPage/hook.ts b/src/components/pages/console/function/NewFunctionPage/hook.ts index 172314e00..080a39e44 100644 --- a/src/components/pages/console/function/NewFunctionPage/hook.ts +++ b/src/components/pages/console/function/NewFunctionPage/hook.ts @@ -51,7 +51,7 @@ export type UseNewFunctionPage = { address: string accountBalance: number createFunctionDisabled: boolean - createFunctionButtonTitle?: string + createFunctionButtonTitle: string values: any control: Control errors: FieldErrors diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx index 4f58c6956..7c542735b 100644 --- a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx +++ b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx @@ -9,7 +9,6 @@ import { NodeScore, TableColumn, NoisyContainer, - TooltipProps, Checkbox, } from '@aleph-front/core' import ButtonWithInfoTooltip from '@/components/common/ButtonWithInfoTooltip' @@ -34,54 +33,8 @@ import Strong from '@/components/common/Strong' import CRNList from '../../../../common/CRNList' import BackButtonSection from '@/components/common/BackButtonSection' 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 { @@ -463,7 +416,6 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { control={control} address={address} cost={cost} - receiverAddress={node?.reward} unlockedAmount={accountBalance} mainRef={mainRef} description={ @@ -488,18 +440,6 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { handleSubmit={handleSubmit} /> } - footerButton={ - - } /> diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts b/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts index e7f47ca9c..c2e880f46 100644 --- a/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts +++ b/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts @@ -79,7 +79,7 @@ export type UseNewGpuInstancePageReturn = { manuallySelectCRNDisabled: boolean manuallySelectCRNDisabledMessage?: TooltipProps['content'] createInstanceDisabled: boolean - createInstanceButtonTitle?: string + createInstanceButtonTitle: string values: any control: Control errors: FieldErrors diff --git a/src/components/pages/console/instance/NewInstancePage/cmp.tsx b/src/components/pages/console/instance/NewInstancePage/cmp.tsx index d62f7fed9..a2a02ba0b 100644 --- a/src/components/pages/console/instance/NewInstancePage/cmp.tsx +++ b/src/components/pages/console/instance/NewInstancePage/cmp.tsx @@ -10,7 +10,6 @@ import { NodeScore, TableColumn, NoisyContainer, - TooltipProps, Checkbox, } from '@aleph-front/core' import ButtonWithInfoTooltip from '@/components/common/ButtonWithInfoTooltip' @@ -24,7 +23,7 @@ import AddNameAndTags from '@/components/form/AddNameAndTags' import CheckoutSummary from '@/components/form/CheckoutSummary' import { EntityDomainType, EntityType, apiServer } from '@/helpers/constants' 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' @@ -36,53 +35,7 @@ import Strong from '@/components/common/Strong' import CRNList from '../../../../common/CRNList' import BackButtonSection from '@/components/common/BackButtonSection' 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 { @@ -473,7 +426,6 @@ export default function NewInstancePage({ mainRef }: PageProps) { control={control} address={address} cost={cost} - receiverAddress={node?.reward} unlockedAmount={accountBalance} mainRef={mainRef} description={ @@ -485,7 +437,6 @@ export default function NewInstancePage({ mainRef }: PageProps) { feature, enabling real-time payment for resources as you use them. } - // 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 786b57504..bf6414252 100644 --- a/src/components/pages/console/instance/NewInstancePage/hook.ts +++ b/src/components/pages/console/instance/NewInstancePage/hook.ts @@ -79,7 +79,7 @@ export type UseNewInstancePageReturn = { manuallySelectCRNDisabled: boolean manuallySelectCRNDisabledMessage?: TooltipProps['content'] createInstanceDisabled: boolean - createInstanceButtonTitle?: string + createInstanceButtonTitle: string values: any control: Control errors: FieldErrors From fbf14d0d099c33d85cb64f729ae3565b069948c9 Mon Sep 17 00:00:00 2001 From: gmolki Date: Thu, 18 Sep 2025 12:23:45 +0200 Subject: [PATCH 13/41] credit for entities --- package-lock.json | 27 +++---- package.json | 4 +- src/components/common/Header/cmp.tsx | 13 +++ src/components/common/Header/hook.ts | 3 + .../common/entityData/EntityPayment/cmp.tsx | 54 +++++++------ .../common/entityData/EntityPayment/hook.ts | 16 +++- .../common/entityData/EntityPayment/types.ts | 11 ++- src/components/form/CheckoutSummary/cmp.tsx | 10 +-- .../form/CheckoutSummaryFooter/cmp.tsx | 14 ++-- .../console/function/ManageFunction/hook.ts | 12 +++ .../console/function/NewFunctionPage/hook.ts | 23 ++++-- .../gpuInstance/NewGpuInstancePage/hook.ts | 8 +- .../console/instance/NewInstancePage/cmp.tsx | 4 +- .../console/instance/NewInstancePage/hook.ts | 8 +- .../console/volume/NewVolumePage/cmp.tsx | 5 +- .../console/volume/NewVolumePage/hook.ts | 9 ++- .../console/website/NewWebsitePage/cmp.tsx | 5 +- .../console/website/NewWebsitePage/hook.ts | 13 +-- src/domain/connect/base.ts | 6 +- src/domain/cost.ts | 3 +- src/domain/executable.ts | 47 ++++++++--- src/domain/instance.ts | 75 +----------------- src/domain/program.ts | 4 +- src/domain/volume.ts | 15 +++- src/domain/website.ts | 6 +- src/helpers/schemas/base.ts | 1 + src/helpers/schemas/instance.ts | 17 +--- src/helpers/utils.ts | 24 +++--- src/hooks/common/useCanAfford.ts | 6 +- .../useEntity/useManageInstanceEntity.ts | 12 +++ src/hooks/common/useEntityCost.ts | 4 +- src/hooks/common/useExecutableActions.ts | 79 +++++++++++++------ src/hooks/common/usePaymentMethod.ts | 9 ++- src/hooks/form/useSelectInstanceSpecs.ts | 3 +- src/store/connection.ts | 11 ++- 35 files changed, 324 insertions(+), 237 deletions(-) diff --git a/package-lock.json b/package-lock.json index af7443641..493220957 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,14 @@ "name": "front-aleph-cloud", "version": "0.34.3", "dependencies": { - "@aleph-front/core": "^1.29.1", + "@aleph-front/core": "file:/../front-core/aleph-front-core-1.29.1-beta-0.tgz", "@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": "file:/../aleph-sdk-ts/packages/message/aleph-sdk-message-1.6.2-beta-2.tgz", "@aleph-sdk/solana": "^1.6.2", "@aleph-sdk/superfluid": "^1.4.5", "@fortawesome/fontawesome-svg-core": "^6.3.0", @@ -61,9 +61,10 @@ } }, "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.29.1-beta-0", + "resolved": "file:../front-core/aleph-front-core-1.29.1-beta-0.tgz", + "integrity": "sha512-ZNhvd8r7lHwEPpEmtmkhS2Jc9VMwYN412kwoVb3SE1YD/2xQ9NQX3hXUdG80EjH6P2KtxyLypKsIS/dd2tzmfg==", + "license": "ISC", "dependencies": { "@monaco-editor/react": "^4.4.6", "react-infinite-scroll-hook": "^4.1.1", @@ -241,9 +242,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==", + "version": "1.6.2-beta-2", + "resolved": "file:../aleph-sdk-ts/packages/message/aleph-sdk-message-1.6.2-beta-2.tgz", + "integrity": "sha512-SXoYWMQxbkUg09eQ4RbSz29XDQuS8Ea1EuwSk5XBp/YTQZuTvteHAm6lEyDQas8Ew8ZDr3l3FwlIChLXMf2s2A==", "license": "MIT", "dependencies": { "axios": "^1.5.1", @@ -19909,9 +19910,8 @@ }, "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": "file:../front-core/aleph-front-core-1.29.1-beta-0.tgz", + "integrity": "sha512-ZNhvd8r7lHwEPpEmtmkhS2Jc9VMwYN412kwoVb3SE1YD/2xQ9NQX3hXUdG80EjH6P2KtxyLypKsIS/dd2tzmfg==", "requires": { "@monaco-editor/react": "^4.4.6", "react-infinite-scroll-hook": "^4.1.1", @@ -20029,9 +20029,8 @@ } }, "@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": "file:../aleph-sdk-ts/packages/message/aleph-sdk-message-1.6.2-beta-2.tgz", + "integrity": "sha512-SXoYWMQxbkUg09eQ4RbSz29XDQuS8Ea1EuwSk5XBp/YTQZuTvteHAm6lEyDQas8Ew8ZDr3l3FwlIChLXMf2s2A==", "requires": { "axios": "^1.5.1", "form-data": "^4.0.0", diff --git a/package.json b/package.json index 316654897..b7aed5070 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": "file:/../front-core/aleph-front-core-1.29.1-beta-0.tgz", "@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": "file:/../aleph-sdk-ts/packages/message/aleph-sdk-message-1.6.2-beta-2.tgz", "@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/Header/cmp.tsx b/src/components/common/Header/cmp.tsx index 1ec7a8684..d0f48f27b 100644 --- a/src/components/common/Header/cmp.tsx +++ b/src/components/common/Header/cmp.tsx @@ -25,6 +25,7 @@ export const Header = () => { networks, accountAddress, accountBalance, + accountCreditBalance, accountVouchers, rewards, selectedNetwork, @@ -54,6 +55,7 @@ export const Header = () => { isMobile accountAddress={accountAddress} accountBalance={accountBalance} + accountCredits={accountCreditBalance} accountVouchers={accountVouchers} blockchains={blockchains} networks={networks} @@ -63,6 +65,11 @@ export const Header = () => { handleConnect={handleConnect} handleDisconnect={handleDisconnect} handleSwitchNetwork={handleSwitchNetwork} + Link={CustomLinkMemo} + externalUrl={{ + text: 'Legacy console', + url: 'https://app.aleph.cloud/', + }} /> ), logoHref: websiteUrl, @@ -74,6 +81,7 @@ export const Header = () => { { handleConnect={handleConnect} handleDisconnect={handleDisconnect} handleSwitchNetwork={handleSwitchNetwork} + Link={CustomLinkMemo} + externalUrl={{ + text: 'Legacy console', + url: 'https://app.aleph.cloud/', + }} /> diff --git a/src/components/common/Header/hook.ts b/src/components/common/Header/hook.ts index 3cdd832a3..fe50be7ab 100644 --- a/src/components/common/Header/hook.ts +++ b/src/components/common/Header/hook.ts @@ -20,6 +20,7 @@ import { useAccountRewards as useNodeRewards } from '../../../hooks/common/node/ export type UseHeaderReturn = UseRoutesReturn & { accountAddress?: string accountBalance?: number + accountCreditBalance?: number accountVouchers?: AccountPickerProps['accountVouchers'] | undefined networks: Network[] pathname: string @@ -41,6 +42,7 @@ export function useHeader(): UseHeaderReturn { blockchain, account, balance: accountBalance, + creditBalance: accountCreditBalance, } = state.connection const { voucherManager } = state.manager @@ -227,6 +229,7 @@ export function useHeader(): UseHeaderReturn { return { accountAddress: account?.address, accountBalance, + accountCreditBalance, accountVouchers, networks, pathname, diff --git a/src/components/common/entityData/EntityPayment/cmp.tsx b/src/components/common/entityData/EntityPayment/cmp.tsx index 338fa37f0..ad284d612 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 + ) : ( + + )} + +
)}
@@ -131,6 +136,7 @@ const PaymentCard = ({ paymentData }: { paymentData: PaymentData }) => { * Takes an array of payment data objects and renders a card for each one */ export const EntityPayment = ({ payments }: EntityPaymentProps) => { + console.log('Rendering EntityPayment with payments:', payments) return ( <>
diff --git a/src/components/common/entityData/EntityPayment/hook.ts b/src/components/common/entityData/EntityPayment/hook.ts index eee7ae4d8..e78fd4181 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]) + }, [cost, isStream, isCredit, runningTime]) // Format blockchain name const formattedBlockchain = useMemo(() => { @@ -65,12 +72,12 @@ 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]) + }, [cost, isCredit, isStream]) // Format start date const formattedStartDate = useMemo(() => { @@ -120,6 +127,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..53472b8d5 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,6 +41,7 @@ export interface EntityPaymentProps { // Formatted data returned by the hook for display export interface FormattedPaymentData { isStream: boolean + isCredit: boolean totalSpent?: string formattedBlockchain?: string formattedFlowRate?: string diff --git a/src/components/form/CheckoutSummary/cmp.tsx b/src/components/form/CheckoutSummary/cmp.tsx index 213f4b879..e0b13942a 100644 --- a/src/components/form/CheckoutSummary/cmp.tsx +++ b/src/components/form/CheckoutSummary/cmp.tsx @@ -2,6 +2,7 @@ import { ellipseAddress, convertByteUnits, humanReadableSize, + humanReadableCurrency, } from '@/helpers/utils' import { Label, StyledHoldingSummaryLine } from './styles' import { @@ -206,10 +207,7 @@ export const CheckoutSummary = ({
AVAILABLE CREDITS
CURRENT WALLET {ellipseAddress(address)}
-
- {unlockedAmount} - {/* */} -
+
{humanReadableCurrency(unlockedAmount)}
{nftVoucherBalance > 0 && ( {/* */}
- {cost?.cost} + {humanReadableCurrency(cost?.cost)} / h
@@ -307,7 +305,7 @@ export const CheckoutSummary = ({ {/* */}
- {cost?.cost * 4} + {humanReadableCurrency(cost?.cost * 4)}
diff --git a/src/components/form/CheckoutSummaryFooter/cmp.tsx b/src/components/form/CheckoutSummaryFooter/cmp.tsx index 202b8ad1b..6fe260765 100644 --- a/src/components/form/CheckoutSummaryFooter/cmp.tsx +++ b/src/components/form/CheckoutSummaryFooter/cmp.tsx @@ -3,6 +3,7 @@ import { Button, ButtonProps } from '@aleph-front/core' import { StyledSeparator } from './styles' import { CheckoutSummaryFooterProps } from './types' import FloatingFooter from '../FloatingFooter' +import { humanReadableCurrency } from '@/helpers/utils' // ------------------------------------------ @@ -35,22 +36,21 @@ export const CheckoutSummaryFooter = ({
- 3 + + {humanReadableCurrency(totalCost)} + Credits / h
-
$0.30/h -
+
*/}
- {/* */} {footerSubmitButtonNode}
diff --git a/src/components/pages/console/function/ManageFunction/hook.ts b/src/components/pages/console/function/ManageFunction/hook.ts index 17eaef50e..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,6 +196,17 @@ 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 [ { diff --git a/src/components/pages/console/function/NewFunctionPage/hook.ts b/src/components/pages/console/function/NewFunctionPage/hook.ts index 080a39e44..936813dd3 100644 --- a/src/components/pages/console/function/NewFunctionPage/hook.ts +++ b/src/components/pages/console/function/NewFunctionPage/hook.ts @@ -31,7 +31,10 @@ 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 } from '@/domain/executable' +import { + CreditPaymentConfiguration, + PaymentConfiguration, +} from '@/domain/executable' import { BlockchainId } from '@/domain/connect/base' export type NewFunctionFormState = NameAndTagsField & { @@ -49,7 +52,7 @@ export type NewFunctionFormState = NameAndTagsField & { export type UseNewFunctionPage = { address: string - accountBalance: number + accountCreditBalance: number createFunctionDisabled: boolean createFunctionButtonTitle: string values: any @@ -66,7 +69,7 @@ export function useNewFunctionPage(): UseNewFunctionPage { const { blockchain, account, - balance: accountBalance = 0, + creditBalance: accountCreditBalance = 0, handleConnect, } = useConnection({ triggerOnMount: false, @@ -152,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, @@ -162,17 +172,18 @@ 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 { canAfford, isCreateButtonDisabled } = useCanAfford({ cost, - accountBalance, + accountCreditBalance, }) // Checks if user can afford with current balance @@ -200,7 +211,7 @@ export function useNewFunctionPage(): UseNewFunctionPage { return { address: account?.address || '', - accountBalance, + accountCreditBalance, createFunctionDisabled, createFunctionButtonTitle, values, diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts b/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts index c2e880f46..a61932f21 100644 --- a/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts +++ b/src/components/pages/console/gpuInstance/NewGpuInstancePage/hook.ts @@ -74,7 +74,7 @@ export type Modal = 'node-list' | 'terms-and-conditions' export type UseNewGpuInstancePageReturn = { address: string - accountBalance: number + accountCreditBalance: number blockchainName: string manuallySelectCRNDisabled: boolean manuallySelectCRNDisabledMessage?: TooltipProps['content'] @@ -110,7 +110,7 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { const { blockchain, account, - balance: accountBalance = 0, + creditBalance: accountCreditBalance = 0, handleConnect, } = useConnection({ triggerOnMount: false, @@ -344,7 +344,7 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { const { canAfford, isCreateButtonDisabled } = useCanAfford({ cost, - accountBalance, + accountCreditBalance, }) // Checks if user can afford with current balance @@ -447,7 +447,7 @@ export function useNewGpuInstancePage(): UseNewGpuInstancePageReturn { return { address, - accountBalance, + accountCreditBalance, blockchainName, createInstanceDisabled, createInstanceButtonTitle, diff --git a/src/components/pages/console/instance/NewInstancePage/cmp.tsx b/src/components/pages/console/instance/NewInstancePage/cmp.tsx index a2a02ba0b..78d9cda88 100644 --- a/src/components/pages/console/instance/NewInstancePage/cmp.tsx +++ b/src/components/pages/console/instance/NewInstancePage/cmp.tsx @@ -40,7 +40,7 @@ import CheckoutButton from '@/components/form/CheckoutButton' export default function NewInstancePage({ mainRef }: PageProps) { const { address, - accountBalance, + accountCreditBalance, blockchainName, manuallySelectCRNDisabled, manuallySelectCRNDisabledMessage, @@ -426,7 +426,7 @@ export default function NewInstancePage({ mainRef }: PageProps) { control={control} address={address} cost={cost} - unlockedAmount={accountBalance} + unlockedAmount={accountCreditBalance} mainRef={mainRef} description={ <> diff --git a/src/components/pages/console/instance/NewInstancePage/hook.ts b/src/components/pages/console/instance/NewInstancePage/hook.ts index bf6414252..3919e5817 100644 --- a/src/components/pages/console/instance/NewInstancePage/hook.ts +++ b/src/components/pages/console/instance/NewInstancePage/hook.ts @@ -74,7 +74,7 @@ export type Modal = 'node-list' | 'terms-and-conditions' export type UseNewInstancePageReturn = { address: string - accountBalance: number + accountCreditBalance: number blockchainName: string manuallySelectCRNDisabled: boolean manuallySelectCRNDisabledMessage?: TooltipProps['content'] @@ -111,7 +111,7 @@ export function useNewInstancePage(): UseNewInstancePageReturn { const { blockchain, account, - balance: accountBalance = 0, + creditBalance: accountCreditBalance = 0, handleConnect, } = useConnection({ triggerOnMount: false, @@ -329,7 +329,7 @@ export function useNewInstancePage(): UseNewInstancePageReturn { const { canAfford, isCreateButtonDisabled } = useCanAfford({ cost, - accountBalance, + accountCreditBalance, }) // Checks if user can afford with current balance @@ -436,7 +436,7 @@ export function useNewInstancePage(): UseNewInstancePageReturn { return { address, - accountBalance, + accountCreditBalance, blockchainName, createInstanceDisabled, createInstanceButtonTitle, diff --git a/src/components/pages/console/volume/NewVolumePage/cmp.tsx b/src/components/pages/console/volume/NewVolumePage/cmp.tsx index 578dfc65a..1633bf45c 100644 --- a/src/components/pages/console/volume/NewVolumePage/cmp.tsx +++ b/src/components/pages/console/volume/NewVolumePage/cmp.tsx @@ -15,7 +15,7 @@ export function NewVolumePage() { const { control, address, - accountBalance, + accountCreditBalance, isCreateButtonDisabled, errors, cost, @@ -43,8 +43,7 @@ export function NewVolumePage() { 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/website/NewWebsitePage/cmp.tsx b/src/components/pages/console/website/NewWebsitePage/cmp.tsx index 245d3cf5c..ae4e90166 100644 --- a/src/components/pages/console/website/NewWebsitePage/cmp.tsx +++ b/src/components/pages/console/website/NewWebsitePage/cmp.tsx @@ -22,7 +22,7 @@ export default function NewWebsitePage({ mainRef }: PageProps) { const { control, address, - accountBalance, + accountCreditBalance, isCreateButtonDisabled, errors, cost, @@ -133,8 +133,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/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 f15d41954..6f79b9854 100644 --- a/src/domain/cost.ts +++ b/src/domain/cost.ts @@ -93,7 +93,8 @@ export class CostManager { ) {} protected static readonly aggregateSourceAddress = - '0xFba561a84A537fCaa567bb7A2257e7142701ae2A' + // '0xFba561a84A537fCaa567bb7A2257e7142701ae2A' + '0xA07B1214bAe0D5ccAA25449C3149c0aC83658874' async getSettingsAggregate(): Promise { const response = await this.sdkClient.fetchAggregate( diff --git a/src/domain/executable.ts b/src/domain/executable.ts index 2ebecf597..797b5a6d4 100644 --- a/src/domain/executable.ts +++ b/src/domain/executable.ts @@ -304,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, ) @@ -318,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}`, ) @@ -866,9 +876,13 @@ export abstract class ExecutableManager { receiver: payment.receiver, } } else { + console.log('Parsing payment for cost estimation:', payment) return { chain: payment?.chain || BlockchainId.ETH, - type: SDKPaymentType.hold, + type: + payment?.type === PaymentMethod.Credit + ? SDKPaymentType.credit + : SDKPaymentType.hold, } } } @@ -889,6 +903,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, @@ -927,13 +947,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/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/program.ts b/src/domain/program.ts index 4fee2d578..efa8034ed 100644 --- a/src/domain/program.ts +++ b/src/domain/program.ts @@ -271,11 +271,12 @@ 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) + console.log('Parsed program for cost estimation:', parsedProgram) const costs = await this.sdkClient.programClient.getEstimatedCost(parsedProgram) @@ -353,6 +354,7 @@ export class ProgramManager ): Promise { const { account = mockAccount, channel } = this const { isPersistent, specs } = newProgram + console.log('Parsing program for cost estimation:', newProgram) const parsedSpecs = this.parseSpecs(specs) const memory = parsedSpecs?.memory 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/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/utils.ts b/src/helpers/utils.ts index 5c1946ad9..70664b23a 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -56,7 +56,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 +65,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) || 150000, // @todo: temporary hack to give free credit balance + } } catch (error) { throw Err.RequestFailed(error) } @@ -78,7 +82,7 @@ export async function getAccountBalance( account: Account, paymentMethod: PaymentMethod, ) { - let balance: number + let balance = 0 if (paymentMethod === PaymentMethod.Stream && isAccountSupported(account)) { try { @@ -92,12 +96,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) { diff --git a/src/hooks/common/useCanAfford.ts b/src/hooks/common/useCanAfford.ts index 1239d1c08..c44152092 100644 --- a/src/hooks/common/useCanAfford.ts +++ b/src/hooks/common/useCanAfford.ts @@ -1,7 +1,7 @@ import { CostSummary } from '@/domain/cost' export type UseCanAffordProps = { - accountBalance: number + accountCreditBalance: number cost?: CostSummary } @@ -11,11 +11,11 @@ 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 : 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..18e1de309 100644 --- a/src/hooks/common/useEntityCost.ts +++ b/src/hooks/common/useEntityCost.ts @@ -52,7 +52,7 @@ 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: [], }), @@ -108,9 +108,11 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { result = await gpuInstanceManager.getCost(props) break case EntityType.Program: + console.log('Fetching program cost with props:', props) if (programManager) result = await programManager.getCost(props) break case EntityType.Website: + console.log('Fetching website cost with props:', props) if (websiteManager) result = await websiteManager.getCost(props) break } diff --git a/src/hooks/common/useExecutableActions.ts b/src/hooks/common/useExecutableActions.ts index a388c9cf2..5c5d6a32d 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({ @@ -120,6 +121,7 @@ export function useExecutableActions({ }, [executable, manager]) const nodeDetails = useMemo(() => { + console.log('crn', crn) if (!crn) return return { name: crn.name || crn.hash, @@ -177,47 +179,76 @@ export function useExecutableActions({ ) const handleStart = useCallback(async () => { + console.log('starting') 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) - // For PAYG, notify CRN - if (isPAYG) { + if (incompatibleNetwork) { + throw Err.NetworkMismatch(incompatibleNetwork) + } + + 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 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/form/useSelectInstanceSpecs.ts b/src/hooks/form/useSelectInstanceSpecs.ts index 29c99fac2..82070fd21 100644 --- a/src/hooks/form/useSelectInstanceSpecs.ts +++ b/src/hooks/form/useSelectInstanceSpecs.ts @@ -61,11 +61,12 @@ export function useSelectInstanceSpecs({ const filterValidNodeSpecs = useCallback( (option: Tier) => { + if (type === EntityType.Program) return true if (!nodeSpecs) return false return nodeManager.validateMinNodeSpecs(option, nodeSpecs) }, - [nodeManager, nodeSpecs], + [nodeManager, nodeSpecs, type], ) // Process and cache valid voucher configurations 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: { From 734c2890846792a086ef49e42a0af8ffc897c691 Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 30 Sep 2025 18:21:10 +0200 Subject: [PATCH 14/41] redirect to legacy console --- src/components/common/ButtonLink/cmp.tsx | 26 +++++++++- src/components/common/ButtonLink/types.ts | 5 +- src/components/common/EntityCard/cmp.tsx | 6 +++ src/components/common/EntityCard/types.ts | 2 + src/components/common/Header/cmp.tsx | 4 +- src/components/common/NewEntityTab/cmp.tsx | 1 + src/components/form/AddVolume/cmp.tsx | 1 + .../pages/console/DashboardPage/cmp.tsx | 48 +++++++++++++++++-- .../function/FunctionDashboardPage/cmp.tsx | 12 +++-- .../function/FunctionsTabContent/cmp.tsx | 4 +- .../pages/console/volume/ManageVolume/cmp.tsx | 4 +- .../volume/VolumeDashboardPage/cmp.tsx | 16 ++++--- .../console/website/ManageWebsite/cmp.tsx | 4 +- .../website/WebsiteDashboardPage/cmp.tsx | 10 ++-- .../website/WebsitesTabContent/cmp.tsx | 4 +- src/helpers/constants.ts | 47 ++++++++++++++++++ src/hooks/form/useAddVolume.ts | 6 +-- 17 files changed, 168 insertions(+), 32 deletions(-) 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 = (
-
+ {/*
Create function -
+
*/} ) : (
diff --git a/src/components/pages/console/volume/ManageVolume/cmp.tsx b/src/components/pages/console/volume/ManageVolume/cmp.tsx index 8eaff0844..a1558650c 100644 --- a/src/components/pages/console/volume/ManageVolume/cmp.tsx +++ b/src/components/pages/console/volume/ManageVolume/cmp.tsx @@ -22,11 +22,11 @@ export default function ManageVolume() { -
+ {/*
Create new volume -
+
*/}
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/website/ManageWebsite/cmp.tsx b/src/components/pages/console/website/ManageWebsite/cmp.tsx index adb30accd..dd4fe3a13 100644 --- a/src/components/pages/console/website/ManageWebsite/cmp.tsx +++ b/src/components/pages/console/website/ManageWebsite/cmp.tsx @@ -372,14 +372,14 @@ export function ManageWebsite() { -
+ {/*
Create new website -
+
*/} 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..7b2850763 100644 --- a/src/components/pages/console/website/WebsitesTabContent/cmp.tsx +++ b/src/components/pages/console/website/WebsitesTabContent/cmp.tsx @@ -68,14 +68,14 @@ export const WebsitesTabContent = React.memo( ]} />
-
+ {/*
Create website -
+
*/} ) : (
diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index c890456b2..1300d9e56 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -198,6 +198,53 @@ export enum WebsiteFrameworkId { export const EXTRA_WEI = 3600 / 10 ** 18 export const NAVIGATION_URLS = { + legacyConsole: { + home: 'https://app.aleph.cloud/console', + settings: { + home: 'https://app.aleph.cloud/console/settings', + ssh: { + home: 'https://app.aleph.cloud/console/settings/ssh', + new: 'https://app.aleph.cloud/console/settings/ssh/new', + }, + domain: { + home: 'https://app.aleph.cloud/console/settings/domain', + new: 'https://app.aleph.cloud/console/settings/domain/new', + }, + }, + web3Hosting: { + home: 'https://app.aleph.cloud/console/hosting/', + website: { + home: 'https://app.aleph.cloud/console/hosting/website', + new: 'https://app.aleph.cloud/console/hosting/website/new', + }, + }, + computing: { + home: 'https://app.aleph.cloud/console/computing', + functions: { + home: 'https://app.aleph.cloud/console/computing/function', + new: 'https://app.aleph.cloud/console/computing/function/new', + }, + instances: { + home: 'https://app.aleph.cloud/console/computing/instance', + new: 'https://app.aleph.cloud/console/computing/instance/new', + }, + gpus: { + home: 'https://app.aleph.cloud/console/computing/gpu-instance', + new: 'https://app.aleph.cloud/console/computing/gpu-instance/new', + }, + confidentials: { + home: 'https://app.aleph.cloud/console/computing/confidential', + new: 'https://app.aleph.cloud/console/computing/confidential/new', + }, + }, + storage: { + home: 'https://app.aleph.cloud/console/storage', + volumes: { + home: 'https://app.aleph.cloud/console/storage/volume', + new: 'https://app.aleph.cloud/console/storage/volume/new', + }, + }, + }, console: { home: '/console', settings: { diff --git a/src/hooks/form/useAddVolume.ts b/src/hooks/form/useAddVolume.ts index d2b19068e..bcddbe28c 100644 --- a/src/hooks/form/useAddVolume.ts +++ b/src/hooks/form/useAddVolume.ts @@ -4,7 +4,7 @@ import { Volume, VolumeManager, VolumeType } from '@/domain/volume' import { Control, UseControllerReturn, useController } from 'react-hook-form' export type NewVolumeStandaloneField = { - volumeType: VolumeType.New + volumeType: VolumeType.Existing file?: File } @@ -32,7 +32,7 @@ export type InstanceSystemVolumeField = { } export const defaultVolume: NewVolumeStandaloneField = { - volumeType: VolumeType.New, + volumeType: VolumeType.Existing, } export type VolumeField = @@ -313,7 +313,7 @@ export function useAddVolume({ const volumeTypeCtrl = useController({ control, name: `${n}.volumeType`, - defaultValue: VolumeType.New, + defaultValue: VolumeType.Existing, }) const handleRemove = useCallback(() => { From caf8f54e5b0cb7083ece0f5dbba097d0913ec40e Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 30 Sep 2025 18:34:47 +0200 Subject: [PATCH 15/41] fix credit entities --- .../DashboardPage/CreditsDashboard/cmp.tsx | 119 ++++++----- .../DashboardPage/CreditsDashboard/hook.ts | 200 ++++++++++++++++++ src/domain/confidential.ts | 2 +- src/domain/gpuInstance.ts | 2 +- 4 files changed, 261 insertions(+), 62 deletions(-) create mode 100644 src/components/pages/console/DashboardPage/CreditsDashboard/hook.ts diff --git a/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx index 84ba79bd1..5dcb9d035 100644 --- a/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx +++ b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' import { Button, Icon, @@ -9,40 +9,37 @@ import { import { SectionTitle } from '@/components/common/CompositeTitle' import ToggleDashboard from '@/components/common/ToggleDashboard' -import StyledTable from '@/components/common/Table' -import tw from 'twin.macro' -import DetailsMenuButton from '@/components/common/DetailsMenuButton' -import { useConnection } from '@/hooks/common/useConnection' +import Skeleton from '@/components/common/Skeleton' +import { useCreditsDashboard } from './hook' export default function CreditsDashboard() { - const [creditsDashboardOpen, setCreditsDashboardOpen] = useState(false) - const { account } = useConnection({ triggerOnMount: false }) - const isConnected = !!account + const { + runRateDays, + creditsDashboardOpen, + setCreditsDashboardOpen, + isConnected, + accountCreditBalance, + isCalculatingCosts, + } = useCreditsDashboard() - useEffect(() => { - if (!isConnected && creditsDashboardOpen) { - setCreditsDashboardOpen(false) - } - }, [isConnected, creditsDashboardOpen]) - - 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, - }, - ] + // 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 (
@@ -71,19 +68,27 @@ export default function CreditsDashboard() { tw="flex flex-col items-start justify-between px-3 py-2 min-w-[6.875rem] min-h-[3.8125rem]" >

AVAILABLE

-

1080

+ {accountCreditBalance !== undefined ? ( +

{accountCreditBalance}

+ ) : ( + + )}

RUN-RATE

-
-

34

-

- DAYS -

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

{runRateDays || '∞'}

+

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

+
+ )}
@@ -106,7 +111,7 @@ export default function CreditsDashboard() { History
-
+ {/*
row.date, }, - { - label: 'AMOUNT', - align: 'left', - sortable: true, - render: (row) => row.amount, - }, - { - label: 'ASSET', - align: 'left', - sortable: true, - render: (row) => row.asset, - }, + // { + // label: 'AMOUNT', + // align: 'left', + // sortable: true, + // render: (row) => row.amount, + // }, + // { + // label: 'ASSET', + // align: 'left', + // sortable: true, + // render: (row) => row.asset, + // }, { label: 'CREDITS', align: 'left', @@ -187,13 +192,7 @@ export default function CreditsDashboard() { render: (row) => { return ( ) }, @@ -203,7 +202,7 @@ export default function CreditsDashboard() { }, ]} /> -
+
*/}
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/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/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 } } From 11f08b81199b9b7ce176bc5cbca80edf8ae2ce73 Mon Sep 17 00:00:00 2001 From: gmolki Date: Wed, 1 Oct 2025 16:14:09 +0200 Subject: [PATCH 16/41] fix: build --- src/components/common/ToggleDashboard/cmp.tsx | 3 +- src/components/form/CheckoutSummary/cmp.tsx | 269 +++++++++--------- .../console/function/NewFunctionPage/cmp.tsx | 4 +- .../gpuInstance/NewGpuInstancePage/cmp.tsx | 4 +- .../pages/console/volume/ManageVolume/cmp.tsx | 1 - .../console/volume/NewVolumePage/cmp.tsx | 1 - .../console/website/ManageWebsite/cmp.tsx | 3 +- .../console/website/NewWebsitePage/cmp.tsx | 6 +- src/hooks/form/useAddVolume.ts | 14 +- src/hooks/form/useAddVolumes.ts | 4 +- 10 files changed, 147 insertions(+), 162 deletions(-) diff --git a/src/components/common/ToggleDashboard/cmp.tsx b/src/components/common/ToggleDashboard/cmp.tsx index 0bb7d582a..7020553b3 100644 --- a/src/components/common/ToggleDashboard/cmp.tsx +++ b/src/components/common/ToggleDashboard/cmp.tsx @@ -5,7 +5,6 @@ import { memo, useCallback, useRef, - useState, } from 'react' import tw from 'twin.macro' import { Button, Icon, useTransition, useBounds } from '@aleph-front/core' @@ -36,7 +35,7 @@ export const ToggleDashboard = ({ }, ...rest }: ToggleDashboardProps) => { - const handleToogle = useCallback(() => setOpen((prev) => !prev), []) + const handleToogle = useCallback(() => setOpen((prev) => !prev), [setOpen]) const ref = useRef(null) diff --git a/src/components/form/CheckoutSummary/cmp.tsx b/src/components/form/CheckoutSummary/cmp.tsx index e0b13942a..c51b5435f 100644 --- a/src/components/form/CheckoutSummary/cmp.tsx +++ b/src/components/form/CheckoutSummary/cmp.tsx @@ -1,164 +1,151 @@ -import { - ellipseAddress, - convertByteUnits, - humanReadableSize, - humanReadableCurrency, -} from '@/helpers/utils' +import { ellipseAddress, humanReadableCurrency } from '@/helpers/utils' import { Label, StyledHoldingSummaryLine } from './styles' -import { - CheckoutSummaryProps, - CheckoutSummaryVolumeLineProps, - CheckoutSummaryWebsiteLineProps, -} from './types' -import { memo, useEffect, useState } from 'react' +import { CheckoutSummaryProps } from './types' +import { memo } from 'react' import React from 'react' -import { VolumeManager, VolumeType } from '@/domain/volume' -import InfoTooltipButton from '../../common/InfoTooltipButton' import { CenteredContainer } from '@/components/common/CenteredContainer' import { TextGradient } from '@aleph-front/core' -import Price from '@/components/common/Price' import CheckoutSummaryFooter from '../CheckoutSummaryFooter' -import { AddWebsite, WebsiteManager } from '@/domain/website' import { useConnection } from '@/hooks/common/useConnection' import { Blockchain } from '@aleph-sdk/core' import { useNFTVoucherBalance } from '@/hooks/common/useNFTVoucherBalance' -const CheckoutSummaryVolumeLine = ({ - volume, - cost, - specs, - priceDuration, -}: CheckoutSummaryVolumeLineProps) => { - const [size, setSize] = useState(0) +// const CheckoutSummaryVolumeLine = ({ +// volume, +// cost, +// specs, +// priceDuration, +// }: CheckoutSummaryVolumeLineProps) => { +// const [size, setSize] = useState(0) - useEffect(() => { - async function load() { - const size = await VolumeManager.getVolumeSize(volume) - setSize(size) - } +// useEffect(() => { +// async function load() { +// const size = await VolumeManager.getVolumeSize(volume) +// setSize(size) +// } - load() - }, [volume]) +// load() +// }, [volume]) - if (!cost) return <> +// if (!cost) return <> - const hasDiscount = !!cost.discount - const fullDiscount = !cost.cost +// const hasDiscount = !!cost.discount +// const fullDiscount = !cost.cost - 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' +// 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' // ------------------------------------------ diff --git a/src/components/pages/console/function/NewFunctionPage/cmp.tsx b/src/components/pages/console/function/NewFunctionPage/cmp.tsx index d8ced9a35..946befb36 100644 --- a/src/components/pages/console/function/NewFunctionPage/cmp.tsx +++ b/src/components/pages/console/function/NewFunctionPage/cmp.tsx @@ -24,7 +24,7 @@ import CheckoutButton from '@/components/form/CheckoutButton' export default function NewFunctionPage({ mainRef }: PageProps) { const { address, - accountBalance, + accountCreditBalance, createFunctionDisabled, createFunctionButtonTitle, values, @@ -174,7 +174,7 @@ export default function NewFunctionPage({ mainRef }: PageProps) { control={control} address={address} cost={cost} - unlockedAmount={accountBalance} + unlockedAmount={accountCreditBalance} mainRef={mainRef} description={ <> diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx index 7c542735b..8ecc14a41 100644 --- a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx +++ b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx @@ -39,7 +39,7 @@ import CheckoutButton from '@/components/form/CheckoutButton' export default function NewGpuInstancePage({ mainRef }: PageProps) { const { address, - accountBalance, + accountCreditBalance, blockchainName, manuallySelectCRNDisabled, manuallySelectCRNDisabledMessage, @@ -416,7 +416,7 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { control={control} address={address} cost={cost} - unlockedAmount={accountBalance} + unlockedAmount={accountCreditBalance} mainRef={mainRef} description={ <> diff --git a/src/components/pages/console/volume/ManageVolume/cmp.tsx b/src/components/pages/console/volume/ManageVolume/cmp.tsx index a1558650c..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' diff --git a/src/components/pages/console/volume/NewVolumePage/cmp.tsx b/src/components/pages/console/volume/NewVolumePage/cmp.tsx index 1633bf45c..a7ec627cc 100644 --- a/src/components/pages/console/volume/NewVolumePage/cmp.tsx +++ b/src/components/pages/console/volume/NewVolumePage/cmp.tsx @@ -1,6 +1,5 @@ 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' diff --git a/src/components/pages/console/website/ManageWebsite/cmp.tsx b/src/components/pages/console/website/ManageWebsite/cmp.tsx index dd4fe3a13..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' diff --git a/src/components/pages/console/website/NewWebsitePage/cmp.tsx b/src/components/pages/console/website/NewWebsitePage/cmp.tsx index ae4e90166..3273f89dd 100644 --- a/src/components/pages/console/website/NewWebsitePage/cmp.tsx +++ b/src/components/pages/console/website/NewWebsitePage/cmp.tsx @@ -1,9 +1,5 @@ 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' diff --git a/src/hooks/form/useAddVolume.ts b/src/hooks/form/useAddVolume.ts index bcddbe28c..3387df8e7 100644 --- a/src/hooks/form/useAddVolume.ts +++ b/src/hooks/form/useAddVolume.ts @@ -4,17 +4,23 @@ import { Volume, VolumeManager, VolumeType } from '@/domain/volume' import { Control, UseControllerReturn, useController } from 'react-hook-form' export type NewVolumeStandaloneField = { - volumeType: VolumeType.Existing + volumeType: VolumeType.New 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,7 +37,7 @@ export type InstanceSystemVolumeField = { size: number } -export const defaultVolume: NewVolumeStandaloneField = { +export const defaultVolume: ExistingVolumeStandaloneField = { volumeType: VolumeType.Existing, } 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, From 8701ea37b73268fa4c171785aa83fb173bd05307 Mon Sep 17 00:00:00 2001 From: gmolki Date: Wed, 1 Oct 2025 18:38:24 +0200 Subject: [PATCH 17/41] deps: update core & message --- package-lock.json | 28 ++++++++++++++-------------- package.json | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 493220957..31e700c9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,14 @@ "name": "front-aleph-cloud", "version": "0.34.3", "dependencies": { - "@aleph-front/core": "file:/../front-core/aleph-front-core-1.29.1-beta-0.tgz", + "@aleph-front/core": "^1.30.1", "@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": "file:/../aleph-sdk-ts/packages/message/aleph-sdk-message-1.6.2-beta-2.tgz", + "@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,10 +61,9 @@ } }, "node_modules/@aleph-front/core": { - "version": "1.29.1-beta-0", - "resolved": "file:../front-core/aleph-front-core-1.29.1-beta-0.tgz", - "integrity": "sha512-ZNhvd8r7lHwEPpEmtmkhS2Jc9VMwYN412kwoVb3SE1YD/2xQ9NQX3hXUdG80EjH6P2KtxyLypKsIS/dd2tzmfg==", - "license": "ISC", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.30.1.tgz", + "integrity": "sha512-si2EjrUG2TwPp1MsD+sq1FA14f/+bz5UrMbAGNhkSKZlFpCVakbp9PdnbHXekgtBktwNJsMgLra2RjV1W74ljg==", "dependencies": { "@monaco-editor/react": "^4.4.6", "react-infinite-scroll-hook": "^4.1.1", @@ -242,10 +241,9 @@ } }, "node_modules/@aleph-sdk/message": { - "version": "1.6.2-beta-2", - "resolved": "file:../aleph-sdk-ts/packages/message/aleph-sdk-message-1.6.2-beta-2.tgz", - "integrity": "sha512-SXoYWMQxbkUg09eQ4RbSz29XDQuS8Ea1EuwSk5XBp/YTQZuTvteHAm6lEyDQas8Ew8ZDr3l3FwlIChLXMf2s2A==", - "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", @@ -19910,8 +19908,9 @@ }, "dependencies": { "@aleph-front/core": { - "version": "file:../front-core/aleph-front-core-1.29.1-beta-0.tgz", - "integrity": "sha512-ZNhvd8r7lHwEPpEmtmkhS2Jc9VMwYN412kwoVb3SE1YD/2xQ9NQX3hXUdG80EjH6P2KtxyLypKsIS/dd2tzmfg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.30.1.tgz", + "integrity": "sha512-si2EjrUG2TwPp1MsD+sq1FA14f/+bz5UrMbAGNhkSKZlFpCVakbp9PdnbHXekgtBktwNJsMgLra2RjV1W74ljg==", "requires": { "@monaco-editor/react": "^4.4.6", "react-infinite-scroll-hook": "^4.1.1", @@ -20029,8 +20028,9 @@ } }, "@aleph-sdk/message": { - "version": "file:../aleph-sdk-ts/packages/message/aleph-sdk-message-1.6.2-beta-2.tgz", - "integrity": "sha512-SXoYWMQxbkUg09eQ4RbSz29XDQuS8Ea1EuwSk5XBp/YTQZuTvteHAm6lEyDQas8Ew8ZDr3l3FwlIChLXMf2s2A==", + "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 b7aed5070..faba57330 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,14 @@ "lint:fix": "next lint --fix" }, "dependencies": { - "@aleph-front/core": "file:/../front-core/aleph-front-core-1.29.1-beta-0.tgz", + "@aleph-front/core": "^1.30.1", "@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": "file:/../aleph-sdk-ts/packages/message/aleph-sdk-message-1.6.2-beta-2.tgz", + "@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", From c3befa4e75ee298084e105baa0e83771cc242728 Mon Sep 17 00:00:00 2001 From: gmolki Date: Wed, 1 Oct 2025 19:05:40 +0200 Subject: [PATCH 18/41] refactor: constants urls --- src/components/common/Header/cmp.tsx | 2 +- src/helpers/constants.ts | 43 ++++++++++++++-------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/components/common/Header/cmp.tsx b/src/components/common/Header/cmp.tsx index c1c2a5995..967650a4c 100644 --- a/src/components/common/Header/cmp.tsx +++ b/src/components/common/Header/cmp.tsx @@ -68,7 +68,7 @@ export const Header = () => { Link={CustomLinkMemo} externalUrl={{ text: 'Legacy console', - url: 'https://app.aleph.cloud/', + url: NAVIGATION_URLS.legacyConsole.home, }} /> ), diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index 1300d9e56..19d50eee5 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -197,51 +197,52 @@ 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: 'https://app.aleph.cloud/console', + home: `${LEGACY_CONSOLE_DOMAIN}`, settings: { - home: 'https://app.aleph.cloud/console/settings', + home: `${LEGACY_CONSOLE_DOMAIN}/settings`, ssh: { - home: 'https://app.aleph.cloud/console/settings/ssh', - new: 'https://app.aleph.cloud/console/settings/ssh/new', + home: `${LEGACY_CONSOLE_DOMAIN}/settings/ssh`, + new: `${LEGACY_CONSOLE_DOMAIN}/settings/ssh/new`, }, domain: { - home: 'https://app.aleph.cloud/console/settings/domain', - new: 'https://app.aleph.cloud/console/settings/domain/new', + home: `${LEGACY_CONSOLE_DOMAIN}/settings/domain`, + new: `${LEGACY_CONSOLE_DOMAIN}/settings/domain/new`, }, }, web3Hosting: { - home: 'https://app.aleph.cloud/console/hosting/', + home: `${LEGACY_CONSOLE_DOMAIN}/hosting/`, website: { - home: 'https://app.aleph.cloud/console/hosting/website', - new: 'https://app.aleph.cloud/console/hosting/website/new', + home: `${LEGACY_CONSOLE_DOMAIN}/hosting/website`, + new: `${LEGACY_CONSOLE_DOMAIN}/hosting/website/new`, }, }, computing: { - home: 'https://app.aleph.cloud/console/computing', + home: `${LEGACY_CONSOLE_DOMAIN}/computing`, functions: { - home: 'https://app.aleph.cloud/console/computing/function', - new: 'https://app.aleph.cloud/console/computing/function/new', + home: `${LEGACY_CONSOLE_DOMAIN}/computing/function`, + new: `${LEGACY_CONSOLE_DOMAIN}/computing/function/new`, }, instances: { - home: 'https://app.aleph.cloud/console/computing/instance', - new: 'https://app.aleph.cloud/console/computing/instance/new', + home: `${LEGACY_CONSOLE_DOMAIN}/computing/instance`, + new: `${LEGACY_CONSOLE_DOMAIN}/computing/instance/new`, }, gpus: { - home: 'https://app.aleph.cloud/console/computing/gpu-instance', - new: 'https://app.aleph.cloud/console/computing/gpu-instance/new', + home: `${LEGACY_CONSOLE_DOMAIN}/computing/gpu-instance`, + new: `${LEGACY_CONSOLE_DOMAIN}/computing/gpu-instance/new`, }, confidentials: { - home: 'https://app.aleph.cloud/console/computing/confidential', - new: 'https://app.aleph.cloud/console/computing/confidential/new', + home: `${LEGACY_CONSOLE_DOMAIN}/computing/confidential`, + new: `${LEGACY_CONSOLE_DOMAIN}/computing/confidential/new`, }, }, storage: { - home: 'https://app.aleph.cloud/console/storage', + home: `${LEGACY_CONSOLE_DOMAIN}/storage`, volumes: { - home: 'https://app.aleph.cloud/console/storage/volume', - new: 'https://app.aleph.cloud/console/storage/volume/new', + home: `${LEGACY_CONSOLE_DOMAIN}/storage/volume`, + new: `${LEGACY_CONSOLE_DOMAIN}/storage/volume/new`, }, }, }, From fcd8ad57c6d06404f1eaa3474a1d8b14776009df Mon Sep 17 00:00:00 2001 From: gmolki Date: Wed, 1 Oct 2025 20:05:42 +0200 Subject: [PATCH 19/41] fix: show credits in account picker --- src/components/common/Header/cmp.tsx | 2 ++ src/helpers/constants.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/common/Header/cmp.tsx b/src/components/common/Header/cmp.tsx index 967650a4c..c15892640 100644 --- a/src/components/common/Header/cmp.tsx +++ b/src/components/common/Header/cmp.tsx @@ -55,6 +55,7 @@ export const Header = () => { isMobile accountAddress={accountAddress} accountBalance={accountBalance} + showCredits accountCredits={accountCreditBalance} accountVouchers={accountVouchers} blockchains={blockchains} @@ -81,6 +82,7 @@ export const Header = () => { Date: Wed, 1 Oct 2025 20:30:23 +0200 Subject: [PATCH 20/41] fix: comment purchases table --- .../DashboardPage/CreditsDashboard/cmp.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx index 5dcb9d035..665cb4c57 100644 --- a/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx +++ b/src/components/pages/console/DashboardPage/CreditsDashboard/cmp.tsx @@ -1,11 +1,5 @@ import React from 'react' -import { - Button, - Icon, - NoisyContainer, - ObjectImg, - TextGradient, -} from '@aleph-front/core' +import { Button, Icon, NoisyContainer, ObjectImg } from '@aleph-front/core' import { SectionTitle } from '@/components/common/CompositeTitle' import ToggleDashboard from '@/components/common/ToggleDashboard' @@ -101,7 +95,7 @@ export default function CreditsDashboard() { -
+ {/*
Purchases @@ -111,7 +105,7 @@ export default function CreditsDashboard() { History
- {/*
+
-
*/} -
+
+
*/}
From 32985901ec53c109d985febfe5b9e4cc1e557f23 Mon Sep 17 00:00:00 2001 From: gmolki Date: Thu, 2 Oct 2025 10:50:43 +0200 Subject: [PATCH 21/41] feat: remove vouchers --- src/components/common/Header/cmp.tsx | 3 -- src/components/common/Header/hook.ts | 45 +--------------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/src/components/common/Header/cmp.tsx b/src/components/common/Header/cmp.tsx index c15892640..a62555a09 100644 --- a/src/components/common/Header/cmp.tsx +++ b/src/components/common/Header/cmp.tsx @@ -26,7 +26,6 @@ export const Header = () => { accountAddress, accountBalance, accountCreditBalance, - accountVouchers, rewards, selectedNetwork, handleToggle, @@ -57,7 +56,6 @@ export const Header = () => { accountBalance={accountBalance} showCredits accountCredits={accountCreditBalance} - accountVouchers={accountVouchers} blockchains={blockchains} networks={networks} selectedNetwork={selectedNetwork} @@ -84,7 +82,6 @@ export const Header = () => { accountBalance={accountBalance} showCredits accountCredits={accountCreditBalance} - accountVouchers={accountVouchers} blockchains={blockchains} networks={networks} selectedNetwork={selectedNetwork} diff --git a/src/components/common/Header/hook.ts b/src/components/common/Header/hook.ts index fe50be7ab..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, @@ -21,7 +21,6 @@ export type UseHeaderReturn = UseRoutesReturn & { accountAddress?: string accountBalance?: number accountCreditBalance?: number - accountVouchers?: AccountPickerProps['accountVouchers'] | undefined networks: Network[] pathname: string breadcrumbNames: UseBreadcrumbNamesReturn['names'] @@ -44,7 +43,6 @@ export function useHeader(): UseHeaderReturn { balance: accountBalance, creditBalance: accountCreditBalance, } = state.connection - const { voucherManager } = state.manager const { handleConnect: connect, handleDisconnect: disconnect } = useConnection({ triggerOnMount: true }) @@ -119,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( @@ -230,7 +188,6 @@ export function useHeader(): UseHeaderReturn { accountAddress: account?.address, accountBalance, accountCreditBalance, - accountVouchers, networks, pathname, routes, From 5e54b6e41a5a1594c4b2f2a32801c44b5ae3441f Mon Sep 17 00:00:00 2001 From: gmolki Date: Thu, 2 Oct 2025 12:15:39 +0200 Subject: [PATCH 22/41] disabled non-credit instances list --- .../instance/InstancesTabContent/cmp.tsx | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/src/components/pages/console/instance/InstancesTabContent/cmp.tsx b/src/components/pages/console/instance/InstancesTabContent/cmp.tsx index bedbc9b32..415de3d0c 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,31 @@ 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 + }, []) + + const isDisabled = useCallback( + (row: Instance) => { + return !isCredit(row) + }, + [isCredit], + ) + + const defaultCellProps = useCallback( + (row: Instance) => { + return isDisabled(row) ? { css: tw`opacity-40!` } : {} + }, + [isDisabled], + ) + return ( <> {data.length > 0 ? ( @@ -29,12 +51,14 @@ export const InstancesTabContent = React.memo( sortable: true, render: (row) => (row?.metadata?.name as string) || ellipseAddress(row.id), + cellProps: (row) => defaultCellProps(row), }, { label: 'Cores', align: 'right', sortable: true, render: (row) => row?.resources?.vcpus || 0, + cellProps: (row) => defaultCellProps(row), }, { label: 'RAM', @@ -46,31 +70,58 @@ export const InstancesTabContent = React.memo( to: 'GiB', displayUnit: true, }), + cellProps: (row) => defaultCellProps(row), }, { - label: 'HDD', + label: 'date', align: 'right', sortable: true, render: (row) => humanReadableSize(row.size, 'MiB'), + cellProps: (row) => defaultCellProps(row), }, { label: 'Date', align: 'right', sortable: true, render: (row) => row.date, + cellProps: (row) => defaultCellProps(row), }, { label: '', align: 'right', - render: (row) => ( - - - - ), + render: (row) => { + const disabled = isDisabled(row) + + return ( + + To manage this instance, go to the{' '} + +

+ ) + } + tooltipPosition={{ + my: 'bottom-right', + at: 'bottom-center', + }} + > + +
+ ) + }, cellProps: () => ({ css: tw`pl-3!`, }), From 60225ca0fb411ed4cabc8bb5cbdda1b7c311d9ca Mon Sep 17 00:00:00 2001 From: gmolki Date: Thu, 2 Oct 2025 12:39:56 +0200 Subject: [PATCH 23/41] disable non-credit enities list --- .../pages/console/DashboardPage/cmp.tsx | 6 +- .../ConfidentialsTabContent/cmp.tsx | 55 ++++++++++++++---- .../GpuInstancesTabContent/cmp.tsx | 55 ++++++++++++++---- .../instance/InstancesTabContent/cmp.tsx | 26 ++------- .../console/volume/VolumesTabContent/cmp.tsx | 43 ++++++++++---- .../website/WebsitesTabContent/cmp.tsx | 56 +++++++++++++++---- 6 files changed, 175 insertions(+), 66 deletions(-) diff --git a/src/components/pages/console/DashboardPage/cmp.tsx b/src/components/pages/console/DashboardPage/cmp.tsx index 70a278b2b..7a9682833 100644 --- a/src/components/pages/console/DashboardPage/cmp.tsx +++ b/src/components/pages/console/DashboardPage/cmp.tsx @@ -128,7 +128,7 @@ export default function DashboardPage() {

To create a Function, navigate to the{' '} To deploy a Website, navigate to the{' '} To create a new volume, navigate to the{' '} { + 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/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/instance/InstancesTabContent/cmp.tsx b/src/components/pages/console/instance/InstancesTabContent/cmp.tsx index 415de3d0c..5b97edbea 100644 --- a/src/components/pages/console/instance/InstancesTabContent/cmp.tsx +++ b/src/components/pages/console/instance/InstancesTabContent/cmp.tsx @@ -20,20 +20,6 @@ export const InstancesTabContent = React.memo( return row.payment?.type === PaymentType.credit }, []) - const isDisabled = useCallback( - (row: Instance) => { - return !isCredit(row) - }, - [isCredit], - ) - - const defaultCellProps = useCallback( - (row: Instance) => { - return isDisabled(row) ? { css: tw`opacity-40!` } : {} - }, - [isDisabled], - ) - return ( <> {data.length > 0 ? ( @@ -43,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={[ { @@ -51,14 +40,12 @@ export const InstancesTabContent = React.memo( sortable: true, render: (row) => (row?.metadata?.name as string) || ellipseAddress(row.id), - cellProps: (row) => defaultCellProps(row), }, { label: 'Cores', align: 'right', sortable: true, render: (row) => row?.resources?.vcpus || 0, - cellProps: (row) => defaultCellProps(row), }, { label: 'RAM', @@ -70,27 +57,24 @@ export const InstancesTabContent = React.memo( to: 'GiB', displayUnit: true, }), - cellProps: (row) => defaultCellProps(row), }, { label: 'date', align: 'right', sortable: true, render: (row) => humanReadableSize(row.size, 'MiB'), - cellProps: (row) => defaultCellProps(row), }, { label: 'Date', align: 'right', sortable: true, render: (row) => row.date, - cellProps: (row) => defaultCellProps(row), }, { label: '', align: 'right', render: (row) => { - const disabled = isDisabled(row) + const disabled = !isCredit(row) return ( To manage this instance, go to the{' '} 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/WebsitesTabContent/cmp.tsx b/src/components/pages/console/website/WebsitesTabContent/cmp.tsx index 7b2850763..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!`, }), From 3cff83a6e2293cb332a6258fc78da1c27309e628 Mon Sep 17 00:00:00 2001 From: gmolki Date: Thu, 2 Oct 2025 15:19:03 +0200 Subject: [PATCH 24/41] disable functions --- .../function/FunctionsTabContent/cmp.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/pages/console/function/FunctionsTabContent/cmp.tsx b/src/components/pages/console/function/FunctionsTabContent/cmp.tsx index 958cd2a03..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', + }} >
From d49a0fa579d5a0012469eca6daeb08b402677d5e Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 6 Oct 2025 10:54:16 +0200 Subject: [PATCH 25/41] fix: round credits --- src/components/common/entityData/EntityPayment/hook.ts | 7 ++++--- src/components/common/entityData/EntityPayment/types.ts | 2 +- src/components/form/CheckoutSummary/cmp.tsx | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/common/entityData/EntityPayment/hook.ts b/src/components/common/entityData/EntityPayment/hook.ts index e78fd4181..80402e567 100644 --- a/src/components/common/entityData/EntityPayment/hook.ts +++ b/src/components/common/entityData/EntityPayment/hook.ts @@ -61,7 +61,7 @@ export function useFormatPayment( const { days, hours, minutes } = getTimeComponents(runningTime) const runningTimeInHours = days * 24 + hours + minutes / 60 - return (cost * runningTimeInHours).toFixed(6) + return Math.round(cost * runningTimeInHours) }, [cost, isStream, isCredit, runningTime]) // Format blockchain name @@ -75,8 +75,9 @@ export function useFormatPayment( if (!isStream && !isCredit) return if (!cost) return - const dailyRate = cost * 24 - return `~${dailyRate.toFixed(4)}/day` + const dailyRate = Math.round(cost * 24) + + return `~${dailyRate}/day` }, [cost, isCredit, isStream]) // Format start date diff --git a/src/components/common/entityData/EntityPayment/types.ts b/src/components/common/entityData/EntityPayment/types.ts index 53472b8d5..dd38a8ab8 100644 --- a/src/components/common/entityData/EntityPayment/types.ts +++ b/src/components/common/entityData/EntityPayment/types.ts @@ -42,7 +42,7 @@ export interface EntityPaymentProps { export interface FormattedPaymentData { isStream: boolean isCredit: boolean - totalSpent?: string + totalSpent?: string | number formattedBlockchain?: string formattedFlowRate?: string formattedStartDate?: string diff --git a/src/components/form/CheckoutSummary/cmp.tsx b/src/components/form/CheckoutSummary/cmp.tsx index c51b5435f..3136d7008 100644 --- a/src/components/form/CheckoutSummary/cmp.tsx +++ b/src/components/form/CheckoutSummary/cmp.tsx @@ -223,7 +223,9 @@ export const CheckoutSummary = ({ {line.cost !== 0 ? (
- {line.cost} + + {humanReadableCurrency(line.cost)} + / h
) : ( From 3596fe9182ae8f223db9fb4d2f683cc523ba6850 Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 6 Oct 2025 11:26:27 +0200 Subject: [PATCH 26/41] fix: disable top up on account picker --- package-lock.json | 14 +++++++------- package.json | 2 +- src/components/common/Header/cmp.tsx | 2 ++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31e700c9b..59e323efe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "front-aleph-cloud", "version": "0.34.3", "dependencies": { - "@aleph-front/core": "^1.30.1", + "@aleph-front/core": "^1.30.2", "@aleph-sdk/account": "^1.2.0", "@aleph-sdk/avalanche": "^1.5.0", "@aleph-sdk/client": "^1.4.5", @@ -61,9 +61,9 @@ } }, "node_modules/@aleph-front/core": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.30.1.tgz", - "integrity": "sha512-si2EjrUG2TwPp1MsD+sq1FA14f/+bz5UrMbAGNhkSKZlFpCVakbp9PdnbHXekgtBktwNJsMgLra2RjV1W74ljg==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.30.2.tgz", + "integrity": "sha512-18dSBQ8QVSGrwYaXaZ6t6NDTK6uuEnlxz8Kw5lagWniqn2iUbqaLKs2n/z5VsulCay/A8IZyWVUzYHtATPn8Gg==", "dependencies": { "@monaco-editor/react": "^4.4.6", "react-infinite-scroll-hook": "^4.1.1", @@ -19908,9 +19908,9 @@ }, "dependencies": { "@aleph-front/core": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.30.1.tgz", - "integrity": "sha512-si2EjrUG2TwPp1MsD+sq1FA14f/+bz5UrMbAGNhkSKZlFpCVakbp9PdnbHXekgtBktwNJsMgLra2RjV1W74ljg==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/@aleph-front/core/-/core-1.30.2.tgz", + "integrity": "sha512-18dSBQ8QVSGrwYaXaZ6t6NDTK6uuEnlxz8Kw5lagWniqn2iUbqaLKs2n/z5VsulCay/A8IZyWVUzYHtATPn8Gg==", "requires": { "@monaco-editor/react": "^4.4.6", "react-infinite-scroll-hook": "^4.1.1", diff --git a/package.json b/package.json index faba57330..2f111cbd3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint:fix": "next lint --fix" }, "dependencies": { - "@aleph-front/core": "^1.30.1", + "@aleph-front/core": "^1.30.2", "@aleph-sdk/account": "^1.2.0", "@aleph-sdk/avalanche": "^1.5.0", "@aleph-sdk/client": "^1.4.5", diff --git a/src/components/common/Header/cmp.tsx b/src/components/common/Header/cmp.tsx index a62555a09..ba8d179dd 100644 --- a/src/components/common/Header/cmp.tsx +++ b/src/components/common/Header/cmp.tsx @@ -55,6 +55,7 @@ export const Header = () => { accountAddress={accountAddress} accountBalance={accountBalance} showCredits + disabledTopUp accountCredits={accountCreditBalance} blockchains={blockchains} networks={networks} @@ -81,6 +82,7 @@ export const Header = () => { accountAddress={accountAddress} accountBalance={accountBalance} showCredits + disabledTopUp accountCredits={accountCreditBalance} blockchains={blockchains} networks={networks} From 518d1dd9cf3921830dd046849d11427cdce218bf Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 6 Oct 2025 11:30:09 +0200 Subject: [PATCH 27/41] fix: executable actions --- src/hooks/common/useExecutableActions.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/hooks/common/useExecutableActions.ts b/src/hooks/common/useExecutableActions.ts index 5c5d6a32d..baf6d1525 100644 --- a/src/hooks/common/useExecutableActions.ts +++ b/src/hooks/common/useExecutableActions.ts @@ -179,7 +179,6 @@ export function useExecutableActions({ ) const handleStart = useCallback(async () => { - console.log('starting') if (!manager) throw Err.ConnectYourWallet if (!executable) throw Err.InstanceNotFound @@ -254,7 +253,7 @@ export function useExecutableActions({ const isAllocated = !!status?.ipv6Parsed const stopDisabled = useMemo(() => { - if (isPAYG || !crn) return true + if (!crn) return true switch (calculatedStatus) { case 'v1': @@ -264,10 +263,10 @@ export function useExecutableActions({ default: return true } - }, [calculatedStatus, crn, isAllocated, isPAYG]) + }, [calculatedStatus, crn, isAllocated]) const startDisabled = useMemo(() => { - if (isPAYG || !crn) return true + if (!crn) return true switch (calculatedStatus) { case 'v1': @@ -279,7 +278,7 @@ export function useExecutableActions({ default: return true } - }, [calculatedStatus, crn, isAllocated, isPAYG]) + }, [calculatedStatus, crn, isAllocated]) const rebootDisabled = useMemo(() => { if (!crn) return true From f2ed4639e0ea12dadf241c31afb624273280081c Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 6 Oct 2025 13:16:42 +0200 Subject: [PATCH 28/41] feat: ipv4 & ipv6 tab --- .../EntityConnectionMethods/cmp.tsx | 81 +++++++++---- .../EntityConnectionMethods/hook.ts | 23 +++- .../EntityConnectionMethods/types.ts | 7 +- .../entityData/EntityPortForwarding/cmp.tsx | 5 +- .../entityData/EntityPortForwarding/hook.ts | 108 +++++------------- .../entityData/EntityPortForwarding/types.ts | 2 + .../entityData/EntityPortForwarding/utils.ts | 11 ++ .../confidential/ManageConfidential/cmp.tsx | 8 ++ .../confidential/ManageConfidential/hook.ts | 36 ++++++ .../gpuInstance/ManageGpuInstance/cmp.tsx | 8 ++ .../gpuInstance/ManageGpuInstance/hook.ts | 36 ++++++ .../console/instance/ManageInstance/cmp.tsx | 8 ++ .../console/instance/ManageInstance/hook.ts | 36 ++++++ src/hooks/common/useForwardedPorts.ts | 106 +++++++++++++++++ 14 files changed, 364 insertions(+), 111 deletions(-) create mode 100644 src/hooks/common/useForwardedPorts.ts 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/EntityPortForwarding/cmp.tsx b/src/components/common/entityData/EntityPortForwarding/cmp.tsx index 3b4815edc..d07d419a7 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 = ( diff --git a/src/components/common/entityData/EntityPortForwarding/hook.ts b/src/components/common/entityData/EntityPortForwarding/hook.ts index 4469accbc..485bce297 100644 --- a/src/components/common/entityData/EntityPortForwarding/hook.ts +++ b/src/components/common/entityData/EntityPortForwarding/hook.ts @@ -5,9 +5,6 @@ import { NodeManager } from '@/domain/node' import { useForwardedPortsManager } from '@/hooks/common/useManager/useForwardedPortsManager' import { useAppState } from '@/contexts/appState' import { - getSystemPorts, - transformAPIPortsToUI, - mergePortsWithMappings, filterOutSystemPorts, validatePortBatch, isSystemPort, @@ -16,9 +13,7 @@ import { hasUnmappedPorts, addPendingPorts, transformPortsToAPIFormat, - mergePendingPortsWithAggregate, addPendingPortRemoval, - applyPendingRemovals, getPendingRemovalsForEntity, } from './utils' @@ -26,12 +21,13 @@ export type UseEntityPortForwardingProps = { entityHash?: string executableStatus?: ExecutableStatus executableManager?: ExecutableManager + 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/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/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/instance/ManageInstance/cmp.tsx b/src/components/pages/console/instance/ManageInstance/cmp.tsx index 31b5cd8ab..926daf55b 100644 --- a/src/components/pages/console/instance/ManageInstance/cmp.tsx +++ b/src/components/pages/console/instance/ManageInstance/cmp.tsx @@ -88,6 +88,11 @@ export default function ManageInstance() { // Navigation handlers handleBack, + + // Ports + ports, + sshForwardedPort, + handlePortsChange, } = useManageInstance() return ( @@ -163,6 +168,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..bac07d288 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,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 UseManageInstanceReturn = UseManageInstanceEntityReturn & { instance?: Instance instanceManager?: InstanceManager + ports: ForwardedPort[] + sshForwardedPort?: string + handlePortsChange: (ports: ForwardedPort[]) => void } export function useManageInstance(): UseManageInstanceReturn { @@ -29,9 +36,38 @@ 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, ...manageInstanceEntityProps, } } 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, + } +} From 01991432a5257bdad5f3644804da12207846f275 Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 6 Oct 2025 15:36:26 +0200 Subject: [PATCH 29/41] fix: remove 150000 fallback --- src/helpers/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 70664b23a..35f2eb2bd 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -71,7 +71,7 @@ export const getAddressBalance = async (address: string) => { return { balance: balance as number, - creditBalance: (credit_balance as number) || 150000, // @todo: temporary hack to give free credit balance + creditBalance: credit_balance as number, } } catch (error) { throw Err.RequestFailed(error) From 066995d0dcca66f63336fe659db39ecd3813372a Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 6 Oct 2025 15:39:22 +0200 Subject: [PATCH 30/41] disable nav items --- src/hooks/common/useRoutes.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/hooks/common/useRoutes.ts b/src/hooks/common/useRoutes.ts index caeab51d3..e5f84d4e0 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', @@ -94,12 +96,14 @@ export function useRoutes(): UseRoutesReturn { icon: 'gpu', label: '(BETA)', href: NAVIGATION_URLS.console.computing.gpus.home, + disabled: true, }, { name: 'Confidentials', href: NAVIGATION_URLS.console.computing.confidentials.home, label: '(BETA)', icon: 'confidential', + disabled: true, }, ], }, @@ -112,6 +116,7 @@ export function useRoutes(): UseRoutesReturn { name: 'Volumes', href: NAVIGATION_URLS.console.storage.home, icon: 'storageSolutions', + disabled: true, }, ], }, From 3413c3f3b9f6c9716b827d8f85f58d39fe579df2 Mon Sep 17 00:00:00 2001 From: gmolki Date: Mon, 6 Oct 2025 15:43:46 +0200 Subject: [PATCH 31/41] fix: remove checkout margin --- src/components/form/CheckoutSummary/cmp.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/form/CheckoutSummary/cmp.tsx b/src/components/form/CheckoutSummary/cmp.tsx index 3136d7008..c88f54501 100644 --- a/src/components/form/CheckoutSummary/cmp.tsx +++ b/src/components/form/CheckoutSummary/cmp.tsx @@ -166,7 +166,6 @@ export const CheckoutSummary = ({ return ( <> -
Date: Mon, 6 Oct 2025 15:44:47 +0200 Subject: [PATCH 32/41] fix: port forwarding tooltip postitio --- src/components/common/entityData/EntityPortForwarding/cmp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/common/entityData/EntityPortForwarding/cmp.tsx b/src/components/common/entityData/EntityPortForwarding/cmp.tsx index d07d419a7..64330c36b 100644 --- a/src/components/common/entityData/EntityPortForwarding/cmp.tsx +++ b/src/components/common/entityData/EntityPortForwarding/cmp.tsx @@ -63,7 +63,7 @@ export const EntityPortForwarding = ({
Date: Tue, 7 Oct 2025 11:48:09 +0200 Subject: [PATCH 33/41] update copies --- .../gpuInstance/NewGpuInstancePage/cmp.tsx | 10 ++++----- .../console/instance/NewInstancePage/cmp.tsx | 22 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx index 8ecc14a41..0a99ba1d3 100644 --- a/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx +++ b/src/components/pages/console/gpuInstance/NewGpuInstancePage/cmp.tsx @@ -420,11 +420,11 @@ export default function NewGpuInstancePage({ mainRef }: PageProps) { 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 diff --git a/src/components/pages/console/instance/NewInstancePage/cmp.tsx b/src/components/pages/console/instance/NewInstancePage/cmp.tsx index 78d9cda88..f3f9ed79f 100644 --- a/src/components/pages/console/instance/NewInstancePage/cmp.tsx +++ b/src/components/pages/console/instance/NewInstancePage/cmp.tsx @@ -261,12 +261,12 @@ export default function NewInstancePage({ mainRef }: PageProps) {

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. + 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.

@@ -430,11 +430,11 @@ export default function NewInstancePage({ mainRef }: PageProps) { 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. } button={ From be391b3bc831d2a8538bc312bb61d3352582b400 Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 7 Oct 2025 15:45:39 +0200 Subject: [PATCH 34/41] remove unused const --- .../pages/console/instance/NewInstancePage/cmp.tsx | 1 - .../pages/console/instance/NewInstancePage/hook.ts | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/pages/console/instance/NewInstancePage/cmp.tsx b/src/components/pages/console/instance/NewInstancePage/cmp.tsx index f3f9ed79f..7e80522a1 100644 --- a/src/components/pages/console/instance/NewInstancePage/cmp.tsx +++ b/src/components/pages/console/instance/NewInstancePage/cmp.tsx @@ -41,7 +41,6 @@ export default function NewInstancePage({ mainRef }: PageProps) { const { address, accountCreditBalance, - blockchainName, manuallySelectCRNDisabled, manuallySelectCRNDisabledMessage, createInstanceDisabled, diff --git a/src/components/pages/console/instance/NewInstancePage/hook.ts b/src/components/pages/console/instance/NewInstancePage/hook.ts index 3919e5817..aa931c825 100644 --- a/src/components/pages/console/instance/NewInstancePage/hook.ts +++ b/src/components/pages/console/instance/NewInstancePage/hook.ts @@ -43,7 +43,7 @@ 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 { BlockchainId } from '@/domain/connect/base' import { CreditPaymentConfiguration, PaymentConfiguration, @@ -75,7 +75,6 @@ export type Modal = 'node-list' | 'terms-and-conditions' export type UseNewInstancePageReturn = { address: string accountCreditBalance: number - blockchainName: string manuallySelectCRNDisabled: boolean manuallySelectCRNDisabledMessage?: TooltipProps['content'] createInstanceDisabled: boolean @@ -309,10 +308,6 @@ export function useNewInstancePage(): UseNewInstancePageReturn { return !!node?.terms_and_conditions }, [node]) - const blockchainName = useMemo(() => { - return blockchain ? blockchains[blockchain]?.name : 'Current network' - }, [blockchain]) - const address = useMemo(() => account?.address || '', [account]) const manuallySelectCRNDisabledMessage: UseNewInstancePageReturn['manuallySelectCRNDisabledMessage'] = @@ -437,7 +432,6 @@ export function useNewInstancePage(): UseNewInstancePageReturn { return { address, accountCreditBalance, - blockchainName, createInstanceDisabled, createInstanceButtonTitle, manuallySelectCRNDisabled, From 070fedbe18d39a97c53a2890e55d6393220a986e Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 7 Oct 2025 15:59:06 +0200 Subject: [PATCH 35/41] use price cmp --- src/components/common/Price/cmp.tsx | 5 ++- src/components/common/Price/types.ts | 3 ++ src/components/form/CheckoutSummary/cmp.tsx | 35 +++++++------------ .../form/CheckoutSummaryFooter/cmp.tsx | 4 +-- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/components/common/Price/cmp.tsx b/src/components/common/Price/cmp.tsx index 8a4573fa8..901b42d8e 100644 --- a/src/components/common/Price/cmp.tsx +++ b/src/components/common/Price/cmp.tsx @@ -6,6 +6,7 @@ import { humanReadableCurrency } from '@/helpers/utils' export const Price = ({ value, + type = 'token', duration, iconSize = '0.75em', ...rest @@ -13,7 +14,9 @@ export const Price = ({ 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..9b9b2fce0 100644 --- a/src/components/common/Price/types.ts +++ b/src/components/common/Price/types.ts @@ -1,8 +1,11 @@ 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 } diff --git a/src/components/form/CheckoutSummary/cmp.tsx b/src/components/form/CheckoutSummary/cmp.tsx index c88f54501..db6ee8552 100644 --- a/src/components/form/CheckoutSummary/cmp.tsx +++ b/src/components/form/CheckoutSummary/cmp.tsx @@ -9,6 +9,7 @@ import CheckoutSummaryFooter from '../CheckoutSummaryFooter' import { useConnection } from '@/hooks/common/useConnection' import { Blockchain } from '@aleph-sdk/core' import { useNFTVoucherBalance } from '@/hooks/common/useNFTVoucherBalance' +import Price from '@/components/common/Price' // const CheckoutSummaryVolumeLine = ({ // volume, @@ -221,21 +222,13 @@ export const CheckoutSummary = ({
{line.cost !== 0 ? ( -
- - {humanReadableCurrency(line.cost)} - - / h -
+ ) : ( - // '-' )}
@@ -278,11 +271,7 @@ export const CheckoutSummary = ({
Total credits / h
- {/* */} -
- {humanReadableCurrency(cost?.cost)} - / h -
+
@@ -291,10 +280,10 @@ export const CheckoutSummary = ({
Min. required
- {/* */} -
- {humanReadableCurrency(cost?.cost * 4)} -
+
diff --git a/src/components/form/CheckoutSummaryFooter/cmp.tsx b/src/components/form/CheckoutSummaryFooter/cmp.tsx index 6fe260765..68c12daf9 100644 --- a/src/components/form/CheckoutSummaryFooter/cmp.tsx +++ b/src/components/form/CheckoutSummaryFooter/cmp.tsx @@ -3,7 +3,7 @@ import { Button, ButtonProps } from '@aleph-front/core' import { StyledSeparator } from './styles' import { CheckoutSummaryFooterProps } from './types' import FloatingFooter from '../FloatingFooter' -import { humanReadableCurrency } from '@/helpers/utils' +import Price from '@/components/common/Price' // ------------------------------------------ @@ -37,7 +37,7 @@ export const CheckoutSummaryFooter = ({
- {humanReadableCurrency(totalCost)} + Credits / h From de8b7777aa7b9683e9982e20de1333c8950267f6 Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 7 Oct 2025 17:05:44 +0200 Subject: [PATCH 36/41] price estimation loading stte --- src/components/common/Price/cmp.tsx | 10 +++ src/components/common/Price/types.ts | 1 + src/components/form/CheckoutSummary/cmp.tsx | 18 ++++- .../form/CheckoutSummaryFooter/cmp.tsx | 3 +- .../form/CheckoutSummaryFooter/types.ts | 1 + src/hooks/common/useCanAfford.ts | 7 +- src/hooks/common/useEntityCost.ts | 81 ++++++++++++------- 7 files changed, 86 insertions(+), 35 deletions(-) diff --git a/src/components/common/Price/cmp.tsx b/src/components/common/Price/cmp.tsx index 901b42d8e..904b20dcd 100644 --- a/src/components/common/Price/cmp.tsx +++ b/src/components/common/Price/cmp.tsx @@ -3,14 +3,24 @@ 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)} diff --git a/src/components/common/Price/types.ts b/src/components/common/Price/types.ts index 9b9b2fce0..316230a28 100644 --- a/src/components/common/Price/types.ts +++ b/src/components/common/Price/types.ts @@ -8,4 +8,5 @@ export type PriceProps = HTMLAttributes & { type?: PriceType duration?: StreamDurationUnit iconSize?: string + loading?: boolean } diff --git a/src/components/form/CheckoutSummary/cmp.tsx b/src/components/form/CheckoutSummary/cmp.tsx index db6ee8552..8084c4176 100644 --- a/src/components/form/CheckoutSummary/cmp.tsx +++ b/src/components/form/CheckoutSummary/cmp.tsx @@ -171,7 +171,8 @@ export const CheckoutSummary = ({ {...{ submitButton: footerButton, mainRef, - totalCost: cost?.cost, + totalCost: cost?.cost?.cost, + loading: cost.loading, }} />
)} - {cost?.lines?.map((line) => ( + {cost?.cost?.lines?.map((line) => (
{line.name} @@ -227,6 +228,7 @@ export const CheckoutSummary = ({ value={line.cost} duration="h" className="tp-body3" + loading={cost.loading} /> ) : ( '-' @@ -271,7 +273,12 @@ export const CheckoutSummary = ({
Total credits / h
- +
@@ -282,7 +289,10 @@ export const CheckoutSummary = ({
diff --git a/src/components/form/CheckoutSummaryFooter/cmp.tsx b/src/components/form/CheckoutSummaryFooter/cmp.tsx index 68c12daf9..4aae30e31 100644 --- a/src/components/form/CheckoutSummaryFooter/cmp.tsx +++ b/src/components/form/CheckoutSummaryFooter/cmp.tsx @@ -11,6 +11,7 @@ export const CheckoutSummaryFooter = ({ submitButton: submitButtonNode, mainRef: containerRef, totalCost, + loading = false, shouldHide = true, thresholdOffset = 600, ...rest @@ -37,7 +38,7 @@ export const CheckoutSummaryFooter = ({
- + Credits / h diff --git a/src/components/form/CheckoutSummaryFooter/types.ts b/src/components/form/CheckoutSummaryFooter/types.ts index 12f7ae73b..6d9d49913 100644 --- a/src/components/form/CheckoutSummaryFooter/types.ts +++ b/src/components/form/CheckoutSummaryFooter/types.ts @@ -8,4 +8,5 @@ export type CheckoutSummaryFooterProps = Pick< submitButton?: ReactNode mainRef?: RefObject totalCost?: number + loading?: boolean } diff --git a/src/hooks/common/useCanAfford.ts b/src/hooks/common/useCanAfford.ts index c44152092..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 = { accountCreditBalance: number - cost?: CostSummary + cost: UseEntityCostReturn } export type UseCanAffordReturn = { @@ -15,7 +15,8 @@ export function useCanAfford({ cost, }: UseCanAffordProps): UseCanAffordReturn { const canAfford = - accountCreditBalance >= (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/useEntityCost.ts b/src/hooks/common/useEntityCost.ts index 18e1de309..aaad7fb27 100644 --- a/src/hooks/common/useEntityCost.ts +++ b/src/hooks/common/useEntityCost.ts @@ -46,7 +46,10 @@ 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 @@ -59,7 +62,8 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { [], ) - 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 @@ -90,34 +100,42 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { // Only make the API call when the debounced props change useEffect(() => { async function load() { + console.log('Loading entity cost with props:', debouncedProps) // 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: - console.log('Fetching program cost with props:', props) - if (programManager) result = await programManager.getCost(props) - break - case EntityType.Website: - console.log('Fetching website cost with props:', props) - 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: + console.log('Fetching program cost with props:', props) + if (programManager) result = await programManager.getCost(props) + break + case EntityType.Website: + console.log('Fetching website cost with props:', props) + 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() @@ -131,5 +149,14 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { websiteManager, ]) - return cost + const isLoading = loading || isDebouncing + + console.log('loading', { + cost, + loading: isLoading, + isDebouncing, + fetchingData: loading, + }) + + return { cost, loading: isLoading } } From fef0a916050da17116a9b8bf3d0c3c1ced81d495 Mon Sep 17 00:00:00 2001 From: gmolki Date: Tue, 7 Oct 2025 17:07:39 +0200 Subject: [PATCH 37/41] remove console logs --- src/components/common/entityData/EntityPayment/cmp.tsx | 1 - src/components/common/entityData/EntityPayment/hook.ts | 2 -- src/domain/executable.ts | 1 - src/domain/program.ts | 2 -- src/hooks/common/useEntityCost.ts | 10 ---------- src/hooks/common/useExecutableActions.ts | 1 - src/hooks/common/useSyncPaymentMethod.ts | 2 -- 7 files changed, 19 deletions(-) diff --git a/src/components/common/entityData/EntityPayment/cmp.tsx b/src/components/common/entityData/EntityPayment/cmp.tsx index ad284d612..74da686c3 100644 --- a/src/components/common/entityData/EntityPayment/cmp.tsx +++ b/src/components/common/entityData/EntityPayment/cmp.tsx @@ -136,7 +136,6 @@ const PaymentCard = ({ paymentData }: { paymentData: PaymentData }) => { * Takes an array of payment data objects and renders a card for each one */ export const EntityPayment = ({ payments }: EntityPaymentProps) => { - console.log('Rendering EntityPayment with payments:', payments) return ( <>
diff --git a/src/components/common/entityData/EntityPayment/hook.ts b/src/components/common/entityData/EntityPayment/hook.ts index 80402e567..4f153fb74 100644 --- a/src/components/common/entityData/EntityPayment/hook.ts +++ b/src/components/common/entityData/EntityPayment/hook.ts @@ -116,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%)' } diff --git a/src/domain/executable.ts b/src/domain/executable.ts index 797b5a6d4..e0d62c6f8 100644 --- a/src/domain/executable.ts +++ b/src/domain/executable.ts @@ -876,7 +876,6 @@ export abstract class ExecutableManager { receiver: payment.receiver, } } else { - console.log('Parsing payment for cost estimation:', payment) return { chain: payment?.chain || BlockchainId.ETH, type: diff --git a/src/domain/program.ts b/src/domain/program.ts index efa8034ed..14fd0453f 100644 --- a/src/domain/program.ts +++ b/src/domain/program.ts @@ -276,7 +276,6 @@ export class ProgramManager const parsedProgram: ProgramPublishConfiguration = await this.parseProgramForCostEstimation(newProgram) - console.log('Parsed program for cost estimation:', parsedProgram) const costs = await this.sdkClient.programClient.getEstimatedCost(parsedProgram) @@ -354,7 +353,6 @@ export class ProgramManager ): Promise { const { account = mockAccount, channel } = this const { isPersistent, specs } = newProgram - console.log('Parsing program for cost estimation:', newProgram) const parsedSpecs = this.parseSpecs(specs) const memory = parsedSpecs?.memory diff --git a/src/hooks/common/useEntityCost.ts b/src/hooks/common/useEntityCost.ts index aaad7fb27..64651c8dc 100644 --- a/src/hooks/common/useEntityCost.ts +++ b/src/hooks/common/useEntityCost.ts @@ -100,7 +100,6 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { // Only make the API call when the debounced props change useEffect(() => { async function load() { - console.log('Loading entity cost with props:', debouncedProps) // Skip if debouncedProps is undefined (no change detected) if (!debouncedProps) return @@ -121,11 +120,9 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { result = await gpuInstanceManager.getCost(props) break case EntityType.Program: - console.log('Fetching program cost with props:', props) if (programManager) result = await programManager.getCost(props) break case EntityType.Website: - console.log('Fetching website cost with props:', props) if (websiteManager) result = await websiteManager.getCost(props) break } @@ -151,12 +148,5 @@ export function useEntityCost(props: UseEntityCostProps): UseEntityCostReturn { const isLoading = loading || isDebouncing - console.log('loading', { - cost, - loading: isLoading, - isDebouncing, - fetchingData: loading, - }) - return { cost, loading: isLoading } } diff --git a/src/hooks/common/useExecutableActions.ts b/src/hooks/common/useExecutableActions.ts index baf6d1525..abaea16e5 100644 --- a/src/hooks/common/useExecutableActions.ts +++ b/src/hooks/common/useExecutableActions.ts @@ -121,7 +121,6 @@ export function useExecutableActions({ }, [executable, manager]) const nodeDetails = useMemo(() => { - console.log('crn', crn) if (!crn) return return { name: crn.name || crn.hash, 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]) } From ca95d0e3e7e325faaabd9328bf74e33985b12626 Mon Sep 17 00:00:00 2001 From: gmolki Date: Wed, 8 Oct 2025 10:11:12 +0200 Subject: [PATCH 38/41] filter nodes by version --- src/domain/cost.ts | 1 + src/helpers/utils.ts | 28 +++++++++++++++++++++ src/hooks/common/useCRNList.ts | 46 ++++++++++++++++++++++++++++------ 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/domain/cost.ts b/src/domain/cost.ts index 6f79b9854..5a6ea37c0 100644 --- a/src/domain/cost.ts +++ b/src/domain/cost.ts @@ -83,6 +83,7 @@ export type PricingAggregate = Record export type SettingsAggregate = { compatibleGpus: GPUDevice[] + lastCrnVersion: string communityWalletAddress: string communityWalletTimestamp: number } diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 35f2eb2bd..54f9b8987 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -657,6 +657,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/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 From a7500086e113f82ea72d0a02c18b69e3cbb369ad Mon Sep 17 00:00:00 2001 From: gmolki Date: Wed, 8 Oct 2025 10:45:07 +0200 Subject: [PATCH 39/41] feat: handle cannot start due insufficient credits --- src/components/common/BorderBox/styles.tsx | 3 + src/components/common/ExternalLink/cmp.tsx | 4 +- src/components/common/ExternalLink/styles.ts | 10 +- src/components/common/ExternalLink/types.ts | 1 + .../entityData/ManageEntityHeader/cmp.tsx | 328 ++++++++++-------- .../entityData/ManageEntityHeader/types.ts | 7 + .../console/instance/ManageInstance/cmp.tsx | 5 + .../console/instance/ManageInstance/hook.ts | 6 + 8 files changed, 222 insertions(+), 142 deletions(-) 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/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/entityData/ManageEntityHeader/cmp.tsx b/src/components/common/entityData/ManageEntityHeader/cmp.tsx index 4cdc011a1..2b0c22dc9 100644 --- a/src/components/common/entityData/ManageEntityHeader/cmp.tsx +++ b/src/components/common/entityData/ManageEntityHeader/cmp.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react' +import React, { memo, useMemo } from 'react' import { Button, Icon, Tooltip } from '@aleph-front/core' import { ManageEntityHeaderProps } from './types' import BackButton from '../../BackButton' @@ -6,6 +6,9 @@ import { useTheme } from 'styled-components' import Skeleton from '../../Skeleton' import EntityStatus from '../EntityStatus' import { RotatingLines } from 'react-loader-spinner' +import { PaymentType } from '@aleph-sdk/message' +import BorderBox from '../../BorderBox' +import ExternalLink from '../../ExternalLink' export const ManageEntityHeader = ({ // Basic data @@ -17,6 +20,12 @@ export const ManageEntityHeader = ({ // Status calculatedStatus, + // Credit balance + creditBalance, + + // Payment data + paymentData, + // Stop action showStop = false, stopDisabled, @@ -47,153 +56,192 @@ export const ManageEntityHeader = ({ }: ManageEntityHeaderProps) => { const theme = useTheme() + const cannotStart = useMemo(() => { + const isStoppedOrNotAllocated = + !isAllocated || calculatedStatus === 'stopped' + + if (!isStoppedOrNotAllocated) return false + + // Find credit payment to get the hourly cost + const creditPayment = paymentData?.find( + (payment) => payment.paymentType === PaymentType.credit, + ) + + if (!creditPayment?.cost) return false + + const hourlyCost = creditPayment.cost + const minimumHours = 4 + const requiredCredits = hourlyCost * minimumHours + + const hasInsufficientCredits = + !creditBalance || creditBalance < requiredCredits + + return hasInsufficientCredits + }, [isAllocated, calculatedStatus, paymentData, creditBalance]) + return ( -
-
-
- -
-
- -
- {entity ? name : } + <> +
+
+
+
-
-
- {showStop && ( - -
+
+ {showStop && ( + - {stopLoading ? ( - - ) : ( - - )} - - - )} - - {showStart && ( - - + + )} + + {showStart && ( + - {startLoading ? ( - - ) : ( - - )} - - - )} - - {showReboot && ( - - + + )} + + {showReboot && ( + - {rebootLoading ? ( - - ) : ( - - )} - - - )} - - {showDownload && ( - - + + )} + + {showDownload && ( + - {downloadLoading ? ( - - ) : ( - - )} - - - )} - - {showDelete && ( - - + + )} + + {showDelete && ( + - {deleteLoading ? ( - - ) : ( - - )} - - - )} + + + )} +
-
-
+
+ {cannotStart && ( +
+ +

+ ⛔️ This resource can't be started due to + insufficiency of credits. +

+

+ {' '} + your wallet to reactivate your instance again. +

+
+
+ )} + ) } ManageEntityHeader.displayName = 'ManageEntityHeader' diff --git a/src/components/common/entityData/ManageEntityHeader/types.ts b/src/components/common/entityData/ManageEntityHeader/types.ts index d175c557c..4567e9727 100644 --- a/src/components/common/entityData/ManageEntityHeader/types.ts +++ b/src/components/common/entityData/ManageEntityHeader/types.ts @@ -1,4 +1,5 @@ import { Executable, ExecutableCalculatedStatus } from '@/domain/executable' +import { PaymentData } from '@/components/common/entityData/EntityPayment/types' export type ManageEntityHeaderProps = { name: string @@ -8,6 +9,12 @@ export type ManageEntityHeaderProps = { calculatedStatus: ExecutableCalculatedStatus + // Credit balance + creditBalance?: number + + // Payment data + paymentData?: PaymentData[] + // Stop action showStop?: boolean stopDisabled?: boolean diff --git a/src/components/pages/console/instance/ManageInstance/cmp.tsx b/src/components/pages/console/instance/ManageInstance/cmp.tsx index 926daf55b..4f644694b 100644 --- a/src/components/pages/console/instance/ManageInstance/cmp.tsx +++ b/src/components/pages/console/instance/ManageInstance/cmp.tsx @@ -93,6 +93,9 @@ export default function ManageInstance() { ports, sshForwardedPort, handlePortsChange, + + // Credit balance + creditBalance, } = useManageInstance() return ( @@ -110,6 +113,8 @@ export default function ManageInstance() { type={EntityType.Instance} isAllocated={isAllocated} calculatedStatus={calculatedStatus} + creditBalance={creditBalance} + paymentData={paymentData} // Start action showStart startDisabled={startDisabled} diff --git a/src/components/pages/console/instance/ManageInstance/hook.ts b/src/components/pages/console/instance/ManageInstance/hook.ts index bac07d288..617b8760b 100644 --- a/src/components/pages/console/instance/ManageInstance/hook.ts +++ b/src/components/pages/console/instance/ManageInstance/hook.ts @@ -10,6 +10,7 @@ import { 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 @@ -17,12 +18,16 @@ export type UseManageInstanceReturn = UseManageInstanceEntityReturn & { 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 || [] @@ -68,6 +73,7 @@ export function useManageInstance(): UseManageInstanceReturn { ports, sshForwardedPort, handlePortsChange, + creditBalance, ...manageInstanceEntityProps, } } From 1b8d96ff8a893a4d1f1a3cd6e380c6e385a61464 Mon Sep 17 00:00:00 2001 From: gmolki Date: Fri, 10 Oct 2025 14:59:29 +0200 Subject: [PATCH 40/41] refactor: show stopped on cannot start --- .../common/entityData/EntityStatus/cmp.tsx | 25 +++++++++++++++---- .../common/entityData/EntityStatus/types.ts | 2 ++ .../entityData/ManageEntityHeader/cmp.tsx | 5 ++-- 3 files changed, 24 insertions(+), 8 deletions(-) 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 (