Skip to content

Commit

Permalink
Embed customization & related fixes (#1795)
Browse files Browse the repository at this point in the history
* utils/pipeThru
* Data: more memoization
* Preview: refactor, partial glacier support, display helper
* partial glacier support
* Preview: fix json loader
* Preview/loaders: more error handling
* embed scoping
* embed: hideRootLink
* embed: padding override example
* handle consecutive slashes in s3 paths
* search: handle more syntax errors
  • Loading branch information
nl0 authored Sep 11, 2020
1 parent 60563d3 commit 0c8c700
Show file tree
Hide file tree
Showing 45 changed files with 1,178 additions and 1,070 deletions.
6 changes: 5 additions & 1 deletion catalog/app/components/BreadCrumbs/BreadCrumbs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import * as React from 'react'
import Link from 'utils/StyledLink'
import tagged from 'utils/tagged'

const EMPTY = <i>{'<EMPTY>'}</i>

export const Crumb = tagged([
'Segment', // { label, to }
'Sep', // value
])

export const Segment = ({ label, to }) => (to ? <Link to={to}>{label}</Link> : label)
export const Segment = ({ label, to }) =>
to ? <Link to={to}>{label || EMPTY}</Link> : label || EMPTY

export const render = (items) =>
items.map(
Expand All @@ -25,6 +28,7 @@ export function copyWithoutSpaces(e) {
document
.getSelection()
.toString()
.replace('<EMPTY>', '')
.replace(/\s*\/\s*/g, '/'),
)
e.preventDefault()
Expand Down
129 changes: 129 additions & 0 deletions catalog/app/components/Preview/Display.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as R from 'ramda'
import * as React from 'react'
import * as M from '@material-ui/core'

import * as AWS from 'utils/AWS'
import AsyncResult from 'utils/AsyncResult'
import * as Config from 'utils/Config'
import StyledLink from 'utils/StyledLink'
import pipeThru from 'utils/pipeThru'

import render from './render'
import { PreviewError } from './types'

const defaultProgress = () => <M.CircularProgress />

const defaultMessage = ({ heading, body, action }) => (
<>
{!!heading && (
<M.Typography variant="h6" gutterBottom>
{heading}
</M.Typography>
)}
{!!body && (
<M.Typography variant="body1" gutterBottom>
{body}
</M.Typography>
)}
{!!action && action}
</>
)

const defaultAction = ({ label, ...rest }) => (
<M.Button variant="outlined" {...rest}>
{label}
</M.Button>
)

export default function PreviewDisplay({
data,
noDownload,
renderContents = R.identity,
renderProgress = defaultProgress,
renderMessage = defaultMessage,
renderAction = defaultAction,
}) {
const cfg = Config.use()
const noDl = noDownload != null ? noDownload : cfg.noDownload
return pipeThru(data)(
AsyncResult.case({
_: renderProgress,
Ok: R.pipe(render, renderContents),
Err: PreviewError.case({
Deleted: () =>
renderMessage({
heading: 'Delete Marker',
body: (
<>
Selected version of the object is a{' '}
<StyledLink
href="https://docs.aws.amazon.com/AmazonS3/latest/dev/DeleteMarker.html"
target="_blank"
>
delete marker
</StyledLink>
</>
),
}),
Archived: () =>
renderMessage({
heading: 'Object Archived',
body: 'Preview not available',
}),
InvalidVersion: () =>
renderMessage({
heading: 'Invalid Version',
body: 'Invalid version id specified',
}),
Forbidden: () =>
renderMessage({
heading: 'Access Denied',
body: 'Preview not available',
}),
Gated: ({ load }) =>
renderMessage({
heading: 'Object is Too Large',
body: 'Large files are not previewed by default',
action: !!load && renderAction({ label: 'Load preview', onClick: load }),
}),
TooLarge: ({ handle }) =>
renderMessage({
heading: 'Object is Too Large',
body: 'Object is too large to preview',
action:
!noDl &&
AWS.Signer.withDownloadUrl(handle, (href) =>
renderAction({ label: 'Download and view in Browser', href }),
),
}),
Unsupported: ({ handle }) =>
renderMessage({
heading: 'Preview Not Available',
action:
!noDl &&
AWS.Signer.withDownloadUrl(handle, (href) =>
renderAction({ label: 'Download and view in Browser', href }),
),
}),
DoesNotExist: () =>
renderMessage({
heading: 'No Such Object',
body: 'Object does not exist',
}),
MalformedJson: ({ message }) =>
renderMessage({
heading: 'Malformed JSON',
body: message,
}),
Unexpected: ({ retry }) =>
renderMessage({
heading: 'Unexpected Error',
body: 'Something went wrong while loading preview',
action: !!retry && renderAction({ label: 'Retry', onClick: retry }),
}),
}),
}),
)
}

export const bind = (props) => (data) => <PreviewDisplay {...props} data={data} />
36 changes: 0 additions & 36 deletions catalog/app/components/Preview/Preview.js

This file was deleted.

5 changes: 4 additions & 1 deletion catalog/app/components/Preview/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './Preview'
export { default as Display, bind as display } from './Display'
export { default as render } from './render'
export { default as load } from './load'
export { PreviewData, PreviewError } from './types'
39 changes: 39 additions & 0 deletions catalog/app/components/Preview/load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react'

import * as Csv from './loaders/Csv'
import * as Excel from './loaders/Excel'
import * as Fcs from './loaders/Fcs'
import * as Html from './loaders/Html'
import * as Image from './loaders/Image'
import * as Json from './loaders/Json'
import * as Markdown from './loaders/Markdown'
import * as Notebook from './loaders/Notebook'
import * as Parquet from './loaders/Parquet'
import * as Pdf from './loaders/Pdf'
import * as Text from './loaders/Text'
import * as Vcf from './loaders/Vcf'
import * as fallback from './loaders/fallback'

const loaderChain = [
Csv,
Excel,
Fcs,
Json,
Markdown,
Notebook,
Parquet,
Pdf,
Vcf,
Html,
Text,
Image,
fallback,
]

export function Load({ handle, children }) {
const key = handle.logicalKey || handle.key
const { Loader } = React.useMemo(() => loaderChain.find((L) => L.detect(key)), [key])
return <Loader {...{ handle, children }} />
}

export default (handle, children) => <Load {...{ handle, children }} />
23 changes: 12 additions & 11 deletions catalog/app/components/Preview/loaders/Csv.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import * as R from 'ramda'

import AsyncResult from 'utils/AsyncResult'

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

export const detect = R.pipe(utils.stripCompression, utils.extIn(['.csv', '.tsv']))

const fetcher = utils.previewFetcher('csv', (json) =>
AsyncResult.Ok(
const isTsv = R.pipe(utils.stripCompression, utils.extIs('.tsv'))

export const Loader = function CsvLoader({ handle, children }) {
const data = utils.usePreview({
type: 'csv',
handle,
query: isTsv(handle.key) ? { sep: '\t' } : undefined,
})
const processed = utils.useProcessing(data.result, (json) =>
PreviewData.DataFrame({
preview: json.html,
note: json.info.note,
warnings: json.info.warnings,
}),
),
)

const isTsv = R.pipe(utils.stripCompression, utils.extIs('.tsv'))

export const load = (handle, callback) =>
fetcher(handle, callback, isTsv(handle.key) ? { query: { sep: '\t' } } : undefined)
)
return children(utils.useErrorHandling(processed, { handle, retry: data.fetch }))
}
12 changes: 6 additions & 6 deletions catalog/app/components/Preview/loaders/Excel.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import * as R from 'ramda'

import AsyncResult from 'utils/AsyncResult'

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

export const detect = R.pipe(utils.stripCompression, utils.extIn(['.xls', '.xlsx']))

export const load = utils.previewFetcher('excel', (json) =>
AsyncResult.Ok(
export const Loader = function ExcelLoader({ handle, children }) {
const data = utils.usePreview({ type: 'excel', handle })
const processed = utils.useProcessing(data.result, (json) =>
PreviewData.DataFrame({
preview: json.html,
note: json.info.note,
warnings: json.info.warnings,
}),
),
)
)
return children(utils.useErrorHandling(processed, { handle, retry: data.fetch }))
}
17 changes: 7 additions & 10 deletions catalog/app/components/Preview/loaders/Fcs.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import * as R from 'ramda'

import AsyncResult from 'utils/AsyncResult'

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

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

export const load = utils.previewFetcher(
'fcs',
R.pipe(
({ html, info }) => ({
export const Loader = function FcsLoader({ handle, children }) {
const data = utils.usePreview({ type: 'fcs', handle })
const processed = utils.useProcessing(data.result, ({ html, info }) =>
PreviewData.Fcs({
preview: html,
metadata: info.metadata,
note: info.note,
warnings: info.warnings,
}),
PreviewData.Fcs,
AsyncResult.Ok,
),
)
)
return children(utils.useErrorHandling(processed, { handle, retry: data.fetch }))
}
25 changes: 10 additions & 15 deletions catalog/app/components/Preview/loaders/Html.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react'
import * as AWS from 'utils/AWS'
import AsyncResult from 'utils/AsyncResult'
import { useIsInStack } from 'utils/BucketConfig'
import useMemoEq from 'utils/useMemoEq'

import { PreviewData } from '../types'

Expand All @@ -10,26 +11,20 @@ import * as utils from './utils'

export const detect = utils.extIn(['.htm', '.html'])

const IFrameLoader = ({ handle, children }) => {
function IFrameLoader({ handle, children }) {
const sign = AWS.Signer.useS3Signer()
const src = React.useMemo(() => sign(handle, { ResponseContentType: 'text/html' }), [
handle.bucket,
handle.key,
handle.version,
sign,
])
return children(AsyncResult.Ok(AsyncResult.Ok(PreviewData.IFrame({ src }))))
const src = useMemoEq([handle, sign], () =>
sign(handle, { ResponseContentType: 'text/html' }),
)
// TODO: issue a head request to ensure existence and get storage class
return children(AsyncResult.Ok(PreviewData.IFrame({ src })))
}

const HtmlLoader = ({ handle, children }) => {
export const Loader = function HtmlLoader({ handle, children }) {
const isInStack = useIsInStack()
return isInStack(handle.bucket) ? (
<IFrameLoader handle={handle}>{children}</IFrameLoader>
<IFrameLoader {...{ handle, children }} />
) : (
Text.load(handle, children)
<Text.Loader {...{ handle, children }} />
)
}

export const load = (handle, callback) => (
<HtmlLoader handle={handle}>{callback}</HtmlLoader>
)
6 changes: 4 additions & 2 deletions catalog/app/components/Preview/loaders/Image.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ import * as utils from './utils'

export const detect = utils.extIn(SUPPORTED_EXTENSIONS)

export const load = (handle, callback) =>
callback(AsyncResult.Ok(AsyncResult.Ok(PreviewData.Image({ handle }))))
// TODO: issue a head request to ensure existance and get storage class
export const Loader = function ImageLoader({ handle, children }) {
return children(AsyncResult.Ok(PreviewData.Image({ handle })))
}
Loading

0 comments on commit 0c8c700

Please sign in to comment.