diff --git a/catalog/app/containers/Bucket/Code.js b/catalog/app/containers/Bucket/CodeSamples/Code.tsx similarity index 59% rename from catalog/app/containers/Bucket/Code.js rename to catalog/app/containers/Bucket/CodeSamples/Code.tsx index f27e888ae25..973772b1b34 100644 --- a/catalog/app/containers/Bucket/Code.js +++ b/catalog/app/containers/Bucket/CodeSamples/Code.tsx @@ -5,11 +5,12 @@ import * as M from '@material-ui/core' import * as Lab from '@material-ui/lab' import * as Notifications from 'containers/Notifications' +import StyledLink from 'utils/StyledLink' import copyToClipboard from 'utils/clipboard' -import Section from './Section' +import Section, { SectionProps } from '../Section' -function highlight(lang, str) { +function highlight(str: string, lang?: string) { if (lang && hljs.getLanguage(lang)) { try { const { value } = hljs.highlight(str, { language: lang }) @@ -23,6 +24,46 @@ function highlight(lang, str) { return str } +const useLineOfCodeStyles = M.makeStyles((t) => ({ + root: { + fontFamily: t.typography.monospace.fontFamily, + fontSize: t.typography.body2.fontSize, + overflowX: 'auto', + overflowY: 'hidden', + whiteSpace: 'pre', + minHeight: t.typography.body2.fontSize, + display: 'flex', + alignItems: 'flex-end', + '&:hover $help': { + opacity: 1, + }, + }, + help: { + display: 'inline-flex', + marginLeft: t.spacing(0.5), + opacity: 0.3, + }, +})) + +interface LineOfCodeProps { + lang?: string + text: string + help?: string +} +function LineOfCode({ lang, text, help }: LineOfCodeProps) { + const classes = useLineOfCodeStyles() + return ( +
+ {highlight(text, lang)} + {help && ( + + [?] + + )} +
+ ) +} + const useStyles = M.makeStyles((t) => ({ container: { alignItems: 'center', @@ -35,16 +76,17 @@ const useStyles = M.makeStyles((t) => ({ height: 32, }, code: { - fontFamily: t.typography.monospace.fontFamily, - fontSize: t.typography.body2.fontSize, - overflowX: 'auto', - overflowY: 'hidden', - whiteSpace: 'pre', + width: '100%', }, })) +interface CodeProps extends Partial { + children: { label: string; contents: string; hl?: string }[] + defaultSelected?: number +} + // children: [{ label: str, contents: str, hl: lang }] -export default function Code({ defaultSelected = 0, children, ...props }) { +export default function Code({ defaultSelected = 0, children, ...props }: CodeProps) { const classes = useStyles() const { push } = Notifications.use() @@ -69,6 +111,27 @@ export default function Code({ defaultSelected = 0, children, ...props }) { [selected.contents, push], ) + const lines = React.useMemo( + () => + selected.contents.split('\n').map((line, index) => { + // Find [[ URL ]] and put it to help prop + const matched = line.match(/(.*) \[\[(.*)\]\]/) + const key = selected.label + index + if (!matched || !matched[1] || !matched[2]) { + return { + key, + text: line, + } + } + return { + help: matched[2], + key, + text: matched[1], + } + }), + [selected.contents, selected.label], + ) + return (
-
{highlight(selected.hl, selected.contents)}
+
+ {lines.map((line) => ( + + ))} +
) } diff --git a/catalog/app/containers/Bucket/CodeSamples/Dir.tsx b/catalog/app/containers/Bucket/CodeSamples/Dir.tsx new file mode 100644 index 00000000000..bfd77c042be --- /dev/null +++ b/catalog/app/containers/Bucket/CodeSamples/Dir.tsx @@ -0,0 +1,54 @@ +import { basename } from 'path' + +import dedent from 'dedent' +import * as React from 'react' + +import { docs } from 'constants/urls' + +import type { SectionProps } from '../Section' + +import Code from './Code' + +const TEMPLATES = { + PY: (bucket: string, path: string, dest: string) => + dedent` + import quilt3 as q3 + b = q3.Bucket("s3://${bucket}") + # List files [[${docs}/api-reference/bucket#bucket.ls]] + b.ls("${path}") + # Download [[${docs}/api-reference/bucket#bucket.fetch]] + b.fetch("${path}", "./${dest}") + `, + CLI: (bucket: string, path: string, dest: string) => + dedent` + # List files [[https://docs.aws.amazon.com/cli/latest/reference/s3/ls.html]] + aws s3 ls "s3://${bucket}/${path}" + # Download [[https://docs.aws.amazon.com/cli/latest/reference/s3/cp.html]] + aws s3 cp --recursive "s3://${bucket}/${path}" "./${dest}" + `, +} + +interface DirCodeSamplesProps extends Partial { + bucket: string + path: string +} + +export default function DirCodeSamples({ bucket, path, ...props }: DirCodeSamplesProps) { + const dest = path ? basename(path) : bucket + const code = React.useMemo( + () => [ + { + label: 'Python', + hl: 'python', + contents: TEMPLATES.PY(bucket, path, dest), + }, + { + label: 'CLI', + hl: 'bash', + contents: TEMPLATES.CLI(bucket, path, dest), + }, + ], + [bucket, path, dest], + ) + return {code} +} diff --git a/catalog/app/containers/Bucket/CodeSamples/File.tsx b/catalog/app/containers/Bucket/CodeSamples/File.tsx new file mode 100644 index 00000000000..f1eea186809 --- /dev/null +++ b/catalog/app/containers/Bucket/CodeSamples/File.tsx @@ -0,0 +1,53 @@ +import { basename } from 'path' + +import dedent from 'dedent' +import * as React from 'react' + +import { docs } from 'constants/urls' + +import type { SectionProps } from '../Section' + +import Code from './Code' + +const TEMPLATES = { + PY: (bucket: string, path: string) => + dedent` + import quilt3 as q3 + b = q3.Bucket("s3://${bucket}") + # Download [[${docs}/api-reference/bucket#bucket.fetch]] + b.fetch("${path}", "./${basename(path)}") + `, + CLI: (bucket: string, path: string) => + dedent` + # Download [[https://docs.aws.amazon.com/cli/latest/reference/s3/cp.html]] + aws s3 cp "s3://${bucket}/${path}" . + `, +} + +interface FileCodeSamplesProps extends Partial { + bucket: string + path: string +} + +export default function FileCodeSamples({ + bucket, + path, + ...props +}: FileCodeSamplesProps) { + const code = React.useMemo( + () => [ + { + label: 'Python', + hl: 'python', + contents: TEMPLATES.PY(bucket, path), + }, + { + label: 'CLI', + hl: 'bash', + contents: TEMPLATES.CLI(bucket, path), + }, + ], + [bucket, path], + ) + return {code} +} diff --git a/catalog/app/containers/Bucket/CodeSamples/Package.tsx b/catalog/app/containers/Bucket/CodeSamples/Package.tsx new file mode 100644 index 00000000000..6a074666f0c --- /dev/null +++ b/catalog/app/containers/Bucket/CodeSamples/Package.tsx @@ -0,0 +1,91 @@ +import dedent from 'dedent' + +import * as R from 'ramda' +import * as React from 'react' + +import { docs } from 'constants/urls' +import * as PackageUri from 'utils/PackageUri' +import * as s3paths from 'utils/s3paths' + +import type { SectionProps } from '../Section' + +import Code from './Code' + +const TEMPLATES = { + PY: (bucket: string, name: string, path: string, hashDisplay: string) => { + const pathPy = path && `, path="${s3paths.ensureNoSlash(path)}"` + const hashPy = hashDisplay && `, top_hash="${hashDisplay}"` + return dedent` + import quilt3 as q3 + # Browse [[${docs}/api-reference/package#package.browse]] + p = q3.Package.browse("${name}"${hashPy}, registry="s3://${bucket}") + # make changes to package adding individual files [[${docs}/api-reference/package#package.set]] + p.set("data.csv", "data.csv") + # or whole directories [[${docs}/api-reference/package#package.set_dir]] + p.set_dir("subdir", "subdir") + # and push changes [[${docs}/api-reference/package#package.push]] + p.push("${name}", registry="s3://${bucket}", message="Hello World") + + # Download (be mindful of large packages) [[${docs}/api-reference/package#package.push]] + q3.Package.install("${name}"${pathPy}${hashPy}, registry="s3://${bucket}", dest=".") + ` + }, + CLI_DOWNLOAD: (bucket: string, name: string, path: string, hashDisplay: string) => { + const pathCli = path && ` --path "${s3paths.ensureNoSlash(path)}"` + const hashCli = hashDisplay && ` --top-hash ${hashDisplay}` + return dedent` + # Download package [[${docs}/api-reference/cli#install]] + quilt3 install "${name}"${pathCli}${hashCli} --registry s3://${bucket} --dest . + ` + }, + CLI_UPLOAD: (bucket: string, name: string) => + dedent` + # Upload package [[${docs}/api-reference/cli#push]] + echo "Hello World" > README.md + quilt3 push "${name}" --registry s3://${bucket} --dir . + `, +} + +interface PackageCodeSamplesProps extends Partial { + bucket: string + name: string + hash: string + hashOrTag: string + path: string +} + +export default function PackageCodeSamples({ + bucket, + name, + hash, + hashOrTag, + path, + ...props +}: PackageCodeSamplesProps) { + const hashDisplay = hashOrTag === 'latest' ? '' : R.take(10, hash) + const code = React.useMemo( + () => [ + { + label: 'Python', + hl: 'python', + contents: TEMPLATES.PY(bucket, name, path, hashDisplay), + }, + { + label: 'CLI', + hl: 'bash', + contents: [ + TEMPLATES.CLI_DOWNLOAD(bucket, name, path, hashDisplay), + !path ? TEMPLATES.CLI_UPLOAD(bucket, name) : '', + ] + .filter(Boolean) + .join('\n\n'), + }, + { + label: 'URI', + contents: PackageUri.stringify({ bucket, name, hash, path }), + }, + ], + [bucket, name, hashDisplay, hash, path], + ) + return {code} +} diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index f0d841c8db4..3c3f2f8f69c 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -1,6 +1,5 @@ -import { basename, join } from 'path' +import { join } from 'path' -import dedent from 'dedent' import * as R from 'ramda' import * as React from 'react' import * as RRDom from 'react-router-dom' @@ -23,7 +22,7 @@ import parseSearch from 'utils/parseSearch' import * as s3paths from 'utils/s3paths' import type * as workflows from 'utils/workflows' -import Code from './Code' +import DirCodeSamples from './CodeSamples/Dir' import * as FileView from './FileView' import { Item, Listing, PrefixFilter } from './Listing' import Menu from './Menu' @@ -289,35 +288,6 @@ export default function Dir({ const prefs = BucketPreferences.use() const { prefix } = parseSearch(l.search) const path = s3paths.decode(encodedPath) - const dest = path ? basename(path) : bucket - - const code = React.useMemo( - () => [ - { - label: 'Python', - hl: 'python', - contents: dedent` - import quilt3 as q3 - b = q3.Bucket("s3://${bucket}") - # list files - b.ls("${path}") - # download - b.fetch("${path}", "./${dest}") - `, - }, - { - label: 'CLI', - hl: 'bash', - contents: dedent` - # list files - aws s3 ls "s3://${bucket}/${path}" - # download - aws s3 cp --recursive "s3://${bucket}/${path}" "./${dest}" - `, - }, - ], - [bucket, path, dest], - ) const [prev, setPrev] = React.useState(null) @@ -417,7 +387,8 @@ export default function Dir({ {BucketPreferences.Result.match( { - Ok: ({ ui: { blocks } }) => blocks.code && {code}, + Ok: ({ ui: { blocks } }) => + blocks.code && , Pending: () => null, Init: () => null, }, diff --git a/catalog/app/containers/Bucket/File.js b/catalog/app/containers/Bucket/File.js index a34f316dcb7..0050c71efb3 100644 --- a/catalog/app/containers/Bucket/File.js +++ b/catalog/app/containers/Bucket/File.js @@ -1,7 +1,6 @@ import { basename } from 'path' import * as dateFns from 'date-fns' -import dedent from 'dedent' import * as R from 'ramda' import * as React from 'react' import { Link, useHistory } from 'react-router-dom' @@ -30,7 +29,7 @@ import parseSearch from 'utils/parseSearch' import { up, decode, handleToHttpsUri } from 'utils/s3paths' import { readableBytes, readableQuantity } from 'utils/string' -import Code from './Code' +import FileCodeSamples from './CodeSamples/File' import FileProperties from './FileProperties' import * as FileView from './FileView' import Section from './Section' @@ -336,28 +335,6 @@ export default function File({ const path = decode(encodedPath) - const code = React.useMemo( - () => [ - { - label: 'Python', - hl: 'python', - contents: dedent` - import quilt3 as q3 - b = q3.Bucket("s3://${bucket}") - b.fetch("${path}", "./${basename(path)}") - `, - }, - { - label: 'CLI', - hl: 'bash', - contents: dedent` - aws s3 cp "s3://${bucket}/${path}" . - `, - }, - ], - [bucket, path], - ) - const [resetKey, setResetKey] = React.useState(0) const objExistsData = useData(requests.getObjectExistence, { s3, @@ -521,7 +498,7 @@ export default function File({ { Ok: ({ ui: { blocks } }) => ( <> - {blocks.code && {code}} + {blocks.code && } {!!cfg.analyticsBucket && !!blocks.analytics && ( )} diff --git a/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx b/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx index 72ab422f8e6..4420286d0c1 100644 --- a/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx +++ b/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx @@ -1,6 +1,5 @@ import { basename } from 'path' -import dedent from 'dedent' import * as R from 'ramda' import * as React from 'react' import * as RRDom from 'react-router-dom' @@ -21,18 +20,16 @@ import * as AWS from 'utils/AWS' import * as BucketPreferences from 'utils/BucketPreferences' import Data from 'utils/Data' import * as GQL from 'utils/GraphQL' -// import * as LinkedData from 'utils/LinkedData' import * as LogicalKeyResolver from 'utils/LogicalKeyResolver' import MetaTitle from 'utils/MetaTitle' import * as NamedRoutes from 'utils/NamedRoutes' -import * as PackageUri from 'utils/PackageUri' import assertNever from 'utils/assertNever' import parseSearch from 'utils/parseSearch' import * as s3paths from 'utils/s3paths' import usePrevious from 'utils/usePrevious' import * as workflows from 'utils/workflows' -import Code from '../Code' +import PackageCodeSamples from '../CodeSamples/Package' import * as Download from '../Download' import { FileProperties } from '../FileProperties' import * as FileView from '../FileView' @@ -58,63 +55,6 @@ import DIR_QUERY from './gql/Dir.generated' import FILE_QUERY from './gql/File.generated' import DELETE_REVISION from './gql/DeleteRevision.generated' -interface PkgCodeProps { - bucket: string - name: string - hash: string - hashOrTag: string - path: string -} - -function PkgCode({ bucket, name, hash, hashOrTag, path }: PkgCodeProps) { - const pathCli = path && ` --path "${s3paths.ensureNoSlash(path)}"` - const pathPy = path && `, path="${s3paths.ensureNoSlash(path)}"` - const hashDisplay = hashOrTag === 'latest' ? '' : R.take(10, hash) - const hashPy = hashDisplay && `, top_hash="${hashDisplay}"` - const hashCli = hashDisplay && ` --top-hash ${hashDisplay}` - const code = [ - { - label: 'Python', - hl: 'python', - contents: dedent` - import quilt3 as q3 - # Browse - p = q3.Package.browse("${name}"${hashPy}, registry="s3://${bucket}") - # make changes to package adding individual files - p.set("data.csv", "data.csv") - # or whole directories - p.set_dir("subdir", "subdir") - # and push changes - p.push("${name}", registry="s3://${bucket}", message="Hello World") - - # Download (be mindful of large packages) - q3.Package.install("${name}"${pathPy}${hashPy}, registry="s3://${bucket}", dest=".") - `, - }, - { - label: 'CLI', - hl: 'bash', - contents: - dedent` - # Download package - quilt3 install "${name}"${pathCli}${hashCli} --registry s3://${bucket} --dest . - ` + - (!path - ? dedent`\n - # Upload package - echo "Hello World" > README.md - quilt3 push "${name}" --registry s3://${bucket} --dir . - ` - : ''), - }, - { - label: 'URI', - contents: PackageUri.stringify({ bucket, name, hash, path }), - }, - ] - return {code} -} - const useTopBarStyles = M.makeStyles((t) => ({ topBar: { alignItems: 'flex-end', @@ -454,7 +394,7 @@ function DirDisplay({ Ok: ({ ui: { blocks } }) => ( <> {blocks.code && ( - + )} {blocks.meta && ( @@ -726,7 +666,7 @@ function FileDisplay({ Ok: ({ ui: { blocks } }) => ( <> {blocks.code && ( - + )} {blocks.meta && ( diff --git a/catalog/app/embed/Dir.js b/catalog/app/embed/Dir.js index 589402f627e..9992cebce12 100644 --- a/catalog/app/embed/Dir.js +++ b/catalog/app/embed/Dir.js @@ -1,6 +1,5 @@ import { basename } from 'path' -import dedent from 'dedent' import * as R from 'ramda' import * as React from 'react' import { useHistory } from 'react-router-dom' @@ -15,7 +14,7 @@ import * as NamedRoutes from 'utils/NamedRoutes' import parseSearch from 'utils/parseSearch' import * as s3paths from 'utils/s3paths' -import Code from 'containers/Bucket/Code' +import DirCodeSamples from 'containers/Bucket/CodeSamples/Dir' import * as FileView from 'containers/Bucket/FileView' import { Listing, PrefixFilter } from 'containers/Bucket/Listing' import Summary from 'containers/Bucket/Summary' @@ -71,35 +70,6 @@ export default function Dir({ const s3 = AWS.S3.use() const { prefix } = parseSearch(l.search) const path = s3paths.decode(encodedPath) - const dest = path ? basename(path) : bucket - - const code = React.useMemo( - () => [ - { - label: 'Python', - hl: 'python', - contents: dedent` - import quilt3 as q3 - b = q3.Bucket("s3://${bucket}") - # list files - b.ls("${path}") - # download - b.fetch("${path}", "./${dest}") - `, - }, - { - label: 'CLI', - hl: 'bash', - contents: dedent` - # list files - aws s3 ls "s3://${bucket}/${path}" - # download - aws s3 cp --recursive "s3://${bucket}/${path}" "./${dest}" - `, - }, - ], - [bucket, path, dest], - ) const [prev, setPrev] = React.useState(null) @@ -164,7 +134,7 @@ export default function Dir({ )} - {!ecfg.hideCode && {code}} + {!ecfg.hideCode && } {data.case({ Err: displayError(), diff --git a/catalog/app/embed/File.js b/catalog/app/embed/File.js index e6f9a2dca74..cd3787ea7e9 100644 --- a/catalog/app/embed/File.js +++ b/catalog/app/embed/File.js @@ -1,7 +1,6 @@ import { basename } from 'path' import * as dateFns from 'date-fns' -import dedent from 'dedent' import * as R from 'ramda' import * as React from 'react' import { Link } from 'react-router-dom' @@ -25,7 +24,7 @@ import parseSearch from 'utils/parseSearch' import * as s3paths from 'utils/s3paths' import { readableBytes, readableQuantity } from 'utils/string' -import Code from 'containers/Bucket/Code' +import FileCodeSamples from 'containers/Bucket/CodeSamples/File' import FileProperties from 'containers/Bucket/FileProperties' import * as FileView from 'containers/Bucket/FileView' import Section from 'containers/Bucket/Section' @@ -363,28 +362,6 @@ export default function File({ const path = s3paths.decode(encodedPath) - const code = React.useMemo( - () => [ - { - label: 'Python', - hl: 'python', - contents: dedent` - import quilt3 as q3 - b = q3.Bucket("s3://${bucket}") - b.fetch("${path}", "./${basename(path)}") - `, - }, - { - label: 'CLI', - hl: 'bash', - contents: dedent` - aws s3 cp "s3://${bucket}/${path}" . - `, - }, - ], - [bucket, path], - ) - const objExistsData = useData(requests.getObjectExistence, { s3, bucket, key: path }) const versionExistsData = useData(requests.getObjectExistence, { s3, @@ -483,7 +460,7 @@ export default function File({ Ok: requests.ObjectExistence.case({ Exists: () => ( <> - {!ecfg.hideCode && {code}} + {!ecfg.hideCode && } {!ecfg.hideAnalytics && !!cfg.analyticsBucket && ( )} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ac50c175dd8..4df6c5d8055 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -25,6 +25,7 @@ Entries inside each section should be ordered by type: * [Fixed] Fix Header's orange flash on load ([#3487](https://github.com/quiltdata/quilt/pull/3487)) * [Fixed] Fix code sample for package push ([#3499](https://github.com/quiltdata/quilt/pull/3499)) * [Added] Add filter for users and buckets tables in Admin dashboards ([#3480](https://github.com/quiltdata/quilt/pull/3480)) +* [Added] Add links to documentation and re-use code samples ([#3496](https://github.com/quiltdata/quilt/pull/3496)) * [Changed] Enable user selection in perspective grids ([#3453](https://github.com/quiltdata/quilt/pull/3453)) # 5.3.1 - 2023-05-02