Problems: Allow HTML in description (#1559)

* Problems: allow HTML in problem description, closes #1557

* Don't replace new line with <br>

* Replace line brakes with <br> if allowDangerousHTML enabled

* List layout: only show html formatted description if option is enabled
This commit is contained in:
Alexander Zobnin
2023-01-20 15:11:12 +01:00
committed by GitHub
parent a5c239f77b
commit bc62e35477
6 changed files with 58 additions and 14 deletions

View File

@@ -96,7 +96,7 @@ export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
} }
// Handle multi-line description // Handle multi-line description
if (trigger.comments) { if (trigger.comments && options.allowDangerousHTML) {
trigger.comments = trigger.comments.replace('\n', '<br>'); trigger.comments = trigger.comments.replace('\n', '<br>');
} }

View File

@@ -128,16 +128,26 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
)} )}
<span className="alert-rule-item__time">{panelOptions.ageField && 'for ' + age}</span> <span className="alert-rule-item__time">{panelOptions.ageField && 'for ' + age}</span>
{panelOptions.descriptionField && !panelOptions.descriptionAtNewLine && ( {panelOptions.descriptionField && !panelOptions.descriptionAtNewLine && (
<>
{panelOptions.allowDangerousHTML ? (
<span className="zbx-description" dangerouslySetInnerHTML={{ __html: problem.comments }} /> <span className="zbx-description" dangerouslySetInnerHTML={{ __html: problem.comments }} />
) : (
<span className="zbx-description">{problem.comments}</span>
)}
</>
)} )}
</div> </div>
{panelOptions.descriptionField && panelOptions.descriptionAtNewLine && ( {panelOptions.descriptionField && panelOptions.descriptionAtNewLine && (
<div className="alert-rule-item__text zbx-description--newline"> <div className="alert-rule-item__text zbx-description--newline">
{panelOptions.allowDangerousHTML ? (
<span <span
className="alert-rule-item__info zbx-description" className="alert-rule-item__info zbx-description"
dangerouslySetInnerHTML={{ __html: problem.comments }} dangerouslySetInnerHTML={{ __html: problem.comments }}
/> />
) : (
<span className="alert-rule-item__info zbx-description">{problem.comments}</span>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -25,6 +25,7 @@ interface Props extends RTRow<ProblemDTO> {
timeRange: TimeRange; timeRange: TimeRange;
showTimeline?: boolean; showTimeline?: boolean;
panelId?: number; panelId?: number;
allowDangerousHTML?: boolean;
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>; getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>; getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>; getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>;
@@ -39,6 +40,7 @@ export const ProblemDetails = ({
timeRange, timeRange,
showTimeline, showTimeline,
panelId, panelId,
allowDangerousHTML,
getProblemAlerts, getProblemAlerts,
getProblemEvents, getProblemEvents,
getScripts, getScripts,
@@ -100,13 +102,19 @@ export const ProblemDetails = ({
const age = moment.unix(problem.timestamp).fromNow(true); const age = moment.unix(problem.timestamp).fromNow(true);
const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0; const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0;
const problemSeverity = Number(problem.severity); const problemSeverity = Number(problem.severity);
const styles = useStyles2(getStyles);
let dsName: string = original.datasource as string; let dsName: string = original.datasource as string;
if ((original.datasource as DataSourceRef)?.uid) { if ((original.datasource as DataSourceRef)?.uid) {
const dsInstance = getDataSourceSrv().getInstanceSettings((original.datasource as DataSourceRef).uid); const dsInstance = getDataSourceSrv().getInstanceSettings((original.datasource as DataSourceRef).uid);
dsName = dsInstance.name; dsName = dsInstance.name;
} }
const styles = useStyles2(getStyles);
const problemDescriptionEl = allowDangerousHTML ? (
<span dangerouslySetInnerHTML={{ __html: problem.comments }} />
) : (
<span>{problem.comments}</span>
);
return ( return (
<div className={`problem-details-container ${displayClass}`}> <div className={`problem-details-container ${displayClass}`}>
@@ -162,22 +170,21 @@ export const ProblemDetails = ({
</div> </div>
{problem.comments && ( {problem.comments && (
<div className="problem-description-row"> <div className="problem-description-row">
<div className="problem-description"> <div className={styles.problemDescription}>
<Tooltip placement="right" content={<span dangerouslySetInnerHTML={{ __html: problem.comments }} />}> <Tooltip placement="right" content={problemDescriptionEl}>
<span className="description-label">Description:&nbsp;</span> <span className="description-label">Description:&nbsp;</span>
</Tooltip> </Tooltip>
{/* <span>{problem.comments}</span> */} {problemDescriptionEl}
<span dangerouslySetInnerHTML={{ __html: problem.comments }} />
</div> </div>
</div> </div>
)} )}
{problem.items && ( {problem.items && (
<div className="problem-description-row"> <div>
<ProblemExpression problem={problem} /> <ProblemExpression problem={problem} />
</div> </div>
)} )}
{problem.hosts && ( {problem.hosts && (
<div className="problem-description-row"> <div>
<ProblemHostsDescription hosts={problem.hosts} /> <ProblemHostsDescription hosts={problem.hosts} />
</div> </div>
)} )}
@@ -239,9 +246,27 @@ const getStyles = (theme: GrafanaTheme2) => ({
position: relative; position: relative;
flex: 10 1 auto; flex: 10 1 auto;
// padding: 0.5rem 1rem 0.5rem 1.2rem; // padding: 0.5rem 1rem 0.5rem 1.2rem;
padding: ${theme.spacing(0.5)} ${theme.spacing(1)} ${theme.spacing(0.5)} ${theme.spacing(1.2)} padding: ${theme.spacing(0.5)} ${theme.spacing(1)} ${theme.spacing(0.5)} ${theme.spacing(1.2)};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
// white-space: pre-line; // white-space: pre-line;
font-size: ${theme.typography.bodySmall.fontSize};
`,
problemDescription: css`
position: relative;
max-height: 6rem;
min-height: 3rem;
overflow: hidden;
&:after {
content: '';
text-align: right;
position: inherit;
bottom: 0;
right: 0;
width: 70%;
height: 1.5rem;
background: linear-gradient(to right, rgba(0, 0, 0, 0), ${theme.colors.background.canvas} 50%);
}
`, `,
}); });

View File

@@ -245,6 +245,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
rootWidth={this.rootWidth} rootWidth={this.rootWidth}
timeRange={this.props.timeRange} timeRange={this.props.timeRange}
showTimeline={panelOptions.problemTimeline} showTimeline={panelOptions.problemTimeline}
allowDangerousHTML={panelOptions.allowDangerousHTML}
panelId={this.props.panelId} panelId={this.props.panelId}
getProblemEvents={this.props.getProblemEvents} getProblemEvents={this.props.getProblemEvents}
getProblemAlerts={this.props.getProblemAlerts} getProblemAlerts={this.props.getProblemAlerts}

View File

@@ -90,6 +90,12 @@ export const plugin = new PanelPlugin<ProblemsPanelOptions, {}>(ProblemsPanel)
}, },
showIf: (options) => options.customLastChangeFormat, showIf: (options) => options.customLastChangeFormat,
}) })
.addBooleanSwitch({
path: 'allowDangerousHTML',
name: 'Allow HTML',
description: `Format problem description and other data as HTML. Use with caution, it's potential cross-site scripting (XSS) vulnerability.`,
defaultValue: defaultPanelOptions.allowDangerousHTML,
})
.addCustomEditor({ .addCustomEditor({
id: 'resetColumns', id: 'resetColumns',
path: 'resizedColumns', path: 'resizedColumns',

View File

@@ -34,6 +34,7 @@ export interface ProblemsPanelOptions {
customLastChangeFormat?: boolean; customLastChangeFormat?: boolean;
lastChangeFormat?: string; lastChangeFormat?: string;
resizedColumns?: RTResized; resizedColumns?: RTResized;
allowDangerousHTML: boolean;
// Triggers severity and colors // Triggers severity and colors
triggerSeverity: TriggerSeverity[]; triggerSeverity: TriggerSeverity[];
okEventColor: TriggerColor; okEventColor: TriggerColor;
@@ -81,6 +82,7 @@ export const defaultPanelOptions: Partial<ProblemsPanelOptions> = {
customLastChangeFormat: false, customLastChangeFormat: false,
lastChangeFormat: '', lastChangeFormat: '',
resizedColumns: [], resizedColumns: [],
allowDangerousHTML: false,
// Triggers severity and colors // Triggers severity and colors
triggerSeverity: getDefaultSeverity(), triggerSeverity: getDefaultSeverity(),
okEventColor: 'rgb(56, 189, 113)', okEventColor: 'rgb(56, 189, 113)',