Skip to content

Commit 95f1b94

Browse files
authored
Add document link downloader to Document Library (#236)
* Add download link in document library UI
1 parent 4b687bd commit 95f1b94

File tree

10 files changed

+115
-16
lines changed

10 files changed

+115
-16
lines changed

jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ module.exports = {
2323
'^.+\\.tsx?$': 'ts-jest'
2424
},
2525
collectCoverage: true,
26+
silent: true,
2627
};

lambda/repository/lambda_functions.py

+34
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,40 @@ def ingest_documents(event: dict, context: dict) -> dict:
505505
}
506506

507507

508+
@api_wrapper
509+
def download_document(event: dict, context: dict) -> str:
510+
"""Generate a pre-signed S3 URL for downloading a file from the RAG ingested files.
511+
Args:
512+
event (dict): The Lambda event object containing:
513+
path_params:
514+
repositoryId - the repository
515+
documentId - the document
516+
517+
Returns:
518+
url: The presigned URL response object with download fields and URL
519+
520+
Notes:
521+
- URL expires in 300 seconds (5 mins)
522+
"""
523+
path_params = event.get("pathParameters", {}) or {}
524+
repository_id = path_params.get("repositoryId")
525+
document_id = path_params.get("documentId")
526+
527+
ensure_repository_access(event, find_repository_by_id(repository_id))
528+
doc = doc_repo.find_by_id(repository_id=repository_id, document_id=document_id)
529+
530+
source = doc.get("source")
531+
bucket, key = source.replace("s3://", "").split("/", 1)
532+
533+
url: str = s3.generate_presigned_url(
534+
ClientMethod="get_object",
535+
Params={"Bucket": bucket, "Key": key},
536+
ExpiresIn=300,
537+
)
538+
539+
return url
540+
541+
508542
@api_wrapper
509543
def presigned_url(event: dict, context: dict) -> dict:
510544
"""Generate a pre-signed URL for uploading files to the RAG ingest bucket.

lib/networking/vpc/security-group-factory.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class SecurityGroupFactory {
4646
description: string,
4747
): ISecurityGroup {
4848
if (securityGroupOverride) {
49-
console.log(`Security Role Override provided. Using ${securityGroupOverride} for ${securityGroupId}`);
49+
console.debug(`Security Role Override provided. Using ${securityGroupOverride} for ${securityGroupId}`);
5050
const sg = SecurityGroup.fromSecurityGroupId(construct, securityGroupId, securityGroupOverride);
5151
// Validate the security group exists
5252
if (!sg) {

lib/rag/api/repository.ts

+10
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ export class RepositoryApi extends Construct {
134134
...baseEnvironment,
135135
},
136136
},
137+
{
138+
name: 'download_document',
139+
resource: 'repository',
140+
description: 'Creates presigned url to download document within repository',
141+
path: 'repository/{repositoryId}/{documentId}/download',
142+
method: 'GET',
143+
environment: {
144+
...baseEnvironment,
145+
},
146+
},
137147
];
138148
apis.forEach((f) => {
139149
registerAPIEndpoint(

lib/rag/index.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -363,14 +363,14 @@ export class LisaRagStack extends Stack {
363363
}
364364

365365
// Create ingest pipeline state machines for each pipeline config
366-
console.log('[DEBUG] Checking pipelines configuration:', {
366+
console.debug('Checking pipelines configuration:', {
367367
hasPipelines: !!ragConfig.pipelines,
368368
pipelinesLength: ragConfig.pipelines?.length || 0
369369
});
370370

371371
if (ragConfig.pipelines) {
372372
ragConfig.pipelines.forEach((pipelineConfig, index) => {
373-
console.log(`[DEBUG] Creating pipeline ${index}:`, {
373+
console.debug(`Creating pipeline ${index}:`, {
374374
pipelineConfig: JSON.stringify(pipelineConfig, null, 2)
375375
});
376376

@@ -420,9 +420,9 @@ export class LisaRagStack extends Stack {
420420
});
421421
policy.attachToRole(lambdaRole);
422422
}
423-
console.log(`[DEBUG] Successfully created pipeline ${index}`);
423+
console.debug(`Successfully created pipeline ${index}`);
424424
} catch (error) {
425-
console.error(`[ERROR] Failed to create pipeline ${index}:`, error);
425+
console.error(`Failed to create pipeline ${index}:`, error);
426426
throw error; // Re-throw to ensure CDK deployment fails
427427
}
428428
});

lib/user-interface/react/src/components/document-library/DocumentLibraryComponent.tsx

+23-7
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import {
2424
TextFilter,
2525
} from '@cloudscape-design/components';
2626
import SpaceBetween from '@cloudscape-design/components/space-between';
27-
import { useDeleteRagDocumentsMutation, useListRagDocumentsQuery } from '../../shared/reducers/rag.reducer';
27+
import {
28+
useDeleteRagDocumentsMutation,
29+
useLazyDownloadRagDocumentQuery,
30+
useListRagDocumentsQuery,
31+
} from '../../shared/reducers/rag.reducer';
2832
import Table from '@cloudscape-design/components/table';
2933
import ButtonDropdown from '@cloudscape-design/components/button-dropdown';
3034
import { DEFAULT_PREFERENCES, PAGE_SIZE_OPTIONS, TABLE_DEFINITION, TABLE_PREFERENCES } from './DocumentLibraryConfig';
@@ -35,6 +39,7 @@ import { selectCurrentUserIsAdmin, selectCurrentUsername } from '../../shared/re
3539
import { RagDocument } from '../types';
3640
import { setConfirmationModal } from '../../shared/reducers/modal.reducer';
3741
import { useLocalStorage } from '../../shared/hooks/use-local-storage';
42+
import { downloadFile } from '../../shared/util/downloader';
3843

3944
type DocumentLibraryComponentProps = {
4045
repositoryId?: string;
@@ -54,9 +59,8 @@ function disabledDeleteReason (selectedItems: ReadonlyArray<RagDocument>) {
5459

5560
export function DocumentLibraryComponent ({ repositoryId }: DocumentLibraryComponentProps): ReactElement {
5661
const { data: allDocs, isFetching } = useListRagDocumentsQuery({ repositoryId }, { refetchOnMountOrArgChange: 5 });
57-
const [deleteMutation, {
58-
isLoading: isDeleteLoading,
59-
}] = useDeleteRagDocumentsMutation();
62+
const [deleteMutation, { isLoading: isDeleteLoading }] = useDeleteRagDocumentsMutation();
63+
6064
const currentUser = useAppSelector(selectCurrentUsername);
6165
const isAdmin = useAppSelector(selectCurrentUserIsAdmin);
6266
const [preferences, setPreferences] = useLocalStorage('DocumentRagPreferences', DEFAULT_PREFERENCES);
@@ -84,13 +88,18 @@ export function DocumentLibraryComponent ({ repositoryId }: DocumentLibraryCompo
8488
selection: { trackBy: 'document_id' },
8589
},
8690
);
87-
91+
const [getDownloadUrl, { isFetching: isDownloading }] = useLazyDownloadRagDocumentQuery();
8892
const actionItems: ButtonDropdownProps.Item[] = [
8993
{
9094
id: 'rm',
9195
text: 'Delete',
9296
disabled: !canDeleteAll(collectionProps.selectedItems, currentUser, isAdmin),
9397
disabledReason: disabledDeleteReason(collectionProps.selectedItems),
98+
}, {
99+
id: 'download',
100+
text: 'Download',
101+
disabled: collectionProps.selectedItems.length > 1,
102+
disabledReason: 'Only one file can be downloaded at a time',
94103
},
95104
];
96105
const handleAction = async (e: any) => {
@@ -109,6 +118,12 @@ export function DocumentLibraryComponent ({ repositoryId }: DocumentLibraryCompo
109118
);
110119
break;
111120
}
121+
case 'download': {
122+
const { document_id, document_name } = collectionProps.selectedItems[0];
123+
const resp = await getDownloadUrl({ documentId: document_id, repositoryId });
124+
downloadFile(resp.data, document_name);
125+
break;
126+
}
112127
default:
113128
console.error('Action not implemented', e.detail.id);
114129
}
@@ -150,8 +165,9 @@ export function DocumentLibraryComponent ({ repositoryId }: DocumentLibraryCompo
150165
>
151166
<ButtonDropdown
152167
items={actionItems}
153-
loading={isDeleteLoading}
154-
onItemClick={(e) => handleAction(e)}
168+
loading={isDeleteLoading || isDownloading}
169+
disabled={collectionProps.selectedItems.length === 0}
170+
onItemClick={async (e) => handleAction(e)}
155171
>
156172
Actions
157173
</ButtonDropdown>

lib/user-interface/react/src/pages/Home.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export function Home ({ setNav }) {
5858
}
5959
>
6060
{visible && <Alert type='error'>You must sign in to access this page!</Alert>}
61-
<Box float='center'>
62-
<div align='center'>
61+
<Box>
62+
<div>
6363
<figure>
6464
<img
6565
src={chatImg}

lib/user-interface/react/src/shared/reducers/rag.reducer.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ export const ragApi = createApi({
135135
},
136136
invalidatesTags: ['Docs'],
137137
}),
138+
downloadRagDocument: builder.query<string, { documentId: string, repositoryId: string }>({
139+
query: ({ documentId, repositoryId }) => ({
140+
url: `/repository/${repositoryId}/${documentId}/download`,
141+
method: 'GET',
142+
}),
143+
}),
138144
}),
139145
});
140146

@@ -145,5 +151,6 @@ export const {
145151
useIngestDocumentsMutation,
146152
useListRagDocumentsQuery,
147153
useDeleteRagDocumentsMutation,
148-
useLazyGetRelevantDocumentsQuery
154+
useLazyGetRelevantDocumentsQuery,
155+
useLazyDownloadRagDocumentQuery,
149156
} = ragApi;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License").
5+
You may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/**
18+
* Download a file using hidden link
19+
* @param url
20+
* @param filename
21+
*/
22+
export function downloadFile (url: string, filename: string) {
23+
const link = document.createElement('a');
24+
link.href = url;
25+
link.download = filename || 'download';
26+
link.hidden = true;
27+
28+
document.body.appendChild(link);
29+
link.click();
30+
document.body.removeChild(link);
31+
}

test/cdk/stacks/nag.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const nagResults: NagResult = {
3939
LisaIAM: [0,13,0,0],
4040
LisaModels: [1,74,0,28],
4141
LisaNetworking: [1,2,3,5],
42-
LisaRAG: [2,41,0,38],
42+
LisaRAG: [2,43,0,38],
4343
LisaServe: [1,21,0,31],
4444
LisaUI: [0,16,0,8],
4545
};

0 commit comments

Comments
 (0)