-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create config page for sw settings (#24)
* feat: create config page for sw settings * Update src/lib/channel.ts * Update src/sw.ts * Update src/lib/channel.ts * Update src/index.tsx * chore: fix build * chore: change gear color * feat: service worker config is shared to subdomains * fix: test running * fix: service worker registration * chore: remove calls to removed to commsChannel methods * chore: use LOCAL_STORAGE_KEYS * feat: config page auto reload works * chore: import react functions directly * chore: use latest verified-fetch * chore: remove console.logs and cleanup * chore: consolidate app logic * feat: users can control debugging output * chore: todo determinism * fix: gateway & routers default value * fix: bug parsing ipfs namespaced subdomains * chore: comment * fix: use configured gateways & routers prior to defaults * feat: config collapsed, reload button, sw-ready-btn * chore: remove unused in config.tsx
- Loading branch information
Showing
29 changed files
with
977 additions
and
569 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,3 +41,7 @@ form { | |
flex: 1; | ||
word-break: break-word; | ||
} | ||
|
||
.cursor-disabled { | ||
cursor: not-allowed; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,21 @@ | ||
import React from 'react' | ||
import React, { useContext } from 'react' | ||
import { ConfigContext } from '../context/config-context.tsx' | ||
import gearIcon from '../gear-icon.svg' | ||
import ipfsLogo from '../ipfs-logo.svg' | ||
|
||
export default function Header (): JSX.Element { | ||
const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext) | ||
|
||
return ( | ||
<header className='flex items-center pa3 bg-navy bb bw3 b--aqua'> | ||
<a href='https://ipfs.io' title='home'> | ||
<img alt='IPFS logo' src={ipfsLogo} style={{ height: 50 }} className='v-top' /> | ||
</a> | ||
|
||
<button onClick={() => { setConfigExpanded(!isConfigExpanded) }} style={{ border: 'none', position: 'absolute', top: '0.5rem', right: '0.5rem', background: 'none', cursor: 'pointer' }}> | ||
{/* https://isotropic.co/tool/hex-color-to-css-filter/ to #ffffff */} | ||
<img alt='Config gear icon' src={gearIcon} style={{ height: 50, filter: 'invert(100%) sepia(100%) saturate(0%) hue-rotate(275deg) brightness(103%) contrast(103%)' }} className='v-top' /> | ||
</button> | ||
</header> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import React, { useState } from 'react' | ||
|
||
export interface CollapsibleProps { | ||
children: React.ReactNode | ||
collapsedLabel: string | ||
expandedLabel: string | ||
collapsed: boolean | ||
} | ||
|
||
export function Collapsible ({ children, collapsedLabel, expandedLabel, collapsed }: CollapsibleProps): JSX.Element { | ||
const [cId] = useState(Math.random().toString(36).substring(7)) | ||
const [isCollapsed, setCollapsed] = useState(collapsed) | ||
|
||
return ( | ||
<React.Fragment> | ||
<input type="checkbox" className="dn" name="collapsible" id={`collapsible-${cId}`} onClick={() => { setCollapsed(!isCollapsed) }} /> | ||
<label htmlFor={`collapsible-${cId}`} className="collapsible__item-label db pv3 link black hover-blue pointer blue">{isCollapsed ? collapsedLabel : expandedLabel}</label> | ||
<div className={`bb b--black-20 ${isCollapsed ? 'dn' : ''}`}> | ||
{children} | ||
</div> | ||
</React.Fragment> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import React, { useCallback, useContext, useEffect, useState } from 'react' | ||
import { ConfigContext } from '../context/config-context.tsx' | ||
import { HeliaServiceWorkerCommsChannel } from '../lib/channel.ts' | ||
import { getConfig, loadConfigFromLocalStorage } from '../lib/config-db.ts' | ||
import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.ts' | ||
import { Collapsible } from './collapsible' | ||
import LocalStorageInput from './local-storage-input.tsx' | ||
import { LocalStorageToggle } from './local-storage-toggle' | ||
import { ServiceWorkerReadyButton } from './sw-ready-button.tsx' | ||
|
||
const channel = new HeliaServiceWorkerCommsChannel('WINDOW') | ||
|
||
const urlValidationFn = (value: string): Error | null => { | ||
try { | ||
const urls = JSON.parse(value) satisfies string[] | ||
let i = 0 | ||
try { | ||
urls.map((url, index) => { | ||
i = index | ||
return new URL(url) | ||
}) | ||
} catch (e) { | ||
throw new Error(`URL "${urls[i]}" at index ${i} is not valid`) | ||
} | ||
return null | ||
} catch (err) { | ||
return err as Error | ||
} | ||
} | ||
|
||
const stringValidationFn = (value: string): Error | null => { | ||
// we accept any string | ||
return null | ||
} | ||
|
||
export default (): JSX.Element | null => { | ||
const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext) | ||
const [error, setError] = useState<Error | null>(null) | ||
|
||
const isLoadedInIframe = window.self !== window.top | ||
|
||
const postFromIframeToParentSw = useCallback(async () => { | ||
if (!isLoadedInIframe) { | ||
return | ||
} | ||
// we get the iframe origin from a query parameter called 'origin', if this is loaded in an iframe | ||
const targetOrigin = decodeURIComponent(window.location.search.split('origin=')[1]) | ||
const config = await getConfig() | ||
|
||
/** | ||
* The reload page in the parent window is listening for this message, and then it passes a RELOAD_CONFIG message to the service worker | ||
*/ | ||
window.parent?.postMessage({ source: 'helia-sw-config-iframe', target: 'PARENT', action: 'RELOAD_CONFIG', config }, { | ||
targetOrigin | ||
}) | ||
}, []) | ||
|
||
useEffect(() => { | ||
/** | ||
* On initial load, we want to send the config to the parent window, so that the reload page can auto-reload if enabled, and the subdomain registered service worker gets the latest config without user interaction. | ||
*/ | ||
void postFromIframeToParentSw() | ||
}, []) | ||
|
||
const saveConfig = useCallback(async () => { | ||
try { | ||
await loadConfigFromLocalStorage() | ||
// update the BASE_URL service worker | ||
// TODO: use channel.messageAndWaitForResponse to ensure that the config is loaded before proceeding. | ||
channel.postMessage({ target: 'SW', action: 'RELOAD_CONFIG' }) | ||
// update the <subdomain>.<namespace>.BASE_URL service worker | ||
await postFromIframeToParentSw() | ||
setConfigExpanded(false) | ||
} catch (err) { | ||
setError(err as Error) | ||
} | ||
}, []) | ||
|
||
if (!isConfigExpanded) { | ||
return null | ||
} | ||
|
||
return ( | ||
<main className='pa4-l bg-snow mw7 center pa4'> | ||
<Collapsible collapsedLabel="View config" expandedLabel='Hide config' collapsed={true}> | ||
<LocalStorageInput localStorageKey={LOCAL_STORAGE_KEYS.config.gateways} label='Gateways' validationFn={urlValidationFn} defaultValue='[]' /> | ||
<LocalStorageInput localStorageKey={LOCAL_STORAGE_KEYS.config.routers} label='Routers' validationFn={urlValidationFn} defaultValue='[]'/> | ||
<LocalStorageToggle localStorageKey={LOCAL_STORAGE_KEYS.config.autoReload} onLabel='Auto Reload' offLabel='Show Config' /> | ||
<LocalStorageInput localStorageKey={LOCAL_STORAGE_KEYS.config.debug} label='Debug logging' validationFn={stringValidationFn} defaultValue=''/> | ||
<ServiceWorkerReadyButton id="save-config" label='Save Config' waitingLabel='Waiting for service worker registration...' onClick={() => { void saveConfig() }} /> | ||
|
||
{error != null && <span style={{ color: 'red' }}>{error.message}</span>} | ||
</Collapsible> | ||
</main> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React, { useEffect, useState } from 'react' | ||
|
||
export interface LocalStorageInputProps { | ||
localStorageKey: string | ||
label: string | ||
placeholder?: string | ||
defaultValue: string | ||
validationFn?(value: string): Error | null | ||
} | ||
|
||
const defaultValidationFunction = (value: string): Error | null => { | ||
try { | ||
JSON.parse(value) | ||
return null | ||
} catch (err) { | ||
return err as Error | ||
} | ||
} | ||
export default ({ localStorageKey, label, placeholder, validationFn, defaultValue }: LocalStorageInputProps): JSX.Element => { | ||
const [value, setValue] = useState(localStorage.getItem(localStorageKey) ?? defaultValue) | ||
const [error, setError] = useState<null | Error>(null) | ||
|
||
if (validationFn == null) { | ||
validationFn = defaultValidationFunction | ||
} | ||
|
||
useEffect(() => { | ||
try { | ||
const err = validationFn?.(value) | ||
if (err != null) { | ||
throw err | ||
} | ||
localStorage.setItem(localStorageKey, value) | ||
setError(null) | ||
} catch (err) { | ||
setError(err as Error) | ||
} | ||
}, [value]) | ||
|
||
return ( | ||
<> | ||
<label htmlFor={localStorageKey} className='f5 ma0 pb2 aqua fw4 db'>{label}:</label> | ||
<input | ||
className='input-reset bn black-80 bg-white pa3 w-100 mb3' | ||
id={localStorageKey} | ||
name={localStorageKey} | ||
type='text' | ||
placeholder={placeholder} | ||
value={value} | ||
onChange={(e) => { setValue(e.target.value) }} | ||
/> | ||
{error != null && <span style={{ color: 'red' }}>{error.message}</span>} | ||
</> | ||
) | ||
} |
Oops, something went wrong.