Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
95e4195
migrate from legacy api
fiskus Sep 3, 2025
3c704dc
handle loading state for query editor
fiskus Sep 3, 2025
6b0ee80
add test for that
fiskus Sep 3, 2025
ae62d71
memoize editor props
fiskus Sep 3, 2025
836634e
revert "the fix"
fiskus Sep 3, 2025
1a0a20c
add new tests for problematic caes
fiskus Sep 3, 2025
757a242
fix switching states during execution
fiskus Sep 3, 2025
b49aa80
minor change
fiskus Sep 3, 2025
c3b6f6f
add comments
fiskus Sep 3, 2025
d3e85a6
fix navigating to executions
fiskus Sep 3, 2025
74ea6f8
lint
fiskus Sep 3, 2025
0682e69
add changelog entry
fiskus Sep 3, 2025
ef00bce
merge with master
fiskus Sep 3, 2025
26bf8c8
Update catalog/app/containers/Bucket/Queries/Athena/model/requests.ts
fiskus Sep 3, 2025
a57c48e
unify list bullets
fiskus Sep 4, 2025
2aad97f
Merge branch 'athena-execution-loading-state' of github.com:quiltdata…
fiskus Sep 4, 2025
e9ad9e8
discrimitating union
fiskus Sep 4, 2025
bd48164
fix types in components
fiskus Sep 4, 2025
c0d211b
Loading → Pending
fiskus Sep 5, 2025
ec2880f
Data → Payload
fiskus Sep 5, 2025
df0bb15
remove extra exports
fiskus Sep 5, 2025
28946d1
expose data: null for consistency
fiskus Sep 5, 2025
be74dcd
fix payload state in callback
fiskus Sep 5, 2025
4ac505f
use consistent naming aliases to avoid data.data
fiskus Sep 5, 2025
eec59d7
clean up; remove extra exports
fiskus Sep 5, 2025
ee108aa
clean up; remove extra exports; fix tests
fiskus Sep 5, 2025
9f7d76b
refactor: unify useEffect state update patterns in Athena hooks
fiskus Sep 5, 2025
4678728
use Data for workgroup instead of DataController
fiskus Sep 5, 2025
f056322
hide useEffect in useRequest for useWorkgroups
fiskus Sep 5, 2025
35bb385
use useMemo instead of useEffect + useState
fiskus Sep 5, 2025
48d47f9
cancellable requests with useRequest
fiskus Sep 8, 2025
9850510
formatting
fiskus Sep 8, 2025
e73c0b7
separate getting value into clean functions
fiskus Sep 8, 2025
a9d9376
use url helpers
fiskus Sep 8, 2025
8e2b1fb
simplify and unify requests
fiskus Sep 9, 2025
ff44121
use @testing-library/react instead of react-test-renderer
fiskus Sep 9, 2025
77e6929
merge with master
fiskus Sep 9, 2025
727b075
Merge branch 'master' of github.com:quiltdata/quilt into athena-state…
fiskus Sep 9, 2025
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
4 changes: 2 additions & 2 deletions catalog/app/containers/Bucket/FallbackToDir.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ function useIsDirectory(handle: Model.S3.S3ObjectLocation, proceed: boolean) {
}

