Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions catalog/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ complete sentence without it.

## Changes

- [Changed] The `data-products` demo fixture data no longer ships in the bundles a browser downloads on the volumes landing; it loads only when the preview is on ([#5259](https://github.com/quiltdata/quilt/pull/5259))
- [Fixed] Quilt+ URI parsing retains the informational `catalog` field, reads a raw `+` in an unencoded path as a `+` rather than a space, and shares its compatibility corpus with quilt3 ([#5255](https://github.com/quiltdata/quilt/pull/5255))
- [Fixed] Quilt+ URIs: a package path containing a literal `%` no longer breaks the URI, and one containing a literal `%20` no longer decodes to a space and points at the wrong entry. Paths from producers that do not percent-encode now resolve instead of failing, and a path that genuinely cannot be decoded reports a real error rather than the literal text `unknown error: ${e}` ([#5256](https://github.com/quiltdata/quilt/pull/5256))
- [Changed] Search: error states are separate, more strictly typed components, safer to extend than one component behind a `kind` prop ([#5238](https://github.com/quiltdata/quilt/pull/5238))
Expand Down
47 changes: 23 additions & 24 deletions catalog/app/model/DataProducts/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,29 @@ import * as Cache from 'utils/ResourceCache'

import type { ContentsResult, DataProductAdapter, EntryBodyResult } from './adapter'
import { supportsBrowsing, supportsFetching } from './adapter'
import { fixtureAdapter } from './fixtureAdapter'
import type { AccessRequest } from './requests'
import type { Connection } from './connections'
import type { DataProduct } from './types'

/**
* Which adapter the hooks read from.
*
* A module-level constant rather than a React context, deliberately: there is
* A module-level choice rather than a React context, deliberately: there is
* exactly one adapter per deployment, chosen by what the registry can serve, and
* nothing in the UI ever wants two at once. A context would invite per-subtree
* overriding -- which sounds flexible and in practice means two screens
* disagreeing about what exists.
*
* Imported dynamically because this module is on the volumes landing's import
* path (through `useProducts`), and a static import puts the whole fixture
* corpus in that chunk -- including for deployments with the preview off, which
* never read it.
*
* When a GraphQL-backed adapter lands this becomes a build- or config-time
* choice here, and no container changes. That is the whole point of the port.
*/
const adapter: DataProductAdapter = fixtureAdapter
const loadAdapter = (): Promise<DataProductAdapter> =>
import('./fixtureAdapter').then((m) => m.fixtureAdapter)

// The cache keys on `input`; these resources take none beyond the ids below, so
// `key` is explicit rather than relying on `R.identity` over an object.
Expand All @@ -55,29 +60,30 @@ const adapter: DataProductAdapter = fixtureAdapter
const ProductsResource = Cache.createResource({
name: 'DataProducts.list',
fetch: ({ enabled }: { enabled: boolean }) =>
enabled ? adapter.listProducts() : Promise.resolve([]),
enabled ? loadAdapter().then((a) => a.listProducts()) : Promise.resolve([]),
// @ts-expect-error
key: ({ enabled }: { enabled: boolean }) => enabled,
})

const ConnectionsResource = Cache.createResource({
name: 'DataProducts.connections',
fetch: ({ enabled }: { enabled: boolean }) =>
enabled ? adapter.listConnections() : Promise.resolve([]),
enabled ? loadAdapter().then((a) => a.listConnections()) : Promise.resolve([]),
// @ts-expect-error
key: ({ enabled }: { enabled: boolean }) => enabled,
})

const ProductResource = Cache.createResource({
name: 'DataProducts.product',
fetch: ({ id }: { id: string }) => adapter.getProduct(id),
fetch: ({ id }: { id: string }) => loadAdapter().then((a) => a.getProduct(id)),
// @ts-expect-error
key: ({ id }: { id: string }) => id,
})

const RequestsResource = Cache.createResource({
name: 'DataProducts.requests',
fetch: ({ productId }: { productId: string }) => adapter.listRequests(productId),
fetch: ({ productId }: { productId: string }) =>
loadAdapter().then((a) => a.listRequests(productId)),
// @ts-expect-error
key: ({ productId }: { productId: string }) => productId,
})
Expand All @@ -94,9 +100,11 @@ const ContentsResource = Cache.createResource({
// enumerate contents at all. NOT_FOUND is the honest answer -- we have no way
// to look, so we did not find anything -- and it keeps the UI on one code
// path rather than branching on adapter shape at every call site.
supportsBrowsing(adapter)
? adapter.listContents(productId, member)
: Promise.resolve<ContentsResult>({ ok: false, reason: 'NOT_FOUND' }),
loadAdapter().then((a) =>
supportsBrowsing(a)
? a.listContents(productId, member)
: ({ ok: false, reason: 'NOT_FOUND' } as ContentsResult),
),
// @ts-expect-error
key: ({ productId, member }: { productId: string; member: string }) =>
`${productId}::${member}`,
Expand All @@ -110,9 +118,11 @@ const EntryBodyResource = Cache.createResource({
// Same reasoning as contents: an adapter that cannot fetch is a real shape,
// not a broken one, and NOT_FOUND keeps the UI on one path rather than
// branching on adapter capability at the call site.
supportsFetching(adapter)
? adapter.fetchEntry(productId, member, logicalKey)
: Promise.resolve<EntryBodyResult>({ ok: false, reason: 'NOT_FOUND' }),
loadAdapter().then((a) =>
supportsFetching(a)
? a.fetchEntry(productId, member, logicalKey)
: ({ ok: false, reason: 'NOT_FOUND' } as EntryBodyResult),
),
// @ts-expect-error
key: ({ productId, member, logicalKey }: EntryInput) =>
`${productId}::${member}::${logicalKey}`,
Expand Down Expand Up @@ -197,14 +207,3 @@ export function useContents(productId: string, member: string): ContentsResult {
{ suspend: true },
) as ContentsResult
}

/**
* The adapter itself, for the one thing hooks cannot express: asking whether a
* write path exists.
*
* Exposed so a container can call `supportsRequests(useAdapter())` and disable
* its submit affordance honestly, instead of hardcoding "no adapter yet".
*/
export function useAdapter(): DataProductAdapter {
return adapter
}
1 change: 0 additions & 1 deletion catalog/app/model/DataProducts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export type {
} from './adapter'
export { supportsBrowsing, supportsFetching, supportsRequests } from './adapter'
export {
useAdapter,
useConnections,
useContents,
useEntryBody,
Expand Down
Loading