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

feat: add visual feedback on API address change #1671

Merged
merged 28 commits into from
Oct 29, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
23d5f3f
Add error text to API address settings
jack-michaud Oct 14, 2020
7d56569
Populate error message for invalid addresses and connection errors
jack-michaud Oct 15, 2020
6268c09
Add outline to ApiAddressForm to indicate address validity
jack-michaud Oct 15, 2020
ff59fd5
Add apiAddress to "could not connect" message
jack-michaud Oct 15, 2020
d4a4456
Add "pending first API connection" handlers to ipfs-provider
jack-michaud Oct 15, 2020
4a89afd
Add full page loader when pending first connection
jack-michaud Oct 15, 2020
01d3cf2
Remove custom error CSS, instead use Notify for errors
jack-michaud Oct 18, 2020
7483a73
Feedback from @rafaelramalho19 - Use arrow function
jack-michaud Oct 18, 2020
9c46977
Feedback from @jessicashilling and @rafaelramalho19
jack-michaud Oct 18, 2020
1cddd15
Remove connectionError action in ipfs-provider.
jack-michaud Oct 18, 2020
58b0f3c
Remove unused icon, comment
jack-michaud Oct 18, 2020
ebd2af5
Fix bug that shows success message before updating API address
jack-michaud Oct 18, 2020
d88fb5a
Fix formatting
jack-michaud Oct 18, 2020
80f86f9
Remove unused "dispatch"
jack-michaud Oct 18, 2020
e3dbde0
Add custom error messages for connecting to a new IPFS API
jack-michaud Oct 18, 2020
2cf8a47
IPFS_CONNECT_SUCCEED/FAILED set fail state in ipfs-provider
jack-michaud Oct 19, 2020
da320ed
Return result of API address update in doUpdateIpfsApiAddress
jack-michaud Oct 19, 2020
4bf7436
Add ipfsInvalidApiAddress to locales/en/notify.json and notify.js
jack-michaud Oct 19, 2020
c330e50
Refocus on input if the API address failed to update
jack-michaud Oct 19, 2020
10d2fb0
Show green/red border for valid/invalid API address or red border for…
jack-michaud Oct 19, 2020
2746ae8
Change ApiAddressForm to more closely follow SelectPeer
jack-michaud Oct 19, 2020
31d53de
Clean up unused code and comments
jack-michaud Oct 19, 2020
038cbc8
Update comments
jack-michaud Oct 19, 2020
34a1c0a
Remove useRef from imports
jack-michaud Oct 19, 2020
e3a41ad
Disables button with an invalid multiaddr and display red border when
jack-michaud Oct 20, 2020
daef34f
Follow formatting
jack-michaud Oct 20, 2020
4954294
Merge branch 'master' of github.com:ipfs-shipyard/ipfs-webui into fea…
jack-michaud Oct 28, 2020
67a4173
Feedback from @rafaelramalho19
jack-michaud Oct 28, 2020
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
91 changes: 77 additions & 14 deletions src/bundles/ipfs-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { perform } from './task'
* @property {boolean} failed
* @property {boolean} ready
* @property {boolean} invalidAddress
* @property {null|string} connectionError
* @property {boolean} pendingFirstConnection
*
*
* @typedef {import('./task').Perform<'IPFS_INIT', Error, InitResult, void>} Init
* @typedef {Object} Stopped
Expand All @@ -34,19 +37,37 @@ import { perform } from './task'
* @typedef {Object} Dismiss
* @property {'IPFS_API_ADDRESS_INVALID_DISMISS'} type
*
* @typedef {Object} ConnectSuccess
* @property {'IPFS_CONNECT_SUCCEED'} type
*
* @typedef {Object} ConnectFail
* @property {'IPFS_CONNECT_FAILED'} type
*
* @typedef {Object} DismissError
* @property {'NOTIFY_DISMISSED'} type
*
* @typedef {Object} PendingFirstConnection
* @property {'IPFS_API_ADDRESS_PENDING_FIRST_CONNECTION'} type
* @property {boolean} pending
*
* @typedef {Object} InitResult
* @property {ProviderName} provider
* @property {IPFSService} ipfs
* @property {string} [apiAddress]
* @typedef {Init|Stopped|AddressUpdated|AddressInvalid|Dismiss} Message
* @typedef {Init|Stopped|AddressUpdated|AddressInvalid|Dismiss|PendingFirstConnection|ConnectFail|ConnectSuccess|DismissError} Message
*/

export const ACTIONS = Enum.from([
'IPFS_INIT',
'IPFS_STOPPED',
'IPFS_API_ADDRESS_UPDATED',
'IPFS_API_ADDRESS_PENDING_FIRST_CONNECTION',
'IPFS_API_ADDRESS_INVALID',
'IPFS_API_ADDRESS_INVALID_DISMISS'
'IPFS_API_ADDRESS_INVALID_DISMISS',
// Notifier actions
'IPFS_CONNECT_FAILED',
'IPFS_CONNECT_SUCCEED',
'NOTIFY_DISMISSED',
])

