Skip to content

Commit c1a233f

Browse files
committed
feat: make ipfs-check service URL configurable
- add retrieval diagnostic service settings section - create IpfsCheckForm component for URL configuration - store ipfs-check URL in local storage with default fallback - update CheckScreen to use configurable URL from redux store - add translations for new settings UI users can now configure a custom ipfs-check instance URL via settings to use self-hosted or alternative diagnostic services Closes #2434
1 parent 8e8b475 commit c1a233f

7 files changed

Lines changed: 146 additions & 12 deletions

File tree

public/locales/en/app.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
"publicSubdomainGatewayForm": {
5555
"placeholder": "Enter a URL (https://dweb.link)"
5656
},
57+
"ipfsCheckForm": {
58+
"label": "Retrieval Check Service URL",
59+
"placeholder": "Enter a URL (https://check.ipfs.network)"
60+
},
5761
"terms": {
5862
"address": "Address",
5963
"addresses": "Addresses",

public/locales/en/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
"apiDescription": "<0>If your node is configured with a <1>custom Kubo RPC API address</1>, including a port other than the default 5001, enter it here.</0>",
2626
"publicSubdomainGatewayDescription": "<0>Select a default <1>Subdomain Gateway</1> for generating shareable links.</0>",
2727
"publicPathGatewayDescription": "<0>Select a fallback <1>Path Gateway</1> for generating shareable links for CIDs that exceed the 63-character DNS limit.</0>",
28+
"retrievalDiagnosticService": {
29+
"title": "Retrieval Diagnostic Service",
30+
"description": "Configure the URL of the <0>ipfs-check</0> service used for <1>retrieval diagnostics</1>. This service checks if content can be successfully fetched from your node and other nodes hosting a specific CID, helping you troubleshoot sharing issues."
31+
},
2832
"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>",
2933
"cliModal": {
3034
"extraNotesJsonConfig": "If you've made changes to the config in this page's code editor that you'd like to save, click the download icon next to the copy button to download it as a JSON file."

src/bundles/gateway.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { readSetting, writeSetting } from './local-storage.js'
33
// TODO: switch to dweb.link when https://github.com/ipfs/kubo/issues/7318
44
export const DEFAULT_PATH_GATEWAY = 'https://ipfs.io'
55
export const DEFAULT_SUBDOMAIN_GATEWAY = 'https://dweb.link'
6+
export const DEFAULT_IPFS_CHECK_URL = 'https://check.ipfs.network'
67
const IMG_HASH_1PX = 'bafkreib6wedzfupqy7qh44sie42ub4mvfwnfukmw6s2564flajwnt4cvc4' // 1x1.png
78
const IMG_ARRAY = [
89
{ id: 'IMG_HASH_1PX', name: '1x1.png', hash: IMG_HASH_1PX },
@@ -20,10 +21,16 @@ const readPublicSubdomainGatewaySetting = () => {
2021
return setting || DEFAULT_SUBDOMAIN_GATEWAY
2122
}
2223

24+
const readIpfsCheckUrlSetting = () => {
25+
const setting = readSetting('ipfsCheckUrl')
26+
return setting || DEFAULT_IPFS_CHECK_URL
27+
}
28+
2329
const init = () => ({
2430
availableGateway: null,
2531
publicGateway: readPublicGatewaySetting(),
26-
publicSubdomainGateway: readPublicSubdomainGatewaySetting()
32+
publicSubdomainGateway: readPublicSubdomainGatewaySetting(),
33+
ipfsCheckUrl: readIpfsCheckUrlSetting()
2734
})
2835

2936
export const checkValidHttpUrl = (value) => {
@@ -169,6 +176,10 @@ const bundle = {
169176
return { ...state, publicSubdomainGateway: action.payload }
170177
}
171178

179+
if (action.type === 'SET_IPFS_CHECK_URL') {
180+
return { ...state, ipfsCheckUrl: action.payload }
181+
}
182+
172183
return state
173184
},
174185

@@ -184,11 +195,18 @@ const bundle = {
184195
dispatch({ type: 'SET_PUBLIC_SUBDOMAIN_GATEWAY', payload: address })
185196
},
186197

198+
doUpdateIpfsCheckUrl: (url) => async ({ dispatch }) => {
199+
await writeSetting('ipfsCheckUrl', url)
200+
dispatch({ type: 'SET_IPFS_CHECK_URL', payload: url })
201+
},
202+
187203
selectAvailableGateway: (state) => state?.gateway?.availableGateway,
188204

189205
selectPublicGateway: (state) => state?.gateway?.publicGateway,
190206

191-
selectPublicSubdomainGateway: (state) => state?.gateway?.publicSubdomainGateway
207+
selectPublicSubdomainGateway: (state) => state?.gateway?.publicSubdomainGateway,
208+
209+
selectIpfsCheckUrl: (state) => state?.gateway?.ipfsCheckUrl
192210
}
193211

194212
export default bundle
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { useState, useEffect } from 'react'
2+
import { connect } from 'redux-bundler-react'
3+
import { withTranslation } from 'react-i18next'
4+
import Button from '../button/button.tsx'
5+
import { checkValidHttpUrl, DEFAULT_IPFS_CHECK_URL } from '../../bundles/gateway.js'
6+
7+
const IpfsCheckForm = ({ t, doUpdateIpfsCheckUrl, ipfsCheckUrl }) => {
8+
const [value, setValue] = useState(ipfsCheckUrl)
9+
const initialIsValidUrl = !checkValidHttpUrl(value)
10+
const [showFailState, setShowFailState] = useState(initialIsValidUrl)
11+
const [isValidUrl, setIsValidUrl] = useState(initialIsValidUrl)
12+
13+
// Updates the border of the input to indicate validity
14+
useEffect(() => {
15+
setShowFailState(!isValidUrl)
16+
}, [isValidUrl])
17+
18+
// Updates the border of the input to indicate validity
19+
useEffect(() => {
20+
const isValid = checkValidHttpUrl(value)
21+
setIsValidUrl(isValid)
22+
setShowFailState(!isValid)
23+
}, [value])
24+
25+
const onChange = (event) => setValue(event.target.value)
26+
27+
const onSubmit = async (event) => {
28+
event.preventDefault()
29+
30+
if (!isValidUrl) {
31+
setShowFailState(true)
32+
return
33+
}
34+
35+
doUpdateIpfsCheckUrl(value)
36+
}
37+
38+
const onDefault = async (event) => {
39+
event.preventDefault()
40+
setValue(DEFAULT_IPFS_CHECK_URL)
41+
doUpdateIpfsCheckUrl(DEFAULT_IPFS_CHECK_URL)
42+
}
43+
44+
const onKeyPress = (event) => {
45+
if (event.key === 'Enter') {
46+
onSubmit(event)
47+
}
48+
}
49+
50+
return (
51+
<form onSubmit={onSubmit}>
52+
<input
53+
id='ipfs-check-url'
54+
aria-label={t('ipfsCheckForm.label')}
55+
placeholder={t('ipfsCheckForm.placeholder')}
56+
type='text'
57+
className={`w-100 lh-copy monospace f5 pl1 pv1 mb2 charcoal input-reset ba b--black-20 br1 ${showFailState ? 'focus-outline-red b--red-muted' : 'focus-outline-green b--green-muted'}`}
58+
onChange={onChange}
59+
onKeyPress={onKeyPress}
60+
value={value}
61+
/>
62+
<div className='tr'>
63+
<Button
64+
id='ipfs-check-default-button'
65+
minWidth={100}
66+
height={40}
67+
bg='bg-charcoal'
68+
className='tc'
69+
disabled={value === DEFAULT_IPFS_CHECK_URL}
70+
onClick={onDefault}>
71+
{t('app:actions.reset')}
72+
</Button>
73+
<Button
74+
id='ipfs-check-submit-button'
75+
minWidth={100}
76+
height={40}
77+
className='mt2 mt0-l ml2-l tc'
78+
disabled={!isValidUrl || value === ipfsCheckUrl}>
79+
{t('actions.submit')}
80+
</Button>
81+
</div>
82+
</form>
83+
)
84+
}
85+
86+
export default connect(
87+
'doUpdateIpfsCheckUrl',
88+
'selectIpfsCheckUrl',
89+
withTranslation('app')(IpfsCheckForm)
90+
)

src/diagnostics/check-screen/check-screen.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import React, { useRef, useEffect, useState } from 'react'
2+
import { connect } from 'redux-bundler-react'
23
import { useDebouncedCallback } from '../../lib/hooks/use-debounced-callback'
4+
import { DEFAULT_IPFS_CHECK_URL } from '../../bundles/gateway.js'
35

46
interface CheckScreenProps {
57
cid?: string
8+
ipfsCheckUrl?: string
69
}
710

8-
// TODO: make configurable via Settings screen
9-
const IPFS_CHECK_BASE_URL = 'https://check.ipfs.network/'
10-
const IPFS_CHECK_ORIGIN = new URL(IPFS_CHECK_BASE_URL).origin
11-
12-
const CheckScreen: React.FC<CheckScreenProps> = ({ cid }) => {
11+
const CheckScreen: React.FC<CheckScreenProps> = ({ cid, ipfsCheckUrl }) => {
12+
const ipfsCheckBaseUrl = ipfsCheckUrl || DEFAULT_IPFS_CHECK_URL
13+
const baseUrl = ipfsCheckBaseUrl.endsWith('/') ? ipfsCheckBaseUrl : `${ipfsCheckBaseUrl}/`
14+
const ipfsCheckOrigin = new URL(baseUrl).origin
1315
const ref = useRef<HTMLIFrameElement>(null)
1416
const [isLoading, setIsLoading] = useState(true)
1517

@@ -25,7 +27,7 @@ const CheckScreen: React.FC<CheckScreenProps> = ({ cid }) => {
2527

2628
const onMsg = (e: MessageEvent<any>) => {
2729
// Validate origin to prevent XSS attacks
28-
if (e.origin !== IPFS_CHECK_ORIGIN) return
30+
if (e.origin !== ipfsCheckOrigin) return
2931
if (e.data?.type !== 'iframe-size:report') return
3032

3133
// Hide loading message as soon as we get first message from iframe
@@ -51,10 +53,10 @@ const CheckScreen: React.FC<CheckScreenProps> = ({ cid }) => {
5153
window.removeEventListener('resize', requestSize)
5254
iframe.removeEventListener('load', onLoad)
5355
}
54-
}, [requestSize])
56+
}, [requestSize, ipfsCheckOrigin])
5557

5658
// Build the iframe URL with optional CID parameter
57-
const iframeSrc = cid ? `${IPFS_CHECK_BASE_URL}?cid=${encodeURIComponent(cid)}` : IPFS_CHECK_BASE_URL
59+
const iframeSrc = cid ? `${baseUrl}?cid=${encodeURIComponent(cid)}` : baseUrl
5860

5961
return (
6062
<div className="relative">
@@ -67,7 +69,7 @@ const CheckScreen: React.FC<CheckScreenProps> = ({ cid }) => {
6769
ref={ref}
6870
className="db bn w-100 overflow-y-hidden overflow-x-hidden"
6971
style={{ height: '80vh' }}
70-
title={`Retrieval Check @ ${IPFS_CHECK_BASE_URL}`}
72+
title={`ipfs-check @ ${baseUrl}`}
7173
src={iframeSrc}
7274
aria-busy={isLoading}
7375
// Sandbox permissions:
@@ -80,4 +82,7 @@ const CheckScreen: React.FC<CheckScreenProps> = ({ cid }) => {
8082
)
8183
}
8284

83-
export default CheckScreen
85+
export default connect(
86+
'selectIpfsCheckUrl',
87+
CheckScreen
88+
)

src/settings/SettingsPage.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import AnalyticsToggle from '../components/analytics-toggle/AnalyticsToggle.js'
1818
import ApiAddressForm from '../components/api-address-form/api-address-form'
1919
import PublicGatewayForm from '../components/public-gateway-form/PublicGatewayForm.js'
2020
import PublicSubdomainGatewayForm from '../components/public-subdomain-gateway-form/PublicSubdomainGatewayForm.js'
21+
import IpfsCheckForm from '../components/ipfs-check-form/IpfsCheckForm.js'
2122
import { JsonEditor } from './editor/JsonEditor.js'
2223
import Experiments from '../components/experiments/ExperimentsPanel.js'
2324
import Title from './Title.js'
@@ -99,6 +100,17 @@ export const SettingsPage = ({
99100
<PinningManager t={t} />
100101
</Box>
101102

103+
<Box className='mb3 pa4-l pa2'>
104+
<Title>{t('retrievalDiagnosticService.title')}</Title>
105+
<p className='ma0 mr2 lh-copy charcoal f6'>
106+
<Trans i18nKey='retrievalDiagnosticService.description' t={t}>
107+
<a className='link blue' href='https://github.com/ipfs/ipfs-check' target='_blank' rel='noopener noreferrer'>ipfs-check</a>
108+
<a className='link blue' href='#/diagnostics/check'>retrieval diagnostics</a>
109+
</Trans>
110+
</p>
111+
<IpfsCheckForm/>
112+
</Box>
113+
102114
<Box className='mb3 pa4-l pa2'>
103115
<div className='joyride-settings-language'>
104116
<Title>{t('language')}</Title>

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"src/bundles/retry-init.js",
7070
"src/bundles/local-storage.js",
7171
"src/bundles/task.js",
72+
"src/bundles/gateway.js",
7273
"src/lib/count-dirs.js",
7374
"src/lib/sort.js",
7475
"src/lib/files.js",

0 commit comments

Comments
 (0)