Skip to content
This repository has been archived by the owner on Feb 2, 2024. It is now read-only.

[Explorer] Add Metadata page #168

Merged
merged 59 commits into from
Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
f61d8af
Add Metadata page allows you to generate a new app data document and …
matextrem Jul 20, 2022
d15fedd
fix typo
matextrem Jul 20, 2022
86b31bc
Update package.json
matextrem Jul 20, 2022
8d2ba3e
Update yarn.lock
matextrem Jul 20, 2022
d5b16ea
fix yarn.lock
matextrem Jul 20, 2022
57e2203
update lock file
ramirotw Jul 21, 2022
df9e28e
Improve validation + Get schema from appData module
matextrem Jul 21, 2022
2bca150
update lock file
ramirotw Jul 21, 2022
587e289
Improve metadata form + Use schema from appData module
matextrem Jul 22, 2022
36df94e
Improve styles
matextrem Jul 22, 2022
2e37128
update icon
matextrem Jul 22, 2022
903e1f6
Update yarn.lock
matextrem Jul 22, 2022
144a814
Fix error validation
matextrem Jul 25, 2022
49c6aaa
Fix validation issues
matextrem Jul 26, 2022
91c8a34
Add keys for IPFS + Improve code
matextrem Jul 28, 2022
463a61d
fix json container width
alongoni Jul 28, 2022
e2f7b61
json container fixed position + styles
alongoni Jul 28, 2022
6f17a6a
move styles to metadata page
alongoni Jul 28, 2022
105c59f
Update merge
matextrem Jul 28, 2022
2300185
Update form
matextrem Jul 28, 2022
d5ca896
layout improvements
alongoni Jul 29, 2022
af74246
add documentation links
alongoni Jul 29, 2022
66ea6cb
Add tooltip to fields
matextrem Jul 29, 2022
d7e08b1
styles improvements2
alongoni Jul 29, 2022
e89d9a9
add basic tooltip content
alongoni Jul 29, 2022
2e35921
Fix issues
matextrem Aug 1, 2022
f03ac2d
feedback points: 1, 2, 3 and 9
alongoni Aug 1, 2022
20f917a
Fix issues with render
matextrem Aug 1, 2022
be42e1a
feedback points: 4
alongoni Aug 1, 2022
b2cbbf9
feedback point: 6
alongoni Aug 1, 2022
c496a47
fix imput width
alongoni Aug 2, 2022
d73ca25
Fix issues with validation
matextrem Aug 2, 2022
1d08397
Bump Version to 2.10.0
henrypalacios Jul 19, 2022
83431c0
add basic tooltip content v2
alongoni Aug 2, 2022
944afd9
Fix IPFS form
matextrem Aug 3, 2022
32d0413
Remove AppDataDoc generation
matextrem Aug 3, 2022
d5d31ec
move appdata hash section
alongoni Aug 3, 2022
dd0941b
Add Decode appData tab
matextrem Aug 3, 2022
f8aec67
fix large resolution issue json section
alongoni Aug 4, 2022
7c55232
add Appdata title
alongoni Aug 4, 2022
1516e1b
fix layout mobile_desktop
alongoni Aug 4, 2022
afca453
fix margin on medium resolutions
alongoni Aug 4, 2022
38113d7
change Decode icon
alongoni Aug 4, 2022
63a0a62
rm duplicate icon
alongoni Aug 4, 2022
8bfb000
Improve decode page
matextrem Aug 4, 2022
e0fe60e
fix appdata decode layout
alongoni Aug 5, 2022
842c1f9
fix notification message on mobile
alongoni Aug 5, 2022
6306d19
revert decodeAppData layout
alongoni Aug 5, 2022
45e6482
fix decode layout v2
alongoni Aug 5, 2022
1d1ef76
wording AppData
alongoni Aug 5, 2022
995ed15
wording AppData v2
alongoni Aug 5, 2022
2ebf550
Add appData hash validation
matextrem Aug 8, 2022
2d5d622
add missing periods
alongoni Aug 8, 2022
c7b9f65
add more periods
alongoni Aug 8, 2022
6de8d73
Refactor code
matextrem Aug 17, 2022
e95f63f
Merge branch 'develop' into 160/metadata-page
matextrem Aug 17, 2022
3435a95
Merge branch 'develop' into 160/metadata-page
matextrem Aug 18, 2022
d188546
Update SDK instance
matextrem Aug 18, 2022
9d54484
Fix type for appDataDoc
matextrem Aug 18, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@
"dependencies": {
"@amcharts/amcharts4": "^4.9.12",
"@apollo/client": "^3.1.5",
"@cowprotocol/cow-sdk": "^0.0.14",
"@cowprotocol/app-data": "^0.0.1-RC.2",
"@cowprotocol/cow-sdk": "^0.0.15",
"@fortawesome/fontawesome-svg-core": "^1.2.26",
"@fortawesome/free-regular-svg-icons": "^5.12.0",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.8",
"@gnosis.pm/dex-js": "^0.10.0",
Expand All @@ -65,6 +66,7 @@
"@material-ui/core": "^4.11.0",
"@material-ui/pickers": "4.0.0-alpha.9",
"@popperjs/core": "^2.0.6",
"@rjsf/core": "^4.2.2",
"@sentry/react": "^6.18.1",
"@sentry/tracing": "^6.18.1",
"@types/react": "^16.9.19",
Expand Down Expand Up @@ -139,7 +141,7 @@
"@types/flexsearch": "^0.7.3",
"@types/hapi__joi": "^16.0.12",
"@types/jest": "^26.0.8",
"@types/node": "^14.0.14",
"@types/node": "^17.0.29",
"@types/qrcode.react": "^1.0.0",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-cytoscapejs": "^1.2.2",
Expand Down
9 changes: 9 additions & 0 deletions src/apps/explorer/ExplorerApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ const NotFound = React.lazy(
),
)

