Track loading and error states
EmbedProvider's overlays swap what a track draws while it loads or fails.
Given overlays, a stock display draws yours and still imports Material UI, so
Material UI stays in your bundle. To keep it out of the module graph, write a
display on DisplayChromeBase, which takes overlays as a prop and imports no
toolkit.
View source — 165 lines
import { useId, useState } from 'react'
import { EmbedProvider, Toolbar, TrackStack } from '@jbrowse/display-ui/embed'
import { useCreateViewState } from '@jbrowse/react-linear-genome-view2'
import { observer } from 'mobx-react'
import type { ErrorBarProps, LoadingProps } from '@jbrowse/display-ui'
import type { EmbedChrome } from '@jbrowse/display-ui/embed'
const pill: React.CSSProperties = {
pointerEvents: 'auto',
margin: 6,
padding: '4px 10px',
borderRadius: 999,
fontSize: '0.78rem',
color: 'CanvasText',
background: 'color-mix(in srgb, Canvas 92%, transparent)',
boxShadow: 'inset 0 0 0 1.5px #3a7ca5',
}
const Pill = observer(function Pill({
children,
action,
}: {
children: React.ReactNode
action?: { label: string; run: () => void }
}) {
return (
<div
style={{
position: 'absolute',
inset: 0,
zIndex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
pointerEvents: 'none',
}}
>
<div style={pill}>
{children}
{action ? (
<button
type="button"
style={{ font: 'inherit', marginLeft: 8, fontWeight: 600 }}
onClick={action.run}
>
{action.label}
</button>
) : null}
</div>
</div>
)
})
const MyErrorBar = observer(function MyErrorBar({
error,
onRetry,
}: ErrorBarProps) {
return (
<div role={onRetry ? 'alert' : 'status'}>
<Pill action={onRetry ? { label: 'Try again', run: onRetry } : undefined}>
{error instanceof Error ? error.message : String(error)}
</Pill>
</div>
)
})
const MyLoading = observer(function MyLoading({
visible,
message,
canceled,
onCancel,
onRetry,
}: LoadingProps) {
return !visible ? null : canceled ? (
<Pill action={onRetry ? { label: 'Resume', run: onRetry } : undefined}>
Stopped
</Pill>
) : (
<Pill action={onCancel ? { label: 'Stop', run: onCancel } : undefined}>
{message || 'Loading…'}
</Pill>
)
})
const myOverlays = { ErrorBar: MyErrorBar, Loading: MyLoading }
const sets = {
'overlays defined in this file': { overlays: myOverlays },
'the plain default': {},
'Material UI': { chrome: 'material' },
} satisfies Record<string, EmbedChrome>
type SetName = keyof typeof sets
const TrackLoadingAndErrors = observer(function TrackLoadingAndErrors() {
const radioGroupId = useId()
const [setName, setSetName] = useState<SetName>(
'overlays defined in this file',
)
const state = useCreateViewState({
assembly: {
name: 'hg38',
uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
refNameAliases: {
uri: 'https://jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
},
},
tracks: [
{
trackId: 'hg38_phylop',
name: 'phyloP 100-way conservation',
uri: 'https://jbrowse.org/demos/phylop/hg38.phyloP100way.brca1.bw',
displayDefaults: {
height: 100,
encoding: { color: { value: '#3a7ca5' } },
},
},
{
trackId: 'hg38_genes',
name: 'RefSeq curated genes',
uri: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz',
index: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz.csi',
displayDefaults: { height: 120 },
},
{
trackId: 'hg38_broken',
name: 'A track that fails to load',
uri: 'https://hgdownload.soe.ucsc.edu/goldenpath/hg38/phyloP100way/does-not-exist.bw',
displayDefaults: { height: 80 },
},
],
view: {
loc: 'chr17:43,044,295..43,125,364',
tracks: ['hg38_phylop', 'hg38_genes', 'hg38_broken'],
},
})
if (!state) {
return null
}
return (
<EmbedProvider session={state.session} {...sets[setName]}>
<Toolbar>
Draw loading and errors with
{Object.keys(sets).map(name => (
<label key={name}>
<input
type="radio"
name={radioGroupId}
checked={setName === name}
onChange={() => {
setSetName(name as SetName)
}}
/>
{name}
</label>
))}
</Toolbar>
<TrackStack view={state.session.view} />
</EmbedProvider>
)
})
export default TrackLoadingAndErrors