/**
Expand Down Expand Up @@ -99,6 +120,10 @@ const update = (state, message) => {
case ACTIONS.IPFS_API_ADDRESS_INVALID_DISMISS: {
return { ...state, invalidAddress: true }
}
case ACTIONS.IPFS_API_ADDRESS_PENDING_FIRST_CONNECTION: {
const { pending } = message
return { ...state, pendingFirstConnection: pending }
}
default: {
return state
}
Expand All @@ -114,7 +139,9 @@ const init = () => {
provider: null,
failed: false,
ready: false,
invalidAddress: false
invalidAddress: false,
connectionError: null,
pendingFirstConnection: false
}
}

Expand All @@ -126,6 +153,14 @@ const readAPIAddressSetting = () => {
return setting == null ? null : asAPIOptions(setting)
}

/**
* @param {string|object} value
* @returns {boolean}
*/
export const checkValidAPIAddress = (value) => {
return asAPIOptions(value) != null;
}

/**
* @param {string|object} value
* @returns {HTTPClientOptions|string|null}
Expand Down Expand Up @@ -297,7 +332,15 @@ const selectors = {
/**
* @param {State} state
*/
selectIpfsInitFailed: state => state.ipfs.failed
selectIpfsInitFailed: state => state.ipfs.failed,
/**
* @param {State} state
*/
selectIpfsConnectionError: state => state.ipfs.connectionError,
/**
* @param {State} state
*/
selectIpfsPendingFirstConnection: state => state.ipfs.pendingFirstConnection,
}

/**
Expand All @@ -310,14 +353,22 @@ const actions = {
/**
* @returns {function(Context):Promise<void>}
*/
doTryInitIpfs: () => async ({ store }) => {
// We need to swallow error that `doInitIpfs` could produce othrewise it
// will bubble up and nothing will handle it. There is a code in
// `bundles/retry-init.js` that reacts to `IPFS_INIT` action and attempts
// to retry.
doTryInitIpfs: () => async ({ store, dispatch }) => {
// There is a code in `bundles/retry-init.js` that reacts to `IPFS_INIT`
// action and attempts to retry.
try {
dispatch({
type: 'NOTIFY_DISMISSED',
});
await store.doInitIpfs()
} catch (_) {
dispatch({
type: 'IPFS_CONNECT_SUCCEED',
});
} catch (error) {
// Catches connection errors like timeouts
dispatch({
type: 'IPFS_CONNECT_FAILED',
});
}
},
/**
Expand Down Expand Up @@ -353,7 +404,7 @@ const actions = {
})

if (!result) {
throw Error('Could not connect to the IPFS API')
throw Error(`Could not connect to the IPFS API (${apiAddress})`)
} else {
return result
}
Expand All @@ -375,12 +426,24 @@ const actions = {
doUpdateIpfsApiAddress: (address) => async (context) => {
const apiAddress = asAPIOptions(address)
if (apiAddress == null) {
context.dispatch({ type: 'IPFS_API_ADDRESS_INVALID' })
context.dispatch({ type: ACTIONS.IPFS_API_ADDRESS_INVALID })
} else {
await writeSetting('ipfsApi', apiAddress)
context.dispatch({ type: 'IPFS_API_ADDRESS_UPDATED', payload: apiAddress })

context.dispatch({ type: ACTIONS.IPFS_API_ADDRESS_UPDATED, payload: apiAddress })

// Sends action to indicate we're going to try to update the IPFS API address.
// There is logic to retry doTryInitIpfs in bundles/retry-init.js, so
// we're triggering the pending update action here to avoid blocking
// the UI while we automatically retry.
context.dispatch({
type: ACTIONS.IPFS_API_ADDRESS_PENDING_FIRST_CONNECTION,
pending: true
});
await context.store.doTryInitIpfs()
context.dispatch({
type: ACTIONS.IPFS_API_ADDRESS_PENDING_FIRST_CONNECTION,
pending: false
});
}
},

Expand Down
23 changes: 23 additions & 0 deletions src/bundles/notify.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ const notify = {
}
}

if (action.type == 'IPFS_CONNECT_FAILED') {
jack-michaud marked this conversation as resolved.
Show resolved Hide resolved
return {
...state,
show: true,
error: true,
eventId: action.type
}
}
if (action.type == 'IPFS_CONNECT_SUCCEED') {
return {
...state,
show: true,
error: false,
eventId: action.type
}
}

return state
},

Expand All @@ -84,6 +101,12 @@ const notify = {
if (eventId === 'STATS_FETCH_FAILED') {
return provider === 'window.ipfs' ? 'windowIpfsRequestFailed' : 'ipfsApiRequestFailed'
}
if (eventId === 'IPFS_CONNECT_FAILED') {
return 'ipfsApiRequestFailed';
}
if (eventId === 'IPFS_CONNECT_SUCCEED') {
return 'ipfsIsBack';
}
jack-michaud marked this conversation as resolved.
Show resolved Hide resolved

if (eventId === 'FILES_EVENT_FAILED') {
const type = code ? code.replace(/^(ERR_)/, '') : ''
Expand Down
20 changes: 17 additions & 3 deletions src/components/api-address-form/ApiAddressForm.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { connect } from 'redux-bundler-react'
import { withTranslation } from 'react-i18next'
import GlyphAttention from '../../icons/GlyphAttention'
import Button from '../button/Button'
import { checkValidAPIAddress } from '../../bundles/ipfs-provider';

const ApiAddressForm = ({ t, doUpdateIpfsApiAddress, ipfsApiAddress }) => {
const ApiAddressForm = ({
t,
doUpdateIpfsApiAddress,
ipfsApiAddress,
}) => {
const [value, setValue] = useState(asAPIString(ipfsApiAddress))
const [isValidAPIAddress, setIsValidAPIAddress] = useState(checkValidAPIAddress(value));

// Updates error based on API connection state.

// Updates "isValidAPIAddress" state
useEffect(() => {
setIsValidAPIAddress(checkValidAPIAddress(value));
}, [value]);

const onChange = (event) => setValue(event.target.value)

Expand All @@ -25,7 +39,7 @@ const ApiAddressForm = ({ t, doUpdateIpfsApiAddress, ipfsApiAddress }) => {
id='api-address'
aria-label={t('apiAddressForm.apiLabel')}
type='text'
className='w-100 lh-copy monospace f5 pl1 pv1 mb2 charcoal input-reset ba b--black-20 br1 focus-outline'
className={`w-100 lh-copy monospace f5 pl1 pv1 mb2 charcoal input-reset ba b--black-20 br1 ${isValidAPIAddress ? 'focus-outline-green' : 'focus-outline-red'}`}
onChange={onChange}
onKeyPress={onKeyPress}
value={value}
Expand Down
19 changes: 17 additions & 2 deletions src/settings/SettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ import Experiments from '../components/experiments/ExperimentsPanel'
import Title from './Title'
import CliTutorMode from '../components/cli-tutor-mode/CliTutorMode'
import Checkbox from '../components/checkbox/Checkbox'
import ComponentLoader from '../loader/ComponentLoader.js'
import StrokeCode from '../icons/StrokeCode'
import { cliCmdKeys, cliCommandList } from '../bundles/files/consts'

const PAUSE_AFTER_SAVE_MS = 3000

export const SettingsPage = ({
t, tReady, isIpfsConnected,
t, tReady, isIpfsConnected, ipfsPendingFirstConnection,
isConfigBlocked, isLoading, isSaving,
hasSaveFailed, hasSaveSucceded, hasErrors, hasLocalChanges, hasExternalChanges,
config, onChange, onReset, onSave, editorKey, analyticsEnabled, doToggleAnalytics,
Expand All @@ -35,11 +36,23 @@ export const SettingsPage = ({
<Helmet>
<title>{t('title')} | IPFS</title>
</Helmet>

{/* Enable a full screen loader after updating to a new IPFS API address.
* Will not show on consequent retries after a failure.
*/}
{ ipfsPendingFirstConnection
? <div className="absolute flex items-center justify-center w-100 h-100"
style={{ background: 'rgba(255, 255, 255, 0.5)', zIndex: '10' }}>
<ComponentLoader pastDelay />
</div>
: null }


