diff --git a/package.json b/package.json index d830d76df..9fea49af7 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-20", + "@subql/components": "1.0.3-21", "@subql/contract-sdk": "0.16.2", "@subql/network-clients": "^0.3.17", "@subql/network-config": "0.3.17-2", diff --git a/src/App.tsx b/src/App.tsx index 6c9a36b91..6aa54b201 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,9 @@ // Copyright 2020-2022 SubQuery Pte Ltd authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { PropsWithChildren, useMemo } from 'react'; +import React, { PropsWithChildren } from 'react'; import { BrowserRouter } from 'react-router-dom'; import { AppInitProvider } from '@containers/AppInitialProvider'; -import { useStudioEnabled } from '@hooks'; -import { entryLinks, externalAppLinks, studioLink } from '@utils/links'; import { RainbowProvider } from './config/rainbowConf'; import { ChainStatus, Header } from './components'; @@ -40,15 +38,11 @@ const Providers: React.FC = ({ children }) => { }; const RenderRouter: React.FC = () => { - const studioEnabled = useStudioEnabled(); - const calEntryLinks = useMemo(() => (studioEnabled ? [...entryLinks, studioLink] : [...entryLinks]), [studioEnabled]); - return (
- {/* TODO: replace with component from ui library */} -
+
diff --git a/src/components/Header/Header.module.less b/src/components/Header/Header.module.less index 15b5df467..acb46abc4 100644 --- a/src/components/Header/Header.module.less +++ b/src/components/Header/Header.module.less @@ -1,159 +1,9 @@ .header { - width: 100%; - display: flex; - justify-content: space-between; - box-shadow: inset 0px -1px 0px rgba(26, 32, 44, 0.06); - padding: 1rem 1.25rem; - - .expandIcon { - display: none; - font-size: 40px; - } - - &Inner { - width: 100%; - } - - &Logo { - display: flex; - } - - &Right { - display: flex; - width: 100%; - align-items: center; - transition: all 0.2s linear; - - } - - .leftHeader { - display: inline-flex; - } -} - -.justifyBetween { - justify-content: space-between; -} - -.flexCenter { - display: flex; - align-items: center; -} - -.leftElement { - margin-left: 2rem; - position: relative; -} - -.appDropdown { - margin: 1rem; -} - -.menuOverlay { - padding: 1rem !important; -} - -.dropMenu { - width: 300px !important; -} - -.navLink { - text-decoration: none; - margin: 0 1vw; - display: flex; - color: var(--gray700); - width: fit-content; -} - -.navLink:hover { - text-decoration: underline; - color: var(--sq-primary-blue); -} - -.navLinkCurrent { - font-weight: 500; - color: var(--sq-info); -} - -@media screen and (max-width: 600px) { - .header { - flex-direction: column; - .expandIcon { - display: inline-block; - } - - .middleHeader { - width: 100%; - } - - &Logo { - width: 100%; - } - - &Inner { - flex-direction: column; - width: 100vw; - padding: 0 20px; - .middleHeader { - flex-direction: column; - } - - :global { - #leftHeader { - margin-left: 0; - } - } - - .appDropdown { - width: 100%; - margin: 0; - padding: 14px 0; - border-bottom: 1px solid var(--sq-gray200); - :global { - .ant-space.ant-dropdown-trigger { - margin: 0 1vw; - } - } - } - } - - &Right { - flex-direction: column; - } - - &Item { - width: 100%; - padding: 14px 0; - border-top: 1px solid var(--sq-gray200); - border-bottom: 1px solid var(--sq-gray200); - - & + .headerItem { - margin-top: -1px; - } - } - - .hideOnMobile { - height: 0; - overflow: hidden; - } - - .showExpand { - height: 570px; - } - - .right { - width: 100%; - margin-bottom: 8px; - - &>a { - width: 100%; - } - } - - .leftHeader { - border-top: 1px solid var(--sq-gray200); - padding: 14px 0; - width: 100%; + :global { + .subql-header { + padding: 0 24px; + display: flex; + align-items: center; } } -} +} \ No newline at end of file diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 7c293e817..e0d174127 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; -import { BsList, BsX } from 'react-icons/bs'; -import { NavLink, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { AccountActions } from '@components/AccountActions'; +import { useStudioEnabled } from '@hooks'; import { ConnectButton } from '@rainbow-me/rainbowkit'; -import { Button, Dropdown, MenuWithDesc, Typography } from '@subql/components'; -import { Divider } from 'antd'; +import { Button, Header as SubqlHeader } from '@subql/components'; +import { entryLinks, externalAppLinks } from '@utils/links'; import clsx from 'clsx'; import { useAccount } from 'wagmi'; @@ -35,177 +35,58 @@ export interface AppNavigation { dropdown?: AppLink[]; } -const isExternalLink = (to: string) => to.startsWith('https') || to.startsWith('http'); - -const renderLink = (to: string, label: string) => { - if (!isExternalLink(to)) { - return ( - - clsx(styles.navLink, isActive && styles.navLinkCurrent)}> - {label} - - - ); - } +export const Header: React.FC = () => { + const { address: account } = useAccount(); + const navigate = useNavigate(); + const studioEnabled = useStudioEnabled(); + const calEntryLinks = React.useMemo(() => { + if (!studioEnabled) { + return entryLinks.map((entry) => { + if (entry.key === 'explorer') { + return { + ...entry, + dropdown: undefined, + }; + } + return entry; + }); + } + return entryLinks; + }, [studioEnabled]); return ( - + ); + }} + + )}
- ); + } - return ( -
- {renderLink(nav.link ?? '/', nav.label)} -
- ); - })} -
- ); - - return ( - <> - <>{middleElement} - <>{sortedAppNavigation} - - ); -}; - -export interface HeaderProps { - logoLink?: string; - dropdownLinks?: DropdownLink; - appNavigation?: AppNavigation[]; - leftElement?: React.ReactElement; - middleElement?: React.ReactElement; - rightElement?: React.ReactElement; - className?: string; -} - -export const Header: React.FC> = ({ - logoLink, - dropdownLinks, - appNavigation, - leftElement, - middleElement, - rightElement, - className, -}) => { - const { address: account } = useAccount(); - const [showExpand, setShowExpand] = React.useState(false); - - return ( -
-
-
- - SubQuery Logo - - - -
{ - setShowExpand(!showExpand); - }} - > - {showExpand ? : } -
-
- -
- - - -
- {account ? ( - - ) : ( - - {({ openConnectModal }) => { - return ( - - ); - }} - - )} -
-
-
+ >
); }; diff --git a/src/components/Markdown/index.module.less b/src/components/Markdown/index.module.less deleted file mode 100644 index 2d74c86fc..000000000 --- a/src/components/Markdown/index.module.less +++ /dev/null @@ -1,19 +0,0 @@ -.markdown { - color: var(--sq-gray700); - :global { - p { - margin: 1em 0; - } - ul { - margin-left: 22px; - } - li { - margin: 0.5em 0; - } - - img { - max-width: 100%; - margin: 1em 0; - } - } -} \ No newline at end of file diff --git a/src/components/Markdown/index.tsx b/src/components/Markdown/index.tsx deleted file mode 100644 index 6f0355c26..000000000 --- a/src/components/Markdown/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2020-2022 SubQuery Pte Ltd authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import React, { FC } from 'react'; -import MarkdownCompiler, { Options } from 'react-markdown'; - -import styles from './index.module.less'; - -const Markdown: FC = (props) => { - return ( -
- -
- ); -}; -export default Markdown; diff --git a/src/components/NewDeployment/NewDeployment.module.css b/src/components/NewDeployment/NewDeployment.module.css deleted file mode 100644 index ebe11b63d..000000000 --- a/src/components/NewDeployment/NewDeployment.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.form { - display: flex; - flex-direction: column; - align-items: flex-start; - width: 100%; -} - -.submit { - width: 95%; - margin-top: 24px; -} - -.error { - color: var(--sq-error); - padding-top: 8px; -} diff --git a/src/components/NewDeployment/NewDeployment.tsx b/src/components/NewDeployment/NewDeployment.tsx deleted file mode 100644 index 80e4a8001..000000000 --- a/src/components/NewDeployment/NewDeployment.tsx +++ /dev/null @@ -1,54 +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 { Button } from '@subql/components'; -import { Form, Formik } from 'formik'; - -import { NewDeployment as NewDeploymentParams, newDeploymentSchema } from '../../models'; -import { ModalContainer } from '../NewProject/NewProject'; -import { FTextInput } from '..'; -import styles from './NewDeployment.module.css'; - -type Props = { - onSubmit: (deployment: NewDeploymentParams) => Promise | void; - onClose?: () => void; -}; - -const NewDeployment: React.FC = (props) => { - const { t } = useTranslation(); - - return ( - - - {({ isSubmitting, submitForm }) => ( -
-
- - - -
-
, + ]} + > + ), + okButtonProps: { + style: { display: 'none' }, + }, + }); } catch (e) { if (isEthError(e) && e.code === 4001) { setSubmitError(t('errors.transactionRejected')); @@ -46,9 +100,16 @@ const Create: React.FC = () => { setSubmitError(parseError(e)); } }, - [navigate, createProject, t], + [navigate, createProject, t, isEdit], ); + if (isEdit && !asyncProject.data) + return ( +
+ +
+ ); + return (
{ image: undefined, version: '1.0.0', versionDescription: '', - deploymentId: '', + deploymentId: query.get('deploymentId') ?? '', + ...(isEdit ? asyncProject.data?.metadata : {}), }} - validationSchema={projectMetadataSchema.shape({}).concat(newDeploymentSchema.shape({}))} + validationSchema={ + isEdit + ? projectMetadataSchema.shape({}) + : projectMetadataSchema.shape({}).concat(newDeploymentSchema.shape({})) + } onSubmit={handleSubmit} > - {({ setFieldValue, values, isSubmitting, submitForm }) => ( -
-
-
-
- setFieldValue('image', value)} - placeholder="/static/default.project.png" - /> -

{values.name}

-
-
- +
-
-
-
- - - - -

Deployment Details

- - - - {submitError && {submitError}} +
+
+ + Project Details + + {t('studio.create.description')} + + + {({ + field, + form, + }: { + field: { name: string; value: string }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form: { setFieldValue: (field: string, val: any) => void }; + }) => { + return ( + { + form.setFieldValue(field.name, e); + }} + /> + ); + }} + + + + {isEdit ? ( + '' + ) : ( + <> +

Deployment Details

+ + + {t('studio.create.versionDesc')} + + {({ + field, + form, + }: { + field: { name: string; value: string }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form: { setFieldValue: (field: string, val: any) => void }; + }) => { + return ( + { + form.setFieldValue(field.name, e); + }} + /> + ); + }} + + + )} + + {submitError && {submitError}} +
- -
- - )} + + ); + }}
); diff --git a/src/pages/studio/Home/Home.tsx b/src/pages/studio/Home/Home.tsx index 4ba8a2a3d..136fa80cc 100644 --- a/src/pages/studio/Home/Home.tsx +++ b/src/pages/studio/Home/Home.tsx @@ -2,150 +2,87 @@ // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; -import Modal from 'react-modal'; import { useNavigate } from 'react-router'; -import { Button } from '@subql/components'; -import { ProjectFieldsFragment } from '@subql/network-query'; -import { useGetProjectsLazyQuery } from '@subql/react-hooks'; -import { useInfiniteScroll } from 'ahooks'; +import { useProjectList } from '@hooks/useProjectList'; +import { Typography } from '@subql/components'; +import { Button, Form, Input, Modal } from 'antd'; +import { useForm } from 'antd/es/form/Form'; -import { CreateInstructions, NewProject, ProjectCard, Spinner } from '../../../components'; import { useWeb3 } from '../../../containers'; -import { useProject } from '../../../hooks'; -import { modalStyles, notEmpty, renderAsync } from '../../../utils'; import { ROUTES } from '../../../utils'; -import { Header } from '../../explorer/Home/Home'; -import styles from './Home.module.css'; -const { STUDIO_CREATE_NAV, STUDIO_PROJECT_NAV } = ROUTES; - -const Project: React.FC<{ - projectId: string; - account: string; - onClick?: () => void; - projectDetails: ProjectFieldsFragment; -}> = ({ projectId, account, onClick, projectDetails }) => { - const asyncProject = useProject(projectId); - return ( -
- {renderAsync(asyncProject, { - error: (e) => { - console.log('ERROR loading project', e); - return {`Failed to load project: ${e.message}`}; - }, - loading: () => { - return ( - - ); - // return {`Loading project id: ${projectId}`} - }, - data: (project) => { - if (!project) return null; - return ( - - ); - }, - })} -
- ); -}; +const { STUDIO_CREATE_NAV, STUDIO_PROJECT_NAV } = ROUTES; const Home: React.FC = () => { const { account } = useWeb3(); const navigate = useNavigate(); - const [showCreateModal, setShowCreateModal] = React.useState(false); - const [getProjects, asyncProjects] = useGetProjectsLazyQuery({ - variables: { offset: 0 }, + const { listsWithSearch } = useProjectList({ + account, + onProjectClick: (projectId) => { + navigate(`${STUDIO_PROJECT_NAV}/${projectId}`); + }, }); - const [projects, setProjects] = React.useState([]); - - const loadMore = async () => { - const res = await getProjects({ - variables: { - offset: projects.length, - }, - }); - - if (res.data?.projects?.nodes) { - setProjects([...projects, ...res.data.projects?.nodes.filter(notEmpty)]); - } - - return { - list: [], - isNoMore: !res.error && !res.data?.projects?.nodes.length, - }; - }; - - const handleCreateProject = (name: string) => { - navigate(`${STUDIO_CREATE_NAV}?name=${encodeURI(name)}`); + const [form] = useForm(); + const [showCreateModal, setShowCreateModal] = React.useState(false); + const handleCreateProject = () => { + navigate( + `${STUDIO_CREATE_NAV}?name=${encodeURI(form.getFieldValue('projectName'))}&deploymentId=${form.getFieldValue( + 'deploymentId', + )}`, + ); }; - const enableCreateModal = () => setShowCreateModal(true); - useInfiniteScroll(() => loadMore(), { - target: document, - isNoMore: (d) => !!d?.isNoMore, - threshold: 300, - }); - return ( -
-
-
-
-
+
+
+ My Projects +
setShowCreateModal(false)} - closeTimeoutMS={200} + title="Publish New Project to the Decentralised Network" + width={572} + open={showCreateModal} + onCancel={() => setShowCreateModal(false)} + onOk={async () => { + await form.validateFields(); + + handleCreateProject(); + }} + okText={'Next'} + okButtonProps={{ + shape: 'round', + size: 'large', + }} + cancelButtonProps={{ + style: { display: 'none' }, + }} > - setShowCreateModal(false)} /> +
+ + Please enter the deployment ID of your SubQuery project below. To get the deployment ID, run subql publish + from your project code to publish it to IPFS. Learn how to publish a SubQuery project here. + + +
+ + + +
+ + + + Project names should be unique, you can edit this later. +
+
+
- {renderAsync(asyncProjects, { - loading: () => , - error: (error) =>

{`Failed to load projects: ${error.message}`}

, - data: (_projects) => { - if (!projects?.length) { - return ; - } - - const renderProjects = projects.filter(({ id, owner }) => account && id && owner === account); - - return ( -
- {renderProjects.map((proj) => ( - navigate(`${STUDIO_PROJECT_NAV}/${proj.id}`)} - account={account ?? ''} - projectDetails={proj} - /> - ))} -
- ); - }, - })} + {listsWithSearch}
); }; diff --git a/src/pages/studio/Project/Deployments.tsx b/src/pages/studio/Project/Deployments.tsx index d2a85fb31..110310d6c 100644 --- a/src/pages/studio/Project/Deployments.tsx +++ b/src/pages/studio/Project/Deployments.tsx @@ -75,7 +75,7 @@ const DeploymentsTab: React.FC = ({ projectId, currentDeployment }) => { return
There has no deployments for this project
; } - return ; + return ; }, }); }; diff --git a/src/pages/studio/Project/Project.module.css b/src/pages/studio/Project/Project.module.css index a4b896d51..51324d062 100644 --- a/src/pages/studio/Project/Project.module.css +++ b/src/pages/studio/Project/Project.module.css @@ -9,13 +9,11 @@ margin-top: 32px; } -.upper { - padding-top: 80px; - background: var(--sq-background-gradient); -} - .content { - padding-bottom: 80px; + padding: 0 80px 80px 80px; + display: flex; + flex-direction: column; + gap: 24px; } .tabContainer { diff --git a/src/pages/studio/Project/Project.tsx b/src/pages/studio/Project/Project.tsx index 7f32d6914..1660f2d55 100644 --- a/src/pages/studio/Project/Project.tsx +++ b/src/pages/studio/Project/Project.tsx @@ -2,52 +2,36 @@ // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import Modal from 'react-modal'; -import { Navigate, Route, useParams } from 'react-router'; -import { Button } from '@subql/components'; -import { Typography } from 'antd'; +import { useNavigate, useParams } from 'react-router'; +import { ExternalLink } from '@components/ProjectOverview/ProjectOverview'; +import { Markdown, Typography } from '@subql/components'; +import { Breadcrumb, Button, Form, Input, Modal } from 'antd'; +import { useForm } from 'antd/es/form/Form'; import clsx from 'clsx'; -import { NewDeployment, ProjectDetail, ProjectEdit, ProjectHeader, Spinner } from '../../../components'; +import { IPFSImage, Spinner } from '../../../components'; import { useWeb3 } from '../../../containers'; -import { useCreateDeployment, useProject, useUpdateProjectMetadata } from '../../../hooks'; -import { FormProjectMetadata, NewDeployment as NewDeploymentParams } from '../../../models'; -import { modalStyles, renderAsync } from '../../../utils'; -import { ROUTES } from '../../../utils'; +import { useCreateDeployment, useProject } from '../../../hooks'; +import { renderAsync } from '../../../utils'; import DeploymentsTab from './Deployments'; import styles from './Project.module.css'; -const { DETAILS, DEPLOYMENTS } = ROUTES; - const Project: React.FC = () => { const { id } = useParams(); const { account } = useWeb3(); const asyncProject = useProject(id ?? ''); - const { t } = useTranslation(); - - const [tab, setTab] = React.useState(DETAILS); + const navigate = useNavigate(); + const [form] = useForm(); const [deploymentModal, setDeploymentModal] = React.useState(false); - const [editing, setEditing] = React.useState(false); const createDeployment = useCreateDeployment(id ?? ''); - const updateMetadata = useUpdateProjectMetadata(id ?? ''); - const handleSubmitCreate = async (details: NewDeploymentParams) => { - await createDeployment(details); + const handleSubmitCreate = async () => { + await form.validateFields(); + await createDeployment(form.getFieldsValue()); + form.resetFields(); setDeploymentModal(false); }; - const handleNewDeployment = () => setDeploymentModal(true); - const handleEditMetadata = () => setEditing(true); - const handleSubmitEdit = async (metadata: FormProjectMetadata) => { - await updateMetadata(metadata); - - // TODO call this once tx submitted, but not confirmed - setEditing(false); - }; - - const tabStyle = (curTab: typeof DETAILS | typeof DEPLOYMENTS) => (curTab === tab ? undefined : 'secondary'); - return renderAsync(asyncProject, { loading: () => , error: (error: Error) => {`Failed to load project: ${error.message}`}, @@ -58,63 +42,134 @@ const Project: React.FC = () => { } if (project.owner !== account) { - return } />; + navigate('/studio'); } return (
setDeploymentModal(false)} - closeTimeoutMS={200} + open={deploymentModal} + onCancel={() => setDeploymentModal(false)} + title="Add New Deployment Version" + width={572} + cancelButtonProps={{ + style: { + display: 'none', + }, + }} + okText="Add" + okButtonProps={{ + shape: 'round', + size: 'large', + }} + onOk={() => { + handleSubmitCreate(); + }} > - setDeploymentModal(false)} /> +
+
+ + + + + + + + { + form.setFieldValue('deploymentDesc', e); + }} + > + +
+
-
-
- {/* ignore it for now. studio would refactor later. */} - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - -
- setTab(DETAILS)} className={`${styles.tab} `} type={tabStyle(DETAILS)}> - {'Details'} - - setTab(DEPLOYMENTS)} - className={styles.tab} - type={tabStyle(DEPLOYMENTS)} - > - {'Deployments'} - +
+ + SubQuery Projects + + ), + onClick: () => { + navigate('/studio'); + }, + }, + { + key: 'current', + title: project.metadata.name, + }, + ]} + > + +
+ {' '} +
+
+ + {project.metadata.name} + + + +
+ + + Project Detail + + +
+ {project.metadata.description} +
+ + + + +
+
- {tab === DETAILS && ( - <> - {editing ? ( - setEditing(false)} /> - ) : ( - - )} - - )} - {tab === DEPLOYMENTS && ( -
- -
- )} +
+ + Deployment Details + + + { + setDeploymentModal(true); + }} + > + Deploy New Version + +
+
); diff --git a/src/utils/limitation.ts b/src/utils/limitation.ts index be9834004..5b3640c0d 100644 --- a/src/utils/limitation.ts +++ b/src/utils/limitation.ts @@ -23,13 +23,11 @@ export const makeCacheKey = ( type?: string; // maybe need to make a const varible. gerneral/sqt/flexplan such as. } = {}, ) => { - // const prefix = options.prefix ?? import.meta.env.MODE - // const type = options.type ?? 'gerneral' - // const suffix = options const { prefix = import.meta.env.MODE, type = 'gerneral', suffix = '' } = options; return `${prefix}-${type}-${key}-${suffix}`; }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const cachedResult: Map = new Map(); export enum limitContractCacheEnum { diff --git a/src/utils/links.ts b/src/utils/links.ts index a9ced8e03..bf99fa615 100644 --- a/src/utils/links.ts +++ b/src/utils/links.ts @@ -29,7 +29,11 @@ export const entryLinks = [ }, { link: ROUTES.EXPLORER, + key: 'explorer', label: t('header.explorer'), + dropdown: [ + studioLink + ] }, { link: ROUTES.INDEXER, diff --git a/yarn.lock b/yarn.lock index ef6c9ee18..c9b1c59dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4629,10 +4629,10 @@ jwt-decode "^3.1.2" lru-cache "^10.0.1" -"@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== +"@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== dependencies: "@graphiql/plugin-explorer" "^0.3.4" "@graphiql/toolkit" "^0.9.1"