Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add xlsx support #275

Merged
merged 3 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -87,7 +87,7 @@
};

fetch(
`${getApiPath()}/reporting/generateReport?${new URLSearchParams(

Check failure on line 90 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'getApiPath' was used before it was defined

Check failure on line 90 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'getApiPath' was used before it was defined
uiSettingsService.getSearchParams()
)}`,
{
Expand Down Expand Up @@ -175,10 +175,18 @@
$(document).on('click', '#generateCSV', function () {
const timeRanges = getTimeFieldsFromUrl();
const queryUrl = replaceQueryURL(location.href);
const saved_search_id = getUuidFromUrl()[1];

Check failure on line 178 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

Identifier 'saved_search_id' is not in camel case

Check failure on line 178 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

Identifier 'saved_search_id' is not in camel case
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];

Check failure on line 186 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

Identifier 'saved_search_id' is not in camel case

Check failure on line 186 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

Identifier 'saved_search_id' is not in camel case
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 Expand Up @@ -224,7 +232,7 @@
});
});

checkURLParams();

Check failure on line 235 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'checkURLParams' was used before it was defined

Check failure on line 235 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'checkURLParams' was used before it was defined
locationHashChanged();
});

Expand Down Expand Up @@ -276,7 +284,7 @@
};

function locationHashChanged() {
const observer = new MutationObserver(function (mutations) {

Check failure on line 287 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'mutations' is defined but never used. Allowed unused args must match /^_/u

Check failure on line 287 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'mutations' is defined but never used

Check failure on line 287 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'mutations' is defined but never used. Allowed unused args must match /^_/u

Check failure on line 287 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'mutations' is defined but never used
const navMenu = document.querySelectorAll(
'nav.euiHeaderLinks > div.euiHeaderLinks__list'
);
Expand All @@ -292,7 +300,7 @@
return;
}
const menuItem = document.createElement('div');
menuItem.innerHTML = getMenuItem(

Check failure on line 303 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment to innerHTML

Check failure on line 303 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment to innerHTML
i18n.translate('opensearch.reports.menu.name', {
defaultMessage: 'Reporting',
})
Expand All @@ -314,7 +322,7 @@
});
}

$(window).one('hashchange', function (e) {

Check failure on line 325 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'e' is defined but never used. Allowed unused args must match /^_/u

Check failure on line 325 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'e' is defined but never used

Check failure on line 325 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'e' is defined but never used. Allowed unused args must match /^_/u

Check failure on line 325 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

'e' is defined but never used
locationHashChanged();
});
/**
Expand All @@ -336,7 +344,7 @@
};

const getApiPath = () => {
if (window.location.href.includes('/data-explorer/discover/')) return '../../../api'

Check failure on line 347 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

Replace `·return·'../../../api'` with `⏎····return·'../../../api';`

Check failure on line 347 in public/components/context_menu/context_menu.js

View workflow job for this annotation

GitHub Actions / Lint

Replace `·return·'../../../api'` with `⏎····return·'../../../api';`
if (window.location.href.includes('/data-explorer/discover')) return '../../api'
return '../api'
}
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
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
Loading