const AppDataDetails = React.lazy(
() =>
import(
/* webpackChunkName: "Metadata_chunk"*/
'./pages/AppData'
),
)

const SearchNotFound = React.lazy(
() =>
import(
Expand Down Expand Up @@ -132,6 +140,7 @@ const AppContent = (): JSX.Element => {
<Route path={pathPrefix + '/address/:address'} exact component={UserDetails} />
<Route path={pathPrefix + '/tx/:txHash'} exact component={TransactionDetails} />
<Route path={pathPrefix + '/search/:searchString?'} exact component={SearchNotFound} />
<Route path={pathPrefix + '/appdata'} exact component={AppDataDetails} />
<Route component={NotFound} />
</Switch>
</React.Suspense>
Expand Down
56 changes: 56 additions & 0 deletions src/apps/explorer/pages/AppData/DecodePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useState } from 'react'
import Form, { FormValidation } from '@rjsf/core'
import { decodeAppDataSchema, FormProps, handleErrors, transformErrors } from './config'
import DecodeAppData from 'components/AppData/DecodeAppData'

const DecodePage: React.FC = () => {
const [formData, setFormdata] = useState<FormProps>()
const [isSubmitted, setIsSubmitted] = useState<boolean>(false)
const [disabled, setDisabled] = useState<boolean>(true)
const [invalidFormDataAttempted, setInvalidFormDataAttempted] = useState<boolean>(false)

const formRef = React.useRef<Form<FormProps>>(null)

const onSubmit = async ({ formData }: FormProps): Promise<void> => {
setFormdata(formData)
setIsSubmitted(true)
}

const handleOnChange = ({ formData }: FormProps): void => {
setFormdata(formData)
if (isSubmitted) {
setIsSubmitted(false)
}
if (JSON.stringify(formData) !== JSON.stringify({})) {
setDisabled(false)
}
}

const onError = (_: FormProps, errors: FormValidation): FormValidation => handleErrors(formRef, errors, setDisabled)

return (
<div className="decode-container">
<Form
className="data-form"
showErrorList={false}
onChange={handleOnChange}
formData={formData}
ref={formRef}
onSubmit={onSubmit}
transformErrors={transformErrors}
liveValidate={invalidFormDataAttempted}
noHtml5Validate
validate={onError}
onError={(): void => setInvalidFormDataAttempted(true)}
schema={decodeAppDataSchema}
>
<button className="btn btn-info" disabled={disabled} type="submit">
DECODE APPDATA
</button>
</Form>
{isSubmitted && <DecodeAppData showExpanded appData={formData?.appData} />}
</div>
)
}

export default DecodePage
254 changes: 254 additions & 0 deletions src/apps/explorer/pages/AppData/FormPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import React, { useCallback, useEffect, useState } from 'react'
import Form, { FormValidation } from '@rjsf/core'
import { JSONSchema7 } from 'json-schema'
import { IpfsHashInfo } from '@cowprotocol/cow-sdk'
import { COW_SDK, DEFAULT_IPFS_READ_URI } from 'const'
import { RowWithCopyButton } from 'components/common/RowWithCopyButton'
import Spinner from 'components/common/Spinner'
import AppDataWrapper from 'components/common/AppDataWrapper'
import { Notification } from 'components/Notification'
import {
INITIAL_FORM_VALUES,
INVALID_IPFS_CREDENTIALS,
getSchema,
transformErrors,
handleErrors,
handleFormatData,
ipfsSchema,
uiSchema,
ipfsUiSchema,
CustomField,
FormProps,
} from './config'
import { IpfsWrapper } from './styled'
import { ExternalLink } from 'components/analytics/ExternalLink'
import { Network } from 'types'

const FormPage: React.FC = () => {
const [schema, setSchema] = useState<JSONSchema7>({})
const [appDataForm, setAppDataForm] = useState({})
const [disabledAppData, setDisabledAppData] = useState<boolean>(true)
const [disabledIPFS, setDisabledIPFS] = useState<boolean>(true)
const [invalidFormDataAttempted, setInvalidFormDataAttempted] = useState<{ appData: boolean; ipfs: boolean }>({
appData: false,
ipfs: false,
})
const [isLoading, setIsLoading] = useState<boolean>(false)
const [ipfsHashInfo, setIpfsHashInfo] = useState<IpfsHashInfo | void | undefined>()
const [ipfsCredentials, setIpfsCredentials] = useState<{ pinataApiKey?: string; pinataApiSecret?: string }>({})
const [isDocUploaded, setIsDocUploaded] = useState<boolean>(false)
const [error, setError] = useState<string>()
const formRef = React.useRef<Form<FormProps>>(null)
const ipfsFormRef = React.useRef<Form<FormProps>>(null)

useEffect(() => {
const fetchSchema = async (): Promise<void> => {
const latestSchema = await getSchema()
setSchema(latestSchema)
setAppDataForm(INITIAL_FORM_VALUES)
}

fetchSchema()
}, [])

const toggleInvalid = (data: { [key: string]: boolean }): void => {
setInvalidFormDataAttempted((prevState) => ({ ...prevState, ...data }))
}

const handleMetadataErrors = useCallback(
(_: FormProps, errors: FormValidation): FormValidation => handleErrors(formRef, errors, setDisabledAppData),
[],
)

const handleIPFSErrors = useCallback(
(_: FormProps, errors: FormValidation): FormValidation => handleErrors(ipfsFormRef, errors, setDisabledIPFS),
[],
)

const handleOnChange = useCallback(
({ formData }: FormProps): void => {
const resetFormFields = (form: string): void => {
setDisabledIPFS(true)
setIpfsCredentials({})
toggleInvalid({ ipfs: false })
if (form === 'appData') {
setDisabledAppData(true)
}
}
setAppDataForm(formData)
if (ipfsHashInfo) {
setIpfsHashInfo(undefined)
setIsDocUploaded(false)
resetFormFields('appData')
setError(undefined)
}
if (JSON.stringify(handleFormatData(formData)) !== JSON.stringify(INITIAL_FORM_VALUES)) {
setDisabledAppData(false)
}
},
[ipfsHashInfo],
)

const onSubmit = useCallback(async ({ formData }: FormProps): Promise<void> => {
setIsLoading(true)
try {
const hashInfo = await COW_SDK[Network.MAINNET]?.metadataApi.calculateAppDataHash(handleFormatData(formData))
setIpfsHashInfo(hashInfo)
} catch (e) {
setError(e.message)
} finally {
setIsLoading(false)
toggleInvalid({ appData: true })
}
}, [])

const handleIPFSOnChange = useCallback(({ formData: ipfsData }: FormProps): void => {
setIpfsCredentials(ipfsData)
if (JSON.stringify(ipfsData) !== JSON.stringify({})) {
setDisabledIPFS(false)
}
}, [])

const onUploadToIPFS = useCallback(
async ({ formData }: FormProps): Promise<void> => {
if (!ipfsHashInfo) return
setIsLoading(true)
try {
await COW_SDK[Network.MAINNET]?.updateContext({ ipfs: formData })
await COW_SDK[Network.MAINNET]?.metadataApi.uploadMetadataDocToIpfs(handleFormatData(appDataForm))
setIsDocUploaded(true)
} catch (e) {
if (INVALID_IPFS_CREDENTIALS.includes(e.message)) {
e.message = 'Invalid API keys provided.'
}
setError(e.message)
setIsDocUploaded(false)
} finally {
setIsLoading(false)
toggleInvalid({ ipfs: true })
}
},
[appDataForm, ipfsHashInfo],
)

return (
<>
<div className="form-container">
<Form
className="data-form"
liveOmit
liveValidate={invalidFormDataAttempted.appData}
omitExtraData
showErrorList={false}
fields={{ cField: CustomField }}
noHtml5Validate
onChange={handleOnChange}
formData={appDataForm}
validate={handleMetadataErrors}
transformErrors={transformErrors}
ref={formRef}
autoComplete="off"
onSubmit={onSubmit}
onError={(): void => toggleInvalid({ appData: true })}
schema={schema}
uiSchema={uiSchema}
>
<button className="btn btn-info" disabled={disabledAppData} type="submit">
GENERATE APPDATA DOC
</button>
</Form>
<AppDataWrapper>
<div className="hidden-content">
<p>AppData Root Schema information can be found in:</p>
<ExternalLink
target={'_blank'}
rel="noopener noreferrer"
href={`https://docs.cow.fi/front-end/creating-app-ids`}
>
{' '}
AppData documentation
</ExternalLink>{' '}
|
<ExternalLink
target={'_blank'}
rel="noopener noreferrer"
href={`https://docs.cow.fi/front-end/creating-app-ids/create-the-order-meta-data-file/metadata`}
>
{' '}
Metadata documentation
</ExternalLink>
<RowWithCopyButton
textToCopy={JSON.stringify(handleFormatData(appDataForm), null, 2)}
contentsToDisplay={
<pre className="json-formatter">{JSON.stringify(handleFormatData(appDataForm), null, 2)}</pre>
}
/>
{ipfsHashInfo && (
<>
<h4>AppData Hash</h4>
<RowWithCopyButton
className="appData-hash"
textToCopy={ipfsHashInfo.appDataHash}
contentsToDisplay={ipfsHashInfo.appDataHash}
/>
</>
)}
</div>
</AppDataWrapper>
</div>
<div className="ipfs-container">
{ipfsHashInfo && (
<>
<IpfsWrapper>
<Form
className="data-form"
showErrorList={false}
onSubmit={onUploadToIPFS}
liveValidate={invalidFormDataAttempted.ipfs}
onChange={handleIPFSOnChange}
formData={ipfsCredentials}
validate={handleIPFSErrors}
fields={{ cField: CustomField }}
ref={ipfsFormRef}
noHtml5Validate
onError={(): void => toggleInvalid({ ipfs: true })}
transformErrors={transformErrors}
schema={ipfsSchema}
uiSchema={ipfsUiSchema}
>
<button className="btn btn-info" disabled={disabledIPFS} type="submit">
UPLOAD APPDATA TO IPFS
</button>
</Form>
</IpfsWrapper>
</>
)}
{isDocUploaded && (
<>
<RowWithCopyButton
className="appData-hash"
textToCopy={`${DEFAULT_IPFS_READ_URI}/${ipfsHashInfo?.cidV0}`}
contentsToDisplay={
<a href={`${DEFAULT_IPFS_READ_URI}/${ipfsHashInfo?.cidV0}`} target="_blank" rel="noopener noreferrer">
{ipfsHashInfo?.cidV0}
</a>
}
/>
<Notification
type="success"
message="Document uploaded successfully!"
closable={false}
appendMessage={false}
/>
</>
)}
{isLoading && <Spinner />}
{error && !isDocUploaded && (
<Notification type="error" message={error} closable={false} appendMessage={false} />
)}
</div>
</>
)
}

export default FormPage
Loading