@@ -10,6 +10,7 @@ import {
1010 Table ,
1111 StatusIndicator ,
1212 Button ,
13+ ButtonDropdown ,
1314 Header ,
1415 FormField ,
1516 Select ,
@@ -19,14 +20,17 @@ import {
1920 Alert ,
2021} from '@cloudscape-design/components' ;
2122import { generateClient } from 'aws-amplify/api' ;
23+ import { ConsoleLogger } from 'aws-amplify/utils' ;
2224
2325import FileViewer from '../document-viewer/JSONViewer' ;
2426import { getSectionConfidenceAlertCount , getSectionConfidenceAlerts } from '../common/confidence-alerts-utils' ;
2527import useConfiguration from '../../hooks/use-configuration' ;
2628import useSettingsContext from '../../contexts/settings' ;
2729import processChanges from '../../graphql/queries/processChanges' ;
30+ import getFileContents from '../../graphql/queries/getFileContents' ;
2831
2932const client = generateClient ( ) ;
33+ const logger = new ConsoleLogger ( 'SectionsPanel' ) ;
3034
3135// Cell renderer components
3236const IdCell = ( { item } ) => < span > { item . Id } </ span > ;
@@ -51,14 +55,166 @@ const ConfidenceAlertsCell = ({ item, mergedConfig }) => {
5155 return < StatusIndicator type = "warning" > { alertCount } </ StatusIndicator > ;
5256} ;
5357
54- const ActionsCell = ( { item, pages, documentItem, mergedConfig } ) => (
55- < FileViewer
56- fileUri = { item . OutputJSONUri }
57- fileType = "json"
58- buttonText = "View/Edit Data"
59- sectionData = { { ...item , pages, documentItem, mergedConfig } }
60- />
61- ) ;
58+ const ActionsCell = ( { item, pages, documentItem, mergedConfig } ) => {
59+ const [ isDownloading , setIsDownloading ] = React . useState ( false ) ;
60+ const [ isViewerOpen , setIsViewerOpen ] = React . useState ( false ) ;
61+ const { settings } = useSettingsContext ( ) ;
62+
63+ // Check if baseline is available based on evaluation status
64+ const isBaselineAvailable = documentItem ?. evaluationStatus === 'BASELINE_AVAILABLE' || documentItem ?. evaluationStatus === 'COMPLETED' ;
65+
66+ // Construct baseline URI by replacing output bucket with evaluation baseline bucket
67+ const constructBaselineUri = ( outputUri ) => {
68+ if ( ! outputUri ) return null ;
69+
70+ // Get actual bucket names from settings
71+ const outputBucketName = settings ?. OutputBucket ;
72+ const baselineBucketName = settings ?. EvaluationBaselineBucket ;
73+
74+ if ( ! outputBucketName || ! baselineBucketName ) {
75+ logger . error ( 'Bucket names not available in settings' ) ;
76+ logger . debug ( 'Settings:' , settings ) ;
77+ return null ;
78+ }
79+
80+ // Parse the S3 URI to extract bucket and key
81+ // Format: s3://bucket-name/path/to/file
82+ const match = outputUri . match ( / ^ s 3 : \/ \/ ( [ ^ / ] + ) \/ ( .+ ) $ / ) ;
83+ if ( ! match ) {
84+ logger . error ( 'Invalid S3 URI format:' , outputUri ) ;
85+ return null ;
86+ }
87+
88+ const [ , bucketName , objectKey ] = match ;
89+
90+ // Verify this is actually the output bucket before replacing
91+ if ( bucketName !== outputBucketName ) {
92+ logger . warn ( `URI bucket (${ bucketName } ) does not match expected output bucket (${ outputBucketName } )` ) ;
93+ }
94+
95+ // Replace the output bucket with the baseline bucket (same object key)
96+ const baselineUri = `s3://${ baselineBucketName } /${ objectKey } ` ;
97+
98+ logger . info ( `Converted output URI to baseline URI:` ) ;
99+ logger . info ( ` Output: ${ outputUri } ` ) ;
100+ logger . info ( ` Baseline: ${ baselineUri } ` ) ;
101+
102+ return baselineUri ;
103+ } ;
104+
105+ // Generate download filename
106+ const generateFilename = ( documentKey , sectionId , type ) => {
107+ // Sanitize document key by replacing forward slashes with underscores
108+ const sanitizedDocId = documentKey . replace ( / \/ / g, '_' ) ;
109+ return `${ sanitizedDocId } _section${ sectionId } _${ type } .json` ;
110+ } ;
111+
112+ // Download handler for both prediction and baseline data
113+ const handleDownload = async ( type ) => {
114+ setIsDownloading ( true ) ;
115+
116+ try {
117+ const fileUri = type === 'baseline' ? constructBaselineUri ( item . OutputJSONUri ) : item . OutputJSONUri ;
118+
119+ if ( ! fileUri ) {
120+ alert ( 'File URI not available' ) ;
121+ return ;
122+ }
123+
124+ logger . info ( `Downloading ${ type } data from:` , fileUri ) ;
125+
126+ // Fetch file contents using GraphQL
127+ const response = await client . graphql ( {
128+ query : getFileContents ,
129+ variables : { s3Uri : fileUri } ,
130+ } ) ;
131+
132+ const result = response . data . getFileContents ;
133+
134+ if ( result . isBinary ) {
135+ alert ( 'This file contains binary content that cannot be downloaded' ) ;
136+ return ;
137+ }
138+
139+ const content = result . content ;
140+
141+ // Create blob and download
142+ const blob = new Blob ( [ content ] , { type : 'application/json' } ) ;
143+ const url = URL . createObjectURL ( blob ) ;
144+ const link = document . createElement ( 'a' ) ;
145+
146+ // Generate filename
147+ const documentKey = documentItem ?. objectKey || documentItem ?. ObjectKey || 'document' ;
148+ const filename = generateFilename ( documentKey , item . Id , type ) ;
149+
150+ link . href = url ;
151+ link . download = filename ;
152+ document . body . appendChild ( link ) ;
153+ link . click ( ) ;
154+ document . body . removeChild ( link ) ;
155+ URL . revokeObjectURL ( url ) ;
156+
157+ logger . info ( `Successfully downloaded ${ type } data as ${ filename } ` ) ;
158+ } catch ( error ) {
159+ logger . error ( `Error downloading ${ type } data:` , error ) ;
160+
161+ let errorMessage = `Failed to download ${ type } data` ;
162+
163+ if ( type === 'baseline' && error . message ?. includes ( 'not found' ) ) {
164+ errorMessage = 'Baseline data not found. The baseline may not have been set for this document yet.' ;
165+ } else if ( error . message ) {
166+ errorMessage = `Failed to download ${ type } data: ${ error . message } ` ;
167+ }
168+
169+ alert ( errorMessage ) ;
170+ } finally {
171+ setIsDownloading ( false ) ;
172+ }
173+ } ;
174+
175+ // Build dropdown menu items
176+ const downloadMenuItems = [
177+ {
178+ id : 'prediction' ,
179+ text : 'Download Data' ,
180+ iconName : 'download' ,
181+ } ,
182+ ] ;
183+
184+ // Add baseline option if available
185+ if ( isBaselineAvailable ) {
186+ downloadMenuItems . push ( {
187+ id : 'baseline' ,
188+ text : 'Download Baseline' ,
189+ iconName : 'download' ,
190+ } ) ;
191+ }
192+
193+ return (
194+ < SpaceBetween direction = "horizontal" size = "xs" >
195+ < FileViewer
196+ fileUri = { item . OutputJSONUri }
197+ fileType = "json"
198+ buttonText = "View/Edit Data"
199+ sectionData = { { ...item , pages, documentItem, mergedConfig } }
200+ onOpen = { ( ) => setIsViewerOpen ( true ) }
201+ onClose = { ( ) => setIsViewerOpen ( false ) }
202+ />
203+ { ! isViewerOpen && (
204+ < ButtonDropdown
205+ items = { downloadMenuItems }
206+ onItemClick = { ( { detail } ) => handleDownload ( detail . id ) }
207+ disabled = { isDownloading }
208+ loading = { isDownloading }
209+ variant = "normal"
210+ expandToViewport
211+ >
212+ Download
213+ </ ButtonDropdown >
214+ ) }
215+ </ SpaceBetween >
216+ ) ;
217+ } ;
62218
63219// Editable cell components for edit mode (moved outside render)
64220const EditableIdCell = ( { item, validationErrors, updateSectionId } ) => (
0 commit comments