diff --git a/package.json b/package.json index ab11a54e8..87e570a23 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "@subql/contract-sdk": "^0.100.3", "@subql/network-clients": "^0.100.0", "@subql/network-config": "^0.100.0", - "@subql/network-query": "0.100.2", - "@subql/react-hooks": "0.100.3-0", + "@subql/network-query": "0.100.3-0", + "@subql/react-hooks": "0.100.3-1", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", diff --git a/src/components/IndexerDetails/Row.tsx b/src/components/IndexerDetails/Row.tsx index 2b783c91f..f4566f0b7 100644 --- a/src/components/IndexerDetails/Row.tsx +++ b/src/components/IndexerDetails/Row.tsx @@ -28,7 +28,7 @@ import { useAccount } from 'wagmi'; import { useWeb3Store } from 'src/stores'; import { useProjectStore } from 'src/stores/project'; -import { useSQToken, useWeb3 } from '../../containers'; +import { useSQToken } from '../../containers'; import { useAsyncMemo, useIndexerMetadata } from '../../hooks'; import PlaygroundIcon from '../../images/playground'; import { IndexerDetails } from '../../models'; diff --git a/src/hooks/useConsumerHostServices.ts b/src/hooks/useConsumerHostServices.tsx similarity index 72% rename from src/hooks/useConsumerHostServices.ts rename to src/hooks/useConsumerHostServices.tsx index 2981ec2ca..f4dd4dab7 100644 --- a/src/hooks/useConsumerHostServices.ts +++ b/src/hooks/useConsumerHostServices.tsx @@ -1,10 +1,11 @@ // Copyright 2020-2022 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { useEffect, useRef } from 'react'; -import { openNotification } from '@subql/components'; +import { useEffect, useRef, useState } from 'react'; +import { openNotification, Typography } from '@subql/components'; import { getAuthReqHeader, parseError, POST } from '@utils'; import { ConsumerHostMessageType, domain, EIP712Domain, withChainIdRequestBody } from '@utils/eip712'; +import { Button } from 'antd'; import axios, { AxiosResponse } from 'axios'; import { BigNumberish } from 'ethers'; import { useAccount, useSignTypedData } from 'wagmi'; @@ -35,7 +36,11 @@ export const useConsumerHostServices = ( ) => { const { address: account } = useAccount(); const { signTypedDataAsync } = useSignTypedData(); - const authHeaders = useRef<{ Authorization: string }>(); + const authHeaders = useRef<{ Authorization: string }>( + getAuthReqHeader(localStorage.getItem(`consumer-host-services-token-${account}`) || ''), + ); + const [hasLogin, setHasLogin] = useState(false); + const [loading, setLoading] = useState(true); const requestConsumerHostToken = async (account: string) => { try { @@ -72,18 +77,23 @@ export const useConsumerHostServices = ( if (error || !response?.ok || sortedResponse?.error) { throw new Error(sortedResponse?.error ?? error); } - return { data: sortedResponse?.token }; } catch (error) { return { error: parseError(error, { defaultGeneralMsg: 'Failed to request token of consumer host.', + errorMappings: [ + { + error: 'Missing consumer', + message: 'Please deposit first', + }, + ], }), }; } }; - const loginConsumerHostToken = async (refresh = false) => { + const loginConsumerHost = async (refresh = false) => { if (account) { if (!refresh) { const cachedToken = localStorage.getItem(`consumer-host-services-token-${account}`); @@ -97,6 +107,7 @@ export const useConsumerHostServices = ( } const res = await requestConsumerHostToken(account); + if (res.error) { return { status: false, @@ -107,6 +118,7 @@ export const useConsumerHostServices = ( if (res.data) { authHeaders.current = getAuthReqHeader(res.data); localStorage.setItem(`consumer-host-services-token-${account}`, res.data); + setHasLogin(true); return { status: true, msg: 'ok', @@ -116,42 +128,79 @@ export const useConsumerHostServices = ( return { status: false, - msg: 'unknow error', + msg: 'Please check your wallet if works', }; }; // do not need retry limitation // login need user confirm sign, so it's a block operation - const shouldLogin = async (res: unknown[] | object): Promise => { + const checkLoginStatusAndLogin = async (res: unknown[] | object): Promise => { if (isConsumerHostError(res) && `${res.code}` === '403') { - const loginStatus = await loginConsumerHostToken(true); + const loginStatus = await loginConsumerHost(true); if (loginStatus.status) { return true; } + } else { + setHasLogin(true); } return false; }; - // TODO: should reuse the login logic. - // but I am not sure how to write = =. + const checkIfHasLogin = async () => { + // this api do not need arguements. so use it to check if need login. + try { + setLoading(true); + const res = await getUserApiKeysApi(); + if (isConsumerHostError(res.data) && `${res.data.code}` === '403') { + setHasLogin(false); + return; + } + setHasLogin(true); + } finally { + setLoading(false); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const alertResDecorator = any>( + func: T, + ): ((...args: Parameters) => Promise>) => { + return async (...args: Parameters): Promise> => { + const res = await func(...args); + + if (alert && isConsumerHostError(res.data)) { + openNotification({ + type: 'error', + description: res.data.error, + duration: 5000, + }); + } + + return res; + }; + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const loginResDecorator = any>( + func: T, + ): ((...args: Parameters) => Promise>) => { + return async (...args: Parameters): Promise> => { + const res = await func(...args); + const sdLogin = await checkLoginStatusAndLogin(res.data); + + if (sdLogin) return await func(...args); + + return res; + }; + }; + + // the apis can use useCallback to speed up for re-render. if necessary const getUserApiKeysApi = async (): Promise> => { const res = await instance.get('/users/apikeys', { headers: authHeaders.current, }); - const sdLogin = await shouldLogin(res.data); - - if (sdLogin) return await getUserApiKeysApi(); - - if (alert && isConsumerHostError(res.data)) { - openNotification({ - type: 'error', - description: res.data.error, - duration: 5000, - }); - } - return res; }; @@ -162,18 +211,6 @@ export const useConsumerHostServices = ( headers: authHeaders.current, }); - const sdLogin = await shouldLogin(res.data); - - if (sdLogin) return await createNewApiKey(params); - - if (alert && isConsumerHostError(res.data)) { - openNotification({ - type: 'error', - description: res.data.error, - duration: 5000, - }); - } - return res; }; @@ -186,18 +223,6 @@ export const useConsumerHostServices = ( }, ); - const sdLogin = await shouldLogin(res.data); - - if (sdLogin) return await createNewApiKey(apikeyId); - - if (alert && isConsumerHostError(res.data)) { - openNotification({ - type: 'error', - description: res.data.error, - duration: 5000, - }); - } - return res; }; @@ -206,17 +231,15 @@ export const useConsumerHostServices = ( headers: authHeaders.current, }); - const sdLogin = await shouldLogin(res.data); - - if (sdLogin) return await createHostingPlanApi(params); + return res; + }; - if (alert && isConsumerHostError(res.data)) { - openNotification({ - type: 'error', - description: res.data.error, - duration: 5000, - }); - } + const updateHostingPlanApi = async ( + params: IPostHostingPlansParams & { id: string | number }, + ): Promise> => { + const res = await instance.post(`/users/hosting-plans/${params.id}`, params, { + headers: authHeaders.current, + }); return res; }; @@ -226,18 +249,6 @@ export const useConsumerHostServices = ( headers: authHeaders.current, }); - const sdLogin = await shouldLogin(res.data); - - if (sdLogin) return await getHostingPlanApi(); - - if (alert && isConsumerHostError(res.data)) { - openNotification({ - type: 'error', - description: res.data.error, - duration: 5000, - }); - } - return res; }; @@ -247,14 +258,6 @@ export const useConsumerHostServices = ( params, }); - if (alert && isConsumerHostError(res.data)) { - openNotification({ - type: 'error', - description: res.data.error, - duration: 5000, - }); - } - return res; }; @@ -263,36 +266,65 @@ export const useConsumerHostServices = ( headers: authHeaders.current, }); - const sdLogin = await shouldLogin(res.data); - - if (sdLogin) return await getUserChannelState(channelId); - - if (alert && isConsumerHostError(res.data)) { - openNotification({ - type: 'error', - description: res.data.error, - duration: 5000, - }); - } - return res; }; + const requestTokenLayout = (pageTitle: string) => { + return ( +
+ + Session Token + + + + To access {pageTitle}, you need to request a session token. + + + +
+ ); + }; + useEffect(() => { + checkIfHasLogin(); if (autoLogin) { - loginConsumerHostToken(); + loginConsumerHost(); } }, [account, autoLogin]); return { - getUserApiKeysApi, - createNewApiKey, - deleteNewApiKey, - createHostingPlanApi, - getHostingPlanApi, - getUserChannelState, - getProjects, + getUserApiKeysApi: alertResDecorator(loginResDecorator(getUserApiKeysApi)), + createNewApiKey: alertResDecorator(loginResDecorator(createNewApiKey)), + deleteNewApiKey: alertResDecorator(loginResDecorator(deleteNewApiKey)), + createHostingPlanApi: alertResDecorator(loginResDecorator(createHostingPlanApi)), + updateHostingPlanApi: alertResDecorator(loginResDecorator(updateHostingPlanApi)), + getHostingPlanApi: alertResDecorator(loginResDecorator(getHostingPlanApi)), + getUserChannelState: alertResDecorator(loginResDecorator(getUserChannelState)), + getProjects: alertResDecorator(getProjects), requestConsumerHostToken, + checkIfHasLogin, + loginConsumerHost, + requestTokenLayout, + hasLogin, + loading, }; }; @@ -325,6 +357,10 @@ export interface IGetHostingPlans { version: string; deployment_id: number; }; + project: { + metadata: string; + id: number; + }; channels: string; maximum: number; price: BigNumberish; diff --git a/src/pages/consumer/MyFlexPlans/MyFlexPlanTable.tsx b/src/pages/consumer/MyFlexPlans/MyFlexPlanTable.tsx index 00c3a2d4c..a647f6b95 100644 --- a/src/pages/consumer/MyFlexPlans/MyFlexPlanTable.tsx +++ b/src/pages/consumer/MyFlexPlans/MyFlexPlanTable.tsx @@ -4,14 +4,15 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router'; -import { TableTitle } from '@subql/components'; +import { useRouteQuery } from '@hooks'; +import { TableTitle, Typography } from '@subql/components'; import { StateChannelFieldsFragment as ConsumerFlexPlan } from '@subql/network-query'; import { ChannelStatus } from '@subql/network-query'; -import { useGetConsumerClosedFlexPlansLazyQuery, useGetConsumerOngoingFlexPlansLazyQuery } from '@subql/react-hooks'; -import { TableProps, Tag, Typography } from 'antd'; +import { useGetConsumerFlexPlansByDeploymentIdLazyQuery } from '@subql/react-hooks'; +import { TableProps, Tag } from 'antd'; +import dayjs from 'dayjs'; import { BigNumber } from 'ethers'; import i18next from 'i18next'; -import moment from 'moment'; import { AntDTable, DeploymentMeta, EmptyList, Spinner, TableText } from '../../../components'; import { ConnectedIndexer } from '../../../components/IndexerDetails/IndexerName'; @@ -23,18 +24,22 @@ import { OngoingFlexPlanActions } from './OngoingFlexPlanActions'; const { ONGOING_PLANS_NAV, CLOSED_PLANS_NAV } = ROUTES; -interface MyFlexPlanTableProps { - queryFn: typeof useGetConsumerOngoingFlexPlansLazyQuery | typeof useGetConsumerClosedFlexPlansLazyQuery; -} - -export const MyFlexPlanTable: React.FC = ({ queryFn }) => { +export const MyFlexPlanTable: React.FC = () => { const { account } = useWeb3(); const { pathname } = useLocation(); + const query = useRouteQuery(); const navigate = useNavigate(); const { t } = useTranslation(); - const [now] = React.useState(moment().toDate()); - const sortedParams = { consumer: account ?? '', now, offset: 0 }; - const [loadFlexPlan, flexPlans] = queryFn({ variables: sortedParams, fetchPolicy: 'no-cache' }); + const sortedParams = { + consumer: account ?? '', + now: dayjs('1970-1-1').toDate(), + offset: 0, + deploymentId: query.get('deploymentId') || '', + }; + const [loadFlexPlan, flexPlans] = useGetConsumerFlexPlansByDeploymentIdLazyQuery({ + variables: sortedParams, + fetchPolicy: 'no-cache', + }); const fetchMoreFlexPlans = () => { loadFlexPlan(); @@ -76,11 +81,7 @@ export const MyFlexPlanTable: React.FC = ({ queryFn }) => { dataIndex: 'expiredAt', width: 30, - title: ( - - ), + title: , render: (expiredAt) => { return ; }, @@ -105,7 +106,7 @@ export const MyFlexPlanTable: React.FC = ({ queryFn }) => width: 30, title: , render: (status: ChannelStatus, plan) => { - if (path === ONGOING_PLANS_NAV) { + if (status === ChannelStatus.OPEN && +new Date(plan.expiredAt) > +new Date()) { return {i18next.t('general.active')}; } else if (status === ChannelStatus.FINALIZED) { return {i18next.t('general.completed')}; @@ -120,13 +121,13 @@ export const MyFlexPlanTable: React.FC = ({ queryFn }) => title: , dataIndex: 'deploymentId', fixed: 'right', - width: path === CLOSED_PLANS_NAV ? 20 : 40, + width: 40, render: (_, plan) => { - if (path === CLOSED_PLANS_NAV) { - return ; + if (plan.status === ChannelStatus.OPEN && +new Date(plan.expiredAt) > +new Date()) { + return ; } - return ; + return ; }, }, ]; @@ -148,11 +149,21 @@ export const MyFlexPlanTable: React.FC = ({ queryFn }) => }, flexPlans), { loading: () => , - error: (e) => {`Failed to load flex plans: ${e}`}, + error: (e) => {`Failed to load flex plans: ${e}`}, empty: () => , data: (flexPlanList) => { return ( -
+
+ + {query.get('projectName')} + { const { loading: loadingBillingBalance, data: billingBalanceData } = consumerHostBalance; const [billBalance] = billingBalanceData ?? []; - // TODO: confirm whether need this part - // React.useEffect(() => { - // const interval = setInterval(() => { - // balance.refetch(); - // consumerHostBalance.refetch(); - - // }, 15000); - // return () => clearInterval(interval); - // }, []); - return (
@@ -63,16 +55,47 @@ const BalanceCards = () => { const Header = () => { const { t } = useTranslation(); const isLogin = useIsLogin(); + const match = useMatch(`/consumer/flex-plans/${ONGOING_PLANS}/details/:id/*`); + const navigate = useNavigate(); + const query = useRouteQuery(); + return ( <> - - {isLogin && ( + {!match ? ( <> - -
- -
+ + {isLogin && ( + <> + +
+ +
+ + )} + ) : ( +
+ + Explorer + + ), + onClick: () => { + navigate('/consumer/flex-plans/ongoing'); + }, + }, + { + key: 'current', + title: query.get('projectName'), + }, + ]} + > +
)} ); @@ -86,14 +109,8 @@ export const MyFlexPlans: React.FC = () => { componentMode element={ - } - /> - } - /> + } /> + }> } /> } /> diff --git a/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.module.less b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.module.less new file mode 100644 index 000000000..e69de29bb diff --git a/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx new file mode 100644 index 000000000..6385a047b --- /dev/null +++ b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx @@ -0,0 +1,185 @@ +// Copyright 2020-2022 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { FC, useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { useProjectMetadata } from '@containers'; +import { IGetHostingPlans, useConsumerHostServices } from '@hooks/useConsumerHostServices'; +import CreateHostingFlexPlan, { + CreateHostingFlexPlanRef, +} from '@pages/explorer/FlexPlans/CreateHostingPlan/CreateHostingPlan'; +import { Typography } from '@subql/components'; +import { formatSQT } from '@subql/react-hooks'; +import { TOKEN } from '@utils'; +import { Table } from 'antd'; +import BigNumberJs from 'bignumber.js'; + +import styles from './MyHostedPlan.module.less'; + +const MyHostedPlan: FC = (props) => { + const navigate = useNavigate(); + const { + updateHostingPlanApi, + getHostingPlanApi, + hasLogin, + loading: consumerHostLoading, + requestTokenLayout, + } = useConsumerHostServices({ + alert: true, + autoLogin: false, + }); + + const [loading, setLoading] = useState(false); + const [createdHostingPlan, setCreatedHostingPlan] = useState<(IGetHostingPlans & { projectName: string | number })[]>( + [], + ); + const [currentEditInfo, setCurrentEditInfo] = useState(); + const { getMetadataFromCid } = useProjectMetadata(); + const ref = useRef(null); + const init = async () => { + try { + setLoading(true); + if (!hasLogin) { + return; + } + + const res = await getHostingPlanApi(); + const allMetadata = await Promise.allSettled( + res.data.map((i) => { + return getMetadataFromCid(i.project.metadata); + }), + ); + setCreatedHostingPlan( + res.data.map((raw, index) => { + const result = allMetadata[index]; + const name = result.status === 'fulfilled' ? result.value.name : raw.id; + return { + ...raw, + projectName: name, + }; + }), + ); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + init(); + }, [hasLogin]); + + if (!hasLogin && !consumerHostLoading) return requestTokenLayout('Hosting Plan'); + + return ( +
+ record.id} + style={{ marginTop: 40 }} + loading={loading || consumerHostLoading} + dataSource={createdHostingPlan} + columns={[ + { + title: 'Project', + dataIndex: 'projectName', + }, + { + title: 'Maximum Price', + dataIndex: 'price', + render: (val: string) => { + return ( + + {formatSQT(BigNumberJs(val).multipliedBy(1000).toString())} {TOKEN} + + ); + }, + }, + { + title: 'Maximum Allocated indexers', + dataIndex: 'maximum', + render: (val: number) => { + return {val}; + }, + }, + { + title: 'Spent', + dataIndex: 'spent', + render: (val: string) => { + return ( + + {formatSQT(BigNumberJs(val).toString())} {TOKEN} + + ); + }, + }, + { + title: 'Action', + fixed: 'right', + dataIndex: 'spent', + render: (_, record) => { + return ( +
+ { + navigate( + `/consumer/flex-plans/ongoing/details/${record.id}?id=${record.id}&projectName=${record.projectName}&deploymentId=${record.deployment.deployment}`, + ); + }} + > + View Details + + { + setCurrentEditInfo(record); + ref.current?.showModal(); + }} + > + {record.price === '0' ? 'Restart' : 'Edit'} + + + { + if (record.price === '0') return; + try { + setLoading(true); + await updateHostingPlanApi({ + id: record.id, + deploymentId: record.deployment.deployment, + price: '0', + maximum: 2, + expiration: 0, + }); + init(); + } finally { + setLoading(false); + } + }} + > + Stop + +
+ ); + }, + }, + ]} + >
+ + init()} + > +
+ ); +}; +export default MyHostedPlan; diff --git a/src/pages/consumer/MyFlexPlans/apiKeys.module.less b/src/pages/consumer/MyFlexPlans/apiKeys.module.less index bcd17ea4a..f34ce1615 100644 --- a/src/pages/consumer/MyFlexPlans/apiKeys.module.less +++ b/src/pages/consumer/MyFlexPlans/apiKeys.module.less @@ -2,6 +2,5 @@ display: flex; flex-direction: column; align-items: center; - justify-content: center; flex:1; } diff --git a/src/pages/consumer/MyFlexPlans/apiKeys.tsx b/src/pages/consumer/MyFlexPlans/apiKeys.tsx index e37a204ed..fa77331a2 100644 --- a/src/pages/consumer/MyFlexPlans/apiKeys.tsx +++ b/src/pages/consumer/MyFlexPlans/apiKeys.tsx @@ -4,7 +4,7 @@ import React, { ChangeEvent, FC, useEffect, useRef, useState } from 'react'; import PasswordField from '@components/PasswordField'; import { GetUserApiKeys, isConsumerHostError, useConsumerHostServices } from '@hooks/useConsumerHostServices'; -import { Modal, openNotification, TextInput, Typography } from '@subql/components'; +import { Modal, openNotification, Spinner, TextInput, Typography } from '@subql/components'; import { Table } from 'antd'; import moment from 'moment'; @@ -29,27 +29,50 @@ const EmptyApiKeys: FC<{ children: React.ReactNode }> = ({ children }) => { }; const ApiKeysFC: FC = () => { - const { getUserApiKeysApi, createNewApiKey, deleteNewApiKey } = useConsumerHostServices({ + const { + getUserApiKeysApi, + createNewApiKey, + deleteNewApiKey, + requestTokenLayout, + hasLogin, + loading: consumerHostLoading, + } = useConsumerHostServices({ alert: true, + autoLogin: false, }); const [openCreateNew, setOpenCreateNew] = useState(false); const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false); + const [loading, setLoading] = useState(false); const currentApiKey = useRef(); const [newApiKeyName, setNewApiKeyName] = useState(''); const [apiKeys, setApiKeys] = useState([]); const init = async () => { - const res = await getUserApiKeysApi(); - - if (!isConsumerHostError(res.data)) { - setApiKeys(res.data); + try { + setLoading(true); + if (!hasLogin) return; + const res = await getUserApiKeysApi(); + if (!isConsumerHostError(res.data)) { + setApiKeys(res.data); + } + } finally { + setLoading(false); } }; useEffect(() => { - init(); - }, []); + if (hasLogin) { + init(); + } + }, [hasLogin]); + + if (consumerHostLoading || loading) + return ( +
+ +
+ ); return (
@@ -113,7 +136,7 @@ const ApiKeysFC: FC = () => { rowKey={'id'} >
- ) : ( + ) : hasLogin ? ( + ) : ( + requestTokenLayout('API Key') )} { +export interface CreateHostingFlexPlanRef { + showModal: () => void; +} + +const CreateHostingFlexPlan = forwardRef< + CreateHostingFlexPlanRef, + { + id?: string; + deploymentId?: string; + editInformation?: IGetHostingPlans; + edit?: boolean; + hideBoard?: boolean; + onSubmit?: () => void; + } +>((props, ref) => { const { account } = useWeb3(); const { consumerHostBalance } = useSQToken(); - const { id } = useParams<{ id: string }>(); + const params = useParams<{ id: string }>(); const navigate = useNavigate(); const query = useRouteQuery(); - const { getProjects } = useConsumerHostServices({ autoLogin: false }); + const { getProjects, createHostingPlanApi, updateHostingPlanApi, getHostingPlanApi, hasLogin } = + useConsumerHostServices({ + alert: true, + autoLogin: false, + }); + + const id = useMemo(() => { + return props.id || params.id; + }, [params, props]); + const deploymentId = useMemo(() => { + return props.deploymentId || query.get('deploymentId') || undefined; + }, [query, props]); const asyncProject = useProjectFromQuery(id ?? ''); - const { createHostingPlanApi, getHostingPlanApi } = useConsumerHostServices({ alert: true, autoLogin: false }); const [form] = Form.useForm(); const priceValue = Form.useWatch('price', form); @@ -44,9 +70,10 @@ const CreateHostingFlexPlan: FC = (props) => { const flexPlans = useAsyncMemo(async () => { try { + if (!hasLogin) return []; const res = await getProjects({ projectId: BigNumber.from(id).toString(), - deployment: query.get('deploymentId') || undefined, + deployment: deploymentId, }); if (res.data?.indexers?.length) { @@ -55,7 +82,7 @@ const CreateHostingFlexPlan: FC = (props) => { } catch (e) { return []; } - }, [id, query]); + }, [id, query, hasLogin]); const matchedCount = React.useMemo(() => { if (!priceValue || !flexPlans.data?.length) return `Matched indexers: 0`; @@ -79,23 +106,32 @@ const CreateHostingFlexPlan: FC = (props) => { const createHostingPlan = async () => { await form.validateFields(); - if (!asyncProject.data?.deploymentId) return; - const created = await getHostingPlans(); - if (!created) return; - if (created && haveCreatedHostingPlan.checkHaveCreated(created)) { - setShowCreateFlexPlan(false); - return; + if (!props.edit) { + if (!asyncProject.data?.deploymentId) return; + const created = await getHostingPlans(); + + if (!created) return; + if (created && haveCreatedHostingPlan.checkHaveCreated(created)) { + setShowCreateFlexPlan(false); + return; + } } - const res = await createHostingPlanApi({ + const api = props.edit ? updateHostingPlanApi : createHostingPlanApi; + const res = await api({ ...form.getFieldsValue(), // default set as one era. expiration: flexPlans?.data?.sort((a, b) => b.max_time - a.max_time)[0].max_time || 3600 * 24 * 7, price: parseEther(`${form.getFieldValue('price')}`) .div(1000) .toString(), - deploymentId: asyncProject.data.deploymentId, + + // props.deploymentId or asyncProject.deploymentId must have one. + deploymentId: props.deploymentId || asyncProject?.data?.deploymentId || '', + + // if is create, id is would not use. + id: `${props.editInformation?.id}` || '0', }); if (res.data.id) { @@ -110,6 +146,7 @@ const CreateHostingFlexPlan: FC = (props) => { }; const getHostingPlans = async () => { + if (!hasLogin) return; const res = await getHostingPlanApi(); if (!isConsumerHostError(res.data)) { setCreatedHostingPlan(res.data); @@ -117,86 +154,109 @@ const CreateHostingFlexPlan: FC = (props) => { } }; + useImperativeHandle(ref, () => ({ + showModal: () => { + setShowCreateFlexPlan(true); + }, + })); + React.useEffect(() => { getHostingPlans(); - }, [account]); + }, [account, hasLogin]); + + useEffect(() => { + if (props.editInformation) { + form.setFieldValue( + 'price', + +formatSQT( + BigNumberJs(props.editInformation.price.toString() || '0') + .multipliedBy(1000) + .toString(), + ), + ); + form.setFieldValue('maximum', props.editInformation.maximum); + } + }, [props.editInformation]); return ( <> -
-
-
-
- - {t('flexPlans.billBalance').toUpperCase()} - + {!props.hideBoard && ( +
+
+
+
+ + {t('flexPlans.billBalance').toUpperCase()} + - - - -
+ + + +
- - {`${formatEther(balance, 4)} ${TOKEN}`} - + + {`${formatEther(balance, 4)} ${TOKEN}`} + +
+
- -
- -
- -
- - {t('flexPlans.flexPlan')} - - - {t('flexPlans.flexPlanDesc')} - + + {haveCreatedHostingPlan.haveCreated ? ( + { + navigate(`/consumer/flex-plans?deploymentCid=${asyncProject.data?.deploymentId}`); + }} + > + View My Flex Plan + + ) : ( + + )}
- - {haveCreatedHostingPlan.haveCreated ? ( - { - navigate(`/consumer/flex-plans?deploymentCid=${asyncProject.data?.deploymentId}`); - }} - > - View My Flex Plan - - ) : ( - - )}
-
+ )} { await createHostingPlan(); + props.onSubmit?.(); }} onCancel={() => { setShowCreateFlexPlan(false); @@ -259,5 +319,5 @@ const CreateHostingFlexPlan: FC = (props) => { ); -}; +}); export default CreateHostingFlexPlan; diff --git a/src/pages/explorer/FlexPlans/FlexPlans.tsx b/src/pages/explorer/FlexPlans/FlexPlans.tsx index 6cdfb5a84..19ef2f054 100644 --- a/src/pages/explorer/FlexPlans/FlexPlans.tsx +++ b/src/pages/explorer/FlexPlans/FlexPlans.tsx @@ -24,7 +24,7 @@ export const FlexPlans: React.FC = () => { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const query = useRouteQuery(); - const { getProjects } = useConsumerHostServices({ autoLogin: false }); + const { getProjects, requestTokenLayout, hasLogin, loading } = useConsumerHostServices({ autoLogin: false }); const { getFlexPlanPrice } = useGetFlexPlanPrice(); // TODO: confirm score threadThread with consumer host service const getColumns = (): TableProps['columns'] => [ @@ -61,6 +61,7 @@ export const FlexPlans: React.FC = () => { const flexPlans = useAsyncMemo(async () => { try { + if (!hasLogin) return []; const res = await getProjects({ projectId: BigNumber.from(id).toString(), deployment: query.get('deploymentId') || undefined, @@ -72,7 +73,7 @@ export const FlexPlans: React.FC = () => { } catch (e) { return []; } - }, [id, query]); + }, [id, query, hasLogin]); React.useEffect(() => { if (!id) { @@ -80,21 +81,33 @@ export const FlexPlans: React.FC = () => { } }, [navigate, id]); + if (!flexPlans.data && !flexPlans.loading && !loading) return ; + return ( <> - {renderAsync(flexPlans, { - loading: () => , - error: (e) => {'Failed to load flex plan.'}, - data: (flexPlans) => { - if (!flexPlans.length) return ; - return ( - <> - - - - ); + {renderAsync( + { + ...flexPlans, + loading: flexPlans.loading || loading, + }, + { + loading: () => , + error: (e) => {'Failed to load flex plan.'}, + data: (flexPlans) => { + if (!flexPlans.length && hasLogin) return ; + return ( + <> + + {!hasLogin ? ( + requestTokenLayout('flex plan') + ) : ( +
+ )} + + ); + }, }, - })} + )} ); }; diff --git a/src/pages/explorer/Home/Home.tsx b/src/pages/explorer/Home/Home.tsx index ba581f5e4..86562ed69 100644 --- a/src/pages/explorer/Home/Home.tsx +++ b/src/pages/explorer/Home/Home.tsx @@ -95,7 +95,6 @@ const Home: React.FC = () => { } else { setInSearchMode(false); } - console.warn(options); const api = searchKeywords.length ? getProjectBySearch : getProjects; const params = searchKeywords.length diff --git a/src/utils/parseError.ts b/src/utils/parseError.ts index df45b0c4a..145ffb9b2 100644 --- a/src/utils/parseError.ts +++ b/src/utils/parseError.ts @@ -93,7 +93,6 @@ export function parseError( if (!error) return; logError(error); const rawErrorMsg = error?.data?.message ?? error?.message ?? error?.error ?? error ?? ''; - const mappingError = () => (options.errorMappings || errorsMapping).find((e) => rawErrorMsg.match(e.error))?.message; const mapContractError = () => { const revertCode = Object.keys(contractErrorCodes).find((key) => diff --git a/yarn.lock b/yarn.lock index 964e3c88c..b539b0074 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4614,13 +4614,14 @@ jwt-decode "^3.1.2" lru-cache "^10.0.1" -"@subql/components@1.0.3-18": - version "1.0.3-18" - resolved "https://registry.npmjs.org/@subql/components/-/components-1.0.3-18.tgz#9eb0d5505be9374831b41aeff99633b01e0ec047" - integrity sha512-BkKIm7VGsEjShCRVS3Ffz4R6rgDxLY0q5y0LXqk2XWkoeEoRhofVYzJlc7K8jRzHbydJq0GVLUlM9akdxQHeHA== +"@subql/components@1.0.3-20": + version "1.0.3-20" + resolved "https://registry.npmjs.org/@subql/components/-/components-1.0.3-20.tgz#c7b7b8b22c68a7d0eb1c5ac6eb22af61c6c43c3b" + integrity sha512-2nmfjbnUXRc634ctnNqzYSJBhLrOndI8BoOUZxurihYOZ1qEfDsMeNCIptFnU4aQDUlSd0GyEctlqvsSkniEtg== dependencies: "@graphiql/plugin-explorer" "^0.3.4" "@graphiql/toolkit" "^0.9.1" + ahooks "^3.7.8" antd "^5.8.4" clsx "^1.2.1" graphiql "^3.0.5" @@ -4630,6 +4631,7 @@ ra-data-graphql "^4.11.3" react-icons "^4.8.0" react-jazzicon "^0.1.3" + react-markdown "^9.0.0" react-router-dom "^6.4.2" rollup-plugin-copy "^3.4.0" string-width "4.2.3" @@ -4667,10 +4669,10 @@ dependencies: graphql "^16.5.0" -"@subql/network-query@0.100.2": - version "0.100.2" - resolved "https://registry.npmjs.org/@subql/network-query/-/network-query-0.100.2.tgz#5f44100dd144c4139291b8258a1d4110d39f896f" - integrity sha512-QEDs0HZj+ChADhKzaoIbx3VKQlGr9PPuu3ErRkCGcMrtFIFO9/N0CdR7loDmUCf9xHB0Y5goFLenSCYNWZTrTQ== +"@subql/network-query@0.100.3-0": + version "0.100.3-0" + resolved "https://registry.npmjs.org/@subql/network-query/-/network-query-0.100.3-0.tgz#46fa62091c815a383a3e276b6f8e6ee0c32a22d1" + integrity sha512-VJBjlcXAece8xjK/7Ii8cEuBvmgXyf3h2bLVtrX6vDuM5+cddn4xpl3pY9tLA330G5/mkQd2GmP+A9lHO7RkDA== dependencies: graphql "^16.5.0" @@ -4685,15 +4687,15 @@ jwt-decode "^3.1.2" lru-cache "^10.0.1" -"@subql/react-hooks@0.100.3-0": - version "0.100.3-0" - resolved "https://registry.npmjs.org/@subql/react-hooks/-/react-hooks-0.100.3-0.tgz#c399ae23e54470abeb91d0841190fed5fdcc767a" - integrity sha512-lvpoz1CURkf/iaqXaXc5SvYUB55ncUQq5crsnngvJXC9gCdsYt6gn69ZqPYC3TolWMsQhdJ+hygw36bpxd1akA== +"@subql/react-hooks@0.100.3-1": + version "0.100.3-1" + resolved "https://registry.npmjs.org/@subql/react-hooks/-/react-hooks-0.100.3-1.tgz#2fb270747568b6f7d0ab8b75b6a114b8984ee2f9" + integrity sha512-HdYx+rNDu8qyzF6gn38w0hbtZvQ2Rupbcnem5jA8/lbgxVF05k0K6nAsBOdNUvxybp7YijPJQqxUAZ2nw0NWZg== dependencies: "@graphql-tools/code-file-loader" "^7.3.6" "@graphql-tools/graphql-tag-pluck" "^7.3.6" "@graphql-tools/load" "^7.7.7" - "@subql/network-query" "0.100.2" + "@subql/network-query" "0.100.3-0" ahooks "^3.7.8" bignumber.js "^9.1.2" class-transformer "^0.5.1"