mirror of
https://github.com/oceanprotocol/commons.git
synced 2023-03-15 18:03:00 +01:00
working prototype of adding files to IPFS during publish flow
This commit is contained in:
parent
414dcd455a
commit
1c59d49d5d
8893
client/package-lock.json
generated
8893
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,7 @@
|
||||
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||
"filesize": "^4.1.2",
|
||||
"history": "^4.9.0",
|
||||
"ipfs": "^0.37.1",
|
||||
"is-url-superb": "^3.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"query-string": "^6.8.2",
|
||||
|
1
client/src/@types/ipfs/index.d.ts
vendored
Normal file
1
client/src/@types/ipfs/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'ipfs'
|
@ -36,23 +36,15 @@ export default class Button extends PureComponent<ButtonProps, any> {
|
||||
classes = styles.button
|
||||
}
|
||||
|
||||
if (to) {
|
||||
return (
|
||||
return to ? (
|
||||
<Link to={to} className={cx(classes, className)} {...props}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
) : href ? (
|
||||
<a href={href} className={cx(classes, className)} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
) : (
|
||||
<button className={cx(classes, className)} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
|
@ -7,7 +7,7 @@
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
// default: red square
|
||||
/* default: red square */
|
||||
.statusIndicator {
|
||||
width: $font-size-small;
|
||||
height: $font-size-small;
|
||||
@ -15,7 +15,7 @@
|
||||
background: $red;
|
||||
}
|
||||
|
||||
// yellow triangle
|
||||
/* yellow triangle */
|
||||
.statusIndicatorCloseEnough {
|
||||
composes: statusIndicator;
|
||||
background: none;
|
||||
@ -26,7 +26,7 @@
|
||||
border-bottom: $font-size-small solid $yellow;
|
||||
}
|
||||
|
||||
// green circle
|
||||
/* green circle */
|
||||
.statusIndicatorActive {
|
||||
composes: statusIndicator;
|
||||
border-radius: 50%;
|
||||
|
54
client/src/hooks/use-ipfs.tsx
Normal file
54
client/src/hooks/use-ipfs.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import Ipfs from 'ipfs'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
let ipfs: any = null
|
||||
let ipfsMessage: string | null = null
|
||||
|
||||
export default function useIpfs() {
|
||||
const [isIpfsReady, setIpfsReady] = useState(Boolean(ipfs))
|
||||
const [ipfsInitError, setIpfsInitError] = useState(null)
|
||||
|
||||
async function startIpfs() {
|
||||
if (ipfs) {
|
||||
console.log('IPFS already started')
|
||||
// } else if (window.ipfs && window.ipfs.enable) {
|
||||
// console.log('Found window.ipfs')
|
||||
// ipfs = await window.ipfs.enable()
|
||||
} else {
|
||||
try {
|
||||
const message = 'IPFS Started'
|
||||
console.time(message)
|
||||
ipfs = await Ipfs.create()
|
||||
console.timeEnd(message)
|
||||
ipfsMessage = message
|
||||
} catch (error) {
|
||||
const message = `IPFS init error: ${error.message}`
|
||||
ipfsMessage = message
|
||||
console.error(message)
|
||||
ipfs = null
|
||||
setIpfsInitError(error)
|
||||
}
|
||||
}
|
||||
setIpfsReady(Boolean(ipfs))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
startIpfs()
|
||||
|
||||
// just like componentDidUnmount()
|
||||
return function cleanup() {
|
||||
if (ipfs && ipfs.stop) {
|
||||
console.time('IPFS Stopped')
|
||||
ipfs.stop()
|
||||
setIpfsReady(false)
|
||||
ipfs = null
|
||||
ipfsMessage = null
|
||||
console.timeEnd('IPFS Stopped')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { ipfs, isIpfsReady, ipfsInitError, ipfsMessage }
|
||||
}
|
5
client/src/routes/Publish/Files/Ipfs.module.scss
Normal file
5
client/src/routes/Publish/Files/Ipfs.module.scss
Normal file
@ -0,0 +1,5 @@
|
||||
@import '../../../styles/variables';
|
||||
|
||||
.ipfsForm {
|
||||
margin-top: $spacer;
|
||||
}
|
61
client/src/routes/Publish/Files/Ipfs.tsx
Normal file
61
client/src/routes/Publish/Files/Ipfs.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import React from 'react'
|
||||
import axios from 'axios'
|
||||
import useIpfs from '../../../hooks/use-ipfs'
|
||||
import styles from './Ipfs.module.scss'
|
||||
|
||||
async function pingUrl(url: string) {
|
||||
try {
|
||||
const response = await axios(url)
|
||||
if (response.status !== 200) console.error(`Could not find ${url}`)
|
||||
|
||||
console.log(`File found under ${url}`)
|
||||
return
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export default function Ipfs({ addItem }: { addItem(url: string): void }) {
|
||||
const { ipfs, ipfsInitError, ipfsMessage } = useIpfs()
|
||||
|
||||
async function saveToIpfs(buffer: Buffer) {
|
||||
try {
|
||||
const response = await ipfs.add(buffer)
|
||||
const cid = response[0].hash
|
||||
console.log(`File added: ${cid}`)
|
||||
|
||||
// ping url to make it globally available
|
||||
const url = `https://ipfs.io/ipfs/${cid}`
|
||||
await pingUrl(url)
|
||||
|
||||
// add IPFS url to file.url
|
||||
addItem(url)
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCaptureFile(files: FileList | null) {
|
||||
const reader: any = new window.FileReader()
|
||||
const file = files && files[0]
|
||||
|
||||
reader.readAsArrayBuffer(file)
|
||||
reader.onloadend = () => {
|
||||
const buffer: any = Buffer.from(reader.result)
|
||||
saveToIpfs(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.ipfsForm}>
|
||||
<input
|
||||
type="file"
|
||||
onChange={e => handleCaptureFile(e.target.files)}
|
||||
/>
|
||||
{ipfsMessage && <div>{ipfsMessage}</div>}
|
||||
{ipfsInitError && <div>{ipfsInitError}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent, waitForElement } from '@testing-library/react'
|
||||
import { render, fireEvent, waitForElement, act } from '@testing-library/react'
|
||||
import mockAxios from 'jest-mock-axios'
|
||||
import Files from '.'
|
||||
|
||||
@ -56,16 +56,32 @@ describe('Files', () => {
|
||||
const { container, getByText } = renderComponent()
|
||||
|
||||
// open
|
||||
fireEvent.click(getByText('+ Add a file'))
|
||||
fireEvent.click(getByText('+ Add a file URL'))
|
||||
await waitForElement(() => getByText('- Cancel'))
|
||||
expect(container.querySelector('.itemForm')).toBeInTheDocument()
|
||||
|
||||
// close
|
||||
fireEvent.click(getByText('- Cancel'))
|
||||
await waitForElement(() => getByText('+ Add a file'))
|
||||
await waitForElement(() => getByText('+ Add a file URL'))
|
||||
expect(container.querySelector('.grow-exit')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('new IPFS file form can be opened and closed', async () => {
|
||||
const { container, getByText } = renderComponent()
|
||||
|
||||
// open
|
||||
act(async () => {
|
||||
fireEvent.click(getByText('+ Add to IPFS'))
|
||||
await waitForElement(() => getByText('- Cancel'))
|
||||
expect(container.querySelector('.ipfsForm')).toBeInTheDocument()
|
||||
|
||||
// close
|
||||
fireEvent.click(getByText('- Cancel'))
|
||||
await waitForElement(() => getByText('+ Add to IPFS'))
|
||||
expect(container.querySelector('.grow-exit')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('item can be removed', async () => {
|
||||
const { getByTitle } = renderComponent()
|
||||
|
||||
@ -76,7 +92,7 @@ describe('Files', () => {
|
||||
it('item can be added', async () => {
|
||||
const { getByText, getByPlaceholderText } = renderComponent()
|
||||
|
||||
fireEvent.click(getByText('+ Add a file'))
|
||||
fireEvent.click(getByText('+ Add a file URL'))
|
||||
await waitForElement(() => getByText('- Cancel'))
|
||||
fireEvent.change(getByPlaceholderText('Hello'), {
|
||||
target: { value: 'https://hello.com' }
|
||||
|
@ -1,15 +1,16 @@
|
||||
import React, { FormEvent, PureComponent, ChangeEvent } from 'react'
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
||||
import axios from 'axios'
|
||||
import { Logger } from '@oceanprotocol/squid'
|
||||
import Button from '../../../components/atoms/Button'
|
||||
import Help from '../../../components/atoms/Form/Help'
|
||||
import ItemForm from './ItemForm'
|
||||
import Item from './Item'
|
||||
import Ipfs from './Ipfs'
|
||||
import styles from './index.module.scss'
|
||||
|
||||
import { serviceUri } from '../../../config'
|
||||
import cleanupContentType from '../../../utils/cleanupContentType'
|
||||
import { Logger } from '@oceanprotocol/squid'
|
||||
|
||||
export interface File {
|
||||
url: string
|
||||
@ -39,11 +40,13 @@ interface FilesProps {
|
||||
|
||||
interface FilesStates {
|
||||
isFormShown: boolean
|
||||
isIpfsFormShown: boolean
|
||||
}
|
||||
|
||||
export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
public state: FilesStates = {
|
||||
isFormShown: false
|
||||
isFormShown: false,
|
||||
isIpfsFormShown: false
|
||||
}
|
||||
|
||||
// for canceling axios requests
|
||||
@ -55,13 +58,17 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
|
||||
private toggleForm = (e: Event) => {
|
||||
e.preventDefault()
|
||||
|
||||
this.setState({ isFormShown: !this.state.isFormShown })
|
||||
}
|
||||
|
||||
private addItem = async (value: string) => {
|
||||
private toggleIpfsForm = (e: Event) => {
|
||||
e.preventDefault()
|
||||
this.setState({ isIpfsFormShown: !this.state.isIpfsFormShown })
|
||||
}
|
||||
|
||||
private addItem = async (url: string) => {
|
||||
const file: File = {
|
||||
url: value,
|
||||
url,
|
||||
contentType: '',
|
||||
found: false // non-standard
|
||||
}
|
||||
@ -71,14 +78,14 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
url: `${serviceUri}/api/v1/urlcheck`,
|
||||
data: { url: value },
|
||||
data: { url },
|
||||
cancelToken: this.signal.token
|
||||
})
|
||||
|
||||
const { contentLength, contentType, found } = response.data.result
|
||||
file.contentLength = contentLength
|
||||
file.contentType = contentType
|
||||
file.compression = await cleanupContentType(contentType)
|
||||
file.compression = cleanupContentType(contentType)
|
||||
file.found = found
|
||||
} catch (error) {
|
||||
!axios.isCancel(error) && Logger.error(error.message)
|
||||
@ -92,7 +99,10 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
}
|
||||
}
|
||||
this.props.onChange(event as any)
|
||||
this.setState({ isFormShown: !this.state.isFormShown })
|
||||
this.setState({
|
||||
isFormShown: !this.state.isFormShown,
|
||||
isIpfsFormShown: !this.state.isIpfsFormShown
|
||||
})
|
||||
}
|
||||
|
||||
private removeItem = (index: number) => {
|
||||
@ -108,8 +118,8 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isFormShown } = this.state
|
||||
const { files, help, placeholder, name, onChange } = this.props
|
||||
const { isFormShown, isIpfsFormShown } = this.state
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -148,7 +158,11 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
)}
|
||||
|
||||
<Button link onClick={this.toggleForm}>
|
||||
{isFormShown ? '- Cancel' : '+ Add a file'}
|
||||
{isFormShown ? '- Cancel' : '+ Add a file URL'}
|
||||
</Button>
|
||||
|
||||
<Button link onClick={this.toggleIpfsForm}>
|
||||
{isIpfsFormShown ? '- Cancel' : '+ Add to IPFS'}
|
||||
</Button>
|
||||
|
||||
<CSSTransition
|
||||
@ -163,6 +177,16 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
addItem={this.addItem}
|
||||
/>
|
||||
</CSSTransition>
|
||||
|
||||
<CSSTransition
|
||||
classNames="grow"
|
||||
in={isIpfsFormShown}
|
||||
timeout={200}
|
||||
unmountOnExit
|
||||
onExit={() => this.setState({ isIpfsFormShown: false })}
|
||||
>
|
||||
<Ipfs addItem={this.addItem} />
|
||||
</CSSTransition>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
18
package-lock.json
generated
18
package-lock.json
generated
@ -559,15 +559,15 @@
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz",
|
||||
"integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
|
||||
"integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-jsx": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz",
|
||||
"integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz",
|
||||
"integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==",
|
||||
"dev": true
|
||||
},
|
||||
"ajv": {
|
||||
@ -2118,9 +2118,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
|
||||
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"strip-ansi": {
|
||||
|
Loading…
Reference in New Issue
Block a user