Skip to content

Commit

Permalink
PORTALS-2701 - Send row selection to CAVATICA
Browse files Browse the repository at this point in the history
- QueryVisualizationWrapper: add rowSelectionPrimaryKey prop to determine how filtering on row selection should work. Defaults to `['id']` for file views and datasets.
- SendToCavaticaConfirmationDialog: refactor and encapsulate query to get actions required in new hook `useGetActionsRequiredForTableEntity`
- ConfirmationDialog: fix confirmButtonDisabled prop not propagating to button
- add tests to confirm actions required blocks export to CAVATICA action
  • Loading branch information
nickgros committed Jun 29, 2023
1 parent 975f326 commit b15514e
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function ConfirmationDialog(props: ConfirmationDialogProps) {
confirmButtonClassName,
confirmButtonColor,
confirmButtonVariant,
confirmButtonDisabled,
onConfirm,
onCancel,
hasCancelButton,
Expand All @@ -75,6 +76,7 @@ export function ConfirmationDialog(props: ConfirmationDialogProps) {
confirmButtonClassName={confirmButtonClassName}
confirmButtonColor={confirmButtonColor}
confirmButtonVariant={confirmButtonVariant}
confirmButtonDisabled={confirmButtonDisabled}
onConfirm={onConfirm}
onCancel={onCancel}
hasCancelButton={hasCancelButton}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ThisTableIsEmpty from './SynapseTable/TableIsEmpty'
import { unCamelCase } from '../utils/functions/unCamelCase'
import { ColumnType, Row } from '@sage-bionetworks/synapse-types'
import { getDisplayValue } from '../utils/functions/getDataFromFromStorage'
import { isFileViewOrDataset } from './SynapseTable/SynapseTableUtils'

export type QueryVisualizationContextType = {
topLevelControlsState: TopLevelControlsState
Expand All @@ -39,6 +40,10 @@ export type QueryVisualizationContextType = {
setIsShowingExportToCavaticaModal: React.Dispatch<
React.SetStateAction<boolean>
>
/** The set of columns that defines a uniqueness constraint on the table for the purposes of filtering based on row selection.
* Note that Synapse tables have no internal concept of a primary key.
*/
rowSelectionPrimaryKey?: string[]
}

/**
Expand Down Expand Up @@ -94,6 +99,10 @@ export type QueryVisualizationWrapperProps = {
/** Default is INTERACTIVE */
noContentPlaceholderType?: NoContentPlaceholderType
isRowSelectionVisible?: boolean
/** The set of columns that defines a uniqueness constraint on the table for the purposes of filtering based on row selection.
* Note that Synapse tables have no internal concept of a primary key.
*/
rowSelectionPrimaryKey?: string[]
}

export type TopLevelControlsState = {
Expand All @@ -117,12 +126,22 @@ export function QueryVisualizationWrapper(
const {
noContentPlaceholderType = NoContentPlaceholderType.INTERACTIVE,
isRowSelectionVisible = false,
columnAliases = {},
} = props

const { data, getLastQueryRequest, isFacetsAvailable, hasResettableFilters } =
useQueryContext()

const { columnAliases = {} } = props
const {
data,
entity,
getLastQueryRequest,
isFacetsAvailable,
hasResettableFilters,
} = useQueryContext()

let { rowSelectionPrimaryKey } = props
if (!rowSelectionPrimaryKey && isFileViewOrDataset(entity)) {
// If the primary key isn't specified on a file view/dataset, we can safely use the 'id' column
rowSelectionPrimaryKey = ['id']
}

const [topLevelControlsState, setTopLevelControlsState] =
useState<TopLevelControlsState>({
Expand Down Expand Up @@ -212,6 +231,7 @@ export function QueryVisualizationWrapper(
isRowSelectionVisible,
isShowingExportToCavaticaModal,
setIsShowingExportToCavaticaModal,
rowSelectionPrimaryKey,
}
/**
* Render the children without any formatting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import FacetFilterControls, {
import FilterAndView from './FilterAndView'
import { NoContentPlaceholderType } from '../SynapseTable/NoContentPlaceholderType'
import { Box } from '@mui/material'
import { SynapseErrorBoundary } from '../error/ErrorBanner'

type QueryWrapperPlotNavOwnProps = {
sql: string
Expand Down Expand Up @@ -76,8 +77,9 @@ type QueryWrapperPlotNavOwnProps = {
| 'rgbIndex'
| 'showLastUpdatedOn'
| 'noContentPlaceholderType'
| 'isRowSelectionVisible',
'unitDescription'
| 'isRowSelectionVisible'
| 'unitDescription'
| 'rowSelectionPrimaryKey'
>

export type SearchParams = {
Expand Down Expand Up @@ -119,6 +121,7 @@ const QueryWrapperPlotNav: React.FunctionComponent<QueryWrapperPlotNavProps> = (
customControls,
isRowSelectionVisible,
unitDescription,
rowSelectionPrimaryKey,
} = props

const entityId = parseEntityIdFromSqlStatement(sql)
Expand Down Expand Up @@ -162,6 +165,7 @@ const QueryWrapperPlotNav: React.FunctionComponent<QueryWrapperPlotNavProps> = (
<QueryWrapper {...props} initQueryRequest={initQueryRequest}>
<QueryVisualizationWrapper
unitDescription={unitDescription}
rowSelectionPrimaryKey={rowSelectionPrimaryKey}
rgbIndex={props.rgbIndex}
columnAliases={props.columnAliases}
visibleColumnCount={props.visibleColumnCount}
Expand Down Expand Up @@ -231,18 +235,20 @@ const QueryWrapperPlotNav: React.FunctionComponent<QueryWrapperPlotNavProps> = (
queryVisualizationContext.setTopLevelControlsState
}
/>
<TopLevelControls
showColumnSelection={tableConfiguration !== undefined}
name={name}
hideDownload={hideDownload}
hideQueryCount={hideQueryCount}
hideFacetFilterControl={!isFaceted}
hideVisualizationsControl={!isFaceted}
hideSqlEditorControl={hideSqlEditorControl}
showExportToCavatica={showExportToCavatica}
cavaticaHelpURL={cavaticaHelpURL}
customControls={customControls}
/>
<SynapseErrorBoundary>
<TopLevelControls
showColumnSelection={tableConfiguration !== undefined}
name={name}
hideDownload={hideDownload}
hideQueryCount={hideQueryCount}
hideFacetFilterControl={!isFaceted}
hideVisualizationsControl={!isFaceted}
hideSqlEditorControl={hideSqlEditorControl}
showExportToCavatica={showExportToCavatica}
cavaticaHelpURL={cavaticaHelpURL}
customControls={customControls}
/>
</SynapseErrorBoundary>
{isFaceted && (
<>
<FacetFilterControls
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Badge, Box, Button, Paper, Typography } from '@mui/material'
import { Box, Button, Paper, Typography } from '@mui/material'
import InlineBadge from '../../styled/InlineBadge'

export type RowSelectionUIProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import React, { useMemo } from 'react'
import { ConfirmationDialog } from '../ConfirmationDialog'
import { Box, Link, Typography } from '@mui/material'
import { Box, Link, Stack, Typography } from '@mui/material'
import { ActionRequiredListItem } from '../DownloadCart/ActionRequiredListItem'
import {
ActionRequiredCount,
ColumnModel,
ColumnSingleValueFilterOperator,
ColumnSingleValueQueryFilter,
} from '@sage-bionetworks/synapse-types'
import { useQueryContext } from '../QueryContext'
import { SynapseConstants } from '../../utils'
import { useGetQueryResultBundleWithAsyncStatus } from '../../synapse-queries'
import { SkeletonParagraph } from '../Skeleton'
import { useExportToCavatica } from '../../synapse-queries/entity/useExportToCavatica'
import { useQueryVisualizationContext } from '../QueryVisualizationWrapper'
import { cloneDeep } from 'lodash-es'
import { getNumberOfResultsToInvokeActionCopy } from './TopLevelControls/TopLevelControlsUtils'
import { getFileColumnModelId } from './SynapseTableUtils'
import { useGetActionsRequiredForTableQuery } from '../../synapse-queries/entity/useActionsRequiredForTableQuery'

export type SendToCavaticaConfirmationDialogProps = {
cavaticaHelpURL?: string
Expand All @@ -37,6 +35,7 @@ export default function SendToCavaticaConfirmationDialog(
isRowSelectionVisible,
selectedRows,
unitDescription,
rowSelectionPrimaryKey,
} = useQueryVisualizationContext()

const hasSelectedRows = isRowSelectionVisible && selectedRows.length > 0
Expand All @@ -46,12 +45,18 @@ export default function SendToCavaticaConfirmationDialog(
if (!hasSelectedRows) {
return request
} else {
if (!rowSelectionPrimaryKey || rowSelectionPrimaryKey.length !== 1) {
// TODO: Handle composite/undefined key
throw new Error(
'rowSelectionPrimaryKey must be defined and have length 1',
)
}
// Add a filter that will just return the selected rows.
const idColIndex = data?.columnModels?.findIndex(cm => cm.name === 'id')
const idColumnFilter: ColumnSingleValueQueryFilter = {
concreteType:
'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter',
columnName: 'id',
columnName: rowSelectionPrimaryKey[0],
operator: ColumnSingleValueFilterOperator.IN,
values: selectedRows!.map(row => row.values[idColIndex!]!),
}
Expand All @@ -67,25 +72,16 @@ export default function SendToCavaticaConfirmationDialog(
cavaticaQueryRequest,
data?.queryResult?.queryResults.headers,
)
const queryRequestCopy = useMemo(() => {
const request = cloneDeep(cavaticaQueryRequest)
const fileColumnId = getFileColumnModelId(data?.columnModels)
if (fileColumnId) {
request.query.selectFileColumn = Number(fileColumnId)
}
request.partMask = SynapseConstants.BUNDLE_MASK_ACTIONS_REQUIRED
return request
}, [cavaticaQueryRequest, data?.columnModels])

const { data: asyncJobStatus, isLoading } =
useGetQueryResultBundleWithAsyncStatus(queryRequestCopy, {
enabled: fileColumnId !== undefined,
const { data: actions, isLoading } = useGetActionsRequiredForTableQuery(
cavaticaQueryRequest,
data?.columnModels as ColumnModel[],
undefined,
{
useErrorBoundary: true,
})

const queryResultBundle = asyncJobStatus?.responseBody
const actions: ActionRequiredCount[] | undefined =
queryResultBundle?.actionsRequired
enabled: !!data?.columnModels,
},
)

const confirmButtonText = `Send ${getNumberOfResultsToInvokeActionCopy(
hasResettableFilters,
Expand Down Expand Up @@ -168,7 +164,7 @@ export default function SendToCavaticaConfirmationDialog(
<>
<Typography
variant="body1"
sx={{ fontWeight: 700, marginBottom: '10px' }}
sx={{ fontWeight: 700, my: '10px' }}
>
You must also take these actions before sending the selected
data to CAVATICA:
Expand All @@ -183,7 +179,7 @@ export default function SendToCavaticaConfirmationDialog(
You must take the following actions before we can send this
data to CAVATICA.
</Typography>
<Box>
<Stack gap={3}>
{actions.map((item: ActionRequiredCount, index) => {
if (item) {
return (
Expand All @@ -198,7 +194,7 @@ export default function SendToCavaticaConfirmationDialog(
)
} else return false
})}
</Box>
</Stack>
</>
)
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import React from 'react'
import { DialogBase } from '../DialogBase'
import SynapseClient from '../../synapse-client'
import {
hasFilesInView,
isDataset,
isDatasetCollection,
isEntityView,
Expand Down Expand Up @@ -48,6 +47,7 @@ import { ICON_STATE } from './SynapseTableConstants'
import {
getColumnIndicesWithType,
getUniqueEntities,
isFileViewOrDataset,
} from './SynapseTableUtils'
import { TablePagination } from './TablePagination'
import EntityIDColumnCopyIcon from './EntityIDColumnCopyIcon'
Expand Down Expand Up @@ -426,15 +426,6 @@ export class SynapseTable extends React.Component<
showDownloadColumn,
} = this.props

/**
* i.e. the view may have FileEntities in it
*
* PORTALS-2010: Enhance change made for PORTALS-1973. File specific action will only be shown for rows that represent FileEntities.
*/
const isFileViewOrDataset =
entity &&
((isEntityView(entity) && hasFilesInView(entity)) || isDataset(entity))

const isShowingAccessColumn: boolean | undefined =
showAccessColumn &&
entity &&
Expand All @@ -443,7 +434,10 @@ export class SynapseTable extends React.Component<
const isLoggedIn = !!this.props.synapseContext.accessToken

const rowsAreDownloadable =
entity && isFileViewOrDataset && isLoggedIn && this.allRowsHaveId()
entity &&
isFileViewOrDataset(entity) &&
isLoggedIn &&
this.allRowsHaveId()

const isShowingAddToV2DownloadListColumn: boolean = !!(
rowsAreDownloadable && !this.props.hideDownload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import {
ColumnTypeEnum,
EntityHeader,
QueryResultBundle,
Table,
UserGroupHeader,
UserProfile,
} from '@sage-bionetworks/synapse-types'
import {
hasFilesInView,
isDataset,
isEntityView,
} from '../../utils/functions/EntityTypeUtils'

export const getColumnIndicesWithType = (
data: QueryResultBundle | undefined,
Expand Down Expand Up @@ -46,6 +52,18 @@ export const getUniqueEntities = (
return distinctEntities
}

/**
* i.e. the view may have FileEntities in it
*
* PORTALS-2010: Enhance change made for PORTALS-1973. File specific action will only be shown for rows that represent FileEntities.
*/
export function isFileViewOrDataset(entity?: Table) {
return (
entity &&
((isEntityView(entity) && hasFilesInView(entity)) || isDataset(entity))
)
}

export const getFileColumnModelId = (
columnModels?: ColumnModel[],
): string | undefined => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
ActionRequiredCount,
AsynchronousJobStatus,
ColumnModel,
QueryBundleRequest,
QueryResultBundle,
} from '@sage-bionetworks/synapse-types'
import { useMemo } from 'react'
import { cloneDeep } from 'lodash-es'
import { getFileColumnModelId } from '../../components/SynapseTable/SynapseTableUtils'
import { SynapseConstants } from '../../utils'
import { useGetQueryResultBundleWithAsyncStatus } from './useGetQueryResultBundle'
import { UseQueryOptions } from 'react-query'
import { SynapseClientError } from '../../utils/SynapseClientError'

export function useGetActionsRequiredForTableQuery(
queryBundleRequest: QueryBundleRequest,
columnModels: ColumnModel[],
fileColumnModelId?: number,
options?: UseQueryOptions<
AsynchronousJobStatus<QueryBundleRequest, QueryResultBundle>,
SynapseClientError,
ActionRequiredCount[]
>,
) {
const queryRequestCopy = useMemo(() => {
const request = cloneDeep(queryBundleRequest)
const fileColumnId = fileColumnModelId || getFileColumnModelId(columnModels)
request.query.selectFileColumn = Number(fileColumnId)
request.partMask = SynapseConstants.BUNDLE_MASK_ACTIONS_REQUIRED
return request
}, [columnModels, fileColumnModelId, queryBundleRequest])

return useGetQueryResultBundleWithAsyncStatus<ActionRequiredCount[]>(
queryRequestCopy,
{
...options,
enabled:
(options?.enabled ?? true) &&
queryRequestCopy.query.selectFileColumn !== undefined,
select: data => {
return data?.responseBody?.actionsRequired!
},
},
)
}
Loading

0 comments on commit b15514e

Please sign in to comment.