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

File view modes unit-test #2205

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
680b4b6
wip
fiskus Apr 9, 2021
3c0873c
Merge branch 'master' of github.com:quiltdata/quilt into voila-viewer
fiskus Apr 9, 2021
9ca33f9
Merge branch 'master' of github.com:quiltdata/quilt into voila-viewer
fiskus Apr 9, 2021
98db554
use dedicated component for Dropdown
fiskus Apr 12, 2021
30a95fe
use route based state
fiskus Apr 12, 2021
a3e6b04
Merge branch 'master' of github.com:quiltdata/quilt into voila-viewer
fiskus Apr 12, 2021
26d9487
create dummy VoilaLoader
fiskus Apr 12, 2021
3135695
progress state
fiskus Apr 12, 2021
191b392
Merge branch 'master' of github.com:quiltdata/quilt into voila-viewer
fiskus Apr 13, 2021
a17e23a
show Voila notebooks on package/file page
fiskus Apr 13, 2021
bd32bef
fix default mode
fiskus Apr 13, 2021
652c675
show Jupyter/Voila select only for notebooks
fiskus Apr 13, 2021
1438890
show Notebook as JSON too
fiskus Apr 14, 2021
ef040a3
Merge branch 'master' of github.com:quiltdata/quilt into voila-viewer
fiskus Apr 19, 2021
cf02ed5
make real request to Voila backend
fiskus Apr 20, 2021
6421924
unsubscribe from listeners, handle error
fiskus Apr 20, 2021
2bcc2ce
fix non-jupyter files view modes
fiskus Apr 20, 2021
043ba2f
leverage Data.use for caching
fiskus Apr 20, 2021
415064f
change registryUrl
fiskus Apr 20, 2021
c021833
disable sandbox
fiskus Apr 20, 2021
95cbe85
set allow same origin
fiskus Apr 20, 2021
26069f0
abort on timeout
fiskus Apr 21, 2021
cc5b7fb
add Changelog entry
fiskus Apr 22, 2021
21442ef
remove extra emptyline
fiskus Apr 22, 2021
e7e1e8f
polishing
fiskus Apr 22, 2021
3d0aadf
Apply suggestions from code review
fiskus Apr 23, 2021
3b37d15
polishing after code review
fiskus Apr 23, 2021
219bb42
merge with remote
fiskus Apr 23, 2021
aae4a41
memoize url
fiskus Apr 23, 2021
2cc78a7
use simple objects for modes
fiskus Apr 23, 2021
d9161ef
Merge branch 'master' of github.com:quiltdata/quilt into voila-viewer
fiskus Apr 26, 2021
cfcc3d1
useMemoEq instead of useMemo
fiskus Apr 26, 2021
8b83649
merge with master
fiskus Apr 29, 2021
9299e1d
Merge branch 'master' of github.com:quiltdata/quilt into voila-viewer
fiskus May 17, 2021
040efa3
Merge branch 'master' of github.com:quiltdata/quilt into voila-viewer
fiskus May 21, 2021
871d041
remove outdated comment
fiskus May 21, 2021
c2f52d0
ping for Voila service to ensure its deployed
fiskus May 21, 2021
4debfe1
fix voila support checker
fiskus May 21, 2021
d12b1db
fix global object
fiskus May 21, 2021
e8a47c5
wip
fiskus May 21, 2021
f86d7e5
install library for testing React hooks
fiskus May 21, 2021
408e154
test hook
fiskus May 21, 2021
afabc94
better structure
fiskus May 21, 2021
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
83 changes: 82 additions & 1 deletion catalog/app/components/Preview/loaders/Notebook.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import * as R from 'ramda'
import * as React from 'react'

import * as AWS from 'utils/AWS'
import * as Data from 'utils/Data'
import * as Config from 'utils/Config'
import mkSearch from 'utils/mkSearch'
import useMemoEq from 'utils/useMemoEq'

import { PreviewData } from '../types'
import * as utils from './utils'
import * as Json from './Json'

export const detect = R.pipe(utils.stripCompression, utils.extIs('.ipynb'))

