mirror of
https://github.com/oceanprotocol/commons.git
synced 2023-03-15 18:03:00 +01:00
more user feedback, refactoring
This commit is contained in:
parent
1c59d49d5d
commit
3b889725f1
@ -4,13 +4,15 @@ import Ipfs from 'ipfs'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
let ipfs: any = null
|
||||
let ipfsMessage: string | null = null
|
||||
let ipfsMessage = ''
|
||||
|
||||
export default function useIpfs() {
|
||||
const [isIpfsReady, setIpfsReady] = useState(Boolean(ipfs))
|
||||
const [ipfsInitError, setIpfsInitError] = useState(null)
|
||||
|
||||
async function startIpfs() {
|
||||
ipfsMessage = 'Starting IPFS...'
|
||||
|
||||
if (ipfs) {
|
||||
console.log('IPFS already started')
|
||||
// } else if (window.ipfs && window.ipfs.enable) {
|
||||
@ -18,7 +20,7 @@ export default function useIpfs() {
|
||||
// ipfs = await window.ipfs.enable()
|
||||
} else {
|
||||
try {
|
||||
const message = 'IPFS Started'
|
||||
const message = 'IPFS started'
|
||||
console.time(message)
|
||||
ipfs = await Ipfs.create()
|
||||
console.timeEnd(message)
|
||||
@ -40,12 +42,13 @@ export default function useIpfs() {
|
||||
// just like componentDidUnmount()
|
||||
return function cleanup() {
|
||||
if (ipfs && ipfs.stop) {
|
||||
console.time('IPFS Stopped')
|
||||
console.time('IPFS stopped')
|
||||
ipfs.stop()
|
||||
setIpfsReady(false)
|
||||
ipfs = null
|
||||
ipfsMessage = null
|
||||
console.timeEnd('IPFS Stopped')
|
||||
ipfsMessage = ''
|
||||
setIpfsInitError(null)
|
||||
console.timeEnd('IPFS stopped')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
@ -1,5 +1,52 @@
|
||||
@import '../../../styles/variables';
|
||||
|
||||
.ipfsForm {
|
||||
margin-top: $spacer;
|
||||
margin-top: $spacer / 2;
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
border-radius: $border-radius;
|
||||
padding: $spacer / 2;
|
||||
background: $body-background;
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
border: .1rem solid $brand-grey-lighter;
|
||||
border-radius: $border-radius;
|
||||
padding: $spacer / 2 $spacer / 2;
|
||||
margin-top: $spacer / 2;
|
||||
background: $brand-white;
|
||||
transition: border .2s ease-out;
|
||||
|
||||
&:hover {
|
||||
border-color: $brand-grey-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: $font-size-small;
|
||||
margin-top: $spacer / 2;
|
||||
color: $brand-grey-light;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: .5rem;
|
||||
height: .5rem;
|
||||
display: inline-block;
|
||||
background: $green;
|
||||
border-radius: 50%;
|
||||
margin-right: $spacer / 4;
|
||||
margin-bottom: .05rem;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
composes: message;
|
||||
color: $red;
|
||||
|
||||
&:before {
|
||||
border-radius: 0;
|
||||
background: $red;
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,66 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import useIpfs from '../../../hooks/use-ipfs'
|
||||
import Label from '../../../components/atoms/Form/Label'
|
||||
import Spinner from '../../../components/atoms/Spinner'
|
||||
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}`)
|
||||
if (response.status !== 200) console.error(`Not found: ${url}`)
|
||||
|
||||
console.log(`File found under ${url}`)
|
||||
console.log(`File found: ${url}`)
|
||||
return
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export default function Ipfs({ addItem }: { addItem(url: string): void }) {
|
||||
const { ipfs, ipfsInitError, ipfsMessage } = useIpfs()
|
||||
function formatBytes(a: number, b: number) {
|
||||
if (a === 0) return '0 Bytes'
|
||||
const c = 1024
|
||||
const d = b || 2
|
||||
const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
const f = Math.floor(Math.log(a) / Math.log(c))
|
||||
|
||||
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f]
|
||||
}
|
||||
|
||||
export default function Ipfs({ addFile }: { addFile(url: string): void }) {
|
||||
const { ipfs, isIpfsReady, ipfsInitError, ipfsMessage } = useIpfs()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
async function saveToIpfs(buffer: Buffer) {
|
||||
setLoading(true)
|
||||
setMessage('Adding to local IPFS node<br />')
|
||||
|
||||
try {
|
||||
const response = await ipfs.add(buffer)
|
||||
const response = await ipfs.add(buffer, {
|
||||
progress: (length: number) => {
|
||||
setMessage(
|
||||
`Adding to local IPFS node<br />
|
||||
${formatBytes(length, 0)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const cid = response[0].hash
|
||||
console.log(`File added: ${cid}`)
|
||||
|
||||
// ping url to make it globally available
|
||||
const url = `https://ipfs.io/ipfs/${cid}`
|
||||
setMessage('Checking global IPFS URL')
|
||||
await pingUrl(url)
|
||||
|
||||
// add IPFS url to file.url
|
||||
addItem(url)
|
||||
addFile(url)
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,12 +77,26 @@ export default function Ipfs({ addItem }: { addItem(url: string): void }) {
|
||||
|
||||
return (
|
||||
<div className={styles.ipfsForm}>
|
||||
<Label htmlFor="fileUpload" required>
|
||||
Add File To IPFS
|
||||
</Label>
|
||||
{loading ? (
|
||||
<Spinner message={message} />
|
||||
) : (
|
||||
<input
|
||||
type="file"
|
||||
name="fileUpload"
|
||||
id="fileUpload"
|
||||
onChange={e => handleCaptureFile(e.target.files)}
|
||||
disabled={!isIpfsReady}
|
||||
/>
|
||||
{ipfsMessage && <div>{ipfsMessage}</div>}
|
||||
{ipfsInitError && <div>{ipfsInitError}</div>}
|
||||
)}
|
||||
{ipfsMessage !== '' && (
|
||||
<div className={styles.message}>{ipfsMessage}</div>
|
||||
)}
|
||||
{ipfsInitError && (
|
||||
<div className={styles.error}>{ipfsInitError}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -2,7 +2,10 @@
|
||||
|
||||
.itemForm {
|
||||
margin-top: $spacer / 2;
|
||||
padding-left: $spacer / 2;
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
border-radius: $border-radius;
|
||||
padding: $spacer / 2;
|
||||
background: $body-background;
|
||||
|
||||
button {
|
||||
margin-top: -($spacer * 2);
|
||||
|
@ -2,10 +2,10 @@ import React from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import ItemForm from './ItemForm'
|
||||
|
||||
const addItem = jest.fn()
|
||||
const addFile = jest.fn()
|
||||
|
||||
const setup = () => {
|
||||
const utils = render(<ItemForm placeholder="Hello" addItem={addItem} />)
|
||||
const utils = render(<ItemForm placeholder="Hello" addFile={addFile} />)
|
||||
const input = utils.getByPlaceholderText('Hello')
|
||||
const button = utils.getByText('Add File')
|
||||
const { container } = utils
|
||||
@ -23,17 +23,17 @@ describe('ItemForm', () => {
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('fires addItem', async () => {
|
||||
it('fires addFile', async () => {
|
||||
const { input, button } = setup()
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'https://hello.com' }
|
||||
})
|
||||
fireEvent.click(button)
|
||||
expect(addItem).toHaveBeenCalled()
|
||||
expect(addFile).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not fire addItem when no url present', () => {
|
||||
it('does not fire addFile when no url present', () => {
|
||||
const { input, button, container } = setup()
|
||||
|
||||
// empty url
|
||||
|
@ -5,7 +5,7 @@ import Button from '../../../components/atoms/Button'
|
||||
import styles from './ItemForm.module.scss'
|
||||
|
||||
interface ItemFormProps {
|
||||
addItem(url: string): void
|
||||
addFile(url: string): void
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ export default class ItemForm extends PureComponent<
|
||||
return
|
||||
}
|
||||
|
||||
this.props.addItem(url)
|
||||
this.props.addFile(url)
|
||||
}
|
||||
|
||||
private onChangeUrl = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
|
@ -2,6 +2,10 @@
|
||||
|
||||
.newItems {
|
||||
margin-top: $spacer / 2;
|
||||
|
||||
> button {
|
||||
margin-right: $spacer;
|
||||
}
|
||||
}
|
||||
|
||||
.itemsList {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent, waitForElement, act } from '@testing-library/react'
|
||||
import { render, fireEvent, waitForElement } from '@testing-library/react'
|
||||
import mockAxios from 'jest-mock-axios'
|
||||
import Files from '.'
|
||||
|
||||
@ -56,21 +56,20 @@ describe('Files', () => {
|
||||
const { container, getByText } = renderComponent()
|
||||
|
||||
// open
|
||||
fireEvent.click(getByText('+ Add a file URL'))
|
||||
fireEvent.click(getByText('+ From public URL'))
|
||||
await waitForElement(() => getByText('- Cancel'))
|
||||
expect(container.querySelector('.itemForm')).toBeInTheDocument()
|
||||
|
||||
// close
|
||||
fireEvent.click(getByText('- Cancel'))
|
||||
await waitForElement(() => getByText('+ Add a file URL'))
|
||||
expect(container.querySelector('.grow-exit')).toBeInTheDocument()
|
||||
await waitForElement(() => getByText('+ From public URL'))
|
||||
expect(container.querySelector('.itemForm')).not.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()
|
||||
@ -78,8 +77,7 @@ describe('Files', () => {
|
||||
// close
|
||||
fireEvent.click(getByText('- Cancel'))
|
||||
await waitForElement(() => getByText('+ Add to IPFS'))
|
||||
expect(container.querySelector('.grow-exit')).toBeInTheDocument()
|
||||
})
|
||||
expect(container.querySelector('.ipfsForm')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('item can be removed', async () => {
|
||||
@ -92,7 +90,7 @@ describe('Files', () => {
|
||||
it('item can be added', async () => {
|
||||
const { getByText, getByPlaceholderText } = renderComponent()
|
||||
|
||||
fireEvent.click(getByText('+ Add a file URL'))
|
||||
fireEvent.click(getByText('+ From public URL'))
|
||||
await waitForElement(() => getByText('- Cancel'))
|
||||
fireEvent.change(getByPlaceholderText('Hello'), {
|
||||
target: { value: 'https://hello.com' }
|
||||
|
@ -1,5 +1,4 @@
|
||||
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'
|
||||
@ -58,15 +57,21 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
|
||||
private toggleForm = (e: Event) => {
|
||||
e.preventDefault()
|
||||
this.setState({ isFormShown: !this.state.isFormShown })
|
||||
this.setState({
|
||||
isFormShown: !this.state.isFormShown,
|
||||
isIpfsFormShown: false
|
||||
})
|
||||
}
|
||||
|
||||
private toggleIpfsForm = (e: Event) => {
|
||||
e.preventDefault()
|
||||
this.setState({ isIpfsFormShown: !this.state.isIpfsFormShown })
|
||||
this.setState({
|
||||
isIpfsFormShown: !this.state.isIpfsFormShown,
|
||||
isFormShown: false
|
||||
})
|
||||
}
|
||||
|
||||
private addItem = async (url: string) => {
|
||||
private async getFile(url: string) {
|
||||
const file: File = {
|
||||
url,
|
||||
contentType: '',
|
||||
@ -83,15 +88,34 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
})
|
||||
|
||||
const { contentLength, contentType, found } = response.data.result
|
||||
|
||||
file.contentLength = contentLength
|
||||
file.contentType = contentType
|
||||
file.compression = cleanupContentType(contentType)
|
||||
file.found = found
|
||||
|
||||
return file
|
||||
} catch (error) {
|
||||
!axios.isCancel(error) && Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
private addFile = async (url: string) => {
|
||||
// check for duplicate urls
|
||||
const duplicateFiles = this.props.files.filter(props =>
|
||||
url.includes(props.url)
|
||||
)
|
||||
|
||||
if (duplicateFiles.length > 0) {
|
||||
return this.setState({
|
||||
isFormShown: false,
|
||||
isIpfsFormShown: false
|
||||
})
|
||||
}
|
||||
|
||||
const file: File | undefined = await this.getFile(url)
|
||||
file && this.props.files.push(file)
|
||||
|
||||
this.props.files.push(file)
|
||||
const event = {
|
||||
currentTarget: {
|
||||
name: 'files',
|
||||
@ -99,13 +123,14 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
}
|
||||
}
|
||||
this.props.onChange(event as any)
|
||||
|
||||
this.setState({
|
||||
isFormShown: !this.state.isFormShown,
|
||||
isIpfsFormShown: !this.state.isIpfsFormShown
|
||||
isFormShown: false,
|
||||
isIpfsFormShown: false
|
||||
})
|
||||
}
|
||||
|
||||
private removeItem = (index: number) => {
|
||||
private removeFile = (index: number) => {
|
||||
this.props.files.splice(index, 1)
|
||||
const event = {
|
||||
currentTarget: {
|
||||
@ -136,57 +161,33 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
|
||||
<div className={styles.newItems}>
|
||||
{files.length > 0 && (
|
||||
<TransitionGroup
|
||||
component="ul"
|
||||
className={styles.itemsList}
|
||||
>
|
||||
<ul className={styles.itemsList}>
|
||||
{files.map((item: any, index: number) => (
|
||||
<CSSTransition
|
||||
key={index}
|
||||
timeout={400}
|
||||
classNames="fade"
|
||||
>
|
||||
<Item
|
||||
key={item.url}
|
||||
item={item}
|
||||
removeItem={() =>
|
||||
this.removeItem(index)
|
||||
}
|
||||
removeItem={() => this.removeFile(index)}
|
||||
/>
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<Button link onClick={this.toggleForm}>
|
||||
{isFormShown ? '- Cancel' : '+ Add a file URL'}
|
||||
{isFormShown ? '- Cancel' : '+ From public URL'}
|
||||
</Button>
|
||||
|
||||
<Button link onClick={this.toggleIpfsForm}>
|
||||
{isIpfsFormShown ? '- Cancel' : '+ Add to IPFS'}
|
||||
</Button>
|
||||
|
||||
<CSSTransition
|
||||
classNames="grow"
|
||||
in={isFormShown}
|
||||
timeout={200}
|
||||
unmountOnExit
|
||||
onExit={() => this.setState({ isFormShown: false })}
|
||||
>
|
||||
{isFormShown && (
|
||||
<ItemForm
|
||||
placeholder={placeholder}
|
||||
addItem={this.addItem}
|
||||
addFile={this.addFile}
|
||||
/>
|
||||
</CSSTransition>
|
||||
)}
|
||||
|
||||
<CSSTransition
|
||||
classNames="grow"
|
||||
in={isIpfsFormShown}
|
||||
timeout={200}
|
||||
unmountOnExit
|
||||
onExit={() => this.setState({ isIpfsFormShown: false })}
|
||||
>
|
||||
<Ipfs addItem={this.addItem} />
|
||||
</CSSTransition>
|
||||
{isIpfsFormShown && <Ipfs addFile={this.addFile} />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user