diff --git a/catalog/app/components/Assistant/Model/GlobalContext/__snapshots__/navigation.spec.ts.snap b/catalog/app/components/Assistant/Model/GlobalContext/__snapshots__/navigation.spec.ts.snap index b04c5a005be..e59091ebbad 100644 --- a/catalog/app/components/Assistant/Model/GlobalContext/__snapshots__/navigation.spec.ts.snap +++ b/catalog/app/components/Assistant/Model/GlobalContext/__snapshots__/navigation.spec.ts.snap @@ -1,42 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/Assistant/Model/GlobalTools/navigation NavigableRouteSchema should decode input 1`] = ` -{ - "name": "search", - "params": { - "buckets": [], - "order": "NEWEST", - "params": { - "filter": [], - "resultType": "p", - "userMetaFilters": [ - { - "path": "/author", - "predicate": { - "type": "KeywordEnum", - "value": { - "terms": [ - "Aneesh", - "Maksim", - ], - }, - }, - }, - ], - }, - "searchString": "", - }, -} -`; - -exports[`components/Assistant/Model/GlobalTools/navigation NavigableRouteSchema should decode input 2`] = ` -{ - "hash": "", - "pathname": "/search", - "search": "o=NEWEST&meta.e%2Fauthor=%22Aneesh%22%2C%22Maksim%22", -} -`; - exports[`components/Assistant/Model/GlobalTools/navigation NavigateSchema produced JSON Schema should match the snapshot 1`] = ` { "$defs": { @@ -667,21 +630,64 @@ exports[`components/Assistant/Model/GlobalTools/navigation NavigateSchema produc }, { "additionalProperties": false, - "description": "Bucket root page", + "description": "Bucket overview page", "properties": { "name": { - "const": "bucket", + "const": "bucket.overview", }, "params": { - "$id": "/schemas/{}", - "oneOf": [ - { - "type": "object", + "additionalProperties": false, + "properties": { + "bucket": { + "type": "string", }, - { - "type": "array", + }, + "required": [ + "bucket", + ], + "type": "object", + }, + }, + "required": [ + "name", + "params", + ], + "type": "object", + }, + { + "additionalProperties": false, + "description": "S3 Object (aka File) Detail page", + "properties": { + "name": { + "const": "bucket.object", + }, + "params": { + "additionalProperties": false, + "properties": { + "bucket": { + "type": "string", }, + "mode": { + "description": "Contents preview mode", + "title": "Viewing Mode", + "type": "string", + }, + "path": { + "description": "a string", + "title": "string & Brand<"S3Path">", + "type": "string", + }, + "version": { + "description": "S3 Object Version ID (omit for latest version)", + "title": "Version ID", + "type": "string", + }, + }, + "required": [ + "bucket", + "path", ], + "type": "object", }, }, "required": [ diff --git a/catalog/app/components/Assistant/Model/GlobalContext/navigation.spec.ts b/catalog/app/components/Assistant/Model/GlobalContext/navigation.spec.ts index 63b867bf3f4..b6864294d21 100644 --- a/catalog/app/components/Assistant/Model/GlobalContext/navigation.spec.ts +++ b/catalog/app/components/Assistant/Model/GlobalContext/navigation.spec.ts @@ -1,3 +1,4 @@ +import * as Eff from 'effect' import { JSONSchema, Schema } from '@effect/schema' import * as nav from './navigation' @@ -16,38 +17,67 @@ describe('components/Assistant/Model/GlobalTools/navigation', () => { }) }) }) - describe('NavigableRouteSchema', () => { - it('should decode input', async () => { - const routeInput = { - name: 'search', - params: { - searchString: '', - buckets: [], - order: 'NEWEST', + describe('routes', () => { + const TEST_CASES = [ + { + route: { + name: 'search', params: { - resultType: 'p', - filter: [], - userMetaFilters: [ - { - path: '/author', - predicate: { - type: 'KeywordEnum', - value: { - terms: ['Aneesh', 'Maksim'], + searchString: '', + buckets: [], + order: 'NEWEST', + params: { + resultType: 'p', + filter: [], + userMetaFilters: [ + { + path: '/author', + predicate: { + type: 'KeywordEnum', + value: { + terms: ['Aneesh', 'Maksim'], + }, }, }, - }, - ], + ], + }, }, }, - } - const routeDecoded = Schema.decodeUnknownSync(nav.NavigableRouteSchema)(routeInput) - expect(routeDecoded).toMatchSnapshot() + loc: { + pathname: '/search', + search: 'o=NEWEST&meta.e%2Fauthor=%22Aneesh%22%2C%22Maksim%22', + hash: '', + }, + }, + { + route: { + name: 'bucket.overview', + params: { + bucket: 'test-bucket', + }, + }, + loc: { + pathname: '/b/test-bucket', + search: '', + hash: '', + }, + }, + ] - const route = nav.routes[routeDecoded.name] - // @ts-expect-error - const loc = await Schema.encodePromise(route.paramsSchema)(routeDecoded.params) - expect(loc).toMatchSnapshot() - }) + const encode = Eff.flow( + Schema.decodeUnknown(nav.NavigableRouteSchema), + Eff.Effect.andThen(nav.locationFromRoute), + Eff.Effect.runPromise, + ) + + for (let i in TEST_CASES) { + const tc = TEST_CASES[i] + describe(`${i + 1}: ${tc.route.name}`, () => { + it('should encode', async () => { + const loc = await encode(tc.route) + expect(loc).toEqual(tc.loc) + }) + }) + } }) }) diff --git a/catalog/app/components/Assistant/Model/GlobalContext/navigation.ts b/catalog/app/components/Assistant/Model/GlobalContext/navigation.ts index 1ce75c9b5eb..a8140f62638 100644 --- a/catalog/app/components/Assistant/Model/GlobalContext/navigation.ts +++ b/catalog/app/components/Assistant/Model/GlobalContext/navigation.ts @@ -3,6 +3,7 @@ import * as React from 'react' import * as RR from 'react-router-dom' import { Schema as S } from '@effect/schema' +import bucketRoutes from 'containers/Bucket/Routes' import search from 'containers/Search/Route' import * as ROUTES from 'constants/routes' import * as Log from 'utils/Logging' @@ -21,6 +22,7 @@ const routeList = [ Nav.makeRoute({ name: 'home', path: ROUTES.home.path, + exact: true, description: 'Home page', // searchParams: S.Struct({ // // XXX: passing this param doesn't actually work bc of how it's implemented in @@ -92,12 +94,7 @@ const routeList = [ // // // - Nav.makeRoute({ - name: 'bucket', - path: ROUTES.bucketRoot.path, - description: 'Bucket root page', - // XXX: this should hold the bucket name and subroute info (e.g. package vs file view) - }), + ...bucketRoutes, ] as const type KnownRoute = (typeof routeList)[number] @@ -114,6 +111,10 @@ export const NavigableRouteSchema = S.Union( type NavigableRoute = typeof NavigableRouteSchema.Type +export const locationFromRoute = (route: NavigableRoute) => + // @ts-expect-error + S.encode(routes[route.name].paramsSchema)(route.params) + type History = ReturnType const WAIT_TIMEOUT = Eff.Duration.seconds(30) @@ -128,9 +129,7 @@ const navigate = ( enter: [`to: ${route.name}`, Log.br, 'params:', route.params], })( Eff.pipe( - route.params, - // @ts-expect-error - S.encode(routes[route.name].paramsSchema), + locationFromRoute(route), Eff.Effect.tap((loc) => Eff.Effect.log(`Navigating to location:`, Log.br, loc)), Eff.Effect.andThen((loc) => Eff.Effect.sync(() => history.push(loc))), Eff.Effect.andThen(() => @@ -166,7 +165,11 @@ interface Match { const matchLocation = (loc: typeof Nav.Location.Type): Match | null => Eff.pipe( Eff.Array.findFirst(routeList, (route) => - RR.matchPath(loc.pathname, { path: route.path, exact: true }) + RR.matchPath(loc.pathname, { + path: route.path, + exact: route.exact, + strict: route.strict, + }) ? Eff.Option.some(route) : Eff.Option.none(), ), diff --git a/catalog/app/containers/Bucket/File.js b/catalog/app/containers/Bucket/File.js index c06fdcbc0fa..14e41f59f06 100644 --- a/catalog/app/containers/Bucket/File.js +++ b/catalog/app/containers/Bucket/File.js @@ -6,6 +6,7 @@ import * as React from 'react' import { Link, useHistory, useLocation, useParams } from 'react-router-dom' import * as M from '@material-ui/core' +import * as Assistant from 'components/Assistant' import * as BreadCrumbs from 'components/BreadCrumbs' import * as Buttons from 'components/Buttons' import * as FileEditor from 'components/FileEditor' @@ -30,15 +31,26 @@ import { up, decode, handleToHttpsUri } from 'utils/s3paths' import { readableBytes, readableQuantity } from 'utils/string' import FileCodeSamples from './CodeSamples/File' +import * as AssistantContext from './FileAssistantContext' import FileProperties from './FileProperties' import * as FileView from './FileView' -import QuratorButton from './Qurator/Button' -import QuratorContext from './Qurator/Context' import Section from './Section' import renderPreview from './renderPreview' import * as requests from './requests' import { useViewModes, viewModeToSelectOption } from './viewModes' +function SummarizeButton() { + const assist = Assistant.use() + const msg = 'Summarize this document' + return ( + assist(msg)} edge="end"> + + assistant + + + ) +} + const useVersionInfoStyles = M.makeStyles(({ typography }) => ({ version: { ...linkStyle, @@ -84,6 +96,8 @@ function VersionInfo({ bucket, path, version }) { const data = useData(requests.objectVersions, { s3, bucket, path }) + AssistantContext.useVersionsContext(data) + return ( <> {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} @@ -344,6 +358,8 @@ export default function File() { resetKey, }) + AssistantContext.useCurrentVersionContext(version, objExistsData, versionExistsData) + const objExists = objExistsData.case({ _: () => false, Ok: requests.ObjectExistence.case({ @@ -483,7 +499,8 @@ export default function File() { {cfg.qurator && BucketPreferences.Result.match( { - Ok: ({ ui: { blocks } }) => (blocks.qurator ? : null), + // XXX: only show this when the object exists? + Ok: ({ ui: { blocks } }) => (blocks.qurator ? : null), _: () => null, }, prefs, @@ -514,7 +531,6 @@ export default function File() { {!!cfg.analyticsBucket && !!blocks.analytics && ( )} - {cfg.qurator && } {blocks.meta && ( <> diff --git a/catalog/app/containers/Bucket/FileAssistantContext.tsx b/catalog/app/containers/Bucket/FileAssistantContext.tsx new file mode 100644 index 00000000000..ef366e7578e --- /dev/null +++ b/catalog/app/containers/Bucket/FileAssistantContext.tsx @@ -0,0 +1,92 @@ +import * as Eff from 'effect' +import * as React from 'react' + +import * as Assistant from 'components/Assistant' +import * as XML from 'utils/XML' + +import { ObjectExistence } from './requests' + +export function useVersionsContext(data: $TSFixMe) { + const msg = React.useMemo( + () => + Eff.pipe( + data.case({ + Ok: (vs: $TSFixMe[]) => + Eff.Option.some( + vs.map((v) => XML.tag('version', { id: v.id }, JSON.stringify(v, null, 2))), + ), + Err: () => Eff.Option.some(['Error fetching versions']), + _: () => Eff.Option.none(), + }), + Eff.Option.map((children: Array) => + XML.tag('object-versions', {}, ...children).toString(), + ), + ), + [data], + ) + + Assistant.Context.usePushContext({ + markers: { versionsReady: Eff.Option.isSome(msg) }, + messages: Eff.Option.toArray(msg), + }) +} + +export function useCurrentVersionContext( + version: string, + objExistsData: $TSFixMe, + versionExistsData: $TSFixMe, +) { + const msg = React.useMemo( + () => + Eff.pipe( + objExistsData.case({ + _: () => Eff.Option.none(), + Err: (e: $TSFixMe) => + Eff.Option.some( + `Could not get object data: ${ + e.code === 'Forbidden' ? 'Access Denied' : e + }`, + ), + Ok: ObjectExistence.case({ + DoesNotExist: () => Eff.Option.some('Object does not exist'), + Exists: () => + versionExistsData.case({ + _: () => Eff.Option.none(), + Err: (e: $TSFixMe) => + Eff.Option.some(`Could not get current object version data: ${e}`), + Ok: ObjectExistence.case({ + Exists: (v: $TSFixMe) => + Eff.Option.some( + JSON.stringify( + { + deleted: v.deleted, + archived: v.archived, + id: v.version, + lastModified: v.lastModified, + }, + null, + 2, + ), + ), + DoesNotExist: () => Eff.Option.some('Object version does not exist'), + }), + }), + }), + }), + Eff.Option.map((children: string) => + XML.tag( + 'object-current-version', + {}, + `Currently displayed version: ${version || 'latest'}`, + children, + ).toString(), + ), + ), + [version, objExistsData, versionExistsData], + ) + + Assistant.Context.usePushContext({ + markers: { currentVersionReady: Eff.Option.isSome(msg) }, + messages: Eff.Option.toArray(msg), + }) +} diff --git a/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx b/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx index 99e8f989f86..9ade98ebfa7 100644 --- a/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx +++ b/catalog/app/containers/Bucket/PackageTree/PackageTree.tsx @@ -37,7 +37,6 @@ import * as FileView from '../FileView' import * as Listing from '../Listing' import PackageCopyDialog from '../PackageCopyDialog' import * as PD from '../PackageDialog' -import QuratorSection from '../Qurator/Section' import Section from '../Section' import * as Successors from '../Successors' import Summary from '../Summary' @@ -678,9 +677,11 @@ function FileDisplay({ )} - {cfg.qurator && blocks.qurator && ( + {/* + cfg.qurator && blocks.qurator && ( - )} + ) + */} ), _: () => null, diff --git a/catalog/app/containers/Bucket/Qurator/Button.tsx b/catalog/app/containers/Bucket/Qurator/Button.tsx deleted file mode 100644 index aeec59f2716..00000000000 --- a/catalog/app/containers/Bucket/Qurator/Button.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import * as M from '@material-ui/core' - -import * as Assistant from 'components/Assistant' - -export default function QuratorButton() { - const assist = Assistant.use() - const msg = 'Summarize this document' - return ( - assist(msg)} edge="end"> - - assistant - - - ) -} diff --git a/catalog/app/containers/Bucket/Qurator/Context.tsx b/catalog/app/containers/Bucket/Qurator/Context.tsx deleted file mode 100644 index bda2b7aa8cf..00000000000 --- a/catalog/app/containers/Bucket/Qurator/Context.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react' - -import * as Assistant from 'components/Assistant' -import type * as Model from 'model' - -interface QuratorContextProps { - handle: Model.S3.S3ObjectLocation -} - -export default function QuratorContext({ handle }: QuratorContextProps) { - const messages = [ - `You are viewing the details page for an S3 object ${JSON.stringify(handle)}`, - ] - return -} diff --git a/catalog/app/containers/Bucket/Qurator/Section.tsx b/catalog/app/containers/Bucket/Qurator/Section.tsx deleted file mode 100644 index a818e983b89..00000000000 --- a/catalog/app/containers/Bucket/Qurator/Section.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react' - -import { ChatSkeleton } from 'components/Chat' -import * as Model from 'model' - -import PageSection, { NodeRenderer } from '../Section' - -const QuratorSummary = React.lazy(() => import('./Summary')) - -interface QuratorSectionProps { - handle: Model.S3.S3ObjectLocation -} - -export default function QuratorSection({ handle }: QuratorSectionProps) { - return ( - - {({ expanded }: Parameters[0]) => - expanded && ( - }> - - - ) - } - - ) -} diff --git a/catalog/app/containers/Bucket/Qurator/Summary.tsx b/catalog/app/containers/Bucket/Qurator/Summary.tsx deleted file mode 100644 index dfd17c1beaa..00000000000 --- a/catalog/app/containers/Bucket/Qurator/Summary.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import * as React from 'react' -import * as Lab from '@material-ui/lab' - -import Chat from 'components/Chat' -import type * as Model from 'model' -import * as APIConnector from 'utils/APIConnector' -import * as AWS from 'utils/AWS' -import mkSearch from 'utils/mkSearch' - -const Loading = Symbol('Loading') - -type EsHit = { _source?: { content?: string } } - -type EsOutput = { hits?: { hits?: { _source?: { content?: string } }[] } } | null - -interface Hit { - value: EsHit | null -} - -const FILE_PROMPT = ( - content: string, -) => `I will ask you questions about the file's content indexed by ElasticSearch. The ElasticSearch JSON output: - -${content}. - -Please summarize the content of this file intelligently and concisely. Focus on file's content, don't mention ElasticSearch if unnecessary.` - -const NO_CONTENT_PROMPT = ( - json: string, -) => `Please respond that you can't answer questions about the content of the file, since it's empty or not yet indexed. However, you can tell about metadata indexed by ElasticSearch. This metadata is: - -${json}` - -const NO_DATA_PROMPT = `Please respond that you can't answer questions about the file, since it's empty or not yet indexed. -However, you can answer questions about Quilt.` - -function getPrompt(hit: Hit) { - if (!hit.value) return NO_DATA_PROMPT - // eslint-disable-next-line no-underscore-dangle - if (!hit.value._source?.content) return NO_CONTENT_PROMPT(JSON.stringify(hit.value)) - return FILE_PROMPT(JSON.stringify(hit.value)) -} - -function useBedrock(foundHit: null | typeof Loading | Error | Hit) { - const invokeModel = AWS.Bedrock.use() - - const [history, setHistory] = React.useState< - null | typeof Loading | Error | AWS.Bedrock.History - >(null) - - React.useEffect(() => { - if (foundHit === null || foundHit === Loading || foundHit instanceof Error) { - setHistory(foundHit) - return - } - - const message = AWS.Bedrock.createMessage(getPrompt(foundHit), 'summarize') - const newHistory = AWS.Bedrock.historyCreate(message) - setHistory(newHistory) - invokeModel(newHistory).then(setHistory).catch(setHistory) - }, [foundHit, invokeModel]) - - const invoke = React.useCallback( - async (userInput: string) => { - if (history === null || history === Loading || history instanceof Error) { - throw new Error('Invoking model when chat UI is not ready') - } - const prompt = AWS.Bedrock.createMessage(userInput) - const newHistory = AWS.Bedrock.historyAppend(prompt, history) - setHistory(newHistory) - try { - setHistory(await invokeModel(newHistory)) - } catch (e) { - setHistory(history) - throw e - } - }, - [history, invokeModel], - ) - return { history, invoke } -} - -interface ApiRequest { - (endpoint: string): Promise -} - -async function loadFileContent( - req: ApiRequest, - handle: Model.S3.S3ObjectLocation, -): Promise { - const qs = mkSearch({ - action: 'freeform', - body: JSON.stringify({ - query: { - query_string: { - query: `key:"${handle.key}"`, - }, - }, - }), - filter_path: 'hits.hits', - index: handle.bucket, - size: 1, - }) - const res: EsOutput = await req(`/search${qs}`) - if (!res?.hits?.hits?.length) return { value: null } - const firstHit: EsHit = res.hits.hits[0] - // eslint-disable-next-line no-underscore-dangle - if (firstHit._source?.content) { - // Take first 80% of "words" (word is the simplest attempt to get token). - // So, some space is left for user to chat. - // eslint-disable-next-line no-underscore-dangle - firstHit._source.content = firstHit._source?.content - .split(' ') - .slice(0, 0.8 * AWS.Bedrock.MAX_TOKENS) - .join(' ') - } - return { value: firstHit } -} - -const NeverResolvedComponent = React.lazy( - () => - new Promise(() => { - /* Never resolved */ - }), -) - -function useFileContent(handle: Model.S3.S3ObjectLocation) { - const req: ApiRequest = APIConnector.use() - const [state, setState] = React.useState(null) - React.useEffect(() => { - setState(Loading) - - loadFileContent(req, handle).then((content) => { - if (content === undefined) { - setState(new Error('Failed to find the content for this file')) - } else { - setState(content) - } - }) - }, [handle, req]) - - return state -} - -interface SummaryProps { - handle: Model.S3.S3ObjectLocation -} - -export default function Summary({ handle }: SummaryProps) { - const fileContent = useFileContent(handle) - const { history, invoke } = useBedrock(fileContent) - - if (history === null) return null - // if `history === Loading`, TS thinks history still can be a symbol - if (typeof history === 'symbol') { - return - } - - if (history instanceof Error) { - return {history.message} - } - - return ( - - ) -} diff --git a/catalog/app/containers/Bucket/Routes.ts b/catalog/app/containers/Bucket/Routes.ts new file mode 100644 index 00000000000..3f33e94d42d --- /dev/null +++ b/catalog/app/containers/Bucket/Routes.ts @@ -0,0 +1,143 @@ +import * as Eff from 'effect' +import * as S from '@effect/schema/Schema' + +import * as routes from 'constants/routes' +// import * as Model from 'model' +import * as Nav from 'utils/Navigation' + +const BucketPathParams = S.Struct({ + // XXX: constraints? + bucket: S.String, +}) + +export const overview = Nav.makeRoute({ + name: 'bucket.overview', + path: routes.bucketOverview.path, + description: 'Bucket overview page', + pathParams: Nav.fromPathParams(BucketPathParams), +}) + +const PATH_SEP = '/' + +const mapSegments = (separator: string, map: (s: string) => string) => + Eff.flow(Eff.String.split(separator), Eff.Array.map(map), Eff.Array.join(separator)) + +const S3Path = S.brand('S3Path')(S.String) + +const S3PathFromString = S.transform(S.String, S3Path, { + encode: mapSegments(PATH_SEP, encodeURIComponent), + decode: mapSegments(PATH_SEP, decodeURIComponent), +}) + +export const s3Object = Nav.makeRoute({ + name: 'bucket.object', + path: routes.bucketFile.path, + exact: true, + strict: true, + description: 'S3 Object (aka File) Detail page', + waitForMarkers: ['versionsReady', 'currentVersionReady'], + pathParams: Nav.fromPathParams( + S.extend( + BucketPathParams, + S.Struct({ + path: S3PathFromString.annotations({ + title: 'Object Key', + description: 'S3 Object Key aka File Path', + }), + }), + ), + ), + searchParams: S.Struct({ + version: Nav.SearchParamLastOpt.annotations({ + title: 'Version ID', + description: 'S3 Object Version ID (omit for latest version)', + }), + // XXX: constrain? + mode: Nav.SearchParamLastOpt.annotations({ + title: 'Viewing Mode', + description: 'Contents preview mode', + }), + // add: Nav.SearchParamLastOpt.annotations({ title: 'add' }), // ignore for now + // edit: S.optional(S.Boolean), + // next: S.optional(S.String), // ignore for now + }), +}) + +// export const bucketDir = route( +// '/b/:bucket/tree/:path(.+/)?', +// (bucket: string, path: string = '', prefix?: string) => +// `/b/${bucket}/tree/${encode(path)}${mkSearch({ prefix: prefix || undefined })}`, +// ) +// export type BucketDirArgs = Parameters +// +// interface BucketPackageListOpts { +// filter?: string +// sort?: string +// p?: string +// } +// +// export const bucketPackageList = route( +// '/b/:bucket/packages/', +// (bucket: string, { filter, sort, p }: BucketPackageListOpts = {}) => +// `/b/${bucket}/packages/${mkSearch({ filter, sort, p })}`, +// ) +// export type BucketPackageListArgs = Parameters +// +// interface BucketPackageDetailOpts { +// action?: string +// } +// +// export const bucketPackageDetail = route( +// `/b/:bucket/packages/:name(${PACKAGE_PATTERN})`, +// (bucket: string, name: string, { action }: BucketPackageDetailOpts = {}) => +// `/b/${bucket}/packages/${name}${mkSearch({ action })}`, +// ) +// export type BucketPackageDetailArgs = Parameters +// +// export const bucketPackageTree = route( +// `/b/:bucket/packages/:name(${PACKAGE_PATTERN})/tree/:revision/:path(.*)?`, +// (bucket: string, name: string, revision?: string, path: string = '', mode?: string) => +// path || (revision && revision !== 'latest') +// ? `/b/${bucket}/packages/${name}/tree/${revision || 'latest'}/${encode( +// path, +// )}${mkSearch({ mode })}` +// : bucketPackageDetail.url(bucket, name), +// ) +// export type BucketPackageTreeArgs = Parameters +// +// interface BucketPackageRevisionsOpts { +// p?: string +// } +// +// export const bucketPackageRevisions = route( +// `/b/:bucket/packages/:name(${PACKAGE_PATTERN})/revisions`, +// (bucket: string, name: string, { p }: BucketPackageRevisionsOpts = {}) => +// `/b/${bucket}/packages/${name}/revisions${mkSearch({ p })}`, +// ) +// +// export const bucketQueries = route( +// '/b/:bucket/queries', +// (bucket: string) => `/b/${bucket}/queries`, +// ) +// +// export const bucketESQueries = route( +// '/b/:bucket/queries/es', +// (bucket: string) => `/b/${bucket}/queries/es`, +// ) +// +// export const bucketAthena = route( +// '/b/:bucket/queries/athena', +// (bucket: string) => `/b/${bucket}/queries/athena`, +// ) +// +// export const bucketAthenaWorkgroup = route( +// '/b/:bucket/queries/athena/:workgroup', +// (bucket: string, workgroup: string) => `/b/${bucket}/queries/athena/${workgroup}`, +// ) +// +// export const bucketAthenaExecution = route( +// '/b/:bucket/queries/athena/:workgroup/:queryExecutionId', +// (bucket: string, workgroup: string, queryExecutionId: string) => +// `/b/${bucket}/queries/athena/${workgroup}/${queryExecutionId}`, + +export default [overview, s3Object]