From 6af42fce5878016876528e12d9346e8522fa9d5d Mon Sep 17 00:00:00 2001 From: cyrbuzz Date: Tue, 28 Nov 2023 14:12:21 +0800 Subject: [PATCH 1/7] lock --- yarn.lock | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 964e3c88c..a97cf26f9 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" From eef1734d9c31d4d01b54cdfcfd709f92e2ee3148 Mon Sep 17 00:00:00 2001 From: cyrbuzz Date: Wed, 22 Nov 2023 17:34:57 +0800 Subject: [PATCH 2/7] feat: plans --- src/components/IndexerDetails/Row.tsx | 2 +- .../consumer/MyFlexPlans/MyFlexPlans.tsx | 25 ++------ .../MyHostedPlan/MyHostedPlan.module.less | 0 .../MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx | 64 +++++++++++++++++++ 4 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.module.less create mode 100644 src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx 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/pages/consumer/MyFlexPlans/MyFlexPlans.tsx b/src/pages/consumer/MyFlexPlans/MyFlexPlans.tsx index 836654daf..27ee97c79 100644 --- a/src/pages/consumer/MyFlexPlans/MyFlexPlans.tsx +++ b/src/pages/consumer/MyFlexPlans/MyFlexPlans.tsx @@ -5,23 +5,23 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { Navigate, Route, Routes } from 'react-router'; import { useIsLogin } from '@hooks/useIsLogin'; -import { useGetConsumerClosedFlexPlansLazyQuery, useGetConsumerOngoingFlexPlansLazyQuery } from '@subql/react-hooks'; +import { useGetConsumerOngoingFlexPlansLazyQuery } from '@subql/react-hooks'; import i18next from 'i18next'; import { AppPageHeader, Card, TabButtons, WalletRoute } from '../../../components'; import { useSQToken } from '../../../containers'; import { formatEther, TOKEN } from '../../../utils'; import { ROUTES } from '../../../utils'; +import MyHostedPlan from './MyHostedPlan/MyHostedPlan'; import ApiKeys from './apiKeys'; import { BillingAction } from './BillingAction'; import styles from './MyFlexPlans.module.css'; import { MyFlexPlanTable } from './MyFlexPlanTable'; -const { ONGOING_PLANS, EXPIRED_PLANS, API_KEY } = ROUTES; +const { ONGOING_PLANS, API_KEY } = ROUTES; const buttonLinks = [ { label: i18next.t('myFlexPlans.ongoing'), link: ONGOING_PLANS }, - { label: i18next.t('myFlexPlans.closed'), link: EXPIRED_PLANS }, { label: i18next.t('myFlexPlans.apiKey'), link: API_KEY }, ]; @@ -33,16 +33,6 @@ const BalanceCards = () => { 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 (
@@ -86,14 +76,11 @@ 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..21caf6325 --- /dev/null +++ b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx @@ -0,0 +1,64 @@ +// Copyright 2020-2022 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { FC } from 'react'; +import { useNavigate } from 'react-router'; +import { SubqlTable, Typography } from '@subql/components'; + +import styles from './MyHostedPlan.module.less'; + +interface IProps {} + +const MyHostedPlan: FC = (props) => { + const navigate = useNavigate(); + + return ( +
+ { + return ( +
+ { + navigate(`/consumer/flex-plans/ongoing/details/1`); + }} + > + View Details + + { + navigate(`/consumer/flex-plans/ongoing/details/1`); + }} + > + Edit + +
+ ); + }, + }, + ]} + >
+
+ ); +}; +export default MyHostedPlan; From 8b6a045243fd8fe016f5b45d2de65db5863834e4 Mon Sep 17 00:00:00 2001 From: cyrbuzz Date: Tue, 28 Nov 2023 16:59:41 +0800 Subject: [PATCH 3/7] feat: refactor --- ...ervices.ts => useConsumerHostServices.tsx} | 209 ++++++++++-------- .../consumer/MyFlexPlans/apiKeys.module.less | 1 - src/pages/consumer/MyFlexPlans/apiKeys.tsx | 17 +- .../CreateHostingPlan/CreateHostingPlan.tsx | 8 +- src/pages/explorer/FlexPlans/FlexPlans.tsx | 13 +- src/utils/parseError.ts | 1 - 6 files changed, 137 insertions(+), 112 deletions(-) rename src/hooks/{useConsumerHostServices.ts => useConsumerHostServices.tsx} (75%) diff --git a/src/hooks/useConsumerHostServices.ts b/src/hooks/useConsumerHostServices.tsx similarity index 75% rename from src/hooks/useConsumerHostServices.ts rename to src/hooks/useConsumerHostServices.tsx index 2981ec2ca..8bc8bd0a6 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,10 @@ 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 requestConsumerHostToken = async (account: string) => { try { @@ -72,18 +76,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 +106,7 @@ export const useConsumerHostServices = ( } const res = await requestConsumerHostToken(account); + if (res.error) { return { status: false, @@ -107,6 +117,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 +127,74 @@ 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 argements. so use it to check if need login. + const res = await getUserApiKeysApi(); + if (isConsumerHostError(res.data) && `${res.data.code}` === '403') { + setHasLogin(false); + return; + } + setHasLogin(true); + }; + + // 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 +205,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 +217,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,18 +225,6 @@ export const useConsumerHostServices = ( headers: authHeaders.current, }); - const sdLogin = await shouldLogin(res.data); - - if (sdLogin) return await createHostingPlanApi(params); - - if (alert && isConsumerHostError(res.data)) { - openNotification({ - type: 'error', - description: res.data.error, - duration: 5000, - }); - } - return res; }; @@ -226,18 +233,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 +242,6 @@ export const useConsumerHostServices = ( params, }); - if (alert && isConsumerHostError(res.data)) { - openNotification({ - type: 'error', - description: res.data.error, - duration: 5000, - }); - } - return res; }; @@ -263,36 +250,62 @@ 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)), + getHostingPlanApi: alertResDecorator(loginResDecorator(getHostingPlanApi)), + getUserChannelState: alertResDecorator(loginResDecorator(getUserChannelState)), + getProjects: alertResDecorator(getProjects), requestConsumerHostToken, + checkIfHasLogin, + loginConsumerHost, + requestTokenLayout, + hasLogin, }; }; 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..144bc4552 100644 --- a/src/pages/consumer/MyFlexPlans/apiKeys.tsx +++ b/src/pages/consumer/MyFlexPlans/apiKeys.tsx @@ -29,9 +29,12 @@ const EmptyApiKeys: FC<{ children: React.ReactNode }> = ({ children }) => { }; const ApiKeysFC: FC = () => { - const { getUserApiKeysApi, createNewApiKey, deleteNewApiKey } = useConsumerHostServices({ - alert: true, - }); + const { getUserApiKeysApi, createNewApiKey, deleteNewApiKey, requestTokenLayout, hasLogin } = useConsumerHostServices( + { + alert: true, + autoLogin: false, + }, + ); const [openCreateNew, setOpenCreateNew] = useState(false); const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false); @@ -40,8 +43,8 @@ const ApiKeysFC: FC = () => { const [apiKeys, setApiKeys] = useState([]); const init = async () => { + if (!hasLogin) return; const res = await getUserApiKeysApi(); - if (!isConsumerHostError(res.data)) { setApiKeys(res.data); } @@ -49,7 +52,7 @@ const ApiKeysFC: FC = () => { useEffect(() => { init(); - }, []); + }, [hasLogin]); return (
@@ -113,7 +116,7 @@ const ApiKeysFC: FC = () => { rowKey={'id'} >
- ) : ( + ) : hasLogin ? ( + ) : ( + requestTokenLayout('API Key') )} { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const query = useRouteQuery(); - const { getProjects } = useConsumerHostServices({ autoLogin: false }); + const { getProjects, createHostingPlanApi, getHostingPlanApi, hasLogin } = useConsumerHostServices({ + alert: true, + autoLogin: false, + }); const asyncProject = useProjectFromQuery(id ?? ''); - const { createHostingPlanApi, getHostingPlanApi } = useConsumerHostServices({ alert: true, autoLogin: false }); const [form] = Form.useForm(); const priceValue = Form.useWatch('price', form); @@ -44,6 +46,7 @@ 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, @@ -110,6 +113,7 @@ const CreateHostingFlexPlan: FC = (props) => { }; const getHostingPlans = async () => { + if (!hasLogin) return; const res = await getHostingPlanApi(); if (!isConsumerHostError(res.data)) { setCreatedHostingPlan(res.data); diff --git a/src/pages/explorer/FlexPlans/FlexPlans.tsx b/src/pages/explorer/FlexPlans/FlexPlans.tsx index 6cdfb5a84..0afc5e4e8 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, checkIfHasLogin, hasLogin } = 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) { @@ -86,11 +87,15 @@ export const FlexPlans: React.FC = () => { loading: () => , error: (e) => {'Failed to load flex plan.'}, data: (flexPlans) => { - if (!flexPlans.length) return ; + if (!flexPlans.length && hasLogin) return ; return ( <> - + {!hasLogin ? ( + requestTokenLayout('flex plan') + ) : ( +
+ )} ); }, 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) => From 50cc9a9491bf203cfac2d5a5ff6d2a0665750fa2 Mon Sep 17 00:00:00 2001 From: cyrbuzz Date: Tue, 28 Nov 2023 18:09:56 +0800 Subject: [PATCH 4/7] feat: edit --- src/hooks/useConsumerHostServices.tsx | 11 + .../MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx | 102 +++++++- .../CreateHostingPlan/CreateHostingPlan.tsx | 220 +++++++++++------- src/pages/explorer/Home/Home.tsx | 1 - 4 files changed, 241 insertions(+), 93 deletions(-) diff --git a/src/hooks/useConsumerHostServices.tsx b/src/hooks/useConsumerHostServices.tsx index 8bc8bd0a6..c9d25dba1 100644 --- a/src/hooks/useConsumerHostServices.tsx +++ b/src/hooks/useConsumerHostServices.tsx @@ -228,6 +228,16 @@ export const useConsumerHostServices = ( return res; }; + 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; + }; + const getHostingPlanApi = async (): Promise> => { const res = await instance.get(`/users/hosting-plans`, { headers: authHeaders.current, @@ -298,6 +308,7 @@ export const useConsumerHostServices = ( 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), diff --git a/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx index 21caf6325..26af9f04b 100644 --- a/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx +++ b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx @@ -1,39 +1,95 @@ // Copyright 2020-2022 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { FC } from 'react'; +import React, { FC, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; -import { SubqlTable, Typography } from '@subql/components'; +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'; -interface IProps {} - -const MyHostedPlan: FC = (props) => { +const MyHostedPlan: FC = (props) => { const navigate = useNavigate(); + const { updateHostingPlanApi, getHostingPlanApi, hasLogin } = useConsumerHostServices({ + alert: true, + autoLogin: false, + }); + + const [loading, setLoading] = useState(false); + const [createdHostingPlan, setCreatedHostingPlan] = useState([]); + const [currentEditInfo, setCurrentEditInfo] = useState(); + const ref = useRef(null); + // const { getMetadataFromCid } = useProjectMetadata() + const init = async () => { + try { + setLoading(true); + if (!hasLogin) { + return; + } + + const res = await getHostingPlanApi(); + setCreatedHostingPlan(res.data); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + init(); + }, [hasLogin]); return (
- { + 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', - render: () => { + dataIndex: 'spent', + render: (_, record) => { return (
= (props) => { { - navigate(`/consumer/flex-plans/ongoing/details/1`); + setCurrentEditInfo(record); + ref.current?.showModal(); }} > Edit + + { + updateHostingPlanApi({ + id: record.id, + deploymentId: record.deployment.deployment, + price: '0', + maximum: 2, + expiration: 0, + }); + }} + > + Stop +
); }, }, ]} - >
+ >
+ + init()} + >
); }; diff --git a/src/pages/explorer/FlexPlans/CreateHostingPlan/CreateHostingPlan.tsx b/src/pages/explorer/FlexPlans/CreateHostingPlan/CreateHostingPlan.tsx index 3c18bd48b..23c8d0514 100644 --- a/src/pages/explorer/FlexPlans/CreateHostingPlan/CreateHostingPlan.tsx +++ b/src/pages/explorer/FlexPlans/CreateHostingPlan/CreateHostingPlan.tsx @@ -1,7 +1,7 @@ // Copyright 2020-2022 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { FC } from 'react'; +import React, { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react'; import { AiOutlineInfoCircle } from 'react-icons/ai'; import { BsExclamationCircle } from 'react-icons/bs'; import { useNavigate, useParams } from 'react-router-dom'; @@ -16,24 +16,48 @@ import { useConsumerHostServices, } from '@hooks/useConsumerHostServices'; import { Modal, openNotification, Steps, Typography } from '@subql/components'; +import { formatSQT } from '@subql/react-hooks'; import { convertStringToNumber, formatEther, TOKEN, tokenDecimals } from '@utils'; import { Button, Divider, Form, InputNumber, Tooltip } from 'antd'; +import BigNumberJs from 'bignumber.js'; import { BigNumber } from 'ethers'; import { formatUnits, parseEther } from 'ethers/lib/utils'; import { t } from 'i18next'; import styles from './index.module.less'; -const CreateHostingFlexPlan: FC = (props) => { +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, createHostingPlanApi, getHostingPlanApi, hasLogin } = useConsumerHostServices({ - alert: true, - 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 ?? ''); @@ -49,7 +73,7 @@ const CreateHostingFlexPlan: FC = (props) => { if (!hasLogin) return []; const res = await getProjects({ projectId: BigNumber.from(id).toString(), - deployment: query.get('deploymentId') || undefined, + deployment: deploymentId, }); if (res.data?.indexers?.length) { @@ -58,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`; @@ -82,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) { @@ -121,86 +154,109 @@ const CreateHostingFlexPlan: FC = (props) => { } }; + useImperativeHandle(ref, () => ({ + showModal: () => { + setShowCreateFlexPlan(true); + }, + })); + React.useEffect(() => { getHostingPlans(); }, [account]); + 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); @@ -263,5 +319,5 @@ const CreateHostingFlexPlan: FC = (props) => { ); -}; +}); export default CreateHostingFlexPlan; 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 From 90e3739999d6a7ea8ca6b1d9f33da30ac2da865b Mon Sep 17 00:00:00 2001 From: cyrbuzz Date: Wed, 29 Nov 2023 11:45:09 +0800 Subject: [PATCH 5/7] feat: edit --- src/hooks/useConsumerHostServices.tsx | 24 ++++-- .../consumer/MyFlexPlans/MyFlexPlanTable.tsx | 10 +-- .../consumer/MyFlexPlans/MyFlexPlans.tsx | 73 ++++++++++++++++--- .../MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx | 71 ++++++++++++++---- src/pages/consumer/MyFlexPlans/apiKeys.tsx | 44 ++++++++--- .../CreateHostingPlan/CreateHostingPlan.tsx | 2 +- src/pages/explorer/FlexPlans/FlexPlans.tsx | 42 ++++++----- 7 files changed, 200 insertions(+), 66 deletions(-) diff --git a/src/hooks/useConsumerHostServices.tsx b/src/hooks/useConsumerHostServices.tsx index c9d25dba1..f4dd4dab7 100644 --- a/src/hooks/useConsumerHostServices.tsx +++ b/src/hooks/useConsumerHostServices.tsx @@ -40,6 +40,7 @@ export const useConsumerHostServices = ( getAuthReqHeader(localStorage.getItem(`consumer-host-services-token-${account}`) || ''), ); const [hasLogin, setHasLogin] = useState(false); + const [loading, setLoading] = useState(true); const requestConsumerHostToken = async (account: string) => { try { @@ -147,13 +148,18 @@ export const useConsumerHostServices = ( }; const checkIfHasLogin = async () => { - // this api do not need argements. so use it to check if need login. - const res = await getUserApiKeysApi(); - if (isConsumerHostError(res.data) && `${res.data.code}` === '403') { - setHasLogin(false); - return; + // 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); } - setHasLogin(true); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -279,6 +285,7 @@ export const useConsumerHostServices = (