function useFallbackToDir(handle: Model.S3.S3ObjectLocation) {
const isObject = useIsObject(handle)
const isDirectory = useIsDirectory(handle, !isObject)
const { result: isObject } = useIsObject(handle)
const { result: isDirectory } = useIsDirectory(handle, !isObject)

if (
isObject === Request.Idle ||
Expand Down
127 changes: 73 additions & 54 deletions catalog/app/containers/Bucket/Queries/Athena/Athena.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import cx from 'classnames'
import invariant from 'invariant'
import * as R from 'ramda'
import * as React from 'react'
import * as RRDom from 'react-router-dom'
Expand All @@ -8,7 +9,6 @@ import Code from 'components/Code'
import Placeholder from 'components/Placeholder'
import Skeleton from 'components/Skeleton'
import * as BucketPreferences from 'utils/BucketPreferences'
import * as NamedRoutes from 'utils/NamedRoutes'

import QuerySelect from '../QuerySelect'

Expand Down Expand Up @@ -107,15 +107,18 @@ interface QueryConstructorProps {
function QueryConstructor({ className }: QueryConstructorProps) {
const { query, queries, queryRun } = Model.use()

if (Model.isError(queries.data)) {
return <Alert className={className} error={queries.data} title="Select query" />
const selected = query.value
const list = queries.data

if (Model.isError(list)) {
return <Alert className={className} error={list.error} title="Select query" />
}

if (!Model.hasData(queries.data) || !Model.isReady(query.value)) {
if (!Model.isReady(list) || !Model.isReady(selected)) {
return <QuerySelectSkeleton className={className} />
}

if (!queries.data.list.length && !Model.isError(query.value)) {
if (!list.data.list.length && !Model.isError(selected)) {
return <M.Typography className={className}>No saved queries.</M.Typography>
}

Expand All @@ -124,32 +127,32 @@ function QueryConstructor({ className }: QueryConstructorProps) {
<QuerySelect<Model.Query | null>
label="Select a query"
className={className}
disabled={Model.isLoading(queryRun)}
disabled={!Model.isReady(queryRun)}
onChange={query.setValue}
onLoadMore={queries.data.next ? queries.loadMore : undefined}
queries={queries.data.list}
value={Model.isError(query.value) ? null : query.value}
onLoadMore={list.data.next ? queries.loadMore : undefined}
queries={list.data.list}
value={Model.hasData(selected) ? selected.data : null}
/>
{Model.isError(query.value) && (
<M.FormHelperText error>{query.value.message}</M.FormHelperText>
{Model.isError(selected) && (
<M.FormHelperText error>{selected.error.message}</M.FormHelperText>
)}
</>
)
}

function HistoryContainer() {
const { bucket, executions } = Model.use()
if (Model.isError(executions.data)) {
return <Alert error={executions.data} title="Executions Data" />
}
if (!Model.hasData(executions.data)) {
return <TableSkeleton size={4} />
}
const { executions } = Model.use()

const list = executions.data

if (!Model.isReady(list)) return <TableSkeleton size={4} />

if (Model.isError(list)) return <Alert error={list.error} title="Executions Data" />

return (
<History
bucket={bucket}
executions={executions.data.list}
onLoadMore={executions.data.next ? executions.loadMore : undefined}
executions={list.data.list}
onLoadMore={list.data.next ? executions.loadMore : undefined}
/>
)
}
Expand All @@ -170,7 +173,6 @@ const useResultsContainerStyles = M.makeStyles((t) => ({
}))

interface ResultsContainerSkeletonProps {
bucket: string
className: string
}

Expand All @@ -181,11 +183,11 @@ const relieveMessages = [
'Hang in there, we haven’t forgotten about you! Your request is still being processed.',
]

function ResultsContainerSkeleton({ bucket, className }: ResultsContainerSkeletonProps) {
function ResultsContainerSkeleton({ className }: ResultsContainerSkeletonProps) {
const classes = useResultsContainerStyles()
return (
<div className={className}>
<ResultsBreadcrumbs bucket={bucket} className={classes.breadcrumbs}>
<ResultsBreadcrumbs className={classes.breadcrumbs}>
<Skeleton height={24} width={144} animate />
</ResultsBreadcrumbs>
<div className={classes.table}>
Expand All @@ -204,43 +206,45 @@ function ResultsContainer({ className }: ResultsContainerProps) {
const classes = useResultsContainerStyles()
const { bucket, execution, results } = Model.use()

const list = results.data

if (!Model.isReady(execution) || !Model.isReady(list)) {
return <ResultsContainerSkeleton className={className} />
}

if (Model.isError(execution)) {
return (
<div className={className}>
<ResultsBreadcrumbs bucket={bucket} className={classes.breadcrumbs} />
<Alert error={execution} title="Query execution" className={className} />
<ResultsBreadcrumbs className={classes.breadcrumbs} />
<Alert error={execution.error} title="Query execution" className={className} />
</div>
)
}

if (Model.isError(results.data)) {
if (Model.isError(list)) {
return (
<div className={className}>
<ResultsBreadcrumbs bucket={bucket} className={classes.breadcrumbs} />
<Alert error={results.data} title="Query results" className={className} />
<ResultsBreadcrumbs className={classes.breadcrumbs} />
<Alert error={list.error} title="Query results" className={className} />
</div>
)
}

if (!Model.isReady(execution) || !Model.isReady(results.data)) {
return <ResultsContainerSkeleton bucket={bucket} className={className} />
}

return (
<div className={className}>
<ResultsBreadcrumbs bucket={bucket} className={classes.breadcrumbs}>
{doQueryResultsContainManifestEntries(results.data) ? (
<ResultsBreadcrumbs className={classes.breadcrumbs}>
{doQueryResultsContainManifestEntries(list.data) ? (
<React.Suspense fallback={<M.CircularProgress />}>
<CreatePackage bucket={bucket} queryResults={results.data} />
<CreatePackage bucket={bucket} queryResults={list.data} />
</React.Suspense>
) : (
<SeeDocsForCreatingPackage />
)}
</ResultsBreadcrumbs>
<Results
rows={results.data.rows}
columns={results.data.columns}
onLoadMore={results.data.next ? results.loadMore : undefined}
rows={list.data.rows}
columns={list.data.columns}
onLoadMore={list.data.next ? results.loadMore : undefined}
/>
</div>
)
Expand Down Expand Up @@ -289,25 +293,26 @@ const useResultsBreadcrumbsStyles = M.makeStyles({
})

interface ResultsBreadcrumbsProps {
bucket: string
children?: React.ReactNode
className?: string
}

function ResultsBreadcrumbs({ bucket, children, className }: ResultsBreadcrumbsProps) {
const { workgroup, queryExecutionId } = Model.use()
function ResultsBreadcrumbs({ children, className }: ResultsBreadcrumbsProps) {
const { queryExecutionId, toWorkgroup } = Model.use()
const classes = useResultsBreadcrumbsStyles()
const overrideClasses = useOverrideStyles()
const { urls } = NamedRoutes.use()

// Get workgroup from URL since it's available in AthenaContainer context
const { workgroup: workgroupFromUrl } = RRDom.useParams<{ workgroup?: string }>()

return (
<div className={cx(classes.root, className)}>
<M.Breadcrumbs classes={overrideClasses}>
<RRDom.Link
className={classes.breadcrumb}
to={urls.bucketAthenaWorkgroup(bucket, workgroup.data)}
>
Query Executions
</RRDom.Link>
{workgroupFromUrl && (
<RRDom.Link className={classes.breadcrumb} to={toWorkgroup(workgroupFromUrl)}>
Query Executions
</RRDom.Link>
)}
<M.Typography className={classes.breadcrumb} color="textPrimary">
Results for<Code className={classes.id}>{queryExecutionId}</Code>
</M.Typography>
Expand All @@ -333,19 +338,25 @@ const useStyles = M.makeStyles((t) => ({
},
}))

function AthenaContainer() {
const { bucket, queryExecutionId, workgroup } = Model.use()
interface AthenaParams {
bucket: string
queryExecutionId?: string
workgroup?: Model.Workgroup
}

function AthenaContainer({}) {
const classes = useStyles()
const { queryExecutionId, workgroup } = Model.use()

return (
<>
<M.Typography className={classes.header} variant="h6">
Athena SQL
</M.Typography>

<Workgroups bucket={bucket} />
<Workgroups />

{Model.hasData(workgroup.data) && (
{Model.hasData(workgroup) && (
<div className={classes.content}>
<div className={classes.section}>
<QueryConstructor />
Expand All @@ -365,11 +376,19 @@ function AthenaContainer() {
}

export default function Wrapper() {
const { bucket, queryExecutionId, workgroup } = RRDom.useParams<AthenaParams>()
invariant(!!bucket, '`bucket` must be defined')

const { prefs } = BucketPreferences.use()
return BucketPreferences.Result.match(
{
Ok: ({ ui }) => (
<Model.Provider preferences={ui.athena}>
<Model.Provider
bucket={bucket}
preferences={ui.athena}
queryExecutionId={queryExecutionId}
workgroupId={workgroup}
>
<AthenaContainer />
</Model.Provider>
),
Expand Down
Loading
Loading