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

[Backport 4.4-7.16] Add header and footer report customization #4505 #4783

Merged
merged 3 commits into from
Oct 31, 2022
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 @@ -11,6 +11,7 @@ All notable changes to the Wazuh app project will be documented in this file.
- Redesign the SCA table from agent's dashboard [#4512](https://github.com/wazuh/wazuh-kibana-app/pull/4512)
- Enhanced the plugin setting description displayed in the UI and the configuration file. [#4501](https://github.com/wazuh/wazuh-kibana-app/pull/4501)
- Added validation to the plugin settings in the form of `Settings/Configuration` and the endpoint to update the plugin configuration [#4503](https://github.com/wazuh/wazuh-kibana-app/pull/4503)
- Added new plugin settings to customize the header and footer on the PDF reports [#4505](https://github.com/wazuh/wazuh-kibana-app/pull/4505)

### Changed

Expand Down
55 changes: 52 additions & 3 deletions common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,11 @@ export enum SettingCategory {
CUSTOMIZATION,
};

type TPluginSettingOptionsTextArea = {
rowsSize?: number
maxLength?: number
};

type TPluginSettingOptionsSelect = {
select: { text: string, value: any }[]
};
Expand Down Expand Up @@ -442,7 +447,13 @@ export type TPluginSetting = {
// Modify the setting requires restarting the plugin platform to take effect.
requiresRestartingPluginPlatform?: boolean
// Define options related to the `type`.
options?: TPluginSettingOptionsNumber | TPluginSettingOptionsEditor | TPluginSettingOptionsFile | TPluginSettingOptionsSelect | TPluginSettingOptionsSwitch
options?:
TPluginSettingOptionsEditor |
TPluginSettingOptionsFile |
TPluginSettingOptionsNumber |
TPluginSettingOptionsSelect |
TPluginSettingOptionsSwitch |
TPluginSettingOptionsTextArea
// Transform the input value. The result is saved in the form global state of Settings/Configuration
uiFormTransformChangedInputValue?: (value: any) => any
// Transform the configuration value or default as initial value for the input in Settings/Configuration
Expand Down Expand Up @@ -545,7 +556,6 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = {
validateBackend: function(schema){
return schema.boolean();
},

},
"checks.fields": {
title: "Known fields",
Expand Down Expand Up @@ -1072,6 +1082,46 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = {
)(value)
},
},
"customization.reports.footer": {
title: "Reports footer",
description: "Set the footer of the reports.",
category: SettingCategory.CUSTOMIZATION,
type: EpluginSettingType.textarea,
defaultValue: "",
defaultValueIfNotSet: REPORTS_PAGE_FOOTER_TEXT,
isConfigurableFromFile: true,
isConfigurableFromUI: true,
options: { rowsSize: 2, maxLength: 30 },
validate: function (value) {
return SettingsValidator.multipleLinesString({
max: this.options.rowsSize,
maxLength: this.options.maxLength
})(value)
},
validateBackend: function (schema) {
return schema.string({ validate: this.validate.bind(this) });
},
},
"customization.reports.header": {
title: "Reports header",
description: "Set the header of the reports.",
category: SettingCategory.CUSTOMIZATION,
type: EpluginSettingType.textarea,
defaultValue: "",
defaultValueIfNotSet: REPORTS_PAGE_HEADER_TEXT,
isConfigurableFromFile: true,
isConfigurableFromUI: true,
options: { rowsSize: 3, maxLength: 20 },
validate: function (value) {
return SettingsValidator.multipleLinesString({
max: this.options.rowsSize,
maxLength: this.options?.maxLength
})(value)
},
validateBackend: function(schema){
return schema.string({validate: this.validate?.bind(this)});
},
},
"disabled_roles": {
title: "Disable roles",
description: "Disabled the plugin visibility for users with the roles.",
Expand Down Expand Up @@ -1785,7 +1835,6 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = {

export type TPluginSettingKey = keyof typeof PLUGIN_SETTINGS;


export enum HTTP_STATUS_CODES {
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
Expand Down
468 changes: 239 additions & 229 deletions common/plugin-settings.test.ts

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions common/services/settings-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,17 @@ export class SettingsValidator {
* @param options
* @returns
*/
static multipleLinesString(options: { min?: number, max?: number } = {}) {
static multipleLinesString(options: { min?: number, max?: number, maxLength?: number } = {}) {
return function (value: number) {
const lines = value.split(/\r\n|\r|\n/).length;
if (typeof options.maxLength !== 'undefined' && value.split('\n').some(line => line.length > options.maxLength)) {
return `The maximum length of a line is ${options.maxLength} characters.`;
};
if (typeof options.min !== 'undefined' && lines < options.min) {
return `The string should have more or ${options.min} line/s.`;
};
if (typeof options.max !== 'undefined' && lines > options.max) {
return `The string should have less or ${options.max} line/s.`;
return `The string should have less or equal to ${options.max} line/s.`;
};
}
};
Expand Down
11 changes: 11 additions & 0 deletions public/components/common/form/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,14 @@ exports[`[component] InputForm Renders correctly to match the snapshot: Input: t
</div>
</div>
`;

exports[`[component] InputForm Renders correctly to match the snapshot: Input: textarea 1`] = `
<div>
<textarea
class="euiTextArea euiTextArea--resizeVertical euiTextArea--fullWidth"
rows="6"
>
test
</textarea>
</div>
`;
15 changes: 8 additions & 7 deletions public/components/common/form/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ describe('[component] InputForm', () => {
const optionsSelect = { select: [{ text: 'Label1', value: 'value1' }, { text: 'Label2', value: 'value2' }] };
const optionsSwitch = { switch: { values: { enabled: { label: 'Enabled', value: true }, disabled: { label: 'Disabled', value: false } } } };
it.each`
inputType | value | options
${'editor'} | ${'{}'} | ${optionsEditor}
${'filepicker'} | ${'{}'} | ${optionsFilepicker}
${'number'} | ${4} | ${undefined}
${'select'} | ${'value1'} | ${optionsSelect}
${'switch'} | ${true} | ${optionsSwitch}
${'text'} | ${'test'} | ${undefined}
inputType | value | options
${'editor'} | ${'{}'} | ${optionsEditor}
${'filepicker'} | ${'{}'} | ${optionsFilepicker}
${'number'} | ${4} | ${undefined}
${'select'} | ${'value1'} | ${optionsSelect}
${'switch'} | ${true} | ${optionsSwitch}
${'text'} | ${'test'} | ${undefined}
${'textarea'} | ${'test'} | ${undefined}
`('Renders correctly to match the snapshot: Input: $inputType', ({ inputType, value, options }) => {
const wrapper = render(
<InputForm
Expand Down
2 changes: 2 additions & 0 deletions public/components/common/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { InputFormText } from './input_text';
import { InputFormSelect } from './input_select';
import { InputFormSwitch } from './input_switch';
import { InputFormFilePicker } from './input_filepicker';
import { InputFormTextArea } from './input_text_area';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';

export const InputForm = ({
Expand Down Expand Up @@ -61,5 +62,6 @@ const Input = {
number: InputFormNumber,
select: InputFormSelect,
text: InputFormText,
textarea: InputFormTextArea,
filepicker: InputFormFilePicker
};
15 changes: 15 additions & 0 deletions public/components/common/form/input_text_area.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { EuiTextArea } from '@elastic/eui';
import { IInputFormType } from './types';

export const InputFormTextArea = ({ value, isInvalid, onChange, options } : IInputFormType) => {
return (
<EuiTextArea
fullWidth
value={value}
isInvalid={isInvalid}
onChange={onChange}
rows={options?.rowsSize}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ export const Category: React.FunctionComponent<ICategoryProps> = ({
aria-label={item.key}
content='Invalid' />
)}

{isUpdated && (
<EuiIconTip
anchorClassName="mgtAdvancedSettings__fieldTitleUnsavedIcon"
Expand Down
2 changes: 1 addition & 1 deletion public/components/settings/configuration/configuration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ const WzConfigurationSettingsProvider = (props) => {
// Update the settings that uploads a file
if(Object.keys(settingsToUpdate.fileUpload).length){
requests.push(...Object.entries(settingsToUpdate.fileUpload)
.map(([pluginSettingKey, {file, extension}]) => {
.map(([pluginSettingKey, {file}]) => {
// Create the form data
const formData = new FormData();
formData.append('file', file);
Expand Down
6 changes: 3 additions & 3 deletions server/controllers/wazuh-reporting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ export class WazuhReportingCtrl {
str += `${
type === 'range'
? `${params.gte}-${params.lt}`
: type === 'phrases'
? '(' + params.join(" OR ") + ')'
: type === 'exists'
: type === 'phrases'
? '(' + params.join(" OR ") + ')'
: type === 'exists'
? '*'
: !!value
? value
Expand Down
67 changes: 41 additions & 26 deletions server/lib/reporting/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import {
import { log } from '../logger';
import * as TimSort from 'timsort';
import { getConfiguration } from '../get-configuration';
import { REPORTS_PRIMARY_COLOR, REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH, REPORTS_PAGE_FOOTER_TEXT, REPORTS_PAGE_HEADER_TEXT } from '../../../common/constants';
import { REPORTS_PRIMARY_COLOR} from '../../../common/constants';
import { getSettingDefaultValue } from '../../../common/services/settings';

const COLORS = {
PRIMARY: REPORTS_PRIMARY_COLOR
};

const pageConfiguration = (nameLogo) => ({
const pageConfiguration = ({ pathToLogo, pageHeader, pageFooter }) => ({
styles: {
h1: {
fontSize: 22,
Expand Down Expand Up @@ -54,11 +55,11 @@ const pageConfiguration = (nameLogo) => ({
margin: [40, 20, 0, 0],
columns: [
{
image: path.join(__dirname, `../../../public/assets/${nameLogo}`),
width: 190
image: path.join(__dirname, `../../../public/assets/${pathToLogo}`),
fit: [190, 50]
},
{
text: REPORTS_PAGE_HEADER_TEXT,
text: pageHeader,
alignment: 'right',
margin: [0, 0, 40, 0],
color: COLORS.PRIMARY
Expand All @@ -70,7 +71,7 @@ const pageConfiguration = (nameLogo) => ({
return {
columns: [
{
text: REPORTS_PAGE_FOOTER_TEXT,
text: pageFooter,
color: COLORS.PRIMARY,
margin: [40, 40, 0, 0]
},
Expand Down Expand Up @@ -473,7 +474,7 @@ export class ReportPrinter{
this.addContent(typeof title === 'string' ? { text: title, style: 'h4' } : title)
.addNewLine();
}

if (!items || !items.length) {
this.addContent({
text: 'No results match your search criteria',
Expand All @@ -494,31 +495,31 @@ export class ReportPrinter{
style: 'standard'
}
})
});
});

// 385 is the max initial width per column
let totalLength = columns.length - 1;
const widthColumn = 385/totalLength;
let totalWidth = totalLength * widthColumn;

const widths:(number)[] = [];

for (let step = 0; step < columns.length - 1; step++) {

let columnLength = this.getColumnWidth(columns[step], tableRows, step);

if (columnLength <= Math.round(totalWidth / totalLength)) {
widths.push(columnLength);
totalWidth -= columnLength;
}
}
else {
widths.push(Math.round(totalWidth / totalLength));
totalWidth -= Math.round((totalWidth / totalLength));
}
totalLength--;
}
widths.push('*');

this.addContent({
fontSize: 8,
table: {
Expand Down Expand Up @@ -562,9 +563,9 @@ export class ReportPrinter{
`agents: ${agents}`,
'debug'
);

this.addNewLine();

this.addContent({
text:
'NOTE: This report only includes the authorized agents of the user who generated the report',
Expand Down Expand Up @@ -613,22 +614,36 @@ export class ReportPrinter{
);
}

async print(reportPath: string){
const nameLogo = ( await getConfiguration() )['customization.logo.reports'] || REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH;
async print(reportPath: string) {
return new Promise((resolve, reject) => {
try {
const configuration = getConfiguration();

const document = this._printer.createPdfKitDocument({...pageConfiguration(nameLogo), content: this._content});
await document.pipe(
fs.createWriteStream(reportPath)
);
document.end();
const pathToLogo = configuration['customization.logo.reports'] || getSettingDefaultValue('customization.logo.reports');
const pageHeader = configuration['customization.reports.header'] || getSettingDefaultValue('customization.reports.header');
const pageFooter = configuration['customization.reports.footer'] || getSettingDefaultValue('customization.reports.footer');

const document = this._printer.createPdfKitDocument({ ...pageConfiguration({ pathToLogo, pageHeader, pageFooter }), content: this._content });

document.on('error', reject);
document.on('end', resolve);

document.pipe(
fs.createWriteStream(reportPath)
);
document.end();
} catch (ex) {
reject(ex);
}
});
}

/**
* Returns the width of a given column
*
* @param column
* @param tableRows
* @param step
*
* @param column
* @param tableRows
* @param step
* @returns {number}
*/
getColumnWidth(column, tableRows, index){
Expand Down
Loading