<Box className='mb3 pa4 joyride-settings-customapi'>
<div className='lh-copy charcoal'>
<Title>{t('app:terms.apiAddress')}</Title>
<Trans i18nKey='apiDescription' t={t}>

<p>If your node is configured with a <a className='link blue' href='https://github.com/ipfs/go-ipfs/blob/master/docs/config.md#addresses' target='_blank' rel='noopener noreferrer'>custom API address</a>, including a port other than the default 5001, enter it here.</p>
</Trans>
<ApiAddressForm/>
Expand Down Expand Up @@ -278,7 +291,7 @@ export class SettingsPageContainer extends React.Component {
const {
t, tReady, isConfigBlocked, ipfsConnected, configIsLoading, configLastError, configIsSaving,
configSaveLastSuccess, configSaveLastError, isIpfsDesktop, analyticsEnabled, doToggleAnalytics, toursEnabled,
handleJoyrideCallback, isCliTutorModeEnabled, doToggleCliTutorMode
handleJoyrideCallback, isCliTutorModeEnabled, doToggleCliTutorMode, ipfsPendingFirstConnection,
} = this.props
const { hasErrors, hasLocalChanges, hasExternalChanges, editableConfig, editorKey } = this.state
const hasSaveSucceded = this.isRecent(configSaveLastSuccess)
Expand All @@ -290,6 +303,7 @@ export class SettingsPageContainer extends React.Component {
t={t}
tReady={tReady}
isIpfsConnected={ipfsConnected}
ipfsPendingFirstConnection={ipfsPendingFirstConnection}
isConfigBlocked={isConfigBlocked}
isLoading={isLoading}
isSaving={configIsSaving}
Expand Down Expand Up @@ -321,6 +335,7 @@ export const TranslatedSettingsPage = withTranslation('settings')(SettingsPageCo
export default connect(
'selectConfig',
'selectIpfsConnected',
'selectIpfsPendingFirstConnection',
'selectIsConfigBlocked',
'selectConfigLastError',
'selectConfigIsLoading',
Expand Down