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:
@@ -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>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 && (
|
||||||
<span className="zbx-description" dangerouslySetInnerHTML={{ __html: problem.comments }} />
|
<>
|
||||||
|
{panelOptions.allowDangerousHTML ? (
|
||||||
|
<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">
|
||||||
<span
|
{panelOptions.allowDangerousHTML ? (
|
||||||
className="alert-rule-item__info zbx-description"
|
<span
|
||||||
dangerouslySetInnerHTML={{ __html: problem.comments }}
|
className="alert-rule-item__info zbx-description"
|
||||||
/>
|
dangerouslySetInnerHTML={{ __html: problem.comments }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="alert-rule-item__info zbx-description">{problem.comments}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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: </span>
|
<span className="description-label">Description: </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%);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
Reference in New Issue
Block a user