Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

epic: pinning services #1615

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .storybook/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { configure } from '@storybook/react'
import 'react-virtualized/styles.css'
import '../src/index.css'


const req = require.context('../src', true, /\.stories\.js$/)

function loadStories () {
Expand Down
488 changes: 259 additions & 229 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"react-dom": "^16.13.1",
"react-faux-dom": "^4.5.0",
"react-helmet": "^5.2.1",
"react-hook-form": "^6.0.6",
"react-i18next": "^11.7.0",
"react-identicons": "^1.2.4",
"react-joyride": "^2.1.1",
Expand Down
13 changes: 8 additions & 5 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@
<noscript>
You need to enable JavaScript to run this app.
</noscript>


<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<div id="portal-dropdown" class="fixed top-0 left-0"></div>
</body>
</html>
2 changes: 2 additions & 0 deletions public/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"save": "Save",
"saving": "Saving…",
"selectAll": "Select all",
"setPinning": "Set pinning",
"submit": "Submit",
"unpin": "Unpin",
"unselectAll": "Unselect all"
Expand Down Expand Up @@ -68,6 +69,7 @@
"peers": "Peers",
"pinNoun": "Pin",
"pins": "Pins",
"pinStatus": "Pin Status",
"publicKey": "Public key",
"rateIn": "Rate in",
"rateOut": "Rate out",
Expand Down
9 changes: 9 additions & 0 deletions public/locales/en/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
"descriptionFolder": "{count, plural, one {Are you sure you want to delete this folder? This action is permanent and cannot be reversed.} other {Are you sure you want to delete these {count} folders? This action is permanent and cannot be reversed.}}",
"descriptionFile": "{count, plural, one {Are you sure you want to delete this file? This action is permanent and cannot be reversed.} other {Are you sure you want to delete these {count} files? This action is permanent and cannot be reversed.}}"
},
"pinningModal": {
"title": "Select the services with which you wish to pin these files.",
"footer": "Need to add or configure a pinning service? Go to <1>Settings</1>.",
"localNode": "Local node"
},
"addByPathModal": {
"title": "Import from IPFS",
"description": "Insert an IPFS path (CID) to import.",
Expand Down Expand Up @@ -91,6 +96,10 @@
"hashUnavailable": "hash unavailable",
"checkboxLabel": "View more options for {name}",
"pinned": "Pinned",
"blocks": "Blocks",
"allBlocks": "all blocks",
"more": "More",
"files": "Files",
"cidNotFileNorDir": "The current link isn't a file, nor a directory. Try to <0>inspect</0> it instead.",
"sortBy": "Sort items by {name}"
}
47 changes: 47 additions & 0 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"title": "Settings",
"save": "Save",
"saving": "Saving…",
"reset": "Reset",
"pinningServices": {
"title": "Pinning Services",
"description": "Use local pinning to ensure files on your local node persist and are never garbage-collected. You can also link your accounts with other remote pinning services to automatically or selectively persist files with those providers, enabling you to keep backup copies of your files and/or make them available to others when your local node is offline. <1>Check the documentation for further information.</1>"
},
"language": "Language",
"analytics": "Analytics",
"cliTutorMode": "CLI Tutor Mode",
Expand All @@ -12,6 +19,46 @@
},
"apiDescription": "<0>If your node is configured with a <1>custom API address</1>, including a port other than the default 5001, enter it here.</0>",
"cliDescription": "<0>Enable this option to display a \"view code\" <1></1> icon next to common IPFS commands. Clicking it opens a modal with that command's CLI code, so you can paste it into the IPFS command-line interface in your terminal.</0>",
"pinningModal": {
"title": "Select a pinning service provider.",
"description": "Don’t see your pinning service provider? <1>Add a custom one.<1>"
},
"pinningServiceModal": {
"title": "Configure a custom pinning service.",
"description": "Want to make your custom pinning service available to others? <1>Learn how.<1>",
"service": "Service",
"nickname": "Nickname",
"nicknamePlaceholder": "Name for your service",
"apiEndpoint": "API endpoint",
"apiEndpointPlaceholder": "URL for its API endpoint",
"secretApiKey": "Secret API key",
"autoUpload": "Auto upload"
},
"errors": {
"nickname": "Nickname is required",
"apiEndpoint": "Must be a valid URL",
"secretApiKey": "Secret key is required"
},
"actions": {
"addService": "Add Service",
"edit": "Change",
"close": "Close",
"save": "Save",
"cancel": "Cancel"
},
"edit": "Edit",
"visitService": "Visit service",
"remove": "Remove",
"localPinning": "Local Pinning",
"service": "Service",
"files": "Files",
"bandwidthUsed": "Bandwidth Used",
"autoUpload": "Auto Upload",
"autoUploadKeys": {
"ALL_FILES": "All files",
"DISABLED": "Disabled",
"PINS_ONLY": "Pins only"
},
"fetchingSettings": "Fetching settings...",
"configApiNotAvailable": "The IPFS config API is not available. Please disable the \"IPFS Companion\" Web Extension and try again.",
"ipfsDaemonOffline": "The IPFS daemon is offline. Please turn it on and try again.",
Expand Down
4 changes: 2 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export class App extends Component {

return connectDropTarget(
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className='sans-serif h-100' onClick={getNavHelper(this.props.doUpdateUrl)}>
<div className='sans-serif h-100 relative' onClick={getNavHelper(this.props.doUpdateUrl)}>
{/* Tinted overlay that appears when dragging and dropping an item */}
{ canDrop && isOver && <div className='w-100 h-100 top-0 left-0 absolute' style={{ background: 'rgba(99, 202, 210, 0.2)' }} /> }
{ canDrop && isOver && <div className='h-100 top-0 right-0 fixed appOverlay' style={{ background: 'rgba(99, 202, 210, 0.2)' }} /> }
<div className='flex flex-row-reverse-l flex-column-reverse justify-end justify-start-l' style={{ minHeight: '100vh' }}>
<div className='flex-auto-l'>
<div className='flex items-center ph3 ph4-l' style={{ WebkitAppRegion: 'drag', height: 75, background: '#F0F6FA', paddingTop: '20px', paddingBottom: '15px' }}>
Expand Down
29 changes: 18 additions & 11 deletions src/bundles/files/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,21 +463,28 @@ const actions = () => ({
doFilesDismissErrors: () => send({ type: ACTIONS.DISMISS_ERRORS }),

/**
* @param {string} path
*/
doFilesNavigateTo: (path) =>
* @param {Object} fileArgs
* @param {string} fileArgs.path
* @param {string|CID} fileArgs.cid
*/
doFilesNavigateTo: ({ path, cid }) =>
/**
* @param {Context} context
*/
async ({ store }) => {
const link = path.split('/').map(p => encodeURIComponent(p)).join('/')
const files = store.selectFiles()
const url = store.selectFilesPathInfo()

if (files && files.path === link && url) {
await store.doFilesFetch()
} else {
await store.doUpdateHash(link)
try {
const link = path.split('/').map(p => encodeURIComponent(p)).join('/')
const files = store.selectFiles()
const url = store.selectFilesPathInfo()

if (files && files.path === link && url) {
await store.doFilesFetch()
} else {
await store.doUpdateHash(link)
}
} catch (e) {
console.error(e)
store.doUpdateHash(`/ipfs/${cid}`)
}
},

Expand Down
3 changes: 2 additions & 1 deletion src/bundles/files/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ export default () => {
pageContent: {
...pageContent,
content
}
},
sorting: action.payload
}
} else {
return state
Expand Down
2 changes: 1 addition & 1 deletion src/bundles/files/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ type FileContent = {
pinned: boolean
}

type DirectoryContent = {
export type DirectoryContent = {
type: 'directory',
fetched: Time,
path: string,
Expand Down
12 changes: 12 additions & 0 deletions src/bundles/files/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ const selectors = () => ({
*/
selectFiles: (state) => state.files.pageContent,

/**
* @param {Model} state
*/
selectCurrentDirectorySize: (state) => {
return state.files.pageContent?.type === 'directory' && state.files.pageContent?.content?.reduce((prev, curr) => prev + curr.size, 0)
},

/**
* @param {Model} state
*/
Expand Down Expand Up @@ -68,6 +75,11 @@ const selectors = () => ({
*/
selectFilesErrors: (state) => state.files.failed,

/**
* @param {Model} state
*/
selectHasUpperDirectory: (state) => state.files.pageContent?.type === 'directory' && state.files.pageContent?.upper,

selectFilesPathInfo: createSelector(
'selectRouteInfo',
/**
Expand Down
2 changes: 2 additions & 0 deletions src/bundles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import nodeBandwidthChartBundle from './node-bandwidth-chart'
import nodeBandwidthBundle from './node-bandwidth'
import peersBundle from './peers'
import peerLocationsBundle from './peer-locations'
import pinningBundle from './pinning'
import routesBundle from './routes'
import redirectsBundle from './redirects'
import filesBundle from './files'
Expand Down Expand Up @@ -43,6 +44,7 @@ export default composeBundles(
nodeBandwidthChartBundle(),
peersBundle,
peerLocationsBundle(),
pinningBundle,
notifyBundle,
connectedBundle,
retryInitBundle,
Expand Down
101 changes: 101 additions & 0 deletions src/bundles/pinning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// @ts-check
/**
* TODO: This might change, current version from: https://github.com/ipfs/go-ipfs/blob/petar/pincli/core/commands/remotepin.go#L53
* @typedef {Object} RemotePin
* @property {string} id
* @property {string} name
* @property {('queued'|'pinning'|'pinned'|'failed')} status
* @property {string} cid
* @property {Array<string>} [delegates] e.g. ["/dnsaddr/pin-service.example.com"]
*/
export default {
name: 'pinning',
reducer: (state = {
remotePins: []
}, action) => {
if (action.type === 'SET_REMOTE_PINS') {
return { ...state, remotePins: action.payload }
}
return state
},

doFetchRemotePins: () => async ({ dispatch, store }) => {
const pinningServices = store.selectPinningServices()

if (!pinningServices?.length) return

// TODO: unmock this (e.g. const pins = ipfs.pin.remote.ls ...)
const response = [
{
id: 'Pinata:UniqueIdOfPinRequest',
status: 'queued',
cid: 'QmQsUbcVx6Vu8vtL858FdxD3sVBE6m8uP3bjFoTzrGubmX',
name: '26_remote.png',
delegates: ['/dnsaddr/pin-service.example.com']
}
]

// TODO: get type of item?

const remotePins = response.map(item => ({
...item,
isRemotePin: true,
type: item.type || 'unknown',
size: Math.random() * 1000// TODO: files.stat in the future
}))

// TODO: handle different status (queued = async fetch in batches to update ui?)

dispatch({ type: 'SET_REMOTE_PINS', payload: remotePins })
},

selectRemotePins: (state) => state.pinning.remotePins || [],

doSelectRemotePinsForFile: (file) => ({ store }) => {
const pinningServicesNames = store.selectPinningServices().map(remote => remote.name)
const remotePinForFile = store.selectRemotePins().filter(pin => pin.cid === file.cid.string)
const servicesBeingUsed = remotePinForFile.map(pin => pin.id.split(':')[0]).filter(pinId => pinningServicesNames.includes(pinId))

return servicesBeingUsed
},

// selectPinningServices: state => state.pinning
// TODO: unmock this
selectPinningServices: () => ([
{
name: 'Pinata',
icon: 'https://ipfs.io/ipfs/QmVYXV4urQNDzZpddW4zZ9PGvcAbF38BnKWSgch3aNeViW?filename=pinata.svg',
totalSize: 3122312,
bandwidthUsed: '10 GB/mo',
autoUpload: 'ALL_FILES',
addedAt: new Date(1592491648581)
}, {
name: 'Infura',
icon: 'https://ipfs.io/ipfs/QmTt6KeaNXyaaUBWn2zEG8RiMfPPPeMesXqnFWqqC5o6yc?filename=infura.png',
totalSize: 4412221323,
bandwidthUsed: '2 GB/mo',
autoUpload: 'DISABLED',
addedAt: new Date(1592491648591)
}, {
name: 'Eternum',
icon: 'https://ipfs.io/ipfs/QmSrqJeuYrYDmSgAy3SeAyTsYMksNPfK5CSN91xk6BBnF9?filename=eternum.png',
totalSize: 512000,
bandwidthUsed: '6 GB/mo',
autoUpload: 'PINS_ONLY',
addedAt: new Date(1592491648691)
}
]),

selectAvailablePinningServices: () => ([
{
name: 'Pinata',
icon: 'https://ipfs.io/ipfs/QmVYXV4urQNDzZpddW4zZ9PGvcAbF38BnKWSgch3aNeViW?filename=pinata.svg'
}, {
name: 'Infura',
icon: 'https://ipfs.io/ipfs/QmTt6KeaNXyaaUBWn2zEG8RiMfPPPeMesXqnFWqqC5o6yc?filename=infura.png'
}, {
name: 'Eternum',
icon: 'https://ipfs.io/ipfs/QmSrqJeuYrYDmSgAy3SeAyTsYMksNPfK5CSN91xk6BBnF9?filename=eternum.png'
}
])
}
5 changes: 5 additions & 0 deletions src/components/button/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@
box-shadow: initial;
cursor: default;
}

.Button.link:focus {
outline: none;
box-shadow: none;
}
24 changes: 18 additions & 6 deletions src/components/button/Button.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import React from 'react'
import './Button.css'
import classNames from 'classnames'

const Button = ({ bg = 'bg-teal', color = 'white', fill = 'white', className = '', disabled, danger, minWidth = 86, children, style, ...props }) => {
const bgClass = danger ? 'bg-red' : disabled ? 'bg-gray-muted' : bg
const fillClass = danger ? 'fill-white' : disabled ? 'fill-snow' : fill
const colorClass = danger ? 'white' : disabled ? 'light-gray' : color
const cls = `Button transition-all sans-serif dib v-mid fw5 nowrap lh-copy bn br1 pa2 focus-outline ${fillClass} ${bgClass} ${colorClass} ${className}`
const getButtonClassName = ({ fill, bg, color, danger, disabled }, type) => {
if (danger) return 'bg-red fill-white white'
if (disabled) return 'bg-gray-muted fill-snow light-gray'
if (type === 'link') return 'link bg-transparent'
return `${fill} ${bg} ${color}`
}

const Button = ({ className, minWidth, children, style, type, ...props }) => {
return (
<button className={cls} disabled={disabled} style={{ minWidth, ...style }} {...props}>
<button type={type} className={classNames('Button transition-all sans-serif dib v-mid fw5 nowrap lh-copy bn br1 pa2 focus-outline', className, getButtonClassName(props, type))} disabled={props.disabled} style={{ minWidth, ...style }} {...props}>
{children}
</button>
)
}

Button.defaultProps = {
bg: 'bg-teal',
color: 'white',
fill: 'white',
className: '',
minWidth: 140
}

export default Button
Loading