diff --git a/package.json b/package.json index 774c65e9d..fd474d027 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@rainbow-me/rainbowkit": "^1.2.0", "@sentry/react": "^7.57.0", "@subql/apollo-links": "^1.2.3", - "@subql/components": "1.0.3-21", + "@subql/components": "1.0.3-22", "@subql/contract-sdk": "^0.100.3", "@subql/network-clients": "^0.100.0", "@subql/network-config": "^0.100.0", diff --git a/src/components/DeploymentInfo/DeploymentInfo.tsx b/src/components/DeploymentInfo/DeploymentInfo.tsx index 2835a2b36..2c968e3da 100644 --- a/src/components/DeploymentInfo/DeploymentInfo.tsx +++ b/src/components/DeploymentInfo/DeploymentInfo.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import UnsafeWarn from '@components/UnsafeWarn'; +import { useGetIfUnsafeDeployment } from '@hooks/useGetIfUnsafeDeployment'; import { Spinner, Typography } from '@subql/components'; import { Tooltip } from 'antd'; @@ -22,10 +23,11 @@ type Props = { deploymentVersion?: string; }; -export const DeploymentInfo: React.FC = ({ project, deploymentId, deploymentVersion }) => { +export const DeploymentInfo: React.FC = ({ project, deploymentId }) => { const { t } = useTranslation(); const deploymentMeta = useDeploymentMetadata(deploymentId); + const { isUnsafe } = useGetIfUnsafeDeployment(deploymentId); const versionHeader = deploymentMeta.data?.version ? `${deploymentMeta.data?.version} - ${t('projects.deploymentId')}:` : t('projects.deploymentId'); @@ -42,7 +44,7 @@ export const DeploymentInfo: React.FC = ({ project, deploymentId, deploym {project?.name} )} - {!!deploymentMeta.data?.unsafe && } + {isUnsafe && }
diff --git a/src/components/Modal/Modal.module.css b/src/components/Modal/Modal.module.css index ba80db781..5d0380d4f 100644 --- a/src/components/Modal/Modal.module.css +++ b/src/components/Modal/Modal.module.css @@ -1,5 +1,5 @@ .steps { - margin: 1.25rem 0; + margin: 0 0 1.25rem 0; } .title { diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 5f464e4be..c12d34010 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; -import { Modal as AntDModal, Steps as AntDSteps, Typography } from 'antd'; +import { Modal as SubqlModal } from '@subql/components'; +import { Steps as AntDSteps, Typography } from 'antd'; import clsx from 'clsx'; import Spinner from '../Spinner'; @@ -54,7 +55,7 @@ export const Modal: React.FC = ({ }; return ( - } open={visible} onOk={onOk} @@ -76,6 +77,6 @@ export const Modal: React.FC = ({ {content} )} - + ); }; diff --git a/src/components/ModalStatus/ModalStatus.module.css b/src/components/ModalStatus/ModalStatus.module.css deleted file mode 100644 index 60d6268c6..000000000 --- a/src/components/ModalStatus/ModalStatus.module.css +++ /dev/null @@ -1,28 +0,0 @@ -.container { - margin: 1rem; -} - -.status { - display: flex; - flex-direction: column; -} - -.statusTitle { - display: flex; -} - -.statusDescription { - margin: 1rem 0; -} - -.successIcon { - color: var(--success) !important; -} - -.errorIcon { - color: var(--error); -} - -.statusText { - margin: 0 1rem; -} diff --git a/src/components/ModalStatus/ModalStatus.tsx b/src/components/ModalStatus/ModalStatus.tsx deleted file mode 100644 index 90434a3db..000000000 --- a/src/components/ModalStatus/ModalStatus.tsx +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2020-2022 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { FaCheckSquare, FaWindowClose } from 'react-icons/fa'; -import { Typography } from '@subql/components'; -import { Modal as AntDModal } from 'antd'; - -import styles from './ModalStatus.module.css'; - -/** - * NOTE: - * Using antd - * Waiting for SubQuery components lib(also based on antD) release and replace - */ - -interface ModalStatusProps { - visible: boolean; - title?: string; - success?: boolean; - successText?: string; - error?: boolean; - errorText?: string; - description?: string; - onCancel: () => void; -} - -export const ModalStatus: React.FC = ({ - success, - successText, - error, - errorText, - description, - title, - visible, - onCancel, -}) => { - const { t } = useTranslation(); - React.useEffect(() => { - const timeoutId = setTimeout(() => { - onCancel(); - }, 2500); - - return () => clearTimeout(timeoutId); - }, [onCancel]); - - const isSuccessStatus = success || successText; - const isErrorStatus = error || errorText; - const StatusIcon = () => - isErrorStatus ? ( - - ) : ( - - ); - const statusText = isErrorStatus - ? errorText || t('status.error') - : isSuccessStatus - ? successText || t('status.success') - : 'Unknown status'; - - const statusDescription = description || t('status.changeValidIn15s'); - - return ( - -
- {statusText && ( -
-
- - - {statusText} - -
- - - {statusDescription} - -
- )} -
-
- ); -}; diff --git a/src/components/ModalStatus/index.ts b/src/components/ModalStatus/index.ts deleted file mode 100644 index 1f6531a7e..000000000 --- a/src/components/ModalStatus/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2020-2022 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export * from './ModalStatus'; diff --git a/src/components/ProjectDeployments/ProjectDeployments.tsx b/src/components/ProjectDeployments/ProjectDeployments.tsx index bf5a601a5..df71e76f4 100644 --- a/src/components/ProjectDeployments/ProjectDeployments.tsx +++ b/src/components/ProjectDeployments/ProjectDeployments.tsx @@ -4,8 +4,9 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useCreateDeployment } from '@hooks'; -import { Markdown, Typography } from '@subql/components'; -import { Form, Modal, Radio } from 'antd'; +import { Markdown, Modal, openNotification, Typography } from '@subql/components'; +import { parseError } from '@utils'; +import { Form, Radio } from 'antd'; import { useForm } from 'antd/es/form/Form'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; @@ -23,19 +24,18 @@ type Props = { deployments: Deployment[]; projectId: string; onRefresh: () => Promise; + currentDeploymentCid?: string; }; -const ProjectDeployments: React.FC = ({ deployments, projectId, onRefresh }) => { +const ProjectDeployments: React.FC = ({ deployments, projectId, currentDeploymentCid, onRefresh }) => { const { t } = useTranslation(); const updateDeployment = useCreateDeployment(projectId); const [deploymentModal, setDeploymentModal] = React.useState(false); const [form] = useForm(); const [currentDeployment, setCurrentDeployment] = React.useState(); - const [addDeploymentsLoading, setAddDeploymentsLoading] = React.useState(false); const handleSubmitUpdate = async () => { try { - setAddDeploymentsLoading(true); await form.validateFields(); await updateDeployment({ ...currentDeployment, @@ -44,8 +44,11 @@ const ProjectDeployments: React.FC = ({ deployments, projectId, onRefresh await onRefresh(); form.resetFields(); setDeploymentModal(false); - } finally { - setAddDeploymentsLoading(false); + } catch (e) { + openNotification({ + type: 'error', + description: parseError(e), + }); } }; @@ -62,16 +65,9 @@ const ProjectDeployments: React.FC = ({ deployments, projectId, onRefresh }, }} okText="Update" - okButtonProps={{ - shape: 'round', - size: 'large', - loading: addDeploymentsLoading, - }} - onOk={() => { - handleSubmitUpdate(); - }} + onSubmit={handleSubmitUpdate} > -
+
= ({ deployments, projectId, onRefresh

- RECOMMENDED + RECOMMENDED

diff --git a/src/components/ProjectHeader/ProjectHeader.tsx b/src/components/ProjectHeader/ProjectHeader.tsx index e72a3e89c..08aeb9758 100644 --- a/src/components/ProjectHeader/ProjectHeader.tsx +++ b/src/components/ProjectHeader/ProjectHeader.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import UnsafeWarn from '@components/UnsafeWarn'; import { ProjectDetailsQuery } from '@hooks/useProjectFromQuery'; import { Address, Typography } from '@subql/components'; +import { Button } from 'antd'; import dayjs from 'dayjs'; import Detail from '../Detail'; @@ -54,13 +55,24 @@ const ProjectHeader: React.FC = ({ project, versions, currentVersion, isU
- + {project.metadata.name} {isUnsafeDeployment && }
+ +
+ {project.metadata.categories && + project.metadata.categories.map((val) => { + return ( + + ); + })} +
{currentVersion && } diff --git a/src/components/index.ts b/src/components/index.ts index 3aebda5df..3ffecb32b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -25,7 +25,6 @@ export * from './ModalInput'; export * from './NumberInput'; export * from './ModalApproveToken'; export * from './ModalClaimIndexerRewards'; -export * from './ModalStatus'; export * from './TransactionModal'; export * from './AppSidebar'; export * from './AppPageHeader'; diff --git a/src/containers/ProjectRegistry.ts b/src/containers/ProjectRegistry.ts index 0fccdaf86..c835355ac 100644 --- a/src/containers/ProjectRegistry.ts +++ b/src/containers/ProjectRegistry.ts @@ -86,7 +86,6 @@ function useProjectRegistryImpl(logger: Logger) { throw new Error('ProjectRegistry contract not available'); } - // TODO: front-end page need to provide an option for user to choose if they want to set this deployment as latest const tx = await contracts.projectRegistry.addOrUpdateDeployment( id, cidToBytes32(deploymentId), diff --git a/src/hooks/useDeploymentMetadata.tsx b/src/hooks/useDeploymentMetadata.tsx index 8653a07fc..3da421147 100644 --- a/src/hooks/useDeploymentMetadata.tsx +++ b/src/hooks/useDeploymentMetadata.tsx @@ -12,7 +12,6 @@ type DeploymentMetadata = { versionId: string; version: string; description: string; - unsafe: boolean; }; export async function getDeploymentMetadata( @@ -23,13 +22,12 @@ export async function getDeploymentMetadata( const raw = await catSingle(versionId); - const { version, description, unsafe = false } = JSON.parse(Buffer.from(raw).toString('utf8')); + const { version, description } = JSON.parse(Buffer.from(raw).toString('utf8')); return { versionId, version, description, - unsafe, }; } diff --git a/src/hooks/useGetIfUnsafeDeployment.tsx b/src/hooks/useGetIfUnsafeDeployment.tsx new file mode 100644 index 000000000..9293f533a --- /dev/null +++ b/src/hooks/useGetIfUnsafeDeployment.tsx @@ -0,0 +1,91 @@ +// Copyright 2020-2022 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useState } from 'react'; +import { WarningOutlined } from '@ant-design/icons'; +import { useIPFS } from '@containers'; +import { Modal, Typography } from '@subql/components'; +import { waitForSomething } from '@utils/waitForSomething'; + +export const useGetIfUnsafeDeployment = (currentDeploymentId?: string) => { + const { catSingle } = useIPFS(); + + const [isUnsafe, setIsUnsafe] = useState(false); + + const getIfUnsafe = async (deploymentId: string) => { + try { + const res = await catSingle(deploymentId); + + if (Buffer.from(res).toString('utf8').includes('unsafe')) { + return true; + } + + return false; + } catch (e) { + return false; + } + }; + + const getIfUnsafeAndWarn = async (deploymentId: string): Promise<'cancel' | 'continue'> => { + const unsafe = await getIfUnsafe(deploymentId); + if (unsafe) { + let waitConfirmOrCancel = 'pending'; + Modal.confirm({ + icon: , + title: This project is not completely safe, + width: 572, + content: ( +
+ + SubQuery can’t guarantee that this project is deterministic, which means it is not entirely safe. + + + This means that two indexers are not guaranteed to index exactly the same data when indexing this project. + In most cases, Indexers will still run this project, however they might be reluctant to do so. + + + By proceeding, you acknowledge the potential risks associated with deploying an 'unsafe' project. Learn + more about unsafe project. + +
+ ), + onCancel: () => { + waitConfirmOrCancel = 'cancel'; + }, + onOk: () => { + waitConfirmOrCancel = 'confirm'; + }, + okButtonProps: { + shape: 'round', + size: 'large', + }, + cancelButtonProps: { + shape: 'round', + size: 'large', + }, + }); + + await waitForSomething({ func: () => waitConfirmOrCancel !== 'pending' }); + + if (waitConfirmOrCancel === 'cancel') return 'cancel'; + } + + return 'continue'; + }; + + useEffect(() => { + const inner = async () => { + if (currentDeploymentId) { + const result = await getIfUnsafe(currentDeploymentId); + setIsUnsafe(result); + } + }; + inner(); + }, [currentDeploymentId]); + + return { + getIfUnsafe, + getIfUnsafeAndWarn, + isUnsafe, + }; +}; diff --git a/src/hooks/useLocalProjects.ts b/src/hooks/useLocalProjects.ts index 19d59c6ef..8dfd99783 100644 --- a/src/hooks/useLocalProjects.ts +++ b/src/hooks/useLocalProjects.ts @@ -15,7 +15,12 @@ import { cloneDeep } from 'lodash-es'; const cacheKey = makeCacheKey('localProjectWithMetadata'); -type ProjectWithMetadata = { description: string; versionDescription: string; name: string } & ProjectFieldsFragment; +type ProjectWithMetadata = { + description: string; + versionDescription: string; + name: string; + categories?: string[]; +} & ProjectFieldsFragment; export const useLocalProjects = () => { // this hooks want to do these things: @@ -69,7 +74,7 @@ export const useLocalProjects = () => { const metadata = rawMetadata.status === 'fulfilled' ? rawMetadata.value - : { name: '', description: '', versionDescription: '' }; + : { name: '', description: '', versionDescription: '', categories: [] }; return { ...project, ...metadata, @@ -104,7 +109,12 @@ export const useLocalProjects = () => { // See fetchAllProject. It's a low-priority(requestsIdleCallback) fetch // if there have cache, use cache first, and then fetch from initial to update. const cached = await localforage.getItem< - ({ description: string; versionDescription: string; name: string } & ProjectFieldsFragment)[] + ({ + description: string; + versionDescription: string; + name: string; + categories?: string[]; + } & ProjectFieldsFragment)[] >(cacheKey); if (cached) { projects.current = cached; @@ -116,14 +126,22 @@ export const useLocalProjects = () => { fetchAllProjects(); }; - const getProjectBySearch = async (params: { offset: number; keywords: string }) => { + const getProjectBySearch = async (params: { offset: number; keywords: string; categories?: string[] }) => { await waitForSomething({ func: () => !loading.current, }); - const total = projects.current.filter((i) => + let total = projects.current.filter((i) => `${i.name}-${i.versionDescription}-${i.description}`.toLowerCase().includes(params.keywords.toLowerCase()), ); + if (params.categories && params.categories.length) { + const setCategories = new Set(params.categories); + total = total.filter((i) => { + if (!i.categories) return false; + return i.categories?.filter((ii) => setCategories.has(ii)).length; + }); + } + return { data: { projects: { diff --git a/src/hooks/useProjectList.tsx b/src/hooks/useProjectList.tsx index 109f23f5a..1ca545044 100644 --- a/src/hooks/useProjectList.tsx +++ b/src/hooks/useProjectList.tsx @@ -1,13 +1,14 @@ // Copyright 2020-2022 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { SearchOutlined } from '@ant-design/icons'; import { ProjectCard } from '@components'; import { useProjectMetadata } from '@containers'; +import { SubqlCheckbox } from '@subql/components'; import { ProjectFieldsFragment, ProjectsOrderBy } from '@subql/network-query'; import { useAsyncMemo, useGetProjectLazyQuery, useGetProjectsLazyQuery } from '@subql/react-hooks'; -import { notEmpty } from '@utils'; +import { categoriesOptions, notEmpty } from '@utils'; import { useInfiniteScroll, useMount } from 'ahooks'; import { Input, Skeleton, Typography } from 'antd'; @@ -45,6 +46,7 @@ export const useProjectList = (props: UseProjectListProps = {}) => { const [getProject, { error: topError }] = useGetProjectLazyQuery(); const [searchKeywords, setSearchKeywords] = React.useState(''); + const [filterCategories, setFilterCategories] = useState([]); const [topProject, setTopProject] = React.useState(); const [projects, setProjects] = React.useState([]); // ref for fetch, state for render. @@ -73,20 +75,31 @@ export const useProjectList = (props: UseProjectListProps = {}) => { } }; - const loadMore = async (options?: { refresh?: boolean }) => { + const loadMore = async (options?: { + refresh?: boolean; + searchParams?: { categories?: string[]; keywords?: string }; + }) => { try { setLoading(true); - if (searchKeywords.length) { + + // TODO: If there have more params, need to optimise + const searchParams = { + keywords: searchKeywords, + categories: filterCategories, + ...options?.searchParams, + }; + const isSearch = searchParams.categories.length || searchParams.keywords.length; + + if (isSearch) { setInSearchMode(true); } else { setInSearchMode(false); } - const api = searchKeywords.length ? getProjectBySearch : getProjects; - - const params = searchKeywords.length + const api = isSearch ? getProjectBySearch : getProjects; + const params = isSearch ? { offset: options?.refresh ? 0 : fetchedProejcts.current.length, - keywords: searchKeywords, + ...searchParams, } : { variables: { @@ -147,10 +160,51 @@ export const useProjectList = (props: UseProjectListProps = {}) => { return ''; }, [inSearchMode, topProject, showTopProject, onProjectClick]); + const projectListItems = useMemo(() => { + if (loading) { + return new Array(projects.length + 10 <= total ? 10 : total - projects.length).fill(0).map((_, i) => { + return ; + }); + } + if (projects.length) { + return projects.map((project) => ( + { + onProjectClick?.(project.id); + }} + /> + )); + } + + if (inSearchMode) return ''; + // TODO: ui + return 'No Projects'; + }, [inSearchMode, loading, projects]); + const listsWithSearch = useMemo(() => { return ( <>
+
+ { + setFilterCategories(val as string[]); + setProjects([]); + const res = await loadMore({ + refresh: true, + searchParams: { + categories: val as string[], + }, + }); + mutate(res); + }} + optionType="button" + > +
{
{topProjectItem} - {projects?.length - ? projects.map((project) => ( - { - onProjectClick?.(project.id); - }} - /> - )) - : // TODO: update UI - loading - ? '' - : 'No projects'} - {loading && - new Array(projects.length + 10 <= total ? 10 : total - projects.length).fill(0).map((_, i) => { - return ; - })} + {projectListItems}
{inSearchMode && !loading && !projects.length && ( diff --git a/src/index.less b/src/index.less index b451cd3ad..8a414eec0 100644 --- a/src/index.less +++ b/src/index.less @@ -293,4 +293,9 @@ label, .ant-steps .ant-steps-item-process .ant-steps-item-icon { background-color: var(--sq-blue600); border-color: var(--sq-blue600); +} + +.staticButton.ant-btn-primary { + background-color: rgba(67, 136, 221, 0.10); + color: var(--sq-blue600); } \ No newline at end of file diff --git a/src/pages/explorer/Project/Project.tsx b/src/pages/explorer/Project/Project.tsx index cdc9e7e91..1e6090bf9 100644 --- a/src/pages/explorer/Project/Project.tsx +++ b/src/pages/explorer/Project/Project.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from 'react-router'; +import { useGetIfUnsafeDeployment } from '@hooks/useGetIfUnsafeDeployment'; import { ServiceAgreementsTable } from '@pages/consumer/ServiceAgreements/ServiceAgreementsTable'; import { captureMessage } from '@sentry/react'; import { Typography } from '@subql/components'; @@ -52,6 +53,7 @@ const ProjectInner: React.FC = () => { }, [asyncProject]); const asyncDeploymentMetadata = useDeploymentMetadata(deploymentId); + const { isUnsafe } = useGetIfUnsafeDeployment(deploymentId); const handleChangeVersion = (value: string) => { navigate(`${location.pathname}?deploymentId=${value}`); @@ -131,7 +133,7 @@ const ProjectInner: React.FC = () => { versions={deploymentVersions} currentVersion={deploymentId} onChangeVersion={handleChangeVersion} - isUnsafeDeployment={!!asyncDeploymentMetadata.data?.unsafe} + isUnsafeDeployment={isUnsafe} />
diff --git a/src/pages/studio/Create/Create.module.less b/src/pages/studio/Create/Create.module.less index 2e860d200..900040f56 100644 --- a/src/pages/studio/Create/Create.module.less +++ b/src/pages/studio/Create/Create.module.less @@ -2,7 +2,7 @@ display: flex; flex-direction: row; align-items: flex-start; - padding: 0 80px 80px 80px; + padding: 0 80px 0 80px; } .deployment { @@ -56,50 +56,3 @@ } } } - -.checkbox { - :global { - .ant-checkbox-group { - .ant-checkbox-wrapper { - background: var(--sq-gray200); - color: var(--sq-gray500); - padding: 5px 16px; - border-radius: 100px; - font-family: var(--sq-font-family); - .ant-checkbox { - &-inner { - width: 0px; - height: 0px; - border: none; - &::after { - width: 0px; - height: 0px; - } - } - } - } - - .ant-checkbox-wrapper.ant-checkbox-wrapper-checked { - color: var(--sq-blue600); - background: rgba(67, 136, 221, 0.10); - - .ant-checkbox { - display: inline-flex; - - &-inner { - // override the antd style. a lazy way. - background: transparent!important; - border: none; - width: 16px; - height: 16px; - &::after { - border-color: var(--sq-blue600); - width: 6px; - height: 9px; - } - } - } - } - } - } -} diff --git a/src/pages/studio/Create/Create.tsx b/src/pages/studio/Create/Create.tsx index e9d195284..74bf2aaef 100644 --- a/src/pages/studio/Create/Create.tsx +++ b/src/pages/studio/Create/Create.tsx @@ -2,49 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; -import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; import { CloseOutlined } from '@ant-design/icons'; import { BigNumber } from '@ethersproject/bignumber'; -import { Markdown, Spinner, Typography } from '@subql/components'; -import { Button, Checkbox, Modal, Radio, Result } from 'antd'; +import { useGetIfUnsafeDeployment } from '@hooks/useGetIfUnsafeDeployment'; +import { Markdown, Modal, openNotification, Spinner, SubqlCheckbox, Typography } from '@subql/components'; +import { Button, Radio, Result } from 'antd'; import clsx from 'clsx'; import { Field, FieldArray, Form, Formik } from 'formik'; +import { t } from 'i18next'; import { FTextInput, ImageInput } from '../../../components'; import { useCreateProject, useProject, useRouteQuery, useUpdateProjectMetadata } from '../../../hooks'; import { FormCreateProjectMetadata, newDeploymentSchema, projectMetadataSchema } from '../../../models'; -import { isEthError, parseError, ROUTES } from '../../../utils'; +import { categoriesOptions, parseError, ROUTES } from '../../../utils'; +import { ProjectDeploymentsDetail } from '../Project/Project'; import styles from './Create.module.less'; const { STUDIO_PROJECT_NAV } = ROUTES; -const categoriesOptions = [ - { - label: 'Dictionary', - value: 'Dictionary', - }, - { - label: 'DeFi', - value: 'DeFi', - }, - { - label: 'Oracle', - value: 'Oracle', - }, - { - label: 'Wallet', - value: 'Wallet', - }, - { - label: 'NFT', - value: 'NFT', - }, -]; - const Create: React.FC = () => { - const { t } = useTranslation(); - const query = useRouteQuery(); const asyncProject = useProject(query.get('id') ?? ''); @@ -53,8 +30,7 @@ const Create: React.FC = () => { const navigate = useNavigate(); const createProject = useCreateProject(); const updateMetadata = useUpdateProjectMetadata(query.get('id') ?? ''); - - const [submitError, setSubmitError] = React.useState(); + const { getIfUnsafeAndWarn } = useGetIfUnsafeDeployment(); const handleSubmit = React.useCallback( async (project: FormCreateProjectMetadata & { versionDescription: string }) => { @@ -74,6 +50,8 @@ const Create: React.FC = () => { }; await updateMetadata(payload); } else { + const processNext = await getIfUnsafeAndWarn(project.deploymentId); + if (processNext === 'cancel') return; // Form can give us a File type that doesn't match the schema const queryId = await createProject(project); @@ -118,14 +96,13 @@ const Create: React.FC = () => { }, }); } catch (e) { - if (isEthError(e) && e.code === 4001) { - setSubmitError(t('errors.transactionRejected')); - return; - } - setSubmitError(parseError(e)); + openNotification({ + type: 'error', + description: parseError(e), + }); } }, - [navigate, createProject, t, isEdit], + [getIfUnsafeAndWarn, navigate, createProject, isEdit], ); if (isEdit && !asyncProject.data) @@ -225,13 +202,15 @@ const Create: React.FC = () => { render={(arrayHelper) => { return (
- { + if (e.length > 2) return; arrayHelper.form.setFieldValue('categories', e); }} - > + optionType="button" + >
); }} @@ -292,14 +271,32 @@ const Create: React.FC = () => { )} - - {submitError && {submitError}}
); }} + + {isEdit ? ( + <> +
+
+
+ {asyncProject.data && ( + + )} + + ) : ( + '' + )}
); }; diff --git a/src/pages/studio/Create/Instructions.module.css b/src/pages/studio/Create/Instructions.module.css deleted file mode 100644 index 58d641085..000000000 --- a/src/pages/studio/Create/Instructions.module.css +++ /dev/null @@ -1,55 +0,0 @@ -.container { - min-width: 395px; - width: 395px; -} - -.title { - padding-top: 32px; - padding-bottom: 8px; - font-family: Futura; - font-weight: 500; - font-size: 18px; - line-height: 28px; - color: var(--gray900); -} - -.subtitle { - padding-top: 32px; - padding-bottom: 8px; - font-family: Futura; - font-size: 16px; - line-height: 28px; - color: var(--gray900); -} - -.content { - font-size: 16px; - line-height: 24px; - - letter-spacing: 0.3px; - color: var(--gray700); -} - -.code { - background: var(--gray200); - border: 1px solid var(--gray300); - box-sizing: border-box; - margin-top: 32px; - padding: 16px 24px; - width: 100%; - - font-family: Futura; - font-size: 16px; - line-height: 22px; - color: var(--gray900); -} - -.codeInline { - background: var(--gray200); - border: 1px solid var(--gray300); - padding: 0 6px; - font-family: Futura; - font-size: 16px; - line-height: 22px; - color: var(--gray900); -} diff --git a/src/pages/studio/Create/Instructions.tsx b/src/pages/studio/Create/Instructions.tsx deleted file mode 100644 index 34a3c5f78..000000000 --- a/src/pages/studio/Create/Instructions.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2020-2022 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import * as React from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import { Button } from '@subql/components'; - -import styles from './Instructions.module.css'; - -const Instructions: React.FC = () => { - const { t } = useTranslation(); - - return ( -
-
- ); -}; - -export default Instructions; diff --git a/src/pages/studio/Home/Home.tsx b/src/pages/studio/Home/Home.tsx index 136fa80cc..d3df663c6 100644 --- a/src/pages/studio/Home/Home.tsx +++ b/src/pages/studio/Home/Home.tsx @@ -4,8 +4,8 @@ import * as React from 'react'; import { useNavigate } from 'react-router'; import { useProjectList } from '@hooks/useProjectList'; -import { Typography } from '@subql/components'; -import { Button, Form, Input, Modal } from 'antd'; +import { Modal, Typography } from '@subql/components'; +import { Button, Form, Input } from 'antd'; import { useForm } from 'antd/es/form/Form'; import { useWeb3 } from '../../../containers'; diff --git a/src/pages/studio/Project/Deployments.tsx b/src/pages/studio/Project/Deployments.tsx index 3c2b648ee..9e9379dda 100644 --- a/src/pages/studio/Project/Deployments.tsx +++ b/src/pages/studio/Project/Deployments.tsx @@ -70,7 +70,7 @@ const DeploymentsTab = forwardRef(({ projectId, currentDep version: result.version, description: result.description, // TODO: backend support - recommended: true, + recommended: currentDeployment?.deployment === deployment.id, }; }), ); @@ -91,9 +91,12 @@ const DeploymentsTab = forwardRef(({ projectId, currentDep return
There has no deployments for this project
; } + const sortedDeployments = deployments.sort((a, b) => +new Date(b.createdAt) - +new Date(a.createdAt)); + return ( { await asyncDeployments.refetch(); diff --git a/src/pages/studio/Project/Project.tsx b/src/pages/studio/Project/Project.tsx index 26f5cad06..f17191731 100644 --- a/src/pages/studio/Project/Project.tsx +++ b/src/pages/studio/Project/Project.tsx @@ -4,11 +4,15 @@ import * as React from 'react'; import { useNavigate, useParams } from 'react-router'; import { ExternalLink } from '@components/ProjectOverview/ProjectOverview'; -import { Markdown, Typography } from '@subql/components'; -import { Breadcrumb, Button, Checkbox, Form, Input, Modal } from 'antd'; +import UnsafeWarn from '@components/UnsafeWarn'; +import { useGetIfUnsafeDeployment } from '@hooks/useGetIfUnsafeDeployment'; +import { Markdown, Modal, SubqlCheckbox, Typography } from '@subql/components'; +import { Breadcrumb, Button, Form, Input } from 'antd'; import { useForm } from 'antd/es/form/Form'; import clsx from 'clsx'; +import { ProjectDetails } from 'src/models'; + import { IPFSImage, Spinner } from '../../../components'; import { useWeb3 } from '../../../containers'; import { useCreateDeployment, useProject } from '../../../hooks'; @@ -16,30 +20,98 @@ import { parseError, renderAsync } from '../../../utils'; import DeploymentsTab, { DeploymendRef } from './Deployments'; import styles from './Project.module.css'; -const Project: React.FC = () => { - const { id } = useParams(); - const { account } = useWeb3(); - const asyncProject = useProject(id ?? ''); - const navigate = useNavigate(); +export const ProjectDeploymentsDetail: React.FC<{ id?: string; project: ProjectDetails }> = ({ id, project }) => { const [form] = useForm(); - const [deploymentModal, setDeploymentModal] = React.useState(false); const createDeployment = useCreateDeployment(id ?? ''); + const { getIfUnsafeAndWarn } = useGetIfUnsafeDeployment(); + + const [deploymentModal, setDeploymentModal] = React.useState(false); const deploymentsRef = React.useRef(null); - const [addDeploymentsLoading, setAddDeploymentsLoading] = React.useState(false); + + const currentDeployment = React.useMemo( + () => ({ deployment: project.deploymentId, version: project.version }), + [project], + ); const handleSubmitCreate = async () => { - try { - setAddDeploymentsLoading(true); - await form.validateFields(); - await createDeployment(form.getFieldsValue()); - await deploymentsRef.current?.refresh(); - form.resetFields(); - setDeploymentModal(false); - } finally { - setAddDeploymentsLoading(false); - } + await form.validateFields(); + const processNext = await getIfUnsafeAndWarn(form.getFieldValue('deploymentId')); + if (processNext === 'cancel') return; + await createDeployment(form.getFieldsValue()); + await deploymentsRef.current?.refresh(); + form.resetFields(); + setDeploymentModal(false); }; + return ( +
+ setDeploymentModal(false)} + title="Add New Deployment Version" + width={572} + cancelButtonProps={{ + style: { + display: 'none', + }, + }} + okText="Add" + onSubmit={async () => { + await handleSubmitCreate(); + }} + > +
+
+ + + + + + + + Set as recommended version + + + { + form.setFieldValue('description', e); + }} + > + +
+
+
+ +
+
+ + Deployment Details + + + { + setDeploymentModal(true); + }} + > + Deploy New Version + +
+ +
+
+ ); +}; + +const Project: React.FC = () => { + const { id } = useParams(); + const { account } = useWeb3(); + const asyncProject = useProject(id ?? ''); + const { isUnsafe } = useGetIfUnsafeDeployment(asyncProject.data?.deploymentId); + const navigate = useNavigate(); + return renderAsync(asyncProject, { loading: () => , error: (error: Error) => { @@ -56,49 +128,7 @@ const Project: React.FC = () => { } return ( -
- setDeploymentModal(false)} - title="Add New Deployment Version" - width={572} - cancelButtonProps={{ - style: { - display: 'none', - }, - }} - okText="Add" - okButtonProps={{ - shape: 'round', - size: 'large', - loading: addDeploymentsLoading, - }} - onOk={() => { - handleSubmitCreate(); - }} - > -
-
- - - - - - - - Set as recommended version - - - { - form.setFieldValue('description', e); - }} - > - -
-
-
+ <>
{ Edit
+ +
{isUnsafe && }
@@ -163,17 +195,9 @@ const Project: React.FC = () => {
{project.metadata.categories?.map((category) => { return ( -
+
+ ); })}
@@ -186,29 +210,8 @@ const Project: React.FC = () => { > -
-
- - Deployment Details - - - { - setDeploymentModal(true); - }} - > - Deploy New Version - -
- -
- + + ); }, }); diff --git a/src/pages/studio/index.tsx b/src/pages/studio/index.tsx index bc61d178a..6c4e4b0b5 100644 --- a/src/pages/studio/index.tsx +++ b/src/pages/studio/index.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { Route, Routes, useNavigate } from 'react-router'; import { WalletRoute } from '@components'; import { useStudioEnabled } from '@hooks'; +import { Footer } from '@subql/components'; import Create from './Create'; import Home from './Home'; @@ -25,6 +26,8 @@ const Studio: React.FC = () => { } /> } /> + + } > diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3aa763438..a1d35136a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -26,3 +26,42 @@ export const tokenNames: { [key: string]: string } = { [STABLE_TOKEN_ADDRESS]: STABLE_TOKEN, [SQT_TOKEN_ADDRESS]: TOKEN, }; + +export const categoriesOptions = [ + { + label: 'Dictionary', + value: 'Dictionary', + }, + { + label: 'DeFi', + value: 'DeFi', + }, + { + label: 'Oracle', + value: 'Oracle', + }, + { + label: 'Wallet', + value: 'Wallet', + }, + { + label: 'NFT', + value: 'NFT', + }, + { + label: 'Gaming', + value: 'Gaming', + }, + { + label: 'Governance', + value: 'Governance', + }, + { + label: 'Analytic', + value: 'Analytic', + }, + { + label: 'Privacy', + value: 'Privacy', + }, +]; diff --git a/yarn.lock b/yarn.lock index 466f6c31c..be3ae788a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4624,10 +4624,10 @@ jwt-decode "^3.1.2" lru-cache "^10.0.1" -"@subql/components@1.0.3-21": - version "1.0.3-21" - resolved "https://registry.npmjs.org/@subql/components/-/components-1.0.3-21.tgz#af1c55bc6dc0f78626e361ea5d024f670249235d" - integrity sha512-B5f6h6CvzL1tQ1s/s034q9IV7IRL/1DJis+rMloHbQBrwCz4NgE5x1oV18iP9Yik9f2t2gXzWCmmlOng/DvYcQ== +"@subql/components@1.0.3-22": + version "1.0.3-22" + resolved "https://registry.npmjs.org/@subql/components/-/components-1.0.3-22.tgz#0a1623a972971b0c2e2056b077ab6e62c6a5f723" + integrity sha512-LUaRWB+iSu7PXUdy2i0N4GNadIjXjNMKb067M0dCq+L9vCFKnRzpWNOslXPiqs41bqUXwgL0kHlNFge0ooZQMg== dependencies: "@graphiql/plugin-explorer" "^0.3.4" "@graphiql/toolkit" "^0.9.1"