Skip to content

Commit

Permalink
uri resolution ui/ux
Browse files Browse the repository at this point in the history
  • Loading branch information
nl0 committed Nov 30, 2020
1 parent f6ec099 commit 254e933
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 9 deletions.
10 changes: 8 additions & 2 deletions catalog/app/constants/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export const search = {
`/search${mkSearch({ q, buckets, p, mode, retry })}`,
}

// immutable URI resolver
export const uriResolver = {
path: '/uri/:uri(.*)',
url: (uri) => `/uri/${uri ? encodeURIComponent(uri) : ''}`,
}

// bucket
export const bucketRoot = {
path: '/b/:bucket',
Expand Down Expand Up @@ -114,9 +120,9 @@ export const bucketPackageDetail = {
export const bucketPackageTree = {
path: `/b/:bucket/packages/:name(${PACKAGE_PATTERN})/tree/:revision/:path(.*)?`,
url: (bucket, name, revision, path = '') =>
revision === 'latest' && !path
(!revision || revision === 'latest') && !path
? bucketPackageDetail.url(bucket, name)
: `/b/${bucket}/packages/${name}/tree/${revision}/${encode(path)}`,
: `/b/${bucket}/packages/${name}/tree/${revision || 'latest'}/${encode(path)}`,
}
export const bucketPackageRevisions = {
path: `/b/:bucket/packages/:name(${PACKAGE_PATTERN})/revisions`,
Expand Down
3 changes: 2 additions & 1 deletion catalog/app/constants/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const appPalette = {
main: colors.orange[600],
},
info: {
main: colors.lightBlue[50],
main: colors.lightBlue[600],
light: colors.lightBlue[50],
},
warning: {
main: colors.yellow[200],
Expand Down
5 changes: 5 additions & 0 deletions catalog/app/containers/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const AuthSignUp = mkLazy(() => import('containers/Auth/SignUp'))
const AuthSSOSignUp = mkLazy(() => import('containers/Auth/SSOSignUp'))
const Bucket = mkLazy(() => import('containers/Bucket'))
const Search = mkLazy(() => import('containers/Search'))
const UriResolver = mkLazy(() => import('containers/UriResolver'))

const Landing = mkLazy(() => import('website/pages/Landing'))
const OpenLanding = mkLazy(() => import('website/pages/OpenLanding'))
Expand Down Expand Up @@ -136,6 +137,10 @@ export default function App() {
<Route path={paths.admin} component={requireAdmin(Admin)} />
)}

{!cfg.disableNavigator && (
<Route path={paths.uriResolver} component={protect(UriResolver)} />
)}

{!cfg.disableNavigator && (
<Route path={paths.bucketRoot} component={protect(Bucket)} />
)}
Expand Down
30 changes: 30 additions & 0 deletions catalog/app/containers/Bucket/PackageTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as R from 'ramda'
import * as React from 'react'
import { Link as RRLink } from 'react-router-dom'
import * as M from '@material-ui/core'
import * as Lab from '@material-ui/lab'

import { Crumb, copyWithoutSpaces, render as renderCrumbs } from 'components/BreadCrumbs'
import * as Intercom from 'components/Intercom'
Expand All @@ -16,7 +17,9 @@ import * as Config from 'utils/Config'
import Data, { useData } from 'utils/Data'
import * as LinkedData from 'utils/LinkedData'
import * as NamedRoutes from 'utils/NamedRoutes'
import * as PackageUri from 'utils/PackageUri'
import Link, { linkStyle } from 'utils/StyledLink'
import parseSearch from 'utils/parseSearch'
import * as s3paths from 'utils/s3paths'
import usePrevious from 'utils/usePrevious'

Expand Down Expand Up @@ -206,6 +209,10 @@ function PkgCode({ bucket, name, hash, revision, path }) {
quilt3 install ${nameWithPath}${hashCli} --registry s3://${bucket} --dest .
`,
},
{
label: 'URI',
contents: PackageUri.stringify({ bucket, name, hash, path }),
},
]
return <Code>{code}</Code>
}
Expand Down Expand Up @@ -490,12 +497,18 @@ const useStyles = M.makeStyles(() => ({
name: {
wordBreak: 'break-all',
},
alertMsg: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
}))

export default function PackageTree({
match: {
params: { bucket, name, revision = 'latest', path: encodedPath = '' },
},
location,
}) {
const classes = useStyles()
const s3 = AWS.S3.use()
Expand All @@ -505,6 +518,8 @@ export default function PackageTree({
const path = s3paths.decode(encodedPath)
const isDir = s3paths.isDir(path)

const { resolvedFrom } = parseSearch(location.search)

const crumbs = React.useMemo(() => {
const segments = [{ label: 'ROOT', path: '' }, ...s3paths.getBreadCrumbs(path)]
return R.intersperse(
Expand Down Expand Up @@ -556,6 +571,21 @@ export default function PackageTree({
),
_: () => null,
})}
{!!resolvedFrom && (
<M.Box mb={2}>
<Lab.Alert severity="info" icon={false} classes={{ message: classes.alertMsg }}>
Resolved from{' '}
<M.Box
fontFamily="monospace.fontFamily"
fontWeight="fontWeightBold"
component="span"
title={resolvedFrom}
>
{resolvedFrom}
</M.Box>
</Lab.Alert>
</M.Box>
)}
<M.Typography variant="body1">
<Link to={urls.bucketPackageDetail(bucket, name)} className={classes.name}>
{name}
Expand Down
1 change: 1 addition & 0 deletions catalog/app/containers/NavBar/NavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ function useLinks() {
const { urls } = NamedRoutes.use()
const cfg = Config.useConfig()
return [
cfg.mode !== 'MARKETING' && { to: urls.uriResolver(), label: 'URI' },
{ href: URLS.docs, label: 'Docs' },
cfg.mode === 'MARKETING' && { to: `${urls.home()}#pricing`, label: 'Pricing' },
(cfg.mode === 'MARKETING' || cfg.mode === 'OPEN') && {
Expand Down
139 changes: 139 additions & 0 deletions catalog/app/containers/UriResolver/UriResolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import * as React from 'react'
import { useHistory, Link, Redirect } from 'react-router-dom'
import * as M from '@material-ui/core'

import Layout from 'components/Layout'
import * as NamedRoutes from 'utils/NamedRoutes'
import * as PackageUri from 'utils/PackageUri'

const useStyles = M.makeStyles((t) => ({
form: {
display: 'flex',
marginTop: t.spacing(3),
},
btn: {
marginLeft: t.spacing(2),
},
linkBox: {
marginTop: t.spacing(4),
marginLeft: 'auto',
marginRight: 'auto',
},
link: {
display: 'block',
padding: t.spacing(1.5, 2),
'&:hover': {
background: t.palette.action.hover,
},
},
field: {
...t.typography.body1,
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
name: {
fontWeight: t.typography.fontWeightMedium,
},
}))

export default function UriResolver({ match }) {
const { urls } = NamedRoutes.use()
const history = useHistory()
const classes = useStyles()

const uri = decodeURIComponent(match.params.uri || '')
const [parsed, error] = React.useMemo(() => {
try {
return [uri ? PackageUri.parse(uri) : null]
} catch (e) {
return [null, e]
}
}, [uri])

const [value, setValue] = React.useState(uri)

const handleChange = React.useCallback(
(e) => {
setValue(e.target.value)
},
[setValue],
)

const handleSubmit = React.useCallback(
(e) => {
e.preventDefault()
if (value !== uri) history.push(urls.uriResolver(value))
},
[value, uri, history, urls],
)

const to =
parsed &&
urls.bucketPackageTree(
parsed.bucket,
parsed.name,
parsed.hash || parsed.tag,
parsed.path,
) + NamedRoutes.mkSearch({ resolvedFrom: uri })

// automatically redirect if URI is pre-filled with a correct value
const [redirect] = React.useState(!!to)
if (redirect) return <Redirect to={to} />

const field = (name, val) => (
<span className={classes.field}>
<span className={classes.name}>{name}:</span> {val}
</span>
)

return (
<Layout
pre={
<M.Box pt={6} pb={4} maxWidth={'656px !important'} component={M.Container}>
<M.Typography variant="h4" align="center">
Resolve a Quilt package URI
</M.Typography>

<form className={classes.form} onSubmit={handleSubmit}>
<M.Input
value={value}
onChange={handleChange}
error={!!error}
placeholder="Enter a URI, e.g. quilt+s3://your-bucket#package=user/package@hash"
fullWidth
/>
<M.Button
className={classes.btn}
type="submit"
variant="contained"
color="primary"
>
Resolve
</M.Button>
</form>

{!!error && (
<M.Box mt={2}>
<M.Typography color="error">
Error parsing URI: {error.msg || `${error}`}
</M.Typography>
</M.Box>
)}
{!!parsed && (
<M.Paper className={classes.linkBox}>
<Link to={to} className={classes.link}>
{field('Registry', `s3://${parsed.bucket}`)}
{field('Package', parsed.name)}
{!!parsed.hash && field('Hash', parsed.hash)}
{!!parsed.tag && field('Tag', parsed.tag)}
{!!parsed.path && field('Path', parsed.path)}
</Link>
</M.Paper>
)}
</M.Box>
}
/>
)
}
1 change: 1 addition & 0 deletions catalog/app/containers/UriResolver/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './UriResolver'
14 changes: 10 additions & 4 deletions catalog/app/utils/PackageUri.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class PackageUriError extends BaseError {
static displayName = 'PackageUriError'

constructor(msg, uri) {
super(`Invalid package URI (${uri}): ${msg}`, { uri })
super(`Invalid package URI (${uri}): ${msg}`, { msg, uri })
}
}

Expand Down Expand Up @@ -63,13 +63,19 @@ export function parse(uri) {
if (!url.slashes) {
throw new PackageUriError('missing slashes between protocol and registry.', uri)
}
const registry = `s3://${url.host}${url.path || ''}`
if (url.path) {
throw new PackageUriError(
'non-bucket-root registries are not supported currently.',
uri,
)
}
const bucket = url.host
const params = parseQs(url.hash.replace('#', ''))
if (!params.package) {
throw new PackageUriError('missing "package=" part.', uri)
}
const { name, hash, tag } = parsePackageSpec(params.package, uri)
return R.reject(R.isNil, { registry, name, hash, tag, path: params.path || null })
return R.reject(R.isNil, { bucket, name, hash, tag, path: params.path || null })
}

export function stringify(parsed) {
Expand All @@ -80,5 +86,5 @@ export function stringify(parsed) {
} else if (parsed.tag) {
pkgSpec += `:${parsed.tag}`
}
return `quilt+${parsed.registry}#package=${pkgSpec}${pathPart}`
return `quilt+s3://${parsed.bucket}#package=${pkgSpec}${pathPart}`
}
4 changes: 2 additions & 2 deletions catalog/app/utils/PackageUri.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('utils/PackageUri', () => {
'quilt+s3://bucket-name#package=quilt/test:latest&path=sub/path',
),
).toEqual({
registry: 's3://bucket-name',
bucket: 's3://bucket-name',
name: 'quilt/test',
tag: 'latest',
path: 'sub/path',
Expand All @@ -21,7 +21,7 @@ describe('utils/PackageUri', () => {
it('should work', () => {
expect(
PackageUri.stringify({
registry: 's3://bucket-name',
bucket: 'bucket-name',
name: 'quilt/test',
tag: 'latest',
path: 'sub/path',
Expand Down

0 comments on commit 254e933

Please sign in to comment.