export const Loader = function NotebookLoader({ handle, children }) {
function NotebookLoader({ handle, children }) {
const data = utils.usePreview({ type: 'ipynb', handle })
const processed = utils.useProcessing(data.result, (json) =>
PreviewData.Notebook({
Expand All @@ -16,3 +24,76 @@ export const Loader = function NotebookLoader({ handle, children }) {
)
return children(utils.useErrorHandling(processed, { handle, retry: data.fetch }))
}

const IFRAME_SANDBOX_ATTRIBUTES = 'allow-scripts allow-same-origin'
const IFRAME_LOAD_TIMEOUT = 30000

function waitForIframe(src) {
let resolved = false

return new Promise((resolve, reject) => {
const handleError = (error) => {
document.body.removeChild(link)
reject(error)
}

const handleSuccess = () => {
resolved = true
document.body.removeChild(link)
resolve(src)
}

const timerId = setTimeout(() => {
if (resolved) return
handleError(new Error('Page is taking too long to load'))
}, IFRAME_LOAD_TIMEOUT)

const link = document.createElement('iframe')
link.addEventListener('load', () => {
clearTimeout(timerId)
handleSuccess()
})
link.src = src
link.style.display = 'none'
link.sandbox = IFRAME_SANDBOX_ATTRIBUTES

document.body.appendChild(link)

const iframeDocument = link.contentWindow || link.contentDocument
if (iframeDocument) {
iframeDocument.addEventListener('error', handleError)
}
})
}

async function loadVoila({ src }) {
// Preload iframe, then insert cached iframe
await waitForIframe(src)
return PreviewData.IFrame({ src, sandbox: IFRAME_SANDBOX_ATTRIBUTES })
}

const useVoilaUrl = (handle) => {
const sign = AWS.Signer.useS3Signer()
const endpoint = Config.use().registryUrl
return useMemoEq(
[endpoint, handle, sign],
() => `${endpoint}/voila/voila/render/${mkSearch({ url: sign(handle) })}`,
)
}

function VoilaLoader({ handle, children }) {
const src = useVoilaUrl(handle)
const data = Data.use(loadVoila, { src })
return children(utils.useErrorHandling(data.result, { handle, retry: data.fetch }))
}

export const Loader = function WrappedNotebookLoader({ handle, children }) {
switch (handle.mode) {
case 'voila':
return <VoilaLoader {...{ handle, children }} />
case 'json':
return <Json.Loader {...{ handle, children }} />
default:
return <NotebookLoader {...{ handle, children }} />
}
}
95 changes: 95 additions & 0 deletions catalog/app/components/SelectDropdown/SelectDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as React from 'react'
import * as M from '@material-ui/core'

const useStyles = M.makeStyles((t) => ({
root: {
display: 'inline-block',
[t.breakpoints.down('sm')]: {
borderRadius: 0,
boxShadow: 'none',
},
},
button: {
...t.typography.body1,
border: 0,
textTransform: 'none',
},
label: {
fontWeight: 600,
marginLeft: t.spacing(1),
},
}))

interface ValueBase {
toString: () => string
valueOf: () => string | number | boolean
}

interface SelectDropdownProps<Value extends ValueBase> {
children: React.ReactNode
onChange: (selected: Value) => void
options: Value[]
value: Value
}

export default function SelectDropdown<Value extends ValueBase>({
children,
onChange,
options,
value,
}: SelectDropdownProps<Value>) {
const classes = useStyles()

const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)

const handleOpen = React.useCallback((event) => setAnchorEl(event.currentTarget), [])

const handleClose = React.useCallback(() => setAnchorEl(null), [])

const handleSelect = React.useCallback(
(selected: Value) => () => {
setAnchorEl(null)
onChange(selected)
},
[onChange],
)

const t = M.useTheme()
const aboveSm = M.useMediaQuery(t.breakpoints.up('sm'))

return (
<M.Paper className={classes.root}>
<M.Button
className={classes.button}
onClick={handleOpen}
size="small"
variant="outlined"
>
{children}
{aboveSm && (
<>
<span className={classes.label}>{value.toString()}</span>
<M.Icon fontSize="inherit">expand_more</M.Icon>
</>
)}
</M.Button>

<M.Menu
anchorEl={anchorEl}
open={!!anchorEl}
onClose={handleClose}
MenuListProps={{ dense: true }}
>
{options.map((item) => (
<M.MenuItem
key={item.toString()}
onClick={handleSelect(item)}
selected={value.valueOf() === item.valueOf()}
>
<M.ListItemText primary={item.toString()} />
</M.MenuItem>
))}
</M.Menu>
</M.Paper>
)
}
1 change: 1 addition & 0 deletions catalog/app/components/SelectDropdown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './SelectDropdown'
10 changes: 6 additions & 4 deletions catalog/app/constants/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export const bucketSearch = {
}
export const bucketFile = {
path: '/b/:bucket/tree/:path(.*[^/])',
url: (bucket, path, version) =>
`/b/${bucket}/tree/${encode(path)}${mkSearch({ version })}`,
url: (bucket, path, version, mode) =>
`/b/${bucket}/tree/${encode(path)}${mkSearch({ mode, version })}`,
}
export const bucketDir = {
path: '/b/:bucket/tree/:path(.+/)?',
Expand All @@ -119,9 +119,11 @@ export const bucketPackageDetail = {
}
export const bucketPackageTree = {
path: `/b/:bucket/packages/:name(${PACKAGE_PATTERN})/tree/:revision/:path(.*)?`,
url: (bucket, name, revision, path = '') =>
url: (bucket, name, revision, path = '', mode) =>
path || (revision && revision !== 'latest')
? `/b/${bucket}/packages/${name}/tree/${revision || 'latest'}/${encode(path)}`
? `/b/${bucket}/packages/${name}/tree/${revision || 'latest'}/${encode(
path,
)}${mkSearch({ mode })}`
: bucketPackageDetail.url(bucket, name),
}
export const bucketPackageRevisions = {
Expand Down
63 changes: 45 additions & 18 deletions catalog/app/containers/Bucket/File.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import dedent from 'dedent'
import * as R from 'ramda'
import * as React from 'react'
import { FormattedRelative } from 'react-intl'
import { Link } from 'react-router-dom'
import { Link, useHistory } from 'react-router-dom'
import * as M from '@material-ui/core'

import { Crumb, copyWithoutSpaces, render as renderCrumbs } from 'components/BreadCrumbs'
Expand All @@ -31,6 +31,7 @@ import * as FileView from './FileView'
import Section from './Section'
import renderPreview from './renderPreview'
import * as requests from './requests'
import useViewModes from './viewModes'

const getCrumbs = ({ bucket, path, urls }) =>
R.chain(
Expand Down Expand Up @@ -289,6 +290,15 @@ function CenteredProgress() {
}

const useStyles = M.makeStyles((t) => ({
actions: {
marginLeft: 'auto',
},
at: {
color: t.palette.text.secondary,
},
button: {
marginLeft: t.spacing(2),
},
crumbs: {
...t.typography.body1,
maxWidth: '100%',
Expand All @@ -307,17 +317,6 @@ const useStyles = M.makeStyles((t) => ({
display: 'flex',
marginBottom: t.spacing(2),
},
at: {
color: t.palette.text.secondary,
},
spacer: {
flexGrow: 1,
},
button: {
flexShrink: 0,
marginBottom: -3,
marginTop: -3,
},
}))

export default function File({
Expand All @@ -326,9 +325,10 @@ export default function File({
},
location,
}) {
const { version } = parseSearch(location.search)
const { version, mode: viewModeSlug } = parseSearch(location.search)
const classes = useStyles()
const { urls } = NamedRoutes.use()
const history = useHistory()
const { analyticsBucket, noDownload } = Config.use()
const s3 = AWS.S3.use()

Expand Down Expand Up @@ -384,7 +384,7 @@ export default function File({

const handle = { bucket, key: path, version }

const withPreview = (callback) =>
const withPreview = (callback, mode) =>
requests.ObjectExistence.case({
Exists: (h) => {
if (h.deleted) {
Expand All @@ -393,12 +393,27 @@ export default function File({
if (h.archived) {
return callback(AsyncResult.Err(Preview.PreviewError.Archived({ handle })))
}
return Preview.load(handle, callback)
return mode
? Preview.load(R.assoc('mode', mode.key, handle), callback)
: Preview.load(handle, callback)
},
DoesNotExist: () =>
callback(AsyncResult.Err(Preview.PreviewError.InvalidVersion({ handle }))),
})

const { registryUrl } = Config.use()
const viewModes = useViewModes(registryUrl, path)
const viewMode = React.useMemo(
() => viewModes.find(({ key }) => key === viewModeSlug) || viewModes[0] || null,
[viewModes, viewModeSlug],
)
const onViewModeChange = React.useCallback(
(mode) => {
history.push(urls.bucketFile(bucket, encodedPath, version, mode.key))
},
[history, urls, bucket, encodedPath, version],
)

return (
<FileView.Root>
<MetaTitle>{[path || 'Files', bucket]}</MetaTitle>
Expand All @@ -420,8 +435,20 @@ export default function File({
'latest'
)}
</div>
<div className={classes.spacer} />
{downloadable && <FileView.DownloadButton handle={handle} />}

<div className={classes.actions}>
{!!viewModes.length && (
<FileView.ViewWithVoilaButtonLayout
className={classes.button}
modesList={viewModes}
mode={viewMode}
onChange={onViewModeChange}
/>
)}
{downloadable && (
<FileView.DownloadButton className={classes.button} handle={handle} />
)}
</div>
</div>
{objExistsData.case({
_: () => <CenteredProgress />,
Expand All @@ -448,7 +475,7 @@ export default function File({
Err: (e) => {
throw e
},
Ok: withPreview(renderPreview),
Ok: withPreview(renderPreview, viewMode),
})}
</Section>
</>
Expand Down
Loading