Skip to content

Commit 5ab063f

Browse files
feat(files) Add combined commits together for file upload/download (#15170)
1 parent 0203ab6 commit 5ab063f

File tree

14 files changed

+410
-188
lines changed

14 files changed

+410
-188
lines changed

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/files/GetPresignedUploadUrlResolver.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.linkedin.datahub.graphql.generated.GetPresignedUploadUrlResponse;
1111
import com.linkedin.datahub.graphql.generated.UploadDownloadScenario;
1212
import com.linkedin.datahub.graphql.resolvers.mutate.DescriptionUtils;
13+
import com.linkedin.metadata.Constants;
1314
import com.linkedin.metadata.config.S3Configuration;
1415
import com.linkedin.metadata.utils.aws.S3Util;
1516
import graphql.schema.DataFetcher;
@@ -107,7 +108,9 @@ private void validateInputForAssetDocumentationScenario(
107108
}
108109

109110
private String generateNewFileId(final GetPresignedUploadUrlInput input) {
110-
return String.format("%s-%s", UUID.randomUUID().toString(), input.getFileName());
111+
return String.format(
112+
"%s%s%s",
113+
UUID.randomUUID().toString(), Constants.S3_FILE_ID_NAME_SEPARATOR, input.getFileName());
111114
}
112115

113116
private String getS3Key(

datahub-web-react/src/alchemy-components/components/Editor/extensions/fileDragDrop/fileUtils.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export const SUPPORTED_FILE_TYPES = [
3333
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
3434
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
3535
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
36-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
3736
'application/vnd.ms-excel',
3837
'application/xml',
3938
'application/vnd.ms-powerpoint',
@@ -45,6 +44,20 @@ export const SUPPORTED_FILE_TYPES = [
4544
'audio/mpeg',
4645
'video/x-ms-wmv',
4746
'image/tiff',
47+
'text/x-python-script',
48+
'application/json',
49+
'text/html',
50+
'text/x-java-source',
51+
'image/svg+xml',
52+
'application/vnd.oasis.opendocument.text',
53+
'application/vnd.oasis.opendocument.spreadsheet',
54+
'application/vnd.oasis.opendocument.presentation',
55+
'text/css',
56+
'application/javascript',
57+
'text/x-yaml',
58+
'application/x-tar',
59+
'text/x-sql',
60+
'application/x-sh',
4861
];
4962

5063
const EXTENSION_TO_FILE_TYPE = {
@@ -73,6 +86,23 @@ const EXTENSION_TO_FILE_TYPE = {
7386
tiff: 'image/tiff',
7487
md: 'text/markdown',
7588
csv: 'text/csv',
89+
py: 'text/x-python-script',
90+
json: 'application/json',
91+
html: 'text/html',
92+
java: 'text/x-java-source',
93+
svg: 'image/svg+xml',
94+
log: 'text/plain',
95+
mov: 'video/quicktime',
96+
odt: 'application/vnd.oasis.opendocument.text',
97+
ods: 'application/vnd.oasis.opendocument.spreadsheet',
98+
odp: 'application/vnd.oasis.opendocument.presentation',
99+
css: 'text/css',
100+
js: 'application/javascript',
101+
yaml: 'text/x-yaml',
102+
yml: 'text/x-yaml',
103+
tar: 'application/x-tar',
104+
sql: 'text/x-sql',
105+
sh: 'application/x-sh',
76106
};
77107

78108
/**

datahub-web-react/src/alchemy-components/components/Editor/extensions/htmlToMarkdown.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,24 @@ const turndownService = new TurndownService({
6868
replacement: (content, node: any) =>
6969
node?.previousElementSibling?.nodeName === 'P' ? `${br}${content}` : content,
7070
})
71-
/* Keep HTML format if image has an explicit width attribute */
71+
/* Handle images with width attributes and wrap URLs in angle brackets for all images */
7272
.addRule('images', {
73-
filter: (node) => node.nodeName === 'IMG' && node.hasAttribute('width'),
74-
replacement: (_, node: any) => `${node.outerHTML}`,
73+
filter: (node) => node.nodeName === 'IMG',
74+
replacement: (test, node: any) => {
75+
// Keep HTML format if image has an explicit width attribute
76+
if (node.hasAttribute('width')) {
77+
return `${node.outerHTML}`;
78+
}
79+
80+
// For standard images, wrap URL in angle brackets to support spaces
81+
const src = node.getAttribute('src') || '';
82+
const alt = node.getAttribute('alt') || '';
83+
84+
// Wrap URL in angle brackets to support spaces and special characters in URLs
85+
const encodedUrl = `<${src}>`;
86+
87+
return `![${alt}](${encodedUrl})`;
88+
},
7589
})
7690
/* Add improved code block support from html (snippet from Remirror). */
7791
.addRule('fencedCodeBlock', {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Image } from '@phosphor-icons/react';
2+
import { useCommands } from '@remirror/react';
3+
import { Form } from 'antd';
4+
import { FormInstance } from 'antd/es/form/Form';
5+
import React, { useState } from 'react';
6+
import styled from 'styled-components';
7+
8+
import { Button } from '@components/components/Button';
9+
import { Dropdown } from '@components/components/Dropdown';
10+
import { CommandButton } from '@components/components/Editor/toolbar/CommandButton';
11+
import { FileUploadContent } from '@components/components/Editor/toolbar/FileUploadContent';
12+
import { Input } from '@components/components/Input';
13+
14+
import ButtonTabs from '@app/homeV3/modules/shared/ButtonTabs/ButtonTabs';
15+
import { colors } from '@src/alchemy-components/theme';
16+
17+
const UPLOAD_FILE_KEY = 'uploadFile';
18+
const URL_KEY = 'url';
19+
20+
const ContentWrapper = styled.div`
21+
width: 300px;
22+
background-color: ${colors.white};
23+
box-shadow: 0 4px 12px 0 rgba(9, 1, 61, 0.12);
24+
display: flex;
25+
flex-direction: column;
26+
padding: 8px;
27+
gap: 8px;
28+
border-radius: 12px;
29+
`;
30+
31+
const StyledButton = styled(Button)`
32+
width: 100%;
33+
display: flex;
34+
justify-content: center;
35+
`;
36+
37+
const FormItem = styled(Form.Item)`
38+
margin-bottom: 8px;
39+
`;
40+
41+
function ImageUrlInput({ form, hideDropdown }: { form: FormInstance<any>; hideDropdown: () => void }) {
42+
const { insertImage } = useCommands();
43+
44+
const handleOk = () => {
45+
form.validateFields()
46+
.then((values) => {
47+
form.resetFields();
48+
hideDropdown();
49+
insertImage(values);
50+
})
51+
.catch((info) => {
52+
console.log('Validate Failed:', info);
53+
});
54+
};
55+
56+
return (
57+
<Form form={form} layout="vertical" colon={false} requiredMark={false}>
58+
<FormItem name="src" rules={[{ required: true }]}>
59+
<Input label="Image URL" placeholder="http://www.example.com/image.jpg" autoFocus />
60+
</FormItem>
61+
<FormItem name="alt">
62+
<Input label="Alt Text" />
63+
</FormItem>
64+
<StyledButton onClick={handleOk}>Embed Image</StyledButton>
65+
</Form>
66+
);
67+
}
68+
69+
export const AddImageButtonV2 = () => {
70+
const [showDropdown, setShowDropdown] = useState(false);
71+
const [form] = Form.useForm();
72+
73+
const tabs = [
74+
{
75+
key: UPLOAD_FILE_KEY,
76+
label: 'Upload File',
77+
content: <FileUploadContent hideDropdown={() => setShowDropdown(false)} />,
78+
},
79+
{
80+
key: URL_KEY,
81+
label: 'URL',
82+
content: <ImageUrlInput form={form} hideDropdown={() => setShowDropdown(false)} />,
83+
},
84+
];
85+
86+
const handleButtonClick = () => {
87+
setShowDropdown(true);
88+
};
89+
90+
const dropdownContent = () => (
91+
<ContentWrapper>
92+
<ButtonTabs tabs={tabs} defaultKey={UPLOAD_FILE_KEY} />
93+
</ContentWrapper>
94+
);
95+
96+
return (
97+
<>
98+
<Dropdown
99+
open={showDropdown}
100+
onOpenChange={(open) => setShowDropdown(open)}
101+
dropdownRender={dropdownContent}
102+
>
103+
<CommandButton
104+
active={false}
105+
icon={<Image size={20} color={colors.gray[1800]} />}
106+
commandName="insertImage"
107+
onClick={handleButtonClick}
108+
/>
109+
</Dropdown>
110+
</>
111+
);
112+
};

datahub-web-react/src/alchemy-components/components/Editor/toolbar/FileUploadButton.tsx

Lines changed: 13 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import { Button, Dropdown, Text, Tooltip, colors, notification } from '@components';
1+
import { Dropdown, Tooltip, colors } from '@components';
22
import { useRemirrorContext } from '@remirror/react';
33
import { FileArrowUp } from 'phosphor-react';
4-
import React, { useRef, useState } from 'react';
4+
import React, { useState } from 'react';
55
import styled from 'styled-components';
66

7-
import {
8-
FileDragDropExtension,
9-
SUPPORTED_FILE_TYPES,
10-
createFileNodeAttributes,
11-
validateFile,
12-
} from '@components/components/Editor/extensions/fileDragDrop';
7+
import { FileDragDropExtension } from '@components/components/Editor/extensions/fileDragDrop';
138
import { CommandButton } from '@components/components/Editor/toolbar/CommandButton';
14-
import { FileUploadFailureType } from '@components/components/Editor/types';
9+
import { FileUploadContent } from '@components/components/Editor/toolbar/FileUploadContent';
1510

1611
const DropdownContainer = styled.div`
1712
box-shadow: 0 4px 12px 0 rgba(9, 1, 61, 0.12);
@@ -24,122 +19,25 @@ const DropdownContainer = styled.div`
2419
background: ${colors.white};
2520
`;
2621

27-
const StyledText = styled(Text)`
28-
text-align: center;
29-
`;
30-
31-
const StyledButton = styled(Button)`
32-
width: 100%;
33-
display: flex;
34-
justify-content: center;
35-
align-items: center;
36-
text-align: center;
37-
`;
38-
39-
const FileInput = styled.input`
40-
display: none;
41-
`;
42-
4322
export const FileUploadButton = () => {
44-
const { commands } = useRemirrorContext();
45-
46-
const fileInputRef = useRef<HTMLInputElement>(null);
4723
const remirrorContext = useRemirrorContext();
4824
const fileExtension = remirrorContext.getExtension(FileDragDropExtension);
4925

5026
const [showDropdown, setShowDropdown] = useState(false);
5127

52-
const handlebuttonClick = () => {
53-
fileInputRef.current?.click();
54-
};
55-
56-
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
57-
const input = event.target as HTMLInputElement;
58-
const files = input.files ? Array.from(input.files) : [];
59-
if (files.length === 0) return;
60-
61-
const supportedTypes = SUPPORTED_FILE_TYPES;
62-
const { onFileUpload, onFileUploadAttempt, onFileUploadFailed, onFileUploadSucceeded } = fileExtension.options;
63-
64-
try {
65-
// Process files concurrently
66-
await Promise.all(
67-
files.map(async (file) => {
68-
onFileUploadAttempt?.(file.type, file.size, 'button');
69-
70-
const validation = validateFile(file, { allowedTypes: supportedTypes });
71-
if (!validation.isValid) {
72-
console.error(validation.error);
73-
onFileUploadFailed?.(
74-
file.type,
75-
file.size,
76-
'button',
77-
validation.failureType || FileUploadFailureType.UNKNOWN,
78-
);
79-
notification.error({
80-
message: 'Upload Failed',
81-
description: validation.displayError || validation.error,
82-
});
83-
}
84-
85-
// Create placeholder node
86-
const attrs = createFileNodeAttributes(file);
87-
commands.insertFileNode({ ...attrs, url: '' });
88-
89-
// Upload file if handler exists
90-
if (onFileUpload) {
91-
try {
92-
const finalUrl = await onFileUpload(file);
93-
fileExtension.updateNodeWithUrl(remirrorContext.view, attrs.id, finalUrl);
94-
onFileUploadSucceeded?.(file.type, file.size, 'button');
95-
} catch (uploadError) {
96-
console.error(uploadError);
97-
onFileUploadFailed?.(
98-
file.type,
99-
file.size,
100-
'button',
101-
FileUploadFailureType.UNKNOWN,
102-
`${uploadError}`,
103-
);
104-
fileExtension.removeNode(remirrorContext.view, attrs.id);
105-
notification.error({
106-
message: 'Upload Failed',
107-
description: 'Something went wrong',
108-
});
109-
}
110-
}
111-
}),
112-
);
113-
} catch (error) {
114-
console.error(error);
115-
onFileUploadFailed?.(files[0].type, files[0].size, 'button', FileUploadFailureType.UNKNOWN, `${error}`);
116-
notification.error({
117-
message: 'Upload Failed',
118-
description: 'Something went wrong',
119-
});
120-
} finally {
121-
input.value = '';
122-
setShowDropdown(false);
123-
}
124-
};
125-
126-
const dropdownContent = () => (
127-
<DropdownContainer>
128-
<StyledButton size="sm" onClick={handlebuttonClick}>
129-
Choose File
130-
</StyledButton>
131-
<FileInput ref={fileInputRef} type="file" onChange={handleFileChange} />
132-
<StyledText color="gray" size="sm" lineHeight="normal">
133-
Max size: 2GB
134-
</StyledText>
135-
</DropdownContainer>
136-
);
137-
13828
// Hide the button when uploading of files is disabled
13929
if (!fileExtension.options.onFileUpload) return null;
14030

14131
return (
142-
<Dropdown open={showDropdown} onOpenChange={(open) => setShowDropdown(open)} dropdownRender={dropdownContent}>
32+
<Dropdown
33+
open={showDropdown}
34+
onOpenChange={(open) => setShowDropdown(open)}
35+
dropdownRender={() => (
36+
<DropdownContainer>
37+
<FileUploadContent hideDropdown={() => setShowDropdown(false)} />
38+
</DropdownContainer>
39+
)}
40+
>
14341
<Tooltip title="Upload File">
14442
<CommandButton
14543
icon={<FileArrowUp size={20} color={colors.gray[1800]} />}

0 commit comments

Comments
 (0)