Skip to content

Commit c752309

Browse files
committed
feat(uploadDataModel): add DataPreviewModal for file upload preview and integrate driver.js for guided tours
1 parent 3b75af9 commit c752309

9 files changed

Lines changed: 1269 additions & 417 deletions

File tree

superset-frontend/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

superset-frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
"dayjs": "^1.11.19",
165165
"dom-to-image-more": "^3.7.2",
166166
"dom-to-pdf": "^0.3.2",
167+
"driver.js": "^1.4.0",
167168
"echarts": "^5.6.0",
168169
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
169170
"fast-glob": "^3.3.2",

superset-frontend/src/features/databases/UploadDataModel/ColumnsPreview.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { type TagType, TagsList } from 'src/components';
2525
interface ColumnsPreviewProps {
2626
columns: string[];
2727
maxColumnsToShow?: number;
28+
fileTooLarge?: boolean;
2829
}
2930

3031
export const StyledDivContainer = styled.div`
@@ -35,14 +36,19 @@ export const StyledDivContainer = styled.div`
3536
const ColumnsPreview: FC<ColumnsPreviewProps> = ({
3637
columns,
3738
maxColumnsToShow = 4,
39+
fileTooLarge = false,
3840
}) => {
3941
const tags: TagType[] = columns.map(column => ({ name: column }));
4042

4143
return (
4244
<StyledDivContainer>
4345
<Typography.Text type="secondary">Columns:</Typography.Text>
4446
{columns.length === 0 ? (
45-
<p className="help-block">{t('Upload file to preview columns')}</p>
47+
<p className="help-block">
48+
{fileTooLarge
49+
? t('Preview is not available for files larger than 5MB')
50+
: t('Upload file to preview columns')}
51+
</p>
4652
) : (
4753
<TagsList tags={tags} maxTags={maxColumnsToShow} />
4854
)}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import {
20+
render,
21+
screen,
22+
waitFor,
23+
userEvent,
24+
} from 'spec/helpers/testing-library';
25+
import { DataPreviewModal } from './DataPreviewModal';
26+
27+
// Polyfill File.prototype.text for jsdom (not implemented)
28+
if (typeof File.prototype.text !== 'function') {
29+
File.prototype.text = function (this: File) {
30+
return new Promise<string>((resolve, reject) => {
31+
const reader = new FileReader();
32+
reader.onload = () => resolve(String(reader.result ?? ''));
33+
reader.onerror = () => reject(reader.error ?? new Error('Read failed'));
34+
reader.readAsText(this);
35+
});
36+
};
37+
}
38+
39+
const defaultProps = {
40+
show: true,
41+
onHide: jest.fn(),
42+
file: null,
43+
type: 'csv' as const,
44+
};
45+
46+
function createCSVFile(content: string): File {
47+
return new File([content], 'test.csv', { type: 'text/csv' });
48+
}
49+
50+
test('renders modal with Data preview title when show is true', () => {
51+
render(<DataPreviewModal {...defaultProps} />);
52+
53+
expect(screen.getByText('Data preview')).toBeInTheDocument();
54+
});
55+
56+
test('shows no data to preview when file is null', async () => {
57+
render(<DataPreviewModal {...defaultProps} />);
58+
59+
await waitFor(() => {
60+
expect(screen.getByText('No data to preview')).toBeInTheDocument();
61+
});
62+
});
63+
64+
test('parses CSV and displays data in table', async () => {
65+
const csvContent = 'name,age,city\nAlice,30,NYC\nBob,25,Boston';
66+
const file = createCSVFile(csvContent);
67+
68+
render(<DataPreviewModal {...defaultProps} file={file} type="csv" />);
69+
70+
await waitFor(() => {
71+
expect(
72+
screen.getByRole('columnheader', { name: 'name' }),
73+
).toBeInTheDocument();
74+
expect(
75+
screen.getByRole('columnheader', { name: 'age' }),
76+
).toBeInTheDocument();
77+
expect(
78+
screen.getByRole('columnheader', { name: 'city' }),
79+
).toBeInTheDocument();
80+
expect(screen.getByRole('cell', { name: 'Alice' })).toBeInTheDocument();
81+
expect(screen.getByRole('cell', { name: '30' })).toBeInTheDocument();
82+
expect(screen.getByRole('cell', { name: 'NYC' })).toBeInTheDocument();
83+
expect(screen.getByRole('cell', { name: 'Bob' })).toBeInTheDocument();
84+
expect(screen.getByRole('cell', { name: '25' })).toBeInTheDocument();
85+
expect(screen.getByRole('cell', { name: 'Boston' })).toBeInTheDocument();
86+
});
87+
});
88+
89+
test('uses custom delimiter for CSV parsing', async () => {
90+
const csvContent = 'name;age;city\nAlice;30;NYC';
91+
const file = createCSVFile(csvContent);
92+
93+
render(
94+
<DataPreviewModal {...defaultProps} file={file} type="csv" delimiter=";" />,
95+
);
96+
97+
await waitFor(() => {
98+
expect(
99+
screen.getByRole('columnheader', { name: 'name' }),
100+
).toBeInTheDocument();
101+
expect(screen.getByRole('cell', { name: 'Alice' })).toBeInTheDocument();
102+
expect(screen.getByRole('cell', { name: '30' })).toBeInTheDocument();
103+
});
104+
});
105+
106+
test('shows error for columnar type', async () => {
107+
const file = createCSVFile('col1\nval1');
108+
109+
render(<DataPreviewModal {...defaultProps} file={file} type="columnar" />);
110+
111+
await waitFor(() => {
112+
expect(
113+
screen.getByText('Data preview is not available for Parquet files.'),
114+
).toBeInTheDocument();
115+
});
116+
});
117+
118+
test('shows No data to preview for empty CSV', async () => {
119+
const file = createCSVFile('');
120+
121+
render(<DataPreviewModal {...defaultProps} file={file} type="csv" />);
122+
123+
await waitFor(() => {
124+
expect(screen.getByText('No data to preview')).toBeInTheDocument();
125+
});
126+
});
127+
128+
test('shows Loading while parsing', async () => {
129+
const file = createCSVFile('a,b\n1,2');
130+
const textSpy = jest.spyOn(File.prototype, 'text');
131+
textSpy.mockImplementation(
132+
() => new Promise(resolve => setTimeout(() => resolve('a,b\n1,2'), 100)),
133+
);
134+
135+
render(<DataPreviewModal {...defaultProps} file={file} type="csv" />);
136+
137+
await waitFor(() => {
138+
expect(screen.getByText('Loading...')).toBeInTheDocument();
139+
});
140+
141+
textSpy.mockRestore();
142+
});
143+
144+
test('calls onHide when modal close button is clicked', async () => {
145+
const onHide = jest.fn();
146+
render(
147+
<DataPreviewModal
148+
{...defaultProps}
149+
onHide={onHide}
150+
file={createCSVFile('a,b\n1,2')}
151+
type="csv"
152+
/>,
153+
);
154+
155+
await waitFor(() => {
156+
expect(screen.getByRole('cell', { name: '1' })).toBeInTheDocument();
157+
});
158+
159+
const closeButton = screen.getByTestId('close-modal-btn');
160+
await userEvent.click(closeButton);
161+
162+
expect(onHide).toHaveBeenCalledTimes(1);
163+
});
164+
165+
test('shows error message when CSV parse fails', async () => {
166+
const file = new File(['invalid'], 'test.csv', { type: 'text/csv' });
167+
const textSpy = jest.spyOn(File.prototype, 'text');
168+
textSpy.mockRejectedValueOnce(new Error('Read failed'));
169+
170+
render(<DataPreviewModal {...defaultProps} file={file} type="csv" />);
171+
172+
await waitFor(() => {
173+
expect(
174+
screen.getByText(/Read failed|Failed to parse file/),
175+
).toBeInTheDocument();
176+
});
177+
178+
textSpy.mockRestore();
179+
});

0 commit comments

Comments
 (0)