1
0
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:
Matthias Kretschmann 2019-09-04 20:25:37 +02:00
parent 1c59d49d5d
commit 3b889725f1
Signed by: m
GPG Key ID: 606EEEF3C479A91F
9 changed files with 182 additions and 85 deletions

View File

@ -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')
}
}
}, [])

View File

@ -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;
}
}

View File

@ -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>
)
}

View File

@ -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);

View File

@ -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

View File

@ -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>) => {

View File

@ -2,6 +2,10 @@
.newItems {
margin-top: $spacer / 2;
> button {
margin-right: $spacer;
}
}
.itemsList {

View File

@ -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' }

View File

@ -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>
</>
)