Skip to content

Commit

Permalink
Add xlsx support (#275) (#324)
Browse files Browse the repository at this point in the history
Signed-off-by: Danila Gulderov <danila@gulderov.me>
Signed-off-by: Rupal Mahajan <maharup@amazon.com>
Co-authored-by: Rupal Mahajan <maharup@amazon.com>
(cherry picked from commit a811806)

Co-authored-by: Danila Gulderov <danila@gulderov.me>
  • Loading branch information
joshuali925 and gulderov authored Mar 7, 2024
1 parent ab31d89 commit 97485cb
Show file tree
Hide file tree
Showing 16 changed files with 695 additions and 96 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dompurify": "^2.4.1",
"elastic-builder": "^2.7.1",
"enzyme-adapter-react-16": "^1.15.5",
"exceljs": "^4.4.0",
"html2canvas": "1.4.1",
"jest-fetch-mock": "^3.0.3",
"jquery": "^3.5.0",
Expand Down
8 changes: 8 additions & 0 deletions public/components/context_menu/context_menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ $(function () {
generateInContextReport(timeRanges, queryUrl, 'csv', { saved_search_id });
});

// generate XLSX onclick
$(document).on('click', '#generateXLSX', function () {
const timeRanges = getTimeFieldsFromUrl();
const queryUrl = replaceQueryURL(location.href);
const saved_search_id = getUuidFromUrl()[1];
generateInContextReport(timeRanges, queryUrl, 'xlsx', { saved_search_id });
});

// navigate to Create report definition page with report source and pre-set time range
$(document).on('click', '#createReportDefinition', function () {
contextMenuCreateReportDefinition(this.baseURI);
Expand Down
20 changes: 17 additions & 3 deletions public/components/context_menu/context_menu_ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const popoverMenu = (savedObjectAvailable) => {
? 'euiContextMenuItem'
: 'euiContextMenuItem euiContextMenuItem-isDisabled';
const button = savedObjectAvailable ? 'button' : 'button disabled';
const popoverHeight = savedObjectAvailable ? '395px' : '380px';
const popoverHeight = getPopoverHeight(false, savedObjectAvailable);
const message = savedObjectAvailable
? i18n.translate('opensearch.reports.menu.visual.waitPrompt', {
defaultMessage:
Expand Down Expand Up @@ -138,14 +138,14 @@ export const popoverMenuDiscover = (savedObjectAvailable) => {
? 'euiContextMenuItem'
: 'euiContextMenuItem euiContextMenuItem-isDisabled';
const button = savedObjectAvailable ? 'button' : 'button disabled';
const popoverHeight = savedObjectAvailable ? '354px' : '322px';
const popoverHeight = getPopoverHeight(true, savedObjectAvailable);
const message = savedObjectAvailable
? i18n.translate('opensearch.reports.menu.csv.waitPrompt', {
defaultMessage:
'Files can take a minute or two to generate depending on the size of your source data.',
})
: i18n.translate('opensearch.reports.menu.csv.savePrompt', {
defaultMessage: 'Save this search to enable CSV reports.',
defaultMessage: 'Save this search to enable CSV/XLSX reports.',
});
const arrowRight = '60px';
const popoverRight = '77px';
Expand Down Expand Up @@ -189,6 +189,12 @@ export const popoverMenuDiscover = (savedObjectAvailable) => {
</svg>
</span>
</button>
<${button} class="${buttonClass}" type="button" data-test-subj="downloadPanel-GeneratePDF" id="generateXLSX">
<span data-html2canvas-ignore class="euiContextMenu__itemLayout">
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" class="euiIcon euiIcon--medium euiIcon-isLoaded euiContextMenu__icon" focusable="false" role="img" aria-hidden="true"><path d="M9 9.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 8a.617.617 0 010-.829.52.52 0 01.77 0L8 9.082V.556C8 .249 8.224 0 8.5 0s.5.249.5.556v8.558z"></path><path d="M16 13.006V10h-1v3.006a.995.995 0 01-.994.994H3.01a.995.995 0 01-.994-.994V10h-1v3.006c0 1.1.892 1.994 1.994 1.994h10.996c1.1 0 1.994-.893 1.994-1.994z"></path></svg>
<span data-html2canvas-ignore class="euiContextMenuItem__text">${i18n.translate('opensearch.reports.menu.csv.generateXLSX', { defaultMessage: 'Generate XLSX' })}</span>
</span>
</button>
</div>
<div class="euiPopoverTitle">
<span data-html2canvas-ignore class="euiContextMenu__itemLayout">
Expand Down Expand Up @@ -394,3 +400,11 @@ export const reportGenerationInProgressModal = () => {
</div>
`;
};

export function getPopoverHeight(isDiscover, isSavedObjectAvailable) {
if (isDiscover && !isSavedObjectAvailable) {
return '372px';
}

return '388px';
}
32 changes: 24 additions & 8 deletions public/components/main/main_utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type fileFormatsOptions = {

export const fileFormatsUpper: fileFormatsOptions = {
csv: 'CSV',
xlsx: 'XLSX',
pdf: 'PDF',
png: 'PNG',
};
Expand All @@ -40,12 +41,17 @@ export const humanReadableDate = (date: string | number | Date) => {
};

export const extractFilename = (filename: string) => {
return filename.substring(0, filename.length - 4);
const index = filename.lastIndexOf('.');
if (index == -1) {
return filename;
}

return filename.slice(0, index);
};

export const extractFileFormat = (filename: string) => {
const fileFormat = filename;
return fileFormat.substring(filename.length - 3, filename.length);
const index = filename.lastIndexOf('.');
return filename.slice(index+1);
};

export const getFileFormatPrefix = (fileFormat: string) => {
Expand Down Expand Up @@ -112,13 +118,23 @@ export const removeDuplicatePdfFileFormat = (filename: string) => {
return filename.substring(0, filename.length - 4);
};

async function getDataReportURL(stream: string, fileFormat: string): Promise<string> {
if (fileFormat == 'xlsx') {
const response = await fetch(stream);
const blob = await response.blob();
return URL.createObjectURL(blob);
}

const blob = new Blob([stream]);
return URL.createObjectURL(blob);
}

export const readDataReportToFile = async (
stream: string,
fileFormat: string,
fileName: string
) => {
const blob = new Blob([stream]);
const url = URL.createObjectURL(blob);
const url = await getDataReportURL(stream, fileFormat);
let link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', fileName);
Expand All @@ -133,7 +149,7 @@ export const readStreamToFile = async (
fileName: string
) => {
let link = document.createElement('a');
if (fileName.includes('csv')) {
if (fileName.includes('csv') || fileName.includes('xlsx')) {
readDataReportToFile(stream, fileFormat, fileName);
return;
}
Expand Down Expand Up @@ -168,7 +184,7 @@ export const generateReportFromDefinitionId = async (
if (!response) return;
const fileFormat = extractFileFormat(response['filename']);
const fileName = response['filename'];
if (fileFormat === 'csv') {
if (fileFormat === 'csv' || fileFormat === 'xlsx') {
await readStreamToFile(await response['data'], fileFormat, fileName);
status = true;
return;
Expand Down Expand Up @@ -212,7 +228,7 @@ export const generateReportById = async (
//TODO: duplicate code, extract to be a function that can reuse. e.g. handleResponse(response)
const fileFormat = extractFileFormat(response['filename']);
const fileName = response['filename'];
if (fileFormat === 'csv') {
if (fileFormat === 'csv' || fileFormat === 'xlsx') {
await readStreamToFile(await response['data'], fileFormat, fileName);
handleSuccessToast();
return response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
PDF_PNG_FILE_FORMAT_OPTIONS,
HEADER_FOOTER_CHECKBOX,
REPORT_SOURCE_TYPES,
SAVED_SEARCH_FORMAT_OPTIONS,
} from './report_settings_constants';
import Showdown from 'showdown';
import ReactMde from 'react-mde';
Expand Down Expand Up @@ -275,6 +276,27 @@ export function ReportSettings(props: ReportSettingProps) {
);
};

const CSVandXLSXFileFormats = () => {
return (
<div>
<EuiFormRow
label={i18n.translate(
'opensearch.reports.reportSettingProps.fileFormat',
{
defaultMessage: 'File format',
}
)}
>
<EuiRadioGroup
options={SAVED_SEARCH_FORMAT_OPTIONS}
idSelected={fileFormat}
onChange={handleFileFormat}
/>
</EuiFormRow>
</div>
);
};

const SettingsMarkdown = () => {
const [
checkboxIdSelectHeaderFooter,
Expand Down Expand Up @@ -437,6 +459,14 @@ export function ReportSettings(props: ReportSettingProps) {
);
};

const DataReportFormatAndMarkdown = () => {
return (
<div>
<CSVandXLSXFileFormats />
</div>
);
};

const setReportSourceDropdownOption = (options, reportSource, url) => {
let index = 0;
if (reportSource === REPORT_SOURCE_TYPES.dashboard) {
Expand Down Expand Up @@ -762,18 +792,7 @@ export function ReportSettings(props: ReportSettingProps) {
<SettingsMarkdown />
</div>
) : (
<div>
<EuiFormRow
label={i18n.translate(
'opensearch.reports.reportSettingProps.form.fileFormat',
{ defaultMessage: 'File format' }
)}
>
<EuiText>
<p>CSV</p>
</EuiText>
</EuiFormRow>
</div>
<DataReportFormatAndMarkdown />
);

const displayNotebooksSelect =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ export const PDF_PNG_FILE_FORMAT_OPTIONS = [

export const SAVED_SEARCH_FORMAT_OPTIONS = [
{
id: 'csvFormat',
id: 'csv',
label: 'CSV',
},
{
id: 'xlsFormat',
label: 'XLS',
id: 'xlsx',
label: 'XLSX',
},
];

Expand Down
2 changes: 2 additions & 0 deletions server/model/backendModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export enum BACKEND_REPORT_FORMAT {
pdf = 'Pdf',
png = 'Png',
csv = 'Csv',
xlsx = 'Xlsx',
}

export enum BACKEND_TRIGGER_TYPE {
Expand All @@ -126,6 +127,7 @@ export const REPORT_SOURCE_DICT = {

export const REPORT_FORMAT_DICT = {
[FORMAT.csv]: BACKEND_REPORT_FORMAT.csv,
[FORMAT.xlsx]: BACKEND_REPORT_FORMAT.xlsx,
[FORMAT.pdf]: BACKEND_REPORT_FORMAT.pdf,
[FORMAT.png]: BACKEND_REPORT_FORMAT.png,
};
Expand Down
3 changes: 1 addition & 2 deletions server/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ export const dataReportSchema = schema.object({
}
},
}),
//TODO: future support schema.literal('xlsx')
report_format: schema.oneOf([schema.literal(FORMAT.csv)]),
report_format: schema.oneOf([schema.literal(FORMAT.csv), schema.literal(FORMAT.xlsx)]),
limit: schema.number({ defaultValue: DEFAULT_MAX_SIZE, min: 0 }),
excel: schema.boolean({ defaultValue: true }),
});
Expand Down
2 changes: 2 additions & 0 deletions server/routes/utils/__tests__/savedSearchReportHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ describe('test create saved search report', () => {
mockTimezone
);
expect(xlsxReport.fileName).toContain('.xlsx');

input.report_definition.report_params.core_params.report_format = 'csv';
}, 20000);

test('create report for empty data set', async () => {
Expand Down
11 changes: 11 additions & 0 deletions server/routes/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum FORMAT {
pdf = 'pdf',
png = 'png',
csv = 'csv',
xlsx = 'xlsx',
}

export enum REPORT_STATE {
Expand Down Expand Up @@ -181,6 +182,11 @@ export const GLOBAL_BASIC_COUNTER: CountersType = {
total: 0,
},
},
xlsx: {
download: {
total: 0,
},
},
},
};

Expand Down Expand Up @@ -288,5 +294,10 @@ export const DEFAULT_ROLLING_COUNTER: CountersType = {
count: 0,
},
},
xlsx: {
download: {
count: 0,
},
},
},
};
2 changes: 1 addition & 1 deletion server/routes/utils/converters/backendToUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ const getDataReportCoreParams = (
): DataReportSchemaType => {
let res: DataReportSchemaType = {
base_url: baseUrl,
report_format: <FORMAT.csv>getUiReportFormat(fileFormat),
report_format: <FORMAT.csv | FORMAT.xlsx>getUiReportFormat(fileFormat),
limit: limit,
time_duration: duration,
saved_search_id: sourceId,
Expand Down
Loading

0 comments on commit 97485cb

Please sign in to comment.