Skip to content
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased
### Added
- Preview live datasource data from the report wizard and sidebar before saving.
- OCS endpoints to list accessible reports and owned datasets, retrieve report metadata and filtered data including shared reports, and add or delete dataset rows.

### Fixed
Expand Down
45 changes: 44 additions & 1 deletion js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ Object.assign(OCA.Analytics.Datasource = {
/**
* Generate configuration form for the selected data source
*/
buildDatasourceRelatedForm: function (datasource) {
buildDatasourceRelatedForm: function (datasource, showPreview = false) {
let template = OCA.Analytics.datasources.options[datasource];
let form = document.createElement('div');
let insideSection = false;
Expand Down Expand Up @@ -582,9 +582,52 @@ Object.assign(OCA.Analytics.Datasource = {
tableRow.appendChild(input);
tableRow.appendChild(infoColumn);
}
if (showPreview) {
const preview = document.importNode(document.getElementById('templateDatasourcePreviewButton').content, true);
preview.querySelector('button').addEventListener('click', () => OCA.Analytics.Datasource.preview(form, datasource));
form.appendChild(preview);
}
return form;
},

/**
* Preview the current datasource settings without saving the report.
*/
preview: async function (form, datasource) {
const options = {};
for (const input of form.querySelectorAll('input, select')) {
if (input.id !== 'dataSourceType') options[input.id] = input.value;
}
const controller = new AbortController();
OCA.Analytics.Notification.htmlDialogInitiate(
t('analytics', 'Data preview'), OCA.Analytics.Notification.dialogClose
);
const dialog = document.getElementById('analyticsDialogContainer');
dialog._analyticsDialogCleanup = () => controller.abort();
try {
const response = await fetch(OC.generateUrl('apps/analytics/data'), {
method: 'POST',
headers: OCA.Analytics.headers(),
signal: controller.signal,
body: JSON.stringify({type: Number(datasource), options: JSON.stringify(options), limit: 20})
});
if (!response.ok) throw new Error(t('analytics', 'The preview request failed.'));
const data = await response.json();
if (data.error && String(data.error) !== '0') throw new Error(String(data.error));
if (document.getElementById('analyticsDialogContainer') !== dialog) return;
const content = document.createElement('pre');
content.id = 'simulationData';
content.textContent = JSON.stringify({header: data.header || [], data: data.data || []}, null, 2);
OCA.Analytics.Notification.htmlDialogUpdate(content, t('analytics', 'Preview of the first 20 rows.'));
} catch (error) {
if (error.name === 'AbortError' || document.getElementById('analyticsDialogContainer') !== dialog) return;
const content = document.createElement('p');
content.setAttribute('role', 'alert');
content.textContent = error.message || t('analytics', 'The preview could not be prepared.');
OCA.Analytics.Notification.htmlDialogUpdate(content, '');
}
},

/**
* Fill the sheet dropdown from the selected workbook.
*/
Expand Down
4 changes: 2 additions & 2 deletions js/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ OCA.Analytics.Sidebar.Report = {
} else if (data['type'] === OCA.Analytics.TYPE_INTERNAL_DB) {
// OCA.Analytics.Sidebar.Report.fillDatasetRelatedFields();
} else {
let optionForm = OCA.Analytics.Datasource.buildDatasourceRelatedForm(data['type']);
let optionForm = OCA.Analytics.Datasource.buildDatasourceRelatedForm(data['type'], true);
document.getElementById('reportDatasourceSection').appendChild(optionForm);
OCA.Analytics.Sidebar.Report.fillDatasourceRelatedFields(data);
}
Expand Down Expand Up @@ -410,7 +410,7 @@ OCA.Analytics.Sidebar.Report = {
const type = parseInt(document.getElementById('wizardNewDatasource').value);
document.getElementById('wizardNewTypeOptionsRow').style.display = 'table-row';
document.getElementById('wizardNewDatasourceSection').innerHTML = '';
document.getElementById('wizardNewDatasourceSection').appendChild(OCA.Analytics.Datasource.buildDatasourceRelatedForm(type));
document.getElementById('wizardNewDatasourceSection').appendChild(OCA.Analytics.Datasource.buildDatasourceRelatedForm(type, true));
OCA.Analytics.Sidebar.Report.buildTemplateDropdownWizard(type);
OCA.Analytics.Sidebar.Report.updateWizardVisualTemplateState(OCA.Analytics.Sidebar.Report.getSelectedTemplateWizard());
OCA.Analytics.Sidebar.Report.syncGlobalTemplateSelection();
Expand Down
6 changes: 3 additions & 3 deletions lib/Controller/OutputController.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,15 @@ private function extractCacheMetadata(array $result): array {
}

/**
* Preview the data
* Preview the data (one row for column pickers, up to 20 for datasource previews)
*
* @param $type
* @param $options
* @return DataResponse|NotFoundResponse
* @throws Exception
*/
#[NoAdminRequired]
public function readPreview($type, $options) {
public function readPreview($type, $options, int $limit = 1) {
$reportMetadata = [];
$array = json_decode($options, true);
foreach ($array as $key => $value) {
Expand All @@ -293,7 +293,7 @@ public function readPreview($type, $options) {

$result = $this->getData($reportMetadata);
unset($result['options'], $result['dimensions'], $result['filterApplied'], $result['thresholds']);
$result['data'] = array_slice($result['data'], 0, 1);
$result['data'] = array_slice($result['data'] ?? [], 0, max(1, min($limit, 20)));

return new DataResponse($result, HTTP::STATUS_OK);
}
Expand Down
4 changes: 4 additions & 0 deletions templates/part.templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -1385,3 +1385,7 @@ class="analyticsModelCardIcon analyticsModelCardIcon--time" alt="">
<p id="chartColumnPreviewMessage" class="chartColumnMappingHint" hidden></p>
</aside>
</template>

<template id="templateDatasourcePreviewButton">
<button type="button" class="button analyticsSecondary" id="datasourcePreviewButton"><?php p($l->t('Data preview')); ?></button>
</template>
1 change: 1 addition & 0 deletions tests/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Supported identifiers:
- `54`, `report-transformations` (shared calculated measures, hidden inputs, sorting, preview, and table-calculation migration)
- `55`, `dialog-keyboard-mobile` (keyboard navigation, modal focus, and mobile report creation)
- `56`, `demo-sales-ranking` (two demo reports sharing one dataset, raw table, Top N, mixed chart, pivot calculations, column order, formatting, indicators, and reload)
- `tests/playwright/datasource_preview.js` (live datasource previews in the wizard and sidebar, unsaved options, and error handling)
- `tests/playwright/dashboard_number_format.js` (standalone dashboard values, locales, and thresholds)
- `91`, `91-delete`, `report-delete`, `delete`
- `92`, `92-delete`, `group-delete`
Expand Down
73 changes: 73 additions & 0 deletions tests/playwright/datasource_preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* SPDX-FileCopyrightText: 2026 Marcel Scherello
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
const assert = require('node:assert/strict');
const {chromium} = require('playwright');
const {buildScenarioConfig, ensureAnalyticsLoaded, createCapture, openReportBasicSettings} = require('./common');
const config = buildScenarioConfig('datasource-preview');
(async () => {
const browser = await chromium.launch({headless: config.headless});
const page = await browser.newPage({viewport: config.viewport});
const capture = createCapture(page, config);
const errors = [];
page.on('pageerror', e => errors.push(e.message));
let reportId;
const source = 'https://raw.githubusercontent.com/Rello/analytics/master/sample_data/sample1.csv';
try {
await ensureAnalyticsLoaded(page, config);
await page.locator('#newReportButton').click();
await page.locator('#newMenuReport').click();
await page.locator('#wizardNewTemplateOwnReport').click();
await page.locator('#wizardNext').click();
await page.locator('#wizardNewTypeRealtime').click();
await page.locator('#wizardNewDatasource').selectOption({label: 'External: csv'});
await page.locator('#dataSourceOptions #link').fill(source);
const type = await page.locator('#wizardNewDatasource').inputValue();
await page.locator('#datasourcePreviewButton').click();
await page.locator('#simulationData').waitFor();
assert.equal(JSON.parse(await page.locator('#simulationData').innerText()).data.length, 20);
await capture('wizard_preview');
await page.locator('#analyticsDialogBtnClose').click();
assert.equal(await page.locator('#dataSourceOptions #link').inputValue(), source);
await page.keyboard.press('Escape');
const name = 'Datasource preview ' + Date.now();
reportId = await page.evaluate(async ({name, type, source}) => {
const response = await fetch(OC.generateUrl('apps/analytics/report'), {
method: 'POST', headers: OCA.Analytics.headers(),
body: JSON.stringify({name, subheader:'', parent:0, type:Number(type), dataset:0,
link:JSON.stringify({link:source, columns:"", offset:""}), visualization:'table', chart:'line', dimension1:'', dimension2:'', value:''})
});
if (!response.ok) throw new Error("Create fixture HTTP " + response.status());
return response.json();
}, {name, type, source});
assert.ok(Number(reportId) > 0, JSON.stringify(reportId));
await page.reload();
await ensureAnalyticsLoaded(page, config);
await openReportBasicSettings(page, name);
await page.locator('#reportDatasourceSectionHeader').click();
if (await page.locator('#optionSectionHeader').isVisible()) await page.locator('#optionSectionHeader').click();
await page.locator('#dataSourceOptions #columns').fill('1,3');
await page.locator('#dataSourceOptions #offset').fill('59');
await page.locator('#datasourcePreviewButton').click();
await page.locator('#simulationData').waitFor();
assert.deepEqual(JSON.parse(await page.locator('#simulationData').innerText()).data, [['Germany', 83073100]]);
await capture('sidebar_unsaved_preview');
await page.locator('#analyticsDialogBtnClose').click();
await page.route('**/apps/analytics/data', route => route.fulfill({json:{error:'Preview failure <unsafe>', data:[]}}));
await page.locator('#datasourcePreviewButton').click();
await page.getByRole('alert').filter({hasText:'Preview failure <unsafe>'}).waitFor();
assert.equal(await page.locator('#analyticsDialogContent unsafe').count(), 0);
await page.locator('#analyticsDialogBtnClose').click();
await page.unroute('**/apps/analytics/data');
assert.deepEqual(errors, []);
console.log('PASS: wizard preview, sidebar unsaved settings, errors, dialog return');
} catch (error) {
await capture('failure');
throw error;
} finally {
if (Number(reportId) > 0) await page.evaluate(async id => fetch(OC.generateUrl('apps/analytics/report/') + id,
{method:'DELETE', headers:OCA.Analytics.headers()}), reportId);
await browser.close();
}
})().catch(error => {console.error(error); process.exitCode = 1;